Search code examples
.netencryptionpaddingcryptostream

.NET CryptoStream reads past end of ciphertext in Dispose() and blows up


I'm puzzled by what appears to be a quirk of the .NET CryptoStream class: its Dispose() method reads past the end of the ciphertext looking for padding that it shouldn't, and throws a CryprographicException as a result.

The C# program below encrypts a few bytes, resizes the ciphertext array so that there are more (nonsense) bytes after the end of the ciphertext, and then attempts to decrypt it. The salient points are:

  • The ciphertext is 8 bytes, one 3DES cipher block. Since I only write 6 bytes into the CryptoStream and it's using PaddingMode.PKCS7 (the default), the remaining two bytes in the block are filled with the padding value 0x02.
  • The ciphertext array is subsequently resized to 16 bytes, two 3DES blocks. The second block is uninitialized nonsense, not valid cipher output.
  • When decrypting, I read exactly 6 bytes from the CryptoStream; I'm not asking it to decrypt into the nonsense portion, and I'm not relying on it recognizing the padding to figure out when it's reached the end of the plaintext.

The problem is that when the decrypting CryptoStream's Dispose() is called (automatically at the end of the using block), I get a CryptographicException with the message "Bad Data". Its stack trace shows that it was executing CryptoStream.FlushFinalBlock(), and all 16 bytes have been consumed from the ciphertextStream, not just the 8 corresponding to the actual encrypted data.

If I remove the line that resizes the ciphertext array, the program works correctly. And if I do tripleDes.Padding = PaddingMode.None before decrypting, the program also works correctly — but that basically makes the padding bytes part of the plaintext, so I'd rather not do that. Clearly, the problem is padding-related; as far as I can tell, it's decrypted that second block and is expecting to find valid PKCS7-style padding at the end of it.

Since I'm only reading enough from the CryptoStream to require one block to be decrypted, and that block is a correctly-padded final block, and then I close the CryptoStream without reading any more, why does the stream think it needs to read another block and look for more padding? Why is it even trying to consume more input as part of its Dispose()?


using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            byte[] plaintext = { 0, 1, 2, 3, 4 };

            using (SymmetricAlgorithm tripleDes = TripleDESCryptoServiceProvider.Create())
            {
                // Encrypt the plaintext
                byte[] ciphertext;
                using (MemoryStream ciphertextStream = new MemoryStream())
                {
                    using (ICryptoTransform encryptor = tripleDes.CreateEncryptor())
                    {
                        using (CryptoStream cryptoStream = new CryptoStream(ciphertextStream, encryptor, CryptoStreamMode.Write))
                        {
                            cryptoStream.WriteByte((byte)plaintext.Length);
                            cryptoStream.Write(plaintext, 0, plaintext.Length);
                            cryptoStream.FlushFinalBlock();
                        }
                    }

                    ciphertext = ciphertextStream.ToArray();
                }

                // *** Add some non-ciphertext garbage to the end ***
                Array.Resize(ref ciphertext, ciphertext.Length + 8);

                // Now decrypt it again
                byte[] decryptedPlaintext;
                using (MemoryStream ciphertextStream = new MemoryStream(ciphertext, false))
                {
                    using (ICryptoTransform decryptor = tripleDes.CreateDecryptor())
                    {
                        using (CryptoStream cryptoStream = new CryptoStream(ciphertextStream, decryptor, CryptoStreamMode.Read))
                        {
                            int length = cryptoStream.ReadByte();

                            decryptedPlaintext = new byte[length];

                            int i = 0;
                            while (i < length)
                            {
                                int bytesRead = cryptoStream.Read(decryptedPlaintext, i, (length - i));
                                if (bytesRead == 0) break;
                                else i += bytesRead;
                            }
                        }  // CryptographicException: "Bad Data"
                    }
                }

                System.Diagnostics.Debug.Assert(decryptedPlaintext.SequenceEqual(plaintext));
            }
        }
    }
}

Solution

  • You are deliberately adding garbage to the end of the stream and then wondering why the stream chokes on the garbage.

    In cryptography everything has to be checked very carefully to ensure that an attacker is not trying something sneaky. If you specify PKCS7 padding then the stream is right to check for PKCS7 padding at the end and right to throw an exception if it does not find the correct padding at the end of the stream.

    The stream has no way of knowing that the actual cyphertext ends in the middle of the stream, rather than at the end. How would you expect it to know? In crypto the rule is to flag any and all anomalies, and faulty padding at the (apparent) end of the stream is something the documentation will tell you causes an exception.