사전 bcrypt
알고리즘

bcrypt

gabury1고친 사람 github-actions[bot]

bcrypt 는 비밀번호를 저장해도 되는 값으로 바꿉니다. 그 계산을 일부러 오래 걸리게 만듭니다. 얼마나 오래 걸리게 할지는 숫자 하나로 정해 두고, 장비가 좋아지면 그 숫자만 올립니다. 훔쳐 간 목록을 들고 비밀번호를 하나씩 맞춰 보려는 공격자의 비용을 그만큼 키우는 것이 목적입니다.

쉽고 빠른 이해

bcrypt 는 비밀번호를 받아 문자열 하나로 바꿔 줍니다. hunter2 를 넣으면 $2b$12$ 로 시작하는 예순 글자짜리 문자열이 나오고, 저장하는 것은 이 문자열입니다.

이게 없으면 목록을 훔쳐 간 공격자가 흔한 비밀번호 후보를 초당 수없이 대입해 볼 수 있습니다. 계산 한 번을 무겁게 만들면 같은 장비로 훨씬 적은 후보밖에 못 넣습니다.

어떻게 도나:

  1. 사용자마다 다른 무작위 값인 솔트를 하나 뽑습니다
  2. 솔트와 비밀번호로 암호 계산을 준비하는 작업을 정해진 횟수만큼 되풀이합니다
  3. 그 상태로 정해진 값 하나를 암호화합니다. 솔트와 되풀이 횟수와 그 결과를 한 문자열에 담습니다

대가는 서버도 같은 비용을 치른다는 것입니다. 로그인 한 번마다 그 계산을 서버가 하므로, 되풀이 횟수를 올리면 로그인 응답도 그만큼 늦어집니다.

상세

bcrypt 는 비밀번호를 저장용 값으로 바꾸는 함수입니다. 해시 함수와 하는 일은 비슷합니다. 다른 점은 계산을 몇 번 되풀이할지를 밖에서 정해 넣을 수 있다는 것입니다. 그 되풀이 횟수를 정하는 숫자를 비용 인자라고 부릅니다.

비밀번호에 왜 오래 걸리는 계산이 필요한지부터 봅니다.

오래 걸리는 계산이 필요한 까닭

비밀번호를 적힌 그대로 저장하면 목록이 새는 순간 전부 털립니다. 그래서 되돌릴 수 없는 값으로 바꿔 저장합니다. 로그인할 때마다 같은 계산을 다시 해서 견줍니다.

문제는 해시 함수가 계산이 빨리 끝나도록 만든 물건이라는 점입니다. 공격자도 그 속도를 그대로 누립니다. 흔한 비밀번호 목록을 놓고 후보를 하나씩 계산해 견주면 됩니다. 한 번이 싸면 백만 번도 쌉니다.

솔트는 이 문제를 풀지 못합니다. 솔트는 사용자마다 다른 무작위 값을 함께 넣어 미리 계산해 둔 표를 못 쓰게 만드는 장치입니다. 표를 못 쓰게 할 뿐, 그 사람의 솔트를 넣어 후보를 하나씩 대입하는 계산은 여전히 금방 끝납니다.

남은 길은 계산 한 번에 드는 시간을 키우는 것입니다. 한 번 맞춰 보는 데 오래 걸리게 하면 같은 시간에 넣어 볼 수 있는 후보 수가 그만큼 줄어듭니다. 이렇게 일부러 계산을 무겁게 만드는 일을 키 스트레칭이라고 부르고, bcrypt 는 그 장치를 함수 안에 넣어 둔 것입니다.

입력과 출력

bcrypt 가 받는 것은 셋입니다. 비밀번호, 사용자마다 다른 솔트, 그리고 계산을 얼마나 되풀이할지 정하는 비용 인자입니다.

내놓는 것은 문자열 하나입니다. 솔트와 비용 인자와 계산 결과를 열 세 개에 나눠 담지 않아도 되도록, 셋을 한 문자열에 같이 적어 둡니다.

아래는 파이썬 bcrypt 라이브러리로 비밀번호 하나를 값으로 만들고, 그 값으로 다시 확인해 본 것입니다.

