728x90

개요

RefreshToken을 쿠키로 전달할 때 보안을 강화하기 위해 AES128 양방향 암호화를 적용했다.

RefreshToken은 사용자가 재인증할 필요 없이 새로운 AcessToken을 발급받도록 해준다. 만약 해커가 RefreshToken을 가로채면 암호를 사용자의 계정에 접근하는데 사용할 수 있다. 이 때, AES128 암호화로 RefreshToken을 암호화하면 RefreshToken을 가로채더라도 사용하기 어려워진다. (물론 이 방법 또한 완벽하지 않다.)

 

AES(Advanced Encryption Standard) 암호화

  • 저장중이거나 전송하려는 데이터를 보호하는 데 사용되는 대칭 암호화 알고리즘. 고정된 크기의 데이터 블록에 데이터를 암호화하는 블록 암호 암호화 알고리즘이다.
  • AES 암호화는 데이터의 발신자와 수신자 간에 공유되는 Secret Key를 사용한다. Secret Key는 일반 텍스트를 암호화하거나 암호를 일반 텍스트로 복호화하는 데 사용된다. 그렇기 때문에 Secret Key는 절대 외부에 노출되서는 안된다.
  • AES 암호화는 Secret Key의 길이에 따라 종류가 다르다. 128bit, 192bit, 256bit를 지원한다. 나는 128bit를 사용했다.
  • AES128 암호화는 자바의 기본 라이브러리를 사용한다. 아래 변수와 함께 알아보자
    • SecretKeySpec : 비밀키를 만드는 데 사용된다.
    • IvParameterSpec : CBC 모드의 IV를 만들기 위해 사용한다.
    • Cipher : AES 및 다양한 암호화 알고리즘을 사용하여 데이터를 암호화하고 해독하는 메서드를 제공한다.

 

구현

다음은 AES128 암호화 및 복호화 기능을 하는 AES128Config 클래스의 코드이다. 순서대로 살펴보자

@Component
public class AES128Config {
    private static final Charset ENCODING_TYPE = StandardCharsets.UTF_8;
    private static final String INSTANCE_TYPE = "AES/CBC/PKCS5Padding";

    @Value("${aes.secret-key}")
    private String secretKey;
    private IvParameterSpec ivParameterSpec;
    private SecretKeySpec secretKeySpec;
    private Cipher cipher;

    @PostConstruct
    public void init() throws NoSuchPaddingException, NoSuchAlgorithmException {
        SecureRandom secureRandom = new SecureRandom();
        byte[] iv = new byte[16];   // 16bytes = 128bits
        secureRandom.nextBytes(iv);
        ivParameterSpec = new IvParameterSpec(iv);
        secretKeySpec = new SecretKeySpec(secretKey.getBytes(ENCODING_TYPE), "AES");
        cipher = Cipher.getInstance(INSTANCE_TYPE);
    }

    // AES 암호화
    public String encryptAes(String plaintext) {
        try {
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
            byte[] encryted = cipher.doFinal(plaintext.getBytes(ENCODING_TYPE));
            return new String(Base64.getEncoder().encode(encryted), ENCODING_TYPE);
        } catch (Exception e) {
            throw new BusinessLogicException(ExceptionCode.ENCRYPTION_FAILED);
        }
    }

    // AES 복호화
    public String decryptAes(String plaintext) {
        try {
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
            byte[] decoded = Base64.getDecoder().decode(plaintext.getBytes(ENCODING_TYPE));
            return new String(cipher.doFinal(decoded), ENCODING_TYPE);
        } catch (Exception e) {
            throw new BusinessLogicException(ExceptionCode.DECRYPTION_FAILED);
        }
    }
}

 

1. IvParameterSpec 생성

SecureRandom 인스턴스를 사용해서 16바이트 크기의 난수 바이트 배열을 생성하고 바이트 배열을 사용해서 IvParameterSpec 객체를 생성한다. SecurityRnadom를 사용한 이유는 SecretKeySpec과 IvParameterSpec에 동일한 키를 사용하게 되면 보안에 취약하기 때문이다. 이 때 보안을 강화하는 방법으로 SecureRandom과 같은 난수 생성기를 사용하는 것이다.

    @PostConstruct
    public void init() throws NoSuchPaddingException, NoSuchAlgorithmException {
        SecureRandom secureRandom = new SecureRandom();
        byte[] iv = new byte[16];   // 16bytes = 128bits
        secureRandom.nextBytes(iv);
        ivParameterSpec = new IvParameterSpec(iv);
        ...
    }

 

2. SecretKeySpec, Cipher 초기화

ENCODING_TYPE(UTF-8)을 사용하여 Secret Key 문자열을 바이트로 변환한 키 값과 문자열 “AES”를 사용해서 SecretKeySpec을 생성한다.

알고리즘 타입을 지정하여 Cipher를 생성한다. 나는 AES/CBC/PKCS5Padding 를 사용했다.

    @PostConstruct
    public void init() throws NoSuchPaddingException, NoSuchAlgorithmException {
        ...
        secretKeySpec = new SecretKeySpec(secretKey.getBytes(ENCODING_TYPE), "AES");
        cipher = Cipher.getInstance(INSTANCE_TYPE);
    }

 

3. 데이터 암호화

secretKeySpec과 ivParameterSpec를 사용하여 암호화 모드(ENCRYPT_MODE)에서 Cipher를 초기화하고 doFinal() 메서드로 데이터를 암호화한다. 암호화한 데이터는 편의를 위해 문자열로 인코딩하여 반환했다.

    public String encryptAes(String plaintext) {
        try {
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
            byte[] encryted = cipher.doFinal(plaintext.getBytes(ENCODING_TYPE));
            return new String(Base64.getEncoder().encode(encryted), ENCODING_TYPE);
        } catch (Exception e) {
            throw new BusinessLogicException(ExceptionCode.ENCRYPTION_FAILED);
        }
    }

 

4. 데이터 복호화

암호화와는 반대로 복호화 모드(DECRYPT_MODE)에서 secretKeySpec과 ivParameterSpec를 사용하여 Cipher를 초기화하고 doFinal() 메서드로 데이터를 복호화 한다. 복호화한 데이터도 문자열로 인코딩하여 반환했다.

    public String decryptAes(String plaintext) {
        try {
            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
            byte[] decoded = Base64.getDecoder().decode(plaintext.getBytes(ENCODING_TYPE));
            return new String(cipher.doFinal(decoded), ENCODING_TYPE);
        } catch (Exception e) {
            throw new BusinessLogicException(ExceptionCode.DECRYPTION_FAILED);
        }
    }

 

5. Test

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;


@Slf4j
@SpringBootTest
class AES128ConfigTest {

    @Autowired
    private AES128Config aes128Config;

    @Test
    @DisplayName("Aes128 암호화가 잘 이루어지는지 테스트")
    void aes128Test() {
        String text = "this is test";
        String enc = aes128Config.encryptAes(text);
        String dec = aes128Config.decryptAes(enc);
        log.info("enc = {}", enc);
        log.info("dec = {}", dec);

        assertThat(dec).isEqualTo(text);
    }
}

728x90
Cold Bean