diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs index e520bf7e6e..e4b71d6eb2 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; @@ -149,8 +150,17 @@ private enum EXECTYPE // cached metadata private _SqlMetaDataSet _cachedMetaData; - private Dictionary keysToBeSentToEnclave; - private bool requiresEnclaveComputations = false; + internal ConcurrentDictionary keysToBeSentToEnclave; + internal bool requiresEnclaveComputations = false; + + private bool ShouldCacheEncryptionMetadata + { + get + { + return !requiresEnclaveComputations || _activeConnection.Parser.AreEnclaveRetriesSupported; + } + } + internal EnclavePackage enclavePackage = null; private SqlEnclaveAttestationParameters enclaveAttestationParameters = null; private byte[] customData = null; @@ -3435,10 +3445,7 @@ private void ResetEncryptionState() } } - if (keysToBeSentToEnclave != null) - { - keysToBeSentToEnclave.Clear(); - } + keysToBeSentToEnclave?.Clear(); enclavePackage = null; requiresEnclaveComputations = false; enclaveAttestationParameters = null; @@ -4143,7 +4150,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi enclaveMetadataExists = false; } - if (isRequestedByEnclave) { if (string.IsNullOrWhiteSpace(this.Connection.EnclaveAttestationUrl)) @@ -4173,12 +4179,12 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi if (keysToBeSentToEnclave == null) { - keysToBeSentToEnclave = new Dictionary(); - keysToBeSentToEnclave.Add(currentOrdinal, cipherInfo); + keysToBeSentToEnclave = new ConcurrentDictionary(); + keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); } else if (!keysToBeSentToEnclave.ContainsKey(currentOrdinal)) { - keysToBeSentToEnclave.Add(currentOrdinal, cipherInfo); + keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); } requiresEnclaveComputations = true; @@ -4315,7 +4321,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi while (ds.Read()) { - if (attestationInfoRead) { throw SQL.MultipleRowsReturnedForAttestationInfo(); @@ -4357,8 +4362,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi } // If we are not in Batch RPC mode, update the query cache with the encryption MD. - // Enclave based Always Encrypted implementation on server side does not support cache at this point. So we should not cache if the query requires keys to be sent to enclave - if (!BatchRPCMode && !requiresEnclaveComputations && (this._parameters != null && this._parameters.Count > 0)) + if (!BatchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) { SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: true); } @@ -5285,8 +5289,8 @@ internal void OnReturnStatus(int status) // If we are not in Batch RPC mode, update the query cache with the encryption MD. // We can do this now that we have distinguished between ReturnValue and ReturnStatus. // Read comment in AddQueryMetadata() for more details. - // Enclave based Always Encrypted implementation on server side does not support cache at this point. So we should not cache if the query requires keys to be sent to enclave - if (!BatchRPCMode && CachingQueryMetadataPostponed && !requiresEnclaveComputations && (this._parameters != null && this._parameters.Count > 0)) + if (!BatchRPCMode && CachingQueryMetadataPostponed && + ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) { SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: false); } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 8f1ba312af..d506f9ba11 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2632,6 +2632,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data) Debug.Assert(_tceVersionSupported <= TdsEnums.MAX_SUPPORTED_TCE_VERSION, "Client support TCE version 2"); _parser.IsColumnEncryptionSupported = true; _parser.TceVersionSupported = _tceVersionSupported; + _parser.AreEnclaveRetriesSupported = _tceVersionSupported == 3; if (data.Length > 1) { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs index 9ddc6e767a..94e91ef3aa 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Diagnostics; @@ -22,7 +23,7 @@ sealed internal class SqlQueryMetadataCache const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. private readonly MemoryCache _cache; - private static readonly SqlQueryMetadataCache _singletonInstance = new SqlQueryMetadataCache(); + private static readonly SqlQueryMetadataCache _singletonInstance = new(); private int _inTrim = 0; private long _cacheHits = 0; private long _cacheMisses = 0; @@ -53,17 +54,17 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) } // Check the cache to see if we have the MD for this query cached. - string cacheLookupKey = GetCacheLookupKeyFromSqlCommand(sqlCommand); - if (cacheLookupKey == null) + (string cacheLookupKey, string enclaveLookupKey) = GetCacheLookupKeysFromSqlCommand(sqlCommand); + if (cacheLookupKey is null) { IncrementCacheMisses(); return false; } - Dictionary ciperMetadataDictionary = _cache.Get(cacheLookupKey) as Dictionary; + Dictionary cipherMetadataDictionary = _cache.Get(cacheLookupKey) as Dictionary; // If we had a cache miss just return false. - if (ciperMetadataDictionary == null) + if (cipherMetadataDictionary is null) { IncrementCacheMisses(); return false; @@ -73,7 +74,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) foreach (SqlParameter param in sqlCommand.Parameters) { SqlCipherMetadata paramCiperMetadata; - bool found = ciperMetadataDictionary.TryGetValue(param.ParameterNameFixed, out paramCiperMetadata); + bool found = cipherMetadataDictionary.TryGetValue(param.ParameterNameFixed, out paramCiperMetadata); // If we failed to identify the encryption for a specific parameter, clear up the cipher MD of all parameters and exit. if (!found) @@ -88,7 +89,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) } // Cached cipher MD should never have an initialized algorithm since this would contain the key. - Debug.Assert(paramCiperMetadata == null || !paramCiperMetadata.IsAlgorithmInitialized()); + Debug.Assert(paramCiperMetadata is null || !paramCiperMetadata.IsAlgorithmInitialized()); // We were able to identify the cipher MD for this parameter, so set it on the param. param.CipherMetadata = paramCiperMetadata; @@ -100,7 +101,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) { SqlCipherMetadata cipherMdCopy = null; - if (param.CipherMetadata != null) + if (param.CipherMetadata is not null) { cipherMdCopy = new SqlCipherMetadata( param.CipherMetadata.EncryptionInfo, @@ -113,7 +114,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) param.CipherMetadata = cipherMdCopy; - if (cipherMdCopy != null) + if (cipherMdCopy is not null) { // Try to get the encryption key. If the key information is stale, this might fail. // In this case, just fail the cache lookup. @@ -143,6 +144,13 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) } } + ConcurrentDictionary enclaveKeys = + _cache.Get(enclaveLookupKey) as ConcurrentDictionary; + if (enclaveKeys is not null) + { + sqlCommand.keysToBeSentToEnclave = CreateCopyOfEnclaveKeys(enclaveKeys); + } + IncrementCacheHits(); return true; } @@ -178,19 +186,19 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu } // Construct the entry and put it in the cache. - string cacheLookupKey = GetCacheLookupKeyFromSqlCommand(sqlCommand); - if (cacheLookupKey == null) + (string cacheLookupKey, string enclaveLookupKey) = GetCacheLookupKeysFromSqlCommand(sqlCommand); + if (cacheLookupKey is null) { return; } - Dictionary ciperMetadataDictionary = new Dictionary(sqlCommand.Parameters.Count); + Dictionary cipherMetadataDictionary = new(sqlCommand.Parameters.Count); // Create a copy of the cipherMD that doesn't have the algorithm and put it in the cache. foreach (SqlParameter param in sqlCommand.Parameters) { SqlCipherMetadata cipherMdCopy = null; - if (param.CipherMetadata != null) + if (param.CipherMetadata is not null) { cipherMdCopy = new SqlCipherMetadata( param.CipherMetadata.EncryptionInfo, @@ -202,9 +210,9 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu } // Cached cipher MD should never have an initialized algorithm since this would contain the key. - Debug.Assert(cipherMdCopy == null || !cipherMdCopy.IsAlgorithmInitialized()); + Debug.Assert(cipherMdCopy is null || !cipherMdCopy.IsAlgorithmInitialized()); - ciperMetadataDictionary.Add(param.ParameterNameFixed, cipherMdCopy); + cipherMetadataDictionary.Add(param.ParameterNameFixed, cipherMdCopy); } // If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly. @@ -228,7 +236,12 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu } // By default evict after 10 hours. - _cache.Set(cacheLookupKey, ciperMetadataDictionary, DateTimeOffset.UtcNow.AddHours(10)); + _cache.Set(cacheLookupKey, cipherMetadataDictionary, DateTimeOffset.UtcNow.AddHours(10)); + if (sqlCommand.requiresEnclaveComputations) + { + ConcurrentDictionary keysToBeCached = CreateCopyOfEnclaveKeys(sqlCommand.keysToBeSentToEnclave); + _cache.Set(enclaveLookupKey, keysToBeCached, DateTimeOffset.UtcNow.AddHours(10)); + } } /// @@ -236,13 +249,14 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu /// internal void InvalidateCacheEntry(SqlCommand sqlCommand) { - string cacheLookupKey = GetCacheLookupKeyFromSqlCommand(sqlCommand); - if (cacheLookupKey == null) + (string cacheLookupKey, string enclaveLookupKey) = GetCacheLookupKeysFromSqlCommand(sqlCommand); + if (cacheLookupKey is null) { return; } _cache.Remove(cacheLookupKey); + _cache.Remove(enclaveLookupKey); } @@ -271,26 +285,46 @@ private void ResetCacheCounts() _cacheMisses = 0; } - private String GetCacheLookupKeyFromSqlCommand(SqlCommand sqlCommand) + private (string, string) GetCacheLookupKeysFromSqlCommand(SqlCommand sqlCommand) { const int SqlIdentifierLength = 128; SqlConnection connection = sqlCommand.Connection; // Return null if we have no connection. - if (connection == null) + if (connection is null) { - return null; + return (null, null); } - StringBuilder cacheLookupKeyBuilder = new StringBuilder(connection.DataSource, capacity: connection.DataSource.Length + SqlIdentifierLength + sqlCommand.CommandText.Length + 6); + StringBuilder cacheLookupKeyBuilder = new(connection.DataSource, capacity: connection.DataSource.Length + SqlIdentifierLength + sqlCommand.CommandText.Length + 6); cacheLookupKeyBuilder.Append(":::"); // Pad database name to 128 characters to avoid any false cache matches because of weird DB names. cacheLookupKeyBuilder.Append(connection.Database.PadRight(SqlIdentifierLength)); cacheLookupKeyBuilder.Append(":::"); cacheLookupKeyBuilder.Append(sqlCommand.CommandText); - return cacheLookupKeyBuilder.ToString(); + string cacheLookupKey = cacheLookupKeyBuilder.ToString(); + string enclaveLookupKey = cacheLookupKeyBuilder.Append(":::enclaveKeys").ToString(); + return (cacheLookupKey, enclaveLookupKey); + } + + private ConcurrentDictionary CreateCopyOfEnclaveKeys(ConcurrentDictionary keysToBeSentToEnclave) + { + ConcurrentDictionary enclaveKeys = new(); + foreach (KeyValuePair kvp in keysToBeSentToEnclave) + { + int ordinal = kvp.Key; + SqlTceCipherInfoEntry original = kvp.Value; + SqlTceCipherInfoEntry copy = new(ordinal); + foreach (SqlEncryptionKeyInfo cekInfo in original.ColumnEncryptionKeyValues) + { + copy.Add(cekInfo.encryptedKey, cekInfo.databaseId, cekInfo.cekId, cekInfo.cekVersion, + cekInfo.cekMdVersion, cekInfo.keyPath, cekInfo.keyStoreName, cekInfo.algorithmName); + } + enclaveKeys.TryAdd(ordinal, copy); + } + return enclaveKeys; } } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs index 44a8f02941..728e521847 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -985,7 +985,7 @@ internal static string GetSniContextEnumName(SniContext sniContext) } // TCE Related constants - internal const byte MAX_SUPPORTED_TCE_VERSION = 0x02; // max version + internal const byte MAX_SUPPORTED_TCE_VERSION = 0x03; // max version internal const byte MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT = 0x02; // min version with enclave support internal const ushort MAX_TCE_CIPHERINFO_SIZE = 2048; // max size of cipherinfo blob internal const long MAX_TCE_CIPHERTEXT_SIZE = 2147483648; // max size of encrypted blob- currently 2GB. diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index 5c526e9c30..04ec52be41 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -157,6 +157,11 @@ internal sealed partial class TdsParser /// internal byte TceVersionSupported { get; set; } + /// + /// Server supports retrying when the enclave CEKs sent by the client do not match what is needed for the query to run. + /// + internal bool AreEnclaveRetriesSupported { get; set; } + /// /// Type of enclave being used by the server /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs index 93002d88e4..bb0264666f 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -24,6 +24,7 @@ using Microsoft.Data.Sql; using Microsoft.Data.SqlClient.Server; using SysTx = System.Transactions; +using System.Collections.Concurrent; namespace Microsoft.Data.SqlClient { @@ -120,9 +121,6 @@ private enum EXECTYPE // cut down on object creation and cache all these // cached metadata private _SqlMetaDataSet _cachedMetaData; - - private Dictionary keysToBeSentToEnclave; - private bool requiresEnclaveComputations = false; internal EnclavePackage enclavePackage = null; private SqlEnclaveAttestationParameters enclaveAttestationParameters = null; private byte[] customData = null; @@ -164,6 +162,15 @@ internal bool ShouldUseEnclaveBasedWorkflow get { return !string.IsNullOrWhiteSpace(_activeConnection.EnclaveAttestationUrl) && IsColumnEncryptionEnabled; } } + internal ConcurrentDictionary keysToBeSentToEnclave; + internal bool requiresEnclaveComputations = false; + private bool ShouldCacheEncryptionMetadata + { + get + { + return !requiresEnclaveComputations || _activeConnection.Parser.AreEnclaveRetriesSupported; + } + } /// /// Per-command custom providers. It can be provided by the user and can be set more than once. /// @@ -3990,10 +3997,7 @@ private void ResetEncryptionState() } } - if (keysToBeSentToEnclave != null) - { - keysToBeSentToEnclave.Clear(); - } + keysToBeSentToEnclave?.Clear(); enclavePackage = null; requiresEnclaveComputations = false; enclaveAttestationParameters = null; @@ -4752,7 +4756,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi enclaveMetadataExists = false; } - if (isRequestedByEnclave) { if (string.IsNullOrWhiteSpace(this.Connection.EnclaveAttestationUrl)) @@ -4782,12 +4785,12 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi if (keysToBeSentToEnclave == null) { - keysToBeSentToEnclave = new Dictionary(); - keysToBeSentToEnclave.Add(currentOrdinal, cipherInfo); + keysToBeSentToEnclave = new ConcurrentDictionary(); + keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); } else if (!keysToBeSentToEnclave.ContainsKey(currentOrdinal)) { - keysToBeSentToEnclave.Add(currentOrdinal, cipherInfo); + keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); } requiresEnclaveComputations = true; @@ -4919,7 +4922,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi while (ds.Read()) { - if (attestationInfoRead) { throw SQL.MultipleRowsReturnedForAttestationInfo(); @@ -4961,8 +4963,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi } // If we are not in Batch RPC mode, update the query cache with the encryption MD. - // Enclave based Always Encrypted implementation on server side does not support cache at this point. So we should not cache if the query requires keys to be sent to enclave - if (!BatchRPCMode && !requiresEnclaveComputations && (this._parameters != null && this._parameters.Count > 0)) + if (!BatchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) { SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: true); } @@ -6156,8 +6157,8 @@ internal void OnReturnStatus(int status) // If we are not in Batch RPC mode, update the query cache with the encryption MD. // We can do this now that we have distinguished between ReturnValue and ReturnStatus. // Read comment in AddQueryMetadata() for more details. - // Enclave based Always Encrypted implementation on server side does not support cache at this point. So we should not cache if the query requires keys to be sent to enclave - if (!BatchRPCMode && CachingQueryMetadataPostponed && !requiresEnclaveComputations && (this._parameters != null && this._parameters.Count > 0)) + if (!BatchRPCMode && CachingQueryMetadataPostponed && + ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) { SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: false); } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 42a6eec32c..b9b0a217e4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -3059,6 +3059,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data) Debug.Assert(_tceVersionSupported <= TdsEnums.MAX_SUPPORTED_TCE_VERSION, "Client support TCE version 2"); _parser.IsColumnEncryptionSupported = true; _parser.TceVersionSupported = _tceVersionSupported; + _parser.AreEnclaveRetriesSupported = _tceVersionSupported == 3; if (data.Length > 1) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs index 3b31138cb4..dad8b877c6 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Diagnostics; @@ -21,7 +22,7 @@ sealed internal class SqlQueryMetadataCache const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming. private readonly MemoryCache _cache; - private static readonly SqlQueryMetadataCache _singletonInstance = new SqlQueryMetadataCache(); + private static readonly SqlQueryMetadataCache _singletonInstance = new(); private int _inTrim = 0; private long _cacheHits = 0; private long _cacheMisses = 0; @@ -55,17 +56,19 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) } // Check the cache to see if we have the MD for this query cached. - string cacheLookupKey = GetCacheLookupKeyFromSqlCommand(sqlCommand); - if (cacheLookupKey == null) + Tuple keys = GetCacheLookupKeysFromSqlCommand(sqlCommand); + string cacheLookupKey = keys?.Item1; + string enclaveLookupKey = keys?.Item2; + if (cacheLookupKey is null) { IncrementCacheMisses(); return false; } - Dictionary ciperMetadataDictionary = _cache.Get(cacheLookupKey) as Dictionary; + Dictionary cipherMetadataDictionary = _cache.Get(cacheLookupKey) as Dictionary; // If we had a cache miss just return false. - if (ciperMetadataDictionary == null) + if (cipherMetadataDictionary is null) { IncrementCacheMisses(); return false; @@ -75,7 +78,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) foreach (SqlParameter param in sqlCommand.Parameters) { SqlCipherMetadata paramCiperMetadata; - bool found = ciperMetadataDictionary.TryGetValue(param.ParameterNameFixed, out paramCiperMetadata); + bool found = cipherMetadataDictionary.TryGetValue(param.ParameterNameFixed, out paramCiperMetadata); // If we failed to identify the encryption for a specific parameter, clear up the cipher MD of all parameters and exit. if (!found) @@ -90,7 +93,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) } // Cached cipher MD should never have an initialized algorithm since this would contain the key. - Debug.Assert(paramCiperMetadata == null || !paramCiperMetadata.IsAlgorithmInitialized()); + Debug.Assert(paramCiperMetadata is null || !paramCiperMetadata.IsAlgorithmInitialized()); // We were able to identify the cipher MD for this parameter, so set it on the param. param.CipherMetadata = paramCiperMetadata; @@ -102,7 +105,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) { SqlCipherMetadata cipherMdCopy = null; - if (param.CipherMetadata != null) + if (param.CipherMetadata is not null) { cipherMdCopy = new SqlCipherMetadata( param.CipherMetadata.EncryptionInfo, @@ -115,7 +118,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) param.CipherMetadata = cipherMdCopy; - if (cipherMdCopy != null) + if (cipherMdCopy is not null) { // Try to get the encryption key. If the key information is stale, this might fail. // In this case, just fail the cache lookup. @@ -145,6 +148,13 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand) } } + ConcurrentDictionary enclaveKeys = + _cache.Get(enclaveLookupKey) as ConcurrentDictionary; + if (enclaveKeys is not null) + { + sqlCommand.keysToBeSentToEnclave = CreateCopyOfEnclaveKeys(enclaveKeys); + } + IncrementCacheHits(); return true; } @@ -180,19 +190,21 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu } // Construct the entry and put it in the cache. - string cacheLookupKey = GetCacheLookupKeyFromSqlCommand(sqlCommand); - if (cacheLookupKey == null) + Tuple keys = GetCacheLookupKeysFromSqlCommand(sqlCommand); + string cacheLookupKey = keys?.Item1; + string enclaveLookupKey = keys?.Item2; + if (cacheLookupKey is null) { return; } - Dictionary ciperMetadataDictionary = new Dictionary(sqlCommand.Parameters.Count); + Dictionary cipherMetadataDictionary = new(sqlCommand.Parameters.Count); // Create a copy of the cipherMD that doesn't have the algorithm and put it in the cache. foreach (SqlParameter param in sqlCommand.Parameters) { SqlCipherMetadata cipherMdCopy = null; - if (param.CipherMetadata != null) + if (param.CipherMetadata is not null) { cipherMdCopy = new SqlCipherMetadata( param.CipherMetadata.EncryptionInfo, @@ -204,9 +216,9 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu } // Cached cipher MD should never have an initialized algorithm since this would contain the key. - Debug.Assert(cipherMdCopy == null || !cipherMdCopy.IsAlgorithmInitialized()); + Debug.Assert(cipherMdCopy is null || !cipherMdCopy.IsAlgorithmInitialized()); - ciperMetadataDictionary.Add(param.ParameterNameFixed, cipherMdCopy); + cipherMetadataDictionary.Add(param.ParameterNameFixed, cipherMdCopy); } // If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly. @@ -230,7 +242,12 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu } // By default evict after 10 hours. - _cache.Set(cacheLookupKey, ciperMetadataDictionary, DateTimeOffset.UtcNow.AddHours(10)); + _cache.Set(cacheLookupKey, cipherMetadataDictionary, DateTimeOffset.UtcNow.AddHours(10)); + if (sqlCommand.requiresEnclaveComputations) + { + ConcurrentDictionary keysToBeCached = CreateCopyOfEnclaveKeys(sqlCommand.keysToBeSentToEnclave); + _cache.Set(enclaveLookupKey, keysToBeCached, DateTimeOffset.UtcNow.AddHours(10)); + } } /// @@ -238,13 +255,16 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu /// internal void InvalidateCacheEntry(SqlCommand sqlCommand) { - string cacheLookupKey = GetCacheLookupKeyFromSqlCommand(sqlCommand); - if (cacheLookupKey == null) + Tuple keys = GetCacheLookupKeysFromSqlCommand(sqlCommand); + string cacheLookupKey = keys?.Item1; + string enclaveLookupKey = keys?.Item2; + if (cacheLookupKey is null) { return; } _cache.Remove(cacheLookupKey); + _cache.Remove(enclaveLookupKey); } @@ -273,26 +293,46 @@ private void ResetCacheCounts() _cacheMisses = 0; } - private String GetCacheLookupKeyFromSqlCommand(SqlCommand sqlCommand) + private Tuple GetCacheLookupKeysFromSqlCommand(SqlCommand sqlCommand) { const int SqlIdentifierLength = 128; SqlConnection connection = sqlCommand.Connection; // Return null if we have no connection. - if (connection == null) + if (connection is null) { return null; } - StringBuilder cacheLookupKeyBuilder = new StringBuilder(connection.DataSource, capacity: connection.DataSource.Length + SqlIdentifierLength + sqlCommand.CommandText.Length + 6); + StringBuilder cacheLookupKeyBuilder = new(connection.DataSource, capacity: connection.DataSource.Length + SqlIdentifierLength + sqlCommand.CommandText.Length + 6); cacheLookupKeyBuilder.Append(":::"); // Pad database name to 128 characters to avoid any false cache matches because of weird DB names. cacheLookupKeyBuilder.Append(connection.Database.PadRight(SqlIdentifierLength)); cacheLookupKeyBuilder.Append(":::"); cacheLookupKeyBuilder.Append(sqlCommand.CommandText); - return cacheLookupKeyBuilder.ToString(); + string cacheLookupKey = cacheLookupKeyBuilder.ToString(); + string enclaveLookupKey = cacheLookupKeyBuilder.Append(":::enclaveKeys").ToString(); + return Tuple.Create(cacheLookupKey, enclaveLookupKey); + } + + private ConcurrentDictionary CreateCopyOfEnclaveKeys(ConcurrentDictionary keysToBeSentToEnclave) + { + ConcurrentDictionary enclaveKeys = new(); + foreach (KeyValuePair kvp in keysToBeSentToEnclave) + { + int ordinal = kvp.Key; + SqlTceCipherInfoEntry original = kvp.Value; + SqlTceCipherInfoEntry copy = new(ordinal); + foreach (SqlEncryptionKeyInfo cekInfo in original.ColumnEncryptionKeyValues) + { + copy.Add(cekInfo.encryptedKey, cekInfo.databaseId, cekInfo.cekId, cekInfo.cekVersion, + cekInfo.cekMdVersion, cekInfo.keyPath, cekInfo.keyStoreName, cekInfo.algorithmName); + } + enclaveKeys.TryAdd(ordinal, copy); + } + return enclaveKeys; } } } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs index 417657eb49..20a7fad79e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -949,7 +949,7 @@ internal enum FedAuthInfoId : byte internal const byte DATA_CLASSIFICATION_VERSION_MAX_SUPPORTED = 0x02; // TCE Related constants - internal const byte MAX_SUPPORTED_TCE_VERSION = 0x02; // max version + internal const byte MAX_SUPPORTED_TCE_VERSION = 0x03; // max version internal const byte MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT = 0x02; // min version with enclave support internal const ushort MAX_TCE_CIPHERINFO_SIZE = 2048; // max size of cipherinfo blob internal const long MAX_TCE_CIPHERTEXT_SIZE = 2147483648; // max size of encrypted blob- currently 2GB. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 328867ddca..f1a714f319 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -279,6 +279,11 @@ internal bool IsColumnEncryptionSupported /// internal byte TceVersionSupported { get; set; } + /// + /// Server supports retrying when the enclave CEKs sent by the client do not match what is needed for the query to run. + /// + internal bool AreEnclaveRetriesSupported { get; set; } + /// /// Type of enclave being used by the server /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.Crypto.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.Crypto.cs index b679414cbe..241c6aaf58 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.Crypto.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.Crypto.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; namespace Microsoft.Data.SqlClient @@ -132,7 +133,7 @@ private SqlColumnEncryptionEnclaveProvider GetEnclaveProvider(SqlConnectionAttes /// connection executing the query /// command executing the query /// - internal EnclavePackage GenerateEnclavePackage(SqlConnectionAttestationProtocol attestationProtocol, Dictionary keysToBeSentToEnclave, string queryText, string enclaveType, EnclaveSessionParameters enclaveSessionParameters, SqlConnection connection, SqlCommand command) + internal EnclavePackage GenerateEnclavePackage(SqlConnectionAttestationProtocol attestationProtocol, ConcurrentDictionary keysToBeSentToEnclave, string queryText, string enclaveType, EnclaveSessionParameters enclaveSessionParameters, SqlConnection connection, SqlCommand command) { SqlEnclaveSession sqlEnclaveSession; long counter; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.NotSupported.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.NotSupported.cs index 19d1d1f58f..7b43c8cb66 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.NotSupported.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.NotSupported.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; namespace Microsoft.Data.SqlClient @@ -36,7 +37,7 @@ internal void GetEnclaveSession(SqlConnectionAttestationProtocol attestationProt throw new PlatformNotSupportedException(); } - internal EnclavePackage GenerateEnclavePackage(SqlConnectionAttestationProtocol attestationProtocol, Dictionary keysTobeSentToEnclave, string queryText, string enclaveType, EnclaveSessionParameters enclaveSessionParameters, SqlConnection connection, SqlCommand command) + internal EnclavePackage GenerateEnclavePackage(SqlConnectionAttestationProtocol attestationProtocol, ConcurrentDictionary keysTobeSentToEnclave, string queryText, string enclaveType, EnclaveSessionParameters enclaveSessionParameters, SqlConnection connection, SqlCommand command) { throw new PlatformNotSupportedException(); } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs index dc45b979f3..fc10fda351 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Security.Cryptography; using System.Text; @@ -48,7 +49,7 @@ private byte[] GetUintBytes(string enclaveType, int intValue, string variableNam /// /// /// - private List GetDecryptedKeysToBeSentToEnclave(Dictionary keysTobeSentToEnclave, string serverName, SqlConnection connection, SqlCommand command) + private List GetDecryptedKeysToBeSentToEnclave(ConcurrentDictionary keysTobeSentToEnclave, string serverName, SqlConnection connection, SqlCommand command) { List decryptedKeysToBeSentToEnclave = new List(); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs index 8f18312f94..31264fa396 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ApiShould.cs @@ -2242,6 +2242,65 @@ public void TestCommandCustomKeyStoreProviderDuringAeQuery(string connectionStri } } + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringSetupForAE), nameof(DataTestUtility.EnclaveEnabled))] + [ClassData(typeof(AEConnectionStringProvider))] + public void TestRetryWhenAEParameterMetadataCacheIsStale(string connectionString) + { + CleanUpTable(connectionString, _tableName); + + const int customerId = 50; + IList values = GetValues(dataHint: customerId); + InsertRows(tableName: _tableName, numberofRows: 1, values: values, connection: connectionString); + + ApiTestTable table = _fixture.ApiTestTable as ApiTestTable; + string enclaveSelectQuery = $@"SELECT CustomerId, FirstName, LastName FROM [{_tableName}] WHERE CustomerId > @CustomerId"; + string alterCekQueryFormatString = "ALTER TABLE [{0}] " + + "ALTER COLUMN [CustomerId] [int] " + + "ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{1}], " + + "ENCRYPTION_TYPE = Randomized, " + + "ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'); " + + "ALTER DATABASE SCOPED CONFIGURATION CLEAR PROCEDURE_CACHE;"; + + using SqlConnection sqlConnection = new(connectionString); + sqlConnection.Open(); + + // execute the select query to add its parameter metadata and enclave-required CEKs to the cache + using SqlCommand cmd = new SqlCommand(enclaveSelectQuery, sqlConnection, null, SqlCommandColumnEncryptionSetting.Enabled); + cmd.Parameters.AddWithValue("CustomerId", 0); + using (SqlDataReader reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + Assert.Equal(customerId, (int)reader[0]); + } + reader.Close(); + }; + + // change the CEK for the CustomerId column from ColumnEncryptionKey1 to ColumnEncryptionKey2 + // this will render the select query's cache entry stale + cmd.Parameters.Clear(); + cmd.CommandText = string.Format(alterCekQueryFormatString, _tableName, table.columnEncryptionKey2.Name); + cmd.ExecuteNonQuery(); + + // execute the select query again. it will attempt to use the stale cache entry, receive + // a retryable error from the server, remove the stale cache entry, retry and succeed + cmd.CommandText = enclaveSelectQuery; + cmd.Parameters.AddWithValue("@CustomerId", 0); + using (SqlDataReader reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + Assert.Equal(customerId, (int)reader[0]); + } + reader.Close(); + } + + // revert the CEK change to the CustomerId column + cmd.Parameters.Clear(); + cmd.CommandText = string.Format(alterCekQueryFormatString, _tableName, table.columnEncryptionKey1.Name); + cmd.ExecuteNonQuery(); + } + private void ExecuteQueryThatRequiresCustomKeyStoreProvider(SqlConnection connection) { using (SqlCommand command = CreateCommandThatRequiresCustomKeyStoreProvider(connection)) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 2aefb3fe36..4a98e0a390 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -40,7 +40,7 @@ public static class DataTestUtility public static readonly string AKVClientSecret = null; public static List AEConnStrings = new List(); public static List AEConnStringsSetup = new List(); - public static readonly bool EnclaveEnabled = false; + public static bool EnclaveEnabled { get; private set; } = false; public static readonly bool TracingEnabled = false; public static readonly bool SupportsIntegratedSecurity = false; public static readonly bool SupportsLocalDb = false;