package org.openhitls.crypto.jce.cipher;
import org.junit.BeforeClass;
import org.junit.Test;
import org.openhitls.crypto.BaseTest;
import org.openhitls.crypto.jce.provider.HiTls4jProvider;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;
import java.util.Locale;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class AESTest extends BaseTest {
private static final String[] MODES = {"ECB", "CBC", "CTR"};
private static final int[] KEY_SIZES = {128, 192, 256};
@BeforeClass
public static void setUp() {
Security.addProvider(new HiTls4jProvider());
}
@Test
public void testAesKnownAnswerVectors() throws Exception {
assertCipherVector(
"AES/ECB/NOPADDING",
"000102030405060708090a0b0c0d0e0f",
null,
"00112233445566778899aabbccddeeff",
"69c4e0d86a7b0430d8cdb78070b4c55a");
assertCipherVector(
"AES/CBC/NOPADDING",
"2b7e151628aed2a6abf7158809cf4f3c",
"000102030405060708090a0b0c0d0e0f",
"6bc1bee22e409f96e93d7e117393172a",
"7649abac8119b246cee98e9b12e9197d");
}
@Test
public void testCipherTransformationUsesLocaleIndependentCaseMapping() throws Exception {
Locale originalLocale = Locale.getDefault();
try {
Locale.setDefault(new Locale("tr", "TR"));
Cipher cipher = Cipher.getInstance("AES/ecb/NoPadding", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(hex("000102030405060708090a0b0c0d0e0f"), "AES"));
byte[] ciphertext = cipher.doFinal(hex("00112233445566778899aabbccddeeff"));
assertArrayEquals(hex("69c4e0d86a7b0430d8cdb78070b4c55a"), ciphertext);
} finally {
Locale.setDefault(originalLocale);
}
}
@Test
public void testAESEncryptDecrypt() throws Exception {
for (String mode : MODES) {
for (int keySize : KEY_SIZES) {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(keySize);
SecretKey key = keyGen.generateKey();
Cipher cipher = Cipher.getInstance("AES/" + mode + "/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
String testData = "Hello, AES Test!";
byte[] input = testData.getBytes(StandardCharsets.UTF_8);
int blockSize = cipher.getBlockSize();
int padding = blockSize - (input.length % blockSize);
byte[] paddedInput = Arrays.copyOf(input, input.length + padding);
byte[] iv = null;
if (!mode.equals("ECB")) {
iv = new byte[16];
new SecureRandom().nextBytes(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
} else {
cipher.init(Cipher.ENCRYPT_MODE, key);
}
byte[] encrypted = cipher.doFinal(paddedInput);
if (iv != null) {
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
} else {
cipher.init(Cipher.DECRYPT_MODE, key);
}
byte[] decrypted = cipher.doFinal(encrypted);
assertArrayEquals("Decryption failed for mode " + mode + " with key size " + keySize,
paddedInput, decrypted);
if (mode.equals("CBC")) {
Cipher cipher2 = Cipher.getInstance("AES/CBC/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
byte[] iv2 = new byte[16];
new SecureRandom().nextBytes(iv2);
cipher2.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv2));
byte[] encrypted2 = cipher2.doFinal(paddedInput);
assertFalse("CBC mode with different IVs produced same ciphertext",
Arrays.equals(encrypted, encrypted2));
cipher2.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv2));
byte[] decrypted2 = cipher2.doFinal(encrypted2);
assertArrayEquals("CBC decryption with different IV failed",
paddedInput, decrypted2);
}
}
}
}
@Test
public void testAesGcmEncryptionDecryption() throws Exception {
for (int keySize : KEY_SIZES) {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(keySize);
SecretKey key = keyGen.generateKey();
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
byte[] aad = new byte[16];
new SecureRandom().nextBytes(aad);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
Cipher encryptCipher = Cipher.getInstance("AES/GCM/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
encryptCipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
String testData = "Hello, AES Test!";
byte[] input = testData.getBytes(StandardCharsets.UTF_8);
encryptCipher.updateAAD(aad);
byte[] encryptedData = encryptCipher.doFinal(input);
Cipher decryptCipher = Cipher.getInstance("AES/GCM/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
decryptCipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
decryptCipher.updateAAD(aad);
byte[] decryptedData = decryptCipher.doFinal(encryptedData);
assertArrayEquals("Decrypted data should match original", input, decryptedData);
}
}
@Test
public void testMultipleBlocksEncryption() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
Cipher cipher = Cipher.getInstance("AES/ECB/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
byte[] input = new byte[64];
Arrays.fill(input, (byte)0x42);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(input);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = cipher.doFinal(encrypted);
assertArrayEquals("Multi-block encryption/decryption failed", input, decrypted);
}
@Test
public void testIncrementalProcessing() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
Cipher cipher = Cipher.getInstance("AES/ECB/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
byte[] input = new byte[48];
Arrays.fill(input, (byte)0x42);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] output = new byte[48];
int outputOffset = 0;
outputOffset += cipher.update(input, 0, 16, output, outputOffset);
outputOffset += cipher.update(input, 16, 16, output, outputOffset);
outputOffset += cipher.update(input, 32, 16, output, outputOffset);
cipher.doFinal();
assertEquals("Incorrect output length", 48, outputOffset);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = new byte[48];
outputOffset = 0;
outputOffset += cipher.update(output, 0, 16, decrypted, outputOffset);
outputOffset += cipher.update(output, 16, 16, decrypted, outputOffset);
outputOffset += cipher.update(output, 32, 16, decrypted, outputOffset);
cipher.doFinal();
assertEquals("Incorrect decrypted length", 48, outputOffset);
assertArrayEquals("Incremental processing failed", input, decrypted);
}
@Test
public void testAesCtrDoFinalContinuesAfterUpdate() throws Exception {
SecretKeySpec key = new SecretKeySpec(hex("000102030405060708090a0b0c0d0e0f"), "AES");
IvParameterSpec ivSpec = new IvParameterSpec(hex("a0a1a2a3a4a5a6a7a8a9aaabacadaeaf"));
byte[] plaintext = new byte[32];
Cipher oneShotCipher = Cipher.getInstance("AES/CTR/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
oneShotCipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] expected = oneShotCipher.doFinal(plaintext);
Cipher splitCipher = Cipher.getInstance("AES/CTR/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
splitCipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] firstBlock = splitCipher.update(plaintext, 0, 16);
byte[] finalBlock = splitCipher.doFinal(plaintext, 16, 16);
byte[] actual = Arrays.copyOf(firstBlock, firstBlock.length + finalBlock.length);
System.arraycopy(finalBlock, 0, actual, firstBlock.length, finalBlock.length);
assertArrayEquals("AES CTR doFinal must continue the counter after update", expected, actual);
}
@Test
public void testCBCMode() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES", HiTls4jProvider.PROVIDER_NAME);
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
byte[] input = new byte[48];
Arrays.fill(input, (byte)0x42);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(input);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] decrypted = cipher.doFinal(encrypted);
assertArrayEquals("CBC mode encryption/decryption failed", input, decrypted);
}
@Test
public void testCBCModeIncremental() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
byte[] input = new byte[48];
Arrays.fill(input, (byte)0x42);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
ByteArrayOutputStream encryptedStream = new ByteArrayOutputStream();
byte[] block1 = cipher.update(input, 0, 16);
if (block1 != null) encryptedStream.write(block1);
byte[] block2 = cipher.update(input, 16, 16);
if (block2 != null) encryptedStream.write(block2);
byte[] finalBlock = cipher.doFinal(input, 32, 16);
encryptedStream.write(finalBlock);
byte[] encrypted = encryptedStream.toByteArray();
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
ByteArrayOutputStream decryptedStream = new ByteArrayOutputStream();
int blockSize = 16;
int fullBlocks = encrypted.length / blockSize - 1;
for (int i = 0; i < fullBlocks; i++) {
byte[] decryptedBlock = cipher.update(encrypted, i * blockSize, blockSize);
if (decryptedBlock != null) decryptedStream.write(decryptedBlock);
}
byte[] lastBlock = cipher.doFinal(encrypted, fullBlocks * blockSize, blockSize);
decryptedStream.write(lastBlock);
byte[] decrypted = decryptedStream.toByteArray();
assertArrayEquals("CBC mode incremental encryption/decryption failed", input, decrypted);
}
@Test
public void testInvalidBlockSize() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
Cipher cipher = Cipher.getInstance("AES/ECB/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] input = new byte[20];
Arrays.fill(input, (byte)0x42);
try {
cipher.doFinal(input);
fail("Expected exception");
} catch (Exception e) {
assertTrue("Expected exception", e instanceof IllegalBlockSizeException);
}
}
@Test
public void testCbcPkcs7Padding() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
byte[] testData = new byte[20];
Arrays.fill(testData, (byte)0x42);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7PADDING", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(testData);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] decrypted = cipher.doFinal(encrypted);
assertArrayEquals("CBC mode with PKCS7 padding failed", testData, decrypted);
}
@Test
public void testCbcPkcs5Padding() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
byte[] testData = new byte[20];
Arrays.fill(testData, (byte)0x42);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(testData);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] decrypted = cipher.doFinal(encrypted);
assertArrayEquals("CBC mode with PKCS5 padding failed", testData, decrypted);
}
@Test
public void testCbcIso7816Padding() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
byte[] testData = new byte[20];
Arrays.fill(testData, (byte)0x42);
Cipher cipher = Cipher.getInstance("AES/CBC/ISO7816PADDING", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(testData);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] decrypted = cipher.doFinal(encrypted);
assertArrayEquals("CBC mode with ISO7816 padding failed", testData, decrypted);
}
@Test
public void testCbcZerosPadding() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] iv = new byte[16];
new SecureRandom().nextBytes(iv);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
byte[] testData = new byte[20];
Arrays.fill(testData, (byte)0x42);
Cipher cipher = Cipher.getInstance("AES/CBC/ZEROSPADDING", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
byte[] encrypted = cipher.doFinal(testData);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] decrypted = cipher.doFinal(encrypted);
int actualLength = testData.length;
byte[] trimmedDecrypted = Arrays.copyOf(decrypted, actualLength);
assertArrayEquals("CBC mode with zeros padding failed", testData, trimmedDecrypted);
}
@Test
public void testEcbPkcs7Padding() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] testData = new byte[20];
Arrays.fill(testData, (byte)0x42);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7PADDING", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(testData);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = cipher.doFinal(encrypted);
assertArrayEquals("ECB mode with PKCS7 padding failed", testData, decrypted);
}
@Test
public void testEcbPkcs5Padding() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] testData = new byte[20];
Arrays.fill(testData, (byte)0x42);
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(testData);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = cipher.doFinal(encrypted);
assertArrayEquals("ECB mode with PKCS5 padding failed", testData, decrypted);
}
@Test
public void testEcbIso7816Padding() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] testData = new byte[20];
Arrays.fill(testData, (byte)0x42);
Cipher cipher = Cipher.getInstance("AES/ECB/ISO7816PADDING", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(testData);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = cipher.doFinal(encrypted);
assertArrayEquals("ECB mode with ISO7816 padding failed", testData, decrypted);
}
@Test
public void testEcbZerosPadding() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256);
SecretKey key = keyGen.generateKey();
byte[] testData = new byte[20];
Arrays.fill(testData, (byte)0x42);
Cipher cipher = Cipher.getInstance("AES/ECB/ZEROSPADDING", HiTls4jProvider.PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encrypted = cipher.doFinal(testData);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypted = cipher.doFinal(encrypted);
int actualLength = testData.length;
byte[] trimmedDecrypted = Arrays.copyOf(decrypted, actualLength);
assertArrayEquals("ECB mode with zeros padding failed", testData, trimmedDecrypted);
}
@Test
public void testInvalidKeyAndIvRejected() throws Exception {
SecretKeySpec shortKey = new SecretKeySpec(new byte[15], "AES");
Cipher ecbCipher = Cipher.getInstance("AES/ECB/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
try {
ecbCipher.init(Cipher.ENCRYPT_MODE, shortKey);
fail("Expected InvalidKeyException for short AES key");
} catch (InvalidKeyException expected) {
}
SecretKeySpec validKey = new SecretKeySpec(new byte[16], "AES");
Cipher cbcCipher = Cipher.getInstance("AES/CBC/NOPADDING", HiTls4jProvider.PROVIDER_NAME);
try {
cbcCipher.init(Cipher.ENCRYPT_MODE, validKey);
fail("Expected InvalidKeyException when CBC IV is missing");
} catch (InvalidKeyException expected) {
}
try {
cbcCipher.init(Cipher.ENCRYPT_MODE, validKey, new IvParameterSpec(new byte[8]));
fail("Expected InvalidAlgorithmParameterException for short CBC IV");
} catch (InvalidAlgorithmParameterException expected) {
}
}
private static void assertCipherVector(String transformation, String keyHex, String ivHex,
String plaintextHex, String ciphertextHex) throws Exception {
SecretKeySpec key = new SecretKeySpec(hex(keyHex), "AES");
byte[] plaintext = hex(plaintextHex);
byte[] expectedCiphertext = hex(ciphertextHex);
Cipher cipher = Cipher.getInstance(transformation, HiTls4jProvider.PROVIDER_NAME);
if (ivHex == null) {
cipher.init(Cipher.ENCRYPT_MODE, key);
} else {
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(hex(ivHex)));
}
byte[] ciphertext = cipher.doFinal(plaintext);
assertArrayEquals("Ciphertext mismatch for " + transformation, expectedCiphertext, ciphertext);
if (ivHex == null) {
cipher.init(Cipher.DECRYPT_MODE, key);
} else {
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(hex(ivHex)));
}
assertArrayEquals("Plaintext mismatch for " + transformation, plaintext, cipher.doFinal(ciphertext));
}
private static byte[] hex(String hex) {
if ((hex.length() & 1) != 0) {
throw new IllegalArgumentException("Hex string must have even length");
}
byte[] bytes = new byte[hex.length() / 2];
for (int i = 0; i < bytes.length; i++) {
int hi = Character.digit(hex.charAt(i * 2), 16);
int lo = Character.digit(hex.charAt(i * 2 + 1), 16);
if (hi < 0 || lo < 0) {
throw new IllegalArgumentException("Invalid hex string");
}
bytes[i] = (byte) ((hi << 4) | lo);
}
return bytes;
}
}