My name is Philipp C. Heckel and I write about nerdy things.

CipherInputStream for AEAD modes is insecure in JDK7 (GCM, EAX, etc.)


Programming, Security

CipherInputStream for AEAD modes is insecure in JDK7 (GCM, EAX, etc.)


If you have a little bit of cryptography know-how, you’ve heard of GCM, EAX and other Authenticated Encryption with Associated Data (AEAD) block cipher modes of operation. If you haven’t, AEAD modes not only encrypt data, but also authenticate it so that the ciphertext cannot be tampered without detection. In addition to that, AEAD modes can addionally authenticate additional (unencrypted) data — header data for example.

Java’s cryptography interface abstracts the underlying cipher very neatly. If you don’t have any associated data, using an AEAD mode is just like using a mode that doesn’t protect ciphertext integrity: Independent of the actual cipher and mode you are using, the Cipher class behaves identical once it has been initialized. For stream processing, the JDK additionally offers a CipherOutputStream and CipherInputStream. Input and output streams are very easy to nest so that one can compress, encrypt and sign data just by chaining different streams.

So far so good. So what’s the problem? The problem is that the CipherInputStream is terribly broken when used with an AEAD mode. In this post, I’d like to demonstrate how.


Content


1. Source Code

This post shows excerpts of source code. If you’d like to download and compile the source yourself, the fully functional code can be found on GitHub. Install instructions are also posted there.

2. CipherInputStream broken? It decrypts just fine! But …

Yes, the CipherInputStream performs the task of decrypting a ciphertext very well and it does that in a very convenient manner. However, unlike encryption-only modes such as CBC, AEAD modes also about authenticate data — meaning that a CipherInputStream does not only have to care about encrypting correctly, but also about checking that the calculated tag during decryption matches the tag at the end of the ciphertext.

You might ask yourself: Doesn’t the underlying cipher — in the case of GCM, that’s the GCMBlockCipher class — take care of that? Yes, it does. And like it should, it throws an exception if the calculated message authentication code (MAC) of an altered ciphertext doesn’t match. It kind of throws the “wrong” exception (BadPaddingException, something like InvalidMacException be a better name), but at least it actually throws an exception. Here’s exactly how that looks like if you tamper with the ciphertext and try to decrypt it using the Cipher class:

So far so good, but here’s the problem: While the Cipher class passes this exception to the application code, the JDK6/7 implementation of the CipherInputStream chooses to ignore this exception by doing the following (look at line 7 and 8):

Yes, that’s right. The catch-block of the BadPaddingException is empty. Nothing is passed on to the application and the application has no way of reacting to the MAC verification failure.

3. Example: AES/GCM with three CipherInputStream implementations

In case you haven’t fully processed the impact of this tiny little bug in the CipherInputStream class, here’s an example. Suppose you encrypt something with AES 128-bit in GCM mode, say a payment confirmation that says “I confirm that I pay 100$”.

3.1. Encrypting with AES/GCM

Here’s a piece of code to do that using Java’s JCA/JCE and the BouncyCastle cryptography provider (the code is just an excerpt; if you’re interested, check out the full code on GitHub):

After that, the originalCiphertext byte array contains a 32 bytes long array. The first 16 are the ciphertext of the message, and the last 16 bytes are the tag (= MAC). Depending on the key and the initialization vector (IV), it might look something like this (in HEX representation):

ba0e6eddfe4c40d3601bdd7e96c4b7defcebddd855b1b2cd37b92151db766928

3.2. Tampering with the ciphertext

After you’ve encrypted it, you send the message to some recipient (or store the message on your disk) and an evil attacker tampers with it — meaning he changes a few bytes of the encrypted text (ciphertext):

If not properly authenticated, GCM behaves much like with CBC: XOR-ing the ciphertext with a value changes the resulting plaintext accordingly (at least for the first block!). So if the attacker XORs the ciphertext at index 8 (position of the “1” in “100$”) with 0x08, the corresponding plaintext becomes a 9 — meaning that “100$” becomes “900$”.

3.3. Decrypting with javax.crypto.Cipher (okay)

Now to the interesting part: The attacker has tampered with the ciphertext and the recipient received the data. Let’s see what happens if we decrypt it. First we’ll try the Cipher class directly, and then a few implementations of the CipherInputStream.

Using the Cipher class, everything works like expected: The GCM implementation in BouncyCastle and the Cipher class in the javax.crypto package behave correctly — meaning that the GCM implementation itself is not an issue:

In the case above that means that a BadPaddingException is thrown when we try to decrypt the altered ciphertext. Although (like I said) the naming of this exception is a bit off, it’s way better than not throwing an exception at all.

