개요
최근 진행한 사이드 프로젝트에서 Spring Security를 사용했습니다. 처음에는 학습 차원에서 인터넷과 책에서 찾은 기본 구현 예제를 따라 해봤는데, 생각보다 개발자가 직접 구현해주어야 하는 부분이 적고 간단해서 기본적인 구현 정도는 쉽게 할 수 있었습니다.
자체 로그인 기능을 구현할 때 비밀번호 암호화 알고리즘으로 BCrypt를 사용했습니다. Spring Security 프레임워크에 기본적으로 내장되어 있는 방식이라 추가 의존성 설치를 할 필요가 없다는 장점이 있고, 비밀번호 암호화에 사용하기 적절한 단방향 암호화 알고리즘에 안정성까지 보장받은 알고리즘이라고 합니다.
그런데, 예제를 따라해보는 동안 한 가지 의문점이 들었습니다.
"내가 직접 Salt 값을 세팅해놓지 않았는데, 왜 안전한 거지? 레인보우 테이블 공격으로부터 어떻게 안전한 걸까?"
제가 지금까지 경험해 봤던 암호화를 구현하는 방식은 서버에서 자체적으로 Salt값을 사용하는 방식이었습니다. 그래서 BCrypt도 당연히 Salt 값을 설정해주어야 할 것이라고 예상했고, Salt값을 설정하는 단계가 나올 것이라는 기대를 하며 예제를 따라 했습니다. 그런데 구현이 끝날 때까지 Salt값을 자체적으로 설정해 주는 단계가 없었습니다. Salt값이 없으면, 전형적인 해킹 기법 중 하나인 레인보우 테이블 공격으로부터 안전할 수가 없을 텐데, BCrypt는 이를 어떻게 해결한 건지와 BCrypt가 안전한 이유가 무엇인지 궁금해졌습니다.
BCrypt 해시값 생성 및 검증해 보기
https://bcrypt-generator.com/ 는 BCrypt 알고리즘으로 암호화된 해시값을 쉽게 만들어보고 검증해 볼 수 있는 사이트입니다. 아래 이미지와 같이 원하는 입력값에 대한 해시값을 만들어볼 수 있고, Salt값을 직접 입력하는 메뉴는 없습니다. 변경 가능한 옵션은 오직 Rounds(Cost Factor) 값뿐입니다.

주목할 점은, 동일한 입력값과 Cost Factor로 여러 번 해싱했을 때 매번 다른 값이 생성된다는 것입니다. 입력 가능한 변수 요소가 변한 게 하나도 없는데, 왜 해시값이 달라지는 걸까요? 얼핏 보면 그저 무작위로 생성된 값일 뿐이라고 오해할 수도 있을 것 같습니다. 아무튼, 생성된 해시값은 정상적으로 검증을 수행할 수 있는 걸까요? 위 사이트에서 검증 절차도 수행해 보겠습니다.


