|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | +// See the LICENSE file in the project root for more information. |
| 4 | + |
| 5 | +usingSystem; |
| 6 | +usingSystem.Collections; |
| 7 | +usingSystem.Collections.Generic; |
| 8 | +usingMicrosoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup; |
| 9 | +usingMicrosoft.Data.SqlClient.ManualTesting.Tests.SystemDataInternals; |
| 10 | +usingXunit; |
| 11 | + |
| 12 | +namespaceMicrosoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted |
| 13 | +{ |
| 14 | +publicsealedclassColumnDecryptErrorTests:IClassFixture<SQLSetupStrategyAzureKeyVault>,IDisposable |
| 15 | +{ |
| 16 | +privateSQLSetupStrategyAzureKeyVaultfixture; |
| 17 | + |
| 18 | +privatereadonlystringtableName; |
| 19 | + |
| 20 | +publicColumnDecryptErrorTests(SQLSetupStrategyAzureKeyVaultcontext) |
| 21 | +{ |
| 22 | +fixture=context; |
| 23 | +tableName=fixture.ColumnDecryptErrorTestTable.Name; |
| 24 | +} |
| 25 | + |
| 26 | +/* |
| 27 | + * This test ensures that column decryption errors and connection pooling play nicely together. |
| 28 | + * When a decryption error is encountered, we expect the connection to be drained of data and |
| 29 | + * properly reset before being returned to the pool. If this doesn't happen, then random bytes |
| 30 | + * may be left in the connection's state. These can interfere with the next operation that utilizes |
| 31 | + * the connection. |
| 32 | + * |
| 33 | + * We test that state is properly reset by triggering the same error condition twice. Routing column key discovery |
| 34 | + * away from AKV toward a dummy key store achieves this. Each connection pulls from a pool of max |
| 35 | + * size one to ensure we are using the same internal connection/socket both times. We expect to |
| 36 | + * receive the "Failed to decrypt column" exception twice. If the state were not cleaned properly, |
| 37 | + * the second error would be different because the TDS stream would be unintelligible. |
| 38 | + * |
| 39 | + * Finally, we assert that restoring the connection to AKV allows a successful query. |
| 40 | + */ |
| 41 | +[ConditionalTheory(typeof(DataTestUtility),nameof(DataTestUtility.IsTargetReadyForAeWithKeyStore),nameof(DataTestUtility.IsAKVSetupAvailable))] |
| 42 | +[ClassData(typeof(TestQueries))] |
| 43 | +publicvoidTestCleanConnectionAfterDecryptFail(stringconnString,stringselectQuery,inttotalColumnsInSelect,string[]types) |
| 44 | +{ |
| 45 | +// Arrange |
| 46 | +Assert.False(string.IsNullOrWhiteSpace(selectQuery),"FAILED: select query should not be null or empty."); |
| 47 | +Assert.True(totalColumnsInSelect<=3,"FAILED: totalColumnsInSelect should <= 3."); |
| 48 | + |
| 49 | +using(SqlConnectionsqlConnection=newSqlConnection(connString)) |
| 50 | +{ |
| 51 | +sqlConnection.Open(); |
| 52 | + |
| 53 | +Table.DeleteData(tableName,sqlConnection); |
| 54 | + |
| 55 | +Customercustomer=newCustomer( |
| 56 | +45, |
| 57 | +"Microsoft", |
| 58 | +"Corporation"); |
| 59 | + |
| 60 | +DatabaseHelper.InsertCustomerData(sqlConnection,null,tableName,customer); |
| 61 | +} |
| 62 | + |
| 63 | + |
| 64 | +// Act - Trigger a column decrypt error on the connection |
| 65 | +Dictionary<String,SqlColumnEncryptionKeyStoreProvider>keyStoreProviders=new() |
| 66 | +{ |
| 67 | +{"AZURE_KEY_VAULT",newDummyKeyStoreProvider()} |
| 68 | +}; |
| 69 | + |
| 70 | +StringpoolEnabledConnString=newSqlConnectionStringBuilder(connString){Pooling=true,MaxPoolSize=1}.ToString(); |
| 71 | + |
| 72 | +using(SqlConnectionsqlConnection=newSqlConnection(poolEnabledConnString)) |
| 73 | +{ |
| 74 | +sqlConnection.Open(); |
| 75 | +sqlConnection.RegisterColumnEncryptionKeyStoreProvidersOnConnection(keyStoreProviders); |
| 76 | + |
| 77 | +usingSqlCommandsqlCommand=newSqlCommand(string.Format(selectQuery,tableName), |
| 78 | +sqlConnection,null,SqlCommandColumnEncryptionSetting.Enabled); |
| 79 | + |
| 80 | +usingSqlDataReadersqlDataReader=sqlCommand.ExecuteReader(); |
| 81 | + |
| 82 | +Assert.True(sqlDataReader.HasRows,"FAILED: Select statement did not return any rows."); |
| 83 | + |
| 84 | +while(sqlDataReader.Read()) |
| 85 | +{ |
| 86 | +varerror=Assert.Throws<SqlException>(()=>DatabaseHelper.CompareResults(sqlDataReader,types,totalColumnsInSelect)); |
| 87 | +Assert.Contains("Failed to decrypt column",error.Message); |
| 88 | +} |
| 89 | +} |
| 90 | + |
| 91 | + |
| 92 | +// Assert |
| 93 | +using(SqlConnectionsqlConnection=newSqlConnection(poolEnabledConnString)) |
| 94 | +{ |
| 95 | +sqlConnection.Open(); |
| 96 | +sqlConnection.RegisterColumnEncryptionKeyStoreProvidersOnConnection(keyStoreProviders); |
| 97 | + |
| 98 | +usingSqlCommandsqlCommand=newSqlCommand(string.Format(selectQuery,tableName), |
| 99 | +sqlConnection,null,SqlCommandColumnEncryptionSetting.Enabled); |
| 100 | +usingSqlDataReadersqlDataReader=sqlCommand.ExecuteReader(); |
| 101 | + |
| 102 | +Assert.True(sqlDataReader.HasRows,"FAILED: Select statement did not return any rows."); |
| 103 | + |
| 104 | +while(sqlDataReader.Read()) |
| 105 | +{ |
| 106 | +varerror=Assert.Throws<SqlException>(()=>DatabaseHelper.CompareResults(sqlDataReader,types,totalColumnsInSelect)); |
| 107 | +Assert.Contains("Failed to decrypt column",error.Message); |
| 108 | +} |
| 109 | +} |
| 110 | + |
| 111 | +using(SqlConnectionsqlConnection=newSqlConnection(poolEnabledConnString)) |
| 112 | +{ |
| 113 | +sqlConnection.Open(); |
| 114 | + |
| 115 | +usingSqlCommandsqlCommand=newSqlCommand(string.Format(selectQuery,tableName), |
| 116 | +sqlConnection,null,SqlCommandColumnEncryptionSetting.Enabled); |
| 117 | +usingSqlDataReadersqlDataReader=sqlCommand.ExecuteReader(); |
| 118 | + |
| 119 | +Assert.True(sqlDataReader.HasRows,"FAILED: Select statement did not return any rows."); |
| 120 | + |
| 121 | +while(sqlDataReader.Read()) |
| 122 | +{ |
| 123 | +DatabaseHelper.CompareResults(sqlDataReader,types,totalColumnsInSelect); |
| 124 | +} |
| 125 | +} |
| 126 | +} |
| 127 | + |
| 128 | + |
| 129 | +publicvoidDispose() |
| 130 | +{ |
| 131 | +foreach(stringconnStrAEinDataTestUtility.AEConnStringsSetup) |
| 132 | +{ |
| 133 | +using(SqlConnectionsqlConnection=newSqlConnection(connStrAE)) |
| 134 | +{ |
| 135 | +sqlConnection.Open(); |
| 136 | +Table.DeleteData(fixture.ColumnDecryptErrorTestTable.Name,sqlConnection); |
| 137 | +} |
| 138 | +} |
| 139 | +} |
| 140 | + |
| 141 | +privatesealedclassDummyKeyStoreProvider:SqlColumnEncryptionKeyStoreProvider |
| 142 | +{ |
| 143 | +publicoverridebyte[]DecryptColumnEncryptionKey(stringmasterKeyPath,stringencryptionAlgorithm,byte[]encryptedColumnEncryptionKey) |
| 144 | +{ |
| 145 | +// Must be 32 to match the key length expected for the 'AEAD_AES_256_CBC_HMAC_SHA256' algorithm |
| 146 | +returnnewbyte[32]; |
| 147 | +} |
| 148 | + |
| 149 | +publicoverridebyte[]EncryptColumnEncryptionKey(stringmasterKeyPath,stringencryptionAlgorithm,byte[]columnEncryptionKey) |
| 150 | +{ |
| 151 | +returnnewbyte[32]; |
| 152 | +} |
| 153 | +} |
| 154 | +} |
| 155 | + |
| 156 | +publicclassTestQueries:IEnumerable<object[]> |
| 157 | +{ |
| 158 | +publicIEnumerator<object[]>GetEnumerator() |
| 159 | +{ |
| 160 | +foreach(stringconnStrAEinDataTestUtility.AEConnStrings) |
| 161 | +{ |
| 162 | +yieldreturnnewobject[]{connStrAE,@"select CustomerId, FirstName, LastName from [{0}] ",3,newstring[]{@"int",@"string",@"string"}}; |
| 163 | +yieldreturnnewobject[]{connStrAE,@"select CustomerId, FirstName from [{0}] ",2,newstring[]{@"int",@"string"}}; |
| 164 | +yieldreturnnewobject[]{connStrAE,@"select LastName from [{0}] ",1,newstring[]{@"string"}}; |
| 165 | +} |
| 166 | +} |
| 167 | +IEnumeratorIEnumerable.GetEnumerator()=>GetEnumerator(); |
| 168 | +} |
| 169 | +} |
| 170 | + |