Byte hex 출력하기 / (bytes[i] & 0xff) + 0x100 가 뭐지?

목표

Byte를 hex 문자로 출력한다.

암호화(Hashing) 코드를 보다가

private static String get_SecurePassword(String passwordToHash, byte[] salt){
  String generatedPassword = null;
  try {
   MessageDigest md = MessageDigest.getInstance("SHA-256");
   md.update(salt);
   byte[] bytes = md.digest(passwordToHash.getBytes());
   StringBuilder sb = new StringBuilder();
   for(int i=0; i< bytes.length ;i++)
   {
    sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
   }
   generatedPassword = sb.toString();
  } 
  catch (NoSuchAlgorithmException e) {
   e.printStackTrace();
  }
  return generatedPassword;
 }

 

문득 아래 코드가 뭐지?

    sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));

라는 생각을 했다. 그래서 알아보자.

 

출력하고자 하는 문자 범위는?

0x00에서 0xff 까지의 8bit로 이루어진 값을 필요로 한다.

이것은 마이너스 범위의 여부는 관심사항이 아니다.

 

Byte의 범위는?

Byte코드는 127 ~ -128의 범위를 갖는다.

총 8bit의 메모리를 갖기 때문에 

$ 2^7 $ ~ $ 2^7-1 $ 의 범위이다. 참고로 양수쪽의 -1은 0을 포함해야 하기 때문이다.

 

bytes의 갯수

SHA-256은 256bit의 hashing값을 생성한다.

        MessageDigest md = MessageDigest.getInstance("SHA-256");

        byte[] bytes = md.digest("abc".getBytes());

        System.out.println(bytes.length * 8);
        
        출력 : 256

참고로 SHA-1은 160bit, MD5는 126bit이다.

        MessageDigest md = MessageDigest.getInstance("SHA-1");

        byte[] bytes = md.digest("abc".getBytes());

        System.out.println(bytes.length * 8);
        
        출력 : 160
        MessageDigest md = MessageDigest.getInstance("MD5");

        byte[] bytes = md.digest("abc".getBytes());

        System.out.println(bytes.length * 8);
        
        출력:128

바이트를 기준으로 함으로 SHA-256은 32Bytes이다.

   for(int i=0; i< bytes.length ;i++)
   {
    sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
   }

총 32회를 순회하면서 한 바이트에 들어있는 8bit값을 16진수로 표현하면 된다.

 

(bytes[i] & 0xff)

0000 0000 에서 부터 1111 1111의 값을 표현해야 하는데,

Integer.toString 처리 시 4byte의 integer type으로 최종 casting되게 된다.

public static String toString(int i, int radix) {

위의 코드를 보면 input 파라메터가 int로 캐스팅 되는 것을 볼 수 있다.

이것은 byte[i]가 음수일때 문제가 발생한다.

양수를 음수화 할때는 다음과 같은 순서를 거치게 되기 때문이다.

마이너스 1을 구하는 방법

  • 1의 비트 : 0000 0000 0001
  • 보수 : 1111 1111 1110
  • +1 : 1111 1111 1111
  • -1의 비트 : 1111 1111 1111

위의 방법을 따르면 최종적으로 음수는 좌측이 모두 1로 채워지게 된다.

4byte integer의 -1은

  • 1111 1111 1111 1111 1111 1111 1111 1111

이렇게 된다.

우리는 음수가 불필요 함으로 8bit이후의 모든 값은 양수화 시켜줘야 한다.

그러기 위해서 "& 0xff" 가 필요하다.

  • 00000000 0000 0000 0000 0000 1111 1111

-1의 byte값이 최종적으로 위와 같이 변화 하게 된다.

이 상태에서 toBinaryString을 처리하면 "ff" 라는 값이 나온다.

 

그럼 0은?

그런데 위와 같이 처리하면 한가지 문제점이 발생한다.

한자리 숫자 1의 경우는 01 이런식으로 2자리 출력을 해야 하는데

값이 1임으로 1로만 출력이 된다.

System.out.println(Integer.toBinaryString(1 & 0xff));

출력 : 1

우리가 필요 한것은 2자리 string값이다.

즉 "01"이 필요하다.

어떠한 경우에도 2자리 이상 출력이 가능하게 할 수 있도록 "0x100"을 더해준다.

1이라는 값을 기준으로 0x100을 더해주면 9비트에 1을 더해주는 것과 같다.

  • 기존 : 00000000 0000 0000 0000 0000 0000 0001
  • 0x100 더함 : 00000000 0000 0000 0000 0001 0000 0001

최종적으로는 

System.out.println(Integer.toString((1 & 0xff) + 0x100, 16));

출력 : 101

이 나오게 된다.

이제 여기서의 출력은 무조건 3자리의 String이 나오게 됨으로써, 우측 2자리 값만 취하면 목표한 결과가 나온다.

System.out.println(Integer.toString((1 & 0xff) + 0x100, 16).substring(1));

출력 : 01

 

결론

사실 

 

        String value = String.format("%02x", 1);
        System.out.println(value);
        
        출력 : 01

위와 같이 String.format을 이용하는 방법이 가장 쉬운데, performance 측면으로 String format이 더 느림으로 위와 같이 bit manipulation을 처리 한다.

728x90
반응형