[JAVA] Bouncy Castle로 LEA/ARIA 블록 암호화 하기
C/C++ 예제는 많은데...
예전부터 국산 암호화 기술에 관심은 많았는데, 이상할 정도로 Java
나 Kotlin
으로 작성된 예제는 그렇게 많지 않았다. 심지어 KISA
공식 홈페이지에서도 LEA
블록 암호화 알고리즘 정도나 암호화 모드별로 샘플 소스코드가 제공되고 있고, ARIA
는 엔진 코드만 제공되어 있을 정도로 Java
진영에서는 찬밥 취급이다. 아마도 국내에서는 해당 알고리즘들이 DRM
위주로만 사용되다 보니까 빠른 암복호화가 필요해서 그런 것 같다. (사실 그냥 AES 쓰면 되서 그렇다.) 그러던 와중에, Java
및 C#
에서 지원하는 Bouncy Castle
이라는 라이브러리를 발견하게 되었다. 이 라이브러리는 놀랍게도 전세계에서 개발된 대부분의 암호화 알고리즘을 지원하고 있으며, 당연하게도 ARIA
와 LEA
블록 암호화 알고리즘도 지원하고 있다.
📣
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;
}
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;
}
정리하며
꽤 오랜 기간동안 LEA
와 ARIA
알고리즘으로 암호화 하는 방법에 대해 조사해본 결과, Bouncy Castle
로 암호화 하는 것이 가장 좋다는 판단하에 해당 라이브러리를 활용하는 방법에 대해 모드 별로 다루어 보았다. 또한 위의 알고리즘 뿐만 아니라 대부분의 블록 암호화 알고리즘은 위의 코드에서 엔진만 변경하면 그대로 사용할 수 있으니 참고 바란다.