에듀테크 프로젝트 대시보드
비밀번호 저장과 해싱
서비스는 사용자의 비밀번호를 절대 원문 그대로 저장하면 안 된다.
DB가 유출돼도 비밀번호가 새어 나가지 않도록, 단방향 해시 함수로 바꿔 저장한다.
하지만 단순 해시만으로는 레인보우 테이블 공격에 뚫린다 —
그래서 salt(사용자별 무작위 값)와 slow hash(일부러 느린 해시)를 함께 쓴다.
아래에서 공격자의 입장이 되어 하나씩 깨 보고, 방어가 어떻게 작동하는지 확인해 보자.
| id | username | password |
|---|---|---|
| 1 | minji | apple1234 |
| 2 | junho | qwerty |
| 3 | 서연 | iloveyou |
| 4 | admin | P@ssw0rd! |
해시 함수는 어떤 입력이든 고정 길이의 값으로 바꾸고, 그 값에서 원문을 되돌릴 수 없다. 로그인할 때는 입력한 비밀번호를 다시 해시해서 저장된 해시와 같은지만 비교한다.
해시 → 원문 복구는 사실상 불가능. 그래서 "복호화"가 아니라 매번 다시 해시해 비교한다.
같은 입력이면 항상 같은 해시. 한 글자만 바뀌어도 결과는 완전히 달라진다(눈사태 효과).
입력이 1글자든 1MB든 출력 길이는 같다. 실무에선 SHA-256, bcrypt, Argon2 등을 쓴다.
hash()는 원리 설명용 장난감 함수다. 실제 비밀번호에는
MD5·SHA-1 같은 빠른 해시도 쓰면 안 되고, 아래에서 볼 salt + slow hash가 필수다.
공격자는 흔한 비밀번호 수억 개를 미리 해시해서 표로 만들어 둔다. 유출된 해시를 이 표에서 찾기만 하면 원문이 바로 나온다. 해시가 salt 없이 저장돼 있으면 순식간에 뚫린다.
| username | hash | 결과 |
|---|
비밀번호 뒤에 사용자별 무작위 문자열(salt)을 붙여서 해시한다. salt는 비밀이 아니라 해시 옆에 같이 저장한다. 그래도 공격자는 salt마다 표를 새로 만들어야 하므로 미리 만들어 둔 레인보우 테이블이 통째로 무력화된다. 아래 두 사용자는 비밀번호가 같지만 salt가 달라 해시가 전혀 다르다.
| username | salt | hash( password + salt ) |
|---|
salt를 써도 공격자는 특정 사용자를 노려 비밀번호를 하나씩 대입(brute force)할 수 있다.
bcrypt·Argon2는 해시를 수만~수십만 번 반복해 일부러 느리게 만든다.
로그인 1회는 체감되지 않는 0.2초지만, 공격자가 수십억 개를 시도하려면 수백 년이 걸린다.
컴퓨터가 빨라지면 work factor(cost)만 올리면 된다.
* 대략적인 추정치다. cost를 1 올리면 반복 횟수와 소요 시간이 2배가 된다(지수 증가).
bcrypt / scrypt / Argon2 사용 · 사용자별 랜덤 salt · 적절한 cost · HTTPS로 전송 · 최소 길이 안내.
평문 저장 · MD5·SHA-1·단일 SHA-256만 사용 · salt 없음 · salt를 모두에게 동일하게 · 비밀번호를 로그에 남기기.
서버 비밀값 pepper 추가 · 로그인 실패 rate limit · 2단계 인증(2FA) · 유출 비밀번호 목록 차단.
아니요. salt의 목적은 미리 계산된 표(레인보우 테이블)와 해시 재사용을 막는 것이지 비밀 유지가 아닙니다.
보통 해시 문자열 안에 같이 저장됩니다($2b$12$...). 대신 사용자마다 달라야 하고 충분히 무작위여야 합니다.
SHA-256은 너무 빠른 게 문제입니다. GPU로 초당 수십억 개를 계산할 수 있어서, 사람이 만든 짧은 비밀번호는 금방 뚫립니다. bcrypt/Argon2는 일부러 느리고 메모리를 많이 써서 대량 대입을 비싸게 만듭니다.
제대로 만든 서비스는 원문을 모릅니다. 그래서 "비밀번호 찾기"가 아니라 재설정 링크를 이메일로 보냅니다. 원문 비밀번호를 메일로 보내 주는 사이트가 있다면 평문이나 복호화 가능하게 저장한다는 뜻이니 위험합니다.
salt는 사용자별로 다르고 DB에 함께 저장됩니다. pepper는 모든 사용자에게 같은 값이지만 DB가 아니라 애플리케이션 코드/환경변수에 둡니다. DB만 유출되고 서버 코드는 안 털렸을 때 한 겹 더 막아 줍니다.