비밀번호를 데이터베이스에 저장할 때, 혹시 아직도 MD5나 SHA-256을 사용하고 계신가요? 2024년 기준으로 최신 GPU는 초당 1,800억 개 이상의 MD5 해시를 계산할 수 있습니다. 이 말은 곧 여러분의 사용자 비밀번호가 몇 분 만에 뚫릴 수 있다는 뜻이죠.
이 글에서는 현재 보안 전문가들이 권장하는 세 가지 비밀번호 해싱 알고리즘인 bcrypt, Argon2, scrypt를 꼼꼼히 비교해 드리겠습니다. 각각의 장단점부터 실제 코드 예제, 그리고 어떤 상황에서 어떤 알고리즘을 선택해야 하는지까지 초보자도 이해할 수 있도록 쉽게 풀어보겠습니다.
1. 해싱(Hashing)과 암호화(Encryption), 뭐가 다른 건가요?
본격적인 알고리즘 비교에 앞서, 먼저 “해싱”과 “암호화”의 차이를 명확히 짚고 넘어가겠습니다. 이 두 개념을 혼동하면 보안 설계에서 치명적인 실수를 할 수 있거든요.
암호화(Encryption)는 양방향 과정입니다. 데이터를 특정 키로 암호화하면, 같은 키(또는 짝이 되는 키)로 다시 복호화해서 원본 데이터를 얻을 수 있죠. 예를 들어 메시지를 보낼 때 상대방이 읽을 수 있도록 암호화하는 것이 이에 해당합니다.
반면 해싱(Hashing)은 단방향 과정입니다. 한 번 해시 처리된 데이터는 원래대로 되돌릴 수 없습니다. 그래서 비밀번호 저장에 해싱을 사용하는 거예요. 누군가 데이터베이스를 탈취하더라도 해시값만으로는 원래 비밀번호를 알아낼 수 없으니까요.
| 구분 | 암호화 (Encryption) | 해싱 (Hashing) |
|---|---|---|
| 방향성 | 양방향 (복호화 가능) | 단방향 (복호화 불가) |
| 키 필요 여부 | 필요 | 불필요 (솔트 사용) |
| 대표 용도 | 데이터 전송, 저장 | 비밀번호 검증, 무결성 확인 |
| 예시 알고리즘 | AES, RSA | bcrypt, Argon2, scrypt |
비밀번호를 저장할 때는 반드시 해싱을 사용해야 합니다. 그것도 일반 해시 함수(MD5, SHA-256 등)가 아닌, 비밀번호 전용으로 설계된 알고리즘을 사용해야 하죠.
2. 왜 MD5, SHA-256은 비밀번호 저장에 부적합할까요?
“SHA-256은 안전한 알고리즘 아닌가요?”라고 물으실 수 있습니다. 맞습니다. SHA-256은 암호학적으로 안전한 해시 함수입니다. 하지만 문제는 너무 빠르다는 것입니다.
SHA-256은 데이터 무결성 검사, 디지털 서명 등을 위해 설계되었습니다. 이런 용도에서는 빠른 속도가 장점이죠. 하지만 비밀번호 해싱에서 빠른 속도는 곧 공격자에게 유리한 환경을 제공합니다.
현대 GPU를 사용하면 초당 수십억 개의 SHA-256 해시를 계산할 수 있습니다. 공격자가 레인보우 테이블(Rainbow Table)이나 무차별 대입 공격(Brute Force Attack)을 실행할 때, 빠른 해시 함수는 그야말로 ‘선물’과 같죠.
그래서 비밀번호 전용 해싱 알고리즘은 의도적으로 느리게 설계됩니다. 정상적인 사용자가 로그인할 때 0.2~0.5초 정도 기다리는 건 별 문제가 없지만, 공격자가 수백만 개의 비밀번호 조합을 시도할 때는 이 지연 시간이 치명적인 장벽이 됩니다.
3. 솔트(Salt)와 페퍼(Pepper), 비밀번호 보안의 기본 양념
비밀번호 해싱에서 빠질 수 없는 개념이 바로 솔트(Salt)입니다. 솔트는 각 비밀번호마다 추가되는 무작위 문자열로, 같은 비밀번호라도 다른 해시값을 생성하게 만듭니다.
예를 들어 두 사용자가 모두 “password123″이라는 비밀번호를 사용한다고 가정해 봅시다. 솔트 없이 해싱하면 두 사람의 해시값이 동일하게 나옵니다. 공격자는 이 점을 이용해 레인보우 테이블 공격을 할 수 있죠.
하지만 각 사용자마다 다른 솔트를 추가하면, 같은 비밀번호라도 완전히 다른 해시값이 생성됩니다. 다행히 bcrypt, Argon2, scrypt 같은 현대적 알고리즘은 솔트를 자동으로 생성하고 관리해 주므로, 개발자가 별도로 신경 쓸 필요가 없습니다.
페퍼(Pepper)는 솔트와 비슷하지만, 모든 비밀번호에 공통으로 적용되는 비밀 값입니다. 페퍼는 데이터베이스와 별도로 저장(예: 환경 변수, HSM)되어, 데이터베이스가 유출되더라도 추가적인 보호막 역할을 합니다.
4. bcrypt – 검증된 노장의 저력
bcrypt는 1999년 Niels Provos와 David Mazières가 개발한, 가장 오래되고 널리 사용되는 비밀번호 해싱 알고리즘입니다. OpenBSD의 기본 비밀번호 해시 알고리즘으로 채택되었고, 25년이 넘는 세월 동안 실전에서 검증되어 왔죠.
bcrypt의 작동 원리
bcrypt는 Blowfish 암호화 알고리즘의 키 설정 단계를 활용합니다. Blowfish는 키 설정에 많은 연산이 필요하도록 설계되었는데, bcrypt는 이 특성을 이용해 의도적으로 느린 해싱을 구현합니다.
가장 큰 특징은 조정 가능한 비용 요소(Cost Factor)입니다. 이 값을 높이면 해싱 시간이 기하급수적으로 증가하므로, 하드웨어 성능이 발전해도 비용 요소만 조정하면 보안 수준을 유지할 수 있습니다.
bcrypt의 장점
- 검증된 신뢰성: 25년 이상 실전 사용, 수많은 보안 감사를 통과
- 간단한 구현: 거의 모든 프로그래밍 언어에서 라이브러리 지원
- 자동 솔트 처리: 개발자가 솔트를 별도로 관리할 필요 없음
- 일관된 성능: 고정된 4KB 메모리 사용으로 예측 가능한 리소스 소비
bcrypt의 단점
- 72바이트 비밀번호 제한: 비밀번호가 72바이트를 초과하면 잘림
- 고정 메모리 사용 (4KB): 현대 GPU 공격에 상대적으로 취약
- 병렬 처리 미지원: CPU 멀티코어 활용 불가
Python에서 bcrypt 사용하기
import bcrypt
# 비밀번호 해싱 (work factor = 12, 약 250ms 소요)
password = b"MySecurePassword123!"
salt = bcrypt.gensalt(rounds=12) # rounds가 비용 요소
hashed = bcrypt.hashpw(password, salt)
print(f"해시값: {hashed}")
# 출력 예: $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.V...
# 비밀번호 검증
if bcrypt.checkpw(password, hashed):
print("비밀번호가 일치합니다!")
else:
print("비밀번호가 틀립니다.")
bcrypt를 Python에서 사용하려면 먼저 라이브러리를 설치해야 합니다:
pip install bcrypt
OWASP 권장 설정
OWASP Password Storage Cheat Sheet에서는 bcrypt 사용 시 다음을 권장합니다:
- 비용 요소(rounds): 10 이상 (가능하면 12~14)
- 비밀번호 최대 길이: 72바이트로 제한
- 사용 시나리오: Argon2나 scrypt를 사용할 수 없는 레거시 시스템
5. scrypt – 메모리 하드 함수의 선구자
scrypt는 2009년 Colin Percival이 개발한 알고리즘입니다. Percival은 암호화 백업 서비스 Tarsnap을 개발하면서, 기존 알고리즘의 한계를 극복하기 위해 scrypt를 만들었습니다.
scrypt가 특별한 이유
scrypt의 핵심 혁신은 메모리 하드(Memory-Hard) 설계입니다. bcrypt가 CPU 연산에만 의존하는 반면, scrypt는 대량의 메모리를 필수적으로 사용하도록 설계되었습니다.
왜 메모리가 중요할까요? GPU나 ASIC 같은 특수 하드웨어는 병렬 연산에 뛰어나지만, 각 연산 유닛에 충분한 메모리를 제공하기는 어렵습니다. 메모리는 비싸고, 대역폭에도 한계가 있거든요. scrypt는 이 점을 이용해 하드웨어 기반 공격을 효과적으로 차단합니다.
scrypt의 파라미터
scrypt는 세 가지 주요 파라미터를 가집니다:
| 파라미터 | 설명 | 권장값 |
|---|---|---|
| N (CPU/Memory Cost) | 메모리 및 CPU 비용, 2의 거듭제곱 | 2^17 (131072) |
| r (Block Size) | 블록 크기 | 8 |
| p (Parallelism) | 병렬화 정도 | 1 |
scrypt의 장점
- GPU/ASIC 공격에 강함: 메모리 하드 설계로 하드웨어 공격 방어
- 검증된 안정성: 10년 이상 암호화폐 채굴 등에서 광범위하게 사용
- 유연한 파라미터: 메모리와 CPU 비용을 독립적으로 조절 가능
scrypt의 단점
- 높은 메모리 사용량: 리소스 제한 환경에서는 부담
- 암호화폐 채굴 하드웨어: Litecoin 등의 채굴로 인해 scrypt ASIC이 존재
- bcrypt보다 복잡한 구현: 파라미터 설정이 더 까다로움
Python에서 scrypt 사용하기
Python 3.6 이상에서는 표준 라이브러리 hashlib에 scrypt가 포함되어 있습니다:
import hashlib
import os
import hmac
# 비밀번호 해싱
password = b"MySecurePassword123!"
salt = os.urandom(16) # 16바이트 무작위 솔트
# scrypt 해시 생성
# n=2^14, r=8, p=1은 웹 애플리케이션에 적합한 설정
hashed = hashlib.scrypt(
password,
salt=salt,
n=16384, # 2^14, 메모리/CPU 비용
r=8, # 블록 크기
p=1, # 병렬화
dklen=32 # 출력 길이 (32바이트)
)
print(f"솔트: {salt.hex()}")
print(f"해시: {hashed.hex()}")
# 데이터베이스에는 솔트와 해시를 함께 저장해야 합니다
# 예: stored_data = salt.hex() + ":" + hashed.hex()
# 비밀번호 검증 함수
def verify_scrypt_password(password: bytes, stored_salt: bytes, stored_hash: bytes) -> bool:
"""저장된 솔트로 비밀번호를 다시 해싱하여 검증"""
new_hash = hashlib.scrypt(
password,
salt=stored_salt,
n=16384,
r=8,
p=1,
dklen=32
)
# 타이밍 공격 방지를 위해 상수 시간 비교 사용
return hmac.compare_digest(new_hash, stored_hash)
# 검증 예시
is_valid = verify_scrypt_password(password, salt, hashed)
print(f"검증 결과: {is_valid}") # True
OWASP 권장 설정
- N (CPU/Memory Cost): 2^17 (131072) 이상
- r (Block Size): 8
- p (Parallelism): 1
6. Argon2 – 2015년 비밀번호 해싱 대회의 우승자
Argon2는 2015년 Password Hashing Competition(PHC)에서 우승한 최신 알고리즘입니다. 룩셈부르크 대학교의 Alex Biryukov, Daniel Dinu, Dmitry Khovratovich가 개발했으며, 현재 OWASP가 가장 먼저 권장하는 알고리즘입니다.
Argon2의 세 가지 변형
Argon2는 용도에 따라 세 가지 버전이 있습니다:
| 변형 | 특징 | 용도 |
|---|---|---|
| Argon2d | 데이터 의존적 메모리 접근, GPU 공격에 강함 | 암호화폐, 오프라인 환경 |
| Argon2i | 데이터 독립적 메모리 접근, 사이드채널 공격에 강함 | 민감한 서버 환경 |
| Argon2id | Argon2d와 Argon2i의 하이브리드 (권장) | 일반적인 비밀번호 저장 |
Argon2id는 처음 절반은 Argon2i 방식으로, 나머지 절반은 Argon2d 방식으로 동작합니다. 이를 통해 사이드채널 공격과 GPU 공격 모두에 대한 방어력을 갖추게 됩니다. RFC 9106에서도 Argon2id 사용을 권장합니다.
Argon2의 파라미터
Argon2는 네 가지 주요 파라미터를 제공합니다:
| 파라미터 | 설명 | OWASP 권장값 |
|---|---|---|
| m (Memory) | 사용할 메모리 (KB) | 19456 (19MB) 또는 47104 (46MB) |
| t (Iterations) | 반복 횟수 | 2 (19MB일 때) 또는 1 (46MB일 때) |
| p (Parallelism) | 병렬 스레드 수 | 1 |
| hash length | 출력 해시 길이 | 32바이트 |
Argon2의 장점
- 최신 설계: GPU, ASIC, 사이드채널 공격을 모두 고려
- 높은 유연성: 메모리, 시간, 병렬성을 독립적으로 조절
- 미래 지향적: 하드웨어 발전에 맞춰 파라미터 상향 가능
- RFC 표준화: RFC 9106으로 공식 표준화됨
Argon2의 단점
- 상대적으로 신생: 2015년 등장, 아직 bcrypt만큼의 실전 경험은 부족
- 높은 리소스 요구: 웹 애플리케이션에서 신중한 파라미터 설정 필요
- 일부 환경에서 라이브러리 미지원: 오래된 시스템에서는 지원 부족할 수 있음
Python에서 Argon2 사용하기
Python에서 Argon2를 사용하려면 argon2-cffi 라이브러리를 설치합니다:
pip install argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
# 기본 설정으로 PasswordHasher 생성
# (기본값: time_cost=3, memory_cost=65536, parallelism=4)
ph = PasswordHasher()
# 비밀번호 해싱
password = "MySecurePassword123!"
hashed = ph.hash(password)
print(f"해시값: {hashed}")
# 출력 예: $argon2id$v=19$m=65536,t=3,p=4$...
# 비밀번호 검증
try:
ph.verify(hashed, password)
print("비밀번호가 일치합니다!")
except VerifyMismatchError:
print("비밀번호가 틀립니다.")
# OWASP 권장 설정으로 커스터마이징
ph_owasp = PasswordHasher(
time_cost=2, # 반복 횟수
memory_cost=19456, # 메모리 (KB), 약 19MB
parallelism=1, # 병렬 스레드
hash_len=32, # 해시 길이
salt_len=16 # 솔트 길이
)
hashed_owasp = ph_owasp.hash(password)
print(f"OWASP 권장 설정 해시: {hashed_owasp}")
Argon2 해시 형식 이해하기
Argon2 해시는 다음과 같은 형식을 가집니다:
$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$SqlVijFGiPG...
argon2id: 사용된 Argon2 변형v=19: Argon2 버전 (1.3)m=19456: 메모리 비용 (KB)t=2: 반복 횟수p=1: 병렬성- 첫 번째 Base64: 솔트
- 두 번째 Base64: 실제 해시값
7. bcrypt vs Argon2 vs scrypt – 한눈에 보는 비교표
세 알고리즘을 한눈에 비교해 보겠습니다. (참고: PBKDF2는 FIPS 인증 환경에서만 권장되므로 별도로 다루지 않습니다)
| 비교 항목 | bcrypt | scrypt | Argon2 (Argon2id) |
|---|---|---|---|
| 출시 연도 | 1999년 | 2009년 | 2015년 |
| 기반 기술 | Blowfish 암호화 | PBKDF2-HMAC-SHA256 | Blake2b 해시 |
| 메모리 사용 | 고정 4KB | 조절 가능 (수 MB~GB) | 조절 가능 (수 MB~GB) |
| GPU 공격 저항 | 중간 | 높음 | 매우 높음 |
| 사이드채널 저항 | 높음 | 낮음 | 높음 (Argon2id) |
| 비밀번호 제한 | 72바이트 | 없음 | 없음 |
| 라이브러리 지원 | 매우 광범위 | 광범위 | 확대 중 |
| OWASP 권장 순위 | 3순위 | 2순위 | 1순위 |
| 해싱 시간 예시 | ~250ms | ~200ms | ~150ms |
성능 비교 (동등한 보안 수준 기준)
실제 서버 환경에서의 성능을 비교하면:
| 알고리즘 | 설정 | 해싱 시간 | 메모리 사용 |
|---|---|---|---|
| bcrypt | cost=13 | 250~350ms | 4KB |
| scrypt | N=2^17, r=8, p=1 | 180~300ms | 128MB |
| Argon2id | m=128MB, t=3, p=2 | 220~280ms | 128MB |
메모리 사용량에서 큰 차이가 나는 것을 볼 수 있습니다. bcrypt는 4KB만 사용하는 반면, scrypt와 Argon2는 수백 MB까지 사용할 수 있죠. 이 메모리 요구사항이 GPU 공격을 어렵게 만드는 핵심 요소입니다.
8. 어떤 알고리즘을 선택해야 할까요? – 상황별 가이드
이제 실제로 어떤 알고리즘을 선택해야 할지 상황별로 정리해 드리겠습니다.
신규 프로젝트라면? → Argon2id
새로 시작하는 프로젝트라면 Argon2id를 선택하세요. OWASP, IETF(RFC 9106) 등 주요 보안 기관에서 권장하는 최신 알고리즘입니다.
# 신규 프로젝트 권장 설정
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=2,
memory_cost=19456, # 19MB
parallelism=1
)
레거시 시스템이라면? → bcrypt
이미 bcrypt를 사용 중인 시스템이라면 굳이 당장 마이그레이션할 필요는 없습니다. bcrypt는 여전히 안전하며, 비용 요소(cost)를 12 이상으로 유지하면 충분한 보안을 제공합니다.
다만 장기적으로 Argon2로의 마이그레이션을 계획하는 것이 좋습니다.
FIPS 인증이 필요하다면? → PBKDF2
금융권이나 정부 기관처럼 FIPS-140 인증이 필수인 환경에서는 PBKDF2를 사용해야 합니다. OWASP는 이 경우 600,000회 이상의 반복과 HMAC-SHA-256 사용을 권장합니다.
리소스가 제한적이라면? → bcrypt
저사양 서버나 IoT 기기처럼 메모리가 제한된 환경에서는 bcrypt가 적합합니다. 4KB의 고정 메모리만 사용하면서도 충분한 보안을 제공하죠.
암호화폐/블록체인 관련이라면? → scrypt 또는 Argon2d
암호화폐 관련 프로젝트에서 키 유도가 필요하다면 scrypt의 실전 경험이 도움이 될 수 있습니다. 단, 사이드채널 공격 우려가 없는 환경에서는 Argon2d도 좋은 선택입니다.
9. 실제 구현 시 주의해야 할 보안 사항
알고리즘 선택만큼 중요한 것이 올바른 구현입니다. 다음 사항들을 반드시 지켜주세요.
절대 하면 안 되는 것들
# ❌ 잘못된 예시: 고정 솔트 사용
static_salt = b"fixed_salt_value" # 위험!
# ❌ 잘못된 예시: 낮은 비용 요소
bcrypt.hashpw(password, bcrypt.gensalt(rounds=4)) # 너무 낮음!
# ❌ 잘못된 예시: 일반 해시 함수 사용
import hashlib
hashlib.sha256(password.encode()).hexdigest() # 비밀번호에 부적합!
반드시 해야 하는 것들
# ✅ 올바른 예시: 라이브러리가 생성하는 무작위 솔트 사용
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# ✅ 올바른 예시: 상수 시간 비교 사용 (타이밍 공격 방지)
import hmac
def safe_compare(a, b):
return hmac.compare_digest(a, b)
# ✅ 올바른 예시: 적절한 비용 요소 설정
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
파라미터 정기 검토
하드웨어 성능은 매년 향상됩니다. OWASP는 매년 파라미터를 검토하고 필요시 상향 조정할 것을 권장합니다.
일반적인 조정 가이드:
- Argon2: 반복 횟수(t) 1 증가
- bcrypt: 비용 요소(cost) 1 증가
- scrypt: N 값 2배 증가
- PBKDF2: 반복 횟수 2배 증가
10. 기존 시스템에서 알고리즘을 마이그레이션하는 방법
이미 운영 중인 서비스에서 해싱 알고리즘을 변경하려면 어떻게 해야 할까요? 비밀번호는 원본을 알 수 없으니 한꺼번에 변환할 수 없습니다. 대신 점진적 마이그레이션 방식을 사용합니다.
점진적 마이그레이션 전략
- 새 알고리즘 도입: 새로 가입하는 사용자부터 Argon2id 적용
- 로그인 시 업그레이드: 기존 사용자가 로그인하면, 검증 후 새 알고리즘으로 재해싱
- 버전 관리: 해시에 버전 정보를 포함하여 어떤 알고리즘인지 식별
import bcrypt
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
def verify_and_upgrade(password: str, stored_hash: str) -> tuple[bool, str | None]:
"""
비밀번호 검증 및 필요시 Argon2로 업그레이드
Returns: (검증 성공 여부, 새 해시 또는 None)
"""
new_hash = None
# 저장된 해시 형식 확인
if stored_hash.startswith("$argon2"):
# 이미 Argon2 사용 중
ph = PasswordHasher()
try:
ph.verify(stored_hash, password)
return True, None
except VerifyMismatchError:
return False, None
elif stored_hash.startswith("$2"):
# bcrypt 해시 - 검증 후 업그레이드 필요
if bcrypt.checkpw(password.encode(), stored_hash.encode()):
# 검증 성공, Argon2로 업그레이드
ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
new_hash = ph.hash(password)
return True, new_hash
return False, None
return False, None
# 사용 예시
success, new_hash = verify_and_upgrade("user_password", stored_hash_from_db)
if success:
if new_hash:
# 데이터베이스에 새 해시 저장
update_user_hash(user_id, new_hash)
# 로그인 성공 처리
11. Node.js에서 비밀번호 해싱 구현하기
Python 외에 Node.js를 사용하는 분들을 위해 각 알고리즘의 구현 예제를 추가로 정리했습니다.
bcrypt (Node.js)
const bcrypt = require('bcrypt');
// 해싱
async function hashPassword(password) {
const saltRounds = 12;
const hash = await bcrypt.hash(password, saltRounds);
return hash;
}
// 검증
async function verifyPassword(password, hash) {
const match = await bcrypt.compare(password, hash);
return match;
}
// 사용
(async () => {
const hash = await hashPassword('MySecurePassword123!');
console.log('Hash:', hash);
const isValid = await verifyPassword('MySecurePassword123!', hash);
console.log('Valid:', isValid); // true
})();
Argon2 (Node.js)
const argon2 = require('argon2');
// 해싱 (OWASP 권장 설정)
async function hashPassword(password) {
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19MB
timeCost: 2,
parallelism: 1
});
return hash;
}
// 검증
async function verifyPassword(password, hash) {
try {
return await argon2.verify(hash, password);
} catch (err) {
return false;
}
}
// 사용
(async () => {
const hash = await hashPassword('MySecurePassword123!');
console.log('Hash:', hash);
const isValid = await verifyPassword('MySecurePassword123!', hash);
console.log('Valid:', isValid); // true
})();
scrypt (Node.js)
Node.js에는 crypto 모듈에 scrypt가 내장되어 있습니다:
const crypto = require('crypto');
// 해싱
async function hashPassword(password) {
return new Promise((resolve, reject) => {
const salt = crypto.randomBytes(16);
crypto.scrypt(password, salt, 32, { N: 16384, r: 8, p: 1 }, (err, derivedKey) => {
if (err) reject(err);
// 솔트와 해시를 함께 저장 (콜론으로 구분)
resolve(salt.toString('hex') + ':' + derivedKey.toString('hex'));
});
});
}
// 검증
async function verifyPassword(password, stored) {
return new Promise((resolve, reject) => {
const [saltHex, hashHex] = stored.split(':');
const salt = Buffer.from(saltHex, 'hex');
const storedHash = Buffer.from(hashHex, 'hex');
crypto.scrypt(password, salt, 32, { N: 16384, r: 8, p: 1 }, (err, derivedKey) => {
if (err) reject(err);
// 타이밍 공격 방지를 위해 timingSafeEqual 사용
resolve(crypto.timingSafeEqual(storedHash, derivedKey));
});
});
}
// 사용
(async () => {
const stored = await hashPassword('MySecurePassword123!');
console.log('Stored:', stored);
const isValid = await verifyPassword('MySecurePassword123!', stored);
console.log('Valid:', isValid); // true
})();
12. 마지막으로 핵심을 정리하자면…
비밀번호 해싱 알고리즘 선택은 사용자 보안에 직접적인 영향을 미치는 중요한 결정입니다. 이 글의 핵심 내용을 정리하면:
알고리즘 선택 우선순위 (OWASP 권장)
- Argon2id – 신규 프로젝트의 첫 번째 선택
- scrypt – Argon2를 사용할 수 없을 때
- bcrypt – 레거시 시스템 또는 리소스 제한 환경
- PBKDF2 – FIPS 인증이 필요한 경우에만
기억해야 할 핵심 포인트
- MD5, SHA-256 등 일반 해시 함수는 비밀번호에 절대 사용하지 마세요
- 솔트는 각 비밀번호마다 고유해야 하며, 현대 알고리즘은 이를 자동 처리합니다
- 비용 요소/파라미터는 서버 성능에 맞게 설정하되, 너무 낮지 않게 유지하세요
- 하드웨어 발전에 따라 매년 파라미터를 검토하고 상향 조정하세요
비밀번호 보안은 한 번 설정하고 끝나는 것이 아니라, 지속적으로 관리하고 개선해야 하는 영역입니다. 이 글이 여러분의 서비스를 더 안전하게 만드는 데 도움이 되었기를 바랍니다.
참고 자료
- OWASP Password Storage Cheat Sheet
- RFC 9106 – Argon2 Memory-Hard Function
- Argon2 Reference Implementation (GitHub)
- bcrypt Python Library (GitHub)
- argon2-cffi Documentation