Skip to content

Perf | Reduce allocations in Always Encrypted key handling, reorganise#4256

Open
edwardneal wants to merge 24 commits into
dotnet:mainfrom
edwardneal:perf/ae-reorg/keys
Open

Perf | Reduce allocations in Always Encrypted key handling, reorganise#4256
edwardneal wants to merge 24 commits into
dotnet:mainfrom
edwardneal:perf/ae-reorg/keys

Conversation

@edwardneal
Copy link
Copy Markdown
Contributor

Description

This reduces the number of memory allocations performed by Always Encrypted functionality, focusing primarily upon the encryption keys.

It also introduces nullability annotations to the touched files, makes some minor documentation improvements, removes the redundant SqlClient or Sql prefixes of internal types and moves them into the AlwaysEncrypted namespace. These don't result in any public API changes or any changes to the calling contract for SqlColumnEncryptionKeyStoreProvider.

The PR's diff appears larger than it truly is; it may be easier to review commit-by-commit.

In sequence:

  • An encryption provider's ColumnEncryptionKeyCacheTtl was being set to a new TimeSpan instance rather than TimeSpan.Zero.
  • Parallel retrievals of encryption keys from their cache were serialized, even though the underlying cache was thread-safe (it's backed by a ConcurrentDictionary.) I've added a fast-path cache lookup before it takes the lock, which reduces contention slightly once the cache has warmed up.
    • The cache also calls SqlColumnEncryptionKeyStoreProvider.DecryptColumnEncryptionKey. We retain the lock in order to preserve the guarantee that this method call will always be serialized.
  • The encryption key used by SqlAeadAes256CbcHmac256Algorithm was being passed the constant algorithm name in the constructor. These two components are tightly coupled, so I removed the redundant parameter name. This had knock-on effects - it meant that the string.Format could be replaced with a constant string, and that the Unicode encoding of this could be cached.
  • Three SymmetricKey allocations were used to store the IV, MAC and cryptographic keys. These acted as a lightweight wrapper, providing no defensive copies or immutability guarantees. We now just store the byte arrays directly.
  • SqlSecurityUtility.GetHMACWithSHA256 used the netfx cryptographic methods, then allocated and copied. On netcore, we can use the oneshots and eliminate all allocations (and in some circumstances, the copy.)

The microbenchmark highlights the impact when instantiating a AeadAes256CbcHmac256EncryptionKey under cold cache conditions: a measurable reduction in GC pressure and a modest throughput improvement.

Method Mean Error StdDev Ratio RatioSD Completed Work Items Lock Contentions Gen0 Allocated Alloc Ratio
PR 1.299 μs 0.0161 μs 0.0151 μs 0.60 0.01 - - 0.0248 216 B 0.07
main 2.170 μs 0.0405 μs 0.0398 μs 1.00 0.03 - - 0.3738 3136 B 1.00

Security considerations

This makes changes which are very close to the core SqlAeadAes256CbcHmac256Algorithm implementation. I've deliberately scoped this PR to exclude any changes to this. While I think there are other performance improvements to be made in that area, they don't stand alone in the same way that these do and need their own discussion/review.

The CEK derivation logic remains unchanged, as does the key validation logic and key usage ordering.

Issues

None.

Testing

Existing automated tests continue to pass. This doesn't introduce any new functionality or make any behavioural changes, so I've not added any new tests.

@edwardneal edwardneal requested a review from a team as a code owner May 3, 2026 01:40
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board May 3, 2026
@apoorvdeshmukh
Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

❌ Patch coverage is 39.82301% with 68 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.30%. Comparing base (7a01dbe) to head (c1e0699).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
...ata/SqlClient/AlwaysEncrypted/SymmetricKeyCache.cs 0.00% 54 Missing ⚠️
...waysEncrypted/AeadAes256CbcHmac256EncryptionKey.cs 82.60% 4 Missing ⚠️
...nt/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs 0.00% 3 Missing ⚠️
...src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs 82.35% 3 Missing ⚠️
...ent/AlwaysEncrypted/AeadAes256CbcHmac256Factory.cs 50.00% 2 Missing ⚠️
.../AlwaysEncrypted/EncryptionAlgorithmFactoryList.cs 0.00% 1 Missing ⚠️
.../Microsoft/Data/SqlClient/SqlCommand.Encryption.cs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4256      +/-   ##
==========================================
- Coverage   66.02%   64.30%   -1.72%     
==========================================
  Files         277      272       -5     
  Lines       42988    65786   +22798     
==========================================
+ Hits        28382    42304   +13922     
- Misses      14606    23482    +8876     
Flag Coverage Δ
CI-SqlClient ?
PR-SqlClient-Project 64.30% <39.82%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cheenamalhotra cheenamalhotra added the Performance 📈 Issues that are targeted to performance improvements. label May 7, 2026
@cheenamalhotra cheenamalhotra added this to the 7.1.0-preview3 milestone May 7, 2026
@cheenamalhotra cheenamalhotra moved this from To triage to In review in SqlClient Board May 7, 2026
Copy link
Copy Markdown
Contributor

@apoorvdeshmukh apoorvdeshmukh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good to me overall.
@edwardneal Would it be possible to add the benchmarking code in the repo? The one with which you measured the performance.
Maybe under src/Microsoft.Data.SqlClient/tests/PerformanceTests?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors Always Encrypted internals to reduce allocations and contention during CEK handling, including reorganizing internal types into the Microsoft.Data.SqlClient.AlwaysEncrypted namespace and adding targeted nullability annotations.

Changes:

  • Replaces/relocates internal Always Encrypted primitives (SymmetricKey, EncryptionType, CEK cache) and updates call sites + tests accordingly.
  • Optimizes CEK caching behavior (fast-path cache lookup; uses TimeSpan.Zero for provider cache TTL).
  • Reduces allocations in key derivation/HMAC paths (span-based HMAC on .NET; avoids extra wrapper allocations for derived keys).

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/NativeAeadBaseline.cs Updates unit tests to new SymmetricKey/EncryptionType types.
src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/CertificateUtility.cs Updates reflection hooks to renamed/moved CEK cache type and singleton accessor.
src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/Utility.cs Updates reflection references for internal key types/constructors.
src/Microsoft.Data.SqlClient/tests/FunctionalTests/AlwaysEncryptedTests/ExceptionsAlgorithmErrors.cs Updates reflection references for internal key types/constructors.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlUtil.cs Switches InvalidEncryptionType helper to new EncryptionType enum.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs Adds span-based HMAC path on .NET; updates AE key decryption flow to new key/cache types.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs Updates encryption-type comparisons to new EncryptionType.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAeadAes256CbcHmac256Algorithm.cs Switches to new derived-key container and updated key properties.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs Updates enclave-related encryption/decryption to new key/encryption type types.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/SymmetricKeyCache.cs Introduces new CEK cache implementation in AlwaysEncrypted namespace (includes concurrency changes).
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/SymmetricKey.cs Renames/moves symmetric key wrapper to AlwaysEncrypted namespace.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptionType.cs Introduces new internal enum aligned to TDS encryption type values.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptionAlgorithmFactoryList.cs Updates algorithm factory dispatch to new key/encryption type types.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/EncryptionAlgorithmFactory.cs Updates factory interface to new key/encryption type types.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/AeadAes256CbcHmac256Factory.cs Updates AEAD factory to new key/encryption type types and derived-key construction.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/AeadAes256CbcHmac256EncryptionKey.cs Adds new derived-key container that caches salts/derived keys.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSymmetricKeyCache.cs Removes legacy CEK cache type from root namespace (replaced by SymmetricKeyCache).
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEncryptionType.cs Removes legacy enum (replaced by AlwaysEncrypted.EncryptionType).
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlAeadAes256CbcHmac256EncryptionKey.cs Removes legacy derived-key wrapper (replaced by AlwaysEncrypted.AeadAes256CbcHmac256EncryptionKey).
Comments suppressed due to low confidence (1)

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AlwaysEncrypted/SymmetricKey.cs:35

  • The XML doc says RootKey "Returns a copy of the plain text key", but the property returns the underlying array reference. Either update the documentation to reflect the actual behavior or return a defensive copy to match the doc (be mindful of the perf/security tradeoff).

Comment on lines +28 to +30
public static SymmetricKeyCache Instance =>
field ??= new();

Comment on lines +53 to +58
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);
Comment on lines 41 to 45
/// <param name="encryptionType">Encryption type. Expected values are either Deterministic or Randomized.</param>
/// <param name="encryptionAlgorithm">Cryptographic algorithm.</param>
/// <returns>An implementation of the AEAD_AES_256_CBC_HMAC_SHA256 cryptographic algorithm.</returns>
internal override SqlClientEncryptionAlgorithm Create(SqlClientSymmetricKey encryptionKey, SqlClientEncryptionType encryptionType, string encryptionAlgorithm)
internal override SqlClientEncryptionAlgorithm Create(SymmetricKey encryptionKey, EncryptionType encryptionType, string encryptionAlgorithm)
{
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Performance 📈 Issues that are targeted to performance improvements.

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

5 participants