포스트

비밀번호 저장 - bcrypt와 argon2의 파라미터를 정하는 기준

“비밀번호는 해시해서 저장한다”까지는 상식이 됐는데, 어떤 해시를 쓰고 파라미터를 얼마로 둘지는 대개 프레임워크 기본값에 맡긴다. 기본값이 나쁘지는 않지만, 그 값이 무엇을 상정한 것인지 모르면 하드웨어가 바뀌어도 그대로 두게 된다. 이 글은 파라미터를 근거 있게 정하는 기준이다.

일반 해시를 쓰면 안 되는 이유

SHA-256은 빠르도록 설계됐다. 그것이 장점인 용도(무결성 검증)와 치명적인 용도(비밀번호)가 나뉜다. 유출된 해시를 가진 공격자는 후보를 대입해 본다. GPU 한 대가 초당 수십억 개의 SHA-256을 계산한다. 사람이 만든 비밀번호의 후보 공간은 그보다 훨씬 작다.

그래서 비밀번호 해시는 일부러 느리게 만든다. 여기에 두 가지가 더 붙는다.

솔트(salt). 사용자마다 다른 무작위 값을 섞어 저장한다. 같은 비밀번호가 다른 해시가 되므로, 미리 계산한 표(레인보우 테이블)가 무력해지고 “같은 비밀번호를 쓰는 사용자” 목록도 드러나지 않는다. 솔트는 비밀이 아니므로 해시와 함께 저장한다. bcrypt와 argon2는 솔트를 출력 문자열 안에 포함한다.

페퍼(pepper). 모든 사용자에게 공통인 비밀값을 추가로 섞는다. DB만 유출되고 애플리케이션 설정은 안 샜을 때 한 겹 더 막아 준다. 다만 페퍼는 회전이 어렵다(바꾸면 전부 재계산해야 한다). 쓸 거라면 HMAC으로 전처리하는 형태가 다루기 쉽다.

bcrypt: 비용 인자 하나

1999년에 나왔고 여전히 널리 쓰인다. 파라미터는 cost(work factor) 하나다. 반복 횟수가 2^cost이므로 cost를 1 올리면 시간이 두 배다.

기준은 단순하다. 인증 1회가 목표 시간(보통 수십~수백 ms)에 걸리는 값을 고르고, 하드웨어가 바뀌면 다시 잰다. 2026년 기준 서버급 장비에서 cost 12~14가 그 범위에 들어가는 경우가 많지만, 숫자를 외우는 것보다 자기 서버에서 재는 것이 맞다.

bcrypt에는 알아 둘 제약이 둘 있다.

  • 입력이 72바이트에서 잘린다. 긴 패스프레이즈의 뒷부분이 무시된다. 긴 입력을 허용한다면 미리 SHA-256으로 줄여 넣는 방식이 쓰인다.
  • 널 바이트에서 잘리는 구현이 있다. 이진 입력을 넣지 않는다.
  • 메모리를 거의 안 쓴다. 그래서 GPU와 ASIC 병렬화에 상대적으로 취약하다. 이것이 argon2가 나온 이유다.

argon2: 메모리를 쓰게 만든다

2015년 Password Hashing Competition의 우승 알고리즘이고, 지금 새로 만든다면 기본 선택지다. 변종 중 argon2id를 쓴다(argon2i의 부채널 저항과 argon2d의 GPU 저항을 섞은 것).

파라미터가 셋이다.

파라미터뜻올리면
m (memory)사용할 메모리GPU·ASIC 병렬화가 어려워진다
t (iterations)반복 횟수시간이 는다
p (parallelism)병렬 레인 수같은 시간에 더 많은 메모리를 쓴다

핵심은 m이다. 해시 하나에 64MB를 쓰게 만들면, 병렬로 1,000개를 계산하려는 공격자는 64GB가 필요하다. 시간이 아니라 메모리로 비용을 매기는 것이 bcrypt와의 차이다.

