spring/spring security

[Spring Security] DelegatingPasswordEncoder와 BCryptPasswordEncoder strength에 따른 수행시간

moonsiri 2022. 3. 20. 20:13
728x90
반응형

DelegatingPasswordEncoder에 대해 정리하기 전에, 스프링 시큐리티 래퍼런스 내용을 살펴보겠습니다.

 

Srping Password Storage

Spring Security’s PasswordEncoder interface is used to perform a one way transformation of a password to allow the password to be stored securely. Given PasswordEncoder is a one way transformation, it is not intended when the password transformation needs to be two way (i.e. storing credentials used to authenticate to a database). Typically PasswordEncoder is used for storing a password that needs to be compared to a user provided password at the time of authentication.

 

DelegatingPasswordEncoder

Prior to Spring Security 5.0 the default PasswordEncoder was NoOpPasswordEncoder which required plain text passwords. Based upon the Password History section you might expect that the default PasswordEncoder is now something like BCryptPasswordEncoder. However, this ignores three real world problems: 

  • There are many applications using old password encodings that cannot easily migrate 
  • The best practice for password storage will change again 
  • As a framework Spring Security cannot make breaking changes frequently 

Instead Spring Security introduces DelegatingPasswordEncoder which solves all of the problems by: 

  • Ensuring that passwords are encoded using the current password storage recommendations 
  • Allowing for validating passwords in modern and legacy formats 
  • Allowing for upgrading the encoding in the future

 

 

Spring Security 5.0 이전에 기본 PasswordEncoder는 일반 텍스트 암호가 필요한 NoOpPasswordEncoder였는데, BCryptPasswordEncoder와 비슷합니다. 스프링 시큐리티에서 제공하는 PasswordEncoder는 비밀번호를 단방향으로 변환하여 저장하는 용도로 사용됩니다. 그리고 시대적인 흐름에 따라서 점점 고도화된 암호화 알고리즘 구현제가 적용되어가는데, 단방향의 변환된 암호를 풀어서 다시 암호화해야 하는 것이 쉬운 일은 아닙니다. 그래서 나온 해결 책이 DelegatingPasswordEncoder이며, DelegatingPasswordEncoder는 최신 및 레거시 형식의 비밀번호 유효성 검사를 하며, 향후 인코딩 업그레이드를 허용합니다.

 

기본 DelegatingPasswordEncoder 생성

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

 

public static PasswordEncoder createDelegatingPasswordEncoder() {
	String encodingId = "bcrypt";
	Map<String, PasswordEncoder> encoders = new HashMap<>();
	encoders.put(encodingId, new BCryptPasswordEncoder());
	encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
	encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
	encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
	encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
	encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
	encoders.put("scrypt", new SCryptPasswordEncoder());
	encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
	encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
	encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
	encoders.put("argon2", new Argon2PasswordEncoder());

	return new DelegatingPasswordEncoder(encodingId, encoders);
}

 

DelegatingPasswordEncoder로 encode 저장 형태는 아래와 같습니다.

{id}encodedPassword

 

Test 코드를 확인하며 더 알아보겠습니다.

