[coop] Temporarily restore MonoThreadInfo when TLS destructor runs. Fixes #43099
[mono.git] / mcs / class / referencesource / System.Data / System / Data / SqlClient / SqlSymmetricKeyCache.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="SqlSymmetricKeyCache.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 // <owner current="true" primary="true">krishnib</owner>
6 // <owner current="true" primary="false">balnee</owner>
7 //------------------------------------------------------------------------------
8
9 namespace System.Data.SqlClient {
10     using System;
11     using System.Collections.Generic;
12     using System.Collections.Concurrent;
13     using System.Diagnostics;
14     using System.Globalization;
15     using System.Linq;
16     using System.Text;
17
18     /// <summary>
19     /// <para> Implements a cache of Symmetric Keys (once they are decrypted).Useful for rapidly decrypting multiple data values.</para>
20     /// </summary>
21     sealed internal class SqlSymmetricKeyCache {
22         private readonly ConcurrentDictionary<string,SqlClientSymmetricKey> _cache;
23         private static readonly SqlSymmetricKeyCache _singletonInstance = new SqlSymmetricKeyCache();
24
25
26         private SqlSymmetricKeyCache () {
27             _cache = new ConcurrentDictionary<string, SqlClientSymmetricKey>(concurrencyLevel: 4 * Environment.ProcessorCount /* default value in ConcurrentDictionary*/, capacity: 2);
28         }
29
30         internal static SqlSymmetricKeyCache GetInstance () {
31             return _singletonInstance;
32         }
33
34         /// <summary>
35         /// <para> Retrieves Symmetric Key (in plaintext) given the encryption material.</para>
36         /// </summary>
37         internal bool GetKey (SqlEncryptionKeyInfo keyInfo, string serverName, out SqlClientSymmetricKey encryptionKey) {
38             Debug.Assert(serverName != null, @"serverName should not be null.");
39
40             StringBuilder cacheLookupKeyBuilder = new StringBuilder(serverName, capacity: serverName.Length + SqlSecurityUtility.GetBase64LengthFromByteLength(keyInfo.encryptedKey.Length) + keyInfo.keyStoreName.Length + 2/*separators*/);
41
42 #if DEBUG
43             int capacity = cacheLookupKeyBuilder.Capacity;
44 #endif //DEBUG
45
46             cacheLookupKeyBuilder.Append(":");
47             cacheLookupKeyBuilder.Append(Convert.ToBase64String(keyInfo.encryptedKey));
48             cacheLookupKeyBuilder.Append(":");
49             cacheLookupKeyBuilder.Append(keyInfo.keyStoreName);
50
51             string cacheLookupKey = cacheLookupKeyBuilder.ToString();
52
53 #if DEBUG
54             Debug.Assert(cacheLookupKey.Length <= capacity, "We needed to allocate a larger array");
55 #endif //DEBUG
56
57             encryptionKey = null;
58
59             // Lookup the key in cache
60             if (!_cache.TryGetValue(cacheLookupKey, out encryptionKey)) {
61                 Debug.Assert(SqlConnection.ColumnEncryptionTrustedMasterKeyPaths != null, @"SqlConnection.ColumnEncryptionTrustedMasterKeyPaths should not be null");
62
63                 // Check against the trusted key paths
64                 //
65                 // Get the List corresponding to the connected server
66                 IList<string> trustedKeyPaths;
67                 if (SqlConnection.ColumnEncryptionTrustedMasterKeyPaths.TryGetValue(serverName, out trustedKeyPaths)) {
68                     // If the list is null or is empty or if the keyPath doesn't exist in the trusted key paths, then throw an exception.
69                     if ((trustedKeyPaths == null) || (trustedKeyPaths.Count() == 0) ||
70                         // (trustedKeyPaths.Where(s => s.Equals(keyInfo.keyPath, StringComparison.InvariantCultureIgnoreCase)).Count() == 0)) {
71                         (trustedKeyPaths.Any(s => s.Equals(keyInfo.keyPath, StringComparison.InvariantCultureIgnoreCase)) == false)) {
72                         // throw an exception since the key path is not in the trusted key paths list for this server
73                         throw SQL.UntrustedKeyPath(keyInfo.keyPath, serverName);
74                     }
75                 }
76
77                 // Key Not found, attempt to look up the provider and decrypt CEK
78                 SqlColumnEncryptionKeyStoreProvider provider;
79                 if (!SqlConnection.TryGetColumnEncryptionKeyStoreProvider(keyInfo.keyStoreName, out provider)) {
80                     throw SQL.UnrecognizedKeyStoreProviderName(keyInfo.keyStoreName, 
81                             SqlConnection.GetColumnEncryptionSystemKeyStoreProviders(),
82                             SqlConnection.GetColumnEncryptionCustomKeyStoreProviders());
83                 }
84
85                 // Decrypt the CEK
86                 // We will simply bubble up the exception from the DecryptColumnEncryptionKey function.
87                 byte[] plaintextKey;
88                 try {
89                     plaintextKey = provider.DecryptColumnEncryptionKey(keyInfo.keyPath, keyInfo.algorithmName, keyInfo.encryptedKey);
90                 }
91                 catch (Exception e) {
92                     // Generate a new exception and throw.
93                     string keyHex = SqlSecurityUtility.GetBytesAsString(keyInfo.encryptedKey, fLast:true, countOfBytes:10);
94                     throw SQL.KeyDecryptionFailed (keyInfo.keyStoreName, keyHex, e);
95                 }
96
97                 encryptionKey = new SqlClientSymmetricKey (plaintextKey);
98
99                 // In case multiple threads reach here at the same time, the first one wins.
100                 // The allocated memory will be reclaimed by Garbage Collector.
101                 _cache.TryAdd(cacheLookupKey, encryptionKey);
102             }
103
104             return true;
105         }
106     }
107 }