노트

비밀번호 저장: 인코딩·암호화·해싱

Password Storage: Encoding, Encryption, and Hashing

백엔드#security · 연결된 개념 6개

쉽게 말하면

비밀번호 해싱은 고기를 갈아서 보관하는 것과 같아요. 간 고기로 원래 덩어리를 되돌릴 순 없지만, 같은 고기를 같은 방식으로 갈면 같은 결과가 나와서 맞는지는 확인할 수 있죠.

비유가 깨지는 곳 그냥 빨리 가는 건 위험해요. 유출되면 후보를 초당 수십억 개 갈아 볼 수 있으니 일부러 느린 BCrypt·Argon2를 쓰고, 사용자마다 솔트를 섞어 같은 비밀번호도 해시가 다르게 나오게 해요.

비밀번호는 원문으로 저장하지 않고, 느린 단방향 해시(one-way hash)로 바꿔 저장한다. 로그인할 때는 입력값을 같은 방식으로 해시해 저장된 값과 비교한다. 인코딩·암호화·해싱은 비슷해 보이지만 목적이 다르다.

인코딩암호화해싱
목적형식 변환비밀 유지동일성 확인
되돌리기누구나 가능키가 있으면 가능불가능(단방향)
예Base64, UTF-8AES(Advanced Encryption Standard), RSASHA-256(Secure Hash Algorithm 256), BCrypt
비밀번호에부적합부적합(키가 새면 전부 노출)적합

왜 "느린" 해시인가

SHA-256 같은 범용 해시는 빠르다. 그래서 DB가 유출되면 공격자가 초당 수십억 개의 후보를 대입해 볼 수 있다. 비밀번호 전용 해시는 일부러 느리고 조절 가능하다.

  • 솔트(salt): 사용자마다 무작위 값을 섞어, 같은 비밀번호라도 해시가 다르게 나온다. 미리 계산한 표(레인보 테이블, rainbow table)를 무력화한다
  • 작업 계수(work factor, cost): 반복 횟수·메모리 사용량을 올려 하드웨어가 빨라져도 대입 비용을 유지한다
  • 대표 알고리즘: BCrypt, PBKDF2(Password-Based Key Derivation Function 2), scrypt, Argon2. OWASP는 Argon2id를 먼저 권하고, 쓸 수 없으면 scrypt, 레거시 시스템에는 BCrypt, FIPS(Federal Information Processing Standards) 준수가 필요하면 PBKDF2를 권한다(2026 기준)

프레임워크 기본값

  • 스프링 시큐리티: PasswordEncoder 인터페이스. BCryptPasswordEncoder가 널리 쓰이고, DelegatingPasswordEncoder는 {bcrypt}...처럼 알고리즘 이름을 앞에 붙여 나중에 알고리즘을 바꿀 수 있게 한다. NoOpPasswordEncoder는 평문이라 테스트 외에는 쓰지 않는다
  • Django: 기본은 PBKDF2이고 PASSWORD_HASHERS 설정으로 Argon2·BCrypt로 바꿀 수 있다. 저장 형식에 알고리즘·반복 횟수·솔트가 함께 들어 있어, 로그인할 때 자동으로 더 강한 설정으로 다시 해시한다
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String stored = encoder.encode(rawPassword);          // {bcrypt}$2a$10$...
boolean ok = encoder.matches(rawPassword, stored);

해시 비교에는 문자열 == 대신 라이브러리가 주는 비교 함수를 쓴다(타이밍 공격(timing attack) 방지). 비밀번호 확인은 인증의 한 단계이고, 스프링 쪽 흐름은 책 9장. 보안의 기초 — 필터체인에서 다룬다. 해시 함수 일반은 해시 테이블을 본다.

출처: OWASP Password Storage Cheat Sheet: Salting · Work Factors · Password Hashing Algorithms · Django 문서: How Django stores passwords

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • XSS

    XSS(Cross-Site Scripting)는 공격자가 넣은 스크립트가 내 사이트의 출처(origin) 권한으로 사용자 브라우저에서 실행되는 공격이다. 같은 출처의 코드로 돌기 때문에 same-origin-policy가 막아 주지 못한다. 그 스크립트는 페이지를 바꾸고, 로그인한 사용자 행세를 하며 요청을 보내고, JS가 읽을 수 있는 데이터(localStorage의 토큰 등)를 빼 갈 수 있다.

  • localStorage와 sessionStorage

    브라우저에 문자열 키-값을 저장하는 Web Storage API의 두 가지. 쿠키와 달리 요청에 자동으로 실리지 않고, 출처 단위로 나뉘며(same-origin-policy), 보통 출처당 5MB 안팎을 쓸 수 있다. 동기 API라 큰 데이터를 자주 읽고 쓰면 메인 스레드를 막는다.

  • CSRF

    CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)는 로그인한 사용자가 악성 사이트를 방문했을 때, 그 사이트가 사용자의 브라우저를 시켜 사용자 모르게 원래 사이트에 요청을 보내게 하는 공격이다. 공격자는 토큰을 훔치지 않는다. 브라우저가 쿠키를 알아서 붙여 준다는 점을 이용한다.

  • 언어적 안티패턴

    이름, 타입, 주석이 말하는 것과 코드가 실제로 하는 일이 어긋나는 것. 읽는 사람을 잘못된 추측으로 이끈다.

  • 저장소 역할 분담 (DB·캐시·큐·검색)

    서버 애플리케이션 옆에는 거의 늘 관계형 DB, 인메모리 캐시, 메시지 브로커(message broker), 검색 엔진이 붙는다. 하나로 다 하지 않는 이유는 데이터의 성격(영구성·속도·전달·검색)마다 잘하는 도구가 다르기 때문이다.

보기 옵션