블로그로 돌아가기
Security 2026-04-28

bcrypt vs Argon2: 2026년 비밀번호 해싱

올바른 비밀번호 해싱 알고리즘 선택과 프로덕션 매개변수 튜닝.

비밀번호 해싱의 임무는 단 하나입니다. 이미 데이터베이스를 훔친 공격자에게 각 추측을 비싸게 만드는 것. 범용 해시는 설계상 이 임무에 실패합니다. 소비자용 GPU 한 장이 초당 수백억 개의 SHA-256 해시를 계산하므로, SHA-256으로 해싱된 8자 비밀번호는 몇 분 만에 무너집니다. bcrypt와 Argon2는 이 경제 방정식을 바꾸기 위해 존재하며, 둘 사이의 선택은 대부분 각각이 무엇을 비싸게 만드는지 이해하는 문제입니다.

위협 모델: 오프라인 크래킹

침해 후 공격자는 해시 값을 손에 쥐고 하드웨어 속도로 로컬에서 추측을 실행합니다. 솔팅(두 알고리즘 모두 자동 처리하며 솔트를 출력 문자열에 내장)은 레인보우 테이블을 막고 사용자별 크래킹을 강제하지만, 추측 속도에는 아무 영향이 없습니다. 추측 속도는 두 개의 레버로 결정됩니다.

  • 시간 비용 — 해시 하나에 필요한 계산량
  • 메모리 비용 — 해시 하나에 필요한 RAM

더 흥미로운 레버는 메모리입니다. GPU는 수천 개의 코어를 갖지만 코어당 빠른 메모리는 제한적입니다. 해시당 64MB를 요구하는 알고리즘은 GPU 병렬성을 수천 개의 동시 추측에서 한 줌으로 줄입니다.

bcrypt: 1999년 설계, 여전히 견고

bcrypt(Provos와 Mazières)는 의도적으로 비싼 Blowfish 키 스케줄 위에 만들어졌습니다. 비용 매개변수는 로그 스케일입니다. cost 12는 4,096회 반복을 의미하고 1 증가할 때마다 작업량이 두 배가 됩니다.

import bcrypt from 'bcrypt';

const hash = await bcrypt.hash(password, 12);

const ok = await bcrypt.compare(password, hash);

4KB의 내부 상태는 SHA 계열보다 GPU에 잘 저항하지만 현대 메모리 하드 함수의 요구량에는 한참 못 미칩니다. 알아둘 날카로운 모서리 두 가지:

  • 72바이트 절단. bcrypt는 72바이트를 넘는 입력을 조용히 무시하므로, 72바이트 이후만 다른 두 문자열은 동일하게 해싱됩니다. 이를 우회하려고 사전 해싱을 한다면 다이제스트를 먼저 base64나 hex로 인코딩하세요. 원시 SHA-256 출력에는 NUL 바이트가 포함될 수 있고, 일부 bcrypt 구현은 첫 NUL에서 절단해 수많은 비밀번호를 하나로 붕괴시킵니다.
  • 변종 동물원. $2a$, $2b$, $2y$ 접두사는 역사적 구현 버그의 흔적입니다. 현대 라이브러리는 $2b$를 출력합니다. 서로 다르게 취급하는 라이브러리를 섞지만 마세요.

Argon2: 메모리 하드 우승자

Argon2는 2015년 Password Hashing Competition에서 우승했고 OWASP의 권장 기본값입니다. Argon2id 변종을 사용하세요. Argon2i의 부채널 공격 방어와 Argon2d의 강한 GPU 저항을 결합한 것입니다.

import argon2 from 'argon2';

const hash = await argon2.hash(password, {

type: argon2.argon2id,

memoryCost: 65536, // KiB 단위, 즉 64 MiB

timeCost: 3,

parallelism: 1

});

const ok = await argon2.verify(hash, password);

