1 Reply Latest reply: Sep 3, 2011 1:10 AM by 884729 RSS

    Custom CipherInputStream fails at padding

    884729
      Hi,


      I have to write my own CipherInputStream. It works quite well (with AES) except for padding. Below is my CipherInputStream class. It fails when any decrypted block has 0x01 as the last byte of the block.

      Please be aware that the source of the ciphered stream might invoke "doFinal" once in a while without closing streams. Thus, the CipherInputStream has to handle multiple "ciphered messages". Searching google provided me with plenty of examples while in every example, the assumption was that the creator of the ciphered stream invokes "doFinal" right before he closes the stream. Yet, I want to continue the stream after someone invoked "doFinal".

      Here is the code.
      import java.io.FilterInputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.security.InvalidKeyException;
      import java.security.NoSuchAlgorithmException;
      
      import javax.crypto.BadPaddingException;
      import javax.crypto.Cipher;
      import javax.crypto.IllegalBlockSizeException;
      import javax.crypto.NoSuchPaddingException;
      import javax.crypto.SecretKey;
      
      public class CCipherInputStream extends InputStream {
           private Cipher m_Cipher;
           private InputStream m_Input;
           private boolean m_DoDecryption;
           private boolean m_IsClosed = false;
           
           private byte[] m_Buffer = null;
           private int m_EndOffset = -1;
           private int m_StartOffset = m_EndOffset;
           
           public boolean isDecrypting() { return m_DoDecryption; }
           
           private byte[] m_ReadFromCipher() throws IOException {
                byte[] inBuffer = (m_Cipher != null) ? new byte[Math.max(1, m_Cipher.getBlockSize())] : new byte[1];
                
                int pos = 0;
                int symbol = -1;
                while ((pos < inBuffer.length) && ((symbol = m_Input.read()) != -1)) { inBuffer[pos++] = (byte)(symbol & 0xFF); }
                
                byte[] result = m_Cipher.update(inBuffer, 0, pos);
                
                if (symbol == -1) { close(); }
                
                return (result == null) ? new byte[0] : result;
           }
           
           
           private byte[] m_ReadDecrypting() throws IOException {
                byte[] Data = m_ReadFromCipher();
      //          return m_Depadding(Data); /*
                return Data; //*/
           }
           
           
           private byte[] m_Depadding(byte[] Data) {
                // Check (from the end of the result, if padding does exist)
                if ((Data.length > 0) && (Data[Data.length-1] > 0) && (Data[Data.length-1] <= m_Cipher.getBlockSize())) {
                     // Padding likely
                     int number = Data[Data.length-1];
                     boolean isPadding = true;
                     for (int counter = 0; counter < number; counter++) {
                          if (Data[Data.length-1-counter] != number) {
                               isPadding = false;
                               break;
                          }
                     }
                     if (isPadding) {
                          byte[] newResult = new byte[Data.length - number];
                          System.arraycopy(Data, 0, newResult, 0, newResult.length);
                          return newResult;
                     } else { return Data; }
                } else { return Data; }
           }
           
           
           private void m_FinalizeCipher() {
                byte[] lastBytes;
                try {
                     lastBytes = m_Cipher.doFinal();
                     lastBytes = m_Depadding(lastBytes);
                     byte[] newBuffer = new byte[m_Buffer.length + lastBytes.length];
                     System.arraycopy(m_Buffer, 0, newBuffer, 0, m_Buffer.length);
                     System.arraycopy(lastBytes, 0, newBuffer, m_Buffer.length, lastBytes.length);
                     m_Buffer = newBuffer;
                     m_EndOffset = m_Buffer.length;
                } catch (IllegalBlockSizeException e) {
                } catch (BadPaddingException e) {
                }
           }
           
           
           public CCipherInputStream(InputStream Input, SecretKey Key) throws IOException {
                m_Input = Input;
                try {
                     m_Cipher = Cipher.getInstance(Key.getAlgorithm());
                     m_Cipher.init(Cipher.DECRYPT_MODE, Key);
                } catch (InvalidKeyException Cause) { throw new IOException(Cause); }
                catch (NoSuchPaddingException Cause) { throw new IOException(Cause); }
                catch (NoSuchAlgorithmException Cause) { throw new IOException(Cause); }
                m_DoDecryption = true;
           }
           
           public CCipherInputStream(InputStream Input) {
                m_Input = Input;
                m_DoDecryption = false;
                m_Cipher = null;
           }
           
           public boolean isClosed() { return m_IsClosed; }
           
           public void setKey(SecretKey Key) throws IOException {
                try {
                     m_Cipher = Cipher.getInstance(Key.getAlgorithm());
                     m_Cipher.init(Cipher.DECRYPT_MODE, Key);
                } catch (NoSuchAlgorithmException Cause) { throw new IOException(Cause); }
                catch (NoSuchPaddingException Cause) { throw new IOException(Cause); }
                catch (InvalidKeyException Cause) { throw new IOException(Cause); }
                m_DoDecryption = true;
           }
           
           
           public void disableDecryption() throws IOException {
                // TODO read last bytes from cipher
                // Disable encryption
                m_DoDecryption = false;
           }
           
           @Override public int available() throws IOException {
                // If there is data in buffer, return number of remaining bytes in buffer
                if (m_StartOffset < m_EndOffset) { return m_EndOffset - m_StartOffset; }
                else {
                     while (!m_IsClosed) {
                          if (m_DoDecryption) {
                               // Read encrypted data stream
                               m_Buffer = m_ReadDecrypting();
                               m_EndOffset = m_Buffer.length;
                          } else {
                               // Read plain data stream (single byte only)
                               byte[] buffer = new byte[1];
                               m_EndOffset = m_Input.read(buffer);
                               m_Buffer = new byte[m_EndOffset];
                               System.arraycopy(buffer, 0, m_Buffer, 0, m_EndOffset);
                          }
                          m_StartOffset = 0;
                          return m_EndOffset - m_StartOffset;
                     }
                     return -1;
                }
           }
           
           @Override public int read() throws IOException {
                if (available() > 0) { return (m_Buffer[m_StartOffset++]); }
                else { return -1; }
           }
           
           @Override public int read(byte[] Buffer) throws IOException { return read(Buffer, 0, Buffer.length); }
           
           @Override public int read(byte[] Buffer, int Offset, int Length) throws IOException {
                // If no data is requested, return immediately
                if (Length <= 0) { return 0; }
                // Check how much data is available
                int toRead = available();
                // If the stream is closed, 
                if (toRead < 0) { return -1; }
                // If there is more data available than shall be read, shorten
                if (toRead > Length) { toRead = Length; }
                if (Buffer != null) {
                     System.arraycopy(m_Buffer, m_StartOffset, Buffer, Offset, toRead);
                     m_StartOffset += toRead;
                     return toRead;
                } else { return 0; }
           }
           
           @Override public long skip(long N) throws IOException {
                // If there is nothing to skip, do not skip
                if (N < 1) { return 0; }
                // Read number of bytes currently available for skipping
                long toSkip = available();
                // If the stream is closed, 0 bytes were skipped
                if (toSkip < 0) { return 0; }
                // If more bytes are available that to skip, limit
                if (toSkip > N) { toSkip = N; }
                // Actually doing the skip
                m_StartOffset += toSkip;
                // Return number of skipped bytes
                return toSkip;
           }
           
           @Override public void close() throws IOException {
                m_IsClosed = true;
                m_FinalizeCipher();
                m_Input.close();
           }
           
           @Override public boolean markSupported() { return false; }
      }
      I also tried the OpenJDK 7 implementation, yet this would result in "Hello World55555" (eventually the 5 is 0x05) when encrypting "Hello World" using AES-128 .

      Thanks for any hints.
        • 1. Re: Custom CipherInputStream fails at padding
          884729
          Hi,

          even after so many nights of messing around with the problem, one night of sleep can help.

          As in my case only data became corrupted where the last byte of a cipher row (i.e. buffer[startindex, startindex + Cipher.getBlockSize]) was 0x01, I decided to implement a very trivial form of padding in the output stream as well. Just to share my experience, I have attached my output stream as well.
          import java.io.ByteArrayInputStream;
          import java.io.ByteArrayOutputStream;
          import java.io.IOException;
          import java.io.OutputStream;
          import java.security.InvalidKeyException;
          import java.security.NoSuchAlgorithmException;
          
          import javax.crypto.BadPaddingException;
          import javax.crypto.Cipher;
          import javax.crypto.IllegalBlockSizeException;
          import javax.crypto.NoSuchPaddingException;
          import javax.crypto.SecretKey;
          
          /** <p>Output stream that is capable to encrypt incoming data using a {@link
           * Cipher} with {@link SecretKey}.</p>
           * 
           * @author Bjoern Wuest, Germany
           * @version 2011-09-02
           *
           */
          public class CCipherOutputStream extends OutputStream {
               /** <p>The output stream to stream to.</p> */
               private OutputStream m_Output;
               /** <p>The cipher to use for encryption, if enabled.</p> */
               private Cipher m_Cipher;
               /** <p>Flag to indicate whether to use encryption or not.</p> */
               private boolean m_DoEncryption;
               
               
               /** <p>Create new output stream.</p>
                * 
                * <p>This output stream wraps the provided output stream and disables
                * encryption mode.</p>
                * 
                * @param Output The output stream to wrap.
                */
               public CCipherOutputStream(OutputStream Output) {
                    if (Output == null) { throw new NullPointerException("The underlying output stream cannot be the null object."); }
                    m_Output = Output;
                    // No cipher, no encryption
                    m_Cipher = null;
                    m_DoEncryption = false;
               }
               
               
               /** <p>Create new output stream.</p>
                * 
                * <p>This output stream wraps the provided output stream and enables
                * encryption mode with the provided secret key.</p>
                * 
                * @param Output The output stream to wrap.
                * @param Key The key to use for encryption.
                * @throws IOException If this stream could not be initialized due to
                * problems with setting up the cipher.
                * @throws NoSuchAlgorithmException If the algorithm of the secret key is not supported.
                * @throws NoSuchPaddingException If the padding given in the secret key is not supported.
                * @throws InvalidKeyException If the key itself is invalid.
                */
               public CCipherOutputStream(OutputStream Output, SecretKey Key) throws IOException {
                    if (Output == null) { throw new NullPointerException("The underlying output stream cannot be the null object."); }
                    m_Output = Output;
                    // Generate cipher
                    try {
                         m_Cipher = Cipher.getInstance(Key.getAlgorithm());
                         m_Cipher.init(Cipher.ENCRYPT_MODE, Key);
                    } catch (NoSuchAlgorithmException Cause) { throw new IOException(Cause); }
                    catch (NoSuchPaddingException Cause) { throw new IOException(Cause); }
                    catch (InvalidKeyException Cause) { throw new IOException(Cause); }
                    // Enable encryption mode
                    m_DoEncryption = true;
               }
               
               
               /** <p>Set new key for encryption and enables encryption.</p>
                * 
                * <p>Before encryption is enabled, buffered data is flushed.</p>
                * 
                * @param Key The key to use for encryption.
                * @throws IOException If flushing did not work or cipher could not be
                * initialized.
                */
               public void setKey(SecretKey Key) throws IOException {
                    // Flush what is in the stream
                    flush();
                    // Generate cipher
                    try {
                         m_Cipher = Cipher.getInstance(Key.getAlgorithm());
                         m_Cipher.init(Cipher.ENCRYPT_MODE, Key);
                    } catch (NoSuchAlgorithmException Cause) { throw new IOException(Cause); }
                    catch (NoSuchPaddingException Cause) { throw new IOException(Cause); }
                    catch (InvalidKeyException Cause) { throw new IOException(Cause); }
                    // Enable encryption
                    m_DoEncryption = true;
               }
               
               
               /** <p>Disable encryption of this output stream.</p>
                * 
                * <p>All buffered data is flushed before encryption is disabled.</p>
                * 
                * @throws IOException If flushing of data failed.
                */
               public void disableEncryption() throws IOException {
                    // Flush what is in the stream
                    flush();
                    // Disable encryption
                    m_DoEncryption = false;
               }
               
               
               /** <p>Returns whether this stream is in encryption mode or not.</p>
                * 
                * @return {@code true} if this stream encrypts incoming data.
                */
               public boolean isEncrypting() { return m_DoEncryption; }
               
               
               @Override public void write(int Data) throws IOException { write(new byte[]{(byte)Data}); }
               
               
               @Override public void write(byte[] Data) throws IOException { write(Data, 0, Data.length); }
               
               
               @Override public void write(byte[] Data, int Offset, int Length) throws IOException {
                    if (m_DoEncryption) {
                         // Copy data to write into a byte buffer
                         ByteArrayInputStream bais = new ByteArrayInputStream(Data, Offset, Length);
                         
                         // Output buffer for encrypted data
                         ByteArrayOutputStream baos = new ByteArrayOutputStream(((Length - Offset) / 15 + 1) * 16);
                         
                         // Process data
                         int cipherBlockSize = m_Cipher.getBlockSize();
                         byte[] buffer = new byte[cipherBlockSize];
                         int bytesRead = 0;
                         // As long as there is something left to process
                         while ((bytesRead = bais.read(buffer, 0, buffer.length-1)) > 0) {
                              // Append padding
                              for (int i = m_Cipher.getBlockSize(); i > bytesRead; i--) { buffer[i-1] = (byte)(cipherBlockSize - bytesRead); }
                              
                              // Write encoded buffer to output stream
                              baos.write(m_Cipher.update(buffer));
                         }
                         
                         // Write data to output stream
                         m_Output.write(baos.toByteArray());
                    } else { m_Output.write(Data, Offset, Length); }
               }
               
               
               @Override public void flush() throws IOException {
                    byte[] buffer = null;
                    // If encryption is enabled, try to cipher what is remaining
                    if (m_DoEncryption) {
                         try { buffer = m_Cipher.doFinal(); }
                         catch (IllegalBlockSizeException Reset) { buffer = null; }
                         catch (BadPaddingException Reset) { buffer = null; }
                    }
                    // If there is any data to write, do so
                    if ((buffer != null) && (buffer.length > 0)) { m_Output.write(buffer); }
                    // Flush underlying stream
                    m_Output.flush();
               }
               
               
               @Override public void close() throws IOException {
                    flush();
                    m_Output.close();
               }
          }