[JAVA] Bouncy Castle로 LEA/ARIA 블록 암호화 하기

C/C++ 예제는 많은데...

예전부터 국산 암호화 기술에 관심은 많았는데, 이상할 정도로 JavaKotlin으로 작성된 예제는 그렇게 많지 않았다. 심지어 KISA 공식 홈페이지에서도 LEA 블록 암호화 알고리즘 정도나 암호화 모드별로 샘플 소스코드가 제공되고 있고, ARIA는 엔진 코드만 제공되어 있을 정도로 Java 진영에서는 찬밥 취급이다. 아마도 국내에서는 해당 알고리즘들이 DRM 위주로만 사용되다 보니까 빠른 암복호화가 필요해서 그런 것 같다. (사실 그냥 AES 쓰면 되서 그렇다.) 그러던 와중에, JavaC#에서 지원하는 Bouncy Castle이라는 라이브러리를 발견하게 되었다. 이 라이브러리는 놀랍게도 전세계에서 개발된 대부분의 암호화 알고리즘을 지원하고 있으며, 당연하게도 ARIALEA 블록 암호화 알고리즘도 지원하고 있다.

📣
Bouncy Castle 라이브러리는 2024년 2월을 기준으로, Java 1.8 이상을 요구하고 있다.

암호화 모드별로 암호화해보기

📣
밑의 예제 코드들은 LEA 알고리즘을 기반으로 작성되었다. ARIA 알고리즘을 사용하고 싶다면, new LEAEngine() 대신에 new ARIAEngine()을 사용하면 된다.

ECB

import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.engines.LEAEngine;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;

import java.util.Arrays;

public void leaecb() {
    byte[] messageBytes = "The lazy dog jumps over the brown fox!".getBytes();

    // 16, 24, 32bytes 길이의 key를 사용할 수 있다
    byte[] key = "0123456789012345".getBytes();

    try {
        byte[] encryptedData = encrypt(key, messageBytes);
        byte[] originalMessage = decrypt(key, encryptedData);
        System.out.println(new String(originalMessage));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private static byte[] encrypt(byte[] key, byte[] plainText) throws Exception {

    // block size가 16의 배수가 아닐경우, 암호화가 안될수 있으므로 항상 데이터를 패딩한다
    BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new LEAEngine());
    cipher.init(true, new KeyParameter(key));

    byte[] outputData = new byte[cipher.getOutputSize(plainText.length)];
    int tam = cipher.processBytes(plainText, 0, plainText.length, outputData, 0);
    cipher.doFinal(outputData, tam);

    return outputData;
}

private static byte[] decrypt(byte[] key, byte[] cipherText) throws Exception {
    BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new LEAEngine());
    cipher.init(false, new KeyParameter(key));

    byte[] outputData = new byte[cipher.getOutputSize(cipherText.length)];
    int tam = cipher.processBytes(cipherText, 0, cipherText.length, outputData, 0);
    int finalLen = cipher.doFinal(outputData, tam);
        
    return Arrays.copyOfRange(outputData, 0, finalLen + tam);
}

CBC

import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.engines.LEAEngine;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

import java.util.Arrays;

