저장 형식을 복구 가능하게 만드는 요소
복구 가능성 순으로 정렬한 네 가지 저장 형식:
| 형식 | 복구 가능? | 예시 | 복구 방법 |
|---|---|---|---|
| 평문 | 예 | password: hunter2 | 파일 읽기 |
| Base64 | 예 | cGFzc3dvcmQ= | base64 --decode |
| Reversible cipher (AES) | 예 | ENC[AES256:...] | 키로 복호화 |
| One-way hash (bcrypt) | 아니요 | $2b$12$... | 역변환 불가; 무차별 대입 필요 |
평문, Base64, 가역 암호: 모두 복구 가능. 단일 자격 증명 데이터베이스 덤프만으로도 공격자는 모든 사용자의 비밀번호를 평문으로 한꺼번에 획득할 수 있습니다. 한 번의 유출로 전체 노출.
The Mailman 2.x Example
Mailman 2.x (GNU 메일링 리스트 관리자): 구독자 비밀번호를 평문으로 저장했습니다. 매월 비밀번호 알림 이메일은 모든 구독자에게 평문 비밀번호를 그대로 전송했습니다. 두 가지 별도의 결함 모두 MOAD-0006에 해당합니다:
1. 저장: 목록 데이터베이스에 평문으로 저장. 서버가 침해되면 모든 구독자의 비밀번호가 노출됩니다.
2. 전송: 매월 이메일로 평문 비밀번호를 구독자의 메일 서버로 SMTP를 통해 전송. 이메일은 여러 SMTP 홉을 거치며 평문으로 전송됩니다.
Mailman 팀은 두 가지 동작을 모두 설계했습니다. 복구(Recovery)는 의도된 기능이었습니다. 구독자는 잊어버린 비밀번호를 다시 찾을 수 있었습니다. Glass Safe라는 이름은 여기서 유래합니다. 금고는 자격 증명을 그대로 보이는 형태로 보관합니다. 금고에 접근할 수 있는 사람은 누구나 모든 내용을 한 번에 읽을 수 있습니다.
이미 도난당한 원칙(The Already-Stolen Principle)
복구 가능한 형태로 저장된 자격 증명은 이미 도난당한 자격 증명입니다. 공격자는 아직 도착하지 않았습니다. 침해는 아직 발생하지 않았습니다. 그러나 아키텍처는 보장합니다. 침해가 발생하면 모든 자격 증명이 동시에 유출됩니다. 침해는 개별적으로 발생하지 않습니다. 복구 가능한 저장소에 있는 모든 자격 증명은 동일한 작업으로 공격자에게 전달됩니다.
MOAD-0006 vs MOAD-0004
MOAD-0004 (Logged Secret): 자격 증명이 실수로 로그에 기록됩니다. 로그 기록은 의도가 아니었습니다. 디버깅을 위해 헤더 로깅을 활성화한 부작용이었습니다.
MOAD-0006 (Glass Safe): 자격 증명이 설계상 복구 가능한 형태로 저장됩니다. 복구가 의도였습니다. 비밀번호 알림 기능은 비밀번호 저장을 필요로 했습니다. 비밀번호 표시 기능도 비밀번호 저장을 필요로 했습니다. 복구를 위한 아키텍처적 결정이 결함을 만들었습니다.
한 줄 요약: MOAD-0004는 자격 증명을 실수로 로그에 남깁니다. MOAD-0006는 자격 증명을 의도적으로 복구 가능한 형태로 저장합니다. 수정은 서로 다른 계층에서 이루어집니다.
구조적 vs 우발적
MOAD-0006와 MOAD-0004 사이의 아키텍처적 차이가 수정 전략을 결정합니다. 우발적인 로그 기록: 직렬화 계층을 수정합니다. 복구를 위해 설계된 저장소: 복구가 필요한 기능을 재설계합니다.
bcrypt가 작동하는 이유
단방향 해시 함수는 비밀번호를 받아 고정 길이의 다이제스트를 생성합니다. 다이제스트가 주어지면 원래 비밀번호를 복구할 수 없습니다. '복구하기 어렵다'가 아니라, 역으로 되돌리는 것이 불가능합니다. 함수는 한 방향으로만 실행됩니다.
자격 증명 저장에 필요한 세 가지 속성:
1. 단방향성 (preimage resistance). hash(password)가 주어졌을 때, 무차별 대입보다 빠르게 password를 복구하는 알고리즘은 존재하지 않습니다. bcrypt, scrypt, argon2 모두 이 속성을 만족합니다.
2. 솔트(Salt). 해시하기 전에 비밀번호 앞에 추가되는 무작위 값입니다. 동일한 비밀번호라도 다른 솔트를 사용하면 다른 해시가 생성됩니다. 목적: 레인보우 테이블(미리 계산된 해시 사전)을 무력화하는 것입니다. 솔트가 없으면 공격자는 hash('password123')을 한 번만 계산해 100만 명의 사용자를 동시에 검사할 수 있습니다. 솔트가 있으면 동일한 비밀번호라도 각 사용자마다 고유한 해시가 생성됩니다.
3. 의도적 느림. bcrypt는 작업 계수(work factor)를 받습니다. 작업 계수가 높을수록 반복 횟수가 늘어나 해시당 계산 시간이 증가합니다. 로그인 시: 한 번 해시하는 데 300ms가 걸립니다. 이는 허용 가능합니다. 무차별 대입 시: 시도당 300ms가 걸립니다. 10억 번 시도하면 비밀번호당 9.5년이 소요됩니다. 공격자에게는 용납할 수 없는 시간입니다. 이 느림은 결함이 아니라 기능입니다.
import bcrypt
# 저장: 솔트가 포함된 단방향 해시
def store_password(plaintext: str) -> bytes:
return bcrypt.hashpw(plaintext.encode(), bcrypt.gensalt(rounds=12))
# 검증: 후보 비밀번호를 해시한 후 다이제스트 비교
def verify_password(plaintext: str, stored_hash: bytes) -> bool:
return bcrypt.checkpw(plaintext.encode(), stored_hash)
# 절대 저장하지 말 것: 평문 비밀번호
# 절대 복구 불가: 해시에서 평문 복원 불가능
# 비밀번호 재설정, 비밀번호 알림 아님
트레이드오프
일방향 해싱은 비밀번호 복구를 불가능하게 만듭니다. 비밀번호를 잊은 사용자는 이를 되찾을 수 없습니다. 비밀번호 알림 이메일은 존재할 수 없습니다. 사용자 경험은 다음과 같이 바뀝니다: '비밀번호를 잊으셨나요? 재설정하세요.' 이는 성능 저하가 아니라 보안 경계입니다. 비밀번호를 복구할 수 없는 시스템은 비밀번호를 유출할 수도 없습니다.
bcrypt 해시가 노출된 데이터베이스 유출: 모든 해시는 보이지만, 비밀번호는 보이지 않습니다. 공격자는 각 해시를 개별적으로 무차별 대입해야 하며, 시도당 300ms가 소요되고, 사용자별 솔트로 인해 미리 계산된 테이블이 무력화됩니다. 평문 비밀번호가 노출된 유출: 즉각적인 전체 노출.
강력한 암호화만으로는 충분하지 않음
보안 감사에서 자격 증명 저장 시스템을 확인했습니다. 비밀번호는 서버 측 키를 사용한 AES-256-CBC 암호화로 저장되어 있습니다. 감사 보고서는 이를 Glass Safe 결함으로 분류했습니다.
엔지니어링 팀이 응답합니다: 'AES-256은 가장 강력한 대칭 암호화 방식입니다. 키는 하드웨어 보안 모듈에 저장됩니다. 어떤 공격자도 이 비밀번호를 복호화할 수 없습니다.'