본문 바로가기
팀 프로젝트/최종 프로젝트

AES 암호화엔 ECB를 사용하면 안된다

by pon9 2025. 2. 24.

지난번 Payload 암호화 글엔 치명적인 오류가 있었다

이참에 CBC로 바꾸면서 공부해보자

(저 팀원이 보내준 책 내용 ...)

 

 

ECB를 사용했던 이유

단순하고 구현이 쉬워서 ECB를 사용했었다. 

하지만 ECB는 동일한 입력 블록이 항상 동일한 암호문 블록으로 변환되기 때문에 데이터 내부 패턴이 그대로 노출될 위험이 있었다

즉, 아래 사진처럼 평문에 존재하는 패턴이 암호문에도 그대로 유지된다.

대놓고 나 리눅스 펭귄이오 하고있음

비트맵 이미지를 이렇게 ECB로 암호화하게 되면 윤곽이 드러나게 된다

 

또한 ECB는 각 데이터 블록을 독립적으로 암호화하는 방식이기 때문에 구조적으로 데이터의 무결성을 보장해주지 않는다.

공격자가 특정 암호문 블록을 조작하더라도 전체 데이터는 여전히 "정상적으로 보이는 잘못된 데이터"로 복호화 될 수 있고, 사용자는 이를 인지하지 못할 가능성이 크다.

 

그래서 팀원이 준 팁대로 CBC를 도입하게 되었다.

CBC는 이전 블록의 암호문과 XOR연산을 통해 각 블록을 암호화하므로,

동일한 평문 블록이라도 항상 다른 암호문을 생성할 수 있다.(오른쪽 그림처럼 형태를 알아볼 수 없다)

 

첫번째 블록은 "이전 블록"이라는 게 존재하지 않기 때문에 IV(initialization vector)가 필요하다.

이 IV를 정적으로 매 토큰마다 같은 값을 사용하느냐, 동적으로 매 토큰을 생성할 때마다 랜덤값을 생성하느냐로 나뉘는데 나는 동적으로 생성하는 방식을 선택했다.

복호화를 할 때는 암호화할 때 사용했던 IV가 필요하므로, IV를 암호문 앞에 붙여서 저장한다.

 

구현 난이도도 쉽고, https와 함께 사용하면 강력한 보안을 제공하므로 많이 사용한다고 한다

 

 

구현 코드

public class Aes256Util {

    private final SecretKeySpec secretKey;

    public Aes256Util(JwtSecurityProperties jwtSecurityProperties) {
        String aesSecretKey = jwtSecurityProperties.secret().aesKey();
        byte[] keyBytes = Base64.getDecoder().decode(aesSecretKey);
        this.secretKey = new SecretKeySpec(keyBytes, "AES");
    }

    /**
     * aes 암호화를 진행하는 메서드
     * CBC 를 사용하며 랜덤 IV 는 동적으로 생성합니다
     * @param plainText 암호화 대상 jwt payload
     */
    public String encrypt(String plainText) {
        try {
            byte[] ivBytes = new byte[16];
            new SecureRandom().nextBytes(ivBytes);  //랜덤 IV 생성
            IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

            Cipher cipher = initCipher(Cipher.ENCRYPT_MODE, ivSpec);
            byte[] encryptedBytes = cipher.doFinal(plainText.getBytes());

            //IV 와 암호화된 데이터 합치기
            byte[] combined = new byte[ivBytes.length + encryptedBytes.length];
            System.arraycopy(ivBytes, 0, combined, 0, ivBytes.length);
            System.arraycopy(encryptedBytes, 0, combined, ivBytes.length, encryptedBytes.length);

            return Base64.getEncoder().encodeToString(combined);
        } catch (Exception e) {
            throw new EncryptException(ServerErrorCode.TOKEN_ENCRYPTION_FAILED);
        }
    }

    //복호화 메서드
    public String decrypt(String encryptedText) {
        try {
            byte[] combined = Base64.getDecoder().decode(encryptedText);

            //IV 추출
            byte[] ivBytes = new byte[16];
            System.arraycopy(combined, 0, ivBytes, 0, 16);
            IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);

            //암호화된 데이터 추출
            byte[] encryptedBytes = new byte[combined.length - 16];
            System.arraycopy(combined, 16, encryptedBytes, 0, encryptedBytes.length);

            Cipher cipher = initCipher(Cipher.DECRYPT_MODE, ivSpec);
            byte[] decryptedBytes = cipher.doFinal(encryptedBytes);

            return new String(decryptedBytes);
        } catch (Exception e) {
            throw new UnAuthorizedException(AuthErrorCode.TOKEN_DECRYPTION_FAILED);
        }
    }

    //Cipher 초기화
    private Cipher initCipher(int mode, IvParameterSpec ivSpec) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(mode, secretKey, ivSpec);
        return cipher;
    }
}

일단 Cipher초기화 하는 메서드에서 기존 ECB에서 Cipher.getInstance("AES/ECB/PKC5Padding")으로 사용하던 부분을 CBC로 변경해주었다.

 

그리고 암호화 코드에서는

1. 매 암호화 시 마다 16바이트의 랜덤IV를 생성하고,

2. 암호화 후 생성한 IV와 암호문을 하나의 배열로 합쳐서 Base64 인코딩 처리한다

 

복호화 코드에선

1. 암호화된 데이터를 다시 Base64 디코딩 하고, 앞의 16바이트를 IV로 추출한다

2. 추출한 IV와 나머지 암호문을 이용해 복호화한다

 

ECB모드는 단순하지만 심각한 보안 취약점 때문에 실무에서는 사용하기 어렵단 점을 깨닫고,

CBC모드로 전환하면서 IV생성과 관리, 암호문과 IV의 결합/분리 과정을 도입하게 되었다.

 

하지만 이런 CBC에도 단점이 존재하는데,

 

첫번째 단점으론 한 블록에서 오류가 나면 이후 블록에도 오류가 연쇄적으로 난다는 점이다.

이런 이유로 CBC모드는 데이터 전송 시 무결성 보장을 위한 추가적인 대책이 거의 필수적이다. 

https를 사용하면 이런 CBC의 비트 오류 문제에 훨씬 안전해지긴 한다.

 

두번째 단점으론 복호화 시엔 병렬 처리가 가능하지만, 암호화 시 병렬 처리가 불가능하다.

첫번째 블록은 IV로 시작하지만 두 번째 블록부터는 이전 블록의 결과를 기반으로 암호화되기 때문에 반드시 순차적으로 처리해야 한다.

따라서 대량의 데이터를 암호화할 때 성능이 떨어진다.

 

 

이번에 Payload 암호화를 하면서 보안에 대해서도 얕게지만 공부해서 좋다.