Search

[Web] 비밀번호 암호화(해싱)

Last update: @7/2/2023

비밀번호 암호화(해싱)

비밀번호는 사용자로부터 입력되어 HTTP 통신을 통해 서버로 전송된 후 DB에 저장, 이후 조회됨
이 비밀번호가 전달/저장되는 과정에서 특정 지점부터는 평문이 아닌 해싱된 암호문으로 저장되어야 함
그렇다면 평문 비밀번호는 언제 암호화 되어야 할까?
HTTP 통신을 한다면 언제든 패킷이 도감청 당할 수 있기 때문에 브라우저단에서 비밀번호를 암호화한 후 전송해야 함
하지만 실 서비스에서는 HTTPS 통신을 하기 때문에 일반적인 상황에서 클라이언트의 요청 내용이 도감청되지 않고, 비밀번호를 평문으로 전송해도 됨
따라서 비밀번호 암호화는 데이터베이스가 해커의 손에 넘어갔을 경우를 대비해야 함. 즉, 비밀번호는 암호화(해싱)하여 DB에 저장해야 함
이후 사용자로부터 받은 평문 비밀번호는 DB에 저장할 때와 같은 알고리즘으로 해싱하여 값을 비교하여 검증할 수 있음

레인보우 테이블(rainbow table)과 솔팅(salting)

SHA-2 알고리즘 등을 통해 해싱을 해서 DB에 넣기만 하면 DB가 털려도 무사할까? 그렇지 않음
해싱 알고리즘이 같다면 같은 값을 해싱했을 경우 매번 같은 결과(다이제스트)가 나옴. 이를 이용해 엄청난 수의 비밀번호 경우의 수를 해싱한 다이제스트를 저장한 테이블을 레인보우 테이블이라고 부름
해커들은 다이제스트를 평문 비밀번호로 되돌릴 수는 없기 때문에, 탈취된 DB의 다이제스트 값과 레인보우 테이블의 다이제스트 값을 비교해 평문을 역추적해내는 방법을 사용함
이를 방지하기 위해 해싱 시에 비밀번호에 더해 랜덤한 값을 섞어서 해커의 레인보우 테이블을 무력화시키는 방법이 솔팅(salting)임
비밀번호를 해싱할 때 소금(salt)을 쳐서 다이제스트가 달라지게 하는 것

브루트 포스(brute force) 공격과 키 스트레칭(key stretching)

모든 유저에 대해 같은 솔트를 쓰게 되면 해커가 해당 솔트값에 대한 레인보우 테이블을 만들 수 있기 때문에 각 유저별로 다른 솔트를 사용하는 것이 바람직함
하지만 이렇게 하더라도 해커는 한 유저에 대한(주로 어드민 계정) 브루트 포스(무차별) 공격을 가할 수 있음. 즉, 엄청나게 많은 경우의 수를 입력해보는 방법으로 한 유저의 비밀번호를 알아낼 위험성이 존재함
이를 해결하기 위한 방법이 키 스트레칭(key stretching)인데, 이는 해싱을 수십만~수백만 번 반복하여 암호화 하는 것을 말함
이렇게 하면 아무리 빠른 해싱 알고리즘도 1회에 수백 밀리초 ~ 수 초가 소요되어 브루트 포스 공격을 무력화할 수 있음
유저는 회원가입이나 로그인이 키 스트레칭으로 인해 1초 더 걸린다고 해서 불만을 갖지 않음
키 스트레칭 덕분에 해커가 salt값을 알아도 브루트 포스 공격이 불가능하기 때문에 유저별 salt값을 DB에 저장해도 됨

PBKDF2(Password-Based Key Derivation Function)

NIST(National Institute of Standards and Technology, 미국표준기술연구소)에 의해서 승인된 알고리즘
미국 정부 시스템에서도 사용자 패스워드의 암호화된 다이제스트를 생성할 때 이 알고리즘을 사용함
PBKDF2는 다음과 같은 4개 파라미터를 기본으로 가짐
DIGEST = PBKDF2(Password, Salt, c, DLen)
Java
복사
Password: 패스워드
Salt: 솔트 - 최소 32글자 이상 권장
c: 해싱 반복 횟수 (키 스트레칭)
DLen: 원하는 다이제스트 길이
ISO-27001의 보안 규정을 준수하고, 서드파티의 라이브러리에 의존하지 않으면서 사용자 패스워드의 다이제스트를 생성하려면 PBKDF2-HMAC-SHA-256/SHA-512을 사용하면 됨

자바 예제

해싱 클래스 제작
public class Pbkdf2 { public String hash(String password, String salt) { try { SecretKeyFactory factory = SecretKeyFactory .getInstance("PBKDF2WithHmacSHA256"); PBEKeySpec spec = new PBEKeySpec( password.toCharArray(), salt.getBytes(), 400_000, 256 ); SecretKey key = factory.generateSecret(spec); byte[] hashedBytes = key.getEncoded(); return Hex.encodeHexString(hashedBytes); } catch (Exception e) { throw new RuntimeException("패스워드를 해싱할 수 없습니다.", e); } } }
Java
복사
패스워드와 솔트를 넣으면 해시된 결괏값을 반환하는 클래스
해싱 40만회로 테스트한 결과 M1 맥북 기준 하나의 패스워드를 해싱하는 데 0.7초정도 소요됨
유저별로 UUID를 생성해 salt로 사용하였음

References

관련 문서