사용 중인 비밀번호의 알고리즘을 바꾸는 방법 #DelegatingPasswordEncoder

한규주·2021년 8월 9일
0

Spring Security

목록 보기
1/1
post-thumbnail

DelegatingPasswordEncoder

spring에는 DelegatingPasswordEncoder가 있다. 여러 알고리즘을 동시에 사용할 수 있게 해주는 아주 착한 녀석이다.

그게 어떻게 가능하냐면,

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

이렇게 저장한 암호문에 prefix를 붙여서 알고리즘을 구별하는 방식이다.

Specification

이 클래스의 constructor를 보면 아래와 같다

public DelegatingPasswordEncoder(
	String idForEncode,     // (1)
	Map<String, PasswordEncoder> idToPasswordEncoder    // (2)
)

(1) idForEncode 암호문을 생성할때 사용할 알고리즘을 지정해야 한다. 즉 디폴트 알고리즘과 같다.

String을 넣어야 하는데 아래 idToPasswordEncoder에 사용되는 key를 사용하면 된다. 자세한 건 아래 예시를 보면 이해가 갈 것이다.

(2) idToPasswordEncoder Map<String, PasswordEncoder>

  • key: 각 알고리즘을 대표하는 prefix
  • value: 해당 알고리즘의 구현체

예제

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.DelegatingPasswordEncoder
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder

val encoder = DelegatingPasswordEncoder(
    "bcrypt",
    mapOf(
        "bcrypt" to BCryptPasswordEncoder(),
        "scrypt" to SCryptPasswordEncoder(),
        "pbkdf2" to Pbkdf2PasswordEncoder()
    )
)

val encrypted = encoder.encode("password")   // (1)

encoder.matches("password", encrypted) shouldBe true 

(1) idForEncode가 "bcrypt" 이므로 encode 호출 시 BCryptPasswordEncoder() 를 가지고 encoding할 것이다. db에는

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

와 같은 결과물이 남는다.

DelegatingPasswordEncoder의 소스를 보면 다음과 같다

@Override
public String encode(CharSequence rawPassword) {
	return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}
  • 여기서 PREFIX는 "{", SUFFIX는 "}"이다
  • this.passwordEncoderForEncodeidToPasswordEncoder.get(idForEncode) 와 동일하다

하위호환성

만약 처음부터 DelegatingPasswordEncoder를 쓰지 않았다면, 어떻게 될까?

현재 DB의 값에는 prefix가 없어서 이 클래스가 대응못할 것이다.

하지만 이런 때를 대비해서 defaultPasswordEncoderForMatches 를 지정할 수 있도록 해두었다.

val encoder = DelegatingPasswordEncoder(
    "bcrypt",
    mapOf(
        "bcrypt" to BCryptPasswordEncoder(),
        "scrypt" to SCryptPasswordEncoder(),
        "pbkdf2" to Pbkdf2PasswordEncoder()
    )
)

encoder.setDefaultPasswordEncoderForMatches(BCryptPasswordEncoder())

이렇게 해두면 prefix가 없는 암호문이 들어왔을때 BCryptPasswordEncoder을 사용하여 encoding하려고 시도할 것이다.

defaultPasswordEncoderForMatches을 지정하지 않았을때 prefix가 없는 암호문이 들어온다면 IllegalArgumentException을 발생시킨다

별첨) upgradeEncoding

인터페이스 PasswordEncoder는 upgradeEncoding을 통해 해당 암호문이 업그레이드 될 필요가 있는지 판단할 수 있도록 해주고 있다.

org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder#upgradeEncoding

@Override
public boolean upgradeEncoding(String encodedPassword) {
	if (encodedPassword == null || encodedPassword.length() == 0) {
		this.logger.warn("Empty encoded password");
		return false;
	}
	Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword);
	if (!matcher.matches()) {
		throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);
	}
	int strength = Integer.parseInt(matcher.group(2));
	return strength < this.strength;
}

BCryptPasswordEncoder의 소스를 참고해보자.

BCryptPasswordEncoder는 strength를 지정할 수 있는데, 이 strength보다 낮은 암호문을 받았을때 true를 return하도록 구현되어 있다.

DelegatingPasswordEncoder.upgradeEncoding()

이 메소드를 이용하면 DelegatingPasswordEncoder를 사용할때도 다음과 같은 동작이 가능하다.

@Compo
class PasswordChecker(
    private val passwordUpdater: PasswordUpdater
) {
    private val encoder = DelegatingPasswordEncoder(
        "bcrypt",
        mapOf(
            "bcrypt" to BCryptPasswordEncoder(),
            "scrypt" to SCryptPasswordEncoder()
        )
    ).also {
        it.setDefaultPasswordEncoderForMatches(BCryptPasswordEncoder())
    }

    fun passwordConfirm(userId: Long, rawPassword: String, encryptedPassword: String): Boolean {
        if (!encoder.matches(rawPassword, encryptedPassword)) return false
        if (encoder.upgradeEncoding(encryptedPassword)) {
            val newPassword = encoder.encode(rawPassword)
            passwordUpdater.update(userId, newPassword)
        }
        return true 
    }
}

DelegatingPasswordEncoder.upgradeEncoding는 해당 암호문이 idForEncode, 즉 디폴트 알고리즘을 사용하고 있지 않은 경우 true를 뱉는다.

즉 위 코드의 목적은 디폴트 알고리즘을 사용하고 있지 않은 암호문을 디폴트 알고리즘으로 갈아치워주는 코드라고 할 수 있다.

profile
토스페이먼츠의 서버개발자로 일하고 있습니다.

0개의 댓글