3

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));
            }
        }
    }
}
Wyzard
  • 33,849
  • 3
  • 67
  • 87

1 Answers1

3

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.

rossum
  • 15,344
  • 1
  • 24
  • 38
  • Shouldn't it know based on the fact that I'm closing the stream after reading one block, and that one block is correctly-padded as a final block? It's choking on garbage that I never asked it to read. Is there a way to tell it that the remainder of the underlying stream is not ciphertext and should be ignored? (I actually want to have some unencrypted data there, not garbage.) – Wyzard Sep 14 '11 at 15:46
  • @Wyzard: All it knows is that you didn't read everything you put into it. I suggest that you set up an ordinary stream, holding both the cyphertext and your unencrypted data. Read in the cyphertext and pipe it through a CryptoStream. Read the non-encrypted data direct from the ordinary stream. – rossum Sep 14 '11 at 16:52
  • The question is not why it's choking on the garbage, it's why it's *reading* to the garbage in the first place. Note the garbage is *not* part of the block padding, it's *past* the padding on the first block. I have a similar situation -- I don't have garbage on the end of my file, but I have 50MB of data I don't want being slurped into memory -- I just want to stream in a few blocks. But when I close the CypherStream, it insists on reading past where I've asked it to. – Mud Feb 14 '12 at 23:26
  • If you have padding on the first block, then you only have a short plaintext, less than a block. Why are you adding 50MB to an 8 byte or 16 byte cypher block? Try asking a separate question, and post your code. – rossum Feb 14 '12 at 23:42
  • I'm not talking about my code, I'm talking about his. He has padding on the first block, he only *reads in* the first block, so why is the stream chocking on data past that? Why does it even get there? That's the question. In my case, I have 50MB is encrypted data, but I only want to *read* a few blocks. The CypherStream reads past that. In my case, it's wasting memory. In the OP's case, it's chocking on data that's not encrypted (but shouldn't have been read in the first place). – Mud Feb 15 '12 at 16:45
  • @Mud: As I read the code, he starts by writing the length to the CryptoStream: `cryptoStream.WriteByte((byte)plaintext.Length);`. Since it is being written to a CryptoStream, it will obviously be encrypted. In order to decrypt that first byte, the CryptoStream is going to have to decrypt at least the first block, and possibly more. I am not sure how the internal buffering will work in Microsoft's code -- internally it could be decrypting the first buffer-full rather than just the first block. It would be useful to see exactly which line is throwing the error. – rossum Feb 15 '12 at 17:39
  • If I remember correctly (it's been over a year), reading that length byte only required decrypting the first block. The second block wasn't consumed from the `ciphertextStream` until the `cryptoStream` was disposed. – Wyzard Oct 14 '12 at 19:24