세 매개변수는 독립적입니다. memoryCost는 해시당 RAM을, timeCost는 그 메모리를 순회하는 횟수를, parallelism은 레인 수를 정합니다. OWASP의 현재 최소치는 19 MiB / t=2 / p=1이고, 64 MiB / t=3 / p=1이면 서버급으로 여유 있는 설정입니다. 출력은 자기 서술적 PHC 문자열($argon2id$v=19$m=65536,t=3,p=1$...)이라 매개변수가 해시와 함께 이동하며, 설정을 올린 뒤에도 기존 해시 검증은 계속 작동합니다.

튜닝 방법론

매개변수는 하드웨어 의존적이므로 숫자를 베끼지 말고 경험적으로 튜닝하세요.

  • 프로덕션급 하드웨어에서 벤치마크하고, 대화형 로그인 기준 해시당 200~500ms를 목표로.
  • 동시성 하에서 테스트. 64 MiB짜리 로그인 50개가 동시에 오면 RAM 3.2GB입니다. 메모리 하드 해싱은 제한 없이 두면 스스로 초래한 DoS 벡터입니다. 해싱 주위에 세마포어나 전용 큐를 두세요.
  • 매년 재평가. 오늘의 bcrypt cost 12는 하드웨어가 따라잡으면 13이 되어야 합니다.

비밀번호 재설정 없는 마이그레이션

해싱 업그레이드를 위해 강제 재설정은 필요 없습니다.

1. 기존 해시를 지금 감싸기: argon2(legacy_hash)를 저장하고 행에 래핑 표시를 해서, 저장 상태의 약한 해시를 즉시 제거합니다.

2. 로그인 시 재해싱: 사용자가 인증할 때 잠시 평문을 쥐게 됩니다. 이전 방식으로 검증한 뒤 새 Argon2id 해시를 저장하세요.

3. 대부분의 라이브러리는 저장된 해시의 매개변수를 현재 정책과 비교하는 needsRehash 검사를 제공합니다. 성공한 로그인마다 호출하면 업그레이드가 자동화됩니다.

흔한 실수

  • 비밀번호에 빠른 해시(MD5, SHA 계열, 솔트를 쳤더라도)를 사용하는 것 자체
  • 상수 시간이 아닌 문자열 비교로 검증을 직접 구현
  • 페퍼를 해시와 같은 데이터베이스에 저장 (KMS나 환경 시크릿에 보관하고, 해싱 전 비밀번호에 HMAC으로 적용)
  • 바쁜 웹 서버에서 parallelism을 높게 잡아 요청 스레드를 굶기기
  • 비밀번호 해싱과 세션 보안의 혼동 — 세션 토큰이 추측 가능하면 완벽한 Argon2 설정도 무의미

결론

신규 시스템: OWASP 매개변수 이상의 Argon2id. cost 12 이상의 기존 bcrypt: 긴급한 마이그레이션은 불필요하지만 로그인 시 재해싱을 연결해 시스템이 스스로 업그레이드되게 하세요. FIPS 제약 환경: 600,000회 이상 반복의 PBKDF2-HMAC-SHA256이 규정 준수 대안입니다. 어떤 경우에도 직접 만들지 마세요.

scrypt의 자리

scrypt(2009)는 Argon2가 존재하기 전에 메모리 하드니스를 개척했고 여전히 견고한 선택입니다. 특히 라이브러리 지원이 성숙한 곳에서 그렇습니다. Node 내장 crypto.scrypt, Go의 x/crypto. 매개변수는 추론하기 더 까다롭습니다. N, r, p가 비자명하게 상호작용하고, 흔히 권장되는 N=2^17, r=8, p=1은 해시당 약 128MB를 소모합니다. 하지만 올바르게 튜닝된 scrypt는 Argon2id보다 의미 있게 약하지 않습니다. 플랫폼이 scrypt를 공짜로 제공하는데 Argon2는 네이티브 의존성을 끌어들여야 한다면, scrypt를 택하는 것은 옹호 가능한 엔지니어링 판단입니다.

비밀번호 품질은 여전히 해시보다 중요합니다. sdk.is/password-generator 의 비밀번호 생성기로 강력한 무작위 비밀번호를 만드세요.