While this example works perfectly fine, using the Cipher class manually (without any streaming possibilities) is not practical. So we want something that inherits from InputStream.

3.4. Decrypting with javax.crypto.CipherInputStream (insecure!)

Now let’s come to the interesting part: The broken CipherInputStream in the javax.crypto package. As I described in the introduction, this class simply ignores BadPaddingExceptions and doesn’t pass them to the application.

Tampering with the ciphertext does not throw a MAC verification error. The decrypted plaintext actually doesn’t match the original plaintext, and message integrity goes out the window. For our example above, that means that the decrypted plaintext is not “Confirm 100$ pay”, but instead we get “Confirm 900$ pay“.

Obviously this is a devestating result: This class is widely used by many applications and since AEAD modes are become more and more popular, this issue is becoming more relevant every day. As of today, this issue is still present in the code for JDK8 (was at: http://hg.openjdk.java.net/jdk8/build/jdk/file/43386cc9a017/src/share/classes/javax/crypto/CipherInputStream.java, site now defunct, July 2019). I will try to post something on the OpenJDK security-dev mailing list.

So what’s the solution? For starters: Don’t use javax.crypto.CipherInputStream with any AEAD mode!

3.5. Decrypting with a “fixed” javax.crypto.CipherInputStream (insecure!)

What else? How about we fix the current implementation of the CipherInputStream. The easiest way to do this is to simply fill the gaps of the catch-blocks and re-throw an appropriate exception:

Because the close() method can only throw IOExceptions, the thrown exception must inherit from this class — so I defined a QuickFixDemoInvalidCipherTextIOException (similar to what BouncyCastle does, see below). I called the “fixed” stream QuickFixDemoCipherInputStream:

When doFinal() in the Cipher class inside the QuickFixDemoCipherInputStream, a BadPaddingException is thrown and passed to the application wrapped in a QuickFixDemoInvalidCipherTextIOException (inner class). This way, MAC verification errors can be detected.

So is this the solution then? Should you use this? No, do not use this class! Why not?

This is a quick and dirty fix and it works just for this test case. I have not tested this fix anywhere else. There was probably a reason why the OpenJDK developers left the catch-block empty. Also: I am not a cryptographer!

3.6. Decrypting with org.bouncycastle.crypto.io.CipherInputStream (broken in 1.50, fixed in future versions)

After telling you one unpractical option, one broken option and one unreliable option how to decrypt in GCM mode (or other AEAD modes), here’s how it actually works. The newest BouncyCastle provider (JDK5on, 1.50, December 2013) has an implementation of a CipherInputStream that supports AEAD block ciphers. This is a brand new feature — version 1.49 did not support AEAD ciphers!

Here’s how you use them to decrypt our ciphertext:

As expected this implementation works very nicely. The altered ciphertext is rejected and the close() method of the CipherInputStream throws an InvalidCipherTextIOException exception. This is the class you should use. BouncyCastle is a well-respected library and is frequently reviewed by cryptographers. If there are bugs, they are probably found and fixed in the next versions.

However, there is one unfortunate inconvenience with this class. As you can see, the initialization of this class is very different from the generic JCA-way of doing things — i.e. calling Cipher.getInstance("AES/GCM/..") and then calling the init() method is not possible. In fact, the AEADCipher interface is not compatible or derived from the JCA Cipher class at all. That’s a pity but not so much of a sacrifice for a GCM-compatible CipherInputStream. And who knows, maybe they’ll offer a way to initialize the AEADBlockCipher through the standard JCA API at a later time.

Update 2 March 2014: Unfortunately I discovered that the BouncyCastle CipherInputStream has its own issues. When plaintext with >4079 bytes is encrypted, the descryption fails with an ArrayIndexOutOfBoundsException. The “Test E” in the code on GitHub demonstrates this behavior. Right now, I don’t think there is any working CipherInputStream out there that supports GCM! Maybe I just don’t know how to use them, but I doubt that an array index exception is supposed to be thrown in any case.

Update 4 March 2014: After a discussion on the BC mailing list (same link as above), David Hook fixed the CipherInputStream in Bouncy Castle (broken in v1.50). The full and working version of the CipherInputStream can be found on GitHub and will be available in BC 1.51. See the Test G in tests on GitHub. It works perfectly fine with the new class. I also noticed that David Hook is the author of Beginning Cryptography with Java — a book I own and read. Pretty neat!

4. Other issues with javax.crypto.CipherInputStream

This post is about the issues with message integrity in AEAD modes when using a CipherInputStream. However, the ignored catch blocks are not the only issues with this class:

5. Summary

The current implementation of javax.crypto.CipherInputStream in OpenJDK6 and OpenJDK7 does not protect message integrity in AEAD modes. This post demonstrated that AES/GCM-encrypted ciphertext can be altered in such a way that if it is decrypted with the CipherInputStream, no exception is thrown and it decrypts to the wrong plaintext. To this day, this error is not yet fixed in OpenJDK8 (was at: http://hg.openjdk.java.net/jdk8/build/jdk/file/43386cc9a017/src/share/classes/javax/crypto/CipherInputStream.java, site now defunct, July 2019). Howver, there’s a bug that describes exactly this issue, and another bug that will apparently solve this for JDK8.

The BouncyCastle library provides a org.bouncycastle.crypto.io.CipherInputStream that supports AEAD modes and handles MAC errors correctly (by throwing an exception). This class should be used instead of the OpenJDK provided class.

8 Comments

  1. Vineeth

    Hi Phil,

    I have say its very good blog and it really helped me to solve an issue which has been mentioned in your blog. However, I am still facing an issue while decryptiing using your fixed CipherInputStream code, I am getting “org.bouncycastle.crypto.InvalidCipherTextException: mac check in GCM failed”. As far I know this exception is thrown when we tamper the plaintext? In my scenario I am encrpting an stream and storing into database as an BLOB and next step I retrieving this BLOB data and converting into an stream “blob.getBinaryStream()” and trying to decrypt it using as key I used for encryption. I am getting below exception
    Caused by: org.bouncycastle.crypto.InvalidCipherTextException: mac check in GCM failed
    at org.bouncycastle.crypto.modes.GCMBlockCipher.doFinal(Unknown Source)

    Any help would be appreciated.

    Thanks In Advance
    Vineeth



  2. Vineeth T Shetty

    Hi Phil,

    Thanks for your response. I have sorted this isue it was with code I have implemented was not escaping first sixteen extra char ( IV) .

    Cheers,
    Vineeth


  3. Martin Andersson

    I get CipherInputStream to work on Java 8u25 not using Bounty Castle, fact is I haven’t even tried BC. Here’s what I did.

    1. I get the Cipher instance using Cipher.getInstance(“AES/GCM/NoPadding”) or Cipher.getInstance(“AES_128/GCM/NoPadding”).

    2. Replace the IV specification parameter in your Cipher.init method calls from “new IvParameterSpec(randomIv)” to “new GCMParameterSpec(96, randomIv)” (otherwise, Java throw a InvalidAlgorithmParameterException when calling Cipher.init).

    The result is that “testB_JavaxCipherInputStreamWithAesGcm” now throw an IOException with a cause set: “javax.crypto.AEADBadTagException: Tag mismatch!”.

    I haven’t found a word about this change in Oracle’s release notes. Now when it comes to cryptography, I am a newbie. So, does CipherInputStream work in Java 8u25 or have I got something wrong?

    Many thanks for a great post =)


  4. Roy Reshef

    Thanks Philipp for the thorough testing and signalling of the problem.
    As Martin has noted above, Java 8u25 has fixed the javax.crypto.CipherInputStream bug and it now neatly throws an exception. In 8u20 the fix was not there (I tested with both, BTW I didn’t need to replace the parameter spec, it worked fine with Phillip’s code).
    This fix, just like other security fixes, is NOT documented in the release notes (http://www.oracle.com/technetwork/java/javase/8u25-relnotes-2296185.html). This one just refers security bug fixes to http://www.oracle.com/technetwork/topics/security/cpuoct2014-1972960.html, which again has no details.
    I have taken a look at the new javax.crypto.CipherInputStream class – the close() method has been changed indeed to throw an IOException if it catches an exception:


  5. Roy Reshef

    Same fix has also been applied to Java 7u71 (I have verified this).


  6. Philipp C. Heckel

    Hello Roy, thanks for sharing. I haven’t been able to test this yet, but it’s good to hear that it’s fixed. However, I am wondering why the Oracle developers had a change of heart. They were pretty set on their idea of not outputting any unverified data in the first place…

    I’ll update the post when I have the time :)


  7. Jonathan Cobb

    This blog article is a several years old, but is still linked authoritatively from Stack Overflow.

    I wanted to post an update, that this problem seems to have been resolved somewhere between Java 8 and Java 11.

    At least in Java 11, altering the ciphertext *does* result in an error: “java.io.IOException: javax.crypto.BadPaddingException: mac check in GCM failed”

    This is in spite of the fact that the empty catch block still exists. I suppose that condition no longer occurs because the underlying stream consumes all the the bytes, leaving nothing to read after the “done” flag is set.

    You can confirm this yourself, by downloading the blog author’s code and running it on Java 11. The testB_JavaxCipherInputStreamWithAesGcm test no longer behaves as it did when this post was written — on Java 11 it throws the “mac check in GCM failed” exception quoted above.