spring security에서 sha256에서 bcrypt로 암호화 방식을 전환하는 방법

2015-12-24 11:19

애플리케이션 보안 측면에서 비밀번호를 암호화하는 것은 정말 중요하다. 비밀번호 암호화를 제대로 하지 않은 상태에서 서버가 털려 한 순간에 서비스가 위험에 처하는 경우를 볼 수 있다.

slipp.net은 자체 회원가입을 하면 sha256으로 비밀번호를 암호화한다. 자체 회원가입 기능을 적용할 때 비밀번호 암호화와 관련해 많은 고민 없이 적용했다. 그러다 학생 수업 준비하면서 안전한 패스워드 저장 문서를 읽은 후 bcrypt 암호화 방식을 적용해야겠다는 생각을 가지게 되었다.

그런데 문제는 기존에 sha256으로 암호화되어 있는 비밀번호도 유지하면서 자연스럽게 bcrypt 암호화 방식으로 전환해야되는데 좋은 방법이 생각나지 않아 무기한 연기하고 있었다. 그렇게 시간이 흘러 다시 한번 해결책을 찾다가 다음과 같은 방식으로 변경하기로 했다.

sha256과 bcrypt로 암호화하는 비밀번호를 같은 칼럼에서 유지하면서 비밀번호에 따라 다른 암호화 기술로 비교하는 것으로 결정했다. 이전에는 이 문제를 해결하기 위해 너무 어렵게 접근했는데 새롭게 적용한 접근방식은 자연스럽게 새로운 암호화 방식으로 전환할 수 있으며, 소스 코드 변경도 많지 않겠다는 생각이 든다.

이에 대한 아이디어는 Automatically converting password hashes in Grails spring-security-core 문서를 통해 얻었다. Automatically converting password hashes in Grails spring-security-core 문서는 spring security의 3.x에서 지원한 PasswordEncoder를 기반으로 하고 있다. 이와 같이 구현할 경우 기존의 new ShaPasswordEncoder(256)로 빈 설정되어 있는 부분을 new Sha256ToBCryptPasswordEncoder()로 빈 생성을 변경하면 다른 코드 수정없이 Sha256과 Bcrypt를 적용하는 것이 가능하다. 이것이 DI의 가장 큰 장점이지 않겠는가?

그런데 spring security에서 org.springframework.security.authentication.encoding.PasswordEncoder는 deprecated된 상태이다. 최신 버전에서는 org.springframework.security.crypto.password.PasswordEncoder을 사용할 것을 권장하고 있다. 최신 버전에서는 salt 값을 내부적으로 자동생성하는 방식으로 동작한다. 최신 버전의 PasswordEncoder에 대한 구현체를 다음과 같이 구현해 적용했다.

import org.springframework.security.authentication.encoding.MessageDigestPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

public class Sha256ToBCryptPasswordEncoder implements PasswordEncoder {
    private PasswordEncoder bcryptPasswordEncoder;
    private MessageDigestPasswordEncoder sha256PasswordEncoder;
    private String salt;

    @Override
    public String encode(CharSequence rawPassword) {
        return bcryptPasswordEncoder.encode(rawPassword);
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword.startsWith("$2a$10$") && encodedPassword.length() == 60) {
            return bcryptPasswordEncoder.matches(rawPassword, encodedPassword);
        }

        if (encodedPassword.length() == 64) {
            return sha256PasswordEncoder.isPasswordValid(encodedPassword, rawPassword.toString(), salt;
        }

        return false;
    }

    public void setBcryptPasswordEncoder(PasswordEncoder bcryptPasswordEncoder) {
        this.bcryptPasswordEncoder = bcryptPasswordEncoder;
    }

    public void setSha256PasswordEncoder(MessageDigestPasswordEncoder sha256PasswordEncoder) {
        this.sha256PasswordEncoder = sha256PasswordEncoder;
    }

    public void setSalt(String salt) {
        this.salt = salt;
    }
}

