들어가며
스터디를 진행하면서 궁금했던 내용에 대해 포스팅도 같이하면 좋을것같아 작성하고자 한다.
매번 다른 암호화된 값을 생성하는 이유
/*
평문을 BCrypt 해싱으로 암호화하는 메서드
*/
@Override
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
String salt = getSalt(); //매번 다른값이 반환된다.
//얻은 salt로 BCrypt 방식의 해싱 진행
return BCrypt.hashpw(rawPassword.toString(), salt);
}
BCryptPasswordEncoder.java
BCryptPasswordEncoder 클래스 내에서 encode 메서드를 통해 BCrypt 방식의 해싱(암호화)을 진행할 때 salt 값을 사용하게되는데 getSalt() 메서드를 호출할경우 매번 다른 salt 값을 응답받게 된다.
salt 는 다음의 요소로 구성된다
salt = BCryptVersion + $ + strength[log_rounds] + $ + base64_encode(random byte sequence) [real salt]
BCryptVersion
- 종류: $2a, $2y, $2b
- BCryptPasswordEncoder 클래스의 default 값은 $2a 다.
strength[log_rounds]
- 암호화 하기 위해 해시함수를 2^strength 만큼 반복한다.
- 기본값은 10이며 4에서 31 사이의 값을 설정할 수 있다.
- strength 값이 높을 수록 암호화하는데 드는시간이 오래걸린다, 하지만 오래걸릴수록 공격자가 패스워드를 추측하기 위해 많은 시간과 자원을 투자해야 하기 때문에 적절한 값으로 설정하는것이 중요하다.
- log_round 값에 따른 해싱 시간은 다음과 같다.
log_rounds 값 | 해싱 시간 (평균)
------------ | -------------
4 | 0.7ms
5 | 1.4ms
6 | 2.7ms
7 | 5.4ms
8 | 11.7ms
9 | 23.4ms
10 | 49.7ms
11 | 99.4ms
12 | 214.7ms
13 | 431.2ms
14 | 930.7ms
15 | 1.9s
16 | 3.9s
17 | 7.8s
18 | 15.6s
19 | 31.2s
20 | 62.5s
21 | 2.1m
22 | 4.3m
23 | 8.6m
24 | 17.2m
25 | 34.4m
26 | 1.1h
27 | 2.2h
28 | 4.4h
29 | 8.8h
30 | 17.6h
31 | 35.2h
base64_encode(random byte sequence) [real salt]
- salt 를 젠할 때마다 매번 다른값을 만들어주는데 이녀석 때문이다.
아래는 salt 를 생성하는 코드다.
public static String gensalt(String prefix, int log_rounds, SecureRandom random) throws IllegalArgumentException {
StringBuilder rs = new StringBuilder();
byte rnd[] = new byte[BCRYPT_SALT_LEN]; //salt 길이는 16으로 고정.
if (!prefix.startsWith("$2") || (prefix.charAt(2) != 'a' && prefix.charAt(2) != 'y' && prefix.charAt(2) != 'b')) {
throw new IllegalArgumentException("Invalid prefix");
}
if (log_rounds < 4 || log_rounds > 31) {
throw new IllegalArgumentException("Invalid log_rounds");
}
random.nextBytes(rnd); //랜덤 바이트 시퀀스 생성
rs.append("$2");
rs.append(prefix.charAt(2));
rs.append("$");
if (log_rounds < 10) {
rs.append("0");
}
rs.append(log_rounds);
rs.append("$");
//rs 값 뒤에 랜덤 바이트 시퀀스를 base64로 인코딩한 문자열을 붙인다.
encode_base64(rnd, rnd.length, rs);
return rs.toString();
}
BCrypt.java
최종적으로 BCryptPasswordEncoder.java 내 아래 코드를 호출하면 salt + 해싱된 값 이 반환된다.(최종 암호화 값)
BCrypt.hashpw(rawPassword.toString(), salt)
즉, BCryptPasswordEncoder는 encode시(암호화시) 동일한 plain text 이더라도 매번 다른 값이 나오는 이유는 해싱에 사용될 salt 값이 gen이 될때마다 salt의 요소 중 하나로 random byte sequence가 존재하기 때문이다.
평문과 암호화된 값은 어떻게 비교할까?
BCryptPasswordEncoder 클래스 내에선 matches 메서드를 사용하면 되고,
원리는 다음과 같다.
해싱된 값(암호화된 값)은 아래의 예시처럼 구성되기 때문에
$2a$[log_rounds]$[base64_encode(random_sequence)][hash_value]
평문을 salt인 $2a$[log_rounds]$[base64_encode(random_sequence)] 를 가지고 실제 해싱을 해보아
[hash_value] 와 비교를 한다.
'🔐 Security' 카테고리의 다른 글
OAuth 2.0 개념 정리 (7) | 2021.04.11 |
---|---|
RS256, HS256 차이 (4) | 2020.07.11 |
Spring API서버에서 Apple 인증(로그인 , 회원가입) 처리하기 (23) | 2020.07.09 |