@Test
public void passwordEncoderTest() {
	String pw = "Moonsiri123!";

	// (1)
	Map<String, PasswordEncoder> encoders = new HashMap<>();
	encoders.put("bcrypt", new BCryptPasswordEncoder());
	encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
	encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());

	// (2)
	PasswordEncoder bcrypt = new DelegatingPasswordEncoder("bcrypt", encoders);
	String bcryptEncodePw = bcrypt.encode(pw);
	System.out.println("bcryptEncodePw : " + bcryptEncodePw);
	System.out.println("matches() result : " + bcrypt.matches(pw, bcryptEncodePw));

	// (3)
	PasswordEncoder noop = new DelegatingPasswordEncoder("noop", encoders);
	String noopEncodePw = noop.encode(pw);
	System.out.println("noopEncodePw : " + noopEncodePw);
	System.out.println("matches() result : " + bcrypt.matches(pw, noopEncodePw));

	// (4)
	PasswordEncoder sha256 = new DelegatingPasswordEncoder("sha256", encoders);
	String sha256EncodePw = sha256.encode(pw);
	System.out.println("sha256EncodePw : " + sha256EncodePw);
	System.out.println("matches() result : " + bcrypt.matches(pw, sha256EncodePw));

	// (5)
	String bcryptPw = "$2a$10$meWMw5TicT.DQ.0Ezq3iheo3/WxQ61J2HTl98XlK19YLMIPcTiwte";
	((DelegatingPasswordEncoder) sha256).setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
	System.out.println("none-prefix bcryptEncodePw : " + bcryptPw);
	System.out.println("matches() result : " + sha256.matches(pw, bcryptPw));
}

 

(1) 사용하고 싶은 passwordEncoder 알고리즘으로 DelegatingPasswordEncoder를

(2) BCryptPasswordEncoder로 encode, matches

(3)(4) noop 또는 sha256으로 인코딩 된 패스워드를 BCryptPasswordEncoder가 대표인 DelegatingPasswordEncoder로 매칭 검사 시 결과 값 true

(5) prefix가 존재하지 않으면 오류가 발생하지만, default로 매칭 할 인코더를 설정해주면 해당 인코더로 검사

 

위 테스트 결과로, 기존에 저장되어 있는 암호화된 비밀번호를 DelegatingPasswordEncoder에서 사용할 수 있도록 이전하는 작업은 간단합니다. 암호화된 비밀번호 앞에 접두사를 추가해 주거나 default 인코더를 설정해주면 됩니다.

 

시큐리티에서 AuthenticationProvider로 DaoAuthenticationProvider를 사용 중이라면, passwordEncoder를 설정해줘야합니다. (default passwordEncoder로 PasswordEncoderFactories.createDelegatingPasswordEncoder()를 사용중이기 때문.)

 

 

BCryptPasswordEncoder

The BCryptPasswordEncoder implementation uses the widely supported bcrypt algorithm to hash the passwords. In order to make it more resistent to password cracking, bcrypt is deliberately slow. Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system. The default implementation of BCryptPasswordEncoder uses strength 10 as mentioned in the Javadoc of BCryptPasswordEncoder. You are encouraged to tune and test the strength parameter on your own system so that it takes roughly 1 second to verify a password.

 

BCryptPasswordEncoder는 BCrpyt 해시 함수를 사용해 비밀번호를 해시하는 PasswordEncdoer인데, Bruteforce attack이나 Rainbow table attack과 같은 Password Cracking에 대한 저항력을 높이기 위해 의도적으로 느리게 설정되어 있습니다. 전문 장비를 이용하면 한 계정에 대한 비밀번호 입력을 1초에 수억 번 이상으로 시도할 수 있어서 이런 유형의 공격을 어렵게 만들기 위해 1개의 암호를 확인하는데 약 1초 정도의 시간이 걸리도록 하는 것을 권장하고 있습니다.

하지만 캡챠를 사용 중이라던가 공격에 저항력이 높다면, 강도를 낮춰 수행 시간을 줄일 수 있습니다.

 

PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(4);

 

BCryptPasswordEncoder의 default 강도(strength)의 값은 10입니다. strength의 범위는 4~31으로, 인코딩 패스워드의 strength값이 10일 경우 matches() 수행 시간이 약 60ms, 4일 경우 약 1ms입니다.

(이미 strength 값이 10으로 인코딩 된 패스워드를 strength 값이 4인 인코더로 매칭 검사하더라도 수행 시간은 인코딩 시점의 strength에 따라감)

 

 

[Reference]

https://velog.io/@corgi/Spring-Security-PasswordEncoder%EB%9E%80-4kkyw8gi

728x90
반응형