두 개의 서로 다른 해시값을 각각 동일한 Original Text로 검증하였는데, 두 번 다 검증에 성공하는 것을 확인할 수 있습니다. 그리고, 검증에 성공했던 해시값에 대해 Original Text를 다른 값으로 변경하면, 정상적으로 검증에 실패하는 것도 확인할 수 있습니다.
도대체 원리가 뭘까요? 변경한 입력값이 없는데 매번 다른 해시값이 튀어나오는 것도 신기하고, 그 값을 정상적으로 검증할 수 있다는 것도 신기합니다.
BCrypt 암호화 동작 방식
BCrypt 암호화의 동작 방식을 쉽게 잘 설명해 놓은 블로그 글을 참고했습니다.
(참고한 블로그: https://jhkimmm.tistory.com/24)
[Spring Security] Bcrypt의 salt는 어디에 저장될까?
Spring Security의 PasswordEncoder를 공부하며 든 궁금증을 정리합니다. 암호화 해시함수는 단방향 알고리즘이기 때문에 해시값으로 저장된 비밀번호를 역으로 계산해서 원래의 암호를 알아내는 것은
jhkimmm.tistory.com
암호화 방식의 원리는 해시값 자체에 있었습니다. 의문이었던 Salt 값이 해시값에 포함이 되어있는 것이었습니다.
BCrypt 알고리즘은 암호화를 할 때마다 Salt값을 랜덤으로 생성합니다. 그리고 그 Salt값은 해시값 문자열의 일부로 함께 저장시킵니다. 그래서 동일한 입력 값을 해싱해도, 매번 다른 해시 결과값이 나오게 됩니다. 그리고 해시값을 검증할 때는 해시 문자열에 들어있는 Salt 값을 추출해서 사용하면 그만입니다.
다음은 BCrypt 해시 문자열의 구조입니다.
해시 문자열:
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
- $2a$: 해시 알고리즘 버전
- 10$: 비용 인자(cost factor)
- N9qo8uLOickgx2ZMRZoMye: Salt값 (첫 22자리)
- IjZAgcfl7p92ldGxad68LJZdL17lhWy: 실제 해시 값
안전한 이유?
처음에는 Salt값을 대놓고 해시값에 포함시킨다는 개념 자체가 조금은 거부감이 들었습니다. 서비스를 개발하다 보면 절대 외부에 노출되어서는 안 되는 비밀 값들을 관리해야 할 일이 많이 생깁니다. 클라우드 인스턴스 접속 암호라던가 API Key, JWT 시그니처 등등.. 그리고 제가 지금까지 경험해 왔던 암호화 방식들이 사용하는 Salt값도 마찬가지로 절대 노출되지 않도록 관리해줘야 하는 값이었습니다.
BCrypt 방식은 왜 Salt값을 해시에 포함시켜 노출시키는 방식을 사용하지만, 안전하다고 할 수 있는 걸까요?
위에서 참고했다고 언급한 블로그 글을 조금 더 인용하여 이해해 보겠습니다.
[세상에 완벽한 비밀값은 존재하지 않는다]
서버에 존재하는 데이터를 만약 평생 완벽하게 비밀로 유지할 수 있다면, 애초에 암호화라는 기술이 필요할까요? 모든 데이터를 비밀로 유지할 수 있는 완벽한 기술이 존재한다면, 데이터를 평문으로 저장해도 아무런 문제가 되지 않을 것입니다. 보안 체계 설계의 시작은 '공격자가 우리가 아는 모든 정보를 이미 알고 있다'는 가정하에 출발합니다.
[해시 Salt 방식의 목적은 레인보우 테이블 공격에 대한 방어이다]
해시 Salt 방식은, 해시 함수를 사용하여 변환 가능한 모든 해시 값을 저장시켜 놓은 레인보우 테이블을 이용한 공격으로부터 보호하기 위해 사용합니다. Salt가 노출이 되든 말든, 레인보우 테이블 공격으로부터 안전하기만 하다면 목적을 달성했다고 할 수 있습니다.
하나의 Salt값에 대한 레인보우 테이블을 생성하는 데에 들어가는 리소스는 상당히 엄청납니다.
BCrypt 해시는 60바이트입니다. 60바이트의 해시에 대응하는 레인보우 테이블은 약 1.1 엑사바이트의 공간이 필요합니다. 또한, BCrypt는 해시를 계산하는 로직을 의도적으로 느리게 설계하였습니다. Cost Factor를 10으로 했을 때 평균적으로 해시 계산에 100ms - 200ms 가 소요된다고 하는데, 이때 하나의 레인보우 테이블을 만드는 데에 대략 100일 정도가 소요된다고 합니다. 보통 운영환경에서 권장되는 Cost Factor 설정값은 12부터 시작한다고 하는데, 이에 대한 레인보우 테이블을 만드는 것은 매우x100 오랜 시간이 필요할 것입니다.
(출처: Reddit - Why is it "too expensive" to make a bcrypt lookup table or rainbow table?)
BCrypt는 매번 랜덤 한 Salt값을 사용하여 해시를 생성합니다. 위에서 설명한 대로, 단 하나의 Salt값에 대응하는 레인보우 테이블을 만드는 데에도 어마무시한 자원과 시간이 필요합니다. 그러니 만약 유저 DB가 해커에게 털렸다고 해도, 모든 암호가 BCrypt로 암호화되어있다면 최소한 레인보우 테이블을 이용한 공격은 현실적으로 불가능하다는 결론이 나게 됩니다.
결론
BCrypt의 Salt 생성 방식과 저장 방식으로 인해, DB가 털려도 해커가 해시된 값들을 깨는 건 현실적으로 불가능합니다. 그러니 믿고 써도 좋을 것 같습니다. 그러나, 언제나 주의할 점은 존재합니다.
우선, 운영 환경에서는 당연히 Cost Factor를 가능한 높게 설정해놓아야 합니다. 운영하고 있는 서버의 환경에 맞게 시스템 안정성과 암호화의 안정성 사이의 균형 있는 값을 찾는 것에 신경을 써야 합니다. Cost Factor를 무작정 높게 잡으면 CPU 자원을 그만큼 높게 사용하고, 너무 낮게 잡으면 브루트 포스 공격에 취약해지게 됩니다.
또, 123456 같은 취약한 암호를 쓰지 않는 게 가장 중요한 것 같습니다. 취약한 암호를 사용하여 해킹을 당하는 경우에 대해서는, 아무리 좋은 암호화 알고리즘이라도 방어가 불가능합니다. 취약한 암호를 사용하지 않는 것은 1차적으로는 유저의 책임이라고 볼 수 있지만, 애초에 유저가 그런 취약한 암호를 설정하지 못하도록 정책을 설정하여 유저의 개인 정보를 지킨다는 책임감을 갖고 서비스를 개발하는 것이 개발자의 중요한 태도 중 하나라고 생각합니다.
+ 널리 알려진 해커들의 공격 방법들에 대해, BCrypt가 왜 안전한지에 대해 정리한 표입니다.
| 공격 방법 | 방식 | 어려운 이유 / 제한점 |
| 무차별 대입 공격 (Brute Force Attack) | 가능한 모든 입력값 시도 | BCrypt의 느린 알고리즘, 비용 인자 증가 시 더욱 느려짐, 복잡한 비밀번호의 경우 수백~수천 년 소요 |
| 사전 공격 (Dictionary Attack) | 자주 사용되는 비밀번호 목록 시도 | 일반적인 비밀번호만 찾을 수 있음, 느린 처리 속도로 시간 소요 |
| 레인보우 테이블 공격 (Rainbow Table Attack) | 미리 계산된 해시값 테이블 사용 | 고유한 솔트 사용으로 레인보우 테이블 생성 불가능 |
| 하드웨어 가속 공격 | GPU나 특수 하드웨어 사용 | BCrypt는 메모리 접근이 많아 GPU 가속에 덜 취약, Argon2나 scrypt보다 취약할 수 있음 |
| 구현 취약점 공격 | 암호화 구현 자체의 취약점 이용 | 잘못된 구현, 부적절한 비용 인자 설정, 부실한 난수 생성기 사용 |
++ 최근 양자 컴퓨터 개발에 대한 뉴스들이 꾸준하게 나오면서, 앞으로 머지않은 미래에 양자 컴퓨터가 보급될 것 같다는 생각이 드는 요즘입니다. 위에서 BCrypt가 안전한 이유에 대해 '현실적으로 불가능'이라는 표현을 썼는데, 현실적으로 가능해지는 시대가 머지 않은 미래에 찾아올 수 있다는 생각이 드네요. 저는 일개 코더일 뿐이니.. 양자 컴퓨터를 사용하는 공격에도 안전한 인증 체계가 개발되기를 기다리고, 그때가 온다면 빠르게 잘 가져다가 쓸 수 있도록 공부나 열심히 해야겠다는 생각뿐입니다.
'Web' 카테고리의 다른 글
| Tailwind v4.1 (Vite) - important 옵션 적용하기 (feat. 바뀐 설치방법) (0) | 2025.09.07 |
|---|---|
| 맥북으로 홈 서버 구축하기 (13) | 2025.03.16 |
| 동시성 문제 해결을 위한 메시지 큐 사용기 (0) | 2025.03.12 |