[Spring Security] PasswordEncoder란?

Corgi·2020년 8월 8일
1
post-thumbnail

개요

이 글에서는 Spring Security 5.3.3 버전에서 지원하는 비밀번호 암호화 인터페이스인 PasswordEncoder와 구현체에 대해 알아보겠습니다.


PasswordEncoder

Spring Security에서는 비밀번호를 안전하게 저장할 수 있도록 비밀번호의 단방향 암호화를 지원하는 PasswordEncoder 인터페이스와 구현체들을 제공합니다. 이 인터페이스는 아래와 같이 심플하게 구성되어 있습니다.

public interface PasswordEncoder {
	
  // 비밀번호를 단방향 암호화
  String encode(CharSequence rawPassword);
	
  // 암호화되지 않은 비밀번호(raw-)와 암호화된 비밀번호(encoded-)가 일치하는지 비교
  boolean matches(CharSequence rawPassword, String encodedPassword);
	
  // 암호화된 비밀번호를 다시 암호화하고자 할 경우 true를 return하게 설정
  default boolean upgradeEncoding(String encodedPassword) { return false; };
}

Spring Security 5.3.3에서 공식 지원하는 PasswordEncoder 구현 클래스들은 아래와 같습니다.
  • BcryptPasswordEncoder : BCrypt 해시 함수를 사용해 비밀번호를 암호화
  • Argon2PasswordEncoder : Argon2 해시 함수를 사용해 비밀번호를 암호화
  • Pbkdf2PasswordEncoder : PBKDF2 해시 함수를 사용해 비밀번호를 암호화
  • SCryptPasswordEncoder : SCrypt 해시 함수를 사용해 비밀번호를 암호화

위 4개의 PasswordEncoder는 Password를 encode할 때, 매번 임의의 salt를 생성해서 encode 하게 되어 있습니다. 예를 들어 BCryptPasswordEncoder Class의 코드를 보면 아래와 같이 되어있습니다.

/*
* BCryptPasswordEncoder.encode() : 암호화
*/
public String encode(CharSequence rawPassword) {

  if (rawPassword == null) {
    throw new IllegalArgumentException("rawPassword cannot be null");
  }
	
  String salt;
  
   if (random != null) {
     salt = BCrypt.gensalt(version.getVersion(), strength, random);
   } else {
     salt = BCrypt.gensalt(version.getVersion(), strength);
   }
   return BCrypt.hashpw(rawPassword.toString(), salt);
}

/**
* BCrypt.gensalt() : Salt 생성
*/
public static String gensalt(String prefix, int log_rounds, SecureRandom random) throws IllegalArgumentException {

 StringBuilder rs = new StringBuilder();
 byte rnd[] = new byte[BCRYPT_SALT_LEN]; // 16byte(128bit) 크기의 Salt 생성

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

BCryptPasswordEncoder

BCrypt 해시 함수를 사용해 비밀번호를 해시하는 PasswordEncoder입니다. Bruteforce attack이나 Rainbow table attack과 같은 Password Cracking에 대한 저항력을 높이기 위해 의도적으로 느리게 설정되어 있습니다.

전문 장비를 이용하면 한 계정에 대한 비밀번호 입력을 1초에 수억번 이상으로 시도할 수 있습니다. 따라서 이런 유형의 공격을 어렵게 만들기 위해 1개의 암호를 확인하는데 약 1초 정도의 시간이 걸리도록 하는 것을 권장합니다. 각 시스템별로 성능 차이가 있기 때문에 PasswordEncoder가 암호를 해독하는데 걸리는 시간은 달라질 수 있습니다. 따라서 시스템에 맞게 테스트하면서 속도를 조정해줘야 합니다.

BCryptPasswordEncoder의 속도는 강도(strength)를 조정해서 조절할 수 있습니다. 강도는 4 ~ 31까지 설정할 수 있으며, BcryptPasswordEncoder는 default 강도로 아래와 같이 10을 사용합니다.

public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {
  
  if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
    throw new IllegalArgumentException("Bad strength");
  }
  
  this.version = version;
  this.strength = strength == -1 ? 10 : strength; // 지정하지 않으면 강도를 10으로 설정
  this.random = random;
}

아래는 BCryptPasswordEncoder의 강도를 16으로 설정한 예제입니다.

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Argon2PasswordEncoder

Argon2 해시 함수 를 사용해 비밀번호를 해시하는 PasswordEncoder입니다. Argon2는 Password Hasing Competition의 우승자(?)로 Password Cracking을 방지하기 위해 다른 PasswordEncoder와 마찬가지로 의도적으로 느리게 실행되도록 설정되어 있습니다. 마찬가지로 1개의 비밀번호를 확인하는데 약 1초 정도가 걸리도록 속도를 조정해줘야 합니다.

// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Pbkdf2PasswordEncoder

PBKDF2 해시 함수 를 사용해 비밀번호를 해시하는 PasswordEncoder입니다. FIPS 인증(Federal Information Processing Standards, 미 연방 시스템 내에서 중요한 데이터를 보호하기 위한 필수 표준) 이 필요한 경우 이 PasswordEncoder를 선택하는 것이 좋습니다.

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

SCryptPasswordEncoder

SCrypt 해시 함수를 사용해 비밀번호를 해시하는 PasswordEncoder입니다.

SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result);

기타 PasswordEncoder

PasswordEncoder 이전 버전과의 호환성을 위해 존재하는 다른 구현 클래스들이 많으나, 이는 안전하지 않은 것으로 간주되기 때문에 더이상 사용되지 않습니다. 그러나 기존 레거시 시스템을 마이그레이션하기 어렵기 때문에 제거할 계획이 없습니다. (Spring Security 曰)


Reference

0개의 댓글