개발을 하다 보면 사용자 인증 기능을 구현할 때 bcrypt 라이브러리를 관습적으로 사용하곤 합니다. 비밀번호를 해싱해서 DB에 저장하고, 로그인할 때는 bcrypt.compare() 함수로 검증하죠.
그런데 문득 이런 궁금증이 들지 않으셨나요? "해싱은 단방향이라 복호화가 불가능한데, 어떻게 입력한 평문과 저장된 해시값을 비교해서 맞는지 알 수 있는 거지?"
오늘은 이 당연하면서도 신기한 bcrypt의 동작 원리를 테크니컬하게 뜯어보겠습니다.
1. 암호화(Encryption) vs 해싱(Hashing)
먼저 두 개념의 차이를 명확히 해야 합니다.
- 암호화(Encryption): 양방향 프로세스입니다. 키(Key)가 있다면 암호문을 다시 평문으로 되돌리는 복호화 가 가능합니다. (예: AES, RSA)
- 해싱(Hashing): 단방향 프로세스입니다. 어떤 데이터를 고정된 길이의 문자열로 변환하며, 이를 다시 원본으로 되돌리는 것은 이론적으로 불가능합니다. (예: SHA-256, bcrypt)
비밀번호는 관리자조차 원본을 알면 안 되기 때문에 해싱 을 사용하는 것이 보안상의 정석입니다.
2. bcrypt 해시 문자열의 구조
bcrypt로 생성된 해시값을 보면 단순히 무작위 문자열처럼 보이지만, 사실 그 안에 검증에 필요한 모든 정보가 포함되어 있습니다.
예를 들어, $2b$10$FQl9h4Nubp9ENv9sPfO0/.kDjBkKpFfOntPPA0wG5UV/UGEesCDiK 라는 해시값은 다음과 같이 구성됩니다.
- Prefix ($2b$): 사용된 알고리즘의 버전을 의미합니다.
- Cost Factor (10): 해싱을 반복할 횟수($2^10 = 1024$번)를 나타냅니다. 이 값이 높을수록 연산 시간이 길어져 브루트 포스 공격을 방어합니다.
- Salt (22자): 동일한 비밀번호라도 매번 다른 해시값이 나오도록 추가하는 무작위 값입니다.
- Hash (31자): 실제 비밀번호와 솔트가 결합되어 계산된 최종 결과물입니다.
핵심은 "솔트(Salt)를 따로 DB에 저장할 필요가 없다"는 점입니다. 해시 문자열 안에 이미 포함되어 있기 때문이죠.
3. bcrypt.compare()의 마법: 복호화 없는 비교
자, 이제 본론입니다. 어떻게 복호화 없이 비밀번호를 맞출까요? 원리는 의외로 간단합니다. "저장된 해시값에서 정보를 추출해, 입력받은 평문을 똑같은 방식으로 다시 해싱해보는 것" 입니다.
동작 순서
- 정보 추출:
compare함수는 DB에 저장된 해시값에서 알고리즘 버전, Cost Factor, 솔트(Salt) 를 먼저 읽어들입니다. - 재해싱: 사용자가 로그인을 위해 입력한 평문 비밀번호 에 방금 추출한 솔트 를 붙이고, 동일한 반복 횟수 로 해싱 연산을 수행합니다.
- 대조: "새로 만든 해시값"과 "DB에 저장되어 있던 해시값"이 완벽히 일치하는지 확인합니다.
즉, 원본을 꺼내는 것이 아니라 똑같은 결과가 나오는지 재현해보는 방식 인 셈입니다.
4. 코드로 보는 내부 동작 (C++ 수준)
Node.js의 node.bcrypt.js 내부를 살펴보면 실제 비교 로직은 C++ 애드온으로 구현되어 있습니다.
void Execute() {
char bcrypted[_PASSWORD_LEN];
// 1. 저장된 해시에서 솔트와 설정을 검증 및 추출
if (ValidateSalt(encrypted.c_str())) {
// 2. 입력된 평문(input)을 저장된 해시(encrypted) 내의 솔트로 다시 해싱
bcrypt(input.c_str(), input.length(), encrypted.c_str(), bcrypted);
// 3. 생성된 값과 기존 값을 비교
result = CompareStrings(bcrypted, encrypted.c_str());
}
}이처럼 bcrypt.compare()는 비동기 워커를 통해 메인 스레드를 방해하지 않으면서도, CPU 집약적인 해싱 연산을 수행하여 보안을 유지합니다.
마무리하며
결국 bcrypt가 복호화 없이 비밀번호를 검증할 수 있는 이유는 해시값 내부에 '조리법(솔트와 설정)'을 숨겨두었기 때문 입니다.
우리가 무심코 사용하는 라이브러리도 내부를 뜯어보면 보안을 위해 설계된 치밀한 메커니즘이 숨어 있습니다. 단방향 해시의 특성을 이해한다면, 왜 비밀번호를 암호화가 아닌 해싱으로 처리해야 하는지 더 명확하게 이해할 수 있을 것입니다.
