一旦我们开始将数据写入加密表,一旦我们尝试读取加密数据,我们就注意到了问题。症状与此处描述的症状相同
这里有一点背景。我们有一个 Web 应用程序,它将客户端信息写入 SQL Server 数据库中的"客户端"表。作为转换解决方案,我们创建了其他表client_enc
并更新了我们的应用程序以写入两个表:原始表和加密表。我们有 4 个 Web 应用的实例,托管在同一个 VM、同一个 IIS 上。
我们的 Web 应用程序的所有 4 个实例都映射到文件系统上的同一文件夹(二进制代码或 web.config 没有区别)。
我们注意到其中一个实例随机写入损坏的值。这些写入发生在没有重新启动/回收 Web 应用程序的情况下(在两次写入之间的几秒钟内)。
以下是特定客户的信息:
客户姓氏:"霍耶">
良好的加密值(我们稍后可以读取的值):
0x015EF5BB1B1EA45EADFA9EFC3611D3F5661616C4B38BEDB06B33D6B6DC084714F235E0818C14DEEC0A95C5547DE8DC3D3A402A4DB8C992AB3716B651037C8ED2E7
加密值损坏:
0x01848FA1EA78BA1FCFC615728CEE9882937A52AAF649472F0B7829A28463060E34080F924AC5CD987AA0C5275507C0A480EC9D44B63B256552EFFE7C1562FEC1DA
环境:
- 主机: 视窗 2012 R2 (Microsoft视窗 NT 6.3 (14393))
- SQL Server 2016 (v13.0.1742.0)
- .NET Framework 4.6.2
web.config
中的目标框架:4.6.2- 数据库只有一个主密钥和一个列密钥
有人能试着猜测是什么导致了这种奇怪的行为吗?
更新:最初,由于测试结果不准确,我做出了错误的判断。我会划掉错误的事实,但为了历史目的,我会把它们留在这里。
经过一周的血腥调试和测试,我得出的结论是,该行为的根源在.NET Framework中的RSACryptoServiceProvider类内部。
让我这样想的事实:
- 我的应用程序配置为每小时回收一次,我注意到应用程序重启后数据损坏发生 1/10;如果应用程序开始写入损坏的数据,则它将永久执行此操作,直到应用程序再次重新启动(或回收)
- 我使用反射来查看对象的内部结构,涉及始终加密功能:
- 在"System.Data.SqlClient.SqlSymmetricKeyCache"中,我正在监视列键解密的结果(另一个私有静态字段_singletonInstance的私有字段_cache)
- 我替换了 SqlColumnEncryptionCertificateStoreProvider 的默认实现,以便记录所有解密列加密密钥的请求
我 - 等待另一次数据损坏发生,然后查看我修补的提供程序和解密密钥的缓存。我发现由 SqlColumnEncryptionCertificateStoreProvider 返回的解密列键
0x0000000000000000正确,但在缓存中似乎已损坏(0x0000000000000000...
我还发现了这篇文章,这让我认为高负载 ASP.NET 应用程序一旦在多线程环境中使用RSACryptoServiceProvider类,可能会出现问题。这正是我的情况,SqlColumnEncryptionCertificateStoreProvider没有任何线程同步机制来避免该问题,该问题发生在RSACryptoServiceProvider内部。
在查看了始终加密相关类的源代码后,我只找到了一个使用解密列键的地方。
// Decrypt the CEK
// We will simply bubble up the exception from the DecryptColumnEncryptionKey function.
byte[] plaintextKey;
try {
plaintextKey = provider.DecryptColumnEncryptionKey(keyInfo.keyPath, keyInfo.algorithmName, keyInfo.encryptedKey);
}
catch (Exception e) {
// Generate a new exception and throw.
string keyHex = SqlSecurityUtility.GetBytesAsString(keyInfo.encryptedKey, fLast: true, countOfBytes: 10);
throw SQL.KeyDecryptionFailed(keyInfo.keyStoreName, keyHex, e);
}
encryptionKey = new SqlClientSymmetricKey(plaintextKey);
// If the cache TTL is zero, don't even bother inserting to the cache.
if (SqlConnection.ColumnEncryptionKeyCacheTtl != TimeSpan.Zero) {
// In case multiple threads reach here at the same time, the first one wins.
// The allocated memory will be reclaimed by Garbage Collector.
DateTimeOffset expirationTime = DateTimeOffset.UtcNow.Add(SqlConnection.ColumnEncryptionKeyCacheTtl);
_cache.Add(cacheLookupKey, encryptionKey, expirationTime);
}
plaintextKey
值是 100% 正确的,因为我在从 DecryptColumnEncryptionKey() 方法返回它之前记录它。
encryptionKey = new SqlClientSymmetricKey(plaintextKey)
内部的密钥损坏不太可能,因为SqlClientSymmetricKey是字节数组的简单包装器。
在我看来,_cache.Add(cacheLookupKey, encryptionKey, expirationTime)
内部的关键腐败也不太可能。
这让我只能对这种情况如何发生有一个合乎逻辑的解释。 由于字节数组(我们的解密密钥)作为引用到处传递,在特定情况下,该密钥的使用者会搞砸数组中的字节值。但不幸的是,我在代码中找不到任何地方来证明这个理论。
解决方法。一旦我在应用程序开始处理请求之前添加了从加密表中的简单读取(在 Global.asax 内部),那么问题就消失了。基本上,这个技巧可以帮助我保证只有一个来自数据库的非并发数据读取触发列键解密和 SqlSymmetricKeyCache 初始化。
会很高兴听到Microsoft团队对这种非常奇怪的行为的一些评论。