Skip to content

Commit 89e15c4

Browse files
author
Johnny Pham
authored
Extend SqlQueryMetadataCache to include enclave-required keys (#1062)
1 parent 688b931 commit 89e15c4

File tree

15 files changed

+235
-82
lines changed

15 files changed

+235
-82
lines changed

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Collections.Concurrent;
67
using System.Collections.Generic;
78
using System.Collections.ObjectModel;
89
using System.Data;
@@ -149,8 +150,17 @@ private enum EXECTYPE
149150
// cached metadata
150151
private _SqlMetaDataSet _cachedMetaData;
151152

152-
private Dictionary<int, SqlTceCipherInfoEntry> keysToBeSentToEnclave;
153-
private bool requiresEnclaveComputations = false;
153+
internal ConcurrentDictionary<int, SqlTceCipherInfoEntry> keysToBeSentToEnclave;
154+
internal bool requiresEnclaveComputations = false;
155+
156+
private bool ShouldCacheEncryptionMetadata
157+
{
158+
get
159+
{
160+
return !requiresEnclaveComputations || _activeConnection.Parser.AreEnclaveRetriesSupported;
161+
}
162+
}
163+
154164
internal EnclavePackage enclavePackage = null;
155165
private SqlEnclaveAttestationParameters enclaveAttestationParameters = null;
156166
private byte[] customData = null;
@@ -3435,10 +3445,7 @@ private void ResetEncryptionState()
34353445
}
34363446
}
34373447

3438-
if (keysToBeSentToEnclave != null)
3439-
{
3440-
keysToBeSentToEnclave.Clear();
3441-
}
3448+
keysToBeSentToEnclave?.Clear();
34423449
enclavePackage = null;
34433450
requiresEnclaveComputations = false;
34443451
enclaveAttestationParameters = null;
@@ -4143,7 +4150,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi
41434150
enclaveMetadataExists = false;
41444151
}
41454152

4146-
41474153
if (isRequestedByEnclave)
41484154
{
41494155
if (string.IsNullOrWhiteSpace(this.Connection.EnclaveAttestationUrl))
@@ -4173,12 +4179,12 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi
41734179

41744180
if (keysToBeSentToEnclave == null)
41754181
{
4176-
keysToBeSentToEnclave = new Dictionary<int, SqlTceCipherInfoEntry>();
4177-
keysToBeSentToEnclave.Add(currentOrdinal, cipherInfo);
4182+
keysToBeSentToEnclave = new ConcurrentDictionary<int, SqlTceCipherInfoEntry>();
4183+
keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo);
41784184
}
41794185
else if (!keysToBeSentToEnclave.ContainsKey(currentOrdinal))
41804186
{
4181-
keysToBeSentToEnclave.Add(currentOrdinal, cipherInfo);
4187+
keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo);
41824188
}
41834189

41844190
requiresEnclaveComputations = true;
@@ -4315,7 +4321,6 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi
43154321

43164322
while (ds.Read())
43174323
{
4318-
43194324
if (attestationInfoRead)
43204325
{
43214326
throw SQL.MultipleRowsReturnedForAttestationInfo();
@@ -4357,8 +4362,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi
43574362
}
43584363

43594364
// If we are not in Batch RPC mode, update the query cache with the encryption MD.
4360-
// 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
4361-
if (!BatchRPCMode && !requiresEnclaveComputations && (this._parameters != null && this._parameters.Count > 0))
4365+
if (!BatchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0))
43624366
{
43634367
SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: true);
43644368
}
@@ -5285,8 +5289,8 @@ internal void OnReturnStatus(int status)
52855289
// If we are not in Batch RPC mode, update the query cache with the encryption MD.
52865290
// We can do this now that we have distinguished between ReturnValue and ReturnStatus.
52875291
// Read comment in AddQueryMetadata() for more details.
5288-
// 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
5289-
if (!BatchRPCMode && CachingQueryMetadataPostponed && !requiresEnclaveComputations && (this._parameters != null && this._parameters.Count > 0))
5292+
if (!BatchRPCMode && CachingQueryMetadataPostponed &&
5293+
ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0))
52905294
{
52915295
SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: false);
52925296
}

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2632,6 +2632,7 @@ internal void OnFeatureExtAck(int featureId, byte[] data)
26322632
Debug.Assert(_tceVersionSupported <= TdsEnums.MAX_SUPPORTED_TCE_VERSION, "Client support TCE version 2");
26332633
_parser.IsColumnEncryptionSupported = true;
26342634
_parser.TceVersionSupported = _tceVersionSupported;
2635+
_parser.AreEnclaveRetriesSupported = _tceVersionSupported == 3;
26352636

