@@ -35,7 +35,7 @@ public void TestEncryptDecryptWithAKV()
3535 AttestationProtocol = SqlConnectionAttestationProtocol . NotSpecified ,
3636 EnclaveAttestationUrl = ""
3737 } ;
38- using SqlConnection sqlConnection = new ( builder . ConnectionString ) ;
38+ using SqlConnection sqlConnection = new ( builder . ConnectionString ) ;
3939
4040 sqlConnection . Open ( ) ;
4141 Customer customer = new ( 45 , "Microsoft" , "Corporation" ) ;
@@ -48,7 +48,7 @@ public void TestEncryptDecryptWithAKV()
4848 }
4949
5050 // Test INPUT parameter on an encrypted parameter
51- using SqlCommand sqlCommand = new ( $ "SELECT CustomerId, FirstName, LastName FROM [{ _akvTableName } ] WHERE FirstName = @firstName",
51+ using SqlCommand sqlCommand = new ( $ "SELECT CustomerId, FirstName, LastName FROM [{ _akvTableName } ] WHERE FirstName = @firstName",
5252 sqlConnection ) ;
5353 SqlParameter customerFirstParam = sqlCommand . Parameters . AddWithValue ( @"firstName" , @"Microsoft" ) ;
5454 customerFirstParam . Direction = System . Data . ParameterDirection . Input ;
@@ -58,11 +58,82 @@ public void TestEncryptDecryptWithAKV()
5858 DatabaseHelper . ValidateResultSet ( sqlDataReader ) ;
5959 }
6060
61+ /*
62+ This unit test is going to assess an issue where a failed decryption leaves a connection in a bad state
63+ when it is returned to the connection pool. If a subsequent connection is retried it will result in an "Internal connection fatal error",
64+ which causes that connection to be doomed, preventing it from being returned to the pool.
65+ Consequently, retrying a third connection will encounter the same decryption error, leading to a repetitive failure cycle.
66+
67+ The purpose of this unit test is to simulate a decryption error and verify that the connection remains usable when returned to the pool.
68+ It aims to confirm that three consecutive connections will consistently fail with the "Failed to decrypt column" error.
69+ */
70+ [ ConditionalFact ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . AreConnStringSetupForAE ) , nameof ( DataTestUtility . IsAKVSetupAvailable ) ) ]
71+ public void ForcedColumnDecryptErrorTestShouldFail ( )
72+ {
73+ SqlConnectionStringBuilder builder = new ( DataTestUtility . TCPConnectionStringHGSVBS )
74+ {
75+ ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting . Enabled ,
76+ AttestationProtocol = SqlConnectionAttestationProtocol . NotSpecified ,
77+ EnclaveAttestationUrl = ""
78+ } ;
79+
80+ // Setup record to query
81+ using ( SqlConnection sqlConnection = new ( builder . ConnectionString ) )
82+ {
83+ sqlConnection . Open ( ) ;
84+ Customer customer = new ( 88 , "Microsoft2" , "Corporation2" ) ;
85+
86+ using ( SqlTransaction sqlTransaction = sqlConnection . BeginTransaction ( ) )
87+ {
88+ DatabaseHelper . InsertCustomerData ( sqlConnection , sqlTransaction , _akvTableName , customer ) ;
89+ sqlTransaction . Commit ( ) ;
90+ }
91+ }
92+
93+ // Setup Empty key store provider
94+ Dictionary < String , SqlColumnEncryptionKeyStoreProvider > emptyKeyStoreProviders = new ( )
95+ {
96+ { "AZURE_KEY_VAULT" , new EmptyKeyStoreProvider ( ) }
97+ } ;
98+
99+ // Three consecutive connections should fail with "Failed to decrypt column" error. This proves that an error in decryption
100+ // does not leave the connection in a bad state.
101+ // In each try, when a "Failed to decrypt error" is thrown, the connection's TDS Parser state object buffer is drained of any
102+ // pending data so it does not interfere with future operations. In addition, the TDS parser state object's reader.DataReady flag
103+ // is set to false so that the calling function that catches the exception will not continue to use the reader. Otherwise, it will
104+ // timeout waiting to read data that doesn't exist. Also, the TDS Parser state object HasPendingData flag is also set to false
105+ // to indicate that the buffer has been cleared and to avoid it getting cleared again in SqlDataReader.TryCloseInternal function.
106+ // Finally, after successfully handling the decryption error, the connection is then returned back to the connection pool without
107+ // an error. A proof that the connection's state object is clean is in the second connection being able to throw the same error.
108+ // The third connection is for making sure we test 3 times as the minimum number of connections to reproduce the issue previously.
109+ for ( int i = 0 ; i < 3 ; i ++ )
110+ {
111+ using ( SqlConnection sqlConnection = new SqlConnection ( builder . ConnectionString ) )
112+ {
113+ sqlConnection . Open ( ) ;
114+ // Setup connection using the empty key store provider thereby forcing a decryption error.
115+ sqlConnection . RegisterColumnEncryptionKeyStoreProvidersOnConnection ( emptyKeyStoreProviders ) ;
116+
117+ using SqlCommand sqlCommand = new ( $ "SELECT FirstName FROM [{ _akvTableName } ] WHERE FirstName = @firstName", sqlConnection ) ;
118+ SqlParameter customerFirstParam = sqlCommand . Parameters . AddWithValue ( @"firstName" , @"Microsoft2" ) ;
119+ customerFirstParam . Direction = System . Data . ParameterDirection . Input ;
120+ customerFirstParam . ForceColumnEncryption = true ;
121+
122+ using SqlDataReader sqlDataReader = sqlCommand . ExecuteReader ( ) ;
123+ while ( sqlDataReader . Read ( ) )
124+ {
125+ var error = Assert . Throws < SqlException > ( ( ) => DatabaseHelper . CompareResults ( sqlDataReader , new string [ ] { @"string" } , 1 ) ) ;
126+ Assert . Contains ( "Failed to decrypt column" , error . Message ) ;
127+ }
128+ }
129+ }
130+ }
131+
61132 [ ConditionalFact ( typeof ( DataTestUtility ) , nameof ( DataTestUtility . IsAKVSetupAvailable ) ) ]
62133 [ PlatformSpecific ( TestPlatforms . Windows ) ]
63134 public void TestRoundTripWithAKVAndCertStoreProvider ( )
64135 {
65- using SQLSetupStrategyCertStoreProvider certStoreFixture = new ( ) ;
136+ using SQLSetupStrategyCertStoreProvider certStoreFixture = new ( ) ;
66137 byte [ ] plainTextColumnEncryptionKey = ColumnEncryptionKey . GenerateRandomBytes ( ColumnEncryptionKey . KeySizeInBytes ) ;
67138 byte [ ] encryptedColumnEncryptionKeyUsingAKV = _fixture . AkvStoreProvider . EncryptColumnEncryptionKey ( DataTestUtility . AKVUrl , @"RSA_OAEP" , plainTextColumnEncryptionKey ) ;
68139 byte [ ] columnEncryptionKeyReturnedAKV2Cert = certStoreFixture . CertStoreProvider . DecryptColumnEncryptionKey ( certStoreFixture . CspColumnMasterKey . KeyPath , @"RSA_OAEP" , encryptedColumnEncryptionKeyUsingAKV ) ;
@@ -120,5 +191,18 @@ public void TestLocalCekCacheIsScopedToProvider()
120191 Exception ex = Assert . Throws < SqlException > ( ( ) => sqlCommand . ExecuteReader ( ) ) ;
121192 Assert . StartsWith ( "The current credential is not configured to acquire tokens for tenant" , ex . InnerException . Message ) ;
122193 }
194+
195+ private class EmptyKeyStoreProvider : SqlColumnEncryptionKeyStoreProvider
196+ {
197+ public override byte [ ] DecryptColumnEncryptionKey ( string masterKeyPath , string encryptionAlgorithm , byte [ ] encryptedColumnEncryptionKey )
198+ {
199+ return new byte [ 32 ] ;
200+ }
201+
202+ public override byte [ ] EncryptColumnEncryptionKey ( string masterKeyPath , string encryptionAlgorithm , byte [ ] columnEncryptionKey )
203+ {
204+ return new byte [ 32 ] ;
205+ }
206+ }
123207 }
124208}
0 commit comments