PasswordDeriveBytes from C# to Java (1) 에서 계속...
2016. 04.18 업데이트) 아래 소스를 정리한 GitHub 저장소를 만들었습니다.
C#에 있는 PasswordDeriveBytes 클래스를 Java 로 구현해야 했던 나는 해당 클래스에 버그가 있다는 것까지 확인했다.
하지만, 이 버그에 대해서 어느 곳에서도 설명하지 않아서 결국 내가 실험을 통해 찾아야 했다.
그리고 한참 실험을 해서 알아낸 내용은 처음 호출한 GetBytes 메소드에 전달한 값에 따라 두 번째 호출한 GetBytes 메소드의 동작이 달라진다는 것이다. 그러므로 다음과 같이 첫 번째 GetBytes의 전달인자를 (A)라고 놓았을 때 두 번째 GetBytes의 반환값을 알아낼 수 있다.
첫 번째 GetBytes의 전달인자 (A) |
두 번째 GetBytes의 결과값 |
1 ~ 9 |
Runtime Error |
10 ~ 19 |
전체 key stream에서 (20 - A) 만큼 건너뛰고 (20 - A) 만큼을 읽어서 결과값의 앞부분으로 이용 |
21 ~ 39 |
전체 key stream에서 (40 - A) 만큼 건너뛰고 (40 - A) 만큼을 읽어서 결과값의 앞부분으로 이용 |
40 ~ | 전체 key stream에서 첫 번째 GetBytes를 읽은 뒷부분부터 그대로 읽어서 돌려줌 |
그리고 이를 코드로 표현하면 다음과 같다.
// 첫 번째 출력 길이 저장
if (state == 1) {
if (cb > 20) {
skip = 40 - result.length;
} else {
skip = 20 - result.length;
}
firstBaseOutput = new byte[result.length];
System.arraycopy(result, 0, firstBaseOutput, 0, result.length);
state = 2;
}
// 두 번째 출력 시 변환 처리
else if (skip > 0) {
byte[] secondBaseOutput = new byte[(firstBaseOutput.length + result.length)];
System.arraycopy(firstBaseOutput, 0, secondBaseOutput, 0, firstBaseOutput.length);
System.arraycopy(result, 0, secondBaseOutput, firstBaseOutput.length, result.length);
System.arraycopy(secondBaseOutput, skip, result, 0, skip);
skip = 0;
}
그리고 앞 포스팅의 기본적인 PasswordDeriveBytes 구현에 위의 내용을 합쳐서 구현한 결과는 다음과 같다.
import java.io.UnsupportedEncodingException;
import java.security.DigestException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class PasswordDeriveBytes {
private String HashNameValue;
private byte[] SaltValue;
private int IterationsValue;
private MessageDigest hash;
private int state;
private byte[] password;
private byte[] initial;
private byte[] output;
private byte[] firstBaseOutput;
private int position;
private int hashnumber;
private int skip;
public PasswordDeriveBytes(String strPassword, byte[] rgbSalt) {
Prepare(strPassword, rgbSalt, "SHA-1", 100);
}
public PasswordDeriveBytes(String strPassword, byte[] rgbSalt, String strHashName, int iterations) {
Prepare(strPassword, rgbSalt, strHashName, iterations);
}
public PasswordDeriveBytes(byte[] password, byte[] salt) {
Prepare(password, salt, "SHA-1", 100);
}
public PasswordDeriveBytes(byte[] password, byte[] salt, String hashName, int iterations) {
Prepare(password, salt, hashName, iterations);
}
private void Prepare(String strPassword, byte[] rgbSalt, String strHashName, int iterations) {
if (strPassword == null)
throw new NullPointerException("strPassword");
byte[] pwd = null;
try {
pwd = strPassword.getBytes("ASCII");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Prepare(pwd, rgbSalt, strHashName, iterations);
}
private void Prepare(byte[] password, byte[] rgbSalt, String strHashName, int iterations) {
if (password == null)
throw new NullPointerException("password");
this.password = password;
state = 0;
setSalt(rgbSalt);
setHashName(strHashName);
setIterationCount(iterations);
initial = new byte[hash.getDigestLength()];
}
public byte[] getSalt() {
if (SaltValue == null)
return null;
return SaltValue;
}
public void setSalt(byte[] salt) {
if (state != 0) {
throw new SecurityException("Can't change this property at this stage");
}
if (salt != null)
SaltValue = salt;
else
SaltValue = null;
}
public String getHashName() {
return HashNameValue;
}
public void setHashName(String hashName) {
if (hashName == null)
throw new NullPointerException("HashName");
if (state != 0) {
throw new SecurityException("Can't change this property at this stage");
}
HashNameValue = hashName;
try {
hash = MessageDigest.getInstance(hashName);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
public int getIterationCount() {
return IterationsValue;
}
public void setIterationCount(int iterationCount) {
if (iterationCount < 1)
throw new NullPointerException("HashName");
if (state != 0) {
throw new SecurityException("Can't change this property at this stage");
}
IterationsValue = iterationCount;
}
public byte[] GetBytes(int cb) throws DigestException {
if (cb < 1) {
throw new IndexOutOfBoundsException("cb");
}
if (state == 0) {
Reset();
state = 1;
}
byte[] result = new byte[cb];
int cpos = 0;
// the initial hash (in reset) + at least one iteration
int iter = Math.max(1, IterationsValue - 1);
// start with the PKCS5 key
if (output == null) {
// calculate the PKCS5 key
output = initial;
// generate new key material
for (int i = 0; i < iter - 1; i++) {
output = hash.digest(output);
}
}
while (cpos < cb) {
byte[] output2 = null;
if (hashnumber == 0) {
// last iteration on output
output2 = hash.digest(output);
// System.out.println("0: initial: " + new String(org.bouncycastle.util.encoders.Hex.encode(output2)).toUpperCase());
} else if (hashnumber < 1000) {
byte[] n = Integer.toString(hashnumber).getBytes();
output2 = new byte[output.length + n.length];
for (int j = 0; j < n.length; j++) {
output2[j] = n[j];
}
System.arraycopy(output, 0, output2, n.length, output.length);
// don't update output
output2 = hash.digest(output2);
// System.out.println(hashnumber + " output2: " + new String(org.bouncycastle.util.encoders.Hex.encode(output2)).toUpperCase());
} else {
throw new SecurityException("too long");
}
int rem = output2.length - position;
int l = Math.min(cb - cpos, rem);
System.arraycopy(output2, position, result, cpos, l);
// System.out.println("result:\t\t" + new String(org.bouncycastle.util.encoders.Hex.encode(result)).toUpperCase());
cpos += l;
position += l;
while (position >= output2.length) {
position -= output2.length;
hashnumber++;
}
}
// 첫 번째 출력 길이 저장
if (state == 1) {
if (cb > 20) {
skip = 40 - result.length;
} else {
skip = 20 - result.length;
}
firstBaseOutput = new byte[result.length];
System.arraycopy(result, 0, firstBaseOutput, 0, result.length);
state = 2;
}
// 두 번째 출력 시 변환 처리
else if (skip > 0) {
byte[] secondBaseOutput = new byte[(firstBaseOutput.length + result.length)];
System.arraycopy(firstBaseOutput, 0, secondBaseOutput, 0, firstBaseOutput.length);
System.arraycopy(result, 0, secondBaseOutput, firstBaseOutput.length, result.length);
// System.out.println("skip:\t\t" + skip);
// System.out.println("secondBaseOutput:\t" + new String(org.bouncycastle.util.encoders.Hex.encode(secondBaseOutput)).toUpperCase());
System.arraycopy(secondBaseOutput, skip, result, 0, skip);
skip = 0;
}
return result;
}
public void Reset() throws DigestException {
state = 0;
position = 0;
hashnumber = 0;
skip = 0;
if (SaltValue != null) {
hash.update(password, 0, password.length);
hash.update(SaltValue, 0, SaltValue.length);
hash.digest(initial, 0, initial.length);
} else {
initial = hash.digest(password);
}
}
}