솔트
솔트는 비밀번호를 저장용 값으로 바꿀 때 함께 넣는 무작위 값입니다. 사람마다 다른 값을 넣으니 같은 비밀번호를 쓰는 두 사람도 서로 다른 값을 남깁니다. 솔트 자체는 비밀이 아닙니다. 바뀐 값 옆에 그대로 저장해 둡니다.
상세
김치를 똑같은 배합으로 담그면 어느 집에서 담가도 맛이 같아집니다. 집집마다 다른 젓갈을 한 숟갈씩 넣으면 배합이 같아도 나오는 맛이 전부 갈라집니다.
비유를 걷어내면 이렇습니다. 솔트는 같은 비밀번호를 서로 다른 저장값으로 갈라놓는 값입니다.
두 사람이 똑같이 hunter2 를 쓰더라도 한쪽에 a3f9…, 다른 쪽에 7c21… 이 붙으면 저장되는
값이 전혀 달라집니다. 훔쳐 간 목록만 봐서는 둘이 같은 비밀번호를 쓴다는 것조차 알 수 없습니다.
솔트는 비밀번호에서 키나 저장용 값을 만들어 낼 때 비밀번호와 함께 집어넣는 값입니다. 사용자마다
따로 뽑은 무작위 값을 씁니다. 고르는 일은 키 유도 함수(Key Derivation Function, 줄여서 KDF)가
합니다. 유도된 키를 DK, 비밀번호를 P, 솔트를 S 라고 하면 DK = KDF(P, S) 꼴입니다. 이 값은
감추지 않습니다. 바뀐 값과 나란히 저장해 둡니다.
flowchart TD
P["비밀번호"] --> K["키 유도 함수"]
S["솔트 · 사용자마다 다르다"] --> K
K --> DK["유도된 키"]
비밀번호 기반 암호의 표준 문서인 RFC(Request for Comments) 8018 은 솔트가 해 온 몫을 이렇게 적습니다. 주어진 비밀번호 하나에 대응하는 커다란 키 집합을 만들어 내고, 그중 하나가 솔트에 따라 무작위로 골라지게 하는 것입니다.
숫자로 풀면 뜻이 분명해집니다. 솔트가 없으면 hunter2 의 저장값은 하나뿐입니다. 솔트가
64비트면 hunter2 하나에 가능한 저장값이 2^64 개로 늘어나고, 실제로 쓰이는 것은 그중 그
사용자의 솔트가 지목하는 하나입니다. 이 "커다란 집합"이 RFC 가 말하는 키 집합입니다.
같은 문서는 여기서 얻는 이득을 둘로 나눠 적습니다.
첫째는 미리 계산을 막는 것입니다. 솔트가 없던 시절에는 흔한 비밀번호의 저장값을 한 번 계산해 표로 만들어 두면 그 표 한 벌로 계정 백만 개를 대조할 수 있었습니다. 솔트가 붙으면 그 표를 솔트마다 새로 만들어야 하는데, 64비트면 표가 2^64 벌 필요합니다. 만들 수 없는 양이죠. 그래서 공격하는 쪽에 남는 길은 목록을 훔쳐 솔트를 알아낸 뒤 그 솔트로 다시 계산하는 것뿐이고, 그 계산은 계정마다 따로 해야 합니다.
둘째는 솔트끼리 겹칠 걱정이 적다는 것입니다. 사용자마다 무작위로 뽑으니 언젠가는 같은 솔트가 두 번 나오는데, 그 "언젠가"가 생각보다 이릅니다. 생일 문제 때문입니다 — 무작위로 뽑은 값들 사이에서 첫 겹침이 나타나는 시점은 전체 가짓수가 아니라 대략 그 제곱근 근처입니다. 그래서 솔트가 64비트(2^64 가지)여도 안심할 수 있는 구간은 2^64 명이 아니라 2^32 명, 약 43억 명까지 입니다. 「경계」에서 볼 NIST 가 최소 128비트를 요구하는 이유가 여기 있습니다.
비밀번호로 메시지를 암호화하는 쪽은 이 이득이 실현된다는 확신을 간단히 얻을 수 있다고 적혀 있습니다. 비밀번호에서 암호화 키를 유도할 때 크고 충분히 무작위한 솔트를 고르는 것으로 됩니다.
배경
비밀번호를 그대로 저장하지 않고 되돌릴 수 없는 값으로 바꿔 두는 방식이 먼저 자리를 잡았습니다. 그런데 바꾸는 함수가 비밀번호만 입력으로 받으면 같은 비밀번호는 언제나 같은 값이 됩니다. 저장된 값 목록을 통째로 손에 넣은 쪽은 여기서 두 가지를 공짜로 얻습니다. 값이 같은 계정 둘은 같은 비밀번호를 쓴다는 것이 하나입니다. 흔한 비밀번호를 한 번씩 미리 계산해 표로 만들어 두면 그 표 한 벌로 목록 전체를 한꺼번에 대조할 수 있다는 것이 다른 하나입니다. 계정이 백만 개여도 계산은 한 벌이면 끝납니다.
필요한 것은 같은 비밀번호가 저장될 때마다 결과가 갈라지게 만드는 장치였습니다. 함수 자체를 계정마다 다르게 만들 수는 없습니다. 그래서 입력 쪽에 계정마다 다른 값을 하나 더 넣기로 합니다. 이 값이 비밀일 필요는 없습니다. 미리 계산한 표를 못 쓰게 만드는 데는 값이 서로 다르기만 하면 됩니다. 대신 검증할 때 같은 값을 다시 넣어야 하므로 결과와 나란히 저장해 둡니다.
이렇게 입력에 덧붙이는 값이 솔트입니다. RFC 8018 이 첫 번째 이득으로 적은 것이 바로 미리 계산의 차단입니다. 공격하는 쪽에 남는 길은 솔트를 알아낸 뒤 그 솔트에 대해 다시 계산하는 것뿐이고, 그 계산은 계정마다 따로 해야 합니다.
예시
PBKDF2 의 시그니처
RFC 8018 이 정의하는 PBKDF2(Password-Based Key Derivation Function 2)는 유사난수 함수를 적용해 키를 유도합니다. 유도된 키의 길이는 사실상 제한이 없습니다. 문서는 새 응용에 이것을 권합니다.
PBKDF2 (P, S, c, dkLen)
P 비밀번호, 옥텟 문자열
S 솔트, 옥텟 문자열
c 반복 횟수, 양의 정수
dkLen 유도할 키의 옥텟 길이, 양의 정수. 최대 (2^32 - 1) * hLen
DK 유도된 키, dkLen 옥텟짜리 문자열
솔트는 두 번째 자리 인자로 시그니처에 박혀 있습니다. hLen 은 밑에 깔린 유사난수 함수가
내놓는 출력의 옥텟 길이입니다.
Python 표준 라이브러리의 호출 한 줄
>>> from hashlib import pbkdf2_hmac
>>> our_app_iters = 500_000 # Application specific, read above.
>>> dk = pbkdf2_hmac('sha256', b'password', b'bad salt' * 2, our_app_iters)
>>> dk.hex()
'15530bba69924174860db778f2c6f8104d3aaf9d26241840c8c4a641c8d000a9'
hashlib.pbkdf2_hmac(hash_name, password, salt, iterations, dklen=None) 은
PKCS(Public-Key Cryptography Standards #5)의 비밀번호 기반 키 유도 함수 2를 제공합니다.
유사난수 함수로는 HMAC(Hash-based Message Authentication Code)을 씁니다. password 와
salt 는 바이트 버퍼로 해석됩니다. 공식 문서는 salt 를 제대로 된 출처에서 얻은 16바이트
이상으로 두라고 적고, 그 출처의 예로 os.urandom() 을 듭니다. 반복 횟수는 해시 알고리즘과
계산 능력을 보고 정하라고 적습니다. 2022년 기준으로 수십만 번이 제안된다고 덧붙입니다.
경계
솔트를 비밀로 지켜야 하는가. 아닙니다.
미국 국립표준기술연구소인 NIST(National Institute of Standards and Technology)의 SP 800-132 는 용어 정의에서 솔트를 비밀이 아닌 이진 값으로 못 박습니다. 주어진 비밀번호에 대해 커다란 키 집합이 생기게 하려고 키 유도 함수의 입력으로 쓰는 값이라고 적습니다. 솔트에 걸리는 요구는 비밀 유지가 아니라 무작위성과 길이입니다. 같은 문서의 「The Salt (S)」 절은 솔트의 전부 또는 일부를 승인된 난수 발생기로 생성해야 하고, 무작위로 생성된 부분의 길이가 최소 128비트여야 한다고 적습니다.
검증자만 알고 있는 비밀 값도 같은 자리인가. 아닙니다. NIST SP 800-63B 는 검증자가 기억된 비밀을 적절한 단방향 키 유도 함수로 솔트와 함께 해시해 저장해야 한다고 적습니다. 그 솔트는 최소 32비트 이상이어야 하고, 저장된 해시들 사이에서 솔트 값 충돌이 최소화되도록 임의로 골라야 합니다. 솔트 값과 그 결과 해시는 둘 다 가입자마다 저장합니다. 그리고 그와 별개로, 검증자만 알고 있는 비밀 솔트 값을 써서 키 유도 함수를 한 번 더 반복하는 것을 권합니다. 이 비밀 솔트 값은 해시된 기억된 비밀과 분리해 하드웨어 보안 모듈 같은 전용 장치에 저장해야 합니다. 나란히 저장하는 값과 따로 떼어 보관하는 값은 이렇게 갈립니다.
암호화 쪽에서 만나는 nonce 는 솔트인가. 아닙니다. RFC 5116 은 인증 암호화 연산의 입력을 넷으로 둡니다. 비밀 키 K, nonce N, 평문 P, 연관 데이터 A 입니다. nonce 에 걸리는 요구는 어떤 키 값에 대해서든 서로 다른 호출마다 값이 달라야 한다는 것입니다. 무작위여야 한다는 요구가 아니라 되풀이되지 않아야 한다는 요구입니다. 들어가는 자리도 다릅니다. nonce 는 암호화 연산의 입력이고 솔트는 키를 유도하는 함수의 입력입니다.
관련 항목
솔트를 채택한 비밀번호 해싱 함수
PBKDF2 · yescrypt · scrypt · bcrypt · Argon2 · Balloon · crypt(3)
솔트가 속하는 상위 분류
솔트로 착각하기 쉬운 값
페퍼 · nonce · 초기화 벡터
키 유도 함수 계산에 같이 쓰이는 값
반복 횟수 · 유사난수 함수 · HMAC · 난수 발생기 · 엔트로피 · 비용 인자
솔트가 막는 공격과 낮추는 충돌 확률
사전 공격 · 오프라인 공격 · 레인보우 테이블 · 무차별 대입 공격 · 해시 충돌 · 생일 문제
솔트를 정의하는 표준·문서
RFC 8018 · PKCS · NIST SP 800-132 · NIST SP 800-63B · RFC 5116
RFC 5116이 규정하는 인증 암호화의 다른 입력
비밀 키 · 평문 · 연관 데이터
다른 이름: salt · 솔팅 · salting