비밀번호 해싱 — bcrypt · scrypt · Argon2
비밀번호 해싱 — bcrypt · scrypt · Argon2
비밀번호를 평문으로 저장하면 안 된다는 것은 익숙하지만, 어떤 해시 함수를 어떻게 쓸지는 시대마다 달랐습니다. MD5 → SHA-1 → bcrypt → scrypt → Argon2 의 흐름은 단순한 유행이 아니라 공격 비용 · 하드웨어 · 기억력 사이의 군비 경쟁의 결과. 이 글은 비밀번호 저장의 의미 · 메모리 강성 함수의 등장 · 라이브러리 · OWASP 권고.
1. 평문 저장의 위험
평문 저장은 DB 가 유출되는 순간 모든 비밀번호가 사용자의 다른 서비스로 옮겨감 (자격 재사용 공격, credential stuffing). 자기 서비스의 권한 모델이 아무리 정교해도 평문 저장은 사용자 다른 서비스의 보안까지 망가뜨림.
2. 단방향 해시의 의미
해시 함수는 입력 → 고정 길이 출력. 같은 입력은 같은 해시. 출력에서 입력을 복원할 수 없는 것이 단방향성.
비밀번호를 해시로만 저장하고 검증 시 같은 해시를 만들어 비교하는 모델이 출발점. 단순 해시는 두 가지 공격에 취약:
- Rainbow table — 자주 쓰이는 비밀번호의 해시를 미리 계산한 표. salt 없으면 한 번의 표로 많은 사용자를 풀 수 있음.
- GPU / ASIC 빠른 계산 — SHA-1 · SHA-256 같은 일반 해시는 초당 수십억 회 → 무차별 공격이 현실적.
이 두 공격을 막기 위해 비밀번호 해시는 두 속성을 더함:
- Salt — 사용자별 무작위 값을 입력에 섞어 같은 비밀번호도 다른 해시.
- Slow / Memory-hard — 의도적으로 느리거나 메모리를 많이 쓰게 만들어 무차별 공격 비용 상승.
3. 함수의 흐름
MD5 / SHA-1 — 부적절. 설계 목적이 빠른 무결성 검증이라 비밀번호에는 부적합. MD5 충돌 (2004), SHA-1 충돌 (2017 SHAttered). 단순 SHA-256 도 부적합 — "느리지 않다" 가 핵심 문제.
bcrypt (1999) — Niels Provos · David Mazières 가 OpenBSD 의 비밀번호 해시로 발표 (USENIX 1999). Blowfish 의 키 스케줄을 변형:
- Cost factor (work factor) — 2^cost 회 라운드. 하드웨어 진보에 맞춰 cost 상향.
- 출력 형식 —
$2b$<cost>$<22-char salt><31-char hash>. salt 함께 저장. - 입력 길이 제한 — 72 바이트. 그 이상은 잘림.
오랫동안 사실상 표준. 한계는 메모리 사용이 작아 GPU / ASIC 의 병렬화에 약함.
scrypt (2009) — Colin Percival 의 함수 (USENIX 2009, RFC 7914). 의도적으로 메모리를 많이 사용 (memory-hard). GPU / ASIC 의 비용을 메모리 측면에서 상승. 매개변수 — N (CPU / memory cost) · r (block size) · p (parallelization). 설정의 균형이 어렵다는 점이 자주 지적.
Argon2 (2015) — Password Hashing Competition (PHC, 2013~2015) 의 우승작. Alex Biryukov · Daniel Dinu · Dmitry Khovratovich 설계. RFC 9106 (2021).
세 가지 변형:
- Argon2d — 데이터 의존 메모리 접근. side-channel 위험.
- Argon2i — 데이터 독립 메모리 접근. side-channel 안전.
- Argon2id — 둘의 절충. RFC 가 권장하는 기본.
매개변수 — m (메모리, KB) · t (반복 횟수) · p (병렬 정도). OWASP 가 현재 가장 강하게 권장.
PBKDF2 — PKCS#5 (RFC 8018) 기반의 키 유도 함수. HMAC + 반복. 메모리 요구가 작아 GPU 공격에 상대적으로 약하지만, FIPS 140 같은 규제 환경에서는 여전히 권장.
4. Salt 와 Pepper
Salt — 사용자별 무작위 값. 보통 16 바이트. DB 에 해시와 함께 저장 (bcrypt · Argon2 의 인코딩에 포함). 같은 비밀번호도 사용자마다 다른 해시 → rainbow table 무력화.
Pepper — 전체 사용자가 공유하는 비밀 값. 환경 변수 · 시크릿 매니저에 보관하고 DB 와 분리. DB 만 유출되면 pepper 없이는 무차별 공격이 어려워짐.
구현은 두 가지가 흔함:
- HMAC:
HMAC(pepper, password)를 비밀번호 해시 함수의 입력으로. - 함수 결과에 다시 HMAC.
pepper 를 바꾸려면 모든 비밀번호를 다시 해시해야 함 (또는 새 비밀번호부터 적용). 운영 정책 결정 사항.
5. 라이브러리
Node:
bcrypt(node-bcrypt) — native binding.bcryptjs— pure JS, native 빌드 없음.argon2(node-argon2) — native binding.
Python:
passlib— bcrypt · scrypt · Argon2 등 통합 인터페이스.argon2-cffi— Argon2 직접.bcrypt— bcrypt 직접.
JVM:
- Spring Security
PasswordEncoder—BCryptPasswordEncoder·Argon2PasswordEncoder·Pbkdf2PasswordEncoder.
기타:
- Go —
golang.org/x/crypto/bcrypt·argon2. - Rust —
argon2·bcrypt크레이트.
라이브러리 선택보다 알고리즘과 매개변수의 선택이 중요.
6. 매개변수 권고
매개변수는 시기 · 하드웨어에 따라 변함. OWASP Password Storage Cheat Sheet 의 권고가 사실상 참조점.
근래의 권고 일반:
- Argon2id — m=19456 (19MB) · t=2 · p=1 또는 더 큰 값.
- bcrypt — cost=10 ~ 12.
- scrypt — N=2^17 · r=8 · p=1 같은 값.
검증 시간 목표는 사용자 응답 0.5 ~ 1 초 안. 너무 길면 사용자 경험 · 가용성 영향, 너무 짧으면 공격 비용이 작음.
7. 정기 재해시
로그인 성공 시 현재 매개변수가 정책에 못 미치면 같은 비밀번호로 다시 해시해 저장:
if password_verify(stored, input):
if needs_rehash(stored, current_policy):
stored = hash(input, current_policy)
db.update(stored)
allow_login()
passlib · Spring Security DelegatingPasswordEncoder 등이 이를 표준화.
8. 추가 정책
- 길이 제한 — 최소 8 자, 권장 12 자 이상. 최대는 매우 길게 (200 자) 허용 — bcrypt 의 72 바이트 절단을 의식해 사전 SHA-256 같은 전처리도 한 패턴.
- 블랙리스트 — 자주 유출된 비밀번호 목록 (
Have I Been PwnedAPI · 자체 사전) 차단. NIST SP 800-63B 권고. - brute-force 차단 — 로그인 시도 한도 (레이트 리밋, 03-rate-limit-redis).
- 2FA — 비밀번호 외 추가 요인. 비밀번호 정책의 강제성을 줄일 수 있음.
- 유출 통지 — DB 사고 시 사용자에게 즉시 통지 · 강제 로테이션.
9. 자주 걸리는 자리
자체 함수 만들기 — 수많은 사고가 자체 해시 조합에서 발생. 검증된 라이브러리 사용.
bcrypt 의 72 바이트 절단 — 매우 긴 비밀번호의 뒷부분이 무시. 적절한 사전 해시 (SHA-256) 또는 Argon2 로 회피.
null 바이트 — 일부 bcrypt 구현은 null 바이트를 종료로 보고 잘라냄. 입력 sanitize.
타이밍 공격 — 비밀번호 비교는 상수 시간 함수. 라이브러리가 보통 처리하지만 직접 구현 자제.
솔트 짧음 · 재사용 — 짧은 솔트, 사용자 ID 를 솔트로 쓰는 사례는 약함. 16 바이트 무작위.
Pepper 의 DB 같이 보관 — 의미가 사라짐. 다른 저장소 · 시크릿 매니저.
매개변수 고정 — 5 년 전 cost 가 지금은 약함. 정기 검토.
로그에 비밀번호 노출 — 디버그 로그 · 에러 메시지에 평문이 들어가는 사고가 흔함. 입력은 즉시 해시 후 폐기.
하고픈 말
비밀번호 저장은 DB 유출이 사용자 다른 서비스의 보안까지 망가뜨릴 수 있는 책임의 자리. Argon2id (또는 bcrypt cost 10~12) + 사용자별 salt + pepper + 정기 재해시 + 레이트 리밋 + 유출 비밀번호 차단 — 이 조합이 OWASP 권고의 실용 셋. 자체 함수 만들기 금지.
Next
- headers-and-cors
- (security 끝)
OWASP Password Storage Cheat Sheet · RFC 9106 Argon2 · RFC 7914 scrypt · bcrypt 원 논문 (Provos · Mazières 1999) · PHC (Password Hashing Competition) · NIST SP 800-63B · Have I Been Pwned API · Spring Security PasswordEncoder 를 참고합니다.