-
Notifications
You must be signed in to change notification settings - Fork 330
Perf | Reduce allocations in Always Encrypted key handling, reorganise #4256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
edwardneal
wants to merge
28
commits into
dotnet:main
Choose a base branch
from
edwardneal:perf/ae-reorg/keys
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
e06ac16
Rename SqlSymmetricKeyCache, move to AlwaysEncrypted namespace.
edwardneal 1c62247
Enable nullability annotations.
edwardneal 2620b05
Align singleton to codebase convention.
edwardneal 6606c06
Eliminate construction of new TimeSpan(0).
edwardneal f5959d5
Reduce lock contention.
edwardneal 59bf3d9
Style cleanup.
edwardneal 521fcd9
Rename SqlSymmetricKeyCache to match new class name.
edwardneal 615f4c7
Rename and move SqlClientSymmetricKey
edwardneal 7ef36d0
Enable nullability annotations.
edwardneal 70d5e19
Member visibility cleanup.
edwardneal a2d5dc6
Rename and move SqlAeadAes256CbcHmac256EncryptionKey
edwardneal 98fd800
Seal AeadAes256CbcHmac256EncryptionKey
edwardneal 9672162
Remove redundant ctor parameter
edwardneal cbf1f40
Replace redundant allocation of SymmetricKeys with autoprops
edwardneal 91096d0
Constant cleanup
edwardneal 7763ddd
Remove redundant string formatting
edwardneal 0f3380f
Remove repeated string decoding of constants
edwardneal b83c751
Make capitalisation of IV and MAC consistent
edwardneal aae7806
Add links to documentation of constants
edwardneal cc72373
Enable nullability annotations
edwardneal ca4e596
Member visibility adjustment
edwardneal 37c879d
Remove unnecessary allocations from GetHMACWithSHA256
edwardneal c762035
Document SqlClientEncryptionType
edwardneal c1e0699
Rename and move SqlClientEncryptionType
edwardneal 45c5cd7
Preinitialize singleton instances
edwardneal 183ff75
Comment fixup
edwardneal d3bef8b
Add unit tests
edwardneal 8d909de
Merge branch 'main' into perf/ae-reorg/keys
edwardneal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
107 changes: 107 additions & 0 deletions
107
...lClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/AeadAes256CbcHmac256EncryptionKey.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System.Text; | ||
|
|
||
| #nullable enable | ||
|
|
||
| namespace Microsoft.Data.SqlClient.AlwaysEncrypted | ||
| { | ||
| /// <summary> | ||
| /// Encryption key class containing 4 keys. This class is used by SqlAeadAes256CbcHmac256Algorithm | ||
| /// 1) root key - Main key that is used to derive the keys used in the encryption algorithm | ||
| /// 2) encryption key - A derived key that is used to encrypt the plain text and generate cipher text | ||
| /// 3) mac_key - A derived key that is used to compute HMAC of the cipher text | ||
| /// 4) iv_key - A derived key that is used to generate a synthetic IV from plain text data. | ||
| /// </summary> | ||
| internal sealed class AeadAes256CbcHmac256EncryptionKey : SymmetricKey | ||
| { | ||
| /// <summary> | ||
| /// Key size in bits. | ||
| /// </summary> | ||
| public const int KeySizeInBits = 256; | ||
|
|
||
| /// <summary> | ||
| /// Key size in bytes. | ||
| /// </summary> | ||
| public const int KeySizeInBytes = KeySizeInBits / 8; | ||
|
|
||
| /// <summary> | ||
| /// <see cref="KeySizeInBits"/> as a string, for use in the salt formats below. | ||
| /// </summary> | ||
| private const string KeySizeInBitsString = "256"; | ||
|
|
||
| /// <summary> | ||
| /// Encryption Key Salt. This is used to derive the encryption key from the root key. | ||
| /// </summary> | ||
| /// <see href="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-cryptography?view=sql-server-ver17#step-2-computing-aes_256_cbc-ciphertext"/> | ||
| private const string EncryptionKeySaltString = $"Microsoft SQL Server cell encryption key with encryption algorithm:{SqlAeadAes256CbcHmac256Algorithm.AlgorithmName} and key length:{KeySizeInBitsString}"; | ||
|
|
||
| /// <summary> | ||
| /// MAC Key Salt. This is used to derive the MAC key from the root key. | ||
| /// </summary> | ||
| /// <see href="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-cryptography?view=sql-server-ver17#step-3-computing-mac"/> | ||
| private const string MacKeySaltString = $"Microsoft SQL Server cell MAC key with encryption algorithm:{SqlAeadAes256CbcHmac256Algorithm.AlgorithmName} and key length:{KeySizeInBitsString}"; | ||
|
|
||
| /// <summary> | ||
| /// IV Key Salt. This is used to derive the IV key from the root key. This is only used for Deterministic encryption. | ||
| /// </summary> | ||
| /// <see href="https://learn.microsoft.com/en-us/sql/relational-databases/security/encryption/always-encrypted-cryptography?view=sql-server-ver17#step-1-generating-the-initialization-vector-iv"/> | ||
| private const string IvKeySaltString = $"Microsoft SQL Server cell IV key with encryption algorithm:{SqlAeadAes256CbcHmac256Algorithm.AlgorithmName} and key length:{KeySizeInBitsString}"; | ||
|
|
||
| private static byte[] EncryptionKeySalt => | ||
| field ??= Encoding.Unicode.GetBytes(EncryptionKeySaltString); | ||
| private static byte[] MacKeySalt => | ||
| field ??= Encoding.Unicode.GetBytes(MacKeySaltString); | ||
| private static byte[] IvKeySalt => | ||
| field ??= Encoding.Unicode.GetBytes(IvKeySaltString); | ||
|
|
||
| /// <summary> | ||
| /// Derives all the required keys from the given root key | ||
| /// </summary> | ||
| /// <param name="rootKey">Root key used to derive all the required derived keys</param> | ||
| public AeadAes256CbcHmac256EncryptionKey(byte[] rootKey) : base(rootKey) | ||
| { | ||
| // Key validation | ||
| if (rootKey.Length != KeySizeInBytes) | ||
| { | ||
| throw SQL.InvalidKeySize(SqlAeadAes256CbcHmac256Algorithm.AlgorithmName, | ||
| rootKey.Length, | ||
| KeySizeInBytes); | ||
| } | ||
|
|
||
| // Derive keys from the root key | ||
| // | ||
| // Derive encryption key | ||
| byte[] buff1 = new byte[KeySizeInBytes]; | ||
| SqlSecurityUtility.GetHMACWithSHA256(EncryptionKeySalt, RootKey, buff1); | ||
| EncryptionKey = buff1; | ||
|
|
||
| // Derive MAC key | ||
| byte[] buff2 = new byte[KeySizeInBytes]; | ||
| SqlSecurityUtility.GetHMACWithSHA256(MacKeySalt, RootKey, buff2); | ||
| MacKey = buff2; | ||
|
|
||
| // Derive IV key | ||
| byte[] buff3 = new byte[KeySizeInBytes]; | ||
| SqlSecurityUtility.GetHMACWithSHA256(IvKeySalt, RootKey, buff3); | ||
| IvKey = buff3; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Encryption key should be used for encryption and decryption | ||
| /// </summary> | ||
| public byte[] EncryptionKey { get; } | ||
|
|
||
| /// <summary> | ||
| /// MAC key should be used to compute and validate HMAC | ||
| /// </summary> | ||
| public byte[] MacKey { get; } | ||
|
|
||
| /// <summary> | ||
| /// IV key should be used to compute synthetic IV from a given plain text | ||
| /// </summary> | ||
| public byte[] IvKey { get; } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptionType.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| namespace Microsoft.Data.SqlClient.AlwaysEncrypted | ||
| { | ||
| /// <summary> | ||
| /// Encryption types supported in TCE. Corresponds to EncryptionAlgoType in MS-TDS. | ||
| /// </summary> | ||
| /// <see href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tds/7091f6f6-b83d-4ed2-afeb-ba5013dfb18f"/> | ||
| internal enum EncryptionType | ||
| { | ||
| PlainText = 0x00, | ||
| Deterministic = 0x01, | ||
| Randomized = 0x02 | ||
| } | ||
| } |
31 changes: 11 additions & 20 deletions
31
...t/Data/SqlClient/SqlClientSymmetricKey.cs → ...SqlClient/AlwaysEncrypted/SymmetricKey.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,45 +1,36 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| namespace Microsoft.Data.SqlClient | ||
| #nullable enable | ||
|
|
||
| namespace Microsoft.Data.SqlClient.AlwaysEncrypted | ||
| { | ||
| /// <summary> | ||
| /// Base class containing raw key bytes for symmetric key algorithms. Some encryption algorithms can use the key directly while others derive sub keys from this. | ||
| /// If an algorithm needs to derive more keys, have a derived class from this and use it in the corresponding encryption algorithm. | ||
| /// </summary> | ||
| internal class SqlClientSymmetricKey | ||
| internal class SymmetricKey | ||
| { | ||
| /// <summary> | ||
| /// The underlying key material | ||
| /// </summary> | ||
| protected readonly byte[] _rootKey; | ||
|
|
||
| /// <summary> | ||
| /// Constructor that initializes the root key. | ||
| /// </summary> | ||
| /// <param name="rootKey">root key</param> | ||
| internal SqlClientSymmetricKey(byte[] rootKey) | ||
| /// <param name="rootKey">Root key</param> | ||
| public SymmetricKey(byte[]? rootKey) | ||
| { | ||
| // Key validation | ||
| if (rootKey == null || rootKey.Length == 0) | ||
| if (rootKey is null || rootKey.Length == 0) | ||
| { | ||
| throw SQL.NullColumnEncryptionKeySysErr(); | ||
| } | ||
|
|
||
| _rootKey = rootKey; | ||
| RootKey = rootKey; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns a copy of the plain text key | ||
| /// Returns the plain text key. | ||
| /// This is needed for actual encryption/decryption. | ||
| /// </summary> | ||
| internal virtual byte[] RootKey | ||
| { | ||
| get | ||
| { | ||
| return _rootKey; | ||
| } | ||
| } | ||
| public byte[] RootKey { get; } | ||
| } | ||
| } |
117 changes: 117 additions & 0 deletions
117
...icrosoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/SymmetricKeyCache.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Text; | ||
| using System.Threading; | ||
| using Microsoft.Extensions.Caching.Memory; | ||
|
|
||
| #nullable enable | ||
|
|
||
| namespace Microsoft.Data.SqlClient.AlwaysEncrypted | ||
| { | ||
| /// <summary> | ||
| /// Implements a cache of Symmetric Keys (once they are decrypted). Useful for rapidly decrypting multiple data values. | ||
| /// </summary> | ||
| internal sealed class SymmetricKeyCache | ||
| { | ||
| private static readonly SymmetricKeyCache s_singletonInstance = new(); | ||
| private static readonly SemaphoreSlim s_cacheLock = new(1, 1); | ||
|
|
||
| private readonly MemoryCache _cache; | ||
|
|
||
| private SymmetricKeyCache() | ||
| { | ||
| _cache = new MemoryCache(new MemoryCacheOptions()); | ||
| } | ||
|
|
||
| public static SymmetricKeyCache Instance => s_singletonInstance; | ||
|
|
||
| /// <summary> | ||
| /// Retrieves Symmetric Key (in plaintext) given the encryption material. | ||
| /// </summary> | ||
| public SymmetricKey GetKey(SqlEncryptionKeyInfo keyInfo, SqlConnection connection, SqlCommand? command) | ||
| { | ||
| string serverName = connection.DataSource; | ||
| Debug.Assert(serverName is not null, @"serverName should not be null."); | ||
| int capacity = serverName!.Length + SqlSecurityUtility.GetBase64LengthFromByteLength(keyInfo.encryptedKey.Length) + keyInfo.keyStoreName.Length + 2 /* separators */; | ||
| StringBuilder cacheLookupKeyBuilder = new(serverName, capacity); | ||
|
|
||
| cacheLookupKeyBuilder.Append(':'); | ||
| cacheLookupKeyBuilder.Append(Convert.ToBase64String(keyInfo.encryptedKey)); | ||
| cacheLookupKeyBuilder.Append(':'); | ||
| cacheLookupKeyBuilder.Append(keyInfo.keyStoreName); | ||
|
|
||
| string cacheLookupKey = cacheLookupKeyBuilder.ToString(); | ||
|
|
||
| Debug.Assert(cacheLookupKey.Length <= capacity, "We needed to allocate a larger array"); | ||
|
|
||
| // Lookup the key in cache | ||
| if (!(_cache.TryGetValue(cacheLookupKey, out SymmetricKey? encryptionKey)) | ||
| // A null cryptographic key is never added to the cache, but this null check satisfies the nullability warning. | ||
| || encryptionKey is null) | ||
| { | ||
| // Acquire the lock to ensure thread safety when modifying the cache, and to guarantee that only one thread calls | ||
| // DecryptColumnEncryptionKey on a user-provided SqlColumnEncryptionKeyStoreProvider at a time. | ||
| s_cacheLock.Wait(); | ||
|
|
||
| try | ||
| { | ||
| // Perform a second check to see if the key was added to the cache while waiting for the lock, to avoid redundant work. | ||
| if (!(_cache.TryGetValue(cacheLookupKey, out encryptionKey)) | ||
| || encryptionKey is null) | ||
| { | ||
| Debug.Assert(SqlConnection.ColumnEncryptionTrustedMasterKeyPaths is not null, @"SqlConnection.ColumnEncryptionTrustedMasterKeyPaths should not be null"); | ||
|
|
||
| SqlSecurityUtility.ThrowIfKeyPathIsNotTrustedForServer(serverName, keyInfo.keyPath); | ||
|
|
||
| // Key Not found, attempt to look up the provider and decrypt CEK | ||
| if (!SqlSecurityUtility.TryGetColumnEncryptionKeyStoreProvider(keyInfo.keyStoreName, out SqlColumnEncryptionKeyStoreProvider provider, connection, command)) | ||
| { | ||
| throw SQL.UnrecognizedKeyStoreProviderName(keyInfo.keyStoreName, | ||
| SqlConnection.GetColumnEncryptionSystemKeyStoreProvidersNames(), | ||
| SqlSecurityUtility.GetListOfProviderNamesThatWereSearched(connection, command)); | ||
| } | ||
|
|
||
| // Decrypt the CEK | ||
| // We will simply bubble up the exception from the DecryptColumnEncryptionKey function. | ||
| byte[] plaintextKey; | ||
| try | ||
| { | ||
| // AKV provider registration supports multi-user scenarios, so it is not safe to cache the CEK in the global provider. | ||
| // The CEK cache is a global cache, and is shared across all connections. | ||
| // To prevent conflicts between CEK caches, global providers should not use their own CEK caches | ||
| provider.ColumnEncryptionKeyCacheTtl = TimeSpan.Zero; | ||
| 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 SymmetricKey(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. | ||
| _cache.Set(cacheLookupKey, encryptionKey, absoluteExpirationRelativeToNow: SqlConnection.ColumnEncryptionKeyCacheTtl); | ||
| } | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| // Release the lock to allow other threads to access the cache | ||
| s_cacheLock.Release(); | ||
| } | ||
| } | ||
|
|
||
| return encryptionKey; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.