비밀번호 해싱
고친 사람 github-actions[bot]
비밀번호 해싱은 사용자가 정한 비밀번호를 되돌릴 수 없는 값으로 바꿔 저장하는 일입니다. 저장해 둔 값이 새어 나가도 공격자가 원래 비밀번호를 곧바로 읽어 내지 못합니다. 이 일에 쓰는 함수는 한 번 계산하는 데 일부러 오래 걸리게 만들어 둡니다.
쉽고 빠른 이해
비밀번호 해싱은 비밀번호를 적힌 그대로 두지 않고, 되돌릴 수 없는 값으로 바꿔 저장하는 일입니다. 로그인할 때는 입력받은 비밀번호로 같은 계산을 다시 해서 저장해 둔 값과 맞춰 봅니다.
이렇게 해 두지 않으면 데이터베이스가 한 번 새는 순간 모든 사용자의 비밀번호가 남의 손에 넘어갑니다. 사람들은 같은 비밀번호를 여러 서비스에 쓰기 때문에 피해가 그 서비스에서 끝나지 않습니다.
어떻게 도나:
- 사용자마다 다른 임의 값인 솔트를 하나 만듭니다
- 비밀번호와 솔트를 함께 넣어 일부러 오래 걸리는 함수로 값을 계산합니다
- 솔트와 해시값을 함께 저장하고, 로그인 때 같은 계산을 다시 해서 맞춰 봅니다
대가는 로그인할 때마다 서버가 그 계산을 한 번씩 치른다는 것입니다. 그리고 비밀번호 자체가 짧고 흔하면 이 방법으로도 못 막습니다.
상세
열쇠를 맡기는 대신 자물쇠만 맡아 두는 셈입니다. 자물쇠를 아무리 들여다봐도 열쇠 모양은 알 수 없습니다. 맞는 열쇠를 꽂아 봐야만 열리는지 아닌지 알 수 있습니다. 게다가 이 자물쇠는 열쇠 하나를 꽂아 보는 데 시간이 걸리도록 일부러 뻑뻑하게 만들어 두었습니다.
비밀번호 해싱은 비밀번호를 해시 함수에 넣어 얻은 값만 저장하고 비밀번호 자체는 버리는 방법입니다. 해시 함수는 어떤 입력을 넣어도 정해진 길이의 값을 내놓습니다. 그 값만 보고는 원래 입력을 되짚어 갈 수 없게 만든 함수입니다. 되짚어 갈 수 없는 이 성질을 역상 저항성이라고 부릅니다.
다만 아무 해시 함수나 쓰지는 않습니다. 비밀번호 해싱에 쓰는 함수는 한 번 계산하는 비용이 크도록 설계되어 있습니다. 그 비용은 운영자가 조절할 수 있습니다. 이 두 가지가 다른 해시 쓰임과 갈리는 지점입니다.
적힌 그대로 저장했을 때 새는 것
비밀번호를 적힌 그대로 저장하면, 그 표를 읽을 수 있는 사람은 전부 모든 사용자의 비밀번호를 알게 됩니다. 백업 파일 하나나 실수로 남은 로그 한 줄이면 충분합니다.
피해는 그 서비스에서 끝나지 않습니다. 사람들은 같은 비밀번호를 여러 곳에 쓰기 때문에, 한 곳에서 새어 나간 목록을 다른 서비스에 그대로 넣어 보는 공격이 성립합니다. 이 공격을 크리덴셜 스터핑이라고 부릅니다.
되돌릴 수 없는 값만 저장해 두면 표를 손에 넣은 쪽도 값만 얻습니다. 그 값에서 비밀번호를 계산해 내는 길이 없으므로, 공격자는 후보를 하나씩 넣어 보며 맞춰 보는 수밖에 없습니다.
일반 해시 함수로는 모자란 이유
SHA-256(Secure Hash Algorithm 256, 보안 해시 알고리즘 256)처럼 널리 쓰이는 해시 함수는 큰 파일의 무결성을 확인하려고 만든 것입니다. 그래서 한 번 계산하는 데 드는 시간이 아주 짧도록 설계되어 있습니다. 그 짧은 시간이 비밀번호 저장에서는 거꾸로 공격자를 돕습니다.
후보를 하나씩 넣어 보는 쪽은 장비를 늘려 같은 계산을 동시에 돌립니다. 그래픽 카드는 같은 계산을 수천 갈래로 한꺼번에 돌리게 만든 장치입니다. GPU(Graphics Processing Unit, 그래픽 처리 장치) 한 장이면 흔한 비밀번호 목록 전체를 짧은 시간에 훑습니다. 이 계산만 하려고 만든 전용 칩인 ASIC(Application-Specific Integrated Circuit, 주문형 반도체)을 쓰면 격차가 더 벌어집니다.
계산을 아예 미리 해 두는 방법도 있습니다. 흔한 비밀번호와 그 해시값을 쌍으로 적어 둔 표를 레인보우 테이블이라고 합니다. 표만 손에 있으면 계산 없이 찾아보기만 하면 됩니다.
비밀번호 해싱은 이 둘을 각각 다른 방법으로 막습니다. 한 번 계산하는 비용을 올려 후보를 넣어 보는 속도를 떨어뜨립니다. 그리고 사용자마다 다르게 만드는 임의 값인 솔트를 비밀번호와 함께 섞어, 미리 만들어 둔 표를 못 쓰게 만듭니다.
저장과 검사가 도는 순서
가입할 때와 로그인할 때 하는 계산은 같습니다. 다른 것은 나온 값을 저장하느냐 저장된 값과 맞춰 보느냐입니다.
sequenceDiagram
participant U as 사용자
participant S as 서버
participant D as 저장소
U->>S: 가입 · 비밀번호를 보낸다
S->>S: 솔트를 새로 만들어 함께 해싱한다
S->>D: 솔트와 해시값을 저장한다
U->>S: 로그인 · 비밀번호를 보낸다
S->>D: 저장해 둔 솔트와 해시값을 읽는다
S->>S: 읽어 온 솔트로 다시 해싱한다
Note over S: 두 값이 같으면 통과시킨다
서버가 하는 일은 두 값을 견주는 것뿐입니다. 저장소에는 비밀번호가 없습니다. 사용자가 비밀번호를 잊어도 알려 줄 방법이 없습니다. 새로 정하게 하는 수밖에 없습니다.
비밀번호를 찾아 주는 서비스가 있다면 그곳은 비밀번호를 되돌릴 수 있는 꼴로 갖고 있다는 뜻입니다.
맞춰 볼 때는 두 값을 끝까지 다 보고 나서 판정합니다. 앞 글자가 다르다고 곧바로 멈추면 걸린 시간이 정답에 얼마나 가까운지를 알려 주는 셈이 됩니다. 걸린 시간에서 정보를 캐내는 이런 공격을 타이밍 공격이라고 부릅니다.
계산을 무겁게 만드는 손잡이 셋
비용을 조절하는 손잡이가 셋 있습니다. 함수마다 쥐고 있는 손잡이가 다릅니다.
반복 횟수를 늘리는 것이 가장 단순한 방법입니다. 같은 계산을 정해진 횟수만큼 되풀이하면 한 번 확인하는 시간이 그만큼 늘어납니다. 공격자가 후보 하나를 확인하는 시간도 똑같이 늘어납니다.
메모리를 많이 쓰게 만드는 방법은 전용 칩을 겨냥합니다. 계산 도중에 큰 표를 만들어 두고 그 표를 여기저기 읽어야 값이 나오도록 설계하면, 계산 하나마다 그만큼의 메모리가 있어야 합니다. 칩 하나에 계산기를 수천 개 박아도 메모리가 모자라 그 수만큼 동시에 못 돌립니다.
병렬도는 계산 하나를 여러 코어에 나눠 맡기는 정도입니다. 서버가 가진 코어를 다 쓰면 사용자를 기다리게 하지 않으면서 더 많은 계산을 치를 수 있습니다.
손잡이를 얼마나 올릴지는 천장 둘이 정합니다. 하나는 로그인 한 번이 사용자를 기다리게 하지 않는 선이고, 다른 하나는 몰려드는 로그인을 서버가 한꺼번에 감당할 수 있는 선입니다.
둘 중 낮은 쪽이 실제 한계입니다. 하드웨어가 발전하면 공격자가 쓰는 장비도 같이 발전합니다. 그래서 이 값은 한 번 정하고 두는 것이 아니라 때때로 올려야 합니다.
솔트가 맡는 몫
솔트는 사용자마다 다르게 만드는 임의의 값입니다. 비밀번호와 함께 넣어 해싱합니다. 숨기지 않고 해시값 옆에 같이 저장합니다.
숨기지 않는데도 쓸모가 있는 까닭은 미리 계산해 둘 수 없게 만들기 때문입니다. 솔트가 사용자마다 다르면 공격자는 표 하나로 모든 사용자를 훑을 수 없습니다. 사용자 한 명마다 표를 새로 만들어야 합니다. 그러면 표를 미리 만들어 두는 이득이 사라집니다.
덤으로 같은 비밀번호를 쓴 두 사용자의 저장값이 서로 달라집니다. 솔트가 없으면 저장된 값만 나란히 놓고 봐도 누구와 누구의 비밀번호가 같은지 드러납니다.
솔트는 예측할 수 없는 난수로 만듭니다. 사용자 이름이나 가입 순번처럼 공격자가 미리 알 수 있는 값을 쓰면 표를 앞질러 만들 수 있게 되어 솔트가 하는 일이 사라집니다.
이 목적에 쓰는 함수
이름이 알려진 함수가 몇 개 있습니다. 어느 손잡이를 쥐고 있는지는 함수마다 다릅니다.
| 함수 | 비싸게 만드는 손잡이 |
|---|---|
| PBKDF2(Password-Based Key Derivation Function 2, 비밀번호 기반 키 유도 함수 2) | 반복 횟수 |
| bcrypt | 반복 횟수 |
| scrypt | 반복 횟수 · 메모리 |
| Argon2 | 반복 횟수 · 메모리 · 병렬도 |
넷 다 비밀번호와 솔트를 받습니다. 비용을 정하는 설정도 함께 받습니다.
이런 함수를 통틀어 키 유도 함수라고 부르기도 합니다. 원래는 비밀번호에서 암호 키를 뽑아내려고 만든 무리입니다. 같은 성질이 비밀번호 저장에도 그대로 쓰입니다.
저장할 때는 해시값만 적지 않습니다. 어떤 함수로 어떤 설정을 써서 계산했는지와 솔트를 해시값과 함께 한 문자열에 적어 둡니다. 함수 이름 · 비용 설정 · 솔트 · 해시값을 정해진 구분 기호로 이어 붙인 꼴입니다.
이렇게 적어 두면 나중에 설정을 올리거나 함수를 바꿔도 옛 값을 그대로 검사할 수 있습니다. 읽어 온 문자열이 어떤 설정으로 계산됐는지 스스로 말해 주기 때문입니다. 로그인이 성공한 순간에 새 설정으로 다시 계산해 덮어쓰면, 사용자에게 비밀번호를 다시 묻지 않고도 조금씩 옮겨 갈 수 있습니다.
비밀번호 해싱이 못 막는 것
흔한 비밀번호는 여전히 뚫립니다. 공격자가 자주 쓰이는 후보부터 넣어 보면, 계산이 비싸도 목록 앞쪽에 있는 비밀번호는 몇 번 만에 맞습니다. 비밀번호 자체가 짧거나 흔하면 이 방법이 벌어 주는 시간도 짧아집니다.
유출 자체를 막지도 않습니다. 저장된 값이 새는 것은 그대로 일어납니다. 비밀번호 해싱은 새어 나간 뒤에 시간을 버는 장치입니다. 그 시간 안에 운영자가 사용자에게 알리고 비밀번호를 새로 정하게 해야 합니다.
전달 구간도 따로입니다. 비밀번호는 브라우저에서 서버까지 읽을 수 있는 꼴로 흐릅니다. 그 구간은 TLS(Transport Layer Security, 전송 계층 보안) 같은 암호화 장치가 지킵니다.
비밀번호 하나에만 기대지 않는 방법도 있습니다. 다중 인증은 비밀번호 말고 다른 수단을 하나 더 거치게 합니다. 비밀번호가 뚫려도 한 겹이 더 남게 하는 장치입니다.
관련 항목
이 일에 쓰는 함수
bcrypt · scrypt · Argon2 · PBKDF2 · 키 유도 함수 · 키 스트레칭
이 일을 이루는 재료
해시 함수 · 암호학적 해시 함수 · 솔트 · 페퍼 · 난수 · 일방향 함수 · 역상 저항성 · SHA-256
저장된 해시를 노리는 공격
레인보우 테이블 · 무차별 대입 공격 · 사전 공격 · 크리덴셜 스터핑 · 타이밍 공격 · 데이터 유출
공격 비용을 좌우하는 계산 자원
GPU · ASIC · CPU · 메모리 · 병렬 처리
이 일이 놓이는 인증 절차
인증 · 비밀번호 · 로그인 · 세션 · 다중 인증 · 비밀번호 재설정
헷갈리는 이웃
다른 이름: password hashing · 패스워드 해싱 · 비밀번호 해시 · 비밀번호 저장 · password storage