BCryptPasswordEncoder는 encode시(암호화시) 동일한 plain text(평문)에 대해서 왜 매번 다른 암호화된 값을 생성 할까?

들어가며

 

스터디를 진행하면서 궁금했던 내용에 대해 포스팅도 같이하면 좋을것같아 작성하고자 한다.

 

 

매번 다른 암호화된 값을 생성하는 이유

 

/*
  평문을 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