重复使用 ICryptoTransform 对象

5
我有一个用于加密文本数据的类。我正在尝试在可能的情况下重复使用ICryptoTransform对象。但是,第二次尝试使用相同的对象时,我得到了部分不正确解密的数据。我认为第一个块是错误的,但其余部分似乎是正确的(我用更长的文本测试过)。
我将该类简化为以下内容:
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();
        }
    }
}

我编写了一个示例使用测试来演示问题:

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 :(");
}

我错过了什么吗?文档提示这些对象是可重复使用的,但我不明白如何实现。请有人帮忙吗?
编辑:正如@bartonjs指出的那样,如果我将包含上述代码的项目重新定向到.NET 4.6(或更高版本),我可以像这样使用System.AppContext.TryGetSwitch:
var reuseTransform = false;
if (null == _decryptorTransform ||
    !(AppContext.TryGetSwitch("Switch.System.Security.Cryptography.AesCryptoServiceProvider.DontCorrectlyResetDecryptor", out reuseTransform) && reuseTransform && _decryptorTransform.CanReuseTransform))
{
    _decryptorTransform?.Dispose();
    _decryptorTransform = _cryptoProvider.Createdecryptor();
}

然后我可以像@bartonjs的答案一样,在主应用程序的app.config中设置此开关。

我忘了提到,如果_decryptorTransform和_encryptorTransform被始终重新创建(无论'if'子句如何),则附加的测试将通过。 我只希望即使我不每次重新创建它们,它也能通过。 - Levente Koncz
一个可能的解决方法是在数据开头填充32个零,然后在解密后删除填充的字节。测试表明,这种方法比重新创建对象快3倍。 - Loathing
1个回答

8
你所缺失的是.NET Framework中的错误和错误修复。关于这个问题有一个Microsoft Connect Issue,具体来说,AesCryptoServiceProvider.CreateDecryptor()返回一个对象,显示CanReuseTransform=true,但似乎没有正确地执行。
该错误已在.NET 4.6.2发布中得到修复,但受到retargeting change的保护。这意味着为了看到修复,您需要:
  1. 安装.NET Framework 4.6.2或更高版本。
  2. 将主可执行文件的最小框架版本更改为4.6.2或更高版本。
如果您安装了更新的框架,但希望保持可执行文件针对较低版本的框架,则需要将开关Switch.System.Security.Cryptography.AesCryptoServiceProvider.DontCorrectlyResetDecryptor设置为false
AppContext类文档(在“备注”下)中:

一旦定义并记录开关,调用者可以通过使用注册表,通过向应用程序配置文件添加AppContextSwitchOverrides元素或通过编程方式调用AppContext.SetSwitch(String, Boolean)方法来使用它。

对于配置文件(your.exe.config):
<configuration>
  <runtime>
    <AppContextSwitchOverrides
      value="Switch.System.Security.Cryptography.AesCryptoServiceProvider.DontCorrectlyResetDecryptor=false" />
  </runtime>
</configuration>

谢谢您提供全面的回答!由于兼容性问题,我们暂时不会重新定位我们的程序集(当前为.NET 4.5.2)。因此我还不能使用_System.AppContext_(它需要.NET 4.6+)。但是我已经关闭了加密转换的重用,直到我们升级到4.6.2版本。 - Levente Koncz
对于使用ASP.NET应用程序的其他人,请确保webconfig中的<httpRuntime targetFramework="4.6.2" />针对4.6.2进行了定位。我的设置为4.5.1,导致我的解密被破坏。 - gorillapower
我在使用Azure云服务时遇到了问题。看起来AES实例没有被重用,结果是解密出现了错误。即使是一个全新的AzureCloud Services项目,框架版本升级到4.6.2也会出现这种情况。这是在本地调试时发生的。我还尝试过像上面提到的AppContext开关,但没有成功。 - gorillapower
Microsoft Connect已经停用,这里是新的文档链接: https://github.com/microsoft/dotnet/blob/master/Documentation/compatibility/aescryptoserviceprovider-decryptor-provides-a-reusable-transform.md - Salar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接