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로 사용하였음