Skip to content

Commit 55178ee

Browse files
authored
Test | Unit Test for decrypt failure to drain data fix (#2844)
1 parent a3c2e85 commit 55178ee

File tree

1 file changed

+87
-3
lines changed
  • src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted

1 file changed

+87
-3
lines changed

src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVTests.cs

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)