使用SQL Server中的“始终加密”损坏的数据

时间:2018-02-03 00:01:12

标签: asp.net .net sql-server encryption always-encrypted

一旦我们开始将数据写入加密表,我们就会在尝试读取加密数据时发现问题。症状与描述HERE

的症状相同

这里有一点背景知识。我们有一个网络应用程序,它将客户信息写入"客户端" SQL Server数据库中的表。作为转换解决方案,我们创建了附加表client_enc并更新了我们的应用程序以写入两个表:原始表和加密表。我们有4个Web应用程序实例,托管在同一个VM上,同一个IIS。

我们的Web应用程序的所有4个实例都映射到文件系统上的同一文件夹(二进制代码或web.config中没有区别)。

我们注意到其中一个实例随机写入了损坏的值。这些写入发生时没有重新启动/回收Web应用程序(写入之间的几秒钟内)。

以下是特定客户的信息:

客户姓氏:" Hoyer"

良好的加密值(我们稍后可以阅读):

0x015EF5BB1B1EA45EADFA9EFC3611D3F5661616C4B38BEDB06B33D6B6DC084714F235E0818C14DEEC0A95C5547DE8DC3D3A402A4DB8C992AB3716B651037C8ED2E7

加密值损坏:

0x01848FA1EA78BA1FCFC615728CEE9882937A52AAF649472F0B7829A28463060E34080F924AC5CD987AA0C5275507C0A480EC9D44B63B256552EFFE7C1562FEC1DA

环境:

  • 主机:Windows 2012 R2(Microsoft Windows NT 6.3(14393))
  • SQL Server 2016(v13.0.1742.0)
  • .NET Framework 4.6.2
  • web.config中的目标框架:4.6.2
  • 数据库只有一个主密钥,只有一个列密钥

有人可以猜测是什么导致了这种奇怪的行为吗?

1 个答案:

答案 0 :(得分:1)

更新:最初,由于测试结果不准确,我做出了错误的判断。我将删除错误的事实,但会将它们留在这里用于历史目的。

经过一周的血腥调试和测试后,我得出结论,该行为的根源在.NET Framework中 RSACryptoServiceProvider 的内部。

事实让我这么想:

  • 我的应用程序配置为每小时回收一次,我注意到应用程序重启后数据损坏发生了10次中的10次;如果应用程序开始写入损坏的数据,那么它会永久执行,直到应用程序再次重新启动(或回收)
  • 我使用反射来查看AlwaysEncrypted功能中涉及的对象内部:
    • 在“System.Data.SqlClient.SqlSymmetricKeyCache”内部我正在查看Column Key解密的结果(另一个私有静态字段_singletonInstance的私有字段_cache)
    • 我替换了SqlColumnEncryptionCertificateStoreProvider的默认实现,以便记录所有解密列加密密钥的请求
  • 我等待另一个数据损坏发生,然后查看我的补丁提供程序和解密密钥缓存。我发现解密的列密钥,由SqlColumnEncryptionCertificateStoreProvider返回 0x0000000000000000 ... 正确,但在缓存中似乎已损坏(0x0000000000000000 ...)

我还发现了这个ARTICLE,这让我觉得高负载的ASP.NET应用程序可能会遇到 RSACryptoServiceProvider 类的问题,一旦它被用于多个线程环境。这正是我的情况和SqlColumnEncryptionCertificateStoreProvider没有任何线程同步机制来避免这个问题,这发生在 RSACryptoServiceProvider 内。

查看Always Encrypted相关类的源代码后,我发现只有ONE PLACE,其中使用了解密的列密钥。

// 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内)之前添加了加密表的简单读取,那么问题就消失了。基本上,这个技巧可以帮助我保证只有一个非并发的DB数据读取触发了列密钥解密和SqlSymmetricKeyCache初始化。

很高兴听到微软团队就这种非常奇怪的行为发表的一些评论。