여기서 운영상의 제약이 나온다. 서버도 그 메모리를 써야 한다. 동시 로그인 100건에 m=64MB면 순간 6.4GB다. 로그인 폭주(배포 직후, 이벤트 시작)에서 OOM이 날 수 있다. 파라미터는 동시 인증 수와 함께 정해야 한다. 인증 요청에 별도의 동시성 상한(세마포어)을 두는 것도 방법이다.

파라미터를 정하는 절차

  1. 목표 시간을 정한다. 로그인 UX와 서버 여유를 보고 대개 0.1~0.5초.
  2. 운영 하드웨어에서 파라미터를 올려 가며 시간을 잰다. 개발 노트북 기준으로 정하지 않는다.
  3. 동시 인증 수를 곱해 메모리 상한을 확인한다(argon2).
  4. 값을 코드가 아니라 설정에 둔다. 하드웨어가 바뀌면 올려야 한다.
  5. 해시 문자열에 파라미터가 들어 있으므로, 로그인 성공 시 옛 파라미터면 다시 해시해 저장한다. 이것으로 점진적 상향이 가능하다.

스프링 시큐리티의 DelegatingPasswordEncoder는 {bcrypt}, {argon2} 같은 접두어로 알고리즘을 식별하므로 여러 방식이 공존할 수 있다. 마이그레이션은 이 구조 위에서 5번 방식으로 한다.

이 설명이 깨지는 곳

  • 해시를 잘 해도 자격 증명 스터핑은 막지 못한다. 다른 사이트에서 샌 비밀번호를 그대로 시도하는 공격에는 속도 제한, 유출 목록 대조, 다중 인증이 답이다.
  • 인증 지연이 사용자 열거 통로가 된다. 없는 사용자에게 즉시 404를 주고 있는 사용자에게만 300ms를 쓰면 응답 시간으로 계정 존재를 알 수 있다. 없는 사용자에도 더미 해시를 계산한다.
  • 비밀번호 정책이 강도를 보장하지 않는다. 복잡도 규칙보다 길이와 유출 목록 대조가 효과적이라는 것이 최근 권고(NIST SP 800-63B)의 방향이다.
  • PBKDF2는 여전히 유효하지만 마지막 선택지다. FIPS 준수가 필요한 환경에서 쓰고, 그 경우 반복 횟수를 크게 잡는다.

무엇을 재면 확인되는가

  1. 운영 장비에서 파라미터별 단일 해시 시간. 이것이 모든 결정의 출발점이다.
  2. 동시 인증 N건에서의 메모리 사용량과 p99. argon2의 m을 올려 가며.
  3. 로그인 폭주를 흉내 내 인증 경로가 다른 API를 느리게 만드는지. 스레드 고갈 실험과 같은 구조의 질문이다. 비밀번호 해시는 의도적으로 느린 CPU 작업이므로 격리 없이 두면 공유 자원을 먹는다.

실무와의 접점

인증 정리에서 해시 저장을 다뤘지만 파라미터의 근거는 적지 않았다. 지금 보면 그 글에 빠진 것은 알고리즘 이름이 아니라 “이 값을 왜 이렇게 두었는가” 다. 보안 설정에서 기본값을 쓰는 것 자체는 문제가 아니고, 그 기본값이 무엇을 상정했는지 모른 채 몇 년을 두는 것이 문제다.

정리

  • 일반 해시는 빠르기 때문에 부적합하다. 비밀번호 해시는 일부러 느리게 만든다.
  • 솔트는 사용자마다 다르게, 비밀이 아니며 해시와 함께 저장한다. 페퍼는 한 겹 더지만 회전이 어렵다.
  • bcrypt는 cost 하나로 시간을 조절하고, 72바이트 절단과 낮은 메모리 사용이라는 제약이 있다.
  • argon2id는 메모리로 비용을 매긴다. m이 핵심이고, 서버의 동시 인증 수와 함께 정해야 한다.
  • 파라미터는 운영 하드웨어에서 재서 정하고, 설정에 두고, 로그인 시 점진적으로 올린다.
  • 없는 사용자에게도 더미 해시를 계산해 응답 시간으로 계정이 드러나지 않게 한다.

참고

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

댓글

아직 댓글이 없습니다