package org.bouncycastle.crypto.test; import org.bouncycastle.crypto.CipherParameters; import org.bouncycastle.crypto.DataLengthException; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.OutputLengthException; import org.bouncycastle.crypto.modes.AEADBlockCipher; import org.bouncycastle.crypto.params.AEADParameters; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.test.SimpleTestResult; import org.bouncycastle.util.test.Test; import org.bouncycastle.util.test.TestFailedException; public class AEADTestUtil { public static void testTampering(Test test, AEADBlockCipher cipher, CipherParameters params) throws InvalidCipherTextException { byte[] plaintext = new byte[1000]; for (int i = 0; i < plaintext.length; i++) { plaintext[i] = (byte)i; } cipher.init(true, params); byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)]; int len = cipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0); cipher.doFinal(ciphertext, len); int macLength = cipher.getMac().length; // Test tampering with a single byte cipher.init(false, params); byte[] tampered = new byte[ciphertext.length]; byte[] output = new byte[plaintext.length]; System.arraycopy(ciphertext, 0, tampered, 0, tampered.length); tampered[0] += 1; cipher.processBytes(tampered, 0, tampered.length, output, 0); try { cipher.doFinal(output, 0); throw new TestFailedException( new SimpleTestResult(false, test + " : tampering of ciphertext not detected.")); } catch (InvalidCipherTextException e) { // Expected } // Test truncation of ciphertext to < tag length cipher.init(false, params); byte[] truncated = new byte[macLength - 1]; System.arraycopy(ciphertext, 0, truncated, 0, truncated.length); cipher.processBytes(truncated, 0, truncated.length, output, 0); try { cipher.doFinal(output, 0); fail(test, "tampering of ciphertext not detected."); } catch (InvalidCipherTextException e) { // Expected } } private static void fail(Test test, String message) { throw new TestFailedException(SimpleTestResult.failed(test, message)); } private static void fail(Test test, String message, String expected, String result) { throw new TestFailedException(SimpleTestResult.failed(test, message, expected, result)); } public static void testReset(Test test, AEADBlockCipher cipher1, AEADBlockCipher cipher2, CipherParameters params) throws InvalidCipherTextException { cipher1.init(true, params); byte[] plaintext = new byte[1000]; byte[] ciphertext = new byte[cipher1.getOutputSize(plaintext.length)]; // Establish baseline answer crypt(cipher1, plaintext, ciphertext); // Test encryption resets checkReset(test, cipher1, params, true, plaintext, ciphertext); // Test decryption resets with fresh instance cipher2.init(false, params); checkReset(test, cipher2, params, false, ciphertext, plaintext); } private static void checkReset(Test test, AEADBlockCipher cipher, CipherParameters params, boolean encrypt, byte[] pretext, byte[] posttext) throws InvalidCipherTextException { // Do initial run byte[] output = new byte[posttext.length]; crypt(cipher, pretext, output); // Check encrypt resets cipher crypt(cipher, pretext, output); if (!Arrays.areEqual(output, posttext)) { fail(test, (encrypt ? "Encrypt" : "Decrypt") + " did not reset cipher."); } // Check init resets data cipher.processBytes(pretext, 0, 100, output, 0); cipher.init(encrypt, params); try { crypt(cipher, pretext, output); } catch (DataLengthException e) { fail(test, "Init did not reset data."); } if (!Arrays.areEqual(output, posttext)) { fail(test, "Init did not reset data.", new String(Hex.encode(posttext)), new String(Hex.encode(output))); } // Check init resets AD cipher.processAADBytes(pretext, 0, 100); cipher.init(encrypt, params); try { crypt(cipher, pretext, output); } catch (DataLengthException e) { fail(test, "Init did not reset additional data."); } if (!Arrays.areEqual(output, posttext)) { fail(test, "Init did not reset additional data."); } // Check reset resets data cipher.processBytes(pretext, 0, 100, output, 0); cipher.reset(); try { crypt(cipher, pretext, output); } catch (DataLengthException e) { fail(test, "Init did not reset data."); } if (!Arrays.areEqual(output, posttext)) { fail(test, "Reset did not reset data."); } // Check reset resets AD cipher.processAADBytes(pretext, 0, 100); cipher.reset(); try { crypt(cipher, pretext, output); } catch (DataLengthException e) { fail(test, "Init did not reset data."); } if (!Arrays.areEqual(output, posttext)) { fail(test, "Reset did not reset additional data."); } } private static void crypt(AEADBlockCipher cipher, byte[] plaintext, byte[] output) throws InvalidCipherTextException { int len = cipher.processBytes(plaintext, 0, plaintext.length, output, 0); cipher.doFinal(output, len); } public static void testOutputSizes(Test test, AEADBlockCipher cipher, AEADParameters params) throws IllegalStateException, InvalidCipherTextException { int maxPlaintext = cipher.getUnderlyingCipher().getBlockSize() * 10; byte[] plaintext = new byte[maxPlaintext]; byte[] ciphertext = new byte[maxPlaintext * 2]; // Check output size calculations for truncated ciphertext lengths cipher.init(true, params); cipher.doFinal(ciphertext, 0); int macLength = cipher.getMac().length; cipher.init(false, params); for (int i = 0; i < macLength; i++) { cipher.reset(); if (cipher.getUpdateOutputSize(i) != 0) { fail(test, "AE cipher should not produce update output with ciphertext length <= macSize"); } if (cipher.getOutputSize(i) != 0) { fail(test, "AE cipher should not produce output with ciphertext length <= macSize"); } } for (int i = 0; i < plaintext.length; i++) { cipher.init(true, params); int expectedCTUpdateSize = cipher.getUpdateOutputSize(i); int expectedCTOutputSize = cipher.getOutputSize(i); if (expectedCTUpdateSize < 0) { fail(test, "Encryption update output size should not be < 0 for size " + i); } if (expectedCTOutputSize < 0) { fail(test, "Encryption update output size should not be < 0 for size " + i); } int actualCTSize = cipher.processBytes(plaintext, 0, i, ciphertext, 0); if (expectedCTUpdateSize != actualCTSize) { fail(test, "Encryption update output size did not match calculated for plaintext length " + i, String.valueOf(expectedCTUpdateSize), String.valueOf(actualCTSize)); } actualCTSize += cipher.doFinal(ciphertext, actualCTSize); if (expectedCTOutputSize != actualCTSize) { fail(test, "Encryption actual final output size did not match calculated for plaintext length " + i, String.valueOf(expectedCTOutputSize), String.valueOf(actualCTSize)); } cipher.init(false, params); int expectedPTUpdateSize = cipher.getUpdateOutputSize(actualCTSize); int expectedPTOutputSize = cipher.getOutputSize(actualCTSize); if (expectedPTOutputSize != i) { fail(test, "Decryption update output size did not original plaintext length " + i, String.valueOf(expectedPTUpdateSize), String.valueOf(i)); } int actualPTSize = cipher.processBytes(ciphertext, 0, actualCTSize, plaintext, 0); if (expectedPTUpdateSize != actualPTSize) { fail(test, "Decryption update output size did not match calculated for plaintext length " + i, String.valueOf(expectedPTUpdateSize), String.valueOf(actualPTSize)); } actualPTSize += cipher.doFinal(plaintext, actualPTSize); if (expectedPTOutputSize != actualPTSize) { fail(test, "Decryption update output size did not match calculated for plaintext length " + i, String.valueOf(expectedPTOutputSize), String.valueOf(actualPTSize)); } } } public static void testBufferSizeChecks(Test test, AEADBlockCipher cipher, AEADParameters params) throws IllegalStateException, InvalidCipherTextException { int blockSize = cipher.getUnderlyingCipher().getBlockSize(); int maxPlaintext = (blockSize * 10); byte[] plaintext = new byte[maxPlaintext]; cipher.init(true, params); int expectedUpdateOutputSize = cipher.getUpdateOutputSize(plaintext.length); byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)]; try { cipher.processBytes(new byte[maxPlaintext - 1], 0, maxPlaintext, new byte[expectedUpdateOutputSize], 0); fail(test, "processBytes should validate input buffer length"); } catch (DataLengthException e) { // Expected } cipher.reset(); if (expectedUpdateOutputSize > 0) { int outputTrigger = 0; // Process bytes until output would be produced for(int i = 0; i < plaintext.length; i++) { if (cipher.getUpdateOutputSize(1) != 0) { outputTrigger = i + 1; break; } cipher.processByte(plaintext[i], ciphertext, 0); } if (outputTrigger == 0) { fail(test, "Failed to find output trigger size"); } try { cipher.processByte(plaintext[0], new byte[cipher.getUpdateOutputSize(1) - 1], 0); fail(test, "Encrypt processByte should validate output buffer length"); } catch (OutputLengthException e) { // Expected } cipher.reset(); // Repeat checking with entire input at once try { cipher.processBytes(plaintext, 0, outputTrigger, new byte[cipher.getUpdateOutputSize(outputTrigger) - 1], 0); fail(test, "Encrypt processBytes should validate output buffer length"); } catch (OutputLengthException e) { // Expected } cipher.reset(); } // Remember the actual ciphertext for later int actualOutputSize = cipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0); actualOutputSize += cipher.doFinal(ciphertext, actualOutputSize); int macSize = cipher.getMac().length; cipher.reset(); try { cipher.processBytes(plaintext, 0, plaintext.length, ciphertext, 0); cipher.doFinal(new byte[cipher.getOutputSize(0) - 1], 0); fail(test, "Encrypt doFinal should validate output buffer length"); } catch (OutputLengthException e) { // Expected } // Decryption tests cipher.init(false, params); expectedUpdateOutputSize = cipher.getUpdateOutputSize(actualOutputSize); if (expectedUpdateOutputSize > 0) { // Process bytes until output would be produced int outputTrigger = 0; for (int i = 0; i < plaintext.length; i++) { if (cipher.getUpdateOutputSize(1) != 0) { outputTrigger = i + 1; break; } cipher.processByte(ciphertext[i], plaintext, 0); } if (outputTrigger == 0) { fail(test, "Failed to find output trigger size"); } try { cipher.processByte(ciphertext[0], new byte[cipher.getUpdateOutputSize(1) - 1], 0); fail(test, "Decrypt processByte should validate output buffer length"); } catch (OutputLengthException e) { // Expected } cipher.reset(); // Repeat test with processBytes try { cipher.processBytes(ciphertext, 0, outputTrigger, new byte[cipher.getUpdateOutputSize(outputTrigger) - 1], 0); fail(test, "Decrypt processBytes should validate output buffer length"); } catch (OutputLengthException e) { // Expected } } cipher.reset(); // Data less than mac length should fail before output length check try { // Assumes AE cipher on decrypt can't return any data until macSize bytes are received if (cipher.processBytes(ciphertext, 0, macSize - 1, plaintext, 0) != 0) { fail(test, "AE cipher unexpectedly produced output"); } cipher.doFinal(new byte[0], 0); fail(test, "Decrypt doFinal should check ciphertext length"); } catch (InvalidCipherTextException e) { // Expected } try { // Search through plaintext lengths until one is found that creates >= 1 buffered byte // during decryption of ciphertext for doFinal to handle for (int i = 2; i < plaintext.length; i++) { cipher.init(true, params); int encrypted = cipher.processBytes(plaintext, 0, i, ciphertext, 0); encrypted += cipher.doFinal(ciphertext, encrypted); cipher.init(false, params); cipher.processBytes(ciphertext, 0, encrypted - 1, plaintext, 0); if (cipher.processByte(ciphertext[encrypted - 1], plaintext, 0) == 0) { cipher.doFinal(new byte[cipher.getOutputSize(0) - 1], 0); fail(test, "Decrypt doFinal should check output length"); cipher.reset(); // Truncated Mac should be reported in preference to inability to output // buffered plaintext byte try { cipher.processBytes(ciphertext, 0, actualOutputSize - 1, plaintext, 0); cipher.doFinal(new byte[cipher.getOutputSize(0) - 1], 0); fail(test, "Decrypt doFinal should check ciphertext length"); } catch (InvalidCipherTextException e) { // Expected } cipher.reset(); } } fail(test, "Decrypt doFinal test couldn't find a ciphertext length that buffered for doFinal"); } catch (OutputLengthException e) { // Expected } } static AEADParameters reuseKey(AEADParameters p) { return new AEADParameters(null, p.getMacSize(), p.getNonce(), p.getAssociatedText()); } }