Reuse ICryptoTransform objects
Asked Answered
G

1

5

I have a class that is used to encrypt textual data. I am trying to reuse the ICryptoTransform objects where possible. However, the second time I am trying to use the same object, I get partially incorrectly decrypted data. I think the first block is wrong but the rest seems to be okay (tested it with longer texts).

I stripped down the class to the following:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace Sample.Crypto
{
    public class EncryptedStreamResolver : IDisposable
    {
        private AesCryptoServiceProvider _cryptoProvider;
        private ICryptoTransform _encryptorTransform;
        private ICryptoTransform _decryptorTransform;

        private ICryptoTransform EncryptorTransform
        {
            get
            {
                if (null == _encryptorTransform || !_encryptorTransform.CanReuseTransform)
                {
                    _encryptorTransform?.Dispose();
                    _encryptorTransform = _cryptoProvider.CreateEncryptor();
                }
                return _encryptorTransform;
            }
        }

        private ICryptoTransform DecryptorTransform
        {
            get
            {
                if (null == _decryptorTransform || !_decryptorTransform.CanReuseTransform)
                {
                    _decryptorTransform?.Dispose();
                    _decryptorTransform = _cryptoProvider.CreateDecryptor();
                }
                return _decryptorTransform;
            }
        }

        public EncryptedStreamResolver()
        {
            GenerateCryptoProvider();
        }

        public Stream OpenRead(string rawPath)
        {
            return new CryptoStream(File.OpenRead(rawPath + ".crypto"), DecryptorTransform, CryptoStreamMode.Read);
        }

        public Stream OpenWrite(string rawPath)
        {
            return new CryptoStream(File.OpenWrite(rawPath + ".crypto"), EncryptorTransform, CryptoStreamMode.Write);
        }

        private void GenerateCryptoProvider(string password = "totallysafepassword")
        {
            _cryptoProvider = new AesCryptoServiceProvider();
            _cryptoProvider.BlockSize = _cryptoProvider.LegalBlockSizes.Select(ks => ks.MaxSize).Max();
            _cryptoProvider.KeySize = _cryptoProvider.LegalKeySizes.Select(ks => ks.MaxSize).Max();
            _cryptoProvider.IV = new byte[_cryptoProvider.BlockSize / 8];
            _cryptoProvider.Key = new byte[_cryptoProvider.KeySize / 8];

            var pwBytes = Encoding.UTF8.GetBytes(password);
            for (var i = 0; i < _cryptoProvider.IV.Length; i++)
                _cryptoProvider.IV[i] = pwBytes[i % pwBytes.Length];
            for (var i = 0; i < _cryptoProvider.Key.Length; i++)
                _cryptoProvider.Key[i] = pwBytes[i % pwBytes.Length];
        }

        public void Dispose()
        {
            _encryptorTransform?.Dispose();
            _decryptorTransform?.Dispose();
            _cryptoProvider?.Dispose();
        }
    }
}

I have written a sample usage test to demonstrate the problem:

public void Can_reuse_encryptor()
{
    const string message = "Secret corporate information here.";
    const string testFilePath1 = "Foo1.xml";
    const string testFilePath2 = "Foo2.xml";
    var sr = new EncryptedStreamResolver();

    // Write secret data to file
    using (var writer = new StreamWriter(sr.OpenWrite(testFilePath1)))
        writer.Write(message);

    // Read it back and compare with original message
    using (var reader = new StreamReader(sr.OpenRead(testFilePath1)))
        if (!message.Equals(reader.ReadToEnd()))
            throw new Exception("This should never happend :(");

    // Write the same data again to a different file
    using (var writer = new StreamWriter(sr.OpenWrite(testFilePath2)))
        writer.Write(message);

    // Read that back and compare
    using (var reader = new StreamReader(sr.OpenRead(testFilePath2)))
        if (!message.Equals(reader.ReadToEnd()))
            throw new Exception("This should never happend :(");
}

What am I missing? The documentation suggests that these objects are reusable but I can't understand how. Can someone help me please?

EDIT:

As @bartonjs pointed out, if I retarget my project containing the codes above to .NET 4.6 (or above) I can use System.AppContext.TryGetSwitch like this:

var reuseTransform = false;
if (null == _decryptorTransform ||
    !(AppContext.TryGetSwitch("Switch.System.Security.Cryptography.AesCryptoServiceProvider.DontCorrectlyResetDecryptor", out reuseTransform) && reuseTransform && _decryptorTransform.CanReuseTransform))
{
    _decryptorTransform?.Dispose();
    _decryptorTransform = _cryptoProvider.Createdecryptor();
}

Then I can set this switch in the main application's app.config, as in @bartonjs' answer.

Gravante answered 24/4, 2017 at 16:37 Comment(2)
I forgot to mention that the attached test passes, if _decryptorTransform and _encryptorTransform are ALWAYS recreated (regardless the 'if' clause). I just want it to pass even if I don't recreate them every time.Gravante
A possible workaround is to pad the data at the start with 32 zeros, and then remove the padded bytes after decrypting. Testing shows this approach is 3x faster than recreating the objects.Tonguetied
A
8

What you're missing is the bug (and bugfix) in .NET Framework :).

There's a Microsoft Connect Issue about this same problem; specifically that AesCryptoServiceProvider.CreateDecryptor() returns an object that says CanReuseTransform=true, but doesn't seem to behave correctly.

The bug was fixed in the .NET 4.6.2 release, but is guarded behind a retargeting change. That means that in order to see the fix you need to

  1. Install .NET Framework 4.6.2 or higher.
  2. Change the minimum framework version of your main executable to be 4.6.2 or higher.

If you have the newer framework installed, but want to keep your executable targeting a lower version of the framework you need to set the switch Switch.System.Security.Cryptography.AesCryptoServiceProvider.DontCorrectlyResetDecryptor to false.

From the AppContext class documentation (under "Remarks"):

Once you define and document the switch, callers can use it by using the registry, by adding an AppContextSwitchOverrides element to their application configuration file, or by calling the AppContext.SetSwitch(String, Boolean) method programmatically.

For the configuration file (your.exe.config):

<configuration>
  <runtime>
    <AppContextSwitchOverrides
      value="Switch.System.Security.Cryptography.AesCryptoServiceProvider.DontCorrectlyResetDecryptor=false" />
  </runtime>
</configuration>
Apogee answered 24/4, 2017 at 22:45 Comment(4)
Thank you for the comprehensive answer! We are not going to retarget our assemblies yet for compatilibity reason (currently it is .NET 4.5.2). For that reason I cannot use System.AppContext yet (it's .NET 4.6+). However I turned off the reuse of crypto transforms until we upgrade to 4.6.2.Gravante
For anyone else using ASP.NET applications, make sure the <httpRuntime targetFramework="4.6.2" /> in the webconfig is targeting 4.6.2. Mine was set to 4.5.1 and was causing the my decryption to be corrupted.Deed
Im running into an issue with Azure Cloud services. It seems the AES instance is not being reused and the result is corrupted decryptions. This happens even a fresh vanilla AzureCloud Services project with an upgrade framework version of 4.6.2. This happened while doing a local debug. I also attempted to do the AppContext switch as mentioned above, but to no avail.Deed
Microsoft Connect is retired, here is the new doc link: github.com/microsoft/dotnet/blob/master/Documentation/…Jihad

© 2022 - 2024 — McMap. All rights reserved.