이전에 배운 두가지 보완 방식을 합친 단방향 암호화의 대표적인 예시중 하나인 BCrypt
에 대해서 알아봅시다.
왜 이거를 선택했냐면... 별 이유는 없습니다. Spring Security를 적용할 때 처음 사용했던 PasswordEncoder이었기 때문이죠.
Key Stretching
기법을 추가함Salt
를 생성해 암호문에 padding.$2A
가 적용됩니다.private String getSalt() {
if (this.random != null) {
return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
}
return BCrypt.gensalt(this.version.getVersion(), this.strength);
}
BCryptVersion
과 Strength
가 필요하나보네요. 자세히 살펴봅시다.public static String gensalt(String prefix, int log_rounds, SecureRandom random) throws IllegalArgumentException {
StringBuilder rs = new StringBuilder();
byte rnd[] = new byte[BCRYPT_SALT_LEN]; // BCRYPT_SALT_LEN == 16
// BCryptVersion, Strength 검증
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");
}
// Salt 난수 생성
random.nextBytes(rnd);
// BCryptVersion 추가
rs.append("$2");
rs.append(prefix.charAt(2));
// Strength 추가
rs.append("$");
if (log_rounds < 10) {
rs.append("0");
}
rs.append(log_rounds);
// Salt 추가
rs.append("$");
encode_base64(rnd, rnd.length, rs); // (16byte Base64 변환) == 22글자 (Base64 postFix에 `=`는 제외를 함.)
return rs.toString();
}
위의 로직을 그대로 따르면 다음과 같은 형식이 됩니다.
$
은 구분자(Delimiter)로 간주하시면 충분합니다.private static String hashpw(byte passwordb[], String salt, boolean for_check) {
BCrypt B;
String real_salt;
byte saltb[], hashed[];
char minor = (char) 0;
int rounds, off;
StringBuilder rs = new StringBuilder();
if (salt == null) {
throw new IllegalArgumentException("salt cannot be null");
}
int saltLength = salt.length();
if (saltLength < 28) {
throw new IllegalArgumentException("Invalid salt");
}
if (salt.charAt(0) != '$' || salt.charAt(1) != '2') {
throw new IllegalArgumentException("Invalid salt version");
}
if (salt.charAt(2) == '$') {
off = 3;
}
else {
minor = salt.charAt(2);
if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') {
throw new IllegalArgumentException("Invalid salt revision");
}
off = 4;
}
// Extract number of rounds
if (salt.charAt(off + 2) > '$') {
throw new IllegalArgumentException("Missing salt rounds");
}
if (off == 4 && saltLength < 29) {
throw new IllegalArgumentException("Invalid salt");
}
rounds = Integer.parseInt(salt.substring(off, off + 2));
real_salt = salt.substring(off + 3, off + 25);
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
if (minor >= 'a') {
passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
}
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0, for_check);
rs.append("$2");
if (minor >= 'a') {
rs.append(minor);
}
rs.append("$");
if (rounds < 10) {
rs.append("0");
}
rs.append(rounds);
rs.append("$");
encode_base64(saltb, saltb.length, rs);
encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
return rs.toString();
}
private static String hashpw(byte passwordb[], String salt, boolean for_check) {
BCrypt B;
String real_salt; // 이전에 만든 salt나, 다른 암호문과 비교를 할 때 추출해야 함.
byte saltb[], hashed[];
char minor = (char) 0;
int rounds, off;
StringBuilder rs = new StringBuilder();
-------------------- 검증 로직 --------------------
isNull(salt);
validateSaltFormat(salt); // BCrypt의 Salt 포맷을 만족시키는가?
char minor = extractBCryptVersion(salt); // a, y, b 중하나였죠?
rounds = extractRound(salt); // Key Stretching 적용을 위해 round 추출
-------------------------------------------------
-------------------- Salt 추출 로직 --------------------
realSalt = extractRealSalt(salt) // `${BCryptVersion}${Strength}$` 을 제외한 나머지 부분 추출
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); // 암호화 적용을 위해 Bytes로 변환
------------------------------------------------------
if (minor >= 'a') {
passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
}
B = new BCrypt();
// [Salt + Password Byptes]암호화 진행
// 보통 Blowfish 알고리즘을 적용
// Key Stretching 진행
hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0, for_check);
---------------- BCrypt 암호문 조립 ----------------
appendBCryptVersion(rs, minor);
appendStrength(rs, rounds);
appendSalt(rs, saltb); // 대신 22Bit로
appendHashs(rs, hashed);
encode_base64(saltb, saltb.length, rs);
encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
return rs.toString();
-------------------------------------------------
}
B.crypt_raw
에서 암호화 알고리즘이 실행됩니다.B.crypt_raw
: Key Stretching 적용 살펴보기private byte[] crypt_raw(byte password[], byte salt[], int log_rounds, boolean sign_ext_bug, int safety,
boolean for_check) {
int cdata[] = bf_crypt_ciphertext.clone();
int clen = cdata.length;
...
else {
rounds = roundsForLogRounds(log_rounds); // shift 연산 (round = 2^log_rounds)
if (rounds < 16 || rounds > 2147483648L) {
throw new IllegalArgumentException("Bad number of rounds");
}
}
...
// round 만큼 반복 (strength가 클 수록 오래걸리는 이유)
for (int i = 0; i < rounds; i++) {
key(password, sign_ext_bug, safety);
key(salt, false, safety);
}
...
}
roundsForLogRounds(int)
와 crypt_raw의 반복문
만 보시면 됩니다.static long roundsForLogRounds(int log_rounds) { // log_rounds가 strength라고 보시면 됩니다.
if (log_rounds < 4 || log_rounds > 31) {
throw new IllegalArgumentException("Bad number of rounds");
}
return 1L << log_rounds;
}
마지막으로 JMH를 통해 BCrypt에 대해 테스트를 진행해보겠습니다.
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(value = 2, jvmArgs= {"-Xms4G", "-Xmx4G"})
public class Test1 {
public static void main(String[] args) throws IOException, RunnerException {
Options opt = new OptionsBuilder()
.include(Test1.class.getSimpleName())
.warmupIterations(10)
.measurementIterations(10)
.forks(1)
.build();
new Runner(opt).run();
}
@Benchmark
public String easyBCrypt() {
String rawPassword = "rawPassword1234";
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
return encoder.encode(rawPassword);
}
@Benchmark
public String strongBCrypt() {
String rawPassword = "rawPassword1234";
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(20);
return encoder.encode(rawPassword);
}
}
Benchmark Mode Cnt Score Error Units
_02_BCryptTest.Test1.easyBCrypt avgt 10 74.939 ± 1.384 ms/op
_02_BCryptTest.Test1.strongBCrypt avgt 10 76895.705 ± 535.434 ms/op