diff options
Diffstat (limited to 'core/src/test/java/org/spongycastle/crypto/test/AEADTestUtil.java')
-rw-r--r-- | core/src/test/java/org/spongycastle/crypto/test/AEADTestUtil.java | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/core/src/test/java/org/spongycastle/crypto/test/AEADTestUtil.java b/core/src/test/java/org/spongycastle/crypto/test/AEADTestUtil.java new file mode 100644 index 00000000..e2ff7e4c --- /dev/null +++ b/core/src/test/java/org/spongycastle/crypto/test/AEADTestUtil.java @@ -0,0 +1,474 @@ +package org.spongycastle.crypto.test; + +import org.spongycastle.crypto.CipherParameters; +import org.spongycastle.crypto.DataLengthException; +import org.spongycastle.crypto.InvalidCipherTextException; +import org.spongycastle.crypto.OutputLengthException; +import org.spongycastle.crypto.modes.AEADBlockCipher; +import org.spongycastle.crypto.params.AEADParameters; +import org.spongycastle.util.Arrays; +import org.spongycastle.util.encoders.Hex; +import org.spongycastle.util.test.SimpleTestResult; +import org.spongycastle.util.test.Test; +import org.spongycastle.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()); + } +} |