SQL 服务器中使用 Always Encrypted 的损坏数据

Corrupted data using Always Encrypted in SQL Server

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

这是一些背景知识。我们有一个 Web 应用程序,它将客户端信息写入 SQL 服务器数据库中的 "client" table。作为过渡解决方案,我们创建了额外的 table client_enc 并更新了我们的应用程序以写入两个 tables:原始的和加密的。我们有 4 个 Web 应用实例,托管在同一个 VM 和同一个 IIS 上。

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

我们注意到其中一个实例随机写入损坏的值。这些写入在没有 restart/recycling 网络应用程序的情况下发生(写入之间的几秒内)。

以下是特定客户的信息:

客户姓氏:"Hoyer"

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

0x015EF5BB1B1EA45EADFA9EFC3611D3F5661616C4B38BEDB06B33D6B6DC084714F235E0818C14DEEC0A95C5547DE8DC3D3A402A4DB8C992AB3716B651037C8ED2E7

损坏的加密值:

0x01848FA1EA78BA1FCFC615728CEE9882937A52AAF649472F0B7829A28463060E34080F924AC5CD987AA0C5275507C0A480EC9D44B63B256552EFFE7C1562FEC1DA

环境:

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

UPDATE: 最初,由于测试结果不准确,我做出了错误的判断。我会划掉错误的事实,但出于历史目的将它们留在此处。


经过一周的调试和测试,我得出的结论是,这种行为的根源在于 RSACryptoServiceProvider class 在 .NET Framework 中。

让我这么想的事实

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

我也发现了这个 ARTICLE,这让我觉得高负载 ASP.NET 应用程序可能与 RSACryptoServiceProvider class,一旦在多线程环境下使用。这正是我的情况,SqlColumnEncryptionCertificateStoreProvider 没有任何线程同步机制来避免这个问题,这个问题发生在 RSACryptoServiceProvider.

内部

查看 Always Encrypted 相关 classes 的源代码后,我发现只有 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% 正确的,因为我在 return 从 DecryptColumnEncryptionKey() 方法对其进行记录之前记录它。

encryptionKey = new SqlClientSymmetricKey(plaintextKey) 内部的密钥损坏的可能性很小,因为 SqlClientSymmetricKey 是一个简单的字节数组包装器。

_cache.Add(cacheLookupKey, encryptionKey, expirationTime) 内部的密钥损坏在我看来也不太可能。

这让我对这是如何发生的只有一个合乎逻辑的解释。 由于字节数组(我们的解密密钥)作为引用在任何地方传递,在特定情况下,该密钥的使用者会搞砸数组中的字节值。但不幸的是,我无法在代码中找到任何地方来证明该理论。

解决方法。一旦我在我的应用程序开始服务请求(在 Global.asax 内部)之前从加密的 table 添加了简单读取,那么问题就消失了。基本上,这个技巧帮助我保证只有一次从数据库中非并发读取数据会触发列键解密和 SqlSymmetricKeyCache 初始化。

很高兴听到 Microsoft 团队对这种非常奇怪的行为的一些评论。