26362637
if (data.Length > 1)
26372638
{

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlQueryMetadataCache.cs

Lines changed: 57 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System;
6+
using System.Collections.Concurrent;
67
using System.Collections.Generic;
78
using System.Data;
89
using System.Diagnostics;
@@ -22,7 +23,7 @@ sealed internal class SqlQueryMetadataCache
2223
const int CacheTrimThreshold = 300; // Threshold above the cache size when we start trimming.
2324

2425
private readonly MemoryCache _cache;
25-
private static readonly SqlQueryMetadataCache _singletonInstance = new SqlQueryMetadataCache();
26+
private static readonly SqlQueryMetadataCache _singletonInstance = new();
2627
private int _inTrim = 0;
2728
private long _cacheHits = 0;
2829
private long _cacheMisses = 0;
@@ -53,17 +54,17 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand)
5354
}
5455

5556
// Check the cache to see if we have the MD for this query cached.
56-
string cacheLookupKey = GetCacheLookupKeyFromSqlCommand(sqlCommand);
57-
if (cacheLookupKey == null)
57+
(string cacheLookupKey, string enclaveLookupKey) = GetCacheLookupKeysFromSqlCommand(sqlCommand);
58+
if (cacheLookupKey is null)
5859
{
5960
IncrementCacheMisses();
6061
return false;
6162
}
6263

63-
Dictionary<string, SqlCipherMetadata> ciperMetadataDictionary = _cache.Get(cacheLookupKey) as Dictionary<string, SqlCipherMetadata>;
64+
Dictionary<string, SqlCipherMetadata> cipherMetadataDictionary = _cache.Get(cacheLookupKey) as Dictionary<string, SqlCipherMetadata>;
6465

6566
// If we had a cache miss just return false.
66-
if (ciperMetadataDictionary == null)
67+
if (cipherMetadataDictionary is null)
6768
{
6869
IncrementCacheMisses();
6970
return false;
@@ -73,7 +74,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand)
7374
foreach (SqlParameter param in sqlCommand.Parameters)
7475
{
7576
SqlCipherMetadata paramCiperMetadata;
76-
bool found = ciperMetadataDictionary.TryGetValue(param.ParameterNameFixed, out paramCiperMetadata);
77+
bool found = cipherMetadataDictionary.TryGetValue(param.ParameterNameFixed, out paramCiperMetadata);
7778

7879
// If we failed to identify the encryption for a specific parameter, clear up the cipher MD of all parameters and exit.
7980
if (!found)
@@ -88,7 +89,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand)
8889
}
8990

9091
// Cached cipher MD should never have an initialized algorithm since this would contain the key.
91-
Debug.Assert(paramCiperMetadata == null || !paramCiperMetadata.IsAlgorithmInitialized());
92+
Debug.Assert(paramCiperMetadata is null || !paramCiperMetadata.IsAlgorithmInitialized());
9293

9394
// We were able to identify the cipher MD for this parameter, so set it on the param.
9495
param.CipherMetadata = paramCiperMetadata;
@@ -100,7 +101,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand)
100101
{
101102
SqlCipherMetadata cipherMdCopy = null;
102103

103-
if (param.CipherMetadata != null)
104+
if (param.CipherMetadata is not null)
104105
{
105106
cipherMdCopy = new SqlCipherMetadata(
106107
param.CipherMetadata.EncryptionInfo,
@@ -113,7 +114,7 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand)
113114

114115
param.CipherMetadata = cipherMdCopy;
115116

116-
if (cipherMdCopy != null)
117+
if (cipherMdCopy is not null)
117118
{
118119
// Try to get the encryption key. If the key information is stale, this might fail.
119120
// In this case, just fail the cache lookup.
@@ -143,6 +144,13 @@ internal bool GetQueryMetadataIfExists(SqlCommand sqlCommand)
143144
}
144145
}
145146

147+
ConcurrentDictionary<int, SqlTceCipherInfoEntry> enclaveKeys =
148+
_cache.Get(enclaveLookupKey) as ConcurrentDictionary<int, SqlTceCipherInfoEntry>;
149+
if (enclaveKeys is not null)
150+
{
151+
sqlCommand.keysToBeSentToEnclave = CreateCopyOfEnclaveKeys(enclaveKeys);
152+
}
153+
146154
IncrementCacheHits();
147155
return true;
148156
}
@@ -178,19 +186,19 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu
178186
}
179187

