목차
시리즈를 실무에서 가장 자주 만나는 자리로 내린다. 비밀번호 저장은 초중급이 제일 많이 틀리는 곳이고, 틀리면 사고가 크다.
비밀번호는 되읽을 필요가 없다
먼저 질문을 바꿔야 한다. “비밀번호를 어떻게 감출까”가 아니라 **“서버가 비밀번호를 되읽을 일이 있나”**다. 없다.
로그인은 사용자가 다시 입력한 값이 맞는지 확인하면 된다. 원래 비밀번호를 꺼내 볼 필요가 전혀 없다. 되읽을 필요가 없다면, 되읽을 수 있게 저장하는 것 자체가 위험이다. 그래서 감추는(암호화)가 아니라 **되돌릴 수 없는 해시**로 저장한다.
평문 저장은 재앙이다
비밀번호를 그대로 저장하면, DB가 한 번 새는 순간 모든 사용자의 비밀번호가 통째로 공격자 손에 간다. 문제는 거기서 안 끝난다 - 사람들은 같은 비밀번호를 여러 사이트에 재사용해서, 이 사이트의 유출이 사용자의 이메일·은행까지 번진다.
평문 저장은 논외다. 그런데 “그럼 암호화하면 되지”가 다음 함정이다.
암호화도 답이 아니다
암호화는 되돌릴 수 있다 - 열쇠만 있으면. 그리고 그 열쇠는 어디 있나? 비밀번호를 검증하는 그 서버에 있어야 한다. 서버가 털리면 공격자는 암호화된 비밀번호와 열쇠를 함께 가져가 전부 복호화한다.
되읽을 필요가 없는데 되읽을 수 있게 뒀다가, 그 능력이 그대로 공격자에게 넘어가는 것이다. 그래서 해시다 - 열쇠가 없으니 훔쳐 갈 열쇠도 없다.
그냥 해시도 뚫린다: 레인보우 테이블
그럼 SHA-256으로 해시해 저장하면 될까? 부족하다. 해시는 같은 입력이 늘 같은 값을 낸다는 성질이 여기선 약점이 된다.
공격자는 흔한 비밀번호 수억 개의 해시를 미리 계산해 표로 갖고 있다(레인보우 테이블). DB에서 훔친 해시를 그 표에서 찾기만 하면 원래 비밀번호가 나온다.
훔친 해시: ef92b778...
표에서 조회: ef92b778... → "password123" (되돌린 게 아니라 미리 계산해 둔 것)되돌린 게 아니다. 미리 다 계산해 두고 맞춰본 것이다. 같은 비밀번호가 같은 해시가 되니 통한다.
솔트를 섞는다
그래서 사용자마다 **무작위 값(솔트, salt)**을 비밀번호에 섞어 해시한다. 솔트는 비밀이 아니어서 해시 옆에 그냥 같이 저장한다.
사용자A: 해시( "password123" + 솔트A ) → 8f3a... (+ 솔트A 저장)
사용자B: 해시( "password123" + 솔트B ) → c1d9... (+ 솔트B 저장)효과가 둘이다. 첫째, 같은 비밀번호라도 솔트가 달라 해시가 달라진다 - 표에서 한 번에 못 찾는다. 둘째, 공격자는 사용자마다 표를 새로 만들어야 해서 미리 계산해 둔 표가 무용지물이 된다. 솔트는 공개돼도 안전을 준다 - 미리 계산을 못 하게 하는 게 목적이지 감추는 게 아니라서다.
빠른 해시가 오히려 약점이다
솔트를 섞어도 공격자는 훔친 솔트를 붙여 하나씩 대입할 수 있다(그 사용자만 노린다면). 이때 SHA-256의 장점이 단점이 된다 - 너무 빠르다. 요즘 하드웨어는 SHA-256을 초당 수십억 번 돌린다. 흔한 비밀번호라면 솔트가 있어도 금방 맞춰진다.
파일 무결성 검사엔 빠른 게 좋지만, 비밀번호엔 느린 게 좋다. 사용자는 로그인할 때 한 번만 계산하면 되지만, 공격자는 수억 번 계산해야 하니까. 한 번의 비용을 일부러 올리면 공격자 쪽이 훨씬 아프다.
그래서 일부러 느리게 만든다
비밀번호 전용 해시는 의도적으로 느리게 설계됐다 - bcrypt·scrypt·argon2다. 이들은 **작업 계수(cost factor)**를 갖는다. 계수를 올리면 계산이 배로 느려진다.
bcrypt cost 12 → 한 번 해시에 약 0.25초
사용자: 로그인에 0.25초 (괜찮다)
공격자: 초당 4번 (수십억 번이 아니라)하드웨어가 빨라지면 계수만 올려 느림을 유지한다. argon2는 한 걸음 더 가서 메모리도 많이 쓰게(메모리 하드) 만든다 - 병렬 전용 하드웨어로 밀어붙이는 공격까지 비싸게 한다. 그리고 이들은 솔트·계수를 결과 문자열에 함께 담아 내주므로, 따로 관리할 것도 줄어든다.
페퍼: 비밀 한 겹 더
솔트가 공개라면, 비밀인 값을 한 겹 더 섞기도 한다 - 페퍼(pepper). 솔트와 달리 페퍼는 DB가 아닌 다른 곳(앱 설정·키 관리 서비스)에 둔다.
그러면 DB만 털렸을 때 공격자에게 페퍼가 없어 대입이 막힌다. 솔트가 “미리 계산 방지”라면 페퍼는 “DB만 새는 흔한 유출에 대한 보험”이다. 선택 사항이고, 앞의 솔트 + 느린 해시가 먼저다.
실무에서: 고르는 것만 잘하면 된다
원리를 알았으니 실무 규칙이 왜 그런지 보인다.
- **argon2id(또는 bcrypt)**를 쓴다. 직접 조합하지 않는다 - 솔트·계수·검증이 다 안에 있다.
- 로그인 검증은 저장된 솔트로 다시 해시해 맞춰본다. 비교는 상수 시간으로(라이브러리가 해준다).
- 비밀번호 길이를 짧게 제한하지 않는다. 긴 암호구절이 더 강하다.
- 비밀번호는 어차피 약한 열쇠다. 그래서 궁극의 방어는 비밀번호에만 기대지 않는 것 - **다중 인증(MFA)**을 얹는다.
정리
- 서버는 비밀번호를 되읽을 필요가 없다. 그래서 암호화(되돌릴 수 있음)가 아니라 **해시(되돌릴 수 없음)**로 저장한다.
- 그냥 해시는 레인보우 테이블에 뚫린다 → 솔트로 미리 계산을 무력화한다.
- 솔트를 섞어도 빠른 해시는 대입이 빠르다 → 일부러 느린 해시(bcrypt·argon2, 작업 계수)로 공격 비용을 올린다.
- 실무는 argon2id/bcrypt를 골라 쓰고, 비밀번호에만 기대지 않고 MFA를 얹는다.
여기까지가 이 시리즈다. 되돌리기로 셋을 갈랐고(암호화·해시·인코딩), 감추기를 대칭·공개키·하이브리드로 풀었고, 무결성과 신원을 해시·HMAC·서명·인증서로 세웠고, 마지막에 그 모두를 비밀번호 저장이라는 한 자리에서 다시 만났다. 원리를 아는 목적은 직접 짜려는 게 아니라, 남이 만든 걸 옳게 고르고 쓰려는 것이다.