1

I'm observing a difference in the output of AES encryption when using System.Security.Cryptography.CryptoStream depending on how I get the plaintext bytes into the CryptoStream, but I don't understand why the differences occur.

private Aes CreateAes()
{
   var aes = Aes.Create();

   aes.Mode = CipherMode.CBC;
   aes.Padding = PaddingMode.PKCS7;
   aes.Key = ...;
   aes.IV = ...;

   return aes;
}

var plaintext = "This is some plaintext. Isn't it nice?";
var plainbytes = Encoding.UTF8.GetBytes(plaintext);


// Method 1: writing a string with a `StreamWriter`:
using (var aes = CreateAes())
{
    using (var output = new MemoryStream())
    using (var encryptor = aes.CreateEncryptor())
    {
        using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
        using (var writer = new StreamWriter(encryptingStream))
        {
            writer.Write(plaintext);
            encryptingStream.FlushFinalBlock();
        }
        var ciphertext = output.ToArray();
        Console.WriteLine("Method 1: {0}", BitConverter.ToString(ciphertext));
    }
}

// Method 2: writing bytes directly to the `CryptoStream`:
using (var aes = CreateAes())
{
    using (var output = new MemoryStream())
    using (var encryptor = aes.CreateEncryptor())
    {
        using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
        {
            encryptingStream.Write(plainbytes, 0, plainbytes.Length);
            encryptingStream.FlushFinalBlock();
        }
        var ciphertext = output.ToArray();
        Console.WriteLine("Method 2: {0}", BitConverter.ToString(ciphertext));
    }
}

// Method 3: copying a written input stream
using (var aes = CreateAes())
{
    using (var input = new MemoryStream())
    using (var writer = new StreamWriter(input))
    {
        writer.Write(plaintext);

        using (var output = new MemoryStream())
        using (var encryptor = aes.CreateEncryptor())
        {
            using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
            {
                input.CopyTo(encryptingStream);
                encryptingStream.FlushFinalBlock();
            }
            var ciphertext = output.ToArray();
            Console.WriteLine("Method 3: {0}", BitConverter.ToString(ciphertext));
        }
    }
}

// Method 4: Copying a byte-wrapper stream:
using (var aes = CreateAes())
{
    using (var input = new MemoryStream(plainbytes, writable: false))
    using (var output = new MemoryStream())
    using (var encryptor = aes.CreateEncryptor())
    {
        using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
        {
            input.CopyTo(encryptingStream);
            encryptingStream.FlushFinalBlock();
        }
        var ciphertext = output.ToArray();
        Console.WriteLine("Method 4: {0}", BitConverter.ToString(ciphertext));
    }
}

When I execute these, I get the following:

Method 1: 4F-5A-EA-93-53-C4-6E-2B-7B-C1-DE-EF-C3-5E-37-9A-A1-94-FA-25-A8-B7-3A-F2-8F-5D-69-74-4E-BD-B3-C6-A9-B8-B7-37-7A-8D-20-B1-3E-9E-8C-D4-EE-F1-EC-BB`

Method 2: A1-94-FA-25-A8-B7-3A-F2-8F-5D-69-74-4E-BD-B3-C6-A9-B8-B7-37-7A-8D-20-B1-3E-9E-8C-D4-EE-F1-EC-BB-F2-C9-39-B4-63-68-9B-0F-FF-75-80-4D-FC-18-5B-09

Method 3: 4F-5A-EA-93-53-C4-6E-2B-7B-C1-DE-EF-C3-5E-37-9A

Method 4: A1-94-FA-25-A8-B7-3A-F2-8F-5D-69-74-4E-BD-B3-C6-A9-B8-B7-37-7A-8D-20-B1-3E-9E-8C-D4-EE-F1-EC-BB-F2-C9-39-B4-63-68-9B-0F-FF-75-80-4D-FC-18-5B-09

I note that methods 2 and 4 produce identical output, while method 3's output is a truncation of method 1.

My understanding is that StreamWriter uses UTF-8 encoding by default, so why am I seeing such differences? And which is actually correct?

3
  • Please rewrite your samples to dispose all related object properly before touching the output stream. Note that ToArray() is essentially designed to be used on the disposed MemoryStream to grab underlying bytes (in some cases you can cheat with manual Close / FlushFinalBlock calls... but you need to pay far more care for that to be useful as minimal reproducible example). Commented Mar 5 at 0:06
  • 1
    Note: to investigate what is going on consider creating "Crypto" stream that does not encrypt anything by implementing your own SymmetricAlgorithm + ICryptoTransform that simply copies data - you should be able to see where code writes nothing and where your code misses data due to incompletly disposed layers. Commented Mar 5 at 0:28
  • input.CopyTo(encryptingStream); you forgot to rewind the stream first input.Position = 0; and also to flush the writer. Commented Mar 5 at 0:33

1 Answer 1

2

Answer to your puzzle question "And which is actually correct"

  • 1 does not write final block, hence have some output but incomplete.
  • 2 produces correct output
  • 3 writes nothing and cryptostream write some sort of nothingness to output for "final" block... similar how 1 behaves - and hence having the same prefix as 1 (obvious due to not resetting input stream position).
  • 4 produces correct output

Correct code should dispose all objects properly before reading the output - 2 and 4 get away by essentially being lucky.

using (var aes = CreateAes())
{
    var output = new MemoryStream();
    using (output)
    using (var encryptor = aes.CreateEncryptor())
    {
        using (var encryptingStream = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
        using (var writer = new StreamWriter(encryptingStream))
        {
            writer.Write(plaintext);
        }
    }
    // ToArray can (should) be called on disposed stream:
    var ciphertext = output.ToArray();
    Console.WriteLine("Method 1: {0}", BitConverter.ToString(ciphertext));

}

All variants would work correctly if all layers disposed before reading the output. Note that for "copy stream" one you need to either "hack" by resetting position or recreate stream on result of ToArray after disposing writer.


If you want to figure out exactly what is happening implement you own SymmetricAlgorithm class that returns no-op Transformer with pass-through TransformBlock/TransformFinalBlock method, add some minor differences to see which one outputs data (i.e. add FF byte to final block) .

Sign up to request clarification or add additional context in comments.

2 Comments

"Correct code should dispose all objects properly before reading the output..." That makes sense, but I note that the practice wasn't followed as recently as the .NET Core 3.1 tests if not even more recently.
Further, it's not done in the CryptoStream or Aes docs either, so your guidance is (somewhat) surprising.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.