← Posts

Cryptography · Password · Hashing

비밀번호는 암호화하지 않는다 - 되읽을 게 아니라 맞춰볼 것이다

서버는 비밀번호를 되읽을 필요가 없다. 그래서 암호화가 아니라 해시로 저장한다. 솔트를 섞고, 일부러 느린 해시를 쓰는 이유까지.

목차
  1. 비밀번호는 되읽을 필요가 없다
  2. 평문 저장은 재앙이다
  3. 암호화도 답이 아니다
  4. 그냥 해시도 뚫린다: 레인보우 테이블
  5. 솔트를 섞는다
  6. 빠른 해시가 오히려 약점이다
  7. 그래서 일부러 느리게 만든다
  8. 페퍼: 비밀 한 겹 더
  9. 실무에서: 고르는 것만 잘하면 된다
  10. 정리

시리즈를 실무에서 가장 자주 만나는 자리로 내린다. 비밀번호 저장은 초중급이 제일 많이 틀리는 곳이고, 틀리면 사고가 크다.

비밀번호는 되읽을 필요가 없다

먼저 질문을 바꿔야 한다. “비밀번호를 어떻게 감출까”가 아니라 **“서버가 비밀번호를 되읽을 일이 있나”**다. 없다.

로그인은 사용자가 다시 입력한 값이 맞는지 확인하면 된다. 원래 비밀번호를 꺼내 볼 필요가 전혀 없다. 되읽을 필요가 없다면, 되읽을 수 있게 저장하는 것 자체가 위험이다. 그래서 감추는(암호화)가 아니라 **되돌릴 수 없는 해시**로 저장한다.

평문 저장은 재앙이다

비밀번호를 그대로 저장하면, DB가 한 번 새는 순간 모든 사용자의 비밀번호가 통째로 공격자 손에 간다. 문제는 거기서 안 끝난다 - 사람들은 같은 비밀번호를 여러 사이트에 재사용해서, 이 사이트의 유출이 사용자의 이메일·은행까지 번진다.

평문 저장은 논외다. 그런데 “그럼 암호화하면 되지”가 다음 함정이다.

암호화도 답이 아니다

암호화는 되돌릴 수 있다 - 열쇠만 있으면. 그리고 그 열쇠는 어디 있나? 비밀번호를 검증하는 그 서버에 있어야 한다. 서버가 털리면 공격자는 암호화된 비밀번호와 열쇠를 함께 가져가 전부 복호화한다.

되읽을 필요가 없는데 되읽을 수 있게 뒀다가, 그 능력이 그대로 공격자에게 넘어가는 것이다. 그래서 해시다 - 열쇠가 없으니 훔쳐 갈 열쇠도 없다.

그냥 해시도 뚫린다: 레인보우 테이블

그럼 SHA-256으로 해시해 저장하면 될까? 부족하다. 해시는 같은 입력이 늘 같은 값을 낸다는 성질이 여기선 약점이 된다.

공격자는 흔한 비밀번호 수억 개의 해시를 미리 계산해 표로 갖고 있다(레인보우 테이블). DB에서 훔친 해시를 그 표에서 찾기만 하면 원래 비밀번호가 나온다.

plaintext
훔친 해시:  ef92b778...
표에서 조회:  ef92b778...  →  "password123"   (되돌린 게 아니라 미리 계산해 둔 것)

되돌린 게 아니다. 미리 다 계산해 두고 맞춰본 것이다. 같은 비밀번호가 같은 해시가 되니 통한다.

솔트를 섞는다

그래서 사용자마다 **무작위 값(솔트, salt)**을 비밀번호에 섞어 해시한다. 솔트는 비밀이 아니어서 해시 옆에 그냥 같이 저장한다.

plaintext
사용자A:  해시( "password123" + 솔트A )  →  8f3a...  (+ 솔트A 저장)
사용자B:  해시( "password123" + 솔트B )  →  c1d9...  (+ 솔트B 저장)

효과가 둘이다. 첫째, 같은 비밀번호라도 솔트가 달라 해시가 달라진다 - 표에서 한 번에 못 찾는다. 둘째, 공격자는 사용자마다 표를 새로 만들어야 해서 미리 계산해 둔 표가 무용지물이 된다. 솔트는 공개돼도 안전을 준다 - 미리 계산을 못 하게 하는 게 목적이지 감추는 게 아니라서다.

빠른 해시가 오히려 약점이다

솔트를 섞어도 공격자는 훔친 솔트를 붙여 하나씩 대입할 수 있다(그 사용자만 노린다면). 이때 SHA-256의 장점이 단점이 된다 - 너무 빠르다. 요즘 하드웨어는 SHA-256을 초당 수십억 번 돌린다. 흔한 비밀번호라면 솔트가 있어도 금방 맞춰진다.

파일 무결성 검사엔 빠른 게 좋지만, 비밀번호엔 느린 게 좋다. 사용자는 로그인할 때 한 번만 계산하면 되지만, 공격자는 수억 번 계산해야 하니까. 한 번의 비용을 일부러 올리면 공격자 쪽이 훨씬 아프다.

그래서 일부러 느리게 만든다

비밀번호 전용 해시는 의도적으로 느리게 설계됐다 - bcrypt·scrypt·argon2다. 이들은 **작업 계수(cost factor)**를 갖는다. 계수를 올리면 계산이 배로 느려진다.

plaintext
bcrypt cost 12  →  한 번 해시에 약 0.25초
                   사용자: 로그인에 0.25초 (괜찮다)
                   공격자: 초당 4번 (수십억 번이 아니라)

하드웨어가 빨라지면 계수만 올려 느림을 유지한다. argon2는 한 걸음 더 가서 메모리도 많이 쓰게(메모리 하드) 만든다 - 병렬 전용 하드웨어로 밀어붙이는 공격까지 비싸게 한다. 그리고 이들은 솔트·계수를 결과 문자열에 함께 담아 내주므로, 따로 관리할 것도 줄어든다.

페퍼: 비밀 한 겹 더

솔트가 공개라면, 비밀인 값을 한 겹 더 섞기도 한다 - 페퍼(pepper). 솔트와 달리 페퍼는 DB가 아닌 다른 곳(앱 설정·키 관리 서비스)에 둔다.

그러면 DB만 털렸을 때 공격자에게 페퍼가 없어 대입이 막힌다. 솔트가 “미리 계산 방지”라면 페퍼는 “DB만 새는 흔한 유출에 대한 보험”이다. 선택 사항이고, 앞의 솔트 + 느린 해시가 먼저다.

실무에서: 고르는 것만 잘하면 된다

원리를 알았으니 실무 규칙이 왜 그런지 보인다.

  • **argon2id(또는 bcrypt)**를 쓴다. 직접 조합하지 않는다 - 솔트·계수·검증이 다 안에 있다.
  • 로그인 검증은 저장된 솔트로 다시 해시해 맞춰본다. 비교는 상수 시간으로(라이브러리가 해준다).
  • 비밀번호 길이를 짧게 제한하지 않는다. 긴 암호구절이 더 강하다.
  • 비밀번호는 어차피 약한 열쇠다. 그래서 궁극의 방어는 비밀번호에만 기대지 않는 것 - **다중 인증(MFA)**을 얹는다.

정리

  • 서버는 비밀번호를 되읽을 필요가 없다. 그래서 암호화(되돌릴 수 있음)가 아니라 **해시(되돌릴 수 없음)**로 저장한다.
  • 그냥 해시는 레인보우 테이블에 뚫린다 → 솔트로 미리 계산을 무력화한다.
  • 솔트를 섞어도 빠른 해시는 대입이 빠르다일부러 느린 해시(bcrypt·argon2, 작업 계수)로 공격 비용을 올린다.
  • 실무는 argon2id/bcrypt를 골라 쓰고, 비밀번호에만 기대지 않고 MFA를 얹는다.

여기까지가 이 시리즈다. 되돌리기로 셋을 갈랐고(암호화·해시·인코딩), 감추기를 대칭·공개키·하이브리드로 풀었고, 무결성과 신원을 해시·HMAC·서명·인증서로 세웠고, 마지막에 그 모두를 비밀번호 저장이라는 한 자리에서 다시 만났다. 원리를 아는 목적은 직접 짜려는 게 아니라, 남이 만든 걸 옳게 고르고 쓰려는 것이다.

관련 글