Python
h = hashpw("hunter2", gensalt(cost=12))
h              # "$2b$12$..." · 60자
checkpw("hunter2", h)   # 맞다
checkpw("hunter3", h)   # 아니다

만드는 함수에는 솔트와 비용 인자를 넘겼지만, 확인하는 함수에는 비밀번호와 저장된 문자열만 넘겼습니다. 확인에 필요한 조건이 그 문자열 안에 이미 들어 있기 때문입니다.

그 문자열은 네 조각으로 나뉩니다.

조각 담는 것
맨 앞 $2b$ 이 값을 만든 bcrypt 의 갈래
이어지는 12$ 비용 인자
다음 22자 솔트
나머지 31자 계산 결과인 해시값

열 하나에 이 문자열만 넣어 두면 검증에 필요한 것이 다 있습니다.

비용 인자

비용 인자는 되풀이 횟수를 그대로 적는 숫자가 아닙니다. 2를 그 숫자만큼 곱한 값이 되풀이 횟수가 됩니다. 비용 인자가 12 면 2를 열두 번 곱한 4,096 번입니다.

그래서 숫자를 하나 올리면 계산 시간이 두 배가 됩니다. 둘 올리면 네 배입니다. 장비가 좋아져 공격자의 시도 횟수가 늘면 이 숫자 하나를 올려 따라갑니다.

비용 인자 되풀이 횟수
10 1,024
12 4,096
14 16,384

bcrypt 의 계산량을 정하는 것은 이 숫자 하나뿐입니다. 입력이 길든 짧든 드는 시간은 거의 같습니다.

고르는 기준은 서버가 감당할 로그인 응답 시간입니다. 숫자를 올릴수록 공격자의 비용이 커지지만, 로그인마다 같은 계산을 하는 서버의 비용도 같이 커집니다. 로그인이 몰리는 서비스에서는 이 숫자가 곧 서버 용량 문제가 됩니다.

안에서 도는 계산

bcrypt 의 안쪽은 블록 암호에서 왔습니다. 블록 암호는 정해진 길이의 데이터 덩이를 키로 뒤섞는 함수입니다. bcrypt 가 가져다 쓴 블록 암호는 Blowfish 입니다.

블록 암호는 뒤섞기 전에 키를 풀어 내부 표를 채우는 준비 작업을 합니다. 이 준비 작업을 키 스케줄이라고 부릅니다. 앞에서 「암호 계산을 준비하는 작업」이라고 한 것이 이것입니다.

bcrypt 가 손댄 곳이 그 준비 작업입니다. 키 스케줄을 한 번만 하지 않습니다. 솔트와 비밀번호를 번갈아 먹이며 비용 인자가 정한 횟수만큼 되풀이합니다.

되풀이가 끝나면 그 상태로 정해진 값 하나를 여러 번 암호화합니다. 그 값은 bcrypt 가 붙박이로 들고 있는 짧은 문자열입니다. 언제나 같은 값이라, 결과를 가르는 것은 앞의 되풀이가 만든 상태뿐입니다. 마지막에 나온 암호문이 해시값입니다.

flowchart TD
    S["솔트 · 비밀번호"] --> K["키 스케줄"]
    K --> R["비용 인자가 정한 횟수만큼 되풀이"]
    R --> K
    R --> E["정해진 값을 암호화"]
    E --> H["해시값"]

그림에서 볼 것은 되풀이가 준비 작업에 걸려 있다는 점입니다. 암호화는 맨 끝에 한 번뿐입니다. 비용은 거의 전부 키 스케줄을 되풀이하는 데서 나옵니다.

이 준비 작업은 내부 표를 채우느라 메모리를 몇 킬로바이트 씁니다. 그래서 같은 계산을 수천 개씩 동시에 돌리는 그래픽 카드에서는 이득이 덜 납니다. 표를 계산 단위마다 따로 둬야 해서 한 장비에 밀어 넣을 수 있는 계산 수가 줄기 때문입니다.

다만 몇 킬로바이트는 큰 값이 아닙니다. 메모리를 훨씬 많이 쓰도록 설계한 scrypt나 Argon2에 비해 bcrypt 는 전용 장비를 짜서 공격하기가 덜 까다롭습니다. 새로 고를 때 그 둘이 먼저 거론되는 까닭이 여기 있습니다.

입력 길이 한계

