CipherInputStream for AEAD modes is insecure (GCM, etc.): ciphertext tampering without detection possible

Xuelei Fan xuelei.fan at oracle.com
Mon Mar 10 08:53:04 UTC 2014


Hi Tim,

Sorry, I should have been cleaner on this point.  What I wanted to
express is that I understood your concerns.  Thanks for the detailed
description and examples.

Thanks,
Xuelei

On 3/10/2014 3:37 PM, Tim Whittington wrote:
> 
> On 10/03/2014, at 1:12 pm, Xuelei Fan <xuelei.fan at oracle.com> wrote:
> 
>> On 3/10/2014 2:31 AM, Tim Whittington wrote:
>>>
>>> On 9/03/2014, at 10:50 pm, Tim Whittington
>>> <jdk-security-dev at whittington.net.nz
>>> <mailto:jdk-security-dev at whittington.net.nz>> wrote:
>>>
>>>>
>>>> On 7/03/2014, at 9:14 am, Philipp Heckel <philipp.heckel at gmail.com
>>>> <mailto:philipp.heckel at gmail.com>> wrote:
>>>>
>>>>> - Using javax.crypto.CipherInputStream with a cipher in GCM mode and
>>>>> the SunJCE provider (JDK8+) is secure, but cannot be used large
>>>>> files, because it will buffer all data until the tag is verified (as
>>>>> defined by the GCM spec) [1]
>>>>
>>>> This (the part about it being secure) is not correct - when using
>>>> javax.crypto.CipherInputStream with a cipher in GCM mode and the
>>>> SunJCE provider (JDK8+) any tampering with the ciphertext will
>>>> silently treat the result as a 0 byte authenticated stream.
>>>>
>>>
>>> Sorry, I should have been clearer here - this problem occurs with any
>>> provider (and any AE mode) not just the SunJCE GCM implementation.
>>>
>> It only happens before the authentication tag get checked.  If the authN
>> tag get read, the tampering can be detected.
>>
>> Your concerns happen to any mode, even CBC mode.  Better to make an
>> improvement here if possible.
> 
> I don’t quite understand what you’re saying here - what I’m saying is that the auth tag is read, the tampering is detected, and then CipherInputStream suppresses the AEADBadTagException, resulting in the reader of the stream interpreting it as a valid (but empty) ciphertext.
> 
> i.e. the code snippet below should print:
> 
> Via CipherInputStream: ciphertext is invalid
> Via Cipher: ciphertext is invalid
> 
> but instead it prints:
> 
> Via CipherInputStream: ciphertext is valid and -1 bytes long
> Via Cipher: ciphertext is invalid
> 
> If you repeat the test with a 0 byte plaintext, and without tampering with the ciphertext, it will print:
> 
> Via CipherInputStream: ciphertext is valid and -1 bytes long
> Via Cipher: ciphertext is valid and 0 bytes long
> 
> Notice that when using CipherInputStream, a tampered ciphertext and a 0 byte valid ciphertext produce the same result, while Cipher behaves correctly by failing the auth on the tampered one.
> This behaviour is what I was saying is insecure - that javax.crypto.CipherInputStream does similar questionable things for bad padding in CBC mode makes no difference.
> 
> From the JavaDoc of InputStream.read(byte[]):  IOException is thrown "If the first byte cannot be read for any reason other than the end of the file”.
> (The first byte in this case is the first byte in the stream that would be placed at b[0]). 
> In my opinion, an AEADBadTagException thrown by a Cipher that prevents it from returning any bytes is such a reason, and as such should cause an IOException to be thrown.
> 
> tim
> 
> 
> ——
> 
>             Cipher c = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
>             c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(new byte[16], "AES"), new GCMParameterSpec(128, new byte[16]));
>             byte[] ct = c.doFinal(new byte[100]);
> 
>             c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(new byte[16], "AES"), new GCMParameterSpec(128, new byte[16]));
>             ct[ct.length - 1] = (byte)(ct[ct.length - 1] + 1);
> 
>             byte[] read = new byte[1000];
>             InputStream in = new javax.crypto.CipherInputStream(new ByteArrayInputStream(ct), c);
>             try
>             {
>                 int size = in.read(read);
>                 System.out.println("Via CipherInputStream: ciphertext is valid and " + size + " bytes long");
>             }
>             catch (IOException e)
>             {
>                 System.errr.println("Via CipherInputStream: ciphertext is invalid");
>             }
>             finally
>             {
>                 in.close();
>             }
> 
>             try
>             {
>                 byte[] pt = c.doFinal(ct);
>                 System.out.println("Via Cipher: ciphertext is valid and " + pt.length + " bytes long");
>             }
>             catch (AEADBadTagException e)
>             {
>                 System.err.println("Via CipherInputStream: ciphertext is invalid");
>             }
> 
> 




More information about the security-dev mailing list