BCrypt
BCrypt에 대해 정리를 한 내용 입니다.
단방향 해시 함수
단방향 해시 함수는 수학적인 연산을 통해 원본 메시지를 변환하여
암호화된 메시지(Digest)
를 생성합니다.
원본 메시지를 알면 암호화된 메시지를 구하기 쉽지만
암호화된 메시지로는 원본 메시지를 알 방법이 없어야 하므로 "단방향성"
이라고 합니다.
단방향 해시 함수의 문제점
1. 인식 가능성
동일한 패스워드 문구
가 동일한 Digest
를 얻게 된다면
Digest
를 다량 확보한 다음,
비교하면서 원본 메시지를 찾아내거나 동일한 효과의 메시지를 얻을 수 있습니다.
이 말은 같은 결과값을 얻어낼 수 있으니
비교할 대상이 많아져 정보를 탈취당하기 좋다는 뜻으로 이해했습니다.
2. 속도
해시 함수는 짧은 시간에 데이터를 검색하기 위해 설계되어있습니다.
처리속도가 빠름으로 인해 임의의 문자열의 Digest
와
해킹할 대상의 Digest
를 비교할 수 있습니다.
해결책
솔팅
Salt는 단방향 해시 함수에서 Digest
를 생성할 때 추가되는
"바이트 단위의 임의의 문자열"
입니다.
Salt인 "8zff4fgflgfd93fgdl4fgdgf4mlf45p1"
를 추가해 Digest를 생성한 모습입니다.
이렇게 하면 Digest를 탈취 당하더라도,
다른 Salt를 사용
한다면 Digest가 다르게 생성
됩니다.
키 스트레칭(key stretching)
입력한 패스워드의 Digest
를 생성하고,
생성된 Digest
를 입력 값으로 하여 Digest
를 생성하고,
또 이를 반복하는 방법으로 Digest
를 생성할 수도 있습니다.
이렇게 하면 입력한 패스워드를 동일한 횟수만큼 해시해야만
입력한 패스워드의 일치 여부를 확인할 수 있습니다.
이것이 기본적인 키 스트레칭
과정입니다.
양식
bcrypt 해시 문자열은 다음과 같은 양식입니다.
$2b$[cost]$[22 character salt][31 character hash]
간단한 예시
$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
- $2a$ : 해시 알고리즘의 식별자이자 version의 값입니다.
- 10 : Cost, 키 스트래칭을 몇 번 했는가 알게 해줍니다.(10 = 2의 10승 라운드)
- N9qo8uLOickgx2ZMRZoMye : salt 값, 22자
- IjZAgcfl7p92ldGxad68LJZdL17lhWy : 해시 값, 31자
궁금증
Salt 값
은 랜덤으로 넣어주기 때문에 위 사진처럼 매번 다른 Digest
가 나옵니다.
그럼 어떻게 패스워드가 같다는걸 확인하는지 궁금했습니다.
Salt 값을 어딘가에 저장해 두는건가?
BCrypt
의 경우 Hash 값 내부에 Salt 값이 포함
되기 때문에
Salt 값을 따로 저장하지 않아도 Hashing 된 값과 평문을 비교할 수 있었습니다.
BCryptPasswordEncoder 내부 코드를 살펴봅니다.
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
if (encodedPassword == null || encodedPassword.length() == 0) {
this.logger.warn("Empty encoded password");
return false;
}
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
matches()로 같은가 다른가를 확인 할 수 있어서 해당 함수를 찾아봤습니다.
그런데 여기서 BCrypt.checkpw
에서 Salt 값
없이 평문과 Hash 값
만으로 값을 검증했습니다.
즉, 검증에는 Salt 값이 필요가 없다는 것이기에 Salt 값은 따로 저장할 필요가 없다고 판별했습니다.
백기선 선생님의 말씀
누군가 저와 같은 생각을 해서 질문을 남겼습니다.
matches로 plain 패스워드와 인코딩 된 패스워드를 비교할 때는 salt를 사용하지 않습니다.
오로지 plain 패스워드와 인코딩된 패스워드만 가지고 해시를 하고
그 결과는 인코딩 된 패스워드와 일치하게 되어있습니다.
따라서 salt가 바뀌는건 신경쓰지 않아도 됩니다. 인코딩 할 때만 쓰이니까요.
그러므로 match를 사용하면
plain 패스워드
와 Digest
를 비교하면 가능하다 라고 생각하게 되었습니다.