Java

PasswordDeriveBytes of C# to Java (2)

gilchris 2014. 2. 8. 14:53

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);

       }

   }

}