180188
// Construct the entry and put it in the cache.
181-
string cacheLookupKey = GetCacheLookupKeyFromSqlCommand(sqlCommand);
182-
if (cacheLookupKey == null)
189+
(string cacheLookupKey, string enclaveLookupKey) = GetCacheLookupKeysFromSqlCommand(sqlCommand);
190+
if (cacheLookupKey is null)
183191
{
184192
return;
185193
}
186194

187-
Dictionary<string, SqlCipherMetadata> ciperMetadataDictionary = new Dictionary<string, SqlCipherMetadata>(sqlCommand.Parameters.Count);
195+
Dictionary<string, SqlCipherMetadata> cipherMetadataDictionary = new(sqlCommand.Parameters.Count);
188196

189197
// Create a copy of the cipherMD that doesn't have the algorithm and put it in the cache.
190198
foreach (SqlParameter param in sqlCommand.Parameters)
191199
{
192200
SqlCipherMetadata cipherMdCopy = null;
193-
if (param.CipherMetadata != null)
201+
if (param.CipherMetadata is not null)
194202
{
195203
cipherMdCopy = new SqlCipherMetadata(
196204
param.CipherMetadata.EncryptionInfo,
@@ -202,9 +210,9 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu
202210
}
203211

204212
// Cached cipher MD should never have an initialized algorithm since this would contain the key.
205-
Debug.Assert(cipherMdCopy == null || !cipherMdCopy.IsAlgorithmInitialized());
213+
Debug.Assert(cipherMdCopy is null || !cipherMdCopy.IsAlgorithmInitialized());
206214

207-
ciperMetadataDictionary.Add(param.ParameterNameFixed, cipherMdCopy);
215+
cipherMetadataDictionary.Add(param.ParameterNameFixed, cipherMdCopy);
208216
}
209217

210218
// If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly.
@@ -228,21 +236,27 @@ internal void AddQueryMetadata(SqlCommand sqlCommand, bool ignoreQueriesWithRetu
228236
}
229237

230238
// By default evict after 10 hours.
231-
_cache.Set(cacheLookupKey, ciperMetadataDictionary, DateTimeOffset.UtcNow.AddHours(10));
239+
_cache.Set(cacheLookupKey, cipherMetadataDictionary, DateTimeOffset.UtcNow.AddHours(10));
240+
if (sqlCommand.requiresEnclaveComputations)
241+
{
242+
ConcurrentDictionary<int, SqlTceCipherInfoEntry> keysToBeCached = CreateCopyOfEnclaveKeys(sqlCommand.keysToBeSentToEnclave);
243+
_cache.Set(enclaveLookupKey, keysToBeCached, DateTimeOffset.UtcNow.AddHours(10));
244+
}
232245
}
233246

234247
/// <summary>
235248
/// <para> Remove the metadata for a specific query from the cache.</para>
236249
/// </summary>
237250
internal void InvalidateCacheEntry(SqlCommand sqlCommand)
238251
{
239-
string cacheLookupKey = GetCacheLookupKeyFromSqlCommand(sqlCommand);
240-
if (cacheLookupKey == null)
252+
(string cacheLookupKey, string enclaveLookupKey) = GetCacheLookupKeysFromSqlCommand(sqlCommand);
253+
if (cacheLookupKey is null)
241254
{
242255
return;
243256
}
244257

245258
_cache.Remove(cacheLookupKey);
259+
_cache.Remove(enclaveLookupKey);
246260
}
247261

