.NET加密类的线程安全性?

17

我有一个高级目标,希望创建一个静态实用程序类,为我的.NET应用程序封装加密。在其中,我想最小化不必要的对象创建。

我的问题是:.NET Framework中实现对称加密的类的线程安全性如何?具体来说,是System.Security.Cryptography.RijndaelManaged和它生成的ICryptoTransform类型。

例如,在我的类构造函数中,我是否可以简单地执行以下操作?

static MyUtility()
{
    using (RijndaelManaged rm = new RijndaelManaged())
    {
        MyUtility.EncryptorTransform = rm.CreateEncryptor(MyUtility.MyKey, MyUtility.MyIV);
        MyUtility.DecryptorTransform = rm.CreateDecryptor(MyUtility.MyKey, MyUtility.MyIV);
    }
}
绕开“在该类中存在Key和IV是否安全”的问题,此示例块提出了一些其他问题:
  1. 我能够一次又一次地重复使用EncryptorTransform和DecryptorTransform吗? *.CanReuseTransform和*.CanTransformMultipleBlocks属性意味着“是”,但我应该注意什么问题吗?
  2. 由于RijndaelManaged实现了IDisposable,因此我倾向于将其放入using块中,特别是因为它可能与外部操作系统级库相连。由于我正在保留ICryptoTransform对象,因此是否有任何注意事项?
  3. 在高度多线程环境中,我会遇到共享ICryptoTransform对象之间的问题吗?
  4. 如果3的答案是它不是线程安全的,那么当我使用ICryptoTransform对象时,将经历严重的性能下降吗?(取决于负载,我想。)
  5. 每次仅实例化新的RijndaelManaged是否更有效?还是存储一个RijndaelManaged并每次生成new RijndaelManaged().CreateEncryptor(...)?
我希望有人知道这些是如何在幕后工作的或具有类似实现的问题的经验。我发现这些性能和线程相关问题通常不会表现出来,直到有相当大的负载。
谢谢!
5个回答

18

1) 是的。

2) 一旦你处理掉它,就无法使用它。在那之前,你可以分享/使用它(但见下文)

3-4) 来自MSDN

"此类型的任何公共静态成员(Visual Basic中为Shared)都是线程安全的。任何实例成员都不能保证是线程安全的。"

如果你想保留这个对象并在多线程之间共享它,你需要实现锁定并将其视作受锁定的资源。否则,我建议根据需要创建单独的版本,并在完成后处理掉它们。

5) 我建议根据需要创建这些对象,然后在发现有性能问题时尝试进行优化。在进行性能分析之前,不要担心创建新版本的性能影响。


好的建议。我想我做出了这样的假设,即由于对本地平台SDK类的底层依赖关系,创建和销毁其中一些对象会带来一些成本。如果没有人认为这是一个值得关注的问题,那么同步机制最终可能会成为更大的瓶颈。 - mckamey
2
确实,您不应该过早地进行优化,但是我们想要强调的是,在我们的情况下,我们发现执行大约500个小加密(所有加密都使用相同的密码)需要近10秒钟,这是一场灾难。几乎所有时间都花在了创建加密器上。重复使用ICryptoTransforms是显而易见的解决方案。 - Bernard
我的看法是,我发现不重复使用 ICryptoTransform 会导致我们的应用程序性能非常糟糕。 - gorillapower

4

使用基于并发堆栈的缓存即可简单解决并发问题:

static ConcurrentStack<ICryptoTransform> decryptors = new ConcurrentStack<ICryptoTransform>();

void Encrypt()
{
   // Pop decryptor from cache...
   ICryptoTransform decryptor;
   if (!decryptors.TryPop(out decryptor))
   {
       // ... or create a new one since cache is depleted
       AesManaged aes = new AesManaged();
       aes.Key = key;
       aes.IV = iv;
       decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
    }

    try
    {
       //// use decryptor
    }
    finally
    {
       decryptors.Push(decryptor);
    }
 }

你好,我知道这是一个很旧的帖子,但对我来说这并没有解决问题。我仍然会遇到随机失败的解密结果,但如果我每次调用时都使用CreateDecryptor(),它就能正常工作。有什么建议吗?在我的情况下,我是在CryptoStream()中使用解密器。谢谢! - Seltix
找到了原因,但还没有真正的解决办法。如果你能帮忙,请告诉我。我已经在下面的回答中发布了我的发现。 - Seltix
找到了原因,但还没有真正的解决办法。如果你能帮忙,请告诉我。我已经在下面的回答中发布了我的发现。 - undefined

0
  1. 绝对不是,我遇到了麻烦。我使用它已经两年了,突然间一些最重要的代码在解密时开始抛出错误。 这是由于微软补丁的结果吗?还是 McAfee?还是 .net framework 的更新?(框架 4.7.2)

当我最终找到问题所在时,我将 CreateEncryptor 和 CreateDecryptor 移到实际调用加密和解密的 using 内部。问题得到解决。


