Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8016171

CipherInputStream masks ciphertext tampering with AEAD ciphers in decrypt mode

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Duplicate
    • Icon: P3 P3
    • None
    • 7u17
    • security-libs

      FULL PRODUCT VERSION :
      java version " 1.7.0_17 "
      Java(TM) SE Runtime Environment (build 1.7.0_17-b02)
      Java HotSpot(TM) 64-Bit Server VM (build 23.7-b01, mixed mode)


      ADDITIONAL OS VERSION INFORMATION :
      Darwin duckbookii.orion.internal 12.3.0 Darwin Kernel Version 12.3.0: Sun Jan 6 22:37:10 PST 2013; root:xnu-2050.22.13~1/RELEASE_X86_64 x86_64


      A DESCRIPTION OF THE PROBLEM :
        From Java 7, AEAD ciphers are supported in the API and are expected to throw javax.crypto.AEADBadTagException " when a {@link Cipher} operating in an AEAD mode (such as GCM/CCM) is unable to verify the supplied authentication tag. "

      When such a cipher is used with CipherInputStream in decrypt mode (i.e. the Cipher decrypts and validates the ciphertext), tampering and truncation attacks on the ciphertext are silently hidden from the end user.

      The root cause of this is twofold:
      - AEADBadTagException extends javax.crypto.BadPaddingException
      - CipherInputStream (in getMoreData() and close()) silently eats BadPaddingExceptions, treating them as an indicator of stream exhaustion.

      This behaviour causes three distinct but related issues:
      1. 'non-compliant' AEAD cipher implementations (i.e. ones that pre-date the Java 7 AEAD API additions that throw BadPaddingException (or IllegalBlockSizeException) when a tag is invalid (i.e. due to ciphertext tampering) will have that tampering silently masked when used with a CipherInputStream.
      2. 'non-compliant' AEAD cipher implementations that throw BadPaddingException or IllegalBlockSizeException when a tag is truncated (i.e. the ciphertext is truncated near to a block boundary so that there is insufficient data remaining for the tag) will have that truncation silently masked when used with a CipherInputStream.
      3. AEAD ciphers implementations that throw AEADBadTagException in either of the above two scenarios will also have the tampering or truncation silently masked when used with a CipherInputStream.

      The following AEAD cipher implementations are known to be affected by one or more of these issues:
      Issue #1 and #2: */GCM, */OCB implementations in Bouncy Castle 1.49
      Issue #2: */CCM and */EAX implementations in Bouncy Castle 1.49
      Issue #3: AES/GCM implementation in OpenJDK 8

      This is a serious issue since it violates reasonable and critical assumptions of the behaviour of an AEAD cipher - i.e. that due to the authentication inherent in an AEAD cipher, it should not be possible to silently decrypt a tampered ciphertext.

      Even outside the context of AEAD ciphers, the silent masking of any corruption in the underlying ciphertext, such as truncation of data or invalid padding, is a highly dubious behaviour - as a programmer using a stream model over a ciphertext I would expect any structural issues in the ciphertext to be re-thrown as IOExceptions (just as I would expect errors in TCP transmission of data to do the same).

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      1. Construct a valid AEAD ciphertext (e.g. using AES/GCM/NoPadding)
      2. Tamper with the ciphertext (e.g. by adding 1 to the first byte)
      3. Construct a CipherInputStream with a Cipher in DECRYPT_MODE over the tampered ciphertext
      4. Read all bytes from the CipherInputSream

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      Execution fails with an IOException indicating that the ciphertext has an invalid tag (i.e. with an AEADBadTagException root cause).
      ACTUAL -
      No exceptions are thrown, some of the data (8 bytes in the case of AES/GCM) is not returned from the CipherInputStream.

      REPRODUCIBILITY :
      This bug can be reproduced always.

      ---------- BEGIN SOURCE ----------
      The code below runs against the AES/GCM implementation in OpenJDK 8.
      It should fail, but instead completes, but returns only 992 of the 1000 original bytes from the input stream.

      public class CipherInputStreamBadPadding
      {
          public static void main(String[] args) throws Exception
          {
              Cipher c = Cipher.getInstance( " AES/GCM/NoPadding " , " SunJCE " );
              Key key = new SecretKeySpec(new byte[16], " AES " );
              byte[] iv = new byte[16];
              c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
              byte[] pt = new byte[1000];
              byte[] ct = c.doFinal(pt);
              
              // Tamper with ciphertext - should fail tag check on decrypt
              ct[0] = (byte)(ct[0] + 1);
              
              c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));
              
              CipherInputStream cin = new CipherInputStream(new ByteArrayInputStream(ct), c);

              // Read until no more data, forces Cipher.doFinal() in getMoreData()
              int count = 0;
              while (cin.read() != -1)
              {
                  count++;
              }
              // Don't close due to other bug in CipherInputStream with AEAD ciphers
              // cin.close();
              System.err.println(count);
          }

      }


      The code below runs in Java SE 7 against BouncyCastle 1.49 and has the same behaviour.

      public class CipherInputStreamBadPadding
      {
          public static void main(String[] args) throws Exception
          {
              Security.addProvider(new BouncyCastleProvider());
              
              Cipher c = Cipher.getInstance( " AES/GCM/NoPadding " , " BC " );
              Key key = new SecretKeySpec(new byte[16], " AES " );
              byte[] iv = new byte[16];
              c.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
              byte[] pt = new byte[1000];
              byte[] ct = c.doFinal(pt);
              
              // Tamper with ciphertext - should fail tag check on decrypt
              ct[0] = (byte)(ct[0] + 1);
              
              c.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
              
              CipherInputStream cin = new CipherInputStream(new ByteArrayInputStream(ct), c);

              // Read until no more data, forces Cipher.doFinal() in getMoreData()
              int count = 0;
              while (cin.read() != -1)
              {
                  count++;
              }
              // Don't close due to other bug in CipherInputStream with AEAD ciphers
              // cin.close();
              System.err.println(count);
          }

      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      The only workaround is to not use CipherInputStream.

            valeriep Valerie Peng
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: