새어 나갈 것을 전제로 저장한다
한 줄 요약
비밀번호는 되돌릴 수 없게, 느리게, 사용자마다 다른 솔트를 넣어 저장합니다. 표준 라이브러리의 hashlib.pbkdf2_hmac(60만 번 이상)이나 hashlib.scrypt(n=2^17, r=8 이상)를 쓰고, 비교는 hmac.compare_digest 로 합니다. 이미 쌓인 옛 저장값은 사용자가 로그인에 성공하는 순간 새 해시로 바꿉니다.
왜 이 요구가 필요한가
저장값은 언젠가 새어 나간다고 가정하고 설계합니다. 백업 파일, 잘못 열린 저장소, 개발용 사본이 흔한 경로입니다. 그때 무엇을 지켜야 하느냐가 이 모듈의 질문입니다.
SHA-256 은 빠르게 만들어진 함수입니다. 빠르다는 것은 저장값을 손에 넣은 쪽이 흔한 비밀번호 목록을 초당 수십억 번 넣어 볼 수 있다는 뜻입니다. 솔트가 없으면 같은 비밀번호는 같은 저장값이 되므로, 한 번 계산한 표를 모든 사용자에게 그대로 쓸 수 있고 '같은 비밀번호를 쓰는 사람들' 이 한눈에 드러납니다. 비밀번호 저장용 함수(KDF)는 일부러 느리고, 솔트를 받아 사용자마다 다른 값을 만듭니다. 우리 서버는 로그인 한 번에 0.3초 안팎을 쓰지만, 목록을 넣어 보는 쪽은 같은 시간을 비밀번호 하나하나에 써야 합니다.
어떻게 지키나
salt = os.urandom(16) # 해시할 때마다 새로
digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 600_000, dklen=32)
stored = "pbkdf2_sha256$600000$%s$%s" % (b64(salt), b64(digest))
저장 문자열에 알고리즘과 매개변수를 함께 적는 것이 핵심입니다. 그래야 확인할 때 저장값에서 매개변수를 읽어 다시 계산할 수 있고, 나중에 기준을 올려도 옛 값을 읽을 수 있습니다. 비용 기준은 OWASP Password Storage Cheat Sheet 를 따릅니다. 이 문서는 Argon2id 를 먼저 권하고, 그것이 없을 때 scrypt(최소 n=2^17, r=8, p=1), FIPS 가 필요하면 PBKDF2-HMAC-SHA256(60만 번)을 권합니다. 실습 이미지에는 Argon2 라이브러리가 없어 표준 라이브러리의 두 가지를 씁니다.
비교는 hmac.compare_digest 로 합니다. 파이썬 문서는 이 함수가 내용에 따라 일찍 끝나는 비교를 피해 시간 분석을 막도록 설계됐다고 적습니다. 솔직히 말하면 비밀번호 해시의 비교에서 시간 차이로 얻을 수 있는 것은 많지 않습니다. 공격하는 쪽이 비교되는 해시의 바이트를 고를 수 없기 때문입니다. 그래도 같은 함수를 쓰는 습관이 중요합니다. 토큰과 서명을 비교할 때는 그 차이가 실제로 문제가 되고, 그 코드도 같은 손에서 나옵니다.
옛 저장값을 어떻게 바꿀까요. 평문 비밀번호를 손에 쥐는 순간은 로그인할 때뿐입니다. 그래서 로그인에 성공했고 저장값이 낡았으면(옛 SHA-256 이거나 비용이 기준보다 낮으면) 그 자리에서 새로 만들어 저장합니다. 실패한 로그인은 저장값을 건드리지 않고, 이미 기준을 채운 값은 다시 만들지 않습니다. 이 판단을 needs_rehash 한 곳에 두면 다음에 기준을 올릴 때 그 함수만 고치면 됩니다.
현장에서 만나는 모습
오래된 서비스일수록 저장값이 여러 세대로 섞여 있습니다. MD5 시절의 값, 솔트를 붙인 SHA-1, 반복 수가 낮은 PBKDF2 가 한 표에 함께 있는 일이 흔합니다. 모양을 알 수 없는 값을 만났을 때 예외를 내지 않고 False 를 돌려주는 것도 중요합니다. 예외가 로그인 화면의 500 이 되면 그 계정은 영영 로그인하지 못하고, 오류 메시지가 저장값의 모양을 드러내기도 합니다.
느린 해시는 서버에도 느립니다. 로그인 요청이 몰리면 CPU 를 먹으므로 요청 수 제한과 함께 두어야 하고, 시험에서도 해시를 몇 번만 부르게 설계합니다. 로그인 실패 기록에는 비밀번호도 저장값도 남기지 않고, 없는 이메일로 시도한 경우에는 입력한 이메일조차 남기지 않습니다. 이메일 칸에 비밀번호를 잘못 넣는 사람이 생각보다 많습니다.
English corner
- 「key derivation function (KDF)」 비밀번호에서 키나 저장값을 만드는 느린 함수입니다. PBKDF2, scrypt, Argon2 가 여기에 속합니다.
- 「work factor」 반복 수나 메모리처럼 계산 비용을 정하는 매개변수입니다. OWASP 는 PBKDF2-HMAC-SHA256 에 "a work factor of 600,000 or more" 를 적습니다.
- 「constant-time comparison」
hmac.compare_digest가 하는 비교입니다. 문서의 표현은 "avoiding content-based short circuiting behaviour" 입니다.
다음 실습에서 할 것
회원 저장소가 비밀번호의 SHA-256 을 그대로 저장하고 == 로 비교합니다. 요구 A(느린 해시와 새 솔트, 다시 계산하면 같은 값)와 요구 B(상수 시간 비교, 옛 값은 로그인 때 새 해시로)를 시험으로 쓰고 고친 뒤, 로그인 기록을 비밀값 없이 남깁니다.