위와 같이 구현하고 적용하는데 2시간 정도 투자한 듯하다. 새로운 암호화 방식을 적용하는 작업이 큰 작업으로 생각했는데 막상 위와 같이 적용했더니 부담없이 적용할 수 있었다. 만약 PasswordEncoder의 interface가 변경되지 않았다면 더 적은 시간 투자로 변경할 수 있었겠다. 나도 암호화와 관련해 깊이 있는 지식을 가지고 있는 것은 아니지만 너무 두려워하지 말고 이미 나와 있는 암호화 라이브러리 잘 사용하고, 새롭게 등장하는 암호화 기술로 전환하는 방법에 대해 고민하면서 많은 비용을 투자하지 않고 적용할 수 있을 것이라 생각한다.

0개의 의견 from FB

5개의 의견 from SLiPP

2015-12-24 15:09

제가 사용하는 소스인데 시간되면 한번 비교를 해 봐야 겠네요. 파일 첨부가 안되서... 보기 싫음 삭제 할께요.

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;

/**
 * SHA256 암호화 
 * @author jeongdae
 *
 */
public class SHA256 {

	private final static int ITERATION_NUMBER = 1000;

	/**
	 * 비밀번호 생성
	 * @param password 평문 비밀번호
	 * @param salt
	 * @return
	 * @throws Exception
	 */
	public static String getPassword(String password, String salt) {
		String encriptPassword = "";
		try {
			MessageDigest digest = MessageDigest.getInstance("SHA-256");
			digest.reset();
			digest.update(salt.getBytes());
			byte[] input = digest.digest(password.getBytes("UTF-8"));
			for (int i = 0; i < ITERATION_NUMBER; i++) {
				digest.reset();
				input = digest.digest(input);
			}
			
			encriptPassword =  new String(Base64.getEncoder().encode(input));
		} catch(Exception e) {
			e.printStackTrace();
		}
		return encriptPassword;
	}
	
	/**
	 * SALT 생성
	 * @return
	 * @throws NoSuchAlgorithmException
	 */
	public static String getSalt() {
		String value = "";
		try {
			SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
			// Salt generation 128 bits long
			byte[] salt = new byte[16];
			secureRandom.nextBytes(salt);
			byte[] encoded = Base64.getEncoder().encode(salt);
			return new String(encoded);
		} catch(Exception e) {
			e.printStackTrace();
		}
		return value;
	}
	
	/**
	 * 평문 비밀번호를 SHA256(평문비밀번호 + salt) 로 암호화 한 후 DB에서 가져온 암호화된 값이랑 일치 여부를 비교
	 * @param password
	 * @param dbEncriptPassword
	 * @param dbSalt
	 * @return
	 */
	public static boolean isEquaslPassword(String password, String dbEncriptPassword, String dbSalt ) {
		String encriptPassword = getPassword(password, dbSalt);
		if(encriptPassword.equals(dbEncriptPassword)) {
			return true;
		} else {
			return false;
		}
	}
	
	/**
	 * 입력 받은 알고리즘 방식에 따른 해시값을 생성
	 * @param seedKey
	 * @param hmac
	 * @return
	 */
	public static byte[] hash(byte[] seedKey, String hmac) {
		String security = null;
		if("HmacSHA1".equals(hmac)) {
			security = "SHA1";
		} else if("HmacSHA256".equals(hmac)) {
			security = "SHA-256";
		} else if("HmacSHA512".equals(hmac)) {
			security = "SHA-512";
		}
		
		try {
			MessageDigest messageDigest = MessageDigest.getInstance(security);
			return messageDigest.digest(seedKey);
		} catch(Exception e) {
			e.printStackTrace();
			return seedKey;
		}
	}
}
2015-12-29 11:08

@gt1000 올려주신 소스는 SHA256 을 사용하는 패스워드 암호화인 것 같네요. 올라온 글은 기존에 SHA256 을 사용하고 있는 경우, 어떤식으로 새로운 패스워드를 Bcrypt 로 변경하는지에 대해 나와있는 내용이어서 조금 다른내용이 아닐까 싶네요.

2015-12-29 11:11

기본적으로 해시된 데이터는 원본데이터로 다시 돌려놓을 수 없기 때문에 패스워드 암호화 방식을 변경하는 건 조금 번거로운 작업이더라구요. SHA256 > BCrypt 는 아니지만 예전에 암호화 방식을 별도 컬럼으로 추가하여 관리하기도 했던 경험이 있습니다. 만약 특정시간동안 Update 나 Insert 를 막을 수 있다면... 패스워드의 최근변경일을 기준으로 암호화 방식을 판단할수도 있겠네요.

의견 추가하기