회사에서 로그인 처리와 관련된 코드를 잠시 보고 있었는데 아래와 같은 코드가 있었다.
String pw = "user01";
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedHash = digest.digest(pw.getBytes(StandardCharsets.UTF_8));
// encodedHash 는 32 바이트(고정적인 바이트 수를 뽑아낸다) ==> 헥사로는 64자리 문자열이 나온다.
StringBuilder builder = new StringBuilder(digest.length * 2);
for (int i = 0; i < digest.length; i++) {
String hex
= Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1).toLowerCase();
builder.append(hex);
}
... 생략 ...
위의 코드는 회원가입 데이터를 받아오면 해당 비밀 번호를 Sha256 암호화를 거치고,
생성된 byte[]
로 Hex String 을 생성하는 것이다.
여기서 궁금한 건 바로 이 코드다
Integer.toString((digest[i] & 0xff) + 0x100, 16).substring(1).toLowerCase();
이게 HexString을 만드는 코드인데... 처음 봤을 때는 도통 이해가 안됐다.
차근 차근 왜 그런지 아래 순서대로 알아보자.
digest[i] & 0xff
+ 0x100
.substring(1)
digest[i] & 0xff
??digest[i]
는 byte 타입이기 때문에 일단 1byte, 즉 8bit의 정보만 담을 수 있다.
그런데 다른 언어와는 다르게 Java에는 unsigned 라는 개념을 안 쓰다보니,
byte 로 표현 가능한 숫자는 -128~127
만 가능하다.
간단 테스트
public static void main(String[] args) {
// OVERFLOW 이전
int integerNumber = 127;
byte byteNumber = (byte) integerNumber;
System.out.println("OVERFLOW 이전");
System.out.println("integerNumber: " + integerNumber);
System.out.println("byteNumber: " + byteNumber);
// OVERFLOW 이후
integerNumber = 128;
byteNumber = (byte) integerNumber;
System.out.println("\nOVERFLOW 이전");
System.out.println("integerNumber: " + integerNumber);
System.out.println("byteNumber: " + byteNumber);
}
출력결과:
OVERFLOW 이전
integerNumber: 127
byteNumber: 127
OVERFLOW 이전
integerNumber: 128
byteNumber: -128
그래서 우리가 8bit로 0~255 까지의 값을 나타내고 싶다면
어쩔 수 없이 1byte가 아닌 4byte의 공간을 갖는 integer 로 형변환이 필요하다.
이를 위해서 0xff(integer 형)에 &
연산을 통하여 암묵적 형변환이 일어나게 한다.
참고로 리터럴로 작성한 정수형 숫자는 기본적으로 integer 형을 갖는다.
+ 0x100
??앞서 1번을 하면 일단 overflow가 일어나는 것을 방지할 수 있다.
그런데 overflow 가 방지했다고 치자. 그다음에 0~255의 값을 받을 수 있는데,
문제는 우리가 8bit 중에서 앞의 4bit는 안 쓰고, 뒤의 4bit만 쓴 상태로 HexString을
만들면 어떻게 될까?
십진수 | binary | hex |
---|---|---|
255 | 1111 1111 | 0xff |
15 | 0000 1111 | 0x0f |
아마 이쁘게 0x0f
딱 2자리로 떨어지길 바라겠지만, 어림없다.
Integer.toString(15, 16) 만 달랑 하면 0xff
가 아니라 0xf
가 나온다.
이를 방지하기 위한 게 +0x100
이다.
앞서 말한 십진수 15에 0x100
을 더하면 아래와 같이 된다.
0000 0000 1111
+0001 0000 0000
----------------
0001 0000 1111 ===> hex String: 0x10f 우리가 원했던 앞의 4bit를 위한 `0`이 붙었다.
하지만 그 과정에서 맨앞에 `1`이 생겼다.
이렇듯 byte 가 앞의 4bit를 쓰든 안 쓰든 항상 0
이 생성되도록 강제하는 것이다.
하지만 이 과정에서 3자리의 hex String이 생성되고 무조건 맨앞에 1이 붙는다.
.substring(1)
??그렇다면 +0x100
을 통해서 필요없이 붙은 맨 앞의 hex String 인 1
을 제거할 필요가 있다.
그렇기 때문에 Integer.toString((digest[i] & 0xff) + 0x100, 16))
연산 이후에
.subString(1)
을 해주는 것이다.
테스트 코드
public static void main(String[] args) throws NoSuchAlgorithmException {
byte b = (byte) 15;
// hex String은 잘 나오지만 1자리면 앞에 0 이 없다.
System.out.println(Integer.toHexString(b & 0xff));
// 방법1
// + 0x100 함으로써 앞에 2자리의 hex string 중 첫번째 자리에 문자열이 있는 것을 보장한다.
System.out.println(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
// 방법2: toHexString의 복잡함을 단순화. String.format 사용하기
// 하지만 format은 속도가 느리다고 한다. 자주 호출되는 것이 아니면 괜찮을 듯하다.
System.out.println(String.format("%02x", b));
}
별거 아닌데 참 복잡하다.
사실 이외에도 byte 를 hexString으로 변경하는 방법은 다양하다.
지금 이 글을 읽고 더 간단한 방법을 원한다면 좀 더 구글링해보자 😁
참고: https://www.baeldung.com/java-byte-arrays-hex-strings
2024-11-15 에 작성된 추가 내용입니다.
Java 17 부터는 더 간단하게 이를 처리할 수 있게 되었습니다.
import java.util.HexFormat;
public static void main(String[] args) {
String result = HexFormat.of().formatHex(new byte[]{(byte)255, (byte)255});
System.out.println("result = " + result);
// 출력: result = ffff
}
궁금한 부분이 정확하게 여기에 있네요! 잘 보고 가요!