[이슈] 양방향 암/복호화 이슈

2024. 8. 22. 15:52이슈 창고

728x90
반응형
SMALL

모든 개발 기능들을 마무리하고 기존에 운영중이던 단방향 비밀번호 암호화를 양방향으로 바꿔달라는 요청이 발생되어 진행하는 도중에 복호화가 정상적으로 수행되지 않는 이슈가 발생하였습니다.

 

javax.crypto.BadPaddingException: Given final block not properly padded ~ 에러가 발생하면서 복호화 시 사용되는 키값이 암호화할 때 사용되던 키와 일치하지 않다고 계속해서 에러가 발생되었습니다.

 

 

문제 발생 

 

이전 양방향 암호화 Config 클래스

@Slf4j
@Component
public class AES128Config {

    private static final Charset ENCODING_TYPE = StandardCharsets.UTF_8;
    private static final String INSTANCE_TYPE = "AES/CBC/PKCS5Padding";

    @Value("${supplier.pwd.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);
        this.ivParameterSpec = new IvParameterSpec(iv);
        this.secretKeySpec = new SecretKeySpec(this.secretKey.getBytes(ENCODING_TYPE), "AES");
        this.cipher = Cipher.getInstance(INSTANCE_TYPE);
    }

    // AES 암호화
    public String encryptAes(String plaintext) throws InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {

        log.info("시크릿 키 : {}", secretKey);
        log.info("암호화할 비밀번호 : {}", plaintext);

        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
        byte[] encryted = cipher.doFinal(plaintext.getBytes(ENCODING_TYPE));
        return new String(Base64.getEncoder().encode(encryted), ENCODING_TYPE);
    }

    // AES 복호화
    public String decryptAes(String plaintext) throws InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {

        log.info("시크릿 키 : {}", secretKey);
        log.info("복호화할 비밀번호 : {}", plaintext);

        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
        byte[] decoded = Base64.getDecoder().decode(plaintext.getBytes(ENCODING_TYPE));
        return new String(cipher.doFinal(decoded), ENCODING_TYPE);
    }

}

 

우선 이전에 사용하고 있던 양방향 암/복호화 클래스 파일을 보면 PostConstruct가 적용된 init이라는 함수가 존재하는데, 이 AES128Config 파일을 암/복호화하고자하는 다른 클래스들에서 의존성 주입받아 사용하게 되면 Spring Bean Container의 Bean들이 초기화 되고 의존성 주입이 수행됨과 동시에  해당 init 함수가 실행되면서 암/복호화될 때 필요한 ivParameterSpec, secretKeySpec, cipher가 초기 설정되어 생성되게 됩니다.

 

설정된 ivParameterSpec, secretKeySpec, cipher들을 가지고 암/복호화를 수행하게 되는데, 간단하게 계정을 생성하고 로그인 시 정상적으로 암/복호화를 통해 로그인이 되는지 확인해보았습니다.

 

ID : newAccount3
Password : newAccount3

아이디와 비밀번호를 newAccount3 라고 하고 계정을 생성했을 때, 정상적으로 양방향 암호화가 진행되어 저장된 것을 확인할 수 있었습니다.

 

또한 로그인을 수행했더니 정상적으로 복호화를 수행하면서 로그인 되는 것 또한 확인할 수 있었습니다.

 

 

그런데, 서버를 껏다가 다시 실행하거나 다른 기능들 중에서 또 암/복호화를 수행해야되는 기능들을 실행하게 되면 javax.crypto.BadPaddingException: Given final block not properly padded ~ 에러가 발생하면서 암/복호화 키가 일치하지 않아 수행할 수 없다라는 에러가 계속해서 발생되게 되었습니다.

 

아무리 로직을 확인을 해봐도 암/복호화 키는 공통적인 키값을 사용하며, 해당 키 값은 properties파일에 하나의 설정값으로 만들어놓고 불러와서 사용하기 때문에 불일치하는 이슈는 절대 발생해서도, 그럴 수도 없는 일이였는데 지속적으로 에러가 발생되었습니다.

 

 

문제 파악 1

일단 제가 처음 의심을 해보았던 부분은 PostConstruct 어노테이션이 적용된 init 함수 부분이였습니다.

 

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

 

아무래도 PostConstruct 어노테이션을 적용하게되면 위에서 언급했다시피 이 클래스를 의존성 주입하고자 하는 클래스에서 호출하게 되면 호출할 때 마다 Bean이 초기화 되고 의존성 주입이 동시에 수행된다기에 이 init 함수가 계속해서 새롭게 실행되어 키 값 내용 및 암호화 알고리즘이 초기화 되어 새로운 암/복호화 알고리즘이 재생성되기 때문에 계속해서 발생된 것이라고 의심되었습니다.

 

따라서 특정 기능 실행 구간에서 이 init 함수를 한 번 실행하고 그 이후에는 적용된 내용을 가지고 재활용해서 필요한 함수마다 AES128Config 클래스를 매개변수로 받아 사용하게끔 수정했습니다.

 

하지만 이 또한 동일한 에러가 계속해서 발생되었습니다.

계속 암/복호화에 사용된 키가 일치하지 않다라는 에러가 발생되었는데, 키는 동일한 것을 확인했음에도 불구하고 지속적인 동일한 에러가 발생되어 답답했습니다.

 

 

 

문제 파악 2

그런데 문득 init 함수를 다시 한 번 살펴보게되었습니다.

 

@PostConstruct
public void init() throws NoSuchPaddingException, NoSuchAlgorithmException {
    SecureRandom secureRandom = new SecureRandom();
    byte[] iv = new byte[16];  // 의심되는 코드
    secureRandom.nextBytes(iv);
    this.ivParameterSpec = new IvParameterSpec(iv);
    this.secretKeySpec = new SecretKeySpec(this.secretKey.getBytes(ENCODING_TYPE), "AES");
    this.cipher = Cipher.getInstance(INSTANCE_TYPE);
}

 

 

이번에도 역시나 init 함수를 다시 한 번 살펴보게 되었습니다.

키 값은 여러번 확인해도 문제가 없으니, 다른 스펙들인 ivParameterSpec, cipher 부분을 확인해봐야겠다고 생각했습니다.

그런데 이 때 ivParameterSpec을 결정짓는 바이트 타입의 iv 변수가 new byte[16]으로 되어있는 것을 보았습니다.

위에서 계속 언급했다시피 PostConstruct 가 적용되어있기 때문에 계속해서 의존성을 주입받고 실행될 때 마다 Bean 이 초기화 되고 새롭게 주입되기 때문에 new Byte[] 로 선언하게 되면 매번 다른 iv 파라미터가 생성되는 것을 의미하고, 이는 암호화 알고리즘이 매번 다르게 생성되는 것을 의미하는 것으로 생각되었습니다.

 

 

AES128Config() throws NoSuchPaddingException, NoSuchAlgorithmException {
    byte[] iv = secretKey.substring(0, 16).getBytes(); 
    this.ivParameterSpec = new IvParameterSpec(iv);
    this.secretKeySpec = new SecretKeySpec(this.secretKey.getBytes(ENCODING_TYPE), "AES");
    this.cipher = Cipher.getInstance(INSTANCE_TYPE);
}

따라서 해당 함수를 PostConstruct 어노테이션을 빼고 생성자로 변경하였고, iv 변수를 사용하고있는 키 값을 기준으로 16자리 수 까지 잘라 공통된 iv 파라미터를 사용하도록 고정시켜주었습니다.

 

이제 다시 에러가 났었던 기능들을 실행해보았더니,

 

정상적으로 암/복호화가 수행되면서 원하던 데이터들을 호출할 수 있었습니다.

역시 원인은 키 값이 아니라 iv 변수였던 것입니다.

 

 

 

+ 정상 수행 동작 변경 코드

@Slf4j
@Component
public class AES128Config {

    private static final Charset ENCODING_TYPE = StandardCharsets.UTF_8;
    private static final String INSTANCE_TYPE = "AES/CBC/PKCS5Padding";

    private final String secretKey = "{암/복호화 시크릿 키}";
    private IvParameterSpec ivParameterSpec;
    private SecretKeySpec secretKeySpec;
    private Cipher cipher;

    AES128Config() throws NoSuchPaddingException, NoSuchAlgorithmException {
        byte[] iv = secretKey.substring(0, 16).getBytes();   // 16bytes = 128bits
        this.ivParameterSpec = new IvParameterSpec(iv);
        this.secretKeySpec = new SecretKeySpec(this.secretKey.getBytes(ENCODING_TYPE), "AES");
        this.cipher = Cipher.getInstance(INSTANCE_TYPE);
    }

    // AES 암호화
    public String encryptAes(String plaintext) throws InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {

        log.info("시크릿 키 : {}", secretKey);
        log.info("암호화할 비밀번호 : {}", plaintext);

        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
        byte[] encryted = cipher.doFinal(plaintext.getBytes(ENCODING_TYPE));
        return new String(Base64.getEncoder().encode(encryted), ENCODING_TYPE);
    }

    // AES 복호화
    public String decryptAes(String plaintext) throws InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException {

        log.info("시크릿 키 : {}", secretKey);
        log.info("복호화할 비밀번호 : {}", plaintext);

        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
        byte[] decoded = Base64.getDecoder().decode(plaintext.getBytes(ENCODING_TYPE));
        return new String(cipher.doFinal(decoded), ENCODING_TYPE);
    }

}

 

728x90
반응형
LIST