about.Programing/ToyProject

[EasyValet.] #10. 현재 프로젝트 비밀번호 저장 방법은 안전할까?

Logan. 2022. 10. 4. 15:59

현재 단방향 해싱 기법을 사용해서 비밀번호를 암호화하여 저장하고 있는데 문득 의문이 생겼습니다. "안전할까?"

 

단방향 Hashing은 같은 값을 해쉬 함수에 넣으면 매번 같은 값을 반환합니다. 뭔가 안전하지 않은데? 생각이 들었습니다. 찾아보니 레인보우 테이블을 이용한 레인보우 어택으로 충분히 시도가 가능하다는 것을 알 수 있었습니다. 즉 단방향 해싱 기법은 안전하지 않다는 말입니다.

 

레인보우 테이블이란 매번 해싱하는 것은 많이 시간이 필요하기 때문에 비밀번호의 경우의 수들을 모두 특정 해싱 알고리즘에 따라 미리 계산해서 테이블에 저장하고 해킹한 해싱된 패스워드를 테이블에서 찾는 방식입니다. 

 

이를 개선할 수 있는 암호화 방식이 없나 찾아봤습니다. 솔팅키스트레칭을 활용하여 같은 비밀번호를 변환하더라도 매번 다른 값이 나오는 암호화 기법이 있었습니다. 따라서 많이 검증되고 널리 사용되는 Spring Security에서 제공하는 BCryptPasswordEncoder 사용하기로 했습니다. BCryptPasswordEncoder 이름에서도 알 수 있듯이 BCrypt 알고리즘을 사용한 비밀번호 암호화 방식입니다. 

 

 

솔팅이 어떤 방식인지 한번 자세히 알아보겠습니다.

📌솔팅(Salting).

같은 비밀 번호를 입력하더라도 매번 다른 hashing값을 반환해줄 수 있는 방법입니다. BCryptPasswordEncoder의 코드를 기준으로 따라가보면 이해하기 편할 것 같습니다.

@Override
public String encode(CharSequence rawPassword) {
   if (rawPassword == null) {
      throw new IllegalArgumentException("rawPassword cannot be null");
   }
   String salt = getSalt();
   return BCrypt.hashpw(rawPassword.toString(), salt);
}

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);
}

public static String gensalt(String prefix, int log_rounds, SecureRandom random) throws IllegalArgumentException {
		StringBuilder rs = new StringBuilder();
		byte rnd[] = new byte[BCRYPT_SALT_LEN];

		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("$");
		encode_base64(rnd, rnd.length, rs);
		return rs.toString();
	}

위의 코드는 스택에 쌓이는 메서드들을 하나씩 가져온 것입니다.(순차적으로 바닥부터 쌓인다고 생각하시면 좋습니다.) encode 함수를 시작으로 가장 상단에 쌓이는 BCyrpt의 gensalt함수가 커스텀 Random클래스를 통해 salt를 만들어 반환해주는 것을 볼 수 있습니다. 그렇게 만들어진 salt를 통해 hashpw()함수를 실행으로 암호화를 합니다. 

 

코드를 정리해보면 plain 텍스트로 넘어온 비밀번호에 랜덤하게 생성된 salt를 추가해서 암호화를 진행합니다. 따라서 랜덤함수가 매번 실행되어 salt를 만들기 때문에 같은 비밀번호가 들어오더라도 매번 다른 암호화된 텍스트가 반환됩니다. 

 

📌 키스트레칭.

키스트레칭은 해싱을 여러번 하는 것을 말합니다. 해싱키를 한번더 해싱함수에 넣어 해싱을 진행합니다. 키를 만드는데 오랜 시간이 걸리게끔 만드는 작업입니다. 따라서 레인보우테이블을 만들때도 더 많은 시간이 소요되며 몇번의 스트레칭을 진행했는지도 알 수 없기 때문에 해킹작업이 쉽지 않습니다. BCryptPasswordEncoder도 키스트레칭을 제공함으로 적절히 섞어 사용하면 좋을 것 같습니다.

 

📌 정리.

Salting 방식이 적용된 암호화 클래스를 사용하여 안전하게 비밀번호를 저장할 수 있게 되었습니다. 조금이라도 알고 사용하자는 취지에서 블로그를 작성했습니다. 부족한 부분이 있다면 언제든 코멘트 부탁드립니다. 

 

📌 느낀점.

이전에는 비밀번호는 암호화해서 사용해야한다는 나라에서 지정한 요구사항에 맞춰 해싱을 통해 암호화해서 저장했습니다. 개발을 할때 모든 행동에 대해 이유를 만드는 연습을 계속 해나가야 겠습니다. 사소한 것 하나라도 그냥 만들게 되면 이렇게 구멍이 발생할 수도 있다는 것을 알게 되었습니다. 

 

 

참조.

https://d2.naver.com/helloworld/318732