From 08f86b629e6901863b7eea319553bc166b3b2448 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 4 Sep 2025 15:52:17 -0500 Subject: [PATCH 01/26] Merge test behavior overrides --- .../Data/SqlClient/SqlCommand.netcore.cs | 27 ---------------- .../Data/SqlClient/SqlCommand.netfx.cs | 26 --------------- .../Microsoft/Data/SqlClient/SqlCommand.cs | 32 +++++++++++++++++-- 3 files changed, 30 insertions(+), 55 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 7a0491ad0f..8af9f7d5d5 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -32,33 +32,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// private bool _wasBatchModeColumnEncryptionSettingSetOnce; -#if DEBUG - /// - /// Force the client to sleep during sp_describe_parameter_encryption in the function TryFetchInputParameterEncryptionInfo. - /// - private static bool _sleepDuringTryFetchInputParameterEncryptionInfo = false; - - /// - /// Force the client to sleep during sp_describe_parameter_encryption in the function RunExecuteReaderTds. - /// - private static bool _sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption = false; - - /// - /// Force the client to sleep during sp_describe_parameter_encryption after ReadDescribeEncryptionParameterResults. - /// - private static bool _sleepAfterReadDescribeEncryptionParameterResults = false; - - /// - /// Internal flag for testing purposes that forces all queries to internally end async calls. - /// - private static bool _forceInternalEndQuery = false; - - /// - /// Internal flag for testing purposes that forces one RetryableEnclaveQueryExecutionException during GenerateEnclavePackage - /// - private static bool _forceRetryableEnclaveQueryExecutionExceptionDuringGenerateEnclavePackage = false; -#endif - private static readonly SqlDiagnosticListener s_diagnosticListener = new SqlDiagnosticListener(); private bool _parentOperationStarted = false; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 68033e95cd..c790823a97 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -33,32 +33,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// private bool _wasBatchModeColumnEncryptionSettingSetOnce; -#if DEBUG - /// - /// Force the client to sleep during sp_describe_parameter_encryption in the function TryFetchInputParameterEncryptionInfo. - /// - private static bool _sleepDuringTryFetchInputParameterEncryptionInfo = false; - - /// - /// Force the client to sleep during sp_describe_parameter_encryption in the function RunExecuteReaderTds. - /// - private static bool _sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption = false; - - /// - /// Force the client to sleep during sp_describe_parameter_encryption after ReadDescribeEncryptionParameterResults. - /// - private static bool _sleepAfterReadDescribeEncryptionParameterResults = false; - - /// - /// Internal flag for testing purposes that forces all queries to internally end async calls. - /// - private static bool _forceInternalEndQuery = false; - - /// - /// Internal flag for testing purposes that forces one RetryableEnclaveQueryExecutionException during GenerateEnclavePackage - /// - private static bool _forceRetryableEnclaveQueryExecutionExceptionDuringGenerateEnclavePackage = false; -#endif internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; private _SqlRPC[] _rpcArrayOf1 = null; // Used for RPC executes diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index 6fc39959cd..d61323ded0 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -43,7 +43,35 @@ public sealed partial class SqlCommand : DbCommand, ICloneable #endregion #region Fields - + #region Test-Only Behavior Overrides + #if DEBUG + /// + /// Force the client to sleep during sp_describe_parameter_encryption in the function TryFetchInputParameterEncryptionInfo. + /// + private static bool _sleepDuringTryFetchInputParameterEncryptionInfo = false; + + /// + /// Force the client to sleep during sp_describe_parameter_encryption in the function RunExecuteReaderTds. + /// + private static bool _sleepDuringRunExecuteReaderTdsForSpDescribeParameterEncryption = false; + + /// + /// Force the client to sleep during sp_describe_parameter_encryption after ReadDescribeEncryptionParameterResults. + /// + private static bool _sleepAfterReadDescribeEncryptionParameterResults = false; + + /// + /// Internal flag for testing purposes that forces all queries to internally end async calls. + /// + private static bool _forceInternalEndQuery = false; + + /// + /// Internal flag for testing purposes that forces one RetryableEnclaveQueryExecutionException during GenerateEnclavePackage + /// + private static bool _forceRetryableEnclaveQueryExecutionExceptionDuringGenerateEnclavePackage = false; + #endif + #endregion + // @TODO: Make property - non-private fields are bad internal SqlDependency _sqlDep; @@ -195,7 +223,7 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// DbDataAdapter. /// private UpdateRowSource _updatedRowSource = UpdateRowSource.Both; - + #endregion #region Constructors From 9063c0b4d99e584ff718398f31a382f618f6fd5a Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 4 Sep 2025 17:41:37 -0500 Subject: [PATCH 02/26] Create encryption partial - the reader partial is getting too big. --- .../src/Microsoft.Data.SqlClient.csproj | 3 +++ .../netfx/src/Microsoft.Data.SqlClient.csproj | 3 +++ .../Data/SqlClient/SqlCommand.Encryption.cs | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index f4e49c1ccb..dd20f91aa4 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -582,6 +582,9 @@ Microsoft\Data\SqlClient\SqlCommand.cs + + Microsoft\Data\SqlClient\SqlCommand.Encryption.cs + Microsoft\Data\SqlClient\SqlCommand.NonQuery.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index da0f8acad6..35fcad5b95 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -756,6 +756,9 @@ Microsoft\Data\SqlClient\SqlCommand.cs + + Microsoft\Data\SqlClient\SqlCommand.Encryption.cs + Microsoft\Data\SqlClient\SqlCommand.NonQuery.cs diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs new file mode 100644 index 0000000000..a735722713 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -0,0 +1,18 @@ +// 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.Collections.ObjectModel; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + public sealed partial class SqlCommand + { + + } +} From 85bda18857fead5de35330f781b180723a3398c2 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 4 Sep 2025 17:43:05 -0500 Subject: [PATCH 03/26] Merge GetParameterEncryptionDataReader from netcore, update netfx to use it. --- .../Data/SqlClient/SqlCommand.netcore.cs | 73 ----------------- .../Data/SqlClient/SqlCommand.netfx.cs | 70 ++-------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 82 +++++++++++++++++++ 3 files changed, 89 insertions(+), 136 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 8af9f7d5d5..dc2f0a419e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -1307,79 +1307,6 @@ private void PrepareForTransparentEncryption( } } - private SqlDataReader GetParameterEncryptionDataReader(out Task returnTask, Task fetchInputParameterEncryptionInfoTask, - SqlDataReader describeParameterEncryptionDataReader, - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool describeParameterEncryptionNeeded, bool isRetry) - { - returnTask = AsyncHelper.CreateContinuationTaskWithState(fetchInputParameterEncryptionInfoTask, this, - (object state) => - { - SqlCommand command = (SqlCommand)state; - bool processFinallyBlockAsync = true; - bool decrementAsyncCountInFinallyBlockAsync = true; - - try - { - // Check for any exceptions on network write, before reading. - command.CheckThrowSNIException(); - - // If it is async, then TryFetchInputParameterEncryptionInfo-> RunExecuteReaderTds would have incremented the async count. - // Decrement it when we are about to complete async execute reader. - SqlInternalConnectionTds internalConnectionTds = command._activeConnection.GetOpenTdsConnection(); - if (internalConnectionTds != null) - { - internalConnectionTds.DecrementAsyncCount(); - decrementAsyncCountInFinallyBlockAsync = false; - } - - // Complete executereader. - describeParameterEncryptionDataReader = command.CompleteAsyncExecuteReader(isInternal: false, forDescribeParameterEncryption: true); - Debug.Assert(command._stateObj == null, "non-null state object in PrepareForTransparentEncryption."); - - // Read the results of describe parameter encryption. - command.ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, isRetry); - -#if DEBUG - // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. - if (_sleepAfterReadDescribeEncryptionParameterResults) - { - Thread.Sleep(10000); - } -#endif - } - catch (Exception e) - { - processFinallyBlockAsync = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - command.PrepareTransparentEncryptionFinallyBlock(closeDataReader: processFinallyBlockAsync, - decrementAsyncCount: decrementAsyncCountInFinallyBlockAsync, - clearDataStructures: processFinallyBlockAsync, - wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, - describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); - } - }, - onFailure: static (Exception exception, object state) => - { - SqlCommand command = (SqlCommand)state; - if (command.CachedAsyncState != null) - { - command.CachedAsyncState.ResetAsyncState(); - } - - if (exception != null) - { - throw exception; - } - } - ); - - return describeParameterEncryptionDataReader; - } - private SqlDataReader GetParameterEncryptionDataReaderAsync(out Task returnTask, SqlDataReader describeParameterEncryptionDataReader, ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool describeParameterEncryptionNeeded, bool isRetry) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index c790823a97..f4984206d4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -1223,69 +1223,13 @@ private void PrepareForTransparentEncryption( // Mark that we should not process the finally block since we have async execution pending. // Note that this should be done outside the task's continuation delegate. processFinallyBlock = false; - returnTask = AsyncHelper.CreateContinuationTask(fetchInputParameterEncryptionInfoTask, () => - { - bool processFinallyBlockAsync = true; - bool decrementAsyncCountInFinallyBlockAsync = true; - - try - { - // Check for any exceptions on network write, before reading. - CheckThrowSNIException(); - - // If it is async, then TryFetchInputParameterEncryptionInfo-> RunExecuteReaderTds would have incremented the async count. - // Decrement it when we are about to complete async execute reader. - SqlInternalConnectionTds internalConnectionTds = _activeConnection.GetOpenTdsConnection(); - if (internalConnectionTds != null) - { - internalConnectionTds.DecrementAsyncCount(); - decrementAsyncCountInFinallyBlockAsync = false; - } - - // Complete executereader. - describeParameterEncryptionDataReader = CompleteAsyncExecuteReader(isInternal: false, forDescribeParameterEncryption: true); - Debug.Assert(_stateObj == null, "non-null state object in PrepareForTransparentEncryption."); - - // Read the results of describe parameter encryption. - ReadDescribeEncryptionParameterResults( - describeParameterEncryptionDataReader, - describeParameterEncryptionRpcOriginalRpcMap, - isRetry); - -#if DEBUG - // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. - if (_sleepAfterReadDescribeEncryptionParameterResults) - { - Thread.Sleep(10000); - } -#endif //DEBUG - } - catch (Exception e) - { - processFinallyBlockAsync = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - PrepareTransparentEncryptionFinallyBlock(closeDataReader: processFinallyBlockAsync, - decrementAsyncCount: decrementAsyncCountInFinallyBlockAsync, - clearDataStructures: processFinallyBlockAsync, - wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, - describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); - } - }, - onFailure: ((exception) => - { - if (CachedAsyncState != null) - { - CachedAsyncState.ResetAsyncState(); - } - if (exception != null) - { - throw exception; - } - })); + describeParameterEncryptionDataReader = GetParameterEncryptionDataReader( + out returnTask, + fetchInputParameterEncryptionInfoTask, + describeParameterEncryptionDataReader, + describeParameterEncryptionRpcOriginalRpcMap, + describeParameterEncryptionNeeded, + isRetry); decrementAsyncCountInFinallyBlock = false; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index a735722713..d242d85281 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -13,6 +13,88 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlCommand { + private SqlDataReader GetParameterEncryptionDataReader( + out Task returnTask, + Task fetchInputParameterEncryptionInfoTask, + SqlDataReader describeParameterEncryptionDataReader, + ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, + bool describeParameterEncryptionNeeded, + bool isRetry) + { + returnTask = AsyncHelper.CreateContinuationTaskWithState( + task: fetchInputParameterEncryptionInfoTask, + state: this, + onSuccess: state => + { + SqlCommand command = (SqlCommand)state; + bool processFinallyBlockAsync = true; + bool decrementAsyncCountInFinallyBlockAsync = true; + try + { + // Check for any exceptions on network write, before reading. + command.CheckThrowSNIException(); + + // If it is async, then TryFetchInputParameterEncryptionInfo -> + // RunExecuteReaderTds would have incremented the async count. Decrement it + // when we are about to complete async execute reader. + SqlInternalConnectionTds internalConnectionTds = + command._activeConnection.GetOpenTdsConnection(); + if (internalConnectionTds is not null) + { + internalConnectionTds.DecrementAsyncCount(); + decrementAsyncCountInFinallyBlockAsync = false; + } + + // Complete executereader. + // @TODO: If we can remove this reference, this could be a static lambda + describeParameterEncryptionDataReader = command.CompleteAsyncExecuteReader( + isInternal: false, + forDescribeParameterEncryption: true); + Debug.Assert(command._stateObj is null, "non-null state object in PrepareForTransparentEncryption."); + + // Read the results of describe parameter encryption. + command.ReadDescribeEncryptionParameterResults( + describeParameterEncryptionDataReader, + describeParameterEncryptionRpcOriginalRpcMap, + isRetry); + + #if DEBUG + // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. + if (_sleepAfterReadDescribeEncryptionParameterResults) + { + Thread.Sleep(TimeSpan.FromSeconds(10)); + } + #endif + } + catch (Exception e) + { + processFinallyBlockAsync = ADP.IsCatchableExceptionType(e); + throw; + } + finally + { + command.PrepareTransparentEncryptionFinallyBlock( + closeDataReader: processFinallyBlockAsync, + decrementAsyncCount: decrementAsyncCountInFinallyBlockAsync, + clearDataStructures: processFinallyBlockAsync, + wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, + describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, + describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); + } + }, + onFailure: static (exception, state) => + { + SqlCommand command = (SqlCommand)state; + command.CachedAsyncState?.ResetAsyncState(); + + if (exception is not null) + { + throw exception; + } + }); + + return describeParameterEncryptionDataReader; + } } } From 45e568b930f9fe28a6edd04db48897abdcd564c1 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 4 Sep 2025 17:45:01 -0500 Subject: [PATCH 04/26] Merge GetParameterEncryptionDataReaderAsync from netcore, update netfx to use it. --- .../Data/SqlClient/SqlCommand.netcore.cs | 56 --------------- .../Data/SqlClient/SqlCommand.netfx.cs | 55 ++------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 68 +++++++++++++++++++ 3 files changed, 74 insertions(+), 105 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index dc2f0a419e..a455099d47 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -1307,62 +1307,6 @@ private void PrepareForTransparentEncryption( } } - private SqlDataReader GetParameterEncryptionDataReaderAsync(out Task returnTask, - SqlDataReader describeParameterEncryptionDataReader, - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool describeParameterEncryptionNeeded, bool isRetry) - { - returnTask = Task.Run(() => - { - bool processFinallyBlockAsync = true; - bool decrementAsyncCountInFinallyBlockAsync = true; - - try - { - // Check for any exceptions on network write, before reading. - CheckThrowSNIException(); - - // If it is async, then TryFetchInputParameterEncryptionInfo-> RunExecuteReaderTds would have incremented the async count. - // Decrement it when we are about to complete async execute reader. - SqlInternalConnectionTds internalConnectionTds = _activeConnection.GetOpenTdsConnection(); - if (internalConnectionTds != null) - { - internalConnectionTds.DecrementAsyncCount(); - decrementAsyncCountInFinallyBlockAsync = false; - } - - // Complete executereader. - describeParameterEncryptionDataReader = CompleteAsyncExecuteReader(isInternal: false, forDescribeParameterEncryption: true); - Debug.Assert(_stateObj == null, "non-null state object in PrepareForTransparentEncryption."); - - // Read the results of describe parameter encryption. - ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, - describeParameterEncryptionRpcOriginalRpcMap, isRetry); -#if DEBUG - // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. - if (_sleepAfterReadDescribeEncryptionParameterResults) - { - Thread.Sleep(10000); - } -#endif - } - catch (Exception e) - { - processFinallyBlockAsync = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - PrepareTransparentEncryptionFinallyBlock(closeDataReader: processFinallyBlockAsync, - decrementAsyncCount: decrementAsyncCountInFinallyBlockAsync, - clearDataStructures: processFinallyBlockAsync, - wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, - describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); - } - }); - return describeParameterEncryptionDataReader; - } - /// /// Executes an RPC to fetch param encryption info from SQL Engine. If this method is not done writing /// the request to wire, it'll set the "task" parameter which can be used to create continuations. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index f4984206d4..9f16f92e23 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -1241,55 +1241,12 @@ private void PrepareForTransparentEncryption( // Mark that we should not process the finally block since we have async execution pending. // Note that this should be done outside the task's continuation delegate. processFinallyBlock = false; - returnTask = Task.Run(() => - { - bool processFinallyBlockAsync = true; - bool decrementAsyncCountInFinallyBlockAsync = true; - - try - { - - // Check for any exceptions on network write, before reading. - CheckThrowSNIException(); - - // If it is async, then TryFetchInputParameterEncryptionInfo-> RunExecuteReaderTds would have incremented the async count. - // Decrement it when we are about to complete async execute reader. - SqlInternalConnectionTds internalConnectionTds = _activeConnection.GetOpenTdsConnection(); - if (internalConnectionTds != null) - { - internalConnectionTds.DecrementAsyncCount(); - decrementAsyncCountInFinallyBlockAsync = false; - } - - // Complete executereader. - describeParameterEncryptionDataReader = CompleteAsyncExecuteReader(isInternal: false, forDescribeParameterEncryption: true); - Debug.Assert(_stateObj == null, "non-null state object in PrepareForTransparentEncryption."); - - // Read the results of describe parameter encryption. - ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, isRetry); -#if DEBUG - // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. - if (_sleepAfterReadDescribeEncryptionParameterResults) - { - Thread.Sleep(10000); - } -#endif - } - catch (Exception e) - { - processFinallyBlockAsync = ADP.IsCatchableExceptionType(e); - throw; - } - finally - { - PrepareTransparentEncryptionFinallyBlock(closeDataReader: processFinallyBlockAsync, - decrementAsyncCount: decrementAsyncCountInFinallyBlockAsync, - clearDataStructures: processFinallyBlockAsync, - wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, - describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); - } - }); + describeParameterEncryptionDataReader = GetParameterEncryptionDataReaderAsync( + out returnTask, + describeParameterEncryptionDataReader, + describeParameterEncryptionRpcOriginalRpcMap, + describeParameterEncryptionNeeded, + isRetry); decrementAsyncCountInFinallyBlock = false; } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index d242d85281..15929e377a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -13,6 +13,7 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlCommand { + // @TODO: Isn't this doing things asynchronously? We should just have a purely asynchronous and a purely synchronous pathway instead of this mix of check this check that and flags. private SqlDataReader GetParameterEncryptionDataReader( out Task returnTask, Task fetchInputParameterEncryptionInfoTask, @@ -96,5 +97,72 @@ private SqlDataReader GetParameterEncryptionDataReader( return describeParameterEncryptionDataReader; } + + private SqlDataReader GetParameterEncryptionDataReaderAsync( + out Task returnTask, + SqlDataReader describeParameterEncryptionDataReader, + ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, + bool describeParameterEncryptionNeeded, + bool isRetry) + { + returnTask = Task.Run(() => + { + bool processFinallyBlockAsync = true; + bool decrementAsyncCountInFinallyBlockAsync = true; + + try + { + // Check for any exception on network write before reading. + CheckThrowSNIException(); + + // If it is async, then TryFetchInputParameterEncryptionInfo -> + // RunExecuteReaderTds would have incremented the async count. Decrement it + // when we are about to complete async execute reader. + SqlInternalConnectionTds internalConnectionTds = _activeConnection.GetOpenTdsConnection(); + if (internalConnectionTds is not null) + { + internalConnectionTds.DecrementAsyncCount(); + decrementAsyncCountInFinallyBlockAsync = false; + } + + // Complete executereader. + describeParameterEncryptionDataReader = CompleteAsyncExecuteReader( + isInternal: false, + forDescribeParameterEncryption: true); + Debug.Assert(_stateObj is null, "non-null state object in PrepareForTransparentEncryption."); + + // Read the results of describe parameter encryption. + ReadDescribeEncryptionParameterResults( + describeParameterEncryptionDataReader, + describeParameterEncryptionRpcOriginalRpcMap, + isRetry); + + #if DEBUG + // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. + if (_sleepAfterReadDescribeEncryptionParameterResults) + { + Thread.Sleep(TimeSpan.FromSeconds(10)); + } + #endif + } + catch (Exception e) + { + processFinallyBlockAsync = ADP.IsCatchableExceptionType(e); + throw; + } + finally + { + PrepareTransparentEncryptionFinallyBlock( + closeDataReader: processFinallyBlockAsync, + decrementAsyncCount: decrementAsyncCountInFinallyBlockAsync, + clearDataStructures: processFinallyBlockAsync, + wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, + describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, + describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); + } + }); + + return describeParameterEncryptionDataReader; + } } } From cc242fb5e96a020f04f30c1fdebe259075a69ffb Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 5 Sep 2025 14:10:52 -0500 Subject: [PATCH 05/26] Factor out the second result set read for ReadDescribeEncryptionParameterResults --- .../Data/SqlClient/SqlCommand.netcore.cs | 61 +------------- .../Data/SqlClient/SqlCommand.netfx.cs | 61 +------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 83 +++++++++++++++++++ .../Data/SqlClient/SqlSecurityUtility.cs | 1 + 4 files changed, 90 insertions(+), 116 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index a455099d47..acec093409 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -1759,71 +1759,16 @@ private void ReadDescribeEncryptionParameterResults( Debug.Assert(rpc != null, "rpc should not be null here."); - int userParamCount = rpc.userParams?.Count ?? 0; + // Read the second result set containing the cipher metadata int receivedMetadataCount = 0; if (!enclaveMetadataExists || ds.NextResult()) { - // Iterate over the parameter names to read the encryption type info - while (ds.Read()) - { -#if DEBUG - rowsAffected++; -#endif - Debug.Assert(rpc != null, "Describe Parameter Encryption requested for non-tce spec proc"); - string parameterName = ds.GetString((int)DescribeParameterEncryptionResultSet2.ParameterName); - - // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. - // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). - for (int index = 0; index < userParamCount; index++) - { - SqlParameter sqlParameter = rpc.userParams[index]; - Debug.Assert(sqlParameter != null, "sqlParameter should not be null."); - - if (SqlParameter.ParameterNamesEqual(sqlParameter.ParameterName, parameterName, StringComparison.Ordinal)) - { - Debug.Assert(sqlParameter.CipherMetadata == null, "param.CipherMetadata should be null."); - sqlParameter.HasReceivedMetadata = true; - receivedMetadataCount += 1; - // Found the param, setup the encryption info. - byte columnEncryptionType = ds.GetByte((int)DescribeParameterEncryptionResultSet2.ColumnEncryptionType); - if ((byte)SqlClientEncryptionType.PlainText != columnEncryptionType) - { - byte cipherAlgorithmId = ds.GetByte((int)DescribeParameterEncryptionResultSet2.ColumnEncryptionAlgorithm); - int columnEncryptionKeyOrdinal = ds.GetInt32((int)DescribeParameterEncryptionResultSet2.ColumnEncryptionKeyOrdinal); - byte columnNormalizationRuleVersion = ds.GetByte((int)DescribeParameterEncryptionResultSet2.NormalizationRuleVersion); - - // Lookup the key, failing which throw an exception - if (!columnEncryptionKeyTable.TryGetValue(columnEncryptionKeyOrdinal, out cipherInfoEntry)) - { - throw SQL.InvalidEncryptionKeyOrdinalParameterMetadata(columnEncryptionKeyOrdinal, columnEncryptionKeyTable.Count); - } - - sqlParameter.CipherMetadata = new SqlCipherMetadata(sqlTceCipherInfoEntry: cipherInfoEntry, - ordinal: unchecked((ushort)-1), - cipherAlgorithmId: cipherAlgorithmId, - cipherAlgorithmName: null, - encryptionType: columnEncryptionType, - normalizationRuleVersion: columnNormalizationRuleVersion); - - // Decrypt the symmetric key.(This will also validate and throw if needed). - Debug.Assert(_activeConnection != null, @"_activeConnection should not be null"); - SqlSecurityUtility.DecryptSymmetricKey(sqlParameter.CipherMetadata, _activeConnection, this); - - // This is effective only for BatchRPCMode even though we set it for non-BatchRPCMode also, - // since for non-BatchRPCMode mode, paramoptions gets thrown away and reconstructed in BuildExecuteSql. - int options = (int)(rpc.userParamMap[index] >> 32); - options |= TdsEnums.RPC_PARAM_ENCRYPTED; - rpc.userParamMap[index] = ((((long)options) << 32) | (long)index); - } - - break; - } - } - } + receivedMetadataCount = ReadDescribeEncryptionParameterResults2(ds, rpc, columnEncryptionKeyTable); } // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). + int userParamCount = rpc.userParams?.Count ?? 0; if (receivedMetadataCount != userParamCount) { for (int index = 0; index < userParamCount; index++) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 9f16f92e23..81494da31e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -1751,71 +1751,16 @@ private void ReadDescribeEncryptionParameterResults( Debug.Assert(rpc != null, "rpc should not be null here."); - int userParamCount = rpc.userParams?.Count ?? 0; + // Read the second result set containing the cipher metadata int receivedMetadataCount = 0; if (!enclaveMetadataExists || ds.NextResult()) { - // Iterate over the parameter names to read the encryption type info - while (ds.Read()) - { -#if DEBUG - rowsAffected++; -#endif - Debug.Assert(rpc != null, "Describe Parameter Encryption requested for non-tce spec proc"); - string parameterName = ds.GetString((int)DescribeParameterEncryptionResultSet2.ParameterName); - - // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. - // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). - for (int index = 0; index < userParamCount; index++) - { - SqlParameter sqlParameter = rpc.userParams[index]; - Debug.Assert(sqlParameter != null, "sqlParameter should not be null."); - - if (SqlParameter.ParameterNamesEqual(sqlParameter.ParameterName, parameterName, StringComparison.Ordinal)) - { - Debug.Assert(sqlParameter.CipherMetadata == null, "param.CipherMetadata should be null."); - sqlParameter.HasReceivedMetadata = true; - receivedMetadataCount += 1; - // Found the param, setup the encryption info. - byte columnEncryptionType = ds.GetByte((int)DescribeParameterEncryptionResultSet2.ColumnEncryptionType); - if ((byte)SqlClientEncryptionType.PlainText != columnEncryptionType) - { - byte cipherAlgorithmId = ds.GetByte((int)DescribeParameterEncryptionResultSet2.ColumnEncryptionAlgorithm); - int columnEncryptionKeyOrdinal = ds.GetInt32((int)DescribeParameterEncryptionResultSet2.ColumnEncryptionKeyOrdinal); - byte columnNormalizationRuleVersion = ds.GetByte((int)DescribeParameterEncryptionResultSet2.NormalizationRuleVersion); - - // Lookup the key, failing which throw an exception - if (!columnEncryptionKeyTable.TryGetValue(columnEncryptionKeyOrdinal, out cipherInfoEntry)) - { - throw SQL.InvalidEncryptionKeyOrdinalParameterMetadata(columnEncryptionKeyOrdinal, columnEncryptionKeyTable.Count); - } - - sqlParameter.CipherMetadata = new SqlCipherMetadata(sqlTceCipherInfoEntry: cipherInfoEntry, - ordinal: unchecked((ushort)-1), - cipherAlgorithmId: cipherAlgorithmId, - cipherAlgorithmName: null, - encryptionType: columnEncryptionType, - normalizationRuleVersion: columnNormalizationRuleVersion); - - // Decrypt the symmetric key.(This will also validate and throw if needed). - Debug.Assert(_activeConnection != null, @"_activeConnection should not be null"); - SqlSecurityUtility.DecryptSymmetricKey(sqlParameter.CipherMetadata, _activeConnection, this); - - // This is effective only for BatchRPCMode even though we set it for non-BatchRPCMode also, - // since for non-BatchRPCMode mode, paramoptions gets thrown away and reconstructed in BuildExecuteSql. - int options = (int)(rpc.userParamMap[index] >> 32); - options |= TdsEnums.RPC_PARAM_ENCRYPTED; - rpc.userParamMap[index] = ((((long)options) << 32) | (long)index); - } - - break; - } - } - } + receivedMetadataCount = ReadDescribeEncryptionParameterResults2(ds, rpc, columnEncryptionKeyTable); } // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). + int userParamCount = rpc.userParams?.Count ?? 0; if (receivedMetadataCount != userParamCount) { for (int index = 0; index < userParamCount; index++) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 15929e377a..027685bb86 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Threading; @@ -164,5 +165,87 @@ private SqlDataReader GetParameterEncryptionDataReaderAsync( return describeParameterEncryptionDataReader; } + + private int ReadDescribeEncryptionParameterResults2( + SqlDataReader ds, + _SqlRPC rpc, + Dictionary columnEncryptionKeyTable) + { + Debug.Assert(rpc is not null, "Describe Parameter Encryption requested for non-TCE spec proc"); + + int receivedMetadataCount = 0; + int userParamCount = rpc.userParams?.Count ?? 0; // @TODO: Make this a property on _SqlRPC + + while (ds.Read()) + { + // @TODO: RowsAffected++; + + string parameterName = ds.GetString((int)DescribeParameterEncryptionResultSet2.ParameterName); + + // When the RPC object gets reused, the parameter array has more parameters than + // the valid params for the command. Null is used to indicate the end of the valid + // part of the array. Refer to GetRPCObject(). + for (int index = 0; index < userParamCount; index++) + { + SqlParameter sqlParameter = rpc.userParams[index]; + Debug.Assert(sqlParameter is not null, "sqlParameter should not be null."); + + // @TODO: And what happens if they're not in the same order? + if (SqlParameter.ParameterNamesEqual(sqlParameter.ParameterName, parameterName)) + { + Debug.Assert(sqlParameter.CipherMetadata is not null, "param.CipherMetaData should not be null."); + + sqlParameter.HasReceivedMetadata = true; + receivedMetadataCount++; + + // Found the param, set up the encryption info. + byte columnEncryptionType = ds.GetByte((int)DescribeParameterEncryptionResultSet2.ColumnEncryptionType); + if (columnEncryptionType != (byte)SqlClientEncryptionType.PlainText) + { + byte cipherAlgorithmId = ds.GetByte( + (int)DescribeParameterEncryptionResultSet2.ColumnEncryptionAlgorithm); + int columnEncryptionKeyOrdinal = ds.GetInt32( + (int)DescribeParameterEncryptionResultSet2.ColumnEncryptionKeyOrdinal); + byte columnNormalizationRuleVersion = ds.GetByte( + (int)DescribeParameterEncryptionResultSet2.NormalizationRuleVersion); + + // Lookup the key, failing which throw an exception + bool cipherInfoEntryFound = columnEncryptionKeyTable.TryGetValue( + columnEncryptionKeyOrdinal, + out SqlTceCipherInfoEntry cipherInfoEntry); + if (!cipherInfoEntryFound) + { + throw SQL.InvalidEncryptionKeyOrdinalParameterMetadata( + columnEncryptionKeyOrdinal, + columnEncryptionKeyTable.Count); + } + + sqlParameter.CipherMetadata = new SqlCipherMetadata( + sqlTceCipherInfoEntry: cipherInfoEntry, + ordinal: unchecked((ushort)-1), + cipherAlgorithmId: cipherAlgorithmId, + cipherAlgorithmName: null, + encryptionType: columnEncryptionType, + normalizationRuleVersion: columnNormalizationRuleVersion); + + // Decrypt the symmetric key. This will also validate and throw if needed. + SqlSecurityUtility.DecryptSymmetricKey(sqlParameter.CipherMetadata, _activeConnection, this); + + // This is effective only for _batchRPCMode even though we set it for + // non-_batchRPCMode also, since for non-_batchRPCMode, param options + // gets thrown away and reconstructed in BuildExecuteSql. + // @TODO: I bet we could make this a bit cleaner + int options = (int)(rpc.userParamMap[index] >> 32); + options |= TdsEnums.RPC_PARAM_ENCRYPTED; + rpc.userParamMap[index] = ((long)options << 32) | (long)index; + } + + break; + } + } + } + + return receivedMetadataCount; + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs index 90fb9a5d32..8fbe908379 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlSecurityUtility.cs @@ -262,6 +262,7 @@ internal static void DecryptSymmetricKey(SqlCipherMetadata md, SqlConnection con /// internal static void DecryptSymmetricKey(SqlTceCipherInfoEntry sqlTceCipherInfoEntry, out SqlClientSymmetricKey sqlClientSymmetricKey, out SqlEncryptionKeyInfo encryptionkeyInfoChosen, SqlConnection connection, SqlCommand command) { + Debug.Assert(connection is not null, "Connection should not be null."); Debug.Assert(sqlTceCipherInfoEntry is not null, "sqlTceCipherInfoEntry should not be null in DecryptSymmetricKey."); Debug.Assert(sqlTceCipherInfoEntry.ColumnEncryptionKeyValues is not null, "sqlTceCipherInfoEntry.ColumnEncryptionKeyValues should not be null in DecryptSymmetricKey."); From d1fbee076b43e017b8bae05bcb1b285702b0e989 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 5 Sep 2025 18:24:04 -0500 Subject: [PATCH 06/26] Factor out ReadDescribeEncryptionParameterResults1 from ReadDescribeEncryptionParameterResults --- .../Data/SqlClient/SqlCommand.netfx.cs | 110 +------------ .../Data/SqlClient/SqlCommand.Encryption.cs | 149 +++++++++++++++++- 2 files changed, 150 insertions(+), 109 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 81494da31e..e8b105c2a7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -1589,8 +1589,8 @@ private void ReadDescribeEncryptionParameterResults( bool isRetry) { _SqlRPC rpc = null; - int currentOrdinal = -1; - SqlTceCipherInfoEntry cipherInfoEntry; + + // @TODO: This should be SqlTceCipherInfoTable Dictionary columnEncryptionKeyTable = new Dictionary(); Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, @@ -1608,6 +1608,7 @@ private void ReadDescribeEncryptionParameterResults( // and the corresponding original rpc requests. bool lookupDictionaryResult; + // @TODO: If this is supposed to read the results of sp_describe_parameter_encryption there should only ever be 2/3 result sets. So no need to loop this. do { if (_batchRPCMode) @@ -1621,109 +1622,8 @@ private void ReadDescribeEncryptionParameterResults( } } - bool enclaveMetadataExists = true; - - // First read the column encryption key list - while (ds.Read()) - { - -#if DEBUG - rowsAffected++; -#endif - - // Column Encryption Key Ordinal. - currentOrdinal = ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyOrdinal); - Debug.Assert(currentOrdinal >= 0, "currentOrdinal cannot be negative."); - - // Try to see if there was already an entry for the current ordinal. - if (!columnEncryptionKeyTable.TryGetValue(currentOrdinal, out cipherInfoEntry)) - { - // If an entry for this ordinal was not found, create an entry in the columnEncryptionKeyTable for this ordinal. - cipherInfoEntry = new SqlTceCipherInfoEntry(currentOrdinal); - columnEncryptionKeyTable.Add(currentOrdinal, cipherInfoEntry); - } - - Debug.Assert(!cipherInfoEntry.Equals(default(SqlTceCipherInfoEntry)), "cipherInfoEntry should not be un-initialized."); - - // Read the CEK. - byte[] encryptedKey = null; - int encryptedKeyLength = (int)ds.GetBytes((int)DescribeParameterEncryptionResultSet1.EncryptedKey, 0, encryptedKey, 0, 0); - encryptedKey = new byte[encryptedKeyLength]; - ds.GetBytes((int)DescribeParameterEncryptionResultSet1.EncryptedKey, 0, encryptedKey, 0, encryptedKeyLength); - - // Read the metadata version of the key. - // It should always be 8 bytes. - byte[] keyMdVersion = new byte[8]; - ds.GetBytes((int)DescribeParameterEncryptionResultSet1.KeyMdVersion, 0, keyMdVersion, 0, keyMdVersion.Length); - - // Validate the provider name - string providerName = ds.GetString((int)DescribeParameterEncryptionResultSet1.ProviderName); - - string keyPath = ds.GetString((int)DescribeParameterEncryptionResultSet1.KeyPath); - cipherInfoEntry.Add(encryptedKey: encryptedKey, - databaseId: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.DbId), - cekId: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyId), - cekVersion: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyVersion), - cekMdVersion: keyMdVersion, - keyPath: keyPath, - keyStoreName: providerName, - algorithmName: ds.GetString((int)DescribeParameterEncryptionResultSet1.KeyEncryptionAlgorithm)); - - bool isRequestedByEnclave = false; - - // Servers supporting enclave computations should always - // return a boolean indicating whether the key is required by enclave or not. - if (this._activeConnection.Parser.TceVersionSupported >= TdsEnums.MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT) - { - isRequestedByEnclave = - ds.GetBoolean((int)DescribeParameterEncryptionResultSet1.IsRequestedByEnclave); - } - else - { - enclaveMetadataExists = false; - } - - if (isRequestedByEnclave) - { - if (string.IsNullOrWhiteSpace(this.Connection.EnclaveAttestationUrl) && Connection.AttestationProtocol != SqlConnectionAttestationProtocol.None) - { - throw SQL.NoAttestationUrlSpecifiedForEnclaveBasedQuerySpDescribe(this._activeConnection.Parser.EnclaveType); - } - - byte[] keySignature = null; - - if (!ds.IsDBNull((int)DescribeParameterEncryptionResultSet1.KeySignature)) - { - int keySignatureLength = (int)ds.GetBytes((int)DescribeParameterEncryptionResultSet1.KeySignature, 0, keySignature, 0, 0); - keySignature = new byte[keySignatureLength]; - ds.GetBytes((int)DescribeParameterEncryptionResultSet1.KeySignature, 0, keySignature, 0, keySignatureLength); - } - - SqlSecurityUtility.VerifyColumnMasterKeySignature(providerName, keyPath, isRequestedByEnclave, keySignature, _activeConnection, this); - - int requestedKey = currentOrdinal; - SqlTceCipherInfoEntry cipherInfo; - - // Lookup the key, failing which throw an exception - if (!columnEncryptionKeyTable.TryGetValue(requestedKey, out cipherInfo)) - { - throw SQL.InvalidEncryptionKeyOrdinalEnclaveMetadata(requestedKey, columnEncryptionKeyTable.Count); - } - - if (keysToBeSentToEnclave == null) - { - keysToBeSentToEnclave = new ConcurrentDictionary(); - keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); - } - else if (!keysToBeSentToEnclave.ContainsKey(currentOrdinal)) - { - keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); - } - - requiresEnclaveComputations = true; - } - } - + // 1) Read the first result set that contains the column encryption key list + bool enclaveMetadataExists = ReadDescribeEncryptionParameterResults1(ds, columnEncryptionKeyTable); if (!enclaveMetadataExists && !ds.NextResult()) { throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 027685bb86..765e39ca16 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.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.Diagnostics; @@ -166,6 +167,148 @@ private SqlDataReader GetParameterEncryptionDataReaderAsync( return describeParameterEncryptionDataReader; } + private bool ReadDescribeEncryptionParameterResults1( + SqlDataReader ds, + Dictionary columnEncryptionKeyTable) + { + bool enclaveMetadataExists = true; + while (ds.Read()) + { + // @TODO: RowsAffected++; + + // Column encryption key ordinal + int currentOrdinal = ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyOrdinal); + Debug.Assert(currentOrdinal >= 0, "currentOrdinal cannot be negative"); + + // See if there was already an entry for the current ordinal, and if not create one. + if (!columnEncryptionKeyTable.TryGetValue(currentOrdinal, out SqlTceCipherInfoEntry cipherInfoEntry)) + { + cipherInfoEntry = new SqlTceCipherInfoEntry(currentOrdinal); + columnEncryptionKeyTable.Add(currentOrdinal, cipherInfoEntry); + } + + Debug.Assert(cipherInfoEntry is not null, "cipherInfoEntry should not be un-initialized."); + + // Read the column encryption key + // @TODO: This pattern is used quite a bit - can we turn it into a helper or extension of SqlDataReader? + int encryptedKeyLength = (int)ds.GetBytes( + (int)DescribeParameterEncryptionResultSet1.EncryptedKey, + dataIndex: 0, + buffer: null, + bufferIndex: 0, + length: 0); + byte[] encryptedKey = new byte[encryptedKeyLength]; + ds.GetBytes( + (int)DescribeParameterEncryptionResultSet1.EncryptedKey, + dataIndex: 0, + buffer: encryptedKey, + bufferIndex: 0, + length: encryptedKeyLength); + + // Read the metadata version of the key. It should always be 8 bytes. + // @TODO: We have so many asserts on the structure of this data, should we have one here too?? + byte[] keyMdVersion = new byte[8]; + ds.GetBytes( + (int)DescribeParameterEncryptionResultSet1.KeyMdVersion, + dataIndex: 0, + buffer: keyMdVersion, + bufferIndex: 0, + length: keyMdVersion.Length); + + // Read the provider name (key store name) + string providerName = ds.GetString((int)DescribeParameterEncryptionResultSet1.ProviderName); + + // Read the key path + string keyPath = ds.GetString((int)DescribeParameterEncryptionResultSet1.KeyPath); + + cipherInfoEntry.Add( + encryptedKey: encryptedKey, + databaseId: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.DbId), + cekId: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyId), + cekVersion: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyVersion), + cekMdVersion: keyMdVersion, + keyPath: keyPath, + keyStoreName: providerName, + algorithmName: ds.GetString((int)DescribeParameterEncryptionResultSet1.KeyEncryptionAlgorithm)); + + // Servers supporting enclave computations should always return a boolean + // indicating whether the key is required by enclave or not. + // @TODO: Do we need to make this check for each row? I doubt it. + bool isRequestedByEnclave = false; + if (_activeConnection.Parser.TceVersionSupported >= TdsEnums.MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT) + { + isRequestedByEnclave = ds.GetBoolean((int)DescribeParameterEncryptionResultSet1.IsRequestedByEnclave); + } + else + { + enclaveMetadataExists = false; + } + + if (isRequestedByEnclave) + { + if (string.IsNullOrWhiteSpace(_activeConnection.EnclaveAttestationUrl) && + _activeConnection.AttestationProtocol != SqlConnectionAttestationProtocol.None) + { + throw SQL.NoAttestationUrlSpecifiedForEnclaveBasedQuerySpDescribe( + _activeConnection.Parser.EnclaveType); + } + + byte[] keySignature = null; + if (!ds.IsDBNull((int)DescribeParameterEncryptionResultSet1.KeySignature)) + { + int keySignatureLength = (int)ds.GetBytes( + (int)DescribeParameterEncryptionResultSet1.KeySignature, + dataIndex: 0, + buffer: null, + bufferIndex: 0, + length: 0); + keySignature = new byte[keySignatureLength]; + ds.GetBytes( + (int)DescribeParameterEncryptionResultSet1.KeySignature, + dataIndex: 0, + buffer: keySignature, + bufferIndex: 0, + length: keySignatureLength); + } + + SqlSecurityUtility.VerifyColumnMasterKeySignature( + providerName, + keyPath, + isEnclaveEnabled: true, + keySignature, + _activeConnection, + this); + + // Lookup the key, failing which throw an exception + // @TODO: Seriously, we *just* did this, why are we looking it up again?? + if (!columnEncryptionKeyTable.TryGetValue(currentOrdinal, out SqlTceCipherInfoEntry cipherInfo)) + { + throw SQL.InvalidEncryptionKeyOrdinalEnclaveMetadata( + currentOrdinal, + columnEncryptionKeyTable.Count); + } + + // @TODO: 1) storing this as Command state seems fishy + // @TODO: 2) despite being concurrent, the usage of ContainsKey -> TryAdd is a race condition + // @TODO: 3) we have SqlTceCipherInfoTable, we should use it - or make it usable. + // @TODO: 4) even if we're supposed to store it as state, is the intention to obliterate the list each time? If so, we should probably store it locally and replace the state obj at the end. + if (keysToBeSentToEnclave is null) + { + keysToBeSentToEnclave = new ConcurrentDictionary(); + keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); + } + else if (!keysToBeSentToEnclave.ContainsKey(currentOrdinal)) + { + keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); + } + + requiresEnclaveComputations = true; + } + } + + return enclaveMetadataExists; + } + private int ReadDescribeEncryptionParameterResults2( SqlDataReader ds, _SqlRPC rpc, @@ -191,6 +334,7 @@ private int ReadDescribeEncryptionParameterResults2( Debug.Assert(sqlParameter is not null, "sqlParameter should not be null."); // @TODO: And what happens if they're not in the same order? + // @TODO: Invert if statement based on answer to above TODO if (SqlParameter.ParameterNamesEqual(sqlParameter.ParameterName, parameterName)) { Debug.Assert(sqlParameter.CipherMetadata is not null, "param.CipherMetaData should not be null."); @@ -210,10 +354,7 @@ private int ReadDescribeEncryptionParameterResults2( (int)DescribeParameterEncryptionResultSet2.NormalizationRuleVersion); // Lookup the key, failing which throw an exception - bool cipherInfoEntryFound = columnEncryptionKeyTable.TryGetValue( - columnEncryptionKeyOrdinal, - out SqlTceCipherInfoEntry cipherInfoEntry); - if (!cipherInfoEntryFound) + if (!columnEncryptionKeyTable.TryGetValue(columnEncryptionKeyOrdinal, out SqlTceCipherInfoEntry cipherInfoEntry)) { throw SQL.InvalidEncryptionKeyOrdinalParameterMetadata( columnEncryptionKeyOrdinal, From 04ea706f91d3d5c3149233960410f309d95f9744 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 5 Sep 2025 18:24:36 -0500 Subject: [PATCH 07/26] Factor out ReadDescribeEncryptionParameterResults3 from ReadDescribeEncryptionParameterResults --- .../Data/SqlClient/SqlCommand.netfx.cs | 40 ++-------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index e8b105c2a7..2cd7842a8e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -1629,7 +1629,7 @@ private void ReadDescribeEncryptionParameterResults( throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); } - // Find the RPC command that generated this tce request + // 2) Find the RPC command that generated this TCE request if (_batchRPCMode) { Debug.Assert(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] != null, "_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] should not be null."); @@ -1651,7 +1651,7 @@ private void ReadDescribeEncryptionParameterResults( Debug.Assert(rpc != null, "rpc should not be null here."); - // Read the second result set containing the cipher metadata + // 3) Read the second result set containing the cipher metadata int receivedMetadataCount = 0; if (!enclaveMetadataExists || ds.NextResult()) { @@ -1682,7 +1682,7 @@ private void ReadDescribeEncryptionParameterResults( "number of rows received (if received) for describe parameter encryption should be equal to rows affected by describe parameter encryption."); #endif - + // 4) Read the third result set containing enclave attestation information if (ShouldUseEnclaveBasedWorkflow && (enclaveAttestationParameters != null) && requiresEnclaveComputations) { if (!ds.NextResult()) @@ -1690,39 +1690,7 @@ private void ReadDescribeEncryptionParameterResults( throw SQL.UnexpectedDescribeParamFormatAttestationInfo(this._activeConnection.Parser.EnclaveType); } - bool attestationInfoRead = false; - - while (ds.Read()) - { - if (attestationInfoRead) - { - throw SQL.MultipleRowsReturnedForAttestationInfo(); - } - - int attestationInfoLength = (int)ds.GetBytes((int)DescribeParameterEncryptionResultSet3.AttestationInfo, 0, null, 0, 0); - byte[] attestationInfo = new byte[attestationInfoLength]; - ds.GetBytes((int)DescribeParameterEncryptionResultSet3.AttestationInfo, 0, attestationInfo, 0, attestationInfoLength); - - SqlConnectionAttestationProtocol attestationProtocol = this._activeConnection.AttestationProtocol; - string enclaveType = this._activeConnection.Parser.EnclaveType; - - EnclaveDelegate.Instance.CreateEnclaveSession( - attestationProtocol, - enclaveType, - GetEnclaveSessionParameters(), - attestationInfo, - enclaveAttestationParameters, - customData, - customDataLength, - isRetry); - enclaveAttestationParameters = null; - attestationInfoRead = true; - } - - if (!attestationInfoRead) - { - throw SQL.AttestationInfoNotReturnedFromSqlServer(this._activeConnection.Parser.EnclaveType, this._activeConnection.EnclaveAttestationUrl); - } + ReadDescribeEncryptionParameterResults3(ds, isRetry); } // The server has responded with encryption related information for this rpc request. So clear the needsFetchParameterEncryptionMetadata flag. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 765e39ca16..505cf327db 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -388,5 +388,53 @@ private int ReadDescribeEncryptionParameterResults2( return receivedMetadataCount; } + + private void ReadDescribeEncryptionParameterResults3(SqlDataReader ds, bool isRetry) + { + bool attestationInfoRead = false; + while (ds.Read()) + { + if (attestationInfoRead) + { + throw SQL.MultipleRowsReturnedForAttestationInfo(); + } + + int attestationInfoLength = (int)ds.GetBytes( + (int)DescribeParameterEncryptionResultSet3.AttestationInfo, + dataIndex: 0, + buffer: null, + bufferIndex: 0, + length: 0); + byte[] attestationInfo = new byte[attestationInfoLength]; + ds.GetBytes( + (int)DescribeParameterEncryptionResultSet3.AttestationInfo, + dataIndex: 0, + buffer: attestationInfo, + bufferIndex: 0, + length: attestationInfoLength); + + SqlConnectionAttestationProtocol attestationProtocol = _activeConnection.AttestationProtocol; + string enclaveType = _activeConnection.Parser.EnclaveType; + + EnclaveDelegate.Instance.CreateEnclaveSession( + attestationProtocol, + enclaveType, + GetEnclaveSessionParameters(), + attestationInfo, + enclaveAttestationParameters, + customData, + customDataLength, + isRetry); + enclaveAttestationParameters = null; + attestationInfoRead = true; + } + + if (!attestationInfoRead) + { + throw SQL.AttestationInfoNotReturnedFromSqlServer( + _activeConnection.Parser.EnclaveType, + _activeConnection.EnclaveAttestationUrl); + } + } } } From 70ba99fae4ecb3ecb00d6bb8b7574fb0251f43fd Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 5 Sep 2025 18:32:31 -0500 Subject: [PATCH 08/26] Repeat factoring out in netcore --- .../Data/SqlClient/SqlCommand.netcore.cs | 150 ++---------------- 1 file changed, 9 insertions(+), 141 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index acec093409..3703aa14d8 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -1597,8 +1597,8 @@ private void ReadDescribeEncryptionParameterResults( bool isRetry) { _SqlRPC rpc = null; - int currentOrdinal = -1; - SqlTceCipherInfoEntry cipherInfoEntry; + + // @TODO: This should be SqlTceCipherInfoTable Dictionary columnEncryptionKeyTable = new Dictionary(); Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, @@ -1616,6 +1616,7 @@ private void ReadDescribeEncryptionParameterResults( // and the corresponding original rpc requests. bool lookupDictionaryResult; + // @TODO: If this is supposed to read the results of sp_describe_parameter_encryption there should only ever be 2/3 result sets. So no need to loop this. do { if (_batchRPCMode) @@ -1629,115 +1630,14 @@ private void ReadDescribeEncryptionParameterResults( } } - bool enclaveMetadataExists = true; - - // First read the column encryption key list - while (ds.Read()) - { - -#if DEBUG - rowsAffected++; -#endif - - // Column Encryption Key Ordinal. - currentOrdinal = ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyOrdinal); - Debug.Assert(currentOrdinal >= 0, "currentOrdinal cannot be negative."); - - // Try to see if there was already an entry for the current ordinal. - if (!columnEncryptionKeyTable.TryGetValue(currentOrdinal, out cipherInfoEntry)) - { - // If an entry for this ordinal was not found, create an entry in the columnEncryptionKeyTable for this ordinal. - cipherInfoEntry = new SqlTceCipherInfoEntry(currentOrdinal); - columnEncryptionKeyTable.Add(currentOrdinal, cipherInfoEntry); - } - - Debug.Assert(!cipherInfoEntry.Equals(default(SqlTceCipherInfoEntry)), "cipherInfoEntry should not be un-initialized."); - - // Read the CEK. - byte[] encryptedKey = null; - int encryptedKeyLength = (int)ds.GetBytes((int)DescribeParameterEncryptionResultSet1.EncryptedKey, 0, encryptedKey, 0, 0); - encryptedKey = new byte[encryptedKeyLength]; - ds.GetBytes((int)DescribeParameterEncryptionResultSet1.EncryptedKey, 0, encryptedKey, 0, encryptedKeyLength); - - // Read the metadata version of the key. - // It should always be 8 bytes. - byte[] keyMdVersion = new byte[8]; - ds.GetBytes((int)DescribeParameterEncryptionResultSet1.KeyMdVersion, 0, keyMdVersion, 0, keyMdVersion.Length); - - // Validate the provider name - string providerName = ds.GetString((int)DescribeParameterEncryptionResultSet1.ProviderName); - - string keyPath = ds.GetString((int)DescribeParameterEncryptionResultSet1.KeyPath); - cipherInfoEntry.Add(encryptedKey: encryptedKey, - databaseId: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.DbId), - cekId: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyId), - cekVersion: ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyVersion), - cekMdVersion: keyMdVersion, - keyPath: keyPath, - keyStoreName: providerName, - algorithmName: ds.GetString((int)DescribeParameterEncryptionResultSet1.KeyEncryptionAlgorithm)); - - bool isRequestedByEnclave = false; - - // Servers supporting enclave computations should always - // return a boolean indicating whether the key is required by enclave or not. - if (this._activeConnection.Parser.TceVersionSupported >= TdsEnums.MIN_TCE_VERSION_WITH_ENCLAVE_SUPPORT) - { - isRequestedByEnclave = - ds.GetBoolean((int)DescribeParameterEncryptionResultSet1.IsRequestedByEnclave); - } - else - { - enclaveMetadataExists = false; - } - - if (isRequestedByEnclave) - { - if (string.IsNullOrWhiteSpace(this.Connection.EnclaveAttestationUrl) && Connection.AttestationProtocol != SqlConnectionAttestationProtocol.None) - { - throw SQL.NoAttestationUrlSpecifiedForEnclaveBasedQuerySpDescribe(this._activeConnection.Parser.EnclaveType); - } - - byte[] keySignature = null; - - if (!ds.IsDBNull((int)DescribeParameterEncryptionResultSet1.KeySignature)) - { - int keySignatureLength = (int)ds.GetBytes((int)DescribeParameterEncryptionResultSet1.KeySignature, 0, keySignature, 0, 0); - keySignature = new byte[keySignatureLength]; - ds.GetBytes((int)DescribeParameterEncryptionResultSet1.KeySignature, 0, keySignature, 0, keySignatureLength); - } - - SqlSecurityUtility.VerifyColumnMasterKeySignature(providerName, keyPath, isRequestedByEnclave, keySignature, _activeConnection, this); - - int requestedKey = currentOrdinal; - SqlTceCipherInfoEntry cipherInfo; - - // Lookup the key, failing which throw an exception - if (!columnEncryptionKeyTable.TryGetValue(requestedKey, out cipherInfo)) - { - throw SQL.InvalidEncryptionKeyOrdinalEnclaveMetadata(requestedKey, columnEncryptionKeyTable.Count); - } - - if (keysToBeSentToEnclave == null) - { - keysToBeSentToEnclave = new ConcurrentDictionary(); - keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); - } - else if (!keysToBeSentToEnclave.ContainsKey(currentOrdinal)) - { - keysToBeSentToEnclave.TryAdd(currentOrdinal, cipherInfo); - } - - requiresEnclaveComputations = true; - } - } - + // 1) Read the first result set that contains the column encryption key list + bool enclaveMetadataExists = ReadDescribeEncryptionParameterResults1(ds, columnEncryptionKeyTable); if (!enclaveMetadataExists && !ds.NextResult()) { throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); } - // Find the RPC command that generated this tce request + // 2) Find the RPC command that generated this TCE request if (_batchRPCMode) { Debug.Assert(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] != null, "_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] should not be null."); @@ -1759,7 +1659,7 @@ private void ReadDescribeEncryptionParameterResults( Debug.Assert(rpc != null, "rpc should not be null here."); - // Read the second result set containing the cipher metadata + // 3) Read the second result set containing the cipher metadata int receivedMetadataCount = 0; if (!enclaveMetadataExists || ds.NextResult()) { @@ -1790,7 +1690,7 @@ private void ReadDescribeEncryptionParameterResults( "number of rows received (if received) for describe parameter encryption should be equal to rows affected by describe parameter encryption."); #endif - + // 4) Read the third result set containing enclave attestation information if (ShouldUseEnclaveBasedWorkflow && (enclaveAttestationParameters != null) && requiresEnclaveComputations) { if (!ds.NextResult()) @@ -1798,39 +1698,7 @@ private void ReadDescribeEncryptionParameterResults( throw SQL.UnexpectedDescribeParamFormatAttestationInfo(this._activeConnection.Parser.EnclaveType); } - bool attestationInfoRead = false; - - while (ds.Read()) - { - if (attestationInfoRead) - { - throw SQL.MultipleRowsReturnedForAttestationInfo(); - } - - int attestationInfoLength = (int)ds.GetBytes((int)DescribeParameterEncryptionResultSet3.AttestationInfo, 0, null, 0, 0); - byte[] attestationInfo = new byte[attestationInfoLength]; - ds.GetBytes((int)DescribeParameterEncryptionResultSet3.AttestationInfo, 0, attestationInfo, 0, attestationInfoLength); - - SqlConnectionAttestationProtocol attestationProtocol = this._activeConnection.AttestationProtocol; - string enclaveType = this._activeConnection.Parser.EnclaveType; - - EnclaveDelegate.Instance.CreateEnclaveSession( - attestationProtocol, - enclaveType, - GetEnclaveSessionParameters(), - attestationInfo, - enclaveAttestationParameters, - customData, - customDataLength, - isRetry); - enclaveAttestationParameters = null; - attestationInfoRead = true; - } - - if (!attestationInfoRead) - { - throw SQL.AttestationInfoNotReturnedFromSqlServer(this._activeConnection.Parser.EnclaveType, this._activeConnection.EnclaveAttestationUrl); - } + ReadDescribeEncryptionParameterResults3(ds, isRetry); } // The server has responded with encryption related information for this rpc request. So clear the needsFetchParameterEncryptionMetadata flag. From db8bd2a56c046aa71e87e702a98929fcec592d43 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 5 Sep 2025 18:35:24 -0500 Subject: [PATCH 09/26] Executive decision: removing debug-only row count - it's to make sure the server returns the right number of rows for the sp_describe_parameter_encryption, but since it's just a debug build check, it has no bearing on prod builds. --- .../src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs | 10 ---------- .../src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs | 10 ---------- .../Microsoft/Data/SqlClient/SqlCommand.Encryption.cs | 4 ---- 3 files changed, 24 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 3703aa14d8..8ae9c56d2f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -1607,11 +1607,6 @@ private void ReadDescribeEncryptionParameterResults( // Indicates the current result set we are reading, used in BatchRPCMode, where we can have more than 1 result set. int resultSetSequenceNumber = 0; -#if DEBUG - // Keep track of the number of rows in the result sets. - int rowsAffected = 0; -#endif - // A flag that used in BatchRPCMode, to assert the result of lookup in to the dictionary maintaining the map of describe parameter encryption requests // and the corresponding original rpc requests. bool lookupDictionaryResult; @@ -1685,11 +1680,6 @@ private void ReadDescribeEncryptionParameterResults( } } -#if DEBUG - Debug.Assert((rowsAffected == 0) || (rowsAffected == RowsAffectedByDescribeParameterEncryption), - "number of rows received (if received) for describe parameter encryption should be equal to rows affected by describe parameter encryption."); -#endif - // 4) Read the third result set containing enclave attestation information if (ShouldUseEnclaveBasedWorkflow && (enclaveAttestationParameters != null) && requiresEnclaveComputations) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 2cd7842a8e..308f352952 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -1599,11 +1599,6 @@ private void ReadDescribeEncryptionParameterResults( // Indicates the current result set we are reading, used in BatchRPCMode, where we can have more than 1 result set. int resultSetSequenceNumber = 0; -#if DEBUG - // Keep track of the number of rows in the result sets. - int rowsAffected = 0; -#endif - // A flag that used in BatchRPCMode, to assert the result of lookup in to the dictionary maintaining the map of describe parameter encryption requests // and the corresponding original rpc requests. bool lookupDictionaryResult; @@ -1677,11 +1672,6 @@ private void ReadDescribeEncryptionParameterResults( } } -#if DEBUG - Debug.Assert((rowsAffected == 0) || (rowsAffected == RowsAffectedByDescribeParameterEncryption), - "number of rows received (if received) for describe parameter encryption should be equal to rows affected by describe parameter encryption."); -#endif - // 4) Read the third result set containing enclave attestation information if (ShouldUseEnclaveBasedWorkflow && (enclaveAttestationParameters != null) && requiresEnclaveComputations) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 505cf327db..904de60e79 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -174,8 +174,6 @@ private bool ReadDescribeEncryptionParameterResults1( bool enclaveMetadataExists = true; while (ds.Read()) { - // @TODO: RowsAffected++; - // Column encryption key ordinal int currentOrdinal = ds.GetInt32((int)DescribeParameterEncryptionResultSet1.KeyOrdinal); Debug.Assert(currentOrdinal >= 0, "currentOrdinal cannot be negative"); @@ -321,8 +319,6 @@ private int ReadDescribeEncryptionParameterResults2( while (ds.Read()) { - // @TODO: RowsAffected++; - string parameterName = ds.GetString((int)DescribeParameterEncryptionResultSet2.ParameterName); // When the RPC object gets reused, the parameter array has more parameters than From 2f870c25d37944241853d45f6fe03e9c40088f5e Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Fri, 5 Sep 2025 18:41:34 -0500 Subject: [PATCH 10/26] Merge ReadDescribeEncryptionParameterResults --- .../Data/SqlClient/SqlCommand.netcore.cs | 129 ------------------ .../Data/SqlClient/SqlCommand.netfx.cs | 129 ------------------ .../Data/SqlClient/SqlCommand.Encryption.cs | 129 ++++++++++++++++++ 3 files changed, 129 insertions(+), 258 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 8ae9c56d2f..9a47608b5c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -1585,135 +1585,6 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques } } - /// - /// Read the output of sp_describe_parameter_encryption - /// - /// Resultset from calling to sp_describe_parameter_encryption - /// Readonly dictionary with the map of parameter encryption rpc requests with the corresponding original rpc requests. - /// Indicates if this is a retry from a failed call. - private void ReadDescribeEncryptionParameterResults( - SqlDataReader ds, - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, - bool isRetry) - { - _SqlRPC rpc = null; - - // @TODO: This should be SqlTceCipherInfoTable - Dictionary columnEncryptionKeyTable = new Dictionary(); - - Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, - "describeParameterEncryptionRpcOriginalRpcMap should be non-null if and only if it is _batchRPCMode."); - - // Indicates the current result set we are reading, used in BatchRPCMode, where we can have more than 1 result set. - int resultSetSequenceNumber = 0; - - // A flag that used in BatchRPCMode, to assert the result of lookup in to the dictionary maintaining the map of describe parameter encryption requests - // and the corresponding original rpc requests. - bool lookupDictionaryResult; - - // @TODO: If this is supposed to read the results of sp_describe_parameter_encryption there should only ever be 2/3 result sets. So no need to loop this. - do - { - if (_batchRPCMode) - { - // If we got more RPC results from the server than what was requested. - if (resultSetSequenceNumber >= _sqlRPCParameterEncryptionReqArray.Length) - { - Debug.Assert(false, "Server sent back more results than what was expected for describe parameter encryption requests in _batchRPCMode."); - // Ignore the rest of the results from the server, if for whatever reason it sends back more than what we expect. - break; - } - } - - // 1) Read the first result set that contains the column encryption key list - bool enclaveMetadataExists = ReadDescribeEncryptionParameterResults1(ds, columnEncryptionKeyTable); - if (!enclaveMetadataExists && !ds.NextResult()) - { - throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); - } - - // 2) Find the RPC command that generated this TCE request - if (_batchRPCMode) - { - Debug.Assert(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] != null, "_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] should not be null."); - - // Lookup in the dictionary to get the original rpc request corresponding to the describe parameter encryption request - // pointed to by _sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] - rpc = null; - lookupDictionaryResult = describeParameterEncryptionRpcOriginalRpcMap.TryGetValue(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber++], out rpc); - - Debug.Assert(lookupDictionaryResult, - "Describe Parameter Encryption RPC request key must be present in the dictionary describeParameterEncryptionRpcOriginalRpcMap"); - Debug.Assert(rpc != null, - "Describe Parameter Encryption RPC request's corresponding original rpc request must not be null in the dictionary describeParameterEncryptionRpcOriginalRpcMap"); - } - else - { - rpc = _rpcArrayOf1[0]; - } - - Debug.Assert(rpc != null, "rpc should not be null here."); - - // 3) Read the second result set containing the cipher metadata - int receivedMetadataCount = 0; - if (!enclaveMetadataExists || ds.NextResult()) - { - receivedMetadataCount = ReadDescribeEncryptionParameterResults2(ds, rpc, columnEncryptionKeyTable); - } - - // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. - // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). - int userParamCount = rpc.userParams?.Count ?? 0; - if (receivedMetadataCount != userParamCount) - { - for (int index = 0; index < userParamCount; index++) - { - SqlParameter sqlParameter = rpc.userParams[index]; - if (!sqlParameter.HasReceivedMetadata && sqlParameter.Direction != ParameterDirection.ReturnValue) - { - // Encryption MD wasn't sent by the server - we expect the metadata to be sent for all the parameters - // that were sent in the original sp_describe_parameter_encryption but not necessarily for return values, - // since there might be multiple return values but server will only send for one of them. - // For parameters that don't need encryption, the encryption type is set to plaintext. - throw SQL.ParamEncryptionMetadataMissing(sqlParameter.ParameterName, rpc.GetCommandTextOrRpcName()); - } - } - } - - // 4) Read the third result set containing enclave attestation information - if (ShouldUseEnclaveBasedWorkflow && (enclaveAttestationParameters != null) && requiresEnclaveComputations) - { - if (!ds.NextResult()) - { - throw SQL.UnexpectedDescribeParamFormatAttestationInfo(this._activeConnection.Parser.EnclaveType); - } - - ReadDescribeEncryptionParameterResults3(ds, isRetry); - } - - // The server has responded with encryption related information for this rpc request. So clear the needsFetchParameterEncryptionMetadata flag. - rpc.needsFetchParameterEncryptionMetadata = false; - } while (ds.NextResult()); - - // Verify that we received response for each rpc call needs tce - if (_batchRPCMode) - { - for (int i = 0; i < _RPCList.Count; i++) - { - if (_RPCList[i].needsFetchParameterEncryptionMetadata) - { - throw SQL.ProcEncryptionMetadataMissing(_RPCList[i].rpcName); - } - } - } - - // If we are not in Batch RPC mode, update the query cache with the encryption MD. - if (!_batchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) - { - SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: true); - } - } - private Task RegisterForConnectionCloseNotification(Task outerTask) { SqlConnection connection = _activeConnection; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 308f352952..fe9dd497aa 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -1577,135 +1577,6 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques } } - /// - /// Read the output of sp_describe_parameter_encryption - /// - /// Resultset from calling to sp_describe_parameter_encryption - /// Readonly dictionary with the map of parameter encryption rpc requests with the corresponding original rpc requests. - /// Indicates if this is a retry from a failed call. - private void ReadDescribeEncryptionParameterResults( - SqlDataReader ds, - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, - bool isRetry) - { - _SqlRPC rpc = null; - - // @TODO: This should be SqlTceCipherInfoTable - Dictionary columnEncryptionKeyTable = new Dictionary(); - - Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, - "describeParameterEncryptionRpcOriginalRpcMap should be non-null if and only if it is _batchRPCMode."); - - // Indicates the current result set we are reading, used in BatchRPCMode, where we can have more than 1 result set. - int resultSetSequenceNumber = 0; - - // A flag that used in BatchRPCMode, to assert the result of lookup in to the dictionary maintaining the map of describe parameter encryption requests - // and the corresponding original rpc requests. - bool lookupDictionaryResult; - - // @TODO: If this is supposed to read the results of sp_describe_parameter_encryption there should only ever be 2/3 result sets. So no need to loop this. - do - { - if (_batchRPCMode) - { - // If we got more RPC results from the server than what was requested. - if (resultSetSequenceNumber >= _sqlRPCParameterEncryptionReqArray.Length) - { - Debug.Assert(false, "Server sent back more results than what was expected for describe parameter encryption requests in _batchRPCMode."); - // Ignore the rest of the results from the server, if for whatever reason it sends back more than what we expect. - break; - } - } - - // 1) Read the first result set that contains the column encryption key list - bool enclaveMetadataExists = ReadDescribeEncryptionParameterResults1(ds, columnEncryptionKeyTable); - if (!enclaveMetadataExists && !ds.NextResult()) - { - throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); - } - - // 2) Find the RPC command that generated this TCE request - if (_batchRPCMode) - { - Debug.Assert(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] != null, "_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] should not be null."); - - // Lookup in the dictionary to get the original rpc request corresponding to the describe parameter encryption request - // pointed to by _sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] - rpc = null; - lookupDictionaryResult = describeParameterEncryptionRpcOriginalRpcMap.TryGetValue(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber++], out rpc); - - Debug.Assert(lookupDictionaryResult, - "Describe Parameter Encryption RPC request key must be present in the dictionary describeParameterEncryptionRpcOriginalRpcMap"); - Debug.Assert(rpc != null, - "Describe Parameter Encryption RPC request's corresponding original rpc request must not be null in the dictionary describeParameterEncryptionRpcOriginalRpcMap"); - } - else - { - rpc = _rpcArrayOf1[0]; - } - - Debug.Assert(rpc != null, "rpc should not be null here."); - - // 3) Read the second result set containing the cipher metadata - int receivedMetadataCount = 0; - if (!enclaveMetadataExists || ds.NextResult()) - { - receivedMetadataCount = ReadDescribeEncryptionParameterResults2(ds, rpc, columnEncryptionKeyTable); - } - - // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. - // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). - int userParamCount = rpc.userParams?.Count ?? 0; - if (receivedMetadataCount != userParamCount) - { - for (int index = 0; index < userParamCount; index++) - { - SqlParameter sqlParameter = rpc.userParams[index]; - if (!sqlParameter.HasReceivedMetadata && sqlParameter.Direction != ParameterDirection.ReturnValue) - { - // Encryption MD wasn't sent by the server - we expect the metadata to be sent for all the parameters - // that were sent in the original sp_describe_parameter_encryption but not necessarily for return values, - // since there might be multiple return values but server will only send for one of them. - // For parameters that don't need encryption, the encryption type is set to plaintext. - throw SQL.ParamEncryptionMetadataMissing(sqlParameter.ParameterName, rpc.GetCommandTextOrRpcName()); - } - } - } - - // 4) Read the third result set containing enclave attestation information - if (ShouldUseEnclaveBasedWorkflow && (enclaveAttestationParameters != null) && requiresEnclaveComputations) - { - if (!ds.NextResult()) - { - throw SQL.UnexpectedDescribeParamFormatAttestationInfo(this._activeConnection.Parser.EnclaveType); - } - - ReadDescribeEncryptionParameterResults3(ds, isRetry); - } - - // The server has responded with encryption related information for this rpc request. So clear the needsFetchParameterEncryptionMetadata flag. - rpc.needsFetchParameterEncryptionMetadata = false; - } while (ds.NextResult()); - - // Verify that we received response for each rpc call needs tce - if (_batchRPCMode) - { - for (int i = 0; i < _RPCList.Count; i++) - { - if (_RPCList[i].needsFetchParameterEncryptionMetadata) - { - throw SQL.ProcEncryptionMetadataMissing(_RPCList[i].rpcName); - } - } - } - - // If we are not in Batch RPC mode, update the query cache with the encryption MD. - if (!_batchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) - { - SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: true); - } - } - private Task RegisterForConnectionCloseNotification(Task outterTask) { SqlConnection connection = _activeConnection; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 904de60e79..635b58676b 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -6,6 +6,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Data; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -167,6 +168,134 @@ private SqlDataReader GetParameterEncryptionDataReaderAsync( return describeParameterEncryptionDataReader; } + /// + /// Read the output of sp_describe_parameter_encryption + /// + /// Resultset from calling to sp_describe_parameter_encryption + /// Readonly dictionary with the map of parameter encryption rpc requests with the corresponding original rpc requests. + /// Indicates if this is a retry from a failed call. + private void ReadDescribeEncryptionParameterResults( + SqlDataReader ds, // @TODO: Rename something more obvious + ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, + bool isRetry) + { + // @TODO: This should be SqlTceCipherInfoTable + Dictionary columnEncryptionKeyTable = new Dictionary(); + + Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, + "describeParameterEncryptionRpcOriginalRpcMap should be non-null if and only if it is _batchRPCMode."); + + // Indicates the current result set we are reading, used in BatchRPCMode, where we can have more than 1 result set. + int resultSetSequenceNumber = 0; + + // A flag that used in BatchRPCMode, to assert the result of lookup in to the dictionary maintaining the map of describe parameter encryption requests + // and the corresponding original rpc requests. + bool lookupDictionaryResult; + + // @TODO: If this is supposed to read the results of sp_describe_parameter_encryption there should only ever be 2/3 result sets. So no need to loop this. + do + { + if (_batchRPCMode) + { + // If we got more RPC results from the server than what was requested. + if (resultSetSequenceNumber >= _sqlRPCParameterEncryptionReqArray.Length) + { + Debug.Assert(false, "Server sent back more results than what was expected for describe parameter encryption requests in _batchRPCMode."); + // Ignore the rest of the results from the server, if for whatever reason it sends back more than what we expect. + break; + } + } + + // 1) Read the first result set that contains the column encryption key list + bool enclaveMetadataExists = ReadDescribeEncryptionParameterResults1(ds, columnEncryptionKeyTable); + if (!enclaveMetadataExists && !ds.NextResult()) + { + throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); + } + + // 2) Find the RPC command that generated this TCE request + _SqlRPC rpc; + if (_batchRPCMode) + { + Debug.Assert(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] != null, "_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] should not be null."); + + // Lookup in the dictionary to get the original rpc request corresponding to the describe parameter encryption request + // pointed to by _sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] + rpc = null; + lookupDictionaryResult = describeParameterEncryptionRpcOriginalRpcMap.TryGetValue(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber++], out rpc); + + Debug.Assert(lookupDictionaryResult, + "Describe Parameter Encryption RPC request key must be present in the dictionary describeParameterEncryptionRpcOriginalRpcMap"); + Debug.Assert(rpc != null, + "Describe Parameter Encryption RPC request's corresponding original rpc request must not be null in the dictionary describeParameterEncryptionRpcOriginalRpcMap"); + } + else + { + rpc = _rpcArrayOf1[0]; + } + + Debug.Assert(rpc != null, "rpc should not be null here."); + + // 3) Read the second result set containing the cipher metadata + int receivedMetadataCount = 0; + if (!enclaveMetadataExists || ds.NextResult()) + { + receivedMetadataCount = ReadDescribeEncryptionParameterResults2(ds, rpc, columnEncryptionKeyTable); + } + + // When the RPC object gets reused, the parameter array has more parameters that the valid params for the command. + // Null is used to indicate the end of the valid part of the array. Refer to GetRPCObject(). + int userParamCount = rpc.userParams?.Count ?? 0; + if (receivedMetadataCount != userParamCount) + { + for (int index = 0; index < userParamCount; index++) + { + SqlParameter sqlParameter = rpc.userParams[index]; + if (!sqlParameter.HasReceivedMetadata && sqlParameter.Direction != ParameterDirection.ReturnValue) + { + // Encryption MD wasn't sent by the server - we expect the metadata to be sent for all the parameters + // that were sent in the original sp_describe_parameter_encryption but not necessarily for return values, + // since there might be multiple return values but server will only send for one of them. + // For parameters that don't need encryption, the encryption type is set to plaintext. + throw SQL.ParamEncryptionMetadataMissing(sqlParameter.ParameterName, rpc.GetCommandTextOrRpcName()); + } + } + } + + // 4) Read the third result set containing enclave attestation information + if (ShouldUseEnclaveBasedWorkflow && (enclaveAttestationParameters != null) && requiresEnclaveComputations) + { + if (!ds.NextResult()) + { + throw SQL.UnexpectedDescribeParamFormatAttestationInfo(this._activeConnection.Parser.EnclaveType); + } + + ReadDescribeEncryptionParameterResults3(ds, isRetry); + } + + // The server has responded with encryption related information for this rpc request. So clear the needsFetchParameterEncryptionMetadata flag. + rpc.needsFetchParameterEncryptionMetadata = false; + } while (ds.NextResult()); + + // Verify that we received response for each rpc call needs tce + if (_batchRPCMode) + { + for (int i = 0; i < _RPCList.Count; i++) + { + if (_RPCList[i].needsFetchParameterEncryptionMetadata) + { + throw SQL.ProcEncryptionMetadataMissing(_RPCList[i].rpcName); + } + } + } + + // If we are not in Batch RPC mode, update the query cache with the encryption MD. + if (!_batchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) + { + SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: true); + } + } + private bool ReadDescribeEncryptionParameterResults1( SqlDataReader ds, Dictionary columnEncryptionKeyTable) From 31662a0dd128e9b01d18057e243c8664c6f61463 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 8 Sep 2025 11:50:42 -0500 Subject: [PATCH 11/26] Merge ShouldCacheEncryptinMetadata, keysToBeSentToEnclave and requiresEnclaveComputations --- .../Microsoft/Data/SqlClient/SqlCommand.netcore.cs | 12 ------------ .../Microsoft/Data/SqlClient/SqlCommand.netfx.cs | 12 ------------ .../src/Microsoft/Data/SqlClient/SqlCommand.cs | 13 +++++++++++++ 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 9a47608b5c..28013cab30 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -44,18 +44,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // cached metadata private _SqlMetaDataSet _cachedMetaData; - // @TODO: Make properties - 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; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index fe9dd497aa..e0f78b49ed 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -42,18 +42,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // cached metadata private _SqlMetaDataSet _cachedMetaData; - // @TODO: Make properties - 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; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index d61323ded0..d15378ef08 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/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.ComponentModel; using System.Data; @@ -75,6 +76,12 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // @TODO: Make property - non-private fields are bad internal SqlDependency _sqlDep; + // @TODO: Make property - non-private fields are bad (this should be read-only externally) + internal ConcurrentDictionary keysToBeSentToEnclave; + + // @TODO: Make property - non-private fields are bad (this can be read-only externally) + internal bool requiresEnclaveComputations = false; + // @TODO: Rename _batchRpcMode to follow pattern private bool _batchRPCMode; @@ -791,6 +798,12 @@ private bool IsDirty private bool IsSimpleTextQuery => CommandType is CommandType.Text && (_parameters is null || _parameters.Count == 0); + private bool ShouldCacheEncryptionMetadata + { + // @TODO: Should we check for null on _activeConnection? + get => !requiresEnclaveComputations || _activeConnection.Parser.AreEnclaveRetriesSupported; + } + #endregion #region Public/Internal Methods From ee8ce4c256e391eabbf5fc24687372060fc3579e Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 8 Sep 2025 12:20:58 -0500 Subject: [PATCH 12/26] Merge enclavePackage, enclaveAttestationParameters, customData, customDataLength --- .../Data/SqlClient/SqlCommand.netcore.cs | 5 ---- .../Data/SqlClient/SqlCommand.netfx.cs | 5 ---- .../Microsoft/Data/SqlClient/SqlCommand.cs | 29 ++++++++++++++----- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 28013cab30..dee47dd4d2 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -44,11 +44,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // cached metadata private _SqlMetaDataSet _cachedMetaData; - internal EnclavePackage enclavePackage = null; - private SqlEnclaveAttestationParameters enclaveAttestationParameters = null; - private byte[] customData = null; - private int customDataLength = 0; - // Last TaskCompletionSource for reconnect task - use for cancellation only private TaskCompletionSource _reconnectionCompletionSource = null; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index e0f78b49ed..9010af2c85 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -42,11 +42,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // cached metadata private _SqlMetaDataSet _cachedMetaData; - internal EnclavePackage enclavePackage = null; - private SqlEnclaveAttestationParameters enclaveAttestationParameters = null; - private byte[] customData = null; - private int customDataLength = 0; - // Last TaskCompletionSource for reconnect task - use for cancellation only private TaskCompletionSource _reconnectionCompletionSource = null; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index d15378ef08..f913ac560d 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -74,7 +74,8 @@ public sealed partial class SqlCommand : DbCommand, ICloneable #endregion // @TODO: Make property - non-private fields are bad - internal SqlDependency _sqlDep; + // @TODO: Rename to match naming convention _enclavePackage + internal EnclavePackage enclavePackage = null; // @TODO: Make property - non-private fields are bad (this should be read-only externally) internal ConcurrentDictionary keysToBeSentToEnclave; @@ -82,6 +83,9 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // @TODO: Make property - non-private fields are bad (this can be read-only externally) internal bool requiresEnclaveComputations = false; + // @TODO: Make property - non-private fields are bad + internal SqlDependency _sqlDep; + // @TODO: Rename _batchRpcMode to follow pattern private bool _batchRPCMode; @@ -120,6 +124,12 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// private CommandType _commandType; + // @TODO: Rename to indicate that this is for enclave stuff, I think... + private byte[] customData = null; + + // @TODO: Rename to indicate that this is for enclave stuff. Or just get rid of it and use the length of customData if possible. + private int customDataLength = 0; + /// /// By default, the cmd object is visible on the design surface (i.e. VS7 Server Tray) to /// limit the number of components that clutter the design surface, when the DataAdapter @@ -128,12 +138,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// // @TODO: Make auto-property private bool _designTimeInvisible; - - /// - /// Current state of preparation of the command. - /// By default, assume the user is not sharing a connection so the command has not been prepared. - /// - private EXECTYPE _execType = EXECTYPE.UNPREPARED; /// /// True if the user changes the command text or number of parameters after the command has @@ -141,7 +145,16 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// // @TODO: Consider renaming "_IsUserDirty" private bool _dirty = false; - + + /// + /// Current state of preparation of the command. + /// By default, assume the user is not sharing a connection so the command has not been prepared. + /// + private EXECTYPE _execType = EXECTYPE.UNPREPARED; + + // @TODO: Rename to match naming conventions _enclaveAttestationParameters + private SqlEnclaveAttestationParameters enclaveAttestationParameters = null; + /// /// On 8.0 and above the Prepared state cannot be left. Once a command is prepared it will /// always be prepared. A change in parameters, command text, etc (IsDirty) automatically From ee295ee9947161abd9247703c8048cbd2da1bd46 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 8 Sep 2025 12:48:37 -0500 Subject: [PATCH 13/26] Merge ShouldUseEnclaveBasedWorkflow, _customColumnEncryptionKeyStoreProviders, HasColumnEncryptionKeyStoreProviderRegistered --- .../Data/SqlClient/SqlCommand.netcore.cs | 12 ----------- .../Data/SqlClient/SqlCommand.netfx.cs | 12 ----------- .../Microsoft/Data/SqlClient/SqlCommand.cs | 21 ++++++++++++++++++- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index dee47dd4d2..3299a47e18 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -51,18 +51,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable internal static int DebugForceAsyncWriteDelay { get; set; } #endif - internal bool ShouldUseEnclaveBasedWorkflow => - (!string.IsNullOrWhiteSpace(_activeConnection.EnclaveAttestationUrl) || Connection.AttestationProtocol == SqlConnectionAttestationProtocol.None) && - IsColumnEncryptionEnabled; - - /// - /// Per-command custom providers. It can be provided by the user and can be set more than once. - /// - private IReadOnlyDictionary _customColumnEncryptionKeyStoreProviders; - - internal bool HasColumnEncryptionKeyStoreProvidersRegistered => - _customColumnEncryptionKeyStoreProviders is not null && _customColumnEncryptionKeyStoreProviders.Count > 0; - // Cached info for async executions private sealed class AsyncState { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 9010af2c85..09c44090c8 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -49,18 +49,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable internal static int DebugForceAsyncWriteDelay { get; set; } #endif - internal bool ShouldUseEnclaveBasedWorkflow => - (!string.IsNullOrWhiteSpace(_activeConnection.EnclaveAttestationUrl) || Connection.AttestationProtocol == SqlConnectionAttestationProtocol.None) && - IsColumnEncryptionEnabled; - - /// - /// Per-command custom providers. It can be provided by the user and can be set more than once. - /// - private IReadOnlyDictionary _customColumnEncryptionKeyStoreProviders; - - internal bool HasColumnEncryptionKeyStoreProvidersRegistered => - _customColumnEncryptionKeyStoreProviders is not null && _customColumnEncryptionKeyStoreProviders.Count > 0; - // Cached info for async executions private sealed class AsyncState { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index f913ac560d..8639372c1a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -124,6 +124,12 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// private CommandType _commandType; + /// + /// Per-command custom providers. It can be provided by the user and can be set more than + /// once. + /// + private IReadOnlyDictionary _customColumnEncryptionKeyStoreProviders; + // @TODO: Rename to indicate that this is for enclave stuff, I think... private byte[] customData = null; @@ -663,7 +669,12 @@ public override UpdateRowSource UpdatedRowSource #endregion #region Internal/Protected/Private Properties - + + internal bool HasColumnEncryptionKeyStoreProvidersRegistered + { + get => _customColumnEncryptionKeyStoreProviders?.Count > 0; + } + internal bool InPrepare => _inPrepare; internal int InternalRecordsAffected @@ -817,6 +828,14 @@ private bool ShouldCacheEncryptionMetadata get => !requiresEnclaveComputations || _activeConnection.Parser.AreEnclaveRetriesSupported; } + private bool ShouldUseEnclaveBasedWorkflow + { + // @TODO: I'm pretty sure the or'd condition is used in several places. We could factor that out. + get => (!string.IsNullOrWhiteSpace(_activeConnection.EnclaveAttestationUrl) || + _activeConnection.AttestationProtocol is SqlConnectionAttestationProtocol.None) && + IsColumnEncryptionEnabled; + } + #endregion #region Public/Internal Methods From cfd33537f1b8e96175042d2ef869965837f9e972 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 8 Sep 2025 13:02:01 -0500 Subject: [PATCH 14/26] Merge _sqlRPCParameterEncryptionRegArray, _currentlyExecutingDescribeParameterEncryptionRPC, IsDescribeParameterEncryptionRPCCurrentlyInProgress --- .../Data/SqlClient/SqlCommand.netcore.cs | 13 ------------- .../Data/SqlClient/SqlCommand.netfx.cs | 13 ------------- .../src/Microsoft/Data/SqlClient/SqlCommand.cs | 17 +++++++++++++++++ 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 3299a47e18..66a16d9c02 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -161,21 +161,8 @@ private AsyncState CachedAsyncState private int _rowsAffectedBySpDescribeParameterEncryption = -1; private List<_SqlRPC> _RPCList; - private _SqlRPC[] _sqlRPCParameterEncryptionReqArray; private int _currentlyExecutingBatch; - /// - /// This variable is used to keep track of which RPC batch's results are being read when reading the results of - /// describe parameter encryption RPC requests in BatchRPCMode. - /// - private int _currentlyExecutingDescribeParameterEncryptionRPC; - - /// - /// A flag to indicate if we have in-progress describe parameter encryption RPC requests. - /// Reset to false when completed. - /// - internal bool IsDescribeParameterEncryptionRPCCurrentlyInProgress { get; private set; } - /// /// A flag to indicate if EndExecute was already initiated by the Begin call. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 09c44090c8..3b11a33163 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -159,21 +159,8 @@ private AsyncState CachedAsyncState private int _rowsAffectedBySpDescribeParameterEncryption = -1; private List<_SqlRPC> _RPCList; - private _SqlRPC[] _sqlRPCParameterEncryptionReqArray; private int _currentlyExecutingBatch; - /// - /// This variable is used to keep track of which RPC batch's results are being read when reading the results of - /// describe parameter encryption RPC requests in BatchRPCMode. - /// - private int _currentlyExecutingDescribeParameterEncryptionRPC; - - /// - /// A flag to indicate if we have in-progress describe parameter encryption RPC requests. - /// Reset to false when completed. - /// - internal bool IsDescribeParameterEncryptionRPCCurrentlyInProgress { get; private set; } - /// /// A flag to indicate if EndExecute was already initiated by the Begin call. /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index 8639372c1a..c37c122203 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -124,6 +124,13 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// private CommandType _commandType; + /// + /// This variable is used to keep track of which RPC batch's results are being read when reading the results of + /// describe parameter encryption RPC requests in BatchRPCMode. + /// + // @TODO: Rename to match naming conventions + private int _currentlyExecutingDescribeParameterEncryptionRPC; + /// /// Per-command custom providers. It can be provided by the user and can be set more than /// once. @@ -228,6 +235,9 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // @TODO: Use int? and replace -1 usage with null private int _rowsAffected = -1; + // @TODO: Rename to match naming convention + private _SqlRPC[] _sqlRPCParameterEncryptionReqArray; + /// /// TDS session the current instance is using. /// @@ -693,6 +703,13 @@ internal int InternalRecordsAffected } } + /// + /// A flag to indicate if we have in-progress describe parameter encryption RPC requests. + /// Reset to false when completed. + /// + // @TODO: Rename to match naming conventions + internal bool IsDescribeParameterEncryptionRPCCurrentlyInProgress { get; private set; } + // @TODO: Rename to match conventions. internal int ObjectID { get; } = Interlocked.Increment(ref _objectTypeCount); From cd5ce3fe944e1ca80930db32a50789f7ec369543 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 8 Sep 2025 13:18:25 -0500 Subject: [PATCH 15/26] Merge InvalidateEnclaveession, GetEnclaveSessionParameters --- .../Data/SqlClient/SqlCommand.netcore.cs | 20 ------------------- .../Data/SqlClient/SqlCommand.netfx.cs | 20 ------------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 19 ++++++++++++++++++ 3 files changed, 19 insertions(+), 40 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 66a16d9c02..e4cf63bc18 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -473,26 +473,6 @@ long firstAttemptStart }, TaskScheduler.Default); } - private void InvalidateEnclaveSession() - { - if (ShouldUseEnclaveBasedWorkflow && this.enclavePackage != null) - { - EnclaveDelegate.Instance.InvalidateEnclaveSession( - this._activeConnection.AttestationProtocol, - this._activeConnection.Parser.EnclaveType, - GetEnclaveSessionParameters(), - this.enclavePackage.EnclaveSession); - } - } - - private EnclaveSessionParameters GetEnclaveSessionParameters() - { - return new EnclaveSessionParameters( - this._activeConnection.DataSource, - this._activeConnection.EnclaveAttestationUrl, - this._activeConnection.Database); - } - private void ValidateCustomProviders(IDictionary customProviders) { // Throw when the provided dictionary is null. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 3b11a33163..aa5217a8e4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -442,26 +442,6 @@ private bool TriggerInternalEndAndRetryIfNecessary( } } - private void InvalidateEnclaveSession() - { - if (ShouldUseEnclaveBasedWorkflow && this.enclavePackage != null) - { - EnclaveDelegate.Instance.InvalidateEnclaveSession( - this._activeConnection.AttestationProtocol, - this._activeConnection.Parser.EnclaveType, - GetEnclaveSessionParameters(), - this.enclavePackage.EnclaveSession); - } - } - - private EnclaveSessionParameters GetEnclaveSessionParameters() - { - return new EnclaveSessionParameters( - this._activeConnection.DataSource, - this._activeConnection.EnclaveAttestationUrl, - this._activeConnection.Database); - } - private void ValidateCustomProviders(IDictionary customProviders) { // Throw when the provided dictionary is null. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 635b58676b..824b8588b7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -16,6 +16,12 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlCommand { + private EnclaveSessionParameters GetEnclaveSessionParameters() => + new EnclaveSessionParameters( + _activeConnection.DataSource, + _activeConnection.EnclaveAttestationUrl, + _activeConnection.Database); + // @TODO: Isn't this doing things asynchronously? We should just have a purely asynchronous and a purely synchronous pathway instead of this mix of check this check that and flags. private SqlDataReader GetParameterEncryptionDataReader( out Task returnTask, @@ -168,6 +174,19 @@ private SqlDataReader GetParameterEncryptionDataReaderAsync( return describeParameterEncryptionDataReader; } + private void InvalidateEnclaveSession() + { + if (ShouldUseEnclaveBasedWorkflow && enclavePackage != null) + { + EnclaveDelegate.Instance.InvalidateEnclaveSession( + _activeConnection.AttestationProtocol, + _activeConnection.Parser.EnclaveType, + GetEnclaveSessionParameters(), + enclavePackage.EnclaveSession); + } + } + + /// /// Read the output of sp_describe_parameter_encryption /// From f4046b596854b81090d055a6ba40e2d6d840281f Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 8 Sep 2025 13:26:27 -0500 Subject: [PATCH 16/26] Merge ValidateCustomProviders --- .../Data/SqlClient/SqlCommand.netcore.cs | 33 ------------------ .../Data/SqlClient/SqlCommand.netfx.cs | 33 ------------------ .../Data/SqlClient/SqlCommand.Encryption.cs | 34 ++++++++++++++++++- 3 files changed, 33 insertions(+), 67 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index e4cf63bc18..05c69cdb1a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -473,39 +473,6 @@ long firstAttemptStart }, TaskScheduler.Default); } - private void ValidateCustomProviders(IDictionary customProviders) - { - // Throw when the provided dictionary is null. - if (customProviders is null) - { - throw SQL.NullCustomKeyStoreProviderDictionary(); - } - - // Validate that custom provider list doesn't contain any of system provider list - foreach (string key in customProviders.Keys) - { - // Validate the provider name - // - // Check for null or empty - if (string.IsNullOrWhiteSpace(key)) - { - throw SQL.EmptyProviderName(); - } - - // Check if the name starts with MSSQL_, since this is reserved namespace for system providers. - if (key.StartsWith(ADP.ColumnEncryptionSystemProviderNamePrefix, StringComparison.InvariantCultureIgnoreCase)) - { - throw SQL.InvalidCustomKeyStoreProviderName(key, ADP.ColumnEncryptionSystemProviderNamePrefix); - } - - // Validate the provider value - if (customProviders[key] is null) - { - throw SQL.NullProviderValue(key); - } - } - } - /// /// This function walks through the registered custom column encryption key store providers and returns an object if found. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index aa5217a8e4..52be00e8ca 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -442,39 +442,6 @@ private bool TriggerInternalEndAndRetryIfNecessary( } } - private void ValidateCustomProviders(IDictionary customProviders) - { - // Throw when the provided dictionary is null. - if (customProviders is null) - { - throw SQL.NullCustomKeyStoreProviderDictionary(); - } - - // Validate that custom provider list doesn't contain any of system provider list - foreach (string key in customProviders.Keys) - { - // Validate the provider name - // - // Check for null or empty - if (string.IsNullOrWhiteSpace(key)) - { - throw SQL.EmptyProviderName(); - } - - // Check if the name starts with MSSQL_, since this is reserved namespace for system providers. - if (key.StartsWith(ADP.ColumnEncryptionSystemProviderNamePrefix, StringComparison.InvariantCultureIgnoreCase)) - { - throw SQL.InvalidCustomKeyStoreProviderName(key, ADP.ColumnEncryptionSystemProviderNamePrefix); - } - - // Validate the provider value - if (customProviders[key] is null) - { - throw SQL.NullProviderValue(key); - } - } - } - /// /// This function walks through the registered custom column encryption key store providers and returns an object if found. /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 824b8588b7..f6b55ce655 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -16,6 +16,39 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlCommand { + private static void ValidateCustomProviders(IDictionary customProviders) + { + // Throw when the provided dictionary is null. + if (customProviders is null) + { + throw SQL.NullCustomKeyStoreProviderDictionary(); + } + + // Validate that custom provider list doesn't contain any of system provider list + foreach (string key in customProviders.Keys) + { + // Validate the provider name + // + // Check for null or empty + if (string.IsNullOrWhiteSpace(key)) + { + throw SQL.EmptyProviderName(); + } + + // Check if the name starts with MSSQL_, since this is reserved namespace for system providers. + if (key.StartsWith(ADP.ColumnEncryptionSystemProviderNamePrefix, StringComparison.InvariantCultureIgnoreCase)) + { + throw SQL.InvalidCustomKeyStoreProviderName(key, ADP.ColumnEncryptionSystemProviderNamePrefix); + } + + // Validate the provider value + if (customProviders[key] is null) + { + throw SQL.NullProviderValue(key); + } + } + } + private EnclaveSessionParameters GetEnclaveSessionParameters() => new EnclaveSessionParameters( _activeConnection.DataSource, @@ -186,7 +219,6 @@ private void InvalidateEnclaveSession() } } - /// /// Read the output of sp_describe_parameter_encryption /// From e0656c3a0e586321795ffd67bbae92da1c295ebd Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 8 Sep 2025 13:30:44 -0500 Subject: [PATCH 17/26] Merge ResetEncryptionState() --- .../Data/SqlClient/SqlCommand.netcore.cs | 34 ------------------- .../Data/SqlClient/SqlCommand.netfx.cs | 34 ------------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 34 +++++++++++++++++++ 3 files changed, 34 insertions(+), 68 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 05c69cdb1a..a7c8fbd669 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -955,40 +955,6 @@ private void CheckNotificationStateAndAutoEnlist() } } - /// - /// Resets the encryption related state of the command object and each of the parameters. - /// BatchRPC doesn't need special handling to cleanup the state of each RPC object and its parameters since a new RPC object and - /// parameters are generated on every execution. - /// - private void ResetEncryptionState() - { - // First reset the command level state. - ClearDescribeParameterEncryptionRequests(); - - // Reset the state for internal End execution. - _internalEndExecuteInitiated = false; - - // Reset the state for the cache. - CachingQueryMetadataPostponed = false; - - // Reset the state of each of the parameters. - if (_parameters != null) - { - for (int i = 0; i < _parameters.Count; i++) - { - _parameters[i].CipherMetadata = null; - _parameters[i].HasReceivedMetadata = false; - } - } - - keysToBeSentToEnclave?.Clear(); - enclavePackage = null; - requiresEnclaveComputations = false; - enclaveAttestationParameters = null; - customData = null; - customDataLength = 0; - } - /// /// Steps to be executed in the Prepare Transparent Encryption finally block. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 52be00e8ca..48ba4acb37 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -949,40 +949,6 @@ static internal string SqlNotificationContext() // SQLBU 329633, SQLBU 329637 return (System.Runtime.Remoting.Messaging.CallContext.GetData("MS.SqlDependencyCookie") as string); } - - /// - /// Resets the encryption related state of the command object and each of the parameters. - /// BatchRPC doesn't need special handling to cleanup the state of each RPC object and its parameters since a new RPC object and - /// parameters are generated on every execution. - /// - private void ResetEncryptionState() - { - // First reset the command level state. - ClearDescribeParameterEncryptionRequests(); - - // Reset the state for internal End execution. - _internalEndExecuteInitiated = false; - - // Reset the state for the cache. - CachingQueryMetadataPostponed = false; - - // Reset the state of each of the parameters. - if (_parameters != null) - { - for (int i = 0; i < _parameters.Count; i++) - { - _parameters[i].CipherMetadata = null; - _parameters[i].HasReceivedMetadata = false; - } - } - - keysToBeSentToEnclave?.Clear(); - enclavePackage = null; - requiresEnclaveComputations = false; - enclaveAttestationParameters = null; - customData = null; - customDataLength = 0; - } /// /// Steps to be executed in the Prepare Transparent Encryption finally block. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index f6b55ce655..615f9173a8 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -612,5 +612,39 @@ private void ReadDescribeEncryptionParameterResults3(SqlDataReader ds, bool isRe _activeConnection.EnclaveAttestationUrl); } } + + /// + /// Resets the encryption related state of the command object and each of the parameters. + /// BatchRPC doesn't need special handling to cleanup the state of each RPC object and its parameters since a new RPC object and + /// parameters are generated on every execution. + /// + private void ResetEncryptionState() + { + // First reset the command level state. + ClearDescribeParameterEncryptionRequests(); + + // Reset the state for internal End execution. + _internalEndExecuteInitiated = false; + + // Reset the state for the cache. + CachingQueryMetadataPostponed = false; + + // Reset the state of each of the parameters. + if (_parameters != null) + { + for (int i = 0; i < _parameters.Count; i++) + { + _parameters[i].CipherMetadata = null; + _parameters[i].HasReceivedMetadata = false; + } + } + + keysToBeSentToEnclave?.Clear(); + enclavePackage = null; + requiresEnclaveComputations = false; + enclaveAttestationParameters = null; + customData = null; + customDataLength = 0; + } } } From cb34479fd832417643d864a701bdc482ea365f47 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Mon, 8 Sep 2025 14:16:48 -0500 Subject: [PATCH 18/26] Merge PrepareTransparentEncryptionFinallyBlock --- .../Data/SqlClient/SqlCommand.netcore.cs | 223 ------------------ .../Data/SqlClient/SqlCommand.netfx.cs | 220 ----------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 221 +++++++++++++++++ 3 files changed, 221 insertions(+), 443 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index a7c8fbd669..4605e436f0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -955,229 +955,6 @@ private void CheckNotificationStateAndAutoEnlist() } } - /// - /// Steps to be executed in the Prepare Transparent Encryption finally block. - /// - private void PrepareTransparentEncryptionFinallyBlock(bool closeDataReader, - bool clearDataStructures, - bool decrementAsyncCount, - bool wasDescribeParameterEncryptionNeeded, - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, - SqlDataReader describeParameterEncryptionDataReader) - { - if (clearDataStructures) - { - // Clear some state variables in SqlCommand that reflect in-progress describe parameter encryption requests. - ClearDescribeParameterEncryptionRequests(); - - if (describeParameterEncryptionRpcOriginalRpcMap != null) - { - describeParameterEncryptionRpcOriginalRpcMap = null; - } - } - - // Decrement the async count. - if (decrementAsyncCount) - { - SqlInternalConnectionTds internalConnectionTds = _activeConnection.GetOpenTdsConnection(); - if (internalConnectionTds != null) - { - internalConnectionTds.DecrementAsyncCount(); - } - } - - if (closeDataReader) - { - // Close the data reader to reset the _stateObj - if (describeParameterEncryptionDataReader != null) - { - describeParameterEncryptionDataReader.Close(); - } - } - } - - /// - /// Executes the reader after checking to see if we need to encrypt input parameters and then encrypting it if required. - /// TryFetchInputParameterEncryptionInfo() -> ReadDescribeEncryptionParameterResults()-> EncryptInputParameters() ->RunExecuteReaderTds() - /// - /// - /// - /// - /// - /// - /// - /// - /// - private void PrepareForTransparentEncryption( - bool isAsync, - int timeout, - TaskCompletionSource completion, - out Task returnTask, - bool asyncWrite, - out bool usedCache, - bool isRetry) - { - // Fetch reader with input params - Task fetchInputParameterEncryptionInfoTask = null; - bool describeParameterEncryptionNeeded = false; - SqlDataReader describeParameterEncryptionDataReader = null; - returnTask = null; - usedCache = false; - - Debug.Assert(_activeConnection != null, "_activeConnection should not be null in PrepareForTransparentEncryption."); - Debug.Assert(_activeConnection.Parser != null, "_activeConnection.Parser should not be null in PrepareForTransparentEncryption."); - Debug.Assert(_activeConnection.Parser.IsColumnEncryptionSupported, - "_activeConnection.Parser.IsColumnEncryptionSupported should be true in PrepareForTransparentEncryption."); - Debug.Assert(_columnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled - || (_columnEncryptionSetting == SqlCommandColumnEncryptionSetting.UseConnectionSetting && _activeConnection.IsColumnEncryptionSettingEnabled), - "ColumnEncryption setting should be enabled for input parameter encryption."); - Debug.Assert(isAsync == (completion != null), "completion should can be null if and only if mode is async."); - - // If we are not in Batch RPC and not already retrying, attempt to fetch the cipher MD for each parameter from the cache. - // If this succeeds then return immediately, otherwise just fall back to the full crypto MD discovery. - if (!_batchRPCMode && !isRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) - { - usedCache = true; - return; - } - - // A flag to indicate if finallyblock needs to execute. - bool processFinallyBlock = true; - - // A flag to indicate if we need to decrement async count on the connection in finally block. - bool decrementAsyncCountInFinallyBlock = false; - - // Flag to indicate if exception is caught during the execution, to govern clean up. - bool exceptionCaught = false; - - // Used in _batchRPCMode to maintain a map of describe parameter encryption RPC requests (Keys) and their corresponding original RPC requests (Values). - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap = null; - - - try - { - try - { - // Fetch the encryption information that applies to any of the input parameters. - describeParameterEncryptionDataReader = TryFetchInputParameterEncryptionInfo(timeout, - isAsync, - asyncWrite, - out describeParameterEncryptionNeeded, - out fetchInputParameterEncryptionInfoTask, - out describeParameterEncryptionRpcOriginalRpcMap, - isRetry); - - Debug.Assert(describeParameterEncryptionNeeded || describeParameterEncryptionDataReader == null, - "describeParameterEncryptionDataReader should be null if we don't need to request describe parameter encryption request."); - - Debug.Assert(fetchInputParameterEncryptionInfoTask == null || isAsync, - "Task returned by TryFetchInputParameterEncryptionInfo, when in sync mode, in PrepareForTransparentEncryption."); - - Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, - "describeParameterEncryptionRpcOriginalRpcMap can be non-null if and only if it is in _batchRPCMode."); - - // If we didn't have parameters, we can fall back to regular code path, by simply returning. - if (!describeParameterEncryptionNeeded) - { - Debug.Assert(fetchInputParameterEncryptionInfoTask == null, - "fetchInputParameterEncryptionInfoTask should not be set if describe parameter encryption is not needed."); - - Debug.Assert(describeParameterEncryptionDataReader == null, - "SqlDataReader created for describe parameter encryption params when it is not needed."); - - return; - } - - // If we are in async execution, we need to decrement our async count on exception. - decrementAsyncCountInFinallyBlock = isAsync; - - Debug.Assert(describeParameterEncryptionDataReader != null, - "describeParameterEncryptionDataReader should not be null, as it is required to get results of describe parameter encryption."); - - // Fire up another task to read the results of describe parameter encryption - if (fetchInputParameterEncryptionInfoTask != null) - { - // Mark that we should not process the finally block since we have async execution pending. - // Note that this should be done outside the task's continuation delegate. - processFinallyBlock = false; - describeParameterEncryptionDataReader = GetParameterEncryptionDataReader( - out returnTask, - fetchInputParameterEncryptionInfoTask, - describeParameterEncryptionDataReader, - describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionNeeded, - isRetry); - - decrementAsyncCountInFinallyBlock = false; - } - else - { - // If it was async, ending the reader is still pending. - if (isAsync) - { - // Mark that we should not process the finally block since we have async execution pending. - // Note that this should be done outside the task's continuation delegate. - processFinallyBlock = false; - describeParameterEncryptionDataReader = GetParameterEncryptionDataReaderAsync( - out returnTask, - describeParameterEncryptionDataReader, - describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionNeeded, - isRetry); - - decrementAsyncCountInFinallyBlock = false; - } - else - { - // For synchronous execution, read the results of describe parameter encryption here. - ReadDescribeEncryptionParameterResults( - describeParameterEncryptionDataReader, - describeParameterEncryptionRpcOriginalRpcMap, - isRetry); - } - -#if DEBUG - // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. - if (_sleepAfterReadDescribeEncryptionParameterResults) - { - Thread.Sleep(10000); - } -#endif - } - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - exceptionCaught = true; - throw; - } - finally - { - // Free up the state only for synchronous execution. For asynchronous execution, free only if there was an exception. - PrepareTransparentEncryptionFinallyBlock(closeDataReader: (processFinallyBlock && !isAsync) || exceptionCaught, - decrementAsyncCount: decrementAsyncCountInFinallyBlock && exceptionCaught, - clearDataStructures: (processFinallyBlock && !isAsync) || exceptionCaught, - wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, - describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); - } - } - catch (Exception e) - { - if (CachedAsyncState != null) - { - CachedAsyncState.ResetAsyncState(); - } - - if (ADP.IsCatchableExceptionType(e)) - { - ReliablePutStateObject(); - } - - throw; - } - } - /// /// Executes an RPC to fetch param encryption info from SQL Engine. If this method is not done writing /// the request to wire, it'll set the "task" parameter which can be used to create continuations. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 48ba4acb37..a0cf9316f7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -950,226 +950,6 @@ static internal string SqlNotificationContext() return (System.Runtime.Remoting.Messaging.CallContext.GetData("MS.SqlDependencyCookie") as string); } - /// - /// Steps to be executed in the Prepare Transparent Encryption finally block. - /// - private void PrepareTransparentEncryptionFinallyBlock(bool closeDataReader, - bool clearDataStructures, - bool decrementAsyncCount, - bool wasDescribeParameterEncryptionNeeded, - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, - SqlDataReader describeParameterEncryptionDataReader) - { - if (clearDataStructures) - { - // Clear some state variables in SqlCommand that reflect in-progress describe parameter encryption requests. - ClearDescribeParameterEncryptionRequests(); - - if (describeParameterEncryptionRpcOriginalRpcMap != null) - { - describeParameterEncryptionRpcOriginalRpcMap = null; - } - } - - // Decrement the async count. - if (decrementAsyncCount) - { - SqlInternalConnectionTds internalConnectionTds = _activeConnection.GetOpenTdsConnection(); - if (internalConnectionTds != null) - { - internalConnectionTds.DecrementAsyncCount(); - } - } - - if (closeDataReader) - { - // Close the data reader to reset the _stateObj - if (describeParameterEncryptionDataReader != null) - { - describeParameterEncryptionDataReader.Close(); - } - } - } - - /// - /// Executes the reader after checking to see if we need to encrypt input parameters and then encrypting it if required. - /// TryFetchInputParameterEncryptionInfo() -> ReadDescribeEncryptionParameterResults()-> EncryptInputParameters() ->RunExecuteReaderTds() - /// - /// - /// - /// - /// - /// - /// - /// - /// - private void PrepareForTransparentEncryption( - bool isAsync, - int timeout, - TaskCompletionSource completion, - out Task returnTask, - bool asyncWrite, - out bool usedCache, - bool isRetry) - { - // Fetch reader with input params - Task fetchInputParameterEncryptionInfoTask = null; - bool describeParameterEncryptionNeeded = false; - SqlDataReader describeParameterEncryptionDataReader = null; - returnTask = null; - usedCache = false; - - Debug.Assert(_activeConnection != null, "_activeConnection should not be null in PrepareForTransparentEncryption."); - Debug.Assert(_activeConnection.Parser != null, "_activeConnection.Parser should not be null in PrepareForTransparentEncryption."); - Debug.Assert(_activeConnection.Parser.IsColumnEncryptionSupported, - "_activeConnection.Parser.IsColumnEncryptionSupported should be true in PrepareForTransparentEncryption."); - Debug.Assert(_columnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled - || (_columnEncryptionSetting == SqlCommandColumnEncryptionSetting.UseConnectionSetting && _activeConnection.IsColumnEncryptionSettingEnabled), - "ColumnEncryption setting should be enabled for input parameter encryption."); - Debug.Assert(isAsync == (completion != null), "completion should can be null if and only if mode is async."); - - // If we are not in Batch RPC and not already retrying, attempt to fetch the cipher MD for each parameter from the cache. - // If this succeeds then return immediately, otherwise just fall back to the full crypto MD discovery. - if (!_batchRPCMode && !isRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) - { - usedCache = true; - return; - } - - // A flag to indicate if finallyblock needs to execute. - bool processFinallyBlock = true; - - // A flag to indicate if we need to decrement async count on the connection in finally block. - bool decrementAsyncCountInFinallyBlock = false; - - // Flag to indicate if exception is caught during the execution, to govern clean up. - bool exceptionCaught = false; - - // Used in _batchRPCMode to maintain a map of describe parameter encryption RPC requests (Keys) and their corresponding original RPC requests (Values). - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap = null; - - try - { - try - { - // Fetch the encryption information that applies to any of the input parameters. - describeParameterEncryptionDataReader = TryFetchInputParameterEncryptionInfo(timeout, - isAsync, - asyncWrite, - out describeParameterEncryptionNeeded, - out fetchInputParameterEncryptionInfoTask, - out describeParameterEncryptionRpcOriginalRpcMap, - isRetry); - - Debug.Assert(describeParameterEncryptionNeeded || describeParameterEncryptionDataReader == null, - "describeParameterEncryptionDataReader should be null if we don't need to request describe parameter encryption request."); - - Debug.Assert(fetchInputParameterEncryptionInfoTask == null || isAsync, - "Task returned by TryFetchInputParameterEncryptionInfo, when in sync mode, in PrepareForTransparentEncryption."); - - Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, - "describeParameterEncryptionRpcOriginalRpcMap can be non-null if and only if it is in _batchRPCMode."); - - // If we didn't have parameters, we can fall back to regular code path, by simply returning. - if (!describeParameterEncryptionNeeded) - { - Debug.Assert(fetchInputParameterEncryptionInfoTask == null, - "fetchInputParameterEncryptionInfoTask should not be set if describe parameter encryption is not needed."); - - Debug.Assert(describeParameterEncryptionDataReader == null, - "SqlDataReader created for describe parameter encryption params when it is not needed."); - - return; - } - - // If we are in async execution, we need to decrement our async count on exception. - decrementAsyncCountInFinallyBlock = isAsync; - - Debug.Assert(describeParameterEncryptionDataReader != null, - "describeParameterEncryptionDataReader should not be null, as it is required to get results of describe parameter encryption."); - - // Fire up another task to read the results of describe parameter encryption - if (fetchInputParameterEncryptionInfoTask != null) - { - // Mark that we should not process the finally block since we have async execution pending. - // Note that this should be done outside the task's continuation delegate. - processFinallyBlock = false; - describeParameterEncryptionDataReader = GetParameterEncryptionDataReader( - out returnTask, - fetchInputParameterEncryptionInfoTask, - describeParameterEncryptionDataReader, - describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionNeeded, - isRetry); - - decrementAsyncCountInFinallyBlock = false; - } - else - { - // If it was async, ending the reader is still pending. - if (isAsync) - { - // Mark that we should not process the finally block since we have async execution pending. - // Note that this should be done outside the task's continuation delegate. - processFinallyBlock = false; - describeParameterEncryptionDataReader = GetParameterEncryptionDataReaderAsync( - out returnTask, - describeParameterEncryptionDataReader, - describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionNeeded, - isRetry); - - decrementAsyncCountInFinallyBlock = false; - } - else - { - // For synchronous execution, read the results of describe parameter encryption here. - ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, isRetry); - } - -#if DEBUG - // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. - if (_sleepAfterReadDescribeEncryptionParameterResults) - { - Thread.Sleep(10000); - } -#endif - } - } - catch (Exception e) - { - processFinallyBlock = ADP.IsCatchableExceptionType(e); - exceptionCaught = true; - throw; - } - finally - { - // Free up the state only for synchronous execution. For asynchronous execution, free only if there was an exception. - PrepareTransparentEncryptionFinallyBlock(closeDataReader: (processFinallyBlock && !isAsync) || exceptionCaught, - decrementAsyncCount: decrementAsyncCountInFinallyBlock && exceptionCaught, - clearDataStructures: (processFinallyBlock && !isAsync) || exceptionCaught, - wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, - describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, - describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); - } - } - // @TODO: CER Exception Handling was removed here (see GH#3581) - catch (Exception e) - { - if (CachedAsyncState != null) - { - CachedAsyncState.ResetAsyncState(); - } - - if (ADP.IsCatchableExceptionType(e)) - { - ReliablePutStateObject(); - } - - throw; - } - } - /// /// Executes an RPC to fetch param encryption info from SQL Engine. If this method is not done writing /// the request to wire, it'll set the "task" parameter which can be used to create continuations. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 615f9173a8..acf789cca7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -219,6 +219,227 @@ private void InvalidateEnclaveSession() } } + /// + /// Executes the reader after checking to see if we need to encrypt input parameters and + /// then encrypting it if required. + /// * TryFetchInputParameterEncryptionInfo() -> + /// * ReadDescribeEncryptionParameterResults() -> + /// * EncryptInputParameters() -> + /// * RunExecuteReaderTds() + /// + private void PrepareForTransparentEncryption( + bool isAsync, + int timeout, + TaskCompletionSource completion, + out Task returnTask, + bool asyncWrite, + out bool usedCache, + bool isRetry) + { + Debug.Assert(_activeConnection != null, + "_activeConnection should not be null in PrepareForTransparentEncryption."); + Debug.Assert(_activeConnection.Parser != null, + "_activeConnection.Parser should not be null in PrepareForTransparentEncryption."); + Debug.Assert(_activeConnection.Parser.IsColumnEncryptionSupported, + "_activeConnection.Parser.IsColumnEncryptionSupported should be true in PrepareForTransparentEncryption."); + Debug.Assert(_columnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled + || (_columnEncryptionSetting == SqlCommandColumnEncryptionSetting.UseConnectionSetting && _activeConnection.IsColumnEncryptionSettingEnabled), + "ColumnEncryption setting should be enabled for input parameter encryption."); + Debug.Assert(isAsync == (completion != null), + "completion should can be null if and only if mode is async."); + + // Fetch reader witn input params + Task fetchInputParameterEncryptionInfoTask = null; + bool describeParameterEncryptionNeeded = false; + SqlDataReader describeParameterEncryptionDataReader = null; + returnTask = null; + usedCache = false; + + // If we are not in _batchRPC and not already retrying, attempt to fetch the cipher MD for each parameter from the cache. + // If this succeeds then return immediately, otherwise just fall back to the full crypto MD discovery. + if (!_batchRPCMode && + !isRetry && + _parameters?.Count > 0 && + SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) + { + usedCache = true; + return; + } + + // A flag to indicate if finallyblock needs to execute. + bool processFinallyBlock = true; + + // A flag to indicate if we need to decrement async count on the connection in finally block. + bool decrementAsyncCountInFinallyBlock = false; + + // Flag to indicate if exception is caught during the execution, to govern clean up. + bool exceptionCaught = false; + + // Used in _batchRPCMode to maintain a map of describe parameter encryption RPC requests (Keys) and their corresponding original RPC requests (Values). + ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap = null; + + try + { + try + { + // Fetch the encryption information that applies to any of the input parameters. + describeParameterEncryptionDataReader = TryFetchInputParameterEncryptionInfo( + timeout, + isAsync, + asyncWrite, + out describeParameterEncryptionNeeded, + out fetchInputParameterEncryptionInfoTask, + out describeParameterEncryptionRpcOriginalRpcMap, + isRetry); + + Debug.Assert(describeParameterEncryptionNeeded || describeParameterEncryptionDataReader == null, + "describeParameterEncryptionDataReader should be null if we don't need to request describe parameter encryption request."); + Debug.Assert(fetchInputParameterEncryptionInfoTask == null || isAsync, + "Task returned by TryFetchInputParameterEncryptionInfo, when in sync mode, in PrepareForTransparentEncryption."); + Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, + "describeParameterEncryptionRpcOriginalRpcMap can be non-null if and only if it is in _batchRPCMode."); + + // If we didn't have parameters, we can fall back to regular code path, by simply returning. + if (!describeParameterEncryptionNeeded) + { + Debug.Assert(fetchInputParameterEncryptionInfoTask == null, + "fetchInputParameterEncryptionInfoTask should not be set if describe parameter encryption is not needed."); + Debug.Assert(describeParameterEncryptionDataReader == null, + "SqlDataReader created for describe parameter encryption params when it is not needed."); + + return; + } + + Debug.Assert(describeParameterEncryptionDataReader != null, + "describeParameterEncryptionDataReader should not be null, as it is required to get results of describe parameter encryption."); + + // If we are in async execution, we need to decrement our async count on exception. + decrementAsyncCountInFinallyBlock = isAsync; + + // Fire up another task to read the results of describe parameter encryption + if (fetchInputParameterEncryptionInfoTask is not null) + { + // Mark that we should not process the finally block since we have async + // execution pending. Note that this should be done outside the task's + // continuation delegate. + processFinallyBlock = false; + describeParameterEncryptionDataReader = GetParameterEncryptionDataReader( + out returnTask, + fetchInputParameterEncryptionInfoTask, + describeParameterEncryptionDataReader, + describeParameterEncryptionRpcOriginalRpcMap, + describeParameterEncryptionNeeded, + isRetry); + + decrementAsyncCountInFinallyBlock = false; + } + else + { + // @TODO Make these else-if/else, or idk flip it around with the main if case + if (isAsync) + { + // If it was async, ending the reader is still pending + // Mark that we should not process the finally block since we have async + // execution pending. Note that this should be done outside the task's + // continuation delegate. + processFinallyBlock = false; + describeParameterEncryptionDataReader = GetParameterEncryptionDataReaderAsync( + out returnTask, + describeParameterEncryptionDataReader, + describeParameterEncryptionRpcOriginalRpcMap, + describeParameterEncryptionNeeded, + isRetry); + + decrementAsyncCountInFinallyBlock = false; + } + else + { + // For synchronous execution, read the results of describe parameter + // encryption here. + ReadDescribeEncryptionParameterResults( + describeParameterEncryptionDataReader, + describeParameterEncryptionRpcOriginalRpcMap, + isRetry); + } + + #if DEBUG + // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. + if (_sleepAfterReadDescribeEncryptionParameterResults) + { + Thread.Sleep(10000); + } + #endif + } + } + catch (Exception e) + { + // @TODO: should this also check if processFinallyBlock has been cleared in the try? + processFinallyBlock = ADP.IsCatchableExceptionType(e); + exceptionCaught = true; + throw; + } + finally + { + // Free up the state only for synchronous execution. For asynchronous + // execution, free only if there was an exception. + // @TODO: processFinallyBlock should probably switch this entire method? + PrepareTransparentEncryptionFinallyBlock( + closeDataReader: (processFinallyBlock && !isAsync) || exceptionCaught, + decrementAsyncCount: decrementAsyncCountInFinallyBlock && exceptionCaught, + clearDataStructures: (processFinallyBlock && !isAsync) || exceptionCaught, + wasDescribeParameterEncryptionNeeded: describeParameterEncryptionNeeded, + describeParameterEncryptionRpcOriginalRpcMap: describeParameterEncryptionRpcOriginalRpcMap, + describeParameterEncryptionDataReader: describeParameterEncryptionDataReader); + } + } + catch (Exception e) + { + CachedAsyncState?.ResetAsyncState(); + if (ADP.IsCatchableExceptionType(e)) + { + ReliablePutStateObject(); + } + + throw; + } + } + + /// + /// Steps to be executed in the Prepare Transparent Encryption finally block. + /// + private void PrepareTransparentEncryptionFinallyBlock( + bool closeDataReader, + bool clearDataStructures, + bool decrementAsyncCount, + bool wasDescribeParameterEncryptionNeeded, // @TODO: This isn't used anywhere + ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, + SqlDataReader describeParameterEncryptionDataReader) + { + if (clearDataStructures) + { + // Clear some state variables in SqlCommand that reflect in-progress describe + // parameter encryption requests. + ClearDescribeParameterEncryptionRequests(); + if (describeParameterEncryptionRpcOriginalRpcMap != null) // @TODO: This doesn't do anything + { + describeParameterEncryptionRpcOriginalRpcMap = null; + } + } + + if (decrementAsyncCount) + { + // Decrement the async count + SqlInternalConnectionTds internalConnection = _activeConnection.GetOpenTdsConnection(); + internalConnection?.DecrementAsyncCount(); + } + + if (closeDataReader) + { + // Close the data reader to reset the _stateObj + describeParameterEncryptionDataReader?.Close(); + } + } + /// /// Read the output of sp_describe_parameter_encryption /// From 6b82413976ac07cfa5e7379b5f2bf600cf45712d Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 9 Sep 2025 16:34:00 -0500 Subject: [PATCH 19/26] Merge _rowsAffectedBySpDescribeParameterEncryption and RowsAffectedByDescribeParameterEncryption --- .../Data/SqlClient/SqlCommand.netcore.cs | 27 ----------------- .../Data/SqlClient/SqlCommand.netfx.cs | 27 ----------------- .../Microsoft/Data/SqlClient/SqlCommand.cs | 29 +++++++++++++++++++ 3 files changed, 29 insertions(+), 54 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 4605e436f0..e131b9fc95 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -155,10 +155,6 @@ private AsyncState CachedAsyncState return _cachedAsyncState; } } - - // number of rows affected by sp_describe_parameter_encryption. - // The below line is used only for debug asserts and not exposed publicly or impacts functionality otherwise. - private int _rowsAffectedBySpDescribeParameterEncryption = -1; private List<_SqlRPC> _RPCList; private int _currentlyExecutingBatch; @@ -2031,29 +2027,6 @@ internal void OnConnectionClosed() } } - /// - /// Get or add to the number of records affected by SpDescribeParameterEncryption. - /// The below line is used only for debug asserts and not exposed publicly or impacts functionality otherwise. - /// - internal int RowsAffectedByDescribeParameterEncryption - { - get - { - return _rowsAffectedBySpDescribeParameterEncryption; - } - set - { - if (-1 == _rowsAffectedBySpDescribeParameterEncryption) - { - _rowsAffectedBySpDescribeParameterEncryption = value; - } - else if (0 < value) - { - _rowsAffectedBySpDescribeParameterEncryption += value; - } - } - } - /// /// Clear the state in sqlcommand related to describe parameter encryption RPC requests. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index a0cf9316f7..a4e766fa5c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -153,10 +153,6 @@ private AsyncState CachedAsyncState return _cachedAsyncState; } } - - // number of rows affected by sp_describe_parameter_encryption. - // The below line is used only for debug asserts and not exposed publicly or impacts functionality otherwise. - private int _rowsAffectedBySpDescribeParameterEncryption = -1; private List<_SqlRPC> _RPCList; private int _currentlyExecutingBatch; @@ -2040,29 +2036,6 @@ internal void OnConnectionClosed() } } - /// - /// Get or add to the number of records affected by SpDescribeParameterEncryption. - /// The below line is used only for debug asserts and not exposed publicly or impacts functionality otherwise. - /// - internal int RowsAffectedByDescribeParameterEncryption - { - get - { - return _rowsAffectedBySpDescribeParameterEncryption; - } - set - { - if (-1 == _rowsAffectedBySpDescribeParameterEncryption) - { - _rowsAffectedBySpDescribeParameterEncryption = value; - } - else if (0 < value) - { - _rowsAffectedBySpDescribeParameterEncryption += value; - } - } - } - /// /// Clear the state in sqlcommand related to describe parameter encryption RPC requests. /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index c37c122203..6d70ec6f08 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -235,6 +235,14 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // @TODO: Use int? and replace -1 usage with null private int _rowsAffected = -1; + /// + /// number of rows affected by sp_describe_parameter_encryption. + /// + // @TODO: Use int? and replace -1 usage with null + // @TODO: This is only used for debug asserts? + // @TODO: Rename to drop Sp + private int _rowsAffectedBySpDescribeParameterEncryption = -1; + // @TODO: Rename to match naming convention private _SqlRPC[] _sqlRPCParameterEncryptionReqArray; @@ -687,6 +695,7 @@ internal bool HasColumnEncryptionKeyStoreProvidersRegistered internal bool InPrepare => _inPrepare; + // @TODO: Rename RowsAffectedInternal or internal int InternalRecordsAffected { get => _rowsAffected; @@ -713,6 +722,26 @@ internal int InternalRecordsAffected // @TODO: Rename to match conventions. internal int ObjectID { get; } = Interlocked.Increment(ref _objectTypeCount); + /// + /// Get or add to the number of records affected by SpDescribeParameterEncryption. + /// The below line is used only for debug asserts and not exposed publicly or impacts functionality otherwise. + /// + internal int RowsAffectedByDescribeParameterEncryption + { + get => _rowsAffectedBySpDescribeParameterEncryption; + set + { + if (_rowsAffectedBySpDescribeParameterEncryption == -1) + { + _rowsAffectedBySpDescribeParameterEncryption = value; + } + else if (value > 0) + { + _rowsAffectedBySpDescribeParameterEncryption += value; + } + } + } + internal SqlStatistics Statistics { get From 588a724bd5b11e5418c93394bb77cd8849464102 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 9 Sep 2025 16:48:33 -0500 Subject: [PATCH 20/26] Merge SetColumnEncryptionSetting and _wasBatchModeColumnEncryptionSettingSetOnce --- .../Data/SqlClient/SqlCommand.netcore.cs | 22 ---------------- .../Data/SqlClient/SqlCommand.netfx.cs | 22 ---------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 25 ++++++++++++++++--- .../Microsoft/Data/SqlClient/SqlCommand.cs | 9 ++++++- 4 files changed, 30 insertions(+), 48 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index e131b9fc95..3d00d9fc52 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -27,11 +27,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable { private const int MaxRPCNameLength = 1046; - /// - /// Indicates if the column encryption setting was set at-least once in the batch rpc mode, when using AddBatchCommand. - /// - private bool _wasBatchModeColumnEncryptionSettingSetOnce; - private static readonly SqlDiagnosticListener s_diagnosticListener = new SqlDiagnosticListener(); private bool _parentOperationStarted = false; @@ -2069,23 +2064,6 @@ internal void SetBatchRPCModeReadyToExecute() _currentlyExecutingBatch = 0; } - /// - /// Set the column encryption setting to the new one. - /// Do not allow conflicting column encryption settings. - /// - private void SetColumnEncryptionSetting(SqlCommandColumnEncryptionSetting newColumnEncryptionSetting) - { - if (!_wasBatchModeColumnEncryptionSettingSetOnce) - { - _columnEncryptionSetting = newColumnEncryptionSetting; - _wasBatchModeColumnEncryptionSettingSetOnce = true; - } - else if (_columnEncryptionSetting != newColumnEncryptionSetting) - { - throw SQL.BatchedUpdateColumnEncryptionSettingMismatch(); - } - } - internal void AddBatchCommand(SqlBatchCommand batchCommand) { Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index a4e766fa5c..4945d43722 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -28,11 +28,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable { private const int MaxRPCNameLength = 1046; - /// - /// Indicates if the column encryption setting was set at-least once in the batch rpc mode, when using AddBatchCommand. - /// - private bool _wasBatchModeColumnEncryptionSettingSetOnce; - internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; private _SqlRPC[] _rpcArrayOf1 = null; // Used for RPC executes @@ -2078,23 +2073,6 @@ internal void SetBatchRPCModeReadyToExecute() _currentlyExecutingBatch = 0; } - /// - /// Set the column encryption setting to the new one. - /// Do not allow conflicting column encryption settings. - /// - private void SetColumnEncryptionSetting(SqlCommandColumnEncryptionSetting newColumnEncryptionSetting) - { - if (!_wasBatchModeColumnEncryptionSettingSetOnce) - { - _columnEncryptionSetting = newColumnEncryptionSetting; - _wasBatchModeColumnEncryptionSettingSetOnce = true; - } - else if (_columnEncryptionSetting != newColumnEncryptionSetting) - { - throw SQL.BatchedUpdateColumnEncryptionSettingMismatch(); - } - } - internal void AddBatchCommand(SqlBatchCommand batchCommand) { Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index acf789cca7..331d786d94 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -230,7 +230,7 @@ private void InvalidateEnclaveSession() private void PrepareForTransparentEncryption( bool isAsync, int timeout, - TaskCompletionSource completion, + TaskCompletionSource completion, // @TODO: Only used for debug checks out Task returnTask, bool asyncWrite, out bool usedCache, @@ -836,8 +836,8 @@ private void ReadDescribeEncryptionParameterResults3(SqlDataReader ds, bool isRe /// /// Resets the encryption related state of the command object and each of the parameters. - /// BatchRPC doesn't need special handling to cleanup the state of each RPC object and its parameters since a new RPC object and - /// parameters are generated on every execution. + /// BatchRPC doesn't need special handling to clean up the state of each RPC object and its + /// parameters since a new RPC object and parameters are generated on every execution. /// private void ResetEncryptionState() { @@ -867,5 +867,24 @@ private void ResetEncryptionState() customData = null; customDataLength = 0; } + + /// + /// Set the column encryption setting to the new one. Do not allow conflicting column + /// encryption settings. + /// @TODO: This basically just allows it to be set once and it cannot be changed after. + /// + private void SetColumnEncryptionSetting(SqlCommandColumnEncryptionSetting newColumnEncryptionSetting) + { + // @TODO: Why do we need a flag *and* the value itself. The value hasn't been set if it's null! + if (!_wasBatchModeColumnEncryptionSettingSetOnce) + { + _columnEncryptionSetting = newColumnEncryptionSetting; + _wasBatchModeColumnEncryptionSettingSetOnce = true; + } + else if (_columnEncryptionSetting != newColumnEncryptionSetting) + { + throw SQL.BatchedUpdateColumnEncryptionSettingMismatch(); + } + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index 6d70ec6f08..87235d8049 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -105,7 +105,7 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// false. This may also be used to set other behavior which overrides connection level /// setting. /// - // @TODO: Make auto-property + // @TODO: Make auto-property, also make nullable. private SqlCommandColumnEncryptionSetting _columnEncryptionSetting = SqlCommandColumnEncryptionSetting.UseConnectionSetting; @@ -268,6 +268,13 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// private UpdateRowSource _updatedRowSource = UpdateRowSource.Both; + /// + /// Indicates if the column encryption setting was set at-least once in the batch rpc mode, + /// when using AddBatchCommand. + /// + // @TODO: can be replaced by using nullable for _columnEncryptionSetting. + private bool _wasBatchModeColumnEncryptionSettingSetOnce; + #endregion #region Constructors From 016756bf9dceee64bf78c3a28e9888697e47f8ea Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 9 Sep 2025 17:03:03 -0500 Subject: [PATCH 21/26] Merge GetColumnEncryptionCustomKeyProvidersNames and TryGetColumnEncryptionKeyStoreProvider --- .../Data/SqlClient/SqlCommand.netcore.cs | 25 ----------- .../Data/SqlClient/SqlCommand.netfx.cs | 25 ----------- .../Data/SqlClient/SqlCommand.Encryption.cs | 43 +++++++++++++++++++ 3 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 3d00d9fc52..f38baf37ed 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -464,31 +464,6 @@ long firstAttemptStart }, TaskScheduler.Default); } - /// - /// This function walks through the registered custom column encryption key store providers and returns an object if found. - /// - /// Provider Name to be searched in custom provider dictionary. - /// If the provider is found, initializes the corresponding SqlColumnEncryptionKeyStoreProvider instance. - /// true if the provider is found, else returns false - internal bool TryGetColumnEncryptionKeyStoreProvider(string providerName, out SqlColumnEncryptionKeyStoreProvider columnKeyStoreProvider) - { - Debug.Assert(!string.IsNullOrWhiteSpace(providerName), "Provider name is invalid"); - return _customColumnEncryptionKeyStoreProviders.TryGetValue(providerName, out columnKeyStoreProvider); - } - - /// - /// This function returns a list of the names of the custom providers currently registered. - /// - /// Combined list of provider names - internal List GetColumnEncryptionCustomKeyStoreProvidersNames() - { - if (_customColumnEncryptionKeyStoreProviders.Count > 0) - { - return new List(_customColumnEncryptionKeyStoreProviders.Keys); - } - return new List(0); - } - // If the user part is quoted, remove first and last brackets and then unquote any right square // brackets in the procedure. This is a very simple parser that performs no validation. As // with the function below, ideally we should have support from the server for this. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 4945d43722..c2337d2193 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -433,31 +433,6 @@ private bool TriggerInternalEndAndRetryIfNecessary( } } - /// - /// This function walks through the registered custom column encryption key store providers and returns an object if found. - /// - /// Provider Name to be searched in custom provider dictionary. - /// If the provider is found, initializes the corresponding SqlColumnEncryptionKeyStoreProvider instance. - /// true if the provider is found, else returns false - internal bool TryGetColumnEncryptionKeyStoreProvider(string providerName, out SqlColumnEncryptionKeyStoreProvider columnKeyStoreProvider) - { - Debug.Assert(!string.IsNullOrWhiteSpace(providerName), "Provider name is invalid"); - return _customColumnEncryptionKeyStoreProviders.TryGetValue(providerName, out columnKeyStoreProvider); - } - - /// - /// This function returns a list of the names of the custom providers currently registered. - /// - /// Combined list of provider names - internal List GetColumnEncryptionCustomKeyStoreProvidersNames() - { - if (_customColumnEncryptionKeyStoreProviders.Count > 0) - { - return new List(_customColumnEncryptionKeyStoreProviders.Keys); - } - return new List(0); - } - // If the user part is quoted, remove first and last brackets and then unquote any right square // brackets in the procedure. This is a very simple parser that performs no validation. As // with the function below, ideally we should have support from the server for this. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 331d786d94..edcdda7a41 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -16,6 +16,47 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlCommand { + + #region Internal Methods + + /// + /// This function returns a list of the names of the custom providers currently registered. + /// + /// Combined list of provider names + // @TODO: 1) This should be a property + // @TODO: 2) There is no reason for this to be a List, or even for it to be copied + // @TODO: 3) This doesn't check for null _customColumnEncryptionKeyStoreProviders + internal List GetColumnEncryptionCustomKeyStoreProvidersNames() + { + if (_customColumnEncryptionKeyStoreProviders.Count > 0) + { + return new List(_customColumnEncryptionKeyStoreProviders.Keys); + } + + return new List(0); + } + + /// + /// This function walks through the registered custom column encryption key store providers + /// and returns an object if found. + /// + /// Provider Name to be searched in custom provider dictionary. + /// + /// If the provider is found, the matching provider is returned. + /// + /// true if the provider is found, else returns false + internal bool TryGetColumnEncryptionKeyStoreProvider( + string providerName, + out SqlColumnEncryptionKeyStoreProvider columnKeyStoreProvider) + { + Debug.Assert(!string.IsNullOrWhiteSpace(providerName), "Provider name is invalid"); + return _customColumnEncryptionKeyStoreProviders.TryGetValue(providerName, out columnKeyStoreProvider); + } + + #endregion + + #region Private Methods + private static void ValidateCustomProviders(IDictionary customProviders) { // Throw when the provided dictionary is null. @@ -886,5 +927,7 @@ private void SetColumnEncryptionSetting(SqlCommandColumnEncryptionSetting newCol throw SQL.BatchedUpdateColumnEncryptionSettingMismatch(); } } + + #endregion } } From 5bc19f97fc9bd1d83e9202e8062fc14e1aff8568 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Tue, 9 Sep 2025 17:32:13 -0500 Subject: [PATCH 22/26] Merge TryFetchInputParameterEncryptionInfo --- .../Data/SqlClient/SqlCommand.netcore.cs | 142 +-------------- .../Data/SqlClient/SqlCommand.netfx.cs | 141 --------------- .../Data/SqlClient/EnclaveDelegate.cs | 1 + .../Data/SqlClient/SqlCommand.Encryption.cs | 170 +++++++++++++++++- 4 files changed, 171 insertions(+), 283 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index f38baf37ed..bb3231f848 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -921,147 +921,6 @@ private void CheckNotificationStateAndAutoEnlist() } } - /// - /// Executes an RPC to fetch param encryption info from SQL Engine. If this method is not done writing - /// the request to wire, it'll set the "task" parameter which can be used to create continuations. - /// - /// - /// - /// - /// - /// - /// - /// Indicates if this is a retry from a failed call. - /// - private SqlDataReader TryFetchInputParameterEncryptionInfo( - int timeout, - bool isAsync, - bool asyncWrite, - out bool inputParameterEncryptionNeeded, - out Task task, - out ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, - bool isRetry) - { - inputParameterEncryptionNeeded = false; - task = null; - describeParameterEncryptionRpcOriginalRpcMap = null; - byte[] serializedAttestationParameters = null; - - if (ShouldUseEnclaveBasedWorkflow) - { - SqlConnectionAttestationProtocol attestationProtocol = this._activeConnection.AttestationProtocol; - string enclaveType = this._activeConnection.Parser.EnclaveType; - - EnclaveSessionParameters enclaveSessionParameters = GetEnclaveSessionParameters(); - - SqlEnclaveSession sqlEnclaveSession = null; - EnclaveDelegate.Instance.GetEnclaveSession(attestationProtocol, enclaveType, enclaveSessionParameters, true, isRetry, out sqlEnclaveSession, out customData, out customDataLength); - if (sqlEnclaveSession == null) - { - enclaveAttestationParameters = EnclaveDelegate.Instance.GetAttestationParameters(attestationProtocol, enclaveType, enclaveSessionParameters.AttestationUrl, customData, customDataLength); - serializedAttestationParameters = EnclaveDelegate.Instance.GetSerializedAttestationParameters(enclaveAttestationParameters, enclaveType); - } - } - - if (_batchRPCMode) - { - // Count the rpc requests that need to be transparently encrypted - // We simply look for any parameters in a request and add the request to be queried for parameter encryption - Dictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcDictionary = new Dictionary<_SqlRPC, _SqlRPC>(); - - for (int i = 0; i < _RPCList.Count; i++) - { - // In BatchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. - // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_RPCList[i].systemParams.Length > 1) - { - _RPCList[i].needsFetchParameterEncryptionMetadata = true; - - // Since we are going to need multiple RPC objects, allocate a new one here for each command in the batch. - _SqlRPC rpcDescribeParameterEncryptionRequest = new _SqlRPC(); - - // Prepare the describe parameter encryption request. - PrepareDescribeParameterEncryptionRequest(_RPCList[i], ref rpcDescribeParameterEncryptionRequest, i == 0 ? serializedAttestationParameters : null); - Debug.Assert(rpcDescribeParameterEncryptionRequest != null, "rpcDescribeParameterEncryptionRequest should not be null, after call to PrepareDescribeParameterEncryptionRequest."); - - Debug.Assert(!describeParameterEncryptionRpcOriginalRpcDictionary.ContainsKey(rpcDescribeParameterEncryptionRequest), - "There should not already be a key referring to the current rpcDescribeParameterEncryptionRequest, in the dictionary describeParameterEncryptionRpcOriginalRpcDictionary."); - - // Add the describe parameter encryption RPC request as the key and its corresponding original rpc request to the dictionary. - describeParameterEncryptionRpcOriginalRpcDictionary.Add(rpcDescribeParameterEncryptionRequest, _RPCList[i]); - } - } - - describeParameterEncryptionRpcOriginalRpcMap = new ReadOnlyDictionary<_SqlRPC, _SqlRPC>(describeParameterEncryptionRpcOriginalRpcDictionary); - - if (describeParameterEncryptionRpcOriginalRpcMap.Count == 0) - { - // If no parameters are present, nothing to do, simply return. - return null; - } - else - { - inputParameterEncryptionNeeded = true; - } - - _sqlRPCParameterEncryptionReqArray = new _SqlRPC[describeParameterEncryptionRpcOriginalRpcMap.Count]; - describeParameterEncryptionRpcOriginalRpcMap.Keys.CopyTo(_sqlRPCParameterEncryptionReqArray, 0); - - Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length > 0, "There should be at-least 1 describe parameter encryption rpc request."); - Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length <= _RPCList.Count, - "The number of decribe parameter encryption RPC requests is more than the number of original RPC requests."); - } - //Always Encrypted generally operates only on parameterized queries. However enclave based Always encrypted also supports unparameterized queries - else if (ShouldUseEnclaveBasedWorkflow || (0 != GetParameterCount(_parameters))) - { - // Fetch params for a single batch - inputParameterEncryptionNeeded = true; - _sqlRPCParameterEncryptionReqArray = new _SqlRPC[1]; - - _SqlRPC rpc = null; - GetRPCObject(0, GetParameterCount(_parameters), ref rpc); - Debug.Assert(rpc != null, "GetRPCObject should not return rpc as null."); - - rpc.rpcName = CommandText; - rpc.userParams = _parameters; - - // Prepare the RPC request for describe parameter encryption procedure. - PrepareDescribeParameterEncryptionRequest(rpc, ref _sqlRPCParameterEncryptionReqArray[0], serializedAttestationParameters); - Debug.Assert(_sqlRPCParameterEncryptionReqArray[0] != null, "_sqlRPCParameterEncryptionReqArray[0] should not be null, after call to PrepareDescribeParameterEncryptionRequest."); - } - - if (inputParameterEncryptionNeeded) - { - // Set the flag that indicates that parameter encryption requests are currently in-progress. - IsDescribeParameterEncryptionRPCCurrentlyInProgress = true; - -#if DEBUG - // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. - if (_sleepDuringTryFetchInputParameterEncryptionInfo) - { - Thread.Sleep(10000); - } -#endif - - // Execute the RPC. - return RunExecuteReaderTds( - CommandBehavior.Default, - runBehavior: RunBehavior.ReturnImmediately, - returnStream: true, - isAsync: isAsync, - timeout: timeout, - task: out task, - asyncWrite: asyncWrite, - isRetry: false, - ds: null, - describeParameterEncryptionRequest: true); - } - else - { - return null; - } - } - /// /// Constructs the sp_describe_parameter_encryption request with the values from the original RPC call. /// Prototype for <sp_describe_parameter_encryption> is @@ -1432,6 +1291,7 @@ private SqlParameter GetParameterForOutputValueExtraction(SqlParameterCollection } } + // @TODO: Why not *return* it? private void GetRPCObject(int systemParamCount, int userParamCount, ref _SqlRPC rpc, bool forSpDescribeParameterEncryption = false) { // Designed to minimize necessary allocations diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index c2337d2193..fd4ee5bb68 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -916,147 +916,6 @@ static internal string SqlNotificationContext() return (System.Runtime.Remoting.Messaging.CallContext.GetData("MS.SqlDependencyCookie") as string); } - /// - /// Executes an RPC to fetch param encryption info from SQL Engine. If this method is not done writing - /// the request to wire, it'll set the "task" parameter which can be used to create continuations. - /// - /// - /// - /// - /// - /// - /// - /// Indicates if this is a retry from a failed call. - /// - private SqlDataReader TryFetchInputParameterEncryptionInfo( - int timeout, - bool isAsync, - bool asyncWrite, - out bool inputParameterEncryptionNeeded, - out Task task, - out ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, - bool isRetry) - { - inputParameterEncryptionNeeded = false; - task = null; - describeParameterEncryptionRpcOriginalRpcMap = null; - byte[] serializedAttestationParameters = null; - - if (ShouldUseEnclaveBasedWorkflow) - { - SqlConnectionAttestationProtocol attestationProtocol = this._activeConnection.AttestationProtocol; - string enclaveType = this._activeConnection.Parser.EnclaveType; - - EnclaveSessionParameters enclaveSessionParameters = GetEnclaveSessionParameters(); - - SqlEnclaveSession sqlEnclaveSession = null; - EnclaveDelegate.Instance.GetEnclaveSession(attestationProtocol, enclaveType, enclaveSessionParameters, true, isRetry, out sqlEnclaveSession, out customData, out customDataLength); - if (sqlEnclaveSession == null) - { - enclaveAttestationParameters = EnclaveDelegate.Instance.GetAttestationParameters(attestationProtocol, enclaveType, enclaveSessionParameters.AttestationUrl, customData, customDataLength); - serializedAttestationParameters = EnclaveDelegate.Instance.GetSerializedAttestationParameters(enclaveAttestationParameters, enclaveType); - } - } - - if (_batchRPCMode) - { - // Count the rpc requests that need to be transparently encrypted - // We simply look for any parameters in a request and add the request to be queried for parameter encryption - Dictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcDictionary = new Dictionary<_SqlRPC, _SqlRPC>(); - - for (int i = 0; i < _RPCList.Count; i++) - { - // In BatchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. - // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_RPCList[i].systemParams.Length > 1) - { - _RPCList[i].needsFetchParameterEncryptionMetadata = true; - - // Since we are going to need multiple RPC objects, allocate a new one here for each command in the batch. - _SqlRPC rpcDescribeParameterEncryptionRequest = new _SqlRPC(); - - // Prepare the describe parameter encryption request. - PrepareDescribeParameterEncryptionRequest(_RPCList[i], ref rpcDescribeParameterEncryptionRequest, i == 0 ? serializedAttestationParameters : null); - Debug.Assert(rpcDescribeParameterEncryptionRequest != null, "rpcDescribeParameterEncryptionRequest should not be null, after call to PrepareDescribeParameterEncryptionRequest."); - - Debug.Assert(!describeParameterEncryptionRpcOriginalRpcDictionary.ContainsKey(rpcDescribeParameterEncryptionRequest), - "There should not already be a key referring to the current rpcDescribeParameterEncryptionRequest, in the dictionary describeParameterEncryptionRpcOriginalRpcDictionary."); - - // Add the describe parameter encryption RPC request as the key and its corresponding original rpc request to the dictionary. - describeParameterEncryptionRpcOriginalRpcDictionary.Add(rpcDescribeParameterEncryptionRequest, _RPCList[i]); - } - } - - describeParameterEncryptionRpcOriginalRpcMap = new ReadOnlyDictionary<_SqlRPC, _SqlRPC>(describeParameterEncryptionRpcOriginalRpcDictionary); - - if (describeParameterEncryptionRpcOriginalRpcMap.Count == 0) - { - // If no parameters are present, nothing to do, simply return. - return null; - } - else - { - inputParameterEncryptionNeeded = true; - } - - _sqlRPCParameterEncryptionReqArray = new _SqlRPC[describeParameterEncryptionRpcOriginalRpcMap.Count]; - describeParameterEncryptionRpcOriginalRpcMap.Keys.CopyTo(_sqlRPCParameterEncryptionReqArray, 0); - - Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length > 0, "There should be at-least 1 describe parameter encryption rpc request."); - Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length <= _RPCList.Count, - "The number of decribe parameter encryption RPC requests is more than the number of original RPC requests."); - } - //Always Encrypted generally operates only on parameterized queries. However enclave based Always encrypted also supports unparameterized queries - else if (ShouldUseEnclaveBasedWorkflow || (0 != GetParameterCount(_parameters))) - { - // Fetch params for a single batch - inputParameterEncryptionNeeded = true; - _sqlRPCParameterEncryptionReqArray = new _SqlRPC[1]; - - _SqlRPC rpc = null; - GetRPCObject(0, GetParameterCount(_parameters), ref rpc); - Debug.Assert(rpc != null, "GetRPCObject should not return rpc as null."); - - rpc.rpcName = CommandText; - rpc.userParams = _parameters; - - // Prepare the RPC request for describe parameter encryption procedure. - PrepareDescribeParameterEncryptionRequest(rpc, ref _sqlRPCParameterEncryptionReqArray[0], serializedAttestationParameters); - Debug.Assert(_sqlRPCParameterEncryptionReqArray[0] != null, "_sqlRPCParameterEncryptionReqArray[0] should not be null, after call to PrepareDescribeParameterEncryptionRequest."); - } - - if (inputParameterEncryptionNeeded) - { - // Set the flag that indicates that parameter encryption requests are currently in-progress. - IsDescribeParameterEncryptionRPCCurrentlyInProgress = true; - -#if DEBUG - // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. - if (_sleepDuringTryFetchInputParameterEncryptionInfo) - { - Thread.Sleep(10000); - } -#endif - - // Execute the RPC. - return RunExecuteReaderTds( - CommandBehavior.Default, - runBehavior: RunBehavior.ReturnImmediately, - returnStream: true, - isAsync: isAsync, - timeout: timeout, - task: out task, - asyncWrite: asyncWrite, - isRetry: false, - ds: null, - describeParameterEncryptionRequest: true); - } - else - { - return null; - } - } - /// /// Constructs the sp_describe_parameter_encryption request with the values from the original RPC call. /// Prototype for <sp_describe_parameter_encryption> is 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 fc10fda351..79b88d6788 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveDelegate.cs @@ -13,6 +13,7 @@ namespace Microsoft.Data.SqlClient /// /// A delegate for communicating with secure enclave /// + // @TODO: This isn't a delegate... it's a utility class internal sealed partial class EnclaveDelegate { private static readonly SqlAeadAes256CbcHmac256Factory s_sqlAeadAes256CbcHmac256Factory = new SqlAeadAes256CbcHmac256Factory(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index edcdda7a41..93afbef83e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -16,7 +16,6 @@ namespace Microsoft.Data.SqlClient { public sealed partial class SqlCommand { - #region Internal Methods /// @@ -928,6 +927,175 @@ private void SetColumnEncryptionSetting(SqlCommandColumnEncryptionSetting newCol } } + /// + /// Executes an RPC to fetch param encryption info from SQL Engine. If this method is not done writing + /// the request to wire, it'll set the "task" parameter which can be used to create continuations. + /// + private SqlDataReader TryFetchInputParameterEncryptionInfo( + int timeout, // @TODO: Units, please + bool isAsync, + bool asyncWrite, + out bool inputParameterEncryptionNeeded, + out Task task, + out ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, + bool isRetry) // @TODO: Does this really matter? When we run the RPC we say it's never a retry. + { + inputParameterEncryptionNeeded = false; + task = null; + describeParameterEncryptionRpcOriginalRpcMap = null; + byte[] serializedAttestationParameters = null; + + if (ShouldUseEnclaveBasedWorkflow) + { + SqlConnectionAttestationProtocol attestationProtocol = _activeConnection.AttestationProtocol; + string enclaveType = _activeConnection.Parser.EnclaveType; + + EnclaveSessionParameters enclaveSessionParameters = GetEnclaveSessionParameters(); + EnclaveDelegate.Instance.GetEnclaveSession( + attestationProtocol, + enclaveType, + enclaveSessionParameters, + generateCustomData: true, + isRetry, + out SqlEnclaveSession sqlEnclaveSession, + out customData, + out customDataLength); + + if (sqlEnclaveSession is null) + { + enclaveAttestationParameters = EnclaveDelegate.Instance.GetAttestationParameters( + attestationProtocol, + enclaveType, + enclaveSessionParameters.AttestationUrl, + customData, + customDataLength); + serializedAttestationParameters = EnclaveDelegate.Instance.GetSerializedAttestationParameters( + enclaveAttestationParameters, + enclaveType); + } + + // @TODO: I think these should just be separate methods + if (_batchRPCMode) + { + // Count the RPC requests that need to be transparently encrypted. We simply + // look for any parameters in a request and add the request to be queried for + // parameter encryption. + Dictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcDictionary = + new Dictionary<_SqlRPC, _SqlRPC>(); + + for (int i = 0; i < _RPCList.Count; i++) + { + // In _batchRPCMode, the actual T-SQL query is in the first parameter and + // not present as the rpcName, as is the case with non-_batchRPCMode. So + // input parameters start at parameters[1]. parameters[0] is the actual + // T-SQL Statement. rpcName is sp_executesql. + if (_RPCList[i].systemParams.Length > 1) + { + _RPCList[i].needsFetchParameterEncryptionMetadata = true; + + // Since we are going to need multiple RPC objects, allocate a new one + // here for each command in the batch. + _SqlRPC rpcDescribeParameterEncryptionRequest = new _SqlRPC(); + + // Prepare the describe parameter encryption request. + PrepareDescribeParameterEncryptionRequest( + _RPCList[i], + ref rpcDescribeParameterEncryptionRequest, + i == 0 ? serializedAttestationParameters : null); + + Debug.Assert(rpcDescribeParameterEncryptionRequest != null, + "rpcDescribeParameterEncryptionRequest should not be null, after call to PrepareDescribeParameterEncryptionRequest."); + Debug.Assert(!describeParameterEncryptionRpcOriginalRpcDictionary.ContainsKey(rpcDescribeParameterEncryptionRequest), + "There should not already be a key referring to the current rpcDescribeParameterEncryptionRequest, in the dictionary describeParameterEncryptionRpcOriginalRpcDictionary."); + + // Add the describe parameter encryption RPC request as the key and its + // corresponding original rpc request to the dictionary. + describeParameterEncryptionRpcOriginalRpcDictionary.Add( + rpcDescribeParameterEncryptionRequest, + _RPCList[i]); + } + } + + describeParameterEncryptionRpcOriginalRpcMap = new ReadOnlyDictionary<_SqlRPC, _SqlRPC>( + describeParameterEncryptionRpcOriginalRpcDictionary); + + if (describeParameterEncryptionRpcOriginalRpcMap.Count == 0) + { + // No parameters are present, nothing to do, simply return. + return null; + } + + inputParameterEncryptionNeeded = true; + _sqlRPCParameterEncryptionReqArray = new _SqlRPC[describeParameterEncryptionRpcOriginalRpcMap.Count]; + describeParameterEncryptionRpcOriginalRpcMap.Keys.CopyTo(_sqlRPCParameterEncryptionReqArray, 0); + + Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length > 0, + "There should be at-least 1 describe parameter encryption rpc request."); + Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length <= _RPCList.Count, + "The number of describe parameter encryption RPC requests is more than the number of original RPC requests."); + } + else if (ShouldUseEnclaveBasedWorkflow || GetParameterCount(_parameters) != 0) + { + // Always Encrypted generally operates only on parameterized queries. However, + // enclave based Always encrypted also supports unparameterized queries. + + // Fetch params for a single batch. + inputParameterEncryptionNeeded = true; + _sqlRPCParameterEncryptionReqArray = new _SqlRPC[1]; + + _SqlRPC rpc = null; + GetRPCObject(systemParamCount: 0, GetParameterCount(_parameters), ref rpc); + Debug.Assert(rpc is not null, "GetRPCObject should not return rpc as null."); + + rpc.rpcName = CommandText; + rpc.userParams = _parameters; + + // Prepare the RPC request for describe parameter encryption procedure. + PrepareDescribeParameterEncryptionRequest( + rpc, + ref _sqlRPCParameterEncryptionReqArray[0], + serializedAttestationParameters); + + Debug.Assert(_sqlRPCParameterEncryptionReqArray[0] is not null, + "_sqlRPCParameterEncryptionReqArray[0] should not be null, after call to PrepareDescribeParameterEncryptionRequest."); + } + } + + // @TODO: Invert to reduce nesting of important code + if (inputParameterEncryptionNeeded) + { + // Set the flag that indicates that parameter encryption requests are currently in + // progress. + IsDescribeParameterEncryptionRPCCurrentlyInProgress = true; + + #if DEBUG + // Failpoint to force the thread to halt to simulate cancellation of SqlCommand. + if (_sleepDuringTryFetchInputParameterEncryptionInfo) + { + Thread.Sleep(10000); + } + #endif + + // Execute the RPC + // @TODO: There should be a separate method for this rather than passing a flag. + return RunExecuteReaderTds( + CommandBehavior.Default, + runBehavior: RunBehavior.ReturnImmediately, + returnStream: true, + isAsync: isAsync, + timeout: timeout, + task: out task, + asyncWrite, + isRetry: false, + ds: null, + describeParameterEncryptionRequest: true); + } + else + { + return null; + } + } + #endregion } } From 06e0bf1c340b960ec54e36f6988f81fca4d3049b Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Wed, 10 Sep 2025 12:23:59 -0500 Subject: [PATCH 23/26] Merge PrepareDescribeParameterEncryptionRequest --- .../Data/SqlClient/SqlCommand.netcore.cs | 137 --------------- .../Data/SqlClient/SqlCommand.netfx.cs | 137 --------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 159 ++++++++++++++++++ 3 files changed, 159 insertions(+), 274 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index bb3231f848..da59214bb9 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -921,143 +921,6 @@ private void CheckNotificationStateAndAutoEnlist() } } - /// - /// Constructs the sp_describe_parameter_encryption request with the values from the original RPC call. - /// Prototype for <sp_describe_parameter_encryption> is - /// exec sp_describe_parameter_encryption @tsql=N'[SQL Statement]', @params=N'@p1 varbinary(256)' - /// - /// - /// - /// - private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcRequest, ref _SqlRPC describeParameterEncryptionRequest, byte[] attestationParameters = null) - { - Debug.Assert(originalRpcRequest != null); - - // Construct the RPC request for sp_describe_parameter_encryption - // sp_describe_parameter_encryption always has 2 parameters (stmt, paramlist). - // sp_describe_parameter_encryption can have an optional 3rd parameter (attestationParameters), used to identify and execute attestation protocol - GetRPCObject(attestationParameters == null ? 2 : 3, 0, ref describeParameterEncryptionRequest, forSpDescribeParameterEncryption: true); - describeParameterEncryptionRequest.rpcName = "sp_describe_parameter_encryption"; - - // Prepare @tsql parameter - string text; - - // In _batchRPCMode, The actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-_batchRPCMode. - if (_batchRPCMode) - { - Debug.Assert(originalRpcRequest.systemParamCount > 0, - "originalRpcRequest didn't have at-least 1 parameter in BatchRPCMode, in PrepareDescribeParameterEncryptionRequest."); - text = (string)originalRpcRequest.systemParams[0].Value; - //@tsql - SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; - tsqlParam.SqlDbType = ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - tsqlParam.Value = text; - tsqlParam.Size = text.Length; - tsqlParam.Direction = ParameterDirection.Input; - } - else - { - text = originalRpcRequest.rpcName; - if (CommandType == CommandType.StoredProcedure) - { - // For stored procedures, we need to prepare @tsql in the following format - // N'EXEC sp_name @param1=@param1, @param1=@param2, ..., @paramN=@paramN' - describeParameterEncryptionRequest.systemParams[0] = BuildStoredProcedureStatementForColumnEncryption(text, originalRpcRequest.userParams); - } - else - { - //@tsql - SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; - tsqlParam.SqlDbType = ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - tsqlParam.Value = text; - tsqlParam.Size = text.Length; - tsqlParam.Direction = ParameterDirection.Input; - } - } - - Debug.Assert(text != null, "@tsql parameter is null in PrepareDescribeParameterEncryptionRequest."); - string parameterList = null; - - // In BatchRPCMode, the input parameters start at parameters[1]. parameters[0] is the T-SQL statement. rpcName is sp_executesql. - // And it is already in the format expected out of BuildParamList, which is not the case with Non-BatchRPCMode. - if (_batchRPCMode) - { - // systemParamCount == 2 when user parameters are supplied to BuildExecuteSql - if (originalRpcRequest.systemParamCount > 1) - { - parameterList = (string)originalRpcRequest.systemParams[1].Value; - } - } - else - { - // Prepare @params parameter - // Need to create new parameters as we cannot have the same parameter being part of two SqlCommand objects - SqlParameterCollection tempCollection = new SqlParameterCollection(); - - if (originalRpcRequest.userParams != null) - { - for (int i = 0; i < originalRpcRequest.userParams.Count; i++) - { - SqlParameter param = originalRpcRequest.userParams[i]; - SqlParameter paramCopy = new SqlParameter( - param.ParameterName, - param.SqlDbType, - param.Size, - param.Direction, - param.Precision, - param.Scale, - param.SourceColumn, - param.SourceVersion, - param.SourceColumnNullMapping, - param.Value, - param.XmlSchemaCollectionDatabase, - param.XmlSchemaCollectionOwningSchema, - param.XmlSchemaCollectionName - ); - paramCopy.CompareInfo = param.CompareInfo; - paramCopy.TypeName = param.TypeName; - paramCopy.UdtTypeName = param.UdtTypeName; - paramCopy.IsNullable = param.IsNullable; - paramCopy.LocaleId = param.LocaleId; - paramCopy.Offset = param.Offset; - - tempCollection.Add(paramCopy); - } - } - - Debug.Assert(_stateObj == null, "_stateObj should be null at this time, in PrepareDescribeParameterEncryptionRequest."); - Debug.Assert(_activeConnection != null, "_activeConnection should not be null at this time, in PrepareDescribeParameterEncryptionRequest."); - TdsParser tdsParser = null; - - if (_activeConnection.Parser != null) - { - tdsParser = _activeConnection.Parser; - if ((tdsParser == null) || (tdsParser.State == TdsParserState.Broken) || (tdsParser.State == TdsParserState.Closed)) - { - // Connection's parser is null as well, therefore we must be closed - throw ADP.ClosedConnectionError(); - } - } - - parameterList = BuildParamList(tdsParser, tempCollection, includeReturnValue: true); - } - - SqlParameter paramsParam = describeParameterEncryptionRequest.systemParams[1]; - paramsParam.SqlDbType = ((parameterList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - paramsParam.Size = parameterList.Length; - paramsParam.Value = parameterList; - paramsParam.Direction = ParameterDirection.Input; - - if (attestationParameters != null) - { - SqlParameter attestationParametersParam = describeParameterEncryptionRequest.systemParams[2]; - attestationParametersParam.SqlDbType = SqlDbType.VarBinary; - attestationParametersParam.Size = attestationParameters.Length; - attestationParametersParam.Value = attestationParameters; - attestationParametersParam.Direction = ParameterDirection.Input; - } - } - private Task RegisterForConnectionCloseNotification(Task outerTask) { SqlConnection connection = _activeConnection; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index fd4ee5bb68..49b092d696 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -916,143 +916,6 @@ static internal string SqlNotificationContext() return (System.Runtime.Remoting.Messaging.CallContext.GetData("MS.SqlDependencyCookie") as string); } - /// - /// Constructs the sp_describe_parameter_encryption request with the values from the original RPC call. - /// Prototype for <sp_describe_parameter_encryption> is - /// exec sp_describe_parameter_encryption @tsql=N'[SQL Statement]', @params=N'@p1 varbinary(256)' - /// - /// - /// - /// - private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcRequest, ref _SqlRPC describeParameterEncryptionRequest, byte[] attestationParameters = null) - { - Debug.Assert(originalRpcRequest != null); - - // Construct the RPC request for sp_describe_parameter_encryption - // sp_describe_parameter_encryption always has 2 parameters (stmt, paramlist). - // sp_describe_parameter_encryption can have an optional 3rd parameter (attestationParameters), used to identify and execute attestation protocol - GetRPCObject(attestationParameters == null ? 2 : 3, 0, ref describeParameterEncryptionRequest, forSpDescribeParameterEncryption: true); - describeParameterEncryptionRequest.rpcName = "sp_describe_parameter_encryption"; - - // Prepare @tsql parameter - string text; - - // In _batchRPCMode, The actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-_batchRPCMode. - if (_batchRPCMode) - { - Debug.Assert(originalRpcRequest.systemParamCount > 0, - "originalRpcRequest didn't have at-least 1 parameter in BatchRPCMode, in PrepareDescribeParameterEncryptionRequest."); - text = (string)originalRpcRequest.systemParams[0].Value; - //@tsql - SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; - tsqlParam.SqlDbType = ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - tsqlParam.Value = text; - tsqlParam.Size = text.Length; - tsqlParam.Direction = ParameterDirection.Input; - } - else - { - text = originalRpcRequest.rpcName; - if (CommandType == CommandType.StoredProcedure) - { - // For stored procedures, we need to prepare @tsql in the following format - // N'EXEC sp_name @param1=@param1, @param1=@param2, ..., @paramN=@paramN' - describeParameterEncryptionRequest.systemParams[0] = BuildStoredProcedureStatementForColumnEncryption(text, originalRpcRequest.userParams); - } - else - { - //@tsql - SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; - tsqlParam.SqlDbType = ((text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - tsqlParam.Value = text; - tsqlParam.Size = text.Length; - tsqlParam.Direction = ParameterDirection.Input; - } - } - - Debug.Assert(text != null, "@tsql parameter is null in PrepareDescribeParameterEncryptionRequest."); - string parameterList = null; - - // In BatchRPCMode, the input parameters start at parameters[1]. parameters[0] is the T-SQL statement. rpcName is sp_executesql. - // And it is already in the format expected out of BuildParamList, which is not the case with Non-BatchRPCMode. - if (_batchRPCMode) - { - // systemParamCount == 2 when user parameters are supplied to BuildExecuteSql - if (originalRpcRequest.systemParamCount > 1) - { - parameterList = (string)originalRpcRequest.systemParams[1].Value; - } - } - else - { - // Prepare @params parameter - // Need to create new parameters as we cannot have the same parameter being part of two SqlCommand objects - SqlParameterCollection tempCollection = new SqlParameterCollection(); - - if (originalRpcRequest.userParams != null) - { - for (int i = 0; i < originalRpcRequest.userParams.Count; i++) - { - SqlParameter param = originalRpcRequest.userParams[i]; - SqlParameter paramCopy = new SqlParameter( - param.ParameterName, - param.SqlDbType, - param.Size, - param.Direction, - param.Precision, - param.Scale, - param.SourceColumn, - param.SourceVersion, - param.SourceColumnNullMapping, - param.Value, - param.XmlSchemaCollectionDatabase, - param.XmlSchemaCollectionOwningSchema, - param.XmlSchemaCollectionName - ); - paramCopy.CompareInfo = param.CompareInfo; - paramCopy.TypeName = param.TypeName; - paramCopy.UdtTypeName = param.UdtTypeName; - paramCopy.IsNullable = param.IsNullable; - paramCopy.LocaleId = param.LocaleId; - paramCopy.Offset = param.Offset; - - tempCollection.Add(paramCopy); - } - } - - Debug.Assert(_stateObj == null, "_stateObj should be null at this time, in PrepareDescribeParameterEncryptionRequest."); - Debug.Assert(_activeConnection != null, "_activeConnection should not be null at this time, in PrepareDescribeParameterEncryptionRequest."); - TdsParser tdsParser = null; - - if (_activeConnection.Parser != null) - { - tdsParser = _activeConnection.Parser; - if ((tdsParser == null) || (tdsParser.State == TdsParserState.Broken) || (tdsParser.State == TdsParserState.Closed)) - { - // Connection's parser is null as well, therefore we must be closed - throw ADP.ClosedConnectionError(); - } - } - - parameterList = BuildParamList(tdsParser, tempCollection, includeReturnValue: true); - } - - SqlParameter paramsParam = describeParameterEncryptionRequest.systemParams[1]; - paramsParam.SqlDbType = ((parameterList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; - paramsParam.Size = parameterList.Length; - paramsParam.Value = parameterList; - paramsParam.Direction = ParameterDirection.Input; - - if (attestationParameters != null) - { - SqlParameter attestationParametersParam = describeParameterEncryptionRequest.systemParams[2]; - attestationParametersParam.SqlDbType = SqlDbType.VarBinary; - attestationParametersParam.Size = attestationParameters.Length; - attestationParametersParam.Value = attestationParameters; - attestationParametersParam.Direction = ParameterDirection.Input; - } - } - private Task RegisterForConnectionCloseNotification(Task outterTask) { SqlConnection connection = _activeConnection; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 93afbef83e..33ec57bbf9 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -259,6 +259,165 @@ private void InvalidateEnclaveSession() } } + /// + /// Constructs the sp_describe_parameter_encryption request with the values from the original RPC call. + /// Prototype for <sp_describe_parameter_encryption> is + /// exec sp_describe_parameter_encryption @tsql=N'[SQL Statement]', @params=N'@p1 varbinary(256)' + /// + // @TODO: Why not just return the RPC? + // @TODO: Can we have a separate method path for batch RPC mode? + private void PrepareDescribeParameterEncryptionRequest( + _SqlRPC originalRpcRequest, + ref _SqlRPC describeParameterEncryptionRequest, + byte[] attestationParameters = null) + { + Debug.Assert(originalRpcRequest is not null); + + // 1) Construct the RPC request for sp_describe_parameter_encryption + // sp_describe_parameter_encryption( + // tsql, + // params, + // [attestationParameters] - used to identify and execute attestation protocol + // ) + // @TODO: forSpDescribeParameterEncryption should just be a separate method. + GetRPCObject( + systemParamCount: attestationParameters is null ? 2 : 3, + userParamCount: 0, + ref describeParameterEncryptionRequest, + forSpDescribeParameterEncryption: true); + describeParameterEncryptionRequest.rpcName = "sp_describe_parameter_encryption"; + + // 2) Prepare @tsql parameter + string text; + if (_batchRPCMode) + { + // In _batchRPCMode, The actual T-SQL query is in the first parameter and not + // present as the rpcName, as is the case with non-_batchRPCMode. + Debug.Assert(originalRpcRequest.systemParamCount > 0, + "originalRpcRequest didn't have at-least 1 parameter in BatchRPCMode, in PrepareDescribeParameterEncryptionRequest."); + + text = (string)originalRpcRequest.systemParams[0].Value; + + SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; + tsqlParam.SqlDbType = (text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + ? SqlDbType.NVarChar + : SqlDbType.NText; // @TODO: Isn't this check being done in a lot of places? Can we factor it out to a utility? + tsqlParam.Value = text; // @TODO: Uh... isn't this the same value as it was before? + tsqlParam.Size = text.Length; + tsqlParam.Direction = ParameterDirection.Input; + } + else + { + text = originalRpcRequest.rpcName; + + if (CommandType is CommandType.StoredProcedure) + { + // For stored procedures, we need to prepare @tsql in the following format: + // N'EXEC sp_name @param1=@param1, @param2=@param2, ..., @paramN=@paramN' + describeParameterEncryptionRequest.systemParams[0] = + BuildStoredProcedureStatementForColumnEncryption(text, originalRpcRequest.userParams); + } + else + { + SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; + tsqlParam.SqlDbType = (text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + ? SqlDbType.NVarChar + : SqlDbType.NText; + tsqlParam.Value = text; + tsqlParam.Size = text.Length; + tsqlParam.Direction = ParameterDirection.Input; + } + } + + Debug.Assert(text is not null, "@tsql parameter is null in PrepareDescribeParameterEncryptionRequest."); + + // 3) Prepare @params parameter + string parameterList; + if (_batchRPCMode) + { + // In _batchRPCMode, the input parameters start at parameters[1], parameters[0] is + // the T-SQL statement. rpcName is sp_executesql, and it is already in the format + // expected for BuildParamList, which is not the case with non-_batchRPCMode. + // systemParamCount == 2 when user parameters are supplied to BuildExecuteSql. + parameterList = originalRpcRequest.systemParamCount > 1 + ? (string)originalRpcRequest.systemParams[1].Value + : null; // @TODO: If it gets set to this, we'll have a null exception later + } + else + { + // Need to create new parameters as we cannot have the same parameter being used in + // two SqlCommand objects. + SqlParameterCollection tempCollection = new SqlParameterCollection(); + if (originalRpcRequest.userParams is not null) + { + for (int i = 0; i < originalRpcRequest.userParams.Count; i++) + { + // @TODO: Use clone?? + SqlParameter param = originalRpcRequest.userParams[i]; + SqlParameter paramCopy = new SqlParameter + { + CompareInfo = param.CompareInfo, + Direction = param.Direction, + IsNullable = param.IsNullable, + LocaleId = param.LocaleId, + Offset = param.Offset, + ParameterName = param.ParameterName, + Precision = param.Precision, + Scale = param.Scale, + Size = param.Size, + SourceColumn = param.SourceColumn, + SourceColumnNullMapping = param.SourceColumnNullMapping, + SourceVersion = param.SourceVersion, + SqlDbType = param.SqlDbType, + TypeName = param.TypeName, + UdtTypeName = param.UdtTypeName, + Value = param.Value, + XmlSchemaCollectionDatabase = param.XmlSchemaCollectionDatabase, + XmlSchemaCollectionName = param.XmlSchemaCollectionName, + XmlSchemaCollectionOwningSchema = param.XmlSchemaCollectionOwningSchema, + }; + tempCollection.Add(paramCopy); + } + } + + Debug.Assert(_stateObj is null, + "_stateObj should be null at this time, in PrepareDescribeParameterEncryptionRequest."); + Debug.Assert(_activeConnection is not null, + "_activeConnection should not be null at this time, in PrepareDescribeParameterEncryptionRequest."); + + // @TODO: Shouldn't there be a way to do all this straight from the connection itself? + TdsParser tdsParser = null; + if (_activeConnection.Parser is not null) + { + tdsParser = _activeConnection.Parser; + if (tdsParser?.State is TdsParserState.Broken or TdsParserState.Closed) + { + // Connection's parser is null as well, therefore we must be closed + throw ADP.ClosedConnectionError(); + } + } + + parameterList = BuildParamList(tdsParser, tempCollection, includeReturnValue: true); + } + + SqlParameter paramsParam = describeParameterEncryptionRequest.systemParams[1]; + paramsParam.SqlDbType = (parameterList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + ? SqlDbType.NVarChar + : SqlDbType.NText; + paramsParam.Size = parameterList.Length; + paramsParam.Value = parameterList; + paramsParam.Direction = ParameterDirection.Input; + + if (attestationParameters is not null) + { + SqlParameter attestationParametersParam = describeParameterEncryptionRequest.systemParams[2]; + attestationParametersParam.SqlDbType = SqlDbType.VarBinary; + attestationParametersParam.Size = attestationParameters.Length; + attestationParametersParam.Value = attestationParameters; + attestationParametersParam.Direction = ParameterDirection.Input; + } + } + /// /// Executes the reader after checking to see if we need to encrypt input parameters and /// then encrypting it if required. From db8b2e7ccf273310591607073a81bdf61bc5e726 Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 11 Sep 2025 17:00:39 -0500 Subject: [PATCH 24/26] Merge ClearDescribeParameterEncryptionRequests --- .../Microsoft/Data/SqlClient/SqlCommand.netcore.cs | 11 ----------- .../src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs | 11 ----------- .../Microsoft/Data/SqlClient/SqlCommand.Encryption.cs | 11 +++++++++++ 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index da59214bb9..691098e115 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -1720,17 +1720,6 @@ internal void OnConnectionClosed() } } - /// - /// Clear the state in sqlcommand related to describe parameter encryption RPC requests. - /// - private void ClearDescribeParameterEncryptionRequests() - { - _sqlRPCParameterEncryptionReqArray = null; - _currentlyExecutingDescribeParameterEncryptionRPC = 0; - IsDescribeParameterEncryptionRPCCurrentlyInProgress = false; - _rowsAffectedBySpDescribeParameterEncryption = -1; - } - internal void ClearBatchCommand() { _RPCList?.Clear(); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 49b092d696..bd61e21fa7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -1728,17 +1728,6 @@ internal void OnConnectionClosed() } } - /// - /// Clear the state in sqlcommand related to describe parameter encryption RPC requests. - /// - private void ClearDescribeParameterEncryptionRequests() - { - _sqlRPCParameterEncryptionReqArray = null; - _currentlyExecutingDescribeParameterEncryptionRPC = 0; - IsDescribeParameterEncryptionRPCCurrentlyInProgress = false; - _rowsAffectedBySpDescribeParameterEncryption = -1; - } - internal void ClearBatchCommand() { _RPCList?.Clear(); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 33ec57bbf9..323ba54054 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -89,6 +89,17 @@ private static void ValidateCustomProviders(IDictionary + /// Clear the state related to describe parameter encryption RPC requests. + /// + private void ClearDescribeParameterEncryptionRequests() + { + _sqlRPCParameterEncryptionReqArray = null; + _currentlyExecutingDescribeParameterEncryptionRPC = 0; + IsDescribeParameterEncryptionRPCCurrentlyInProgress = false; + _rowsAffectedBySpDescribeParameterEncryption = -1; + } + private EnclaveSessionParameters GetEnclaveSessionParameters() => new EnclaveSessionParameters( _activeConnection.DataSource, From 0b683cc75021ef31458bd8e49f47cec7a1e6426f Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 11 Sep 2025 17:42:39 -0500 Subject: [PATCH 25/26] Merge _rpcForEncryption, BuildStoredProcedureStatementForColumnEncryption --- .../Data/SqlClient/SqlCommand.netcore.cs | 118 --------------- .../Data/SqlClient/SqlCommand.netfx.cs | 118 --------------- .../Data/SqlClient/SqlCommand.Encryption.cs | 134 ++++++++++++++++++ .../Microsoft/Data/SqlClient/SqlCommand.cs | 5 + .../Microsoft/Data/SqlClient/SqlParameter.cs | 1 + 5 files changed, 140 insertions(+), 236 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 691098e115..eaf34d17e3 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs @@ -33,7 +33,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; private _SqlRPC[] _rpcArrayOf1 = null; // Used for RPC executes - private _SqlRPC _rpcForEncryption = null; // Used for sp_describe_parameter_encryption RPC executes // cut down on object creation and cache all these // cached metadata @@ -1332,123 +1331,6 @@ private static int GetParameterCount(SqlParameterCollection parameters) return parameters != null ? parameters.Count : 0; } - /// - /// This function constructs a string parameter containing the exec statement in the following format - /// N'EXEC sp_name @param1=@param1, @param1=@param2, ..., @paramN=@paramN' - /// TODO: Need to handle return values. - /// - /// Stored procedure name - /// SqlParameter list - /// A string SqlParameter containing the constructed sql statement value - private SqlParameter BuildStoredProcedureStatementForColumnEncryption(string storedProcedureName, SqlParameterCollection parameters) - { - Debug.Assert(CommandType == CommandType.StoredProcedure, "BuildStoredProcedureStatementForColumnEncryption() should only be called for stored procedures"); - Debug.Assert(!string.IsNullOrWhiteSpace(storedProcedureName), "storedProcedureName cannot be null or empty in BuildStoredProcedureStatementForColumnEncryption"); - - StringBuilder execStatement = new StringBuilder(); - execStatement.Append(@"EXEC "); - - if (parameters is null) - { - execStatement.Append(ParseAndQuoteIdentifier(storedProcedureName, false)); - return new SqlParameter( - null, - ((execStatement.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, - execStatement.Length) - { - Value = execStatement.ToString() - }; - } - - // Find the return value parameter (if any). - SqlParameter returnValueParameter = null; - foreach (SqlParameter param in parameters) - { - if (param.Direction == ParameterDirection.ReturnValue) - { - returnValueParameter = param; - break; - } - } - - // If there is a return value parameter we need to assign the result to it. - // EXEC @returnValue = moduleName [parameters] - if (returnValueParameter != null) - { - SqlParameter.AppendPrefixedParameterName(execStatement, returnValueParameter.ParameterName); - execStatement.Append('='); - } - - execStatement.Append(ParseAndQuoteIdentifier(storedProcedureName, false)); - - // Build parameter list in the format - // @param1=@param1, @param1=@param2, ..., @paramn=@paramn - - // Append the first parameter - int index = 0; - int count = parameters.Count; - SqlParameter parameter; - if (count > 0) - { - // Skip the return value parameters. - while (index < parameters.Count && parameters[index].Direction == ParameterDirection.ReturnValue) - { - index++; - } - - if (index < count) - { - parameter = parameters[index]; - // Possibility of a SQL Injection issue through parameter names and how to construct valid identifier for parameters. - // Since the parameters comes from application itself, there should not be a security vulnerability. - // Also since the query is not executed, but only analyzed there is no possibility for elevation of privilege, but only for - // incorrect results which would only affect the user that attempts the injection. - execStatement.Append(' '); - SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); - execStatement.Append('='); - SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); - - // InputOutput and Output parameters need to be marked as such. - if (parameter.Direction == ParameterDirection.Output || - parameter.Direction == ParameterDirection.InputOutput) - { - execStatement.AppendFormat(@" OUTPUT"); - } - } - } - - // Move to the next parameter. - index++; - - // Append the rest of parameters - for (; index < count; index++) - { - parameter = parameters[index]; - if (parameter.Direction != ParameterDirection.ReturnValue) - { - execStatement.Append(", "); - SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); - execStatement.Append('='); - SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); - - // InputOutput and Output parameters need to be marked as such. - if ( - parameter.Direction == ParameterDirection.Output || - parameter.Direction == ParameterDirection.InputOutput - ) - { - execStatement.AppendFormat(@" OUTPUT"); - } - } - } - - // Construct @tsql SqlParameter to be returned - SqlParameter tsqlParameter = new SqlParameter(null, ((execStatement.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, execStatement.Length); - tsqlParameter.Value = execStatement.ToString(); - - return tsqlParameter; - } - // paramList parameter for sp_executesql, sp_prepare, and sp_prepexec internal string BuildParamList(TdsParser parser, SqlParameterCollection parameters, bool includeReturnValue = false) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index bd61e21fa7..0763c1b3f2 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs @@ -31,7 +31,6 @@ public sealed partial class SqlCommand : DbCommand, ICloneable internal static readonly Action s_cancelIgnoreFailure = CancelIgnoreFailureCallback; private _SqlRPC[] _rpcArrayOf1 = null; // Used for RPC executes - private _SqlRPC _rpcForEncryption = null; // Used for sp_describe_parameter_encryption RPC executes // cut down on object creation and cache all these // cached metadata @@ -1340,123 +1339,6 @@ private static int GetParameterCount(SqlParameterCollection parameters) return parameters != null ? parameters.Count : 0; } - /// - /// This function constructs a string parameter containing the exec statement in the following format - /// N'EXEC sp_name @param1=@param1, @param1=@param2, ..., @paramN=@paramN' - /// TODO: Need to handle return values. - /// - /// Stored procedure name - /// SqlParameter list - /// A string SqlParameter containing the constructed sql statement value - private SqlParameter BuildStoredProcedureStatementForColumnEncryption(string storedProcedureName, SqlParameterCollection parameters) - { - Debug.Assert(CommandType == CommandType.StoredProcedure, "BuildStoredProcedureStatementForColumnEncryption() should only be called for stored procedures"); - Debug.Assert(!string.IsNullOrWhiteSpace(storedProcedureName), "storedProcedureName cannot be null or empty in BuildStoredProcedureStatementForColumnEncryption"); - - StringBuilder execStatement = new StringBuilder(); - execStatement.Append(@"EXEC "); - - if (parameters is null) - { - execStatement.Append(ParseAndQuoteIdentifier(storedProcedureName, false)); - return new SqlParameter( - null, - ((execStatement.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, - execStatement.Length) - { - Value = execStatement.ToString() - }; - } - - // Find the return value parameter (if any). - SqlParameter returnValueParameter = null; - foreach (SqlParameter param in parameters) - { - if (param.Direction == ParameterDirection.ReturnValue) - { - returnValueParameter = param; - break; - } - } - - // If there is a return value parameter we need to assign the result to it. - // EXEC @returnValue = moduleName [parameters] - if (returnValueParameter != null) - { - SqlParameter.AppendPrefixedParameterName(execStatement, returnValueParameter.ParameterName); - execStatement.Append('='); - } - - execStatement.Append(ParseAndQuoteIdentifier(storedProcedureName, false)); - - // Build parameter list in the format - // @param1=@param1, @param1=@param2, ..., @paramn=@paramn - - // Append the first parameter - int index = 0; - int count = parameters.Count; - SqlParameter parameter; - if (count > 0) - { - // Skip the return value parameters. - while (index < parameters.Count && parameters[index].Direction == ParameterDirection.ReturnValue) - { - index++; - } - - if (index < count) - { - parameter = parameters[index]; - // Possibility of a SQL Injection issue through parameter names and how to construct valid identifier for parameters. - // Since the parameters comes from application itself, there should not be a security vulnerability. - // Also since the query is not executed, but only analyzed there is no possibility for elevation of privilege, but only for - // incorrect results which would only affect the user that attempts the injection. - execStatement.Append(' '); - SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); - execStatement.Append('='); - SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); - - // InputOutput and Output parameters need to be marked as such. - if (parameter.Direction == ParameterDirection.Output || - parameter.Direction == ParameterDirection.InputOutput) - { - execStatement.AppendFormat(@" OUTPUT"); - } - } - } - - // Move to the next parameter. - index++; - - // Append the rest of parameters - for (; index < count; index++) - { - parameter = parameters[index]; - if (parameter.Direction != ParameterDirection.ReturnValue) - { - execStatement.Append(", "); - SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); - execStatement.Append('='); - SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); - - // InputOutput and Output parameters need to be marked as such. - if ( - parameter.Direction == ParameterDirection.Output || - parameter.Direction == ParameterDirection.InputOutput - ) - { - execStatement.AppendFormat(@" OUTPUT"); - } - } - } - - // Construct @tsql SqlParameter to be returned - SqlParameter tsqlParameter = new SqlParameter(null, ((execStatement.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText, execStatement.Length); - tsqlParameter.Value = execStatement.ToString(); - - return tsqlParameter; - } - // paramList parameter for sp_executesql, sp_prepare, and sp_prepexec internal string BuildParamList(TdsParser parser, SqlParameterCollection parameters, bool includeReturnValue = false) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 323ba54054..8eaeb31acf 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -8,6 +8,7 @@ using System.Collections.ObjectModel; using System.Data; using System.Diagnostics; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; @@ -89,6 +90,139 @@ private static void ValidateCustomProviders(IDictionary + /// This function constructs a string parameter containing the exec statement in the following format + /// N'EXEC sp_name @param1=@param1, @param1=@param2, ..., @paramN=@paramN' + /// + /// Stored procedure name + /// SqlParameter list + /// A string SqlParameter containing the constructed sql statement value + // @TODO: This isn't building a statement, it's building a parameter? + private SqlParameter BuildStoredProcedureStatementForColumnEncryption( + string storedProcedureName, + SqlParameterCollection parameters) + { + Debug.Assert(CommandType is CommandType.StoredProcedure, + "BuildStoredProcedureStatementForColumnEncryption() should only be called for stored procedures"); + Debug.Assert(!string.IsNullOrWhiteSpace(storedProcedureName), + "storedProcedureName cannot be null or empty in BuildStoredProcedureStatementForColumnEncryption"); + + StringBuilder execStatement = new StringBuilder(@"EXEC "); + + if (parameters is null) + { + execStatement.Append(ParseAndQuoteIdentifier(storedProcedureName, isUdtTypeName: false)); + return new SqlParameter + { + ParameterName = null, + Size = execStatement.Length, + SqlDbType = (execStatement.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + ? SqlDbType.NVarChar + : SqlDbType.NText, + Value = execStatement.ToString() + }; + } + + // Find the return value parameter (if any) + SqlParameter returnValueParameter = null; + for (int i = 0; i < parameters.Count; i++) + { + if (parameters[i].Direction is ParameterDirection.ReturnValue) + { + returnValueParameter = parameters[i]; + break; + } + } + + // If there is a return value parameter, we need to assign the result to it + // EXEC @returnValue=moduleName [parameters] + // @TODO: This could be done in above loop to remove need for storing it + if (returnValueParameter is not null) + { + SqlParameter.AppendPrefixedParameterName(execStatement, returnValueParameter.ParameterName); + execStatement.Append("="); + } + + execStatement.Append(ParseAndQuoteIdentifier(storedProcedureName, isUdtTypeName: false)); + + // Build parameter list in the format + // @param1=@param1, @param2=@param2, ..., @paramN=@paramN + + // Append the first parameter + // @TODO: I guarantee there's a way to collapse these into a single loop + int index = 0; + int count = parameters.Count; + SqlParameter parameter; + if (count > 0) + { + // Skip the return value parameters + // @TODO: We assume there's only one return value param above, but here we assume there could me multiple? + while (index < parameters.Count && parameters[index].Direction is ParameterDirection.ReturnValue) + { + index++; + } + + if (index < count) + { + parameter = parameters[index]; + + // Possibility of a SQL Injection issue through parameter names and how to + // construct valid identifier for parameters. Since the parameters come from + // application itself, there should not be a security vulnerability. Also since + // the query is not executed, but only analyzed there is no possibility for + // elevation of privilege, but only for incorrect results which would only + // affect the user that attempts the injection. + // @TODO: See notes on SqlCommand.AppendPrefixedParameterName + execStatement.Append(' '); + SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); + execStatement.Append('='); + SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); + + // InputOutput and Output parameters need to be marked as such + if (parameter.Direction is ParameterDirection.Output or ParameterDirection.InputOutput) + { + execStatement.Append(@" OUTPUT"); + } + } + } + + // Move to the next parameter + index++; + + // Append the rest of the parameters + // @TODO: No, like, for real, this is doing the exact same thing as the n=1 case above!! + for (; index < count; index++) + { + parameter = parameters[index]; + + // @TODO: Invert + if (parameter.Direction is not ParameterDirection.ReturnValue) + { + execStatement.Append(' '); + SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); + execStatement.Append('='); + SqlParameter.AppendPrefixedParameterName(execStatement, parameter.ParameterName); + + // InputOutput and Output parameters need to be marked as such + if (parameter.Direction is ParameterDirection.Output or ParameterDirection.InputOutput) + { + execStatement.Append(@" OUTPUT"); + } + } + } + + // Construct @tsql SqlParameter to be returned + return new SqlParameter + { + ParameterName = null, + Size = execStatement.Length, + SqlDbType = (execStatement.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + ? SqlDbType.NVarChar + : SqlDbType.NText, + Value = execStatement.ToString() + }; + } + /// /// Clear the state related to describe parameter encryption RPC requests. /// diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs index 87235d8049..6789e36db3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -243,6 +243,11 @@ public sealed partial class SqlCommand : DbCommand, ICloneable // @TODO: Rename to drop Sp private int _rowsAffectedBySpDescribeParameterEncryption = -1; + /// + /// RPC for tracking execution of sp_describe_parameter_encryption. + /// + private _SqlRPC _rpcForEncryption = null; + // @TODO: Rename to match naming convention private _SqlRPC[] _sqlRPCParameterEncryptionReqArray; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs index fb9cdbb8e0..6e2a014ab7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameter.cs @@ -1050,6 +1050,7 @@ internal string GetPrefixedParameterName() /// /// /// + // @TODO: This is only used in SqlCommand, and literally just adds a '@' at the beginning. This belongs in SqlCommand, without the append logic. internal static void AppendPrefixedParameterName(StringBuilder builder, string rawParameterName) { if (!string.IsNullOrEmpty(rawParameterName)) From dfe729bf2483cd5aa42805d304f1d3ad1074e8ff Mon Sep 17 00:00:00 2001 From: Ben Russell Date: Thu, 9 Oct 2025 18:10:57 -0500 Subject: [PATCH 26/26] Addressing comments from @edwardneal --- .../Data/SqlClient/SqlCommand.Encryption.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs index 8eaeb31acf..4652f49f4e 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -531,15 +531,11 @@ private void PrepareDescribeParameterEncryptionRequest( "_activeConnection should not be null at this time, in PrepareDescribeParameterEncryptionRequest."); // @TODO: Shouldn't there be a way to do all this straight from the connection itself? - TdsParser tdsParser = null; - if (_activeConnection.Parser is not null) + TdsParser tdsParser = _activeConnection.Parser; + if (tdsParser is null || tdsParser.State is TdsParserState.Broken or TdsParserState.Closed) { - tdsParser = _activeConnection.Parser; - if (tdsParser?.State is TdsParserState.Broken or TdsParserState.Closed) - { - // Connection's parser is null as well, therefore we must be closed - throw ADP.ClosedConnectionError(); - } + // Connection's parser is null as well, therefore we must be closed + throw ADP.ClosedConnectionError(); } parameterList = BuildParamList(tdsParser, tempCollection, includeReturnValue: true); @@ -590,9 +586,9 @@ private void PrepareForTransparentEncryption( || (_columnEncryptionSetting == SqlCommandColumnEncryptionSetting.UseConnectionSetting && _activeConnection.IsColumnEncryptionSettingEnabled), "ColumnEncryption setting should be enabled for input parameter encryption."); Debug.Assert(isAsync == (completion != null), - "completion should can be null if and only if mode is async."); + "completion should be null if and only if mode is async."); - // Fetch reader witn input params + // Fetch reader with input params Task fetchInputParameterEncryptionInfoTask = null; bool describeParameterEncryptionNeeded = false; SqlDataReader describeParameterEncryptionDataReader = null; @@ -1078,7 +1074,7 @@ private int ReadDescribeEncryptionParameterResults2( // @TODO: Invert if statement based on answer to above TODO if (SqlParameter.ParameterNamesEqual(sqlParameter.ParameterName, parameterName)) { - Debug.Assert(sqlParameter.CipherMetadata is not null, "param.CipherMetaData should not be null."); + Debug.Assert(sqlParameter.CipherMetadata is null, "param.CipherMetadata should be null."); sqlParameter.HasReceivedMetadata = true; receivedMetadataCount++;