248262

@@ -271,26 +285,46 @@ private void ResetCacheCounts()
271285
_cacheMisses = 0;
272286
}
273287

274-
private String GetCacheLookupKeyFromSqlCommand(SqlCommand sqlCommand)
288+
private (string, string) GetCacheLookupKeysFromSqlCommand(SqlCommand sqlCommand)
275289
{
276290
const int SqlIdentifierLength = 128;
277291

278292
SqlConnection connection = sqlCommand.Connection;
279293

280294
// Return null if we have no connection.
281-
if (connection == null)
295+
if (connection is null)
282296
{
283-
return null;
297+
return (null, null);
284298
}
285299

286-
StringBuilder cacheLookupKeyBuilder = new StringBuilder(connection.DataSource, capacity: connection.DataSource.Length + SqlIdentifierLength + sqlCommand.CommandText.Length + 6);
300+
StringBuilder cacheLookupKeyBuilder = new(connection.DataSource, capacity: connection.DataSource.Length + SqlIdentifierLength + sqlCommand.CommandText.Length + 6);
287301
cacheLookupKeyBuilder.Append(":::");
288302
// Pad database name to 128 characters to avoid any false cache matches because of weird DB names.
289303
cacheLookupKeyBuilder.Append(connection.Database.PadRight(SqlIdentifierLength));
290304
cacheLookupKeyBuilder.Append(":::");
291305
cacheLookupKeyBuilder.Append(sqlCommand.CommandText);
292306

293-
return cacheLookupKeyBuilder.ToString();
307+
string cacheLookupKey = cacheLookupKeyBuilder.ToString();
308+
string enclaveLookupKey = cacheLookupKeyBuilder.Append(":::enclaveKeys").ToString();
309+
return (cacheLookupKey, enclaveLookupKey);
310+
}
311+
312+
private ConcurrentDictionary<int, SqlTceCipherInfoEntry> CreateCopyOfEnclaveKeys(ConcurrentDictionary<int, SqlTceCipherInfoEntry> keysToBeSentToEnclave)
313+
{
314+
ConcurrentDictionary<int, SqlTceCipherInfoEntry> enclaveKeys = new();
315+
foreach (KeyValuePair<int, SqlTceCipherInfoEntry> kvp in keysToBeSentToEnclave)
316+
{
317+
int ordinal = kvp.Key;
318+
SqlTceCipherInfoEntry original = kvp.Value;
319+
SqlTceCipherInfoEntry copy = new(ordinal);
320+
foreach (SqlEncryptionKeyInfo cekInfo in original.ColumnEncryptionKeyValues)
321+
{
322+
copy.Add(cekInfo.encryptedKey, cekInfo.databaseId, cekInfo.cekId, cekInfo.cekVersion,
323+
cekInfo.cekMdVersion, cekInfo.keyPath, cekInfo.keyStoreName, cekInfo.algorithmName);
324+
}
325+
enclaveKeys.TryAdd(ordinal, copy);
326+
}
327+
return enclaveKeys;
294328
}
295329
}
296330
}

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ internal static string GetSniContextEnumName(SniContext sniContext)
985985
}
986986

987987
// TCE Related constants
988-
internal const byte MAX_SUPPORTED_TCE_VERSION = 0x02; // max version
988+
internal const byte MAX_SUPPORTED_TCE_VERSION = 0x03; // max version
989989
internal const byte MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT = 0x02; // min version with enclave support
990990
internal const ushort MAX_TCE_CIPHERINFO_SIZE = 2048; // max size of cipherinfo blob
991991
internal const long MAX_TCE_CIPHERTEXT_SIZE = 2147483648; // max size of encrypted blob- currently 2GB.

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ internal sealed partial class TdsParser
157157
/// </summary>
158158
internal byte TceVersionSupported { get; set; }
159159

160+
/// <summary>
161+
/// Server supports retrying when the enclave CEKs sent by the client do not match what is needed for the query to run.
162+
/// </summary>
163+
internal bool AreEnclaveRetriesSupported { get; set; }
164+
160165
/// <summary>
161166
/// Type of enclave being used by the server
162167
/// </summary>

0 commit comments

Comments
 (0)