bcrypt 는 비밀번호를 72바이트까지만 씁니다. 그보다 긴 입력은 뒤가 잘려 나가 결과에 영향을 주지 않습니다.

한글 한 글자를 몇 바이트로 적느냐는 문자 인코딩이 정합니다. 널리 쓰는 UTF-8(Unicode Transformation Format 8-bit, 유니코드 변환 형식 8비트)은 한글 한 글자를 3바이트로 적습니다. 그러니 한글로만 지은 비밀번호는 스물네 글자쯤에서 이 한계에 닿습니다.

이 한계는 조용합니다. 오류를 내지 않고 그냥 잘립니다. 긴 문구를 비밀번호로 받는 서비스라면 앞부분이 같은 두 문구가 같은 값으로 저장될 수 있습니다.

검증하는 순서

로그인할 때 서버가 하는 일은 저장된 문자열에서 조건을 꺼내는 것으로 시작합니다. 앞에서 본 네 조각이 각각 어디로 흘러드는지를 그리면 이렇습니다.

flowchart TD
    S["저장된 문자열"] --> V["갈래 · 비용 인자"]
    S --> T["솔트"]
    S --> H["저장된 해시값"]
    V --> C["받은 비밀번호로 같은 계산을 다시 한다"]
    T --> C
    C --> R["새로 나온 값"]
    R --> X["견준다"]
    H --> X

갈래와 비용 인자와 솔트는 계산을 다시 하는 데 쓰이고, 해시값만 끝에서 견주는 데 쓰입니다.

되돌리는 계산은 없습니다. 저장된 값에서 비밀번호로 돌아가는 길이 마련되어 있지 않으니, 같은 조건으로 다시 계산해 견주는 것이 유일한 확인 방법입니다.

견줄 때는 두 값이 어긋나는 곳에서 바로 멈추지 않고 끝까지 훑는 비교 함수를 씁니다. 언제 멈췄는지를 재서 앞부분이 맞았는지 알아내는 공격이 있기 때문입니다. 이런 공격을 타이밍 공격이라고 부릅니다.

쓰는 곳과 안 쓰는 곳

비밀번호를 다루는 곳마다 bcrypt 를 써도 되는지 따져 봅니다. 가르는 기준은 하나입니다. 사람이 고른 비밀번호를 놓고 누가 후보를 대입해 볼 수 있는 상황인가입니다.

쓰임 bcrypt 로 괜찮나
사용자 비밀번호 저장 됩니다
이미 bcrypt 로 저장해 온 서비스 됩니다. 비용 인자만 올려 갑니다
새로 고르는 경우 Argon2·scrypt 처럼 메모리를 많이 쓰는 함수가 먼저 거론됩니다
긴 문구를 비밀번호로 받는 곳 72바이트 한계를 먼저 따집니다
파일이 깨졌는지 확인 아닙니다. 그 일은 해시 함수가 합니다
암호화 키 만들기 아닙니다. 원하는 길이의 키를 뽑는 함수를 씁니다

후보를 대입해 볼 수 있는 상황이 아니면 계산을 무겁게 할 이유가 없습니다.

관련 항목

bcrypt 가 속하는 상위 분류

비밀번호 해싱 · 키 유도 함수 · 해시 함수 · 암호학적 해시 함수 · 해시

bcrypt 를 대신할 수 있는 다른 함수

Argon2 · Argon2id · scrypt · PBKDF2 · yescrypt

bcrypt 가 입력으로 함께 받는 값

솔트 · 페퍼 · 비용 인자 · 키 스트레칭

비밀번호를 알아내려고 쓰는 공격

레인보우 테이블 · 무차별 대입 공격 · 사전 공격 · 크리덴셜 스터핑 · 타이밍 공격

bcrypt 의 계산 비용을 좌우하는 장치와 장비

Blowfish · 블록 암호 · 키 스케줄 · GPU · ASIC

bcrypt 값으로 사람을 들여보내는 절차

인증과 인가 · 세션 · 액세스 토큰 · 비밀번호 재설정 · 다중 인증

비밀번호 저장에 쓰면 안 되는 범용 해시 함수

SHA-1 · SHA-256 · MD5 · HMAC · 체크섬

다른 이름: 비크립트