스프링 부트

[SpringBoot] Encrypt - AES128/256 ECB and CBC(with random iv) 테스트 코드

h__hj 2022. 10. 3. 17:56

# Encrypt - AES128/256 ECB and CBC(with random iv) 테스트 코드

aes 암호화/복호화 ecb, cbc 테스트 코드 입니다. 

# 테스트에 앞서

초기화 벡터(IV)
초기화 벡터는 키와 다른 보안 요건을 가지므로 보통 IV가 비밀일 필요는 없습니다.
대부분의 블록 암호 모드에서는 초기화 벡터를 같은 키(암호 난스)로 재사용하지 않는 것이 중요합니다.
많은 블록 암호 모드에는 IV가 랜덤 또는 의사 난수여야 하는 등 더 강력한 요건이 있습니다.
일부 블록 암호에는 (일부 키에 대해) 모든 0 IV가 암호화를 생성하지 않는 등 특정 초기화 벡터에 특정 문제가 있습니다.

CBC 모드에서는 암호화 시 IV는 예측할 수 없는(랜덤 또는 의사랜덤) 것이어야 합니다.
특히, (이전에는) 메시지의 마지막 암호 텍스트블록을 다음 메시지의 IV로 재사용하는 일반적인 방법은 안전하지 않습니다(예를 들어 이 방식은 SSL 2.0에서 사용되었습니다).
공격자가 다음 평문이 지정되기 전에 IV(또는 이전 암호문 블록)를 알고 있으면 이전에 같은 키로 암호화되어 있던 일부 블록의 평문에 대한 추측을 확인할 수 있습니다(이것은 TLS CBC IV [9]공격이라고 불립니다).

아래 테스트 코드에서는 iv를 고정된 값으로 사용 하고 있어, 좋은 코드라고 볼 수 없습니다.

참고만 부탁드립니다.

  • random한 IV를 사용해서 암호화 및 복호화를 해야 된다면,,,,, 
  • 맨 아래 Example Code (CBC128 and Random IV) 를 확인해 주세요!
  • 참고만 해주세요!

# 환경

/**
 * tool: STS 4.13.0
 * version: 2.7.3-SNAPSHOT
 * java: 11
 * type: MAVEN
 * view: THYMELEAF
 * jQuery: 3.6.0
 */

# Example Code (CBC 128)

public static void main(String[] args) {
    try {
        String encryptStr = "";
        String decryptStr = "";
        byte[] lawData = "PrivateContent".getBytes("UTF-8");
			
        byte[] aes128SecretKey = "0123456789ABCDEF".getBytes("UTF-8");
        byte[] aesIv = "0123456789ABCDEF".getBytes("UTF-8");
			
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec secretKey128 = new SecretKeySpec(aes128SecretKey, "AES");
        IvParameterSpec secretIv   = new IvParameterSpec(aesIv);
			
        // AES 128 암호화
        cipher.init(Cipher.ENCRYPT_MODE, secretKey128, secretIv);
        byte[] encryptBytes128 = cipher.doFinal(lawData);
        encryptStr = Base64.getEncoder().encodeToString(encryptBytes128); 
        System.out.println("CBC-128-E: " + encryptStr);
			
        // AES 128 복호화
        cipher.init(Cipher.DECRYPT_MODE, secretKey128, secretIv);
        byte[] decryptBytes128 = cipher.doFinal(Base64.getDecoder().decode(encryptStr));
        decryptStr = new String(decryptBytes128, "UTF-8");
        System.out.println("CBC-128-D: " + decryptStr);
		
    } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException| UnsupportedEncodingException e) {
            e.printStackTrace();
    }
}
/* 결과
CBC-128-E: QX4cT+Oxgz9dKZFlIcJamA==
CBC-128-D: PrivateContent
*/

# Example Code (CBC 256)

public static void main(String[] args) {
    try {
        String encryptStr = "";
        String decryptStr = "";
        byte[] lawData = "PrivateContent".getBytes("UTF-8");
			
        byte[] aes256SecretKey = "0123456789ABCDEF0123456789ABCDEF".getBytes("UTF-8");
        byte[] aesIv = "0123456789ABCDEF".getBytes("UTF-8");
			
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        SecretKeySpec secretKey256 = new SecretKeySpec(aes256SecretKey, "AES");
        IvParameterSpec secretIv   = new IvParameterSpec(aesIv);
			
        // AES 256 암호화
        cipher.init(Cipher.ENCRYPT_MODE, secretKey256, secretIv);
        byte[] encryptBytes256 = cipher.doFinal(lawData);
        encryptStr = Base64.getEncoder().encodeToString(encryptBytes256); 
        System.out.println("CBC-256-E: " + encryptStr);
			
        // AES 256 복호화
        cipher.init(Cipher.DECRYPT_MODE, secretKey256, secretIv);
        byte[] decryptBytes256 = cipher.doFinal(Base64.getDecoder().decode(encryptStr));
        decryptStr = new String(decryptBytes256, "UTF-8");
        System.out.println("CBC-256-D: " + decryptStr);

    } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException| UnsupportedEncodingException e) {
            e.printStackTrace();
    }
}
/* 결과
CBC-256-E: S26DxNceALGuBG1jo8tI8Q==
CBC-256-D: PrivateContent
*/

# Example Code (ECB 128)

public static void main(String[] args) {
    try {
        String encryptStr = "";
        String decryptStr = "";
        byte[] lawData = "PrivateContent".getBytes("UTF-8");			
       		
        byte[] secretKeyECB = "0123456789ABCDEF".getBytes("UTF-8");
        Cipher ECB = Cipher.getInstance("AES/ECB/PKCS5Padding");
        SecretKeySpec secretKey = new SecretKeySpec(secretKeyECB, "AES");
			
        // ECB 128 암호화
        ECB.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] encryptBytesECB = ECB.doFinal(lawData);
        encryptStr = Base64.getEncoder().encodeToString(encryptBytesECB); 
        System.out.println("ECB-128-E: " + encryptStr);
			
        // ECB 128 복호화			
        ECB.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] decryptBytesECB = ECB.doFinal(Base64.getDecoder().decode(encryptStr));
        decryptStr = new String(decryptBytesECB, "UTF-8");
        System.out.println("ECB-128-D: " + decryptStr);
					
    } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException| UnsupportedEncodingException e) {
            e.printStackTrace();
    }
}
/* 결과
ECB-128-E: wethk3G5rBW+4zeRwPubjA==
ECB-128-D: PrivateContent
*/

# Example Code (CBC128 and Random IV)

@Configuration
public class CryptoConfig {
	
    public byte[] generateSalt() {
        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[16];
        random.nextBytes(salt);
        return salt;
    }
	
    public static void main(String[] args) {
        try {
            // use random iv
            CryptoConfig config = new CryptoConfig();
			
            String encryptStr = "";
            String decryptStr = "";
            String originData   = "OriginPrivateContent";
            String strSecretKey = "0123456789ABCDEF"; // AES 128
            String strRandomIV  = EncryptService.byteArrayToHexString(config.generateSalt());
			
            Cipher cipherCBC = Cipher.getInstance("AES/CBC/PKCS5Padding");
            Cipher cipherECB = Cipher.getInstance("AES/ECB/PKCS5Padding");
			
            SecretKeySpec privateKey = new SecretKeySpec(strSecretKey.getBytes(), "AES");
			
            // ENCRYPT
            IvParameterSpec randomIV = new IvParameterSpec(EncryptService.hexStringToByteArray(strRandomIV));
            cipherCBC.init(Cipher.ENCRYPT_MODE, privateKey, randomIV);
			
            byte[] encodeData = cipherCBC.doFinal(originData.getBytes());
			
            encryptStr = Base64.getEncoder().encodeToString(encodeData); 
            System.out.println("RANDOM-IV-CBC-E : " + encryptStr);
			
            String addIvEncryptStr = strRandomIV.concat("|").concat(encryptStr);
            System.out.println("RANDOM-IV-ECB-E : " + addIvEncryptStr);
			
            cipherECB.init(Cipher.ENCRYPT_MODE, privateKey);
            byte[] encodeData2 = cipherECB.doFinal(addIvEncryptStr.getBytes());
			
            encryptStr = Base64.getEncoder().encodeToString(encodeData2); 
            System.out.println("RANDOM-IV-RESULT: " + encryptStr);
			
            // DECRYPT
            cipherECB.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] decodeData1 = cipherECB.doFinal(Base64.getDecoder().decode(encryptStr));
            decryptStr = new String(decodeData1, "UTF-8");
			
            System.out.println("RANDOM-IV-ECB-D : " + decryptStr);
            String[] ivAndData = decryptStr.split("\\|");
            String decIv = ivAndData[0];
            String decData = ivAndData[1];
			
            IvParameterSpec randomIV2 = new IvParameterSpec(EncryptService.hexStringToByteArray(decIv));
            cipherCBC.init(Cipher.DECRYPT_MODE, privateKey, randomIV2);
            byte[] decodeData2 = cipherCBC.doFinal(Base64.getDecoder().decode(decData));
            decryptStr = new String(decodeData2, "UTF-8");
			
            System.out.println("RANDOM-IV-CBC-D : " + decryptStr);

        } catch (IllegalBlockSizeException | BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException| UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }
}
@Service
public class EncryptService {

    public static String byteArrayToHexString(byte[] bytes) {
        if(bytes == null || bytes.length == 0) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        for (byte b: bytes) {
            builder.append(String.format("%02X", b));
        }
        return builder.toString();
    }
    public static byte[] hexStringToByteArray(String hexString) {
        if(StringUtils.isEmpty(hexString)) {
            return new byte[0];
        }
        byte[] bytes = new byte[hexString.length()/2];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) Integer.parseInt(hexString.substring(i*2, i*2 + 2), 16);
        }
        return bytes;
    }
}
/* 결과
RANDOM-IV-CBC-E : iH6e43aKeK7FmNt0dsIsw+jkhy7yQ3fwF8WSTls+3FI=
RANDOM-IV-ECB-E : 95E03239B309C0E8C3E585E15284988E|iH6e43aKeK7FmNt0dsIsw+jkhy7yQ3fwF8WSTls+3FI=
RANDOM-IV-RESULT: n7ln6UkZA8Xo2/PYpYyYJXJyqq+76nAjYqEkN1/LuYJ7vUxNrZcj31Ik+WpQ+79cZCpukPo/NVyUGoYCfncvJMnV2ruQCKPUD4B6qWtvfyU=
RANDOM-IV-ECB-D : 95E03239B309C0E8C3E585E15284988E|iH6e43aKeK7FmNt0dsIsw+jkhy7yQ3fwF8WSTls+3FI=
RANDOM-IV-CBC-D : OriginPrivateContent
*/
  • 이런식의 코드 작성을 올바르지 않을 수 있음.
  • 하지만 IV 을 Random으로 사용해야 된다고 생각했을 때의 작성한 코드다.
  • 순서는 아래와 같다.

[ 암호화 ]

  1. 평문을 Random한 IV 값으로 CBC 암호화 진행.
  2. RandomIV 값과 암호화된 평문을 "|" 로 연결.
  3. 연결된 값을 ECB 방식으로 암호화.

[ 복호화 ]

  1. [ 암호화 ] 에서 작성한 암호화값을 ECB 방식으로 복호화.
  2. 복호화된 값을 "|" 값으로 나누어 앞에는 IV, 뒤에는 암호화값으로 확인.
  3. 확인한 IV 값을 이용하여 암호화 값을 CBC 방식으로 복호화.

# 암복호화 시리즈 

https://hjho95.tistory.com/25 Encrypt - AES128/256 ECB and CBC(with random iv) 테스트 코드
https://hjho95.tistory.com/26 Encrypt - AES128 CBC(with iv) example code
https://hjho95.tistory.com/27 Encrypt - SHA256(with salt) MessageDigest example code

# 암복호화 시리즈 참조 페이지

aes example: https://aesencryption.net/
cbc vs ecb: https://yoda.wiki/wiki/Block_cipher_mode_of_operation
sha example: http://wiki.hash.kr/index.php/SHA256#cite_note-8