C#에 있는 System.Security.Cryptography.PasswordDeriveBytes 클래스를 자바에서 구현해야 하는 일이 생겼다. 고생은 그렇게 시작되었다.
MSDN에 있는 설명을 보면
PBKDF1 알고리즘의 확장을 사용하여 암호에서 키를 파생시킵니다.
라고 되어 있다.
그럼 PBKDF1이 뭔지부터 알아보자. PBKDF1은 여기의 설명을 직역해 보면 암호화에서 사용할 키를 만들기 위해서 쓰는 해시 함수 적용?이라고 한다. 무슨 소리냐면 적당한 문자열 비밀번호를 넣으면 해시 함수를 돌려서 얻은 값을 돌려주고 그 돌려받은 값으로 암호화할 때 비밀키로 쓰라는 거다.
위에 살짝 걸어둔 링크에서 시키는대로 구현을 해도 동작은 할 테지만 귀찮아서 누가 자바로 만들어 놓은게 없나 찾아봤다. 상용 라이브러리에 포함된 것 말고는 bouncy-castle 포럼의 한 쓰레드에서 논의가 되었던 것만 간신히 하나 찾아냈다. 그 쓰레드의 코드는 mono 프로젝트에서 만든 걸 보고 만든 거라는 부분을 읽고 쓰레드의 코드를 밑바탕으로 하고 mono 프로젝트에 있는 코드를 참조해서 구현을 했다.
2016-10-16 수정) mono 프로젝트가 4.2 버전부터 직접 구현한 것 대신에 MS에서 준 것을 이용한다고 한다. 그래서 이제 mono 프로젝트 안에 PasswordDeriveBytes 코드가 없다.
이 결과물이 아래에 있는 코드다.
package com.example.test;
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 int position;
private int hashnumber;
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);
} 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);
} 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);
cpos += l;
position += l;
while (position >= output2.length) {
position -= output2.length;
hashnumber++;
}
}
return result;
}
public void Reset() throws DigestException {
state = 0;
position = 0;
hashnumber = 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);
}
}
}
그런데 위의 코드는 제대로 동작하지 않는다!! 분명 정상적으로 구현을 했지만 특정 상황에서는 GetBytes 함수의 출력이 C#과 다르다. 이 문제점은 bouncy-castle 포럼 쓰레드의 맨 마지막 댓글에서도 제기하고 있다.
mono 프로젝트는 이런 문제를 알고 있지 않을까 싶어 검색을 해본 결과 mono 프로젝트에서는 MS의 버그이니 안고치기로 했다고 한다. (https://bugzilla.novell.com/show_bug.cgi?id=316364) 하지만 나는 버그까지 구현해서 동일하게 동작해야만 했다. ㅠㅠ
2편에서 이이서 계속...