220102 JBCrypt를 이용한 비밀번호 암호화하기

GuruneLee·2022년 1월 2일
2

GIST청원사이트-BE

목록 보기
7/11

문제상황

spring security 에 대한 정확한 이해가 부족하다고 생각하여, 기존 tutorial 등을 보고 따라 구현했던 security 의존적인 코드를 걷어내기로 하였다. 이 중, password 를 암호화 하는데 security 의 BCryptPasswordEncoder 를 사용하고 있어 이를 걷어내고 다시 구현해야 했다.

해결

jbcrypt

java 라이브러리 중 BCrypt 를 이용해 패스워드를 암호화할 수 있는 jbcrypt 라는 라이브러리가 있다. 이를 래핑해서 BcryptEncoder 를 만들었다.
구조는 다음과 같다

user 
├── BcryptEncoder.java (impl.)
└── Encryptor.java (interface)

interface-implementation 구조

처음에 어떤 라이브러리를 사용할지, 어떤 해싱 알고리즘을 사용할지 정해지지 않았었기에 interface 를 만들어뒀었다. 현재는 꼭 필요한 interface 는 아니지만 추후 다른 알고리즘을 사용하게 되거나 로직이 추가될 경우 유연하게 사용할 수 있을 듯 하여 유지하였다.

BCrypt

BCrypt 에 대해 내가 이해한 부분을 정리해보자 ref

사용 이유

SHA 알고리즘은 속도가 너무 빨라 brute force 로 뚫릴 가능성이 있다. bcrypt 는 key setup 이라는 전처리로 해싱 시간을 마음대로 늘릴게 하여 이러한 위험성을 없앨 수 있다 (해싱 속도를 조절 가능ㅇ한 hash 함수!!)

해싱에 쓰이는 요소

  • password : 암호화 할 raw 데이터 (평문)
  • salt : 같은 password 라도 다른 해싱 결과를 만들기위해 평문에 추가하는 문자열 (같은 평문에 다른 salt 를 추가하면 다른 해시값이 생성된다)
  • log_rounds : key setting 을 돌리는 횟수를 결정하는 인자. 실제로는 2^(log_rounds) 만큼 루프를 돌게된다
//BCrypt.java 의 crypt_raw() 중
		...
		rounds = 1 << log_rounds;
		...
		ekskey(salt, password);
		for (i = 0; i != rounds; i++) {
			key(password);
			key(salt);
		}
        ...

해싱 스트링

해싱 결과는 '메타데이터 + salt + hashed' 로 이루어진다
예를 들어 2a$10vl8aWBnW3fID.ZQ4/z01G.q1IRps.9cGLcZEiGDMVr5yUP1KUOYTa 에서는

  • $2a (bcrypt 버전 정보)
  • $10 (라운드 정보)
  • $vl8aWBnW3fID.ZQ4/z01G.q1IRps.9cGLcZEiGDMVr5yUP1KUOYTa (salt + hashed)
    로 이루어진다

salt 와 validation

salt 는 해싱을 할 때 마다 새롭게 생성된다 -> 같은 평문을 넣어도 항상 다른 값이 나온다
즉, 입력한 pw 가 일치하는지 확인하기 위해서는 salt 를 알아야한다.

비밀번호 저장 : pw 를 해싱해서 저장한다. 이 해싱값에는 salt 가 노출되어있다.
비밀번호 확인 : 확인하고자하는 해싱값을 가져와서 salt 를 챙겨간다. 이를 이용해 입력한 pw를 새롭게 해싱하여 원래 해싱값과 비교한다. (salt 가 같으면 해싱값이 같다)

(사실 round도 같아야한다)

구현

다음과 같이 implementation 하였다.

@Component
public class BcryptEncoder implements Encryptor {
    @Override
    public String hashPassword(String raw) {
        return BCrypt.hashpw(raw, BCrypt.gensalt());
    }

    @Override
    public boolean isMatch(String raw, String hashed) {
        return BCrypt.checkpw(raw, hashed);
    }
}

테스트

테스트는 다음과 같이 진행했다. jbcrypt 의 wrapping class 이기 때문에, hashPassword() 는 bcrypt 해싱 스트링 모양을 만들어내는지 테스트하였고, isMatch()는 jbcrypt 와 동일하게 동작하는지 테스트 하였다. 다른 빈은 사용하지 않기 때문에 @SpringBootTest 어노테이션을 사용하지 않았다

class BcryptEncoderTest {

    private final BcryptEncoder encoder = new BcryptEncoder();

    @Test
    void passwordIsHashedByBCrypt() {
        String password = "test-password";
        String hashed = encoder.hashPassword(password);

        assertThat(hashed).hasSize(60);
        assertTrue(hashed.startsWith("$2a$10$"));
    }

    @Test
    void isMatch() {
        String password = "test-password";
        assertTrue(encoder.isMatch(password, BCrypt.hashpw(password, BCrypt.gensalt())));
    }
    @Test
    void isMatchFailed() {
        String own = "own-password";
        String other = "other-password";
        assertFalse(encoder.isMatch(other, BCrypt.hashpw(own, BCrypt.gensalt())));
    }
}

생각해볼 만한 것

BcryptEncoder 를 꼭 빈으로 등록해야 하는가?
: 어떠한 정보도 받지 않기 때문에 싱글톤을 유지할 필요가 없지 않을까? 아니, 빈으로 등록하는것과 싱글톤은 동치인가?
BcryptEncoder 를 static 으로 만드는건 어떨까?
: static 으로 만들면 어떤 장점이 있을까?

profile
Today, I Shoveled AGAIN....

1개의 댓글

comment-user-thumbnail
2022년 2월 9일

복습하고 갑니다 ㅎㅎ

답글 달기