diff options
author | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-11-16 11:39:21 +0100 |
---|---|---|
committer | Tor Brede Vekterli <vekterli@yahooinc.com> | 2022-11-16 14:33:42 +0100 |
commit | d1c10b7598a1fd1a17107ef5c2781819a3c57c52 (patch) | |
tree | 60a28568c1ff8efe0688afd7db7d25ab0d601913 /security-utils/src/test | |
parent | a91a411a0893853fd6f62dfeb9f31ec14c896604 (diff) |
Use BouncyCastle AES GCM cipher and I/O streams instead of JCA
This resolves two issues:
* `javax.crypto.OutputCipherStream` swallows MAC tag mismatch exceptions
when the stream is closed, which means that corruptions (intentional
or not) are not caught. This is documented behavior, but still very
surprising and a rather questionable default. BC's interchangeable
`CipherOutputStream` throws as expected. To avoid regressions, add an
explicit test that both ciphertext and MAC tag corruptions are propagated.
* The default-provided `AES/GCM/NoPadding` `Cipher` instance will not emit
decrypted plaintext per `update()` chunk, but buffer everything until
`doFinal()` is invoked when the stream is closed. This means that decrypting
very large ciphertexts can blow up memory usage since internal output
buffers are reallocated and increased per iteration...! Instead use an
explicit BC `GCMBlockCipher` which has the expected behavior (and actually
lets cipher streams, well, _stream_). Add an `AeadCipher` abstraction to
avoid leaking BC APIs outside the security module.
Diffstat (limited to 'security-utils/src/test')
-rw-r--r-- | security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java | 34 |
1 files changed, 30 insertions, 4 deletions
diff --git a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java index 23e22345cc6..aede100574d 100644 --- a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java +++ b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java @@ -3,8 +3,6 @@ package com.yahoo.security; import org.junit.jupiter.api.Test; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -142,7 +140,7 @@ public class SharedKeyTest { static byte[] streamEncryptString(String data, SecretSharedKey secretSharedKey) throws IOException { var cipher = SharedKeyGenerator.makeAesGcmEncryptionCipher(secretSharedKey); var outStream = new ByteArrayOutputStream(); - try (var cipherStream = new CipherOutputStream(outStream, cipher)) { + try (var cipherStream = cipher.wrapOutputStream(outStream)) { cipherStream.write(data.getBytes(StandardCharsets.UTF_8)); cipherStream.flush(); } @@ -154,7 +152,7 @@ public class SharedKeyTest { var inStream = new ByteArrayInputStream(encrypted); var total = ByteBuffer.allocate(encrypted.length); // Assume decrypted form can't be _longer_ byte[] tmp = new byte[8]; // short buf to test chunking - try (var cipherStream = new CipherInputStream(inStream, cipher)) { + try (var cipherStream = cipher.wrapInputStream(inStream)) { while (true) { int read = cipherStream.read(tmp); if (read == -1) { @@ -180,4 +178,32 @@ public class SharedKeyTest { assertEquals(terrifyingSecret, decrypted); } + // javax.crypto.CipherOutputStream swallows exceptions caused by MAC failures in cipher + // decryption mode (!) and must therefore _not_ be used for this purpose. This is documented, + // but still very surprising behavior. + @Test + void cipher_output_stream_tag_mismatch_is_not_swallowed() throws Exception { + var receiverKeyPair = KeyUtils.generateX25519KeyPair(); + var myShared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), KEY_ID_1); + String plaintext = "...hello world?"; + byte[] encrypted = streamEncryptString(plaintext, myShared); + // Corrupt MAC tag in ciphertext + encrypted[encrypted.length - 1] ^= 0x80; + // We don't necessarily know _which_ exception is thrown, but one _should_ be thrown! + assertThrows(Exception.class, () -> doOutputStreamCipherDecrypt(myShared, encrypted)); + // Also try with corrupted ciphertext (pre MAC tag) + encrypted[encrypted.length - 1] ^= 0x80; // Flip MAC bit back to correct state + encrypted[encrypted.length - 17] ^= 0x80; // Pre 128-bit MAC tag + assertThrows(Exception.class, () -> doOutputStreamCipherDecrypt(myShared, encrypted)); + } + + private static void doOutputStreamCipherDecrypt(SecretSharedKey myShared, byte[] encrypted) throws Exception { + var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(myShared); + var outStream = new ByteArrayOutputStream(); + try (var cipherStream = cipher.wrapOutputStream(outStream)) { + cipherStream.write(encrypted); + cipherStream.flush(); + } + } + } |