public void leacbc() {
    byte[] messageBytes = "The lazy dog jumps over the brown fox!".getBytes();
    byte[] key = "0123456789012345".getBytes();
    byte[] iv = "0123456789012345".getBytes();

    try {
        byte[] encryptedData = encrypt(key, iv, messageBytes);
        byte[] originalMessage = decrypt(key, iv, encryptedData);
        System.out.println(new String(originalMessage));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

private static byte[] encrypt(byte[] key, byte[] iv, byte[] plainText) throws Exception {
    BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(CBCBlockCipher.newInstance(new LEAEngine()));
    cipher.init(true, new ParametersWithIV(new KeyParameter(key), iv));

    byte[] outputData = new byte[cipher.getOutputSize(plainText.length)];
    int tam = cipher.processBytes(plainText, 0, plainText.length, outputData, 0);
    cipher.doFinal(outputData, tam);

    return outputData;
}

private static byte[] decrypt(byte[] key, byte[] iv, byte[] cipherText) throws Exception {
    BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(CBCBlockCipher.newInstance(new LEAEngine()));
    cipher.init(false, new ParametersWithIV(new KeyParameter(key), iv));

    byte[] outputData = new byte[cipher.getOutputSize(cipherText.length)];
    int tam = cipher.processBytes(cipherText, 0, cipherText.length, outputData, 0);
    int finalLen = cipher.doFinal(outputData, tam);

    return Arrays.copyOfRange(outputData, 0, finalLen + tam);
}

CFB

import org.bouncycastle.crypto.engines.LEAEngine;
import org.bouncycastle.crypto.modes.CFBBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

public void leacfb() {
    byte[] messageBytes = "The lazy dog jumps over the brown fox!".getBytes();
    byte[] key = "0123456789012345".getBytes();
    byte[] iv = "0123456789012345".getBytes();

    byte[] encryptedData = encrypt(key, iv, messageBytes);
    byte[] originalMessage = decrypt(key, iv, encryptedData);

    System.out.println(new String(originalMessage));
}

public static byte[] encrypt(byte[] key, byte[] iv, byte[] plainText) {
        
    // blockSize는 64 혹은 128만 입력 가능 (128 권장)
    CFBModeCipher cipher = CFBBlockCipher.newInstance(new LEAEngine(), 128);
    cipher.init(true, new ParametersWithIV(new KeyParameter(key), iv));

    byte[] outputData = new byte[plainText.length];
    cipher.processBytes(plainText, 0, plainText.length, outputData, 0);

    return outputData;
}

public static byte[] decrypt(byte[] key, byte[] iv, byte[] cipherText) {
    CFBModeCipher cipher = CFBBlockCipher.newInstance(new LEAEngine(), 128);
    cipher.init(false, new ParametersWithIV(new KeyParameter(key), iv));

    byte[] result = new byte[cipherText.length];
    cipher.processBytes(cipherText, 0, cipherText.length, result, 0);

    return result;
}

OFB

import org.bouncycastle.crypto.engines.LEAEngine;
import org.bouncycastle.crypto.modes.OFBBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

public void leaofb() {
    byte[] messageBytes = "The lazy dog jumps over the brown fox!".getBytes();
    byte[] key = "0123456789012345".getBytes();
    byte[] iv = "0123456789012345".getBytes();

    byte[] encryptedData = encrypt(key, iv, messageBytes);
    byte[] originalMessage = decrypt(key, iv, encryptedData);

    System.out.println(new String(originalMessage));
}

public static byte[] encrypt(byte[] key, byte[] iv, byte[] plainText) {

    // blockSize는 8 혹은 16만 입력 가능 (16 권장)
    // OFBBlockCipher는 newInstance() 메소드가 없다
    OFBBlockCipher cipher = new OFBBlockCipher(new LEAEngine(), 16);
    cipher.init(true, new ParametersWithIV(new KeyParameter(key), iv));

    byte[] outputData = new byte[plainText.length];
    cipher.processBytes(plainText, 0, plainText.length, outputData, 0);

    return outputData;
}

public static byte[] decrypt(byte[] key, byte[] iv, byte[] cipherText) {
    OFBBlockCipher cipher = new OFBBlockCipher(new LEAEngine(), 16);
    cipher.init(false, new ParametersWithIV(new KeyParameter(key), iv));

    byte[] result = new byte[cipherText.length];
    cipher.processBytes(cipherText, 0, cipherText.length, result, 0);

    return result;
}

CTS

import org.bouncycastle.crypto.engines.LEAEngine;
import org.bouncycastle.crypto.modes.CTSBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;

public void leacts() {
    byte[] messageBytes = "The lazy dog jumps over the brown fox!".getBytes();
    byte[] key = "0123456789012345".getBytes();

    try {
        byte[] encryptedData = encrypt(key, messageBytes);
        byte[] originalMessage = decrypt(key, encryptedData);
        System.out.println(new String(originalMessage));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static byte[] encrypt(byte[] key, byte[] plainText) throws Exception {
    CTSBlockCipher cipher = new CTSBlockCipher(new LEAEngine());
    cipher.init(true, new KeyParameter(key));

    byte[] outputData = new byte[plainText.length];
    int tam = cipher.processBytes(plainText, 0, plainText.length, outputData, 0);
    cipher.doFinal(outputData, tam);

    return outputData;
}

public static byte[] decrypt(byte[] key, byte[] cipherText) throws Exception {
    CTSBlockCipher cipher = new CTSBlockCipher(new LEAEngine());
    cipher.init(false, new KeyParameter(key));

    byte[] result = new byte[cipherText.length];
    int finalLen = cipher.processBytes(cipherText, 0, cipherText.length, result, 0);
    cipher.doFinal(result, finalLen);

    return result;
}

CTR

import org.bouncycastle.crypto.engines.LEAEngine;
import org.bouncycastle.crypto.modes.CTRModeCipher;
import org.bouncycastle.crypto.modes.SICBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

public void leactr() {
    byte[] messageBytes = "The lazy dog jumps over the brown fox!".getBytes();
    byte[] key = "0123456789012345".getBytes();
    byte[] iv = "0123456789012345".getBytes();

    byte[] encryptedData = encrypt(key, iv, messageBytes);
    byte[] originalMessage = decrypt(key, iv, encryptedData);

    System.out.println(new String(originalMessage));
}

public static byte[] encrypt(byte[] key, byte[] iv, byte[] plainText) {
    CTRModeCipher cipher = SICBlockCipher.newInstance(new LEAEngine());
    cipher.init(true, new ParametersWithIV(new KeyParameter(key), iv));

    byte[] outputData = new byte[plainText.length];
    cipher.processBytes(plainText, 0, plainText.length, outputData, 0);

    return outputData;
}

public static byte[] decrypt(byte[] key, byte[] iv, byte[] cipherText) {
    CTRModeCipher cipher = SICBlockCipher.newInstance(new LEAEngine());
    cipher.init(false, new ParametersWithIV(new KeyParameter(key), iv));

    byte[] result = new byte[cipherText.length];
    cipher.processBytes(cipherText, 0, cipherText.length, result, 0);

    return result;
}

CCM

public void leaccm() throws Exception {
    String message = "The lazy dog jumps over the brown fox!";
    byte[] messageBytes = message.getBytes();
    byte[] key = "0123456789012345".getBytes();

    // CCM 모드에서 iv는 7~13bytes 길이의 값이다 (12bytes 길이 권장)
    byte[] iv = "012345678901".getBytes();

    // aad 값은 필수는 아니며, 길이는 2^64 bit보다 작아야 한다
    byte[] aad = "0123456789012345".getBytes();

    List<byte[]> encryptedDataAndMac = encrypt(key, iv, messageBytes, aad);
    byte[] encryptedData = encryptedDataAndMac.get(0);
    byte[] mac = encryptedDataAndMac.get(1);
    byte[] originalMessage = decrypt(key, iv, encryptedData, aad, mac);

    System.out.println(new String(originalMessage));
}

public static List<byte[]> encrypt(byte[] key, byte[] iv, byte[] plainText, byte[] aad) throws InvalidCipherTextException {
    int macSize = 128;
    CCMModeCipher cipher = CCMBlockCipher.newInstance(new LEAEngine());
        cipher.init(true, new AEADParameters(new KeyParameter(key), macSize, iv, aad));

    byte[] outputData = new byte[cipher.getOutputSize(plainText.length)];
    int tam = cipher.processBytes(plainText, 0, plainText.length, outputData, 0);
    cipher.doFinal(outputData, tam);

    List<byte[]> arr = new ArrayList<>();
    arr.add(outputData);
    arr.add(cipher.getMac());

    return arr;
}

public static byte[] decrypt(byte[] key, byte[] iv, byte[] cipherText, byte[] aad, byte[] mac) throws Exception {
    int macSize = 128;
    CCMModeCipher cipher = CCMBlockCipher.newInstance(new LEAEngine());
    cipher.init(false, new AEADParameters(new KeyParameter(key), macSize, iv, aad));

    byte[] result = new byte[cipher.getOutputSize(cipherText.length)];
    int tam = cipher.processBytes(cipherText, 0, cipherText.length, result, 0);
    cipher.doFinal(result, tam);

    // encrypt의 cipher.mac 값과 decrypt의 cipher.mac 값이 다르면 암호화 된 데이터가 위조 혹은 변조된 것이다
    if (!Arrays.equals(mac, cipher.getMac())) {
        throw new Exception("데이터가 위변조되었습니다.");
    }

    return result;
}
CCM 모드의 핵심은 mac 값을 검증하는 것이다. mac 값이 서로 다르면 암호화 된 데이터가 위조 혹은 변조된 것이다.

GCM

import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.LEAEngine;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;

public void leagcm() {
    String message = "The lazy dog jumps over the brown fox!";
    byte[] messageBytes = message.getBytes();
    byte[] key = "0123456789012345".getBytes();
    byte[] iv = "012345678901".getBytes();
    
    // aad 값은 필수는 아니며, 길이는 2^64 bit보다 작아야 한다
    byte[] aad = "0123456789012345".getBytes();

    try {
        byte[] encryptedData = encrypt(key, iv, messageBytes, aad);
        byte[] originalMessage = decrypt(key, iv, encryptedData, aad);
        System.out.println(new String(originalMessage));
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public static byte[] encrypt(byte[] key, byte[] iv, byte[] plainText, byte[] aad) throws Exception {
    int macSize = 128;
    GCMModeCipher cipher = GCMBlockCipher.newInstance(new LEAEngine());
    cipher.init(true, new AEADParameters(new KeyParameter(key), macSize, iv, aad));

    byte[] encryptedData = new byte[cipher.getOutputSize(plainText.length)];
    int tam = cipher.processBytes(plainText, 0, plainText.length, encryptedData, 0);

    try {
        cipher.doFinal(encryptedData, tam);
    } catch (InvalidCipherTextException e) {
        throw new Exception("GCM authentication tag generation failed: " + e.getMessage(), e);
    }

    return encryptedData;
}

public static byte[] decrypt(byte[] key, byte[] iv, byte[] cipherText, byte[] aad) throws Exception {
    int macSize = 128;
    GCMModeCipher cipher = GCMBlockCipher.newInstance(new LEAEngine());
    cipher.init(false, new AEADParameters(new KeyParameter(key), macSize, iv, aad));

    byte[] outputData = new byte[cipher.getOutputSize(cipherText.length)];
    int tam = cipher.processBytes(cipherText, 0, cipherText.length, outputData, 0);

    try {
        cipher.doFinal(outputData, tam);
    } catch (InvalidCipherTextException e) {
        throw new Exception("GCM authentication tag generation failed: " + e.getMessage(), e);
    }

    return outputData;
}
GCM 모드는 CCM 모드와 달리 개발자가 직접 mac 값을 검증할 필요가 없다.

정리하며

꽤 오랜 기간동안 LEAARIA 알고리즘으로 암호화 하는 방법에 대해 조사해본 결과, Bouncy Castle로 암호화 하는 것이 가장 좋다는 판단하에 해당 라이브러리를 활용하는 방법에 대해 모드 별로 다루어 보았다. 또한 위의 알고리즘 뿐만 아니라 대부분의 블록 암호화 알고리즘은 위의 코드에서 엔진만 변경하면 그대로 사용할 수 있으니 참고 바란다.

참고한 문헌 및 글

  1. bouncycastle.org
  2. https://people.eecs.berkeley.edu/~jonah/bc/org/bouncycastle/crypto/BlockCipher.html
  3. http://www.java2s.com/example/java-api/org/bouncycastle/crypto/modes/type-list-index.html