我知道这已经过时了,但我遇到了同样的问题,我甚至尝试了@Andreas上面提供的使用ConcurrentStack实现的解决方案,但它没有修复问题,由于某种原因,解密后我仍然随机得到错误的结果。对我来说唯一有效的解决方案是在每次调用时使用CreateDecryptor()。不知道为什么。我尝试了4.7.2和4.8版本的框架 :s - Seltix
我知道这是老问题了,但我遇到了同样的问题。我甚至尝试了@Andreas上面提供的使用ConcurrentStack的解决方案,但是它并没有解决问题,出现了随机错误的解密结果。唯一对我有效的解决方案是在每次调用时使用CreateDecryptor()方法。不知道为什么会这样。我尝试了4.7.2和4.8版本的框架。 - undefined
找到了原因,但没有找到真正的解决办法。如果你能帮忙,请告诉我。我在下面的回答中发布了我的发现。 - Seltix
找到了原因,但还没有真正的解决办法。如果你能帮忙,请告诉我。我已经在下面的答案中发布了我的发现。 - undefined

0

重复使用从调用AesManaged.CreateDecryptor返回的ICryptoTransform实例意味着您正在使用相同的密钥和初始化向量来解密多个先前使用相同密钥和初始化向量加密的消息。

这是一个非常非常糟糕的想法。

初始化向量(IV)永远不应该被重复使用,密钥只能通过使用主密钥保护实际会话或消息密钥来间接地重复使用,每个消息应具有唯一的密钥。因此,唯一重复使用的密钥是主密钥,它仅用于加密其他密钥,而不是数据。密钥或IV之间也不应存在任何关联,即不要从密钥派生IV或从主密钥派生会话密钥等。

因此,回答原始问题和后续问题“如何安全地重复使用'解密器'对象”,答案很简单:不要这样做,因为这样将同时重复使用密钥和IV,而两者都不应该被重复使用。

自己设计加密方案总是充满风险的,一般情况下我建议根本不要这样做。只需看看多少次大公司和组织(例如Microsoft的Office加密、IEEE WiFi加密)出错了,甚至是多次。

这不是关于线程安全与否的问题,而是关于为什么首先要进行加密的问题。如果你没有正确地进行加密,那么最好就不要进行加密,因为这样至少可以明确消息没有受到保护。错误地进行加密只不过是一种混淆手段而已。

0
我一直在与这个问题斗争,我以为它与线程安全有关,但我刚刚发现它是不同的。这个“解决方案”实际上与原始问题没有关系,但当我搜索解决方案时,这篇帖子是我找到的最接近的,所以我会在这里发布,也许能帮助其他人。
当使用CryptoStream时:
 using (MemoryStream memoryStream = new MemoryStream())
 using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write))
 {
    cryptoStream.Write(cipherData, 0, cipherData.Length);
    cryptoStream.FlushFinalBlock();
    result = memoryStream.ToArray();
 }

如果cipherData不正确(例如内容是明文而不是加密的),可能会在FlushFinalBlock()处引发异常,例如:
"The input data is not a complete block." (System.Security.Cryptography.CryptographicException)

因此,某些事情将无法完成,并且会导致下一次使用此函数(使用相同的解密器)返回带有上一次使用剩余部分的结果,从而导致后续处理结果时出现意外错误。
正如@Doug Hill建议的那样,在每次调用时创建一个新的CreateEncryptor()/CreateDecryptor()将修复此问题,因为这是函数每次调用之间的链接,但会牺牲性能。
借助@Andreas的基础代码,我创建了这个解决方案,它只在抛出异常时才释放这些ICryptoTransform对象,因为我不知道如何清除/重置它以便再次重用。它并不完美,但应该提高性能。
private Aes _aes;
private static ConcurrentStack<ICryptoTransform> decryptors = new ConcurrentStack<ICryptoTransform>();

public void Initialize()
{
    _aes = Aes.Create();
    // ...
}

public byte[] Decode(byte[] cipherData)
{
    var decryptor = DecryptorPop();
    
    try
    {
        byte[] result = null;
        using (MemoryStream memoryStream = new MemoryStream())
        using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write))
        {
            cryptoStream.Write(cipherData, 0, cipherData.Length);
            cryptoStream.FlushFinalBlock();
            result = memoryStream.ToArray();
        }
        return result;
    }
    catch (Exception ex)
    {
        // since I do not know how to fix the decryptor I will just dispose it
        decryptor.Dispose();
        decryptor = null;

        // throw the same exception again if you want to catch it outside
        System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex).Throw();
    }
    finally
    {
        DecryptorPush(decryptor);
    }

    return null;
}

private ICryptoTransform DecryptorPop()
{
    ICryptoTransform decryptor;
    if (!decryptors.TryPop(out decryptor))
    {
        decryptor = _aes.CreateDecryptor();
    }

    return decryptor;
}

private void DecryptorPush(ICryptoTransform decryptor)
{
    if(decryptor == null) return;

    decryptors.Push(decryptor);
}

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