[Spring Security] Bcrypt의 salt는 무엇일까?

신민철·2023년 7월 30일
3

Spring

목록 보기
5/5
post-thumbnail

Spring Security를 공부하고, CS 공부를 하면서 salt라는 개념이 나와 한번 다뤄보려고 한다.


암호화 해시함수는 단방향 알고리즘이기 때문에 해시값으로 변환된 비밀번호를 역으로 알아내는 것은 불가능하고 로그인할 때는 해당 값을 다시 입력하고 해시함수를 적용함으로써 같은 값이 DB에 있는지를 확인함으로써 로그인을 한다.

하지만 로그인은 Rainbow table 공격에 취약한데, rainbow table은 입력 가능한 문자열 조합을 해시함수에 넣어서 결과를 저장한 테이블이다. 이 테이블로 일일이 대조하여 브루트 포스 방식으로 공격이 가능하다.

현대에는 이를 어떻게 방지했을까? 정답은 salt이다. 비밀번호 자체 문자열에 랜덤으로 생성된 값인 salt를 더함으로써 Rainbow table 공격을 막을 수 있게 되었다.


Spring Security에서는 BCryptPasswordEncoder를 사용하고 이를 통해 비밀번호를 암호화한다.

public void encodePassword(PasswordEncoder passwordEncoder) {
		this.password = passwordEncoder.encode(password);
	}

그럼 비밀번호를 암호화하게 되면 salt 값이 어디엔가 존재해야 하는데 그것을 어디에 저장하는지 궁금해졌다. 그래서 BCryptEncoder를 한번 까봤다.

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

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

getSalt() 메소드가 있지만 따로 salt 값을 가져오는 로직은 보이지 않는다

알고보니.. salt 값은 DB에 password가 암호화 되어서 저장되는데 해당 암호화된 비밀번호에 salt 값이 같이 저장되는 것이었다

현재 진행 중인 프로젝트 DB에서 가져온 테스트 값이다

Bcrypt는 해시값을 위와 같이 생성하게 되는데 $로 3가지 필드가 구분된다.

  • ‘2a’는 bcrypt의 알고리즘 버전을 의미한다
  • ‘10’은 cost factor이다. 이는 key derivation 함수가 2^10번 반복 실행된다는 의미인데 이 값이 커질수록 해시값을 구하는 속도가 느려진다고 한다.
  • 그리고 그 뒤에 값은 salt와 암호화된 비밀번호 값이 붙어있는 상황이다. salt 값은 첫 22개 문자(16 byte)가 salt 값이다.

“하지만 위에서 우리가 얘기했던 것처럼 salt는 Rainbow Table 공격을 막으려고 한건데, 이렇게 비밀번호와 같이 저장되어 있으면 의미가 있는 것일까?”라는 의구심이 생기게 되었다.


그런데 찾아보니 salt값을 특별히 private하게 저장할 필요가 없다라는 결론에 도달했다.

일단 첫번째로, salt값은 유저별로 다르기 때문에 특정 유저의 값을 알아낸다고 하더라도 다른 유저의 salt와는 다른 것이다.


두번째, salt 값을 완벽히 보안이 안전하게 저장할 수 없다는 사실이다.

salt를 완벽히 저장할 수 있게 된다면 그냥 비밀번호를 안전하게 저장하면 되는 일이다.


세번째, salt의 목적은 Rainbow Table을 사용하지 못하는 데에 있다.

이는 salt를 적용하기만 해도 충분히 방지할 수 있는 일이다.


마지막으로, salt 값이 노출되어도 Rainbow Table을 새로 만들기 어렵다.

Rainbow Table을 만드는 데는 엄청난 시간과 리소스가 들어간다. 최대 8자리 영문 대소문자, 숫자를 충족하는 입력값이면 사전 텍스트 파일 크기가 대략 1663GB이고, 10자리까지 늘어난다면 초당 1억번 대입을 해도 1년 이상 걸린다고 한다.

그리고 bcrypt가 해시함수를 계산하는 것(key derivation function)이 1초 이상 걸린다고 한다.

또 보안이 더 중요하다고 생각되면 위에서 설명했던 cost factor를 더 늘리게 된다면 브루트포스는 더 오래 걸리게 되는 것이다. → 사실상 Rainbow Table 공격이 불가능하다

참고

How can bcrypt have built-in salts?

bcrypt

Spring Security는 어떻게 비밀번호를 검사할까

2개의 댓글

comment-user-thumbnail
2023년 7월 30일

공감하며 읽었습니다. 좋은 글 감사드립니다.

1개의 답글