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/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.netcore.cs index 7a0491ad0f..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 @@ -27,67 +27,17 @@ 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; - -#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; 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 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; - private int customDataLength = 0; - // Last TaskCompletionSource for reconnect task - use for cancellation only private TaskCompletionSource _reconnectionCompletionSource = null; @@ -95,18 +45,6 @@ private bool ShouldCacheEncryptionMetadata 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 { @@ -211,27 +149,10 @@ 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 _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. /// @@ -542,84 +463,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. - 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. - /// - /// 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. @@ -1077,1135 +920,145 @@ 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() + private Task RegisterForConnectionCloseNotification(Task outerTask) { - // 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) + SqlConnection connection = _activeConnection; + if (connection == null) { - for (int i = 0; i < _parameters.Count; i++) - { - _parameters[i].CipherMetadata = null; - _parameters[i].HasReceivedMetadata = false; - } + // No connection + throw ADP.ClosedConnectionError(); } - keysToBeSentToEnclave?.Clear(); - enclavePackage = null; - requiresEnclaveComputations = false; - enclaveAttestationParameters = null; - customData = null; - customDataLength = 0; + return connection.RegisterForConnectionCloseNotification(outerTask, this, SqlReferenceCollection.CommandTag); } - /// - /// 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) + // validates that a command has commandText and a non-busy open connection + // throws exception for error case, returns false if the commandText is empty + private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "") { - if (clearDataStructures) + if (_activeConnection == null) { - // Clear some state variables in SqlCommand that reflect in-progress describe parameter encryption requests. - ClearDescribeParameterEncryptionRequests(); - - if (describeParameterEncryptionRpcOriginalRpcMap != null) - { - describeParameterEncryptionRpcOriginalRpcMap = null; - } + throw ADP.ConnectionRequired(method); } - // Decrement the async count. - if (decrementAsyncCount) + // Ensure that the connection is open and that the Parser is in the correct state + SqlInternalConnectionTds tdsConnection = _activeConnection.InnerConnection as SqlInternalConnectionTds; + + // Ensure that if column encryption override was used then server supports its + if (((SqlCommandColumnEncryptionSetting.UseConnectionSetting == ColumnEncryptionSetting && _activeConnection.IsColumnEncryptionSettingEnabled) + || (ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled || ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.ResultSetOnly)) + && tdsConnection != null + && tdsConnection.Parser != null + && !tdsConnection.Parser.IsColumnEncryptionSupported) { - SqlInternalConnectionTds internalConnectionTds = _activeConnection.GetOpenTdsConnection(); - if (internalConnectionTds != null) - { - internalConnectionTds.DecrementAsyncCount(); - } + throw SQL.TceNotSupported(); } - if (closeDataReader) + if (tdsConnection != null) { - // Close the data reader to reset the _stateObj - if (describeParameterEncryptionDataReader != null) + var parser = tdsConnection.Parser; + if ((parser == null) || (parser.State == TdsParserState.Closed)) + { + throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); + } + else if (parser.State != TdsParserState.OpenLoggedIn) { - describeParameterEncryptionDataReader.Close(); + throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); } } - } - - /// - /// 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; + else if (_activeConnection.State == ConnectionState.Closed) + { + throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); } - - // 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 + else if (_activeConnection.State == ConnectionState.Broken) { - 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."); + throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); + } - return; - } + ValidateAsyncCommand(); - // If we are in async execution, we need to decrement our async count on exception. - decrementAsyncCountInFinallyBlock = isAsync; + // close any non MARS dead readers, if applicable, and then throw if still busy. + // Throw if we have a live reader on this command + _activeConnection.ValidateConnectionForExecute(method, this); + // Check to see if the currently set transaction has completed. If so, + // null out our local reference. + if (_transaction != null && _transaction.Connection == null) + { + _transaction = null; + } - Debug.Assert(describeParameterEncryptionDataReader != null, - "describeParameterEncryptionDataReader should not be null, as it is required to get results of describe parameter encryption."); + // throw if the connection is in a transaction but there is no + // locally assigned transaction object + if (_activeConnection.HasLocalTransactionFromAPI && _transaction == null) + { + throw ADP.TransactionRequired(method); + } - // 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 we have a transaction, check to ensure that the active + // connection property matches the connection associated with + // the transaction + if (_transaction != null && _activeConnection != _transaction.Connection) + { + throw ADP.TransactionConnectionMismatch(); + } -#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); - } + if (string.IsNullOrEmpty(this.CommandText)) + { + throw ADP.CommandTextRequired(method); } - catch (Exception e) + } + + private void ValidateAsyncCommand() + { + if (CachedAsyncState != null && CachedAsyncState.PendingAsyncOperation) { - if (CachedAsyncState != null) + // Enforce only one pending async execute at a time. + if (CachedAsyncState.IsActiveConnectionValid(_activeConnection)) { - CachedAsyncState.ResetAsyncState(); + throw SQL.PendingBeginXXXExists(); } - - if (ADP.IsCatchableExceptionType(e)) + else { - ReliablePutStateObject(); + _stateObj = null; // Session was re-claimed by session pool upon connection close. + CachedAsyncState.ResetAsyncState(); } - - throw; } } - private SqlDataReader GetParameterEncryptionDataReader(out Task returnTask, Task fetchInputParameterEncryptionInfoTask, - SqlDataReader describeParameterEncryptionDataReader, - ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap, bool describeParameterEncryptionNeeded, bool isRetry) + private void GetStateObject(TdsParser parser = null) { - 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; - } + Debug.Assert(_stateObj == null, "StateObject not null on GetStateObject"); + Debug.Assert(_activeConnection != null, "no active connection?"); - // Complete executereader. - describeParameterEncryptionDataReader = command.CompleteAsyncExecuteReader(isInternal: false, forDescribeParameterEncryption: true); - Debug.Assert(command._stateObj == null, "non-null state object in PrepareForTransparentEncryption."); + if (_pendingCancel) + { + _pendingCancel = false; // Not really needed, but we'll reset anyways. - // Read the results of describe parameter encryption. - command.ReadDescribeEncryptionParameterResults(describeParameterEncryptionDataReader, describeParameterEncryptionRpcOriginalRpcMap, isRetry); + // If a pendingCancel exists on the object, we must have had a Cancel() call + // between the point that we entered an Execute* API and the point in Execute* that + // we proceeded to call this function and obtain a stateObject. In that case, + // we now throw a cancelled error. + throw SQL.OperationCancelled(); + } -#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) => + if (parser == null) + { + parser = _activeConnection.Parser; + if ((parser == null) || (parser.State == TdsParserState.Broken) || (parser.State == TdsParserState.Closed)) { - SqlCommand command = (SqlCommand)state; - if (command.CachedAsyncState != null) - { - command.CachedAsyncState.ResetAsyncState(); - } - - if (exception != null) - { - throw exception; - } + // Connection's parser is null as well, therefore we must be closed + throw ADP.ClosedConnectionError(); } - ); + } - return describeParameterEncryptionDataReader; - } + TdsParserStateObject stateObj = parser.GetSession(this); + stateObj.StartSession(this); - 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; + _stateObj = stateObj; - 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. - /// - /// - /// - /// - /// - /// - /// - /// 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 - /// 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; - } - } - - /// - /// 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; - int currentOrdinal = -1; - SqlTceCipherInfoEntry cipherInfoEntry; - 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; - -#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; - - 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; - } - } - - 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; - } - } - - if (!enclaveMetadataExists && !ds.NextResult()) - { - throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); - } - - // 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."); - - int userParamCount = rpc.userParams?.Count ?? 0; - 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; - } - } - } - } - - // 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(). - 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()); - } - } - } - -#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 - - - if (ShouldUseEnclaveBasedWorkflow && (enclaveAttestationParameters != null) && requiresEnclaveComputations) - { - if (!ds.NextResult()) - { - 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); - } - } - - // 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; - if (connection == null) - { - // No connection - throw ADP.ClosedConnectionError(); - } - - return connection.RegisterForConnectionCloseNotification(outerTask, this, SqlReferenceCollection.CommandTag); - } - - // validates that a command has commandText and a non-busy open connection - // throws exception for error case, returns false if the commandText is empty - private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "") - { - if (_activeConnection == null) - { - throw ADP.ConnectionRequired(method); - } - - // Ensure that the connection is open and that the Parser is in the correct state - SqlInternalConnectionTds tdsConnection = _activeConnection.InnerConnection as SqlInternalConnectionTds; - - // Ensure that if column encryption override was used then server supports its - if (((SqlCommandColumnEncryptionSetting.UseConnectionSetting == ColumnEncryptionSetting && _activeConnection.IsColumnEncryptionSettingEnabled) - || (ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.Enabled || ColumnEncryptionSetting == SqlCommandColumnEncryptionSetting.ResultSetOnly)) - && tdsConnection != null - && tdsConnection.Parser != null - && !tdsConnection.Parser.IsColumnEncryptionSupported) - { - throw SQL.TceNotSupported(); - } - - if (tdsConnection != null) - { - var parser = tdsConnection.Parser; - if ((parser == null) || (parser.State == TdsParserState.Closed)) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); - } - else if (parser.State != TdsParserState.OpenLoggedIn) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); - } - } - else if (_activeConnection.State == ConnectionState.Closed) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Closed); - } - else if (_activeConnection.State == ConnectionState.Broken) - { - throw ADP.OpenConnectionRequired(method, ConnectionState.Broken); - } - - ValidateAsyncCommand(); - - // close any non MARS dead readers, if applicable, and then throw if still busy. - // Throw if we have a live reader on this command - _activeConnection.ValidateConnectionForExecute(method, this); - // Check to see if the currently set transaction has completed. If so, - // null out our local reference. - if (_transaction != null && _transaction.Connection == null) - { - _transaction = null; - } - - // throw if the connection is in a transaction but there is no - // locally assigned transaction object - if (_activeConnection.HasLocalTransactionFromAPI && _transaction == null) - { - throw ADP.TransactionRequired(method); - } - - // if we have a transaction, check to ensure that the active - // connection property matches the connection associated with - // the transaction - if (_transaction != null && _activeConnection != _transaction.Connection) - { - throw ADP.TransactionConnectionMismatch(); - } - - if (string.IsNullOrEmpty(this.CommandText)) - { - throw ADP.CommandTextRequired(method); - } - } - - private void ValidateAsyncCommand() - { - if (CachedAsyncState != null && CachedAsyncState.PendingAsyncOperation) - { - // Enforce only one pending async execute at a time. - if (CachedAsyncState.IsActiveConnectionValid(_activeConnection)) - { - throw SQL.PendingBeginXXXExists(); - } - else - { - _stateObj = null; // Session was re-claimed by session pool upon connection close. - CachedAsyncState.ResetAsyncState(); - } - } - } - - private void GetStateObject(TdsParser parser = null) - { - Debug.Assert(_stateObj == null, "StateObject not null on GetStateObject"); - Debug.Assert(_activeConnection != null, "no active connection?"); - - if (_pendingCancel) - { - _pendingCancel = false; // Not really needed, but we'll reset anyways. - - // If a pendingCancel exists on the object, we must have had a Cancel() call - // between the point that we entered an Execute* API and the point in Execute* that - // we proceeded to call this function and obtain a stateObject. In that case, - // we now throw a cancelled error. - throw SQL.OperationCancelled(); - } - - if (parser == null) - { - parser = _activeConnection.Parser; - if ((parser == null) || (parser.State == TdsParserState.Broken) || (parser.State == TdsParserState.Closed)) - { - // Connection's parser is null as well, therefore we must be closed - throw ADP.ClosedConnectionError(); - } - } - - TdsParserStateObject stateObj = parser.GetSession(this); - stateObj.StartSession(this); - - _stateObj = stateObj; - - if (_pendingCancel) - { - _pendingCancel = false; // Not really needed, but we'll reset anyways. + if (_pendingCancel) + { + _pendingCancel = false; // Not really needed, but we'll reset anyways. // If a pendingCancel exists on the object, we must have had a Cancel() call // between the point that we entered this function and the point where we obtained @@ -2300,6 +1153,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 @@ -2477,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) { @@ -2865,40 +1602,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. - /// - private void ClearDescribeParameterEncryptionRequests() - { - _sqlRPCParameterEncryptionReqArray = null; - _currentlyExecutingDescribeParameterEncryptionRPC = 0; - IsDescribeParameterEncryptionRPCCurrentlyInProgress = false; - _rowsAffectedBySpDescribeParameterEncryption = -1; - } - internal void ClearBatchCommand() { _RPCList?.Clear(); @@ -2930,23 +1633,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.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/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.netfx.cs index 68033e95cd..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 @@ -28,63 +28,14 @@ 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; - -#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 - private _SqlRPC _rpcForEncryption = null; // Used for sp_describe_parameter_encryption RPC executes // cut down on object creation and cache all these // 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; - private int customDataLength = 0; - // Last TaskCompletionSource for reconnect task - use for cancellation only private TaskCompletionSource _reconnectionCompletionSource = null; @@ -92,18 +43,6 @@ private bool ShouldCacheEncryptionMetadata 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 { @@ -208,27 +147,10 @@ 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 _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. /// @@ -510,84 +432,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. - 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. - /// - /// 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. @@ -1070,963 +914,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. - /// - 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; - 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; - } - })); - - 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; - 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); - } - }); - - 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. - /// - /// - /// - /// - /// - /// - /// - /// 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 - /// 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; - } - } - - /// - /// 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; - int currentOrdinal = -1; - SqlTceCipherInfoEntry cipherInfoEntry; - 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; - -#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; - - 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; - } - } - - 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; - } - } - - if (!enclaveMetadataExists && !ds.NextResult()) - { - throw SQL.UnexpectedDescribeParamFormatParameterMetadata(); - } - - // 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."); - - int userParamCount = rpc.userParams?.Count ?? 0; - 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; - } - } - } - } - - // 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(). - 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()); - } - } - } - -#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 - - - if (ShouldUseEnclaveBasedWorkflow && (enclaveAttestationParameters != null) && requiresEnclaveComputations) - { - if (!ds.NextResult()) - { - 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); - } - } - - // 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) { @@ -2452,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) { @@ -2840,40 +1610,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. - /// - private void ClearDescribeParameterEncryptionRequests() - { - _sqlRPCParameterEncryptionReqArray = null; - _currentlyExecutingDescribeParameterEncryptionRPC = 0; - IsDescribeParameterEncryptionRPCCurrentlyInProgress = false; - _rowsAffectedBySpDescribeParameterEncryption = -1; - } - internal void ClearBatchCommand() { _RPCList?.Clear(); @@ -2905,23 +1641,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/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 new file mode 100644 index 0000000000..4652f49f4e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Encryption.cs @@ -0,0 +1,1401 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data; +using System.Diagnostics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Common; + +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. + 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 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. + /// + private void ClearDescribeParameterEncryptionRequests() + { + _sqlRPCParameterEncryptionReqArray = null; + _currentlyExecutingDescribeParameterEncryptionRPC = 0; + IsDescribeParameterEncryptionRPCCurrentlyInProgress = false; + _rowsAffectedBySpDescribeParameterEncryption = -1; + } + + 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, + 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; + } + + 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; + } + + private void InvalidateEnclaveSession() + { + if (ShouldUseEnclaveBasedWorkflow && enclavePackage != null) + { + EnclaveDelegate.Instance.InvalidateEnclaveSession( + _activeConnection.AttestationProtocol, + _activeConnection.Parser.EnclaveType, + GetEnclaveSessionParameters(), + enclavePackage.EnclaveSession); + } + } + + /// + /// 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 = _activeConnection.Parser; + if (tdsParser is null || 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. + /// * TryFetchInputParameterEncryptionInfo() -> + /// * ReadDescribeEncryptionParameterResults() -> + /// * EncryptInputParameters() -> + /// * RunExecuteReaderTds() + /// + private void PrepareForTransparentEncryption( + bool isAsync, + int timeout, + TaskCompletionSource completion, // @TODO: Only used for debug checks + 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 be null if and only if mode is async."); + + // Fetch reader with 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 + /// + /// 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) + { + bool enclaveMetadataExists = true; + while (ds.Read()) + { + // 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, + 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()) + { + 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? + // @TODO: Invert if statement based on answer to above TODO + if (SqlParameter.ParameterNamesEqual(sqlParameter.ParameterName, parameterName)) + { + Debug.Assert(sqlParameter.CipherMetadata is null, "param.CipherMetadata should 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 + if (!columnEncryptionKeyTable.TryGetValue(columnEncryptionKeyOrdinal, out SqlTceCipherInfoEntry 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. + 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; + } + + 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); + } + } + + /// + /// Resets the encryption related state of the command object and each of the parameters. + /// 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() + { + // 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; + } + + /// + /// 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(); + } + } + + /// + /// 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 + } +} 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..6789e36db3 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; @@ -43,7 +44,45 @@ 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 + // @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; + + // @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; @@ -66,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; @@ -85,6 +124,25 @@ 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. + /// + private IReadOnlyDictionary _customColumnEncryptionKeyStoreProviders; + + // @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 @@ -93,12 +151,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 @@ -106,7 +158,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 @@ -174,6 +235,22 @@ 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; + + /// + /// RPC for tracking execution of sp_describe_parameter_encryption. + /// + private _SqlRPC _rpcForEncryption = null; + + // @TODO: Rename to match naming convention + private _SqlRPC[] _sqlRPCParameterEncryptionReqArray; + /// /// TDS session the current instance is using. /// @@ -195,7 +272,14 @@ public sealed partial class SqlCommand : DbCommand, ICloneable /// DbDataAdapter. /// 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 @@ -615,9 +699,15 @@ public override UpdateRowSource UpdatedRowSource #endregion #region Internal/Protected/Private Properties - + + internal bool HasColumnEncryptionKeyStoreProvidersRegistered + { + get => _customColumnEncryptionKeyStoreProviders?.Count > 0; + } + internal bool InPrepare => _inPrepare; + // @TODO: Rename RowsAffectedInternal or internal int InternalRecordsAffected { get => _rowsAffected; @@ -634,9 +724,36 @@ 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); + /// + /// 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 @@ -763,6 +880,20 @@ 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; + } + + 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 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)) 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.");