From 185fd6f88b07bf2304a91c95386e5a7d9a59e715 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Mon, 29 Jun 2020 08:18:11 -0700 Subject: [PATCH 01/17] AKV Upgrade --- BUILDGUIDE.md | 1 + .../AzureKeyVaultProviderTokenCredential.cs | 64 +++ .../AzureSqlKeyCryptographer.cs | 232 ++++++++++ ...waysEncrypted.AzureKeyVaultProvider.csproj | 6 +- ...qlColumnEncryptionAzureKeyVaultProvider.cs | 427 +++++------------- .../AzureKeyVaultProvider/Strings.Designer.cs | 82 +--- .../AzureKeyVaultProvider/Strings.resx | 28 +- .../AzureKeyVaultProvider/Validator.cs | 93 ++++ .../AlwaysEncrypted/AKVUnitTests.cs | 67 +++ .../TestFixtures/Setup/CertificateUtility.cs | 81 ++-- .../TestFixtures/Setup/ColumnMasterKey.cs | 3 - .../ManualTests/DataCommon/DataTestUtility.cs | 9 +- ....Data.SqlClient.ManualTesting.Tests.csproj | 2 + .../tests/ManualTests/config.default.json | 1 + tools/props/Versions.props | 7 +- 15 files changed, 646 insertions(+), 457 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs create mode 100644 src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs create mode 100644 src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md index 67d7a955d0..943ddc1eba 100644 --- a/BUILDGUIDE.md +++ b/BUILDGUIDE.md @@ -112,6 +112,7 @@ Manual Tests require the below setup to run: |AADSecurePrincipalId | (Optional) The Application Id of a registered application which has been granted permission to the database defined in the AADPasswordConnectionString. | {Application ID} | |AADSecurePrincipalSecret | (Optional) A Secret defined for a registered application which has been granted permission to the database defined in the AADPasswordConnectionString. | {Secret} | |AzureKeyVaultURL | (Optional) Azure Key Vault Identifier URL | `https://{keyvaultname}.vault.azure.net/` | +|AzureKeyVaultTenantId | (Optional) The Azure Active Directory tenant (directory) Id of the service principal. | _{Tenant ID of Active Directory}_ | |AzureKeyVaultClientId | (Optional) "Application (client) ID" of an Active Directory registered application, granted access to the Azure Key Vault specified in `AZURE_KEY_VAULT_URL`. Requires the key permissions Get, List, Import, Decrypt, Encrypt, Unwrap, Wrap, Verify, and Sign. | _{Client Application ID}_ | |AzureKeyVaultClientSecret | (Optional) "Client Secret" of the Active Directory registered application, granted access to the Azure Key Vault specified in `AZURE_KEY_VAULT_URL` | _{Client Application Secret}_ | |SupportsLocalDb | (Optional) Whether or not a LocalDb instance of SQL Server is installed on the machine running the tests. |`true` OR `false`| diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs new file mode 100644 index 0000000000..39d6d41159 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs @@ -0,0 +1,64 @@ +// 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.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; + +namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider +{ + internal class AzureKeyVaultProviderTokenCredential : TokenCredential + { + private AuthenticationCallback Callback { get; set; } + + private string Authority { get; set; } + + private string Resource { get; set; } + + internal AzureKeyVaultProviderTokenCredential(AuthenticationCallback authenticationCallback, string masterKeyPath) + { + Callback = authenticationCallback; + HttpClient httpClient = new HttpClient(); + HttpResponseMessage response = httpClient.GetAsync(masterKeyPath).GetAwaiter().GetResult(); + string challenge = response?.Headers.WwwAuthenticate.FirstOrDefault()?.ToString(); + string trimmedChallenge = ValidateChallenge(challenge); + string[] pairs = trimmedChallenge.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + Authority = pairs[0].Split('=')[1].Trim().Trim(new char[] { '\"' }); + Resource = pairs[1].Split('=')[1].Trim().Trim(new char[] { '\"' }); + } + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + string token = Callback.Invoke(Authority, Resource, string.Empty).GetAwaiter().GetResult(); + return new AccessToken(token, DateTimeOffset.Now.AddHours(1)); + } + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + string token = Callback.Invoke(Authority, Resource, string.Empty).GetAwaiter().GetResult(); + Task task = Task.FromResult(new AccessToken(token, DateTimeOffset.Now.AddHours(1))); + return new ValueTask(task); + } + + private static string ValidateChallenge(string challenge) + { + const string bearer = "Bearer "; + if (string.IsNullOrEmpty(challenge)) + throw new ArgumentNullException("challenge"); + + string trimmedChallenge = challenge.Trim(); + + if (!trimmedChallenge.StartsWith(bearer)) + throw new ArgumentException("Challenge is not Bearer", "challenge"); + + return trimmedChallenge.Substring(bearer.Length); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs new file mode 100644 index 0000000000..45c1e44f01 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs @@ -0,0 +1,232 @@ +// 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 Azure.Core; +using Azure.Security.KeyVault.Keys; +using Azure.Security.KeyVault.Keys.Cryptography; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using static Azure.Security.KeyVault.Keys.Cryptography.SignatureAlgorithm; + + +namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider +{ + internal class AzureSqlKeyCryptographer + { + /// + /// TokenCredential to be used with the KeyClient + /// + private TokenCredential TokenCredential { get; set; } + + /// + /// AuthenticationCallback to be used with the KeyClient for legacy support. + /// + private AuthenticationCallback AuthenticationCallback { get; set; } + + /// + /// A flag to determine whether to use AuthenticationCallback with the KeyClient for legacy support. + /// + private readonly bool isUsingLegacyAuthentication = false; + + /// + /// A mapping of the KeyClient objects to the corresponding Azure Key Vault URI + /// + private readonly Dictionary keyClientDictionary = new Dictionary(); + + /// + /// Holds references to the fetch key tasks and maps them to their corresponding Azure Key Vault Key Identifier (URI). + /// These tasks will be used for returning the key in the event that the fetch task has not finished depositing the + /// key into the key dictionary. + /// + private readonly Dictionary>> _keyFetchTaskDictionary = new Dictionary>>(); + + /// + /// Holds references to the Azure Key Vault keys and maps them to their corresponding Azure Key Vault Key Identifier (URI). + /// + private readonly Dictionary _keyDictionary = new Dictionary(); + + /// + /// Holds references to the Azure Key Vault CryptographyClient objects and maps them to their corresponding Azure Key Vault Key Identifier (URI). + /// + private readonly Dictionary cryptoClientDictionary = new Dictionary(); + + /// + /// Constructs a new KeyCryptographer + /// + /// + internal AzureSqlKeyCryptographer(TokenCredential tokenCredential) + { + TokenCredential = tokenCredential; + } + + internal AzureSqlKeyCryptographer(AuthenticationCallback authenticationCallback) + { + AuthenticationCallback = authenticationCallback; + isUsingLegacyAuthentication = true; + } + + /// + /// Adds the key, specified by the Key Identifier URI, to the cache. + /// + /// + internal void AddKey(string keyIdentifierUri) + { + if (TheKeyHasNotBeenCached(keyIdentifierUri)) + { + if (isUsingLegacyAuthentication) + { + TokenCredential = new AzureKeyVaultProviderTokenCredential(AuthenticationCallback, keyIdentifierUri); + } + + ParseAKVPath(keyIdentifierUri, out Uri vaultUri, out string keyName); + CreateKeyClient(vaultUri); + FetchKey(vaultUri, keyName, keyIdentifierUri); + } + + bool TheKeyHasNotBeenCached(string k) => !_keyDictionary.ContainsKey(k) && !_keyFetchTaskDictionary.ContainsKey(k); + } + + /// + /// Returns the key specified by the Key Identifier URI + /// + /// + /// + internal KeyVaultKey GetKey(string keyIdentifierUri) + { + if (_keyDictionary.ContainsKey(keyIdentifierUri)) + { + return _keyDictionary[keyIdentifierUri]; + } + + if (_keyFetchTaskDictionary.ContainsKey(keyIdentifierUri)) + { + return Task.Run(() => _keyFetchTaskDictionary[keyIdentifierUri]).Result; + } + + // Not a public exception - not likely to occur. + throw new KeyNotFoundException($"The key with identifier {keyIdentifierUri} was not found."); + } + + /// + /// Gets the public Key size in bytes. + /// + /// The key vault key identifier URI + /// + internal int GetKeySize(string keyIdentifierUri) + { + return GetKey(keyIdentifierUri).Key.N.Length; + } + + /// + /// Generates signature based on RSA PKCS#v1.5 scheme using a specified Azure Key Vault Key URL. + /// + /// The data to sign + /// The key vault key identifier URI + /// + internal byte[] SignData(byte[] message, string keyIdentifierUri) + { + CryptographyClient cryptographyClient = GetCryptographyClient(keyIdentifierUri); + return cryptographyClient.SignData(RS256, message).Signature; + } + + internal bool VerifyData(byte[] message, byte[] signature, string keyIdentifierUri) + { + CryptographyClient cryptographyClient = GetCryptographyClient(keyIdentifierUri); + return cryptographyClient.VerifyData(RS256, message, signature).IsValid; + } + + internal byte[] UnwrapKey(KeyWrapAlgorithm keyWrapAlgorithm, byte[] encryptedKey, string keyIdentifierUri) + { + CryptographyClient cryptographyClient = GetCryptographyClient(keyIdentifierUri); + return cryptographyClient.UnwrapKey(keyWrapAlgorithm, encryptedKey).Key; + } + + internal byte[] WrapKey(KeyWrapAlgorithm keyWrapAlgorithm, byte[] key, string keyIdentifierUri) + { + CryptographyClient cryptographyClient = GetCryptographyClient(keyIdentifierUri); + return cryptographyClient.WrapKey(keyWrapAlgorithm, key).EncryptedKey; + } + + private CryptographyClient GetCryptographyClient(string keyIdentifierUri) + { + if (cryptoClientDictionary.ContainsKey(keyIdentifierUri)) + { + return cryptoClientDictionary[keyIdentifierUri]; + } + + CryptographyClient cryptographyClient = new CryptographyClient(GetKey(keyIdentifierUri).Id, TokenCredential); + cryptoClientDictionary[keyIdentifierUri] = cryptographyClient; + + return cryptographyClient; + } + + /// + /// + /// + /// The Azure Key Vault URI + /// The name of the Azure Key Vault key + /// The Azure Key Vault key identifier + private void FetchKey(Uri vaultUri, string keyName, string keyResourceUri) + { + var fetchKeyTask = FetchKeyFromKeyVault(vaultUri, keyName); + _keyFetchTaskDictionary[keyResourceUri] = fetchKeyTask; + + fetchKeyTask + .ContinueWith(k => ValidateRsaKey(k.Result)) + .ContinueWith(k => _keyDictionary[keyResourceUri] = k.Result); + + Task.Run(() => fetchKeyTask); + } + + /// + /// Looks up the KeyClient object by it's URI and then fethces the key by name. + /// + /// The Azure Key Vault URI + /// Then name of the key + /// + private Task> FetchKeyFromKeyVault(Uri vaultUri, string keyName) => keyClientDictionary[vaultUri].GetKeyAsync(keyName); + + /// + /// Validates that a key is of type RSA + /// + /// + /// + private KeyVaultKey ValidateRsaKey(KeyVaultKey key) + { + if (key.KeyType != KeyType.Rsa && key.KeyType != KeyType.RsaHsm) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, Strings.NonRsaKeyTemplate, key.KeyType)); + } + + return key; + } + + /// + /// Instantiates and adds a KeyClient to the KeyClient dictionary + /// + /// The Azure Key Vault URI + private void CreateKeyClient(Uri vaultUri) + { + if (!keyClientDictionary.ContainsKey(vaultUri)) + { + keyClientDictionary[vaultUri] = new KeyClient(vaultUri, TokenCredential); + } + } + + /// + /// Validates and parses the Azure Key Vault URI and key name. + /// + /// The Azure Key Vault key identifier + /// The Azure Key Vault URI + /// The name of the key + private void ParseAKVPath(string masterKeyPath, out Uri vaultUri, out string masterKeyName) + { + Uri masterKeyPathUri = new Uri(masterKeyPath); + vaultUri = new Uri(masterKeyPathUri.GetLeftPart(UriPartial.Authority)); + masterKeyName = masterKeyPathUri.Segments[2]; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj index 9a32df9809..741018266e 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj @@ -22,9 +22,7 @@ - - - - + + diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index ef1758bf8e..a5d192cbac 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -3,17 +3,15 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; -using Microsoft.Data.SqlClient; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; -using Microsoft.Azure.KeyVault; -using Microsoft.Azure.KeyVault.WebKey; -using Microsoft.Azure.KeyVault.Models; +using Azure.Core; +using Azure.Security.KeyVault.Keys.Cryptography; +using static Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.Validator; namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider { @@ -61,18 +59,16 @@ public class SqlColumnEncryptionAzureKeyVaultProvider : SqlColumnEncryptionKeySt public const string ProviderName = "AZURE_KEY_VAULT"; /// - /// Algorithm version + /// Key storage and cryptography client /// - private readonly byte[] firstVersion = new byte[] { 0x01 }; + private AzureSqlKeyCryptographer KeyCryptographer { get; set; } /// - /// Azure Key Vault Client + /// Algorithm version /// - public KeyVaultClient KeyVaultClient - { - get; - private set; - } + private readonly static byte[] s_firstVersion = new byte[] { 0x01 }; + + private readonly static KeyWrapAlgorithm s_keyWrapAlgorithm = KeyWrapAlgorithm.RsaOaep; /// /// List of Trusted Endpoints @@ -87,7 +83,7 @@ public KeyVaultClient KeyVaultClient /// to authenticate to Azure Key Vault. /// /// Callback function used for authenticating to AAD. - public SqlColumnEncryptionAzureKeyVaultProvider(KeyVaultClient.AuthenticationCallback authenticationCallback) : + public SqlColumnEncryptionAzureKeyVaultProvider(AuthenticationCallback authenticationCallback) : this(authenticationCallback, Constants.AzureKeyVaultPublicDomainNames) { } @@ -96,7 +92,7 @@ public SqlColumnEncryptionAzureKeyVaultProvider(KeyVaultClient.AuthenticationCal /// /// Callback function used for authenticating to AAD. /// TrustedEndpoint is used to validate the master key path - public SqlColumnEncryptionAzureKeyVaultProvider(KeyVaultClient.AuthenticationCallback authenticationCallback, string trustedEndPoint) : + public SqlColumnEncryptionAzureKeyVaultProvider(AuthenticationCallback authenticationCallback, string trustedEndPoint) : this(authenticationCallback, new[] { trustedEndPoint }) { } @@ -106,28 +102,51 @@ public SqlColumnEncryptionAzureKeyVaultProvider(KeyVaultClient.AuthenticationCal /// /// Callback function used for authenticating to AAD. /// TrustedEndpoints are used to validate the master key path - public SqlColumnEncryptionAzureKeyVaultProvider(KeyVaultClient.AuthenticationCallback authenticationCallback, string[] trustedEndPoints) + public SqlColumnEncryptionAzureKeyVaultProvider(AuthenticationCallback authenticationCallback, string[] trustedEndPoints) { - if (authenticationCallback == null) - { - throw new ArgumentNullException("authenticationCallback"); - } + ValidateNotNull(authenticationCallback, nameof(authenticationCallback)); + ValidateNotNull(trustedEndPoints, nameof(trustedEndPoints)); + ValidateNotEmpty(trustedEndPoints, nameof(trustedEndPoints)); + ValidateNotNullOrWhitespaceForEach(trustedEndPoints, nameof(trustedEndPoints)); - if (trustedEndPoints == null || trustedEndPoints.Length == 0) - { - throw new ArgumentException(Strings.InvalidTrustedEndpointsList); - } + KeyCryptographer = new AzureSqlKeyCryptographer(authenticationCallback); + TrustedEndPoints = trustedEndPoints; + } - foreach (string trustedEndPoint in trustedEndPoints) - { - if (String.IsNullOrWhiteSpace(trustedEndPoint)) - { - throw new ArgumentException(String.Format(Strings.InvalidTrustedEndpointTemplate, trustedEndPoint)); - } - } + // New constructors - KeyVaultClient = new KeyVaultClient(authenticationCallback); - this.TrustedEndPoints = trustedEndPoints; + /// + /// Constructor that takes an implementation of Token Credential that is capable of providing an OAuth Token. + /// + /// + public SqlColumnEncryptionAzureKeyVaultProvider(TokenCredential tokenCredential) : + this(tokenCredential, Constants.AzureKeyVaultPublicDomainNames) + { } + + /// + /// Constructor that takes an implementation of Token Credential that is capable of providing an OAuth Token and a trusted endpoint. + /// + /// Instance of an implementation of Token Credential that is capable of providing an OAuth Token. + /// TrustedEndpoint is used to validate the master key path. + public SqlColumnEncryptionAzureKeyVaultProvider(TokenCredential tokenCredential, string trustedEndPoint) : + this(tokenCredential, new[] { trustedEndPoint }) + { } + + /// + /// Constructor that takes an instance of an implementation of Token Credential that is capable of providing an OAuth Token + /// and an array of trusted endpoints. + /// + /// Instance of an implementation of Token Credential that is capable of providing an OAuth Token + /// TrustedEndpoints are used to validate the master key path + public SqlColumnEncryptionAzureKeyVaultProvider(TokenCredential tokenCredential, string[] trustedEndPoints) + { + ValidateNotNull(tokenCredential, nameof(tokenCredential)); + ValidateNotNull(trustedEndPoints, nameof(trustedEndPoints)); + ValidateNotEmpty(trustedEndPoints, nameof(trustedEndPoints)); + ValidateNotNullOrWhitespaceForEach(trustedEndPoints, nameof(trustedEndPoints)); + + KeyCryptographer = new AzureSqlKeyCryptographer(tokenCredential); + TrustedEndPoints = trustedEndPoints; } #region Public methods @@ -140,9 +159,12 @@ public SqlColumnEncryptionAzureKeyVaultProvider(KeyVaultClient.AuthenticationCal /// Encrypted column encryption key public override byte[] SignColumnMasterKeyMetadata(string masterKeyPath, bool allowEnclaveComputations) { - var hash = ComputeMasterKeyMetadataHash(masterKeyPath, allowEnclaveComputations, isSystemOp: false); - byte[] signedHash = AzureKeyVaultSignHashedData(hash, masterKeyPath); - return signedHash; + ValidateNonEmptyAKVPath(masterKeyPath, isSystemOp: false); + + // Also validates key is of RSA type. + KeyCryptographer.AddKey(masterKeyPath); + byte[] message = CompileMasterKeyMetadata(masterKeyPath, allowEnclaveComputations); + return KeyCryptographer.SignData(message, masterKeyPath); } /// @@ -154,8 +176,12 @@ public override byte[] SignColumnMasterKeyMetadata(string masterKeyPath, bool al /// Boolean indicating whether the master key metadata can be verified based on the provided signature public override bool VerifyColumnMasterKeyMetadata(string masterKeyPath, bool allowEnclaveComputations, byte[] signature) { - var hash = ComputeMasterKeyMetadataHash(masterKeyPath, allowEnclaveComputations, isSystemOp: true); - return AzureKeyVaultVerifySignature(hash, signature, masterKeyPath); + ValidateNonEmptyAKVPath(masterKeyPath, isSystemOp: true); + + // Also validates key is of RSA type. + KeyCryptographer.AddKey(masterKeyPath); + byte[] message = CompileMasterKeyMetadata(masterKeyPath, allowEnclaveComputations); + return KeyCryptographer.VerifyData(message, signature, masterKeyPath); } /// @@ -169,48 +195,25 @@ public override bool VerifyColumnMasterKeyMetadata(string masterKeyPath, bool al public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] encryptedColumnEncryptionKey) { // Validate the input parameters - this.ValidateNonEmptyAKVPath(masterKeyPath, isSystemOp: true); + ValidateNonEmptyAKVPath(masterKeyPath, isSystemOp: true); + ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: true); + ValidateNotNull(encryptedColumnEncryptionKey, nameof(encryptedColumnEncryptionKey)); + ValidateNotEmpty(encryptedColumnEncryptionKey, nameof(encryptedColumnEncryptionKey)); + ValidateVersionByte(encryptedColumnEncryptionKey[0], s_firstVersion[0]); - if (null == encryptedColumnEncryptionKey) - { - throw new ArgumentNullException(Constants.AeParamEncryptedCek, Strings.NullCekvInternal); - } + // Also validates whether the key is RSA one or not and then get the key size + KeyCryptographer.AddKey(masterKeyPath); - if (0 == encryptedColumnEncryptionKey.Length) - { - throw new ArgumentException(Strings.EmptyCekvInternal, Constants.AeParamEncryptedCek); - } - - // Validate encryptionAlgorithm - this.ValidateEncryptionAlgorithm(ref encryptionAlgorithm, isSystemOp: true); - - // Validate whether the key is RSA one or not and then get the key size - int keySizeInBytes = GetAKVKeySize(masterKeyPath); - - // Validate and decrypt the EncryptedColumnEncryptionKey - // Format is - // version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature - // - // keyPath is present in the encrypted column encryption key for identifying the original source of the asymmetric key pair and - // we will not validate it against the data contained in the CMK metadata (masterKeyPath). - - // Validate the version byte - if (encryptedColumnEncryptionKey[0] != firstVersion[0]) - { - throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, Strings.InvalidAlgorithmVersionTemplate, - encryptedColumnEncryptionKey[0].ToString(@"X2"), - firstVersion[0].ToString("X2")), - Constants.AeParamEncryptedCek); - } + int keySizeInBytes = KeyCryptographer.GetKeySize(masterKeyPath); // Get key path length - int currentIndex = firstVersion.Length; - UInt16 keyPathLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex); - currentIndex += sizeof(UInt16); + int currentIndex = s_firstVersion.Length; + ushort keyPathLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex); + currentIndex += sizeof(ushort); // Get ciphertext length - UInt16 cipherTextLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex); - currentIndex += sizeof(UInt16); + ushort cipherTextLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex); + currentIndex += sizeof(ushort); // Skip KeyPath // KeyPath exists only for troubleshooting purposes and doesnt need validation. @@ -219,7 +222,7 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e // validate the ciphertext length if (cipherTextLength != keySizeInBytes) { - throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, Strings.InvalidCiphertextLengthTemplate, + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidCiphertextLengthTemplate, cipherTextLength, keySizeInBytes, masterKeyPath), @@ -230,7 +233,7 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength; if (signatureLength != keySizeInBytes) { - throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureLengthTemplate, + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureLengthTemplate, signatureLength, keySizeInBytes, masterKeyPath), @@ -238,37 +241,28 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e } // Get ciphertext - byte[] cipherText = new byte[cipherTextLength]; - Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, cipherText, 0, cipherTextLength); + byte[] cipherText = encryptedColumnEncryptionKey.Skip(currentIndex).Take(cipherTextLength).ToArray(); currentIndex += cipherTextLength; // Get signature - byte[] signature = new byte[signatureLength]; - Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, signature, 0, signature.Length); + byte[] signature = encryptedColumnEncryptionKey.Skip(currentIndex).Take(signatureLength).ToArray(); // Compute the hash to validate the signature - byte[] hash; - using (SHA256 sha256 = SHA256.Create()) - { - sha256.TransformFinalBlock(encryptedColumnEncryptionKey, 0, encryptedColumnEncryptionKey.Length - signature.Length); - hash = sha256.Hash; - } + byte[] hash = encryptedColumnEncryptionKey.Take(encryptedColumnEncryptionKey.Length - signatureLength).ToArray(); if (null == hash) { throw new CryptographicException(Strings.NullHash); } - // Validate the signature - if (!AzureKeyVaultVerifySignature(hash, signature, masterKeyPath)) + if (!KeyCryptographer.VerifyData(hash, signature, masterKeyPath)) { - throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureTemplate, + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureTemplate, masterKeyPath), Constants.AeParamEncryptedCek); } - // Decrypt the CEK - return this.AzureKeyVaultUnWrap(masterKeyPath, encryptionAlgorithm, cipherText); + return KeyCryptographer.UnwrapKey(s_keyWrapAlgorithm, cipherText, masterKeyPath); } /// @@ -282,161 +276,57 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey) { // Validate the input parameters - this.ValidateNonEmptyAKVPath(masterKeyPath, isSystemOp: true); - - if (null == columnEncryptionKey) - { - throw new ArgumentNullException(Constants.AeParamColumnEncryptionKey, Strings.NullCek); - } - - if (0 == columnEncryptionKey.Length) - { - throw new ArgumentException(Strings.EmptyCek, Constants.AeParamColumnEncryptionKey); - } + ValidateNonEmptyAKVPath(masterKeyPath, isSystemOp: true); + ValidateNotNullOrWhitespace(encryptionAlgorithm, nameof(encryptionAlgorithm)); + ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: true); + ValidateNotNull(columnEncryptionKey, nameof(columnEncryptionKey)); + ValidateNotEmpty(columnEncryptionKey, nameof(columnEncryptionKey)); - // Validate encryptionAlgorithm - this.ValidateEncryptionAlgorithm(ref encryptionAlgorithm, isSystemOp: false); - - // Validate whether the key is RSA one or not and then get the key size - int keySizeInBytes = GetAKVKeySize(masterKeyPath); + // Also validates whether the key is RSA one or not and then get the key size + KeyCryptographer.AddKey(masterKeyPath); + int keySizeInBytes = KeyCryptographer.GetKeySize(masterKeyPath); // Construct the encryptedColumnEncryptionKey // Format is // version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature // // We currently only support one version - byte[] version = new byte[] { firstVersion[0] }; + byte[] version = new byte[] { s_firstVersion[0] }; // Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath byte[] masterKeyPathBytes = Encoding.Unicode.GetBytes(masterKeyPath.ToLowerInvariant()); - byte[] keyPathLength = BitConverter.GetBytes((Int16)masterKeyPathBytes.Length); + byte[] keyPathLength = BitConverter.GetBytes((short)masterKeyPathBytes.Length); // Encrypt the plain text - byte[] cipherText = this.AzureKeyVaultWrap(masterKeyPath, encryptionAlgorithm, columnEncryptionKey); - byte[] cipherTextLength = BitConverter.GetBytes((Int16)cipherText.Length); + byte[] cipherText = KeyCryptographer.WrapKey(s_keyWrapAlgorithm, columnEncryptionKey, masterKeyPath); + byte[] cipherTextLength = BitConverter.GetBytes((short)cipherText.Length); if (cipherText.Length != keySizeInBytes) { - throw new CryptographicException(Strings.CiphertextLengthMismatch); + throw new CryptographicException(Strings.CipherTextLengthMismatch); } // Compute hash // SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext) - byte[] hash; - using (SHA256 sha256 = SHA256.Create()) - { - sha256.TransformBlock(version, 0, version.Length, version, 0); - sha256.TransformBlock(keyPathLength, 0, keyPathLength.Length, keyPathLength, 0); - sha256.TransformBlock(cipherTextLength, 0, cipherTextLength.Length, cipherTextLength, 0); - sha256.TransformBlock(masterKeyPathBytes, 0, masterKeyPathBytes.Length, masterKeyPathBytes, 0); - sha256.TransformFinalBlock(cipherText, 0, cipherText.Length); - hash = sha256.Hash; - } + byte[] hash = version.Concat(keyPathLength).Concat(cipherTextLength).Concat(masterKeyPathBytes).Concat(cipherText).ToArray(); // Sign the hash - byte[] signedHash = AzureKeyVaultSignHashedData(hash, masterKeyPath); + byte[] signature = KeyCryptographer.SignData(hash, masterKeyPath); - if (signedHash.Length != keySizeInBytes) + if (signature.Length != keySizeInBytes) { throw new CryptographicException(Strings.HashLengthMismatch); } - if (!this.AzureKeyVaultVerifySignature(hash, signedHash, masterKeyPath)) - { - throw new CryptographicException(Strings.InvalidSignature); - } - - // Construct the encrypted column encryption key - // EncryptedColumnEncryptionKey = version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature - int encryptedColumnEncryptionKeyLength = version.Length + cipherTextLength.Length + keyPathLength.Length + cipherText.Length + masterKeyPathBytes.Length + signedHash.Length; - byte[] encryptedColumnEncryptionKey = new byte[encryptedColumnEncryptionKeyLength]; - - // Copy version byte - int currentIndex = 0; - Buffer.BlockCopy(version, 0, encryptedColumnEncryptionKey, currentIndex, version.Length); - currentIndex += version.Length; + ValidateSignature(masterKeyPath, hash, signature); - // Copy key path length - Buffer.BlockCopy(keyPathLength, 0, encryptedColumnEncryptionKey, currentIndex, keyPathLength.Length); - currentIndex += keyPathLength.Length; - - // Copy ciphertext length - Buffer.BlockCopy(cipherTextLength, 0, encryptedColumnEncryptionKey, currentIndex, cipherTextLength.Length); - currentIndex += cipherTextLength.Length; - - // Copy key path - Buffer.BlockCopy(masterKeyPathBytes, 0, encryptedColumnEncryptionKey, currentIndex, masterKeyPathBytes.Length); - currentIndex += masterKeyPathBytes.Length; - - // Copy ciphertext - Buffer.BlockCopy(cipherText, 0, encryptedColumnEncryptionKey, currentIndex, cipherText.Length); - currentIndex += cipherText.Length; - - // copy the signature - Buffer.BlockCopy(signedHash, 0, encryptedColumnEncryptionKey, currentIndex, signedHash.Length); - - return encryptedColumnEncryptionKey; + return hash.Concat(signature).ToArray(); } #endregion #region Private methods - /// - /// This function validates that the encryption algorithm is RSA_OAEP and if it is not, - /// then throws an exception - /// - /// Asymmetric key encryption algorithm - /// is the operation a system operation - private void ValidateEncryptionAlgorithm(ref string encryptionAlgorithm, bool isSystemOp) - { - // This validates that the encryption algorithm is RSA_OAEP - if (null == encryptionAlgorithm) - { - if (isSystemOp) - { - throw new ArgumentNullException(Constants.AeParamEncryptionAlgorithm, Strings.NullAlgorithmInternal); - } - else - { - throw new ArgumentNullException(Constants.AeParamEncryptionAlgorithm, Strings.NullAlgorithm); - } - } - - // Transform to standard format (dash instead of underscore) to support both "RSA_OAEP" and "RSA-OAEP" - if (encryptionAlgorithm.Equals("RSA_OAEP", StringComparison.OrdinalIgnoreCase)) - { - encryptionAlgorithm = JsonWebKeyEncryptionAlgorithm.RSAOAEP; - } - - if (String.Equals(encryptionAlgorithm, JsonWebKeyEncryptionAlgorithm.RSAOAEP, StringComparison.OrdinalIgnoreCase) != true) - { - throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, Strings.InvalidKeyAlgorithm, - encryptionAlgorithm, "RSA_OAEP' or 'RSA-OAEP"), // For supporting both algorithm formats. - Constants.AeParamEncryptionAlgorithm); - } - } - - private byte[] ComputeMasterKeyMetadataHash(string masterKeyPath, bool allowEnclaveComputations, bool isSystemOp) - { - // Validate the input parameters - ValidateNonEmptyAKVPath(masterKeyPath, isSystemOp); - - // Validate whether the key is RSA one or not and then get the key size - GetAKVKeySize(masterKeyPath); - - string masterkeyMetadata = ProviderName + masterKeyPath + allowEnclaveComputations; - byte[] masterkeyMetadataBytes = Encoding.Unicode.GetBytes(masterkeyMetadata.ToLowerInvariant()); - - // Compute hash - byte[] hash; - using (SHA256 sha256 = SHA256.Create()) - { - sha256.TransformFinalBlock(masterkeyMetadataBytes, 0, masterkeyMetadataBytes.Length); - hash = sha256.Hash; - } - return hash; - } /// /// Checks if the Azure Key Vault key path is Empty or Null (and raises exception if they are). @@ -444,11 +334,11 @@ private byte[] ComputeMasterKeyMetadataHash(string masterKeyPath, bool allowEncl internal void ValidateNonEmptyAKVPath(string masterKeyPath, bool isSystemOp) { // throw appropriate error if masterKeyPath is null or empty - if (String.IsNullOrWhiteSpace(masterKeyPath)) + if (string.IsNullOrWhiteSpace(masterKeyPath)) { string errorMessage = null == masterKeyPath ? Strings.NullAkvPath - : String.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvPathTemplate, masterKeyPath); + : string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvPathTemplate, masterKeyPath); if (isSystemOp) { @@ -458,17 +348,16 @@ internal void ValidateNonEmptyAKVPath(string masterKeyPath, bool isSystemOp) throw new ArgumentException(errorMessage, Constants.AeParamMasterKeyPath); } - Uri parsedUri; - if (!Uri.TryCreate(masterKeyPath, UriKind.Absolute, out parsedUri)) + if (!Uri.TryCreate(masterKeyPath, UriKind.Absolute, out Uri parsedUri)) { // Return an error indicating that the AKV url is invalid. - throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvUrlTemplate, masterKeyPath), Constants.AeParamMasterKeyPath); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvUrlTemplate, masterKeyPath), Constants.AeParamMasterKeyPath); } // A valid URI. // Check if it is pointing to trusted endpoint. - foreach (string trustedEndPoint in this.TrustedEndPoints) + foreach (string trustedEndPoint in TrustedEndPoints) { if (parsedUri.Host.EndsWith(trustedEndPoint, StringComparison.OrdinalIgnoreCase)) { @@ -477,100 +366,32 @@ internal void ValidateNonEmptyAKVPath(string masterKeyPath, bool isSystemOp) } // Return an error indicating that the AKV url is invalid. - throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvKeyPathTrustedTemplate, masterKeyPath, String.Join(", ", this.TrustedEndPoints.ToArray())), Constants.AeParamMasterKeyPath); + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvKeyPathTrustedTemplate, masterKeyPath, string.Join(", ", TrustedEndPoints.ToArray())), Constants.AeParamMasterKeyPath); } - /// - /// Encrypt the text using specified Azure Key Vault key. - /// - /// Azure Key Vault key url. - /// Encryption Algorithm. - /// Plain text Column Encryption Key. - /// Returns an encrypted blob or throws an exception if there are any errors. - private byte[] AzureKeyVaultWrap(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey) + private void ValidateSignature(string masterKeyPath, byte[] message, byte[] signature) { - if (null == columnEncryptionKey) + if (!KeyCryptographer.VerifyData(message, signature, masterKeyPath)) { - throw new ArgumentNullException("columnEncryptionKey"); - } - - var wrappedKey = Task.Run(() => KeyVaultClient.WrapKeyAsync(masterKeyPath, encryptionAlgorithm, columnEncryptionKey)).Result; - return wrappedKey.Result; - } - - /// - /// Encrypt the text using specified Azure Key Vault key. - /// - /// Azure Key Vault key url. - /// Encryption Algorithm. - /// Encrypted Column Encryption Key. - /// Returns the decrypted plaintext Column Encryption Key or throws an exception if there are any errors. - private byte[] AzureKeyVaultUnWrap(string masterKeyPath, string encryptionAlgorithm, byte[] encryptedColumnEncryptionKey) - { - if (null == encryptedColumnEncryptionKey) - { - throw new ArgumentNullException("encryptedColumnEncryptionKey"); - } - - if (0 == encryptedColumnEncryptionKey.Length) - { - throw new ArgumentException(Strings.EncryptedCekEmpty); + throw new CryptographicException(Strings.InvalidSignature); } - - - var unwrappedKey = Task.Run(() => KeyVaultClient.UnwrapKeyAsync(masterKeyPath, encryptionAlgorithm, encryptedColumnEncryptionKey)).Result; - - return unwrappedKey.Result; - } - - /// - /// Generates signature based on RSA PKCS#v1.5 scheme using a specified Azure Key Vault Key URL. - /// - /// Text to sign. - /// Azure Key Vault key url. - /// Signature - private byte[] AzureKeyVaultSignHashedData(byte[] dataToSign, string masterKeyPath) - { - Debug.Assert((dataToSign != null) && (dataToSign.Length != 0)); - - var signedData = Task.Run(() => KeyVaultClient.SignAsync(masterKeyPath, Constants.HashingAlgorithm, dataToSign)).Result; - - return signedData.Result; } - /// - /// Verifies the given RSA PKCSv1.5 signature. - /// - /// - /// - /// Azure Key Vault key url. - /// true if signature is valid, false if it is not valid - private bool AzureKeyVaultVerifySignature(byte[] dataToVerify, byte[] signature, string masterKeyPath) - { - Debug.Assert((dataToVerify != null) && (dataToVerify.Length != 0)); - Debug.Assert((signature != null) && (signature.Length != 0)); - - return Task.Run(() => KeyVaultClient.VerifyAsync(masterKeyPath, Constants.HashingAlgorithm, dataToVerify, signature)).Result; - } - - /// - /// Gets the public Key size in bytes - /// - /// Azure Key Vault Key path - /// Key size in bytes - private int GetAKVKeySize(string masterKeyPath) + private byte[] CompileMasterKeyMetadata(string masterKeyPath, bool allowEnclaveComputations) { - KeyBundle retrievedKey = Task.Run(() => KeyVaultClient.GetKeyAsync(masterKeyPath)).Result; - - if (!String.Equals(retrievedKey.Key.Kty, JsonWebKeyType.Rsa, StringComparison.InvariantCultureIgnoreCase) && - !String.Equals(retrievedKey.Key.Kty, JsonWebKeyType.RsaHsm, StringComparison.InvariantCultureIgnoreCase)) - { - throw new Exception(String.Format(CultureInfo.InvariantCulture, Strings.NonRsaKeyTemplate, retrievedKey.Key.Kty)); - } - - return retrievedKey.Key.N.Length; + string masterkeyMetadata = ProviderName + masterKeyPath + allowEnclaveComputations; + return Encoding.Unicode.GetBytes(masterkeyMetadata.ToLowerInvariant()); } #endregion } + + /// + /// The authentication callback delegate which is to be implemented by the client code + /// + /// Identifier of the authority, a URL. + /// Identifier of the target resource that is the recipient of the requested token, a URL. + /// The scope of the authentication request. + /// access token + public delegate Task AuthenticationCallback(string authority, string resource, string scope); } diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs index a725197027..97e15ebd56 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs @@ -71,55 +71,33 @@ internal Strings() /// /// Looks up a localized string similar to CipherText length does not match the RSA key size.. /// - internal static string CiphertextLengthMismatch + internal static string CipherTextLengthMismatch { get { - return ResourceManager.GetString("CiphertextLengthMismatch", resourceCulture); + return ResourceManager.GetString("CipherTextLengthMismatch", resourceCulture); } } /// - /// Looks up a localized string similar to Empty column encryption key specified.. + /// Looks up a localized string similar to '{0}' cannot be null or empty or consist of only whitespace.. /// - internal static string EmptyCek + internal static string NullOrWhitespaceArgument { get { - return ResourceManager.GetString("EmptyCek", resourceCulture); + return ResourceManager.GetString("NullOrWhitespaceArgument", resourceCulture); } } /// - /// Looks up a localized string similar to Empty encrypted column encryption key specified.. + /// Looks up a localized string similar to Internal error. Empty '{0}' specified.. /// - internal static string EmptyCekv + internal static string EmptyArgumentInternal { get { - return ResourceManager.GetString("EmptyCekv", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Internal error: Empty encrypted column encryption key specified.. - /// - internal static string EmptyCekvInternal - { - get - { - return ResourceManager.GetString("EmptyCekvInternal", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to encryptedColumnEncryptionKey length should not be zero.. - /// - internal static string EncryptedCekEmpty - { - get - { - return ResourceManager.GetString("EncryptedCekEmpty", resourceCulture); + return ResourceManager.GetString("EmptyArgumentInternal", resourceCulture); } } @@ -233,17 +211,6 @@ internal static string InvalidSignatureTemplate } } - /// - /// Looks up a localized string similar to trustedEndPoints cannot be null or empty.. - /// - internal static string InvalidTrustedEndpointsList - { - get - { - return ResourceManager.GetString("InvalidTrustedEndpointsList", resourceCulture); - } - } - /// /// Looks up a localized string similar to Invalid trusted endpoint specified: '{0}'; a trusted endpoint must have a value.. /// @@ -299,39 +266,6 @@ internal static string NullAlgorithmInternal } } - /// - /// Looks up a localized string similar to Column encryption key cannot be null.. - /// - internal static string NullCek - { - get - { - return ResourceManager.GetString("NullCek", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Encrypted column encryption key cannot be null.. - /// - internal static string NullCekv - { - get - { - return ResourceManager.GetString("NullCekv", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Internal error: Encrypted column encryption key cannot be null.. - /// - internal static string NullCekvInternal - { - get - { - return ResourceManager.GetString("NullCekvInternal", resourceCulture); - } - } - /// /// Looks up a localized string similar to Hash should not be null while decrypting encrypted column encryption key.. /// diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx index 7d4e2e6db3..7446876a52 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx @@ -117,23 +117,17 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - trustedEndPoints cannot be null or empty. - Invalid trusted endpoint specified: '{0}'; a trusted endpoint must have a value. - + CipherText length does not match the RSA key size. - - Empty column encryption key specified. - - - Empty encrypted column encryption key specified. + + {0} cannot be null or empty or consist of only whitespace. - - encryptedColumnEncryptionKey length should not be zero. + + Internal error. Empty {0} specified. Signed hash length does not match the RSA key size. @@ -174,22 +168,10 @@ Key encryption algorithm cannot be null. - - Column encryption key cannot be null. - - - Encrypted column encryption key cannot be null. - Hash should not be null while decrypting encrypted column encryption key. - - Internal error. Empty encrypted column encryption key specified. - Internal error. Key encryption algorithm cannot be null. - - Internal error. Encrypted column encryption key cannot be null. - diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs new file mode 100644 index 0000000000..cca06fdea6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs @@ -0,0 +1,93 @@ +// 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; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; + +namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider +{ + internal static class Validator + { + internal static void ValidateNotNull(object parameter, string name) + { + if (null == parameter) + { + throw new ArgumentNullException(name); + } + } + + internal static void ValidateNotNullOrWhitespace(string parameter, string name) + { + if (string.IsNullOrWhiteSpace(parameter)) + { + throw new ArgumentException(name, Strings.NullOrWhitespaceArgument); + } + } + + internal static void ValidateNotEmpty(IList parameter, string name) + { + if (parameter.Count == 0) + { + throw new ArgumentException(name, Strings.EmptyArgumentInternal); + } + } + + internal static void ValidateNotNullOrWhitespaceForEach(string[] parameters, string name) + { + foreach (var parameter in parameters) + { + if (null == parameter) + { + throw new ArgumentException(parameter, Strings.InvalidTrustedEndpointTemplate); + } + } + } + + internal static void ValidateEncryptionAlgorithm(string encryptionAlgorithm, bool isSystemOp) + { + // This validates that the encryption algorithm is RSA_OAEP + if (null == encryptionAlgorithm) + { + if (isSystemOp) + { + throw new ArgumentNullException(Constants.AeParamEncryptionAlgorithm, Strings.NullAlgorithmInternal); + } + else + { + throw new ArgumentNullException(Constants.AeParamEncryptionAlgorithm, Strings.NullAlgorithm); + } + } + + if (!encryptionAlgorithm.Equals("RSA_OAEP", StringComparison.OrdinalIgnoreCase) + && !encryptionAlgorithm.Equals("RSA-OAEP", StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidKeyAlgorithm, + encryptionAlgorithm, "RSA_OAEP' or 'RSA-OAEP"), // For supporting both algorithm formats. + Constants.AeParamEncryptionAlgorithm); + } + } + + internal static void ValidateVersionByte(byte encryptedByte, byte firstVersionByte) + { + // Validate and decrypt the EncryptedColumnEncryptionKey + // Format is + // version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature + // + // keyPath is present in the encrypted column encryption key for identifying the original source of the asymmetric key pair and + // we will not validate it against the data contained in the CMK metadata (masterKeyPath). + + // Validate the version byte + if (encryptedByte != firstVersionByte) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAlgorithmVersionTemplate, + encryptedByte.ToString(@"X2"), + firstVersionByte.ToString("X2")), + Constants.AeParamEncryptedCek); + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs new file mode 100644 index 0000000000..7d117bc4ca --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; +using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Azure.Identity; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted +{ + public class AKVUnitTests + { + const string EncryptionAlgorithm = "RSA_OAEP"; + public static readonly byte[] ColumnEncryptionKey = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 }; + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] + public void BackwardCompatibilityWithAuthenticationCallbackWorks() + { + SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(AzureActiveDirectoryAuthenticationCallback); + byte[] encryptedCek = akvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); + byte[] decryptedCek = akvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCek); + + Assert.Equal(ColumnEncryptionKey, decryptedCek); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] + public void TokenCredentialWorks() + { + ClientSecretCredential clientSecretCredential = new ClientSecretCredential(DataTestUtility.AKVTenantId, DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); + SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); + byte[] encryptedCek = akvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); + byte[] decryptedCek = akvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCek); + + Assert.Equal(ColumnEncryptionKey, decryptedCek); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] + public void IsCompatibleWithProviderUsingLegacyClient() + { + ClientSecretCredential clientSecretCredential = new ClientSecretCredential(DataTestUtility.AKVTenantId, DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); + SqlColumnEncryptionAzureKeyVaultProvider newAkvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); + SqlColumnEncryptionAzureKeyVaultProvider oldAkvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(AzureActiveDirectoryAuthenticationCallback); + + byte[] encryptedCekWithNewProvider = newAkvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); + byte[] decryptedCekWithOldProvider = oldAkvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCekWithNewProvider); + Assert.Equal(ColumnEncryptionKey, decryptedCekWithOldProvider); + + byte[] encryptedCekWithOldProvider = oldAkvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); + byte[] decryptedCekWithNewProvider = newAkvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCekWithOldProvider); + Assert.Equal(ColumnEncryptionKey, decryptedCekWithNewProvider); + } + + public static async Task AzureActiveDirectoryAuthenticationCallback(string authority, string resource, string scope) + { + var authContext = new AuthenticationContext(authority); + ClientCredential clientCred = new ClientCredential(DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); + AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred); + if (result == null) + { + throw new InvalidOperationException($"Failed to retrieve an access token for {resource}"); + } + + return result.AccessToken; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/CertificateUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/CertificateUtility.cs index 6a66bf487e..468d18ab10 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/CertificateUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/CertificateUtility.cs @@ -7,12 +7,13 @@ using System.Diagnostics; using System.Reflection; using System.Runtime.Caching; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; -using Microsoft.Azure.KeyVault; -using Microsoft.Azure.KeyVault.Models; -using Microsoft.Azure.KeyVault.WebKey; -using Microsoft.Rest.Azure; +using System.Threading.Tasks; +using Azure; +using Azure.Identity; +using Azure.Security.KeyVault.Keys; namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted { @@ -133,54 +134,48 @@ internal static X509Certificate2 CreateCertificate() if (DataTestUtility.IsAKVSetupAvailable()) { - KeyVaultClient keyVaultClient = keyVaultClient = new KeyVaultClient(AADUtility.AzureActiveDirectoryAuthenticationCallback); - IPage keys = keyVaultClient.GetKeysAsync(DataTestUtility.AKVBaseUrl).Result; - bool testAKVKeyExists = false; - while (true) - { - foreach (KeyItem ki in keys) - { - if (ki.Identifier.Name.Equals(DataTestUtility.AKVKeyName)) - { - testAKVKeyExists = true; - } - } + SetupAKVKeysAsync().Wait(); + } - if (!string.IsNullOrEmpty(keys.NextPageLink)) - { - keys = keyVaultClient.GetKeysNextAsync(keys.NextPageLink).Result; - } - else - { - break; - } - } + return certificate; + } - if (!testAKVKeyExists) + private static async Task SetupAKVKeysAsync() + { + ClientSecretCredential clientSecretCredential = new ClientSecretCredential(DataTestUtility.AKVTenantId, DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); + KeyClient keyClient = new KeyClient(DataTestUtility.AKVBaseUri, clientSecretCredential); + AsyncPageable keys = keyClient.GetPropertiesOfKeysAsync(); + IAsyncEnumerator enumerator = keys.GetAsyncEnumerator(); + + bool testAKVKeyExists = false; + try + { + while (await enumerator.MoveNextAsync()) { - RSAParameters p = certificate.GetRSAPrivateKey().ExportParameters(true); - KeyBundle kb = new KeyBundle() + KeyProperties keyProperties = enumerator.Current; + if (keyProperties.Name.Equals(DataTestUtility.AKVKeyName)) { - Key = new Azure.KeyVault.WebKey.JsonWebKey - { - Kty = JsonWebKeyType.Rsa, - D = p.D, - DP = p.DP, - DQ = p.DQ, - P = p.P, - Q = p.Q, - QI = p.InverseQ, - N = p.Modulus, - E = p.Exponent, - }, - }; - keyVaultClient.ImportKeyAsync(DataTestUtility.AKVBaseUrl, DataTestUtility.AKVKeyName, kb); + testAKVKeyExists = true; + } } } + finally + { + await enumerator.DisposeAsync(); + } - return certificate; + if (!testAKVKeyExists) + { + var rsaKeyOptions = new CreateRsaKeyOptions(DataTestUtility.AKVKeyName, hardwareProtected: false) + { + KeySize = 2048, + ExpiresOn = DateTimeOffset.Now.AddYears(1) + }; + keyClient.CreateRsaKey(rsaKeyOptions); + } } + /// /// Removes a certificate from the local certificate store (useful for test cleanup). /// diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs index 5eb02ffcd9..26e1660e48 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/ColumnMasterKey.cs @@ -3,9 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Data; -using System.Data.SqlTypes; -using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 1e8fa398c6..633edc4dd2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -32,6 +32,7 @@ public static class DataTestUtility public static readonly string AADAccessToken = null; public static readonly string AKVBaseUrl = null; public static readonly string AKVUrl = null; + public static readonly string AKVTenantId = null; public static readonly string AKVClientId = null; public static readonly string AKVClientSecret = null; public static List AEConnStrings = new List(); @@ -42,6 +43,7 @@ public static class DataTestUtility public static readonly bool SupportsLocalDb = false; public static readonly bool SupportsFileStream = false; public static readonly bool UseManagedSNIOnWindows = false; + public static Uri AKVBaseUri = null; public static readonly string DNSCachingConnString = null; public static readonly string DNSCachingServerCR = null; // this is for the control ring @@ -75,6 +77,7 @@ private class Config public string AzureKeyVaultURL = null; public string AzureKeyVaultClientId = null; public string AzureKeyVaultClientSecret = null; + public string AzureKeyVaultTenantId = null; public bool EnclaveEnabled = false; public bool TracingEnabled = false; public bool SupportsIntegratedSecurity = false; @@ -136,14 +139,14 @@ static DataTestUtility() } string url = c.AzureKeyVaultURL; - Uri AKVBaseUri = null; + if (!string.IsNullOrEmpty(url) && Uri.TryCreate(url, UriKind.Absolute, out AKVBaseUri)) { AKVBaseUri = new Uri(AKVBaseUri, "/"); AKVBaseUrl = AKVBaseUri.AbsoluteUri; AKVUrl = (new Uri(AKVBaseUri, $"/keys/{AKVKeyName}")).AbsoluteUri; } - + AKVTenantId = c.AzureKeyVaultTenantId; AKVClientId = c.AzureKeyVaultClientId; AKVClientSecret = c.AzureKeyVaultClientSecret; } @@ -318,7 +321,7 @@ public static bool IsNotAzureServer() public static bool IsAKVSetupAvailable() { - return !string.IsNullOrEmpty(AKVUrl) && !string.IsNullOrEmpty(AKVClientId) && !string.IsNullOrEmpty(AKVClientSecret); + return !string.IsNullOrEmpty(AKVUrl) && !string.IsNullOrEmpty(AKVClientId) && !string.IsNullOrEmpty(AKVClientSecret) && !string.IsNullOrEmpty(AKVTenantId); } public static bool IsUsingManagedSNI() => UseManagedSNIOnWindows; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 34bd1e6ead..3c55286028 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -60,6 +60,7 @@ Common\System\Collections\DictionaryExtensions.cs + @@ -282,6 +283,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/config.default.json b/src/Microsoft.Data.SqlClient/tests/ManualTests/config.default.json index 681c3b309d..17d0fcdc0f 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/config.default.json +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/config.default.json @@ -11,6 +11,7 @@ "AADServicePrincipalId": "", "AADServicePrincipalSecret": "", "AzureKeyVaultURL": "", + "AzureKeyVaultTenantId": "", "AzureKeyVaultClientId": "", "AzureKeyVaultClientSecret": "", "SupportsIntegratedSecurity": true, diff --git a/tools/props/Versions.props b/tools/props/Versions.props index 16c550dfd6..72fccd1a2a 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -44,13 +44,12 @@ - [3.0.4,4.0.0) - [3.0.4,4.0.0) - [2.3.20,3.0.0) - [3.3.19,4.0.0) + [1.2.2,2.0.0) + [4.0.3,5.0.0) + 1.1.1 3.1.1 5.2.6 15.9.0 From 5b637b7c480ae05574422e17e35cf07bd189d792 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Mon, 29 Jun 2020 19:22:17 -0700 Subject: [PATCH 02/17] Add net461 support, drops net46 support. --- .../add-ons/Directory.Build.props | 2 +- ...waysEncrypted.AzureKeyVaultProvider.nuspec | 28 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props b/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props index 4fc891665f..c895f8b3c9 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props +++ b/src/Microsoft.Data.SqlClient/add-ons/Directory.Build.props @@ -19,7 +19,7 @@ netcoreapp2.1 - net46 + net461 diff --git a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec index 62365bd9da..37556f44aa 100644 --- a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec +++ b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec @@ -24,35 +24,31 @@ Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyStoreProvider.SqlColumnEncrypti © Microsoft Corporation. All rights reserved. sqlclient microsoft.data.sqlclient azurekeyvaultprovider akvprovider alwaysencrypted - + - - - - + + - - - - + + - - + + - - - - - + + + + + From 7c0ecde80213424536e8538b71ee27ff2924ed09 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Mon, 6 Jul 2020 15:12:20 -0700 Subject: [PATCH 03/17] Adjust tests for AKV Upgrade --- .../Microsoft.Data.SqlClient.Tests.csproj | 5 +++- .../TestFixtures/Setup/CertificateUtility.cs | 9 +++++-- ....Data.SqlClient.ManualTesting.Tests.csproj | 24 ++++++++++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj index c0cdf6fd85..2b4726b1de 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj @@ -88,10 +88,13 @@ - + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/CertificateUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/CertificateUtility.cs index 468d18ab10..7e32a4605d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/CertificateUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/Setup/CertificateUtility.cs @@ -11,10 +11,11 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; +#if !NET46 using Azure; using Azure.Identity; using Azure.Security.KeyVault.Keys; - +#endif namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted { class CertificateUtility @@ -132,6 +133,10 @@ internal static X509Certificate2 CreateCertificate() } } +#if NET46 + return certificate; + } +#else if (DataTestUtility.IsAKVSetupAvailable()) { SetupAKVKeysAsync().Wait(); @@ -174,7 +179,7 @@ private static async Task SetupAKVKeysAsync() keyClient.CreateRsaKey(rsaKeyOptions); } } - +#endif /// /// Removes a certificate from the local certificate store (useful for test cleanup). diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 3c55286028..b26d8c12b2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -16,12 +16,19 @@ - + + + + + + + + + - @@ -32,14 +39,12 @@ - - @@ -47,7 +52,6 @@ - @@ -60,7 +64,6 @@ Common\System\Collections\DictionaryExtensions.cs - @@ -278,15 +281,18 @@ - - + + + - + + + From 6d7eb45ffd7da077370d119128a3961c38a17916 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Wed, 8 Jul 2020 13:54:15 -0700 Subject: [PATCH 04/17] Apply feedback comments --- .../AzureKeyVaultProviderTokenCredential.cs | 50 ++++++++++++++----- .../AzureSqlKeyCryptographer.cs | 9 ++-- ...qlColumnEncryptionAzureKeyVaultProvider.cs | 7 +-- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs index 39d6d41159..f4d91cc3c0 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs @@ -15,6 +15,8 @@ namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider { internal class AzureKeyVaultProviderTokenCredential : TokenCredential { + private const string Bearer = "Bearer "; + private AuthenticationCallback Callback { get; set; } private string Authority { get; set; } @@ -24,14 +26,39 @@ internal class AzureKeyVaultProviderTokenCredential : TokenCredential internal AzureKeyVaultProviderTokenCredential(AuthenticationCallback authenticationCallback, string masterKeyPath) { Callback = authenticationCallback; - HttpClient httpClient = new HttpClient(); - HttpResponseMessage response = httpClient.GetAsync(masterKeyPath).GetAwaiter().GetResult(); - string challenge = response?.Headers.WwwAuthenticate.FirstOrDefault()?.ToString(); - string trimmedChallenge = ValidateChallenge(challenge); - string[] pairs = trimmedChallenge.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); + using (HttpClient httpClient = new HttpClient()) + { + HttpResponseMessage response = httpClient.GetAsync(masterKeyPath).GetAwaiter().GetResult(); + string challenge = response?.Headers.WwwAuthenticate.FirstOrDefault()?.ToString(); + string trimmedChallenge = ValidateChallenge(challenge); + string[] pairs = trimmedChallenge.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + if (pairs != null && pairs.Length > 0) + { + for (int i = 0; i < pairs.Length; i++) + { + string[] pair = pairs[i]?.Split('='); + + if (pair.Length == 2) + { + string key = pair[0]?.Trim().Trim(new char[] { '\"' }); + string value = pair[1]?.Trim().Trim(new char[] { '\"' }); - Authority = pairs[0].Split('=')[1].Trim().Trim(new char[] { '\"' }); - Resource = pairs[1].Split('=')[1].Trim().Trim(new char[] { '\"' }); + if (!string.IsNullOrEmpty(key)) + { + if (key.Equals("authorization", StringComparison.InvariantCultureIgnoreCase)) + { + Authority = value; + } + else if (key.Equals("resource", StringComparison.InvariantCultureIgnoreCase)) + { + Resource = value; + } + } + } + } + } + } } public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) @@ -49,16 +76,15 @@ public override ValueTask GetTokenAsync(TokenRequestContext request private static string ValidateChallenge(string challenge) { - const string bearer = "Bearer "; if (string.IsNullOrEmpty(challenge)) - throw new ArgumentNullException("challenge"); + throw new ArgumentNullException(nameof(challenge)); string trimmedChallenge = challenge.Trim(); - if (!trimmedChallenge.StartsWith(bearer)) - throw new ArgumentException("Challenge is not Bearer", "challenge"); + if (!trimmedChallenge.StartsWith(Bearer)) + throw new ArgumentException("Challenge is not Bearer", nameof(challenge)); - return trimmedChallenge.Substring(bearer.Length); + return trimmedChallenge.Substring(Bearer.Length); } } } diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs index 45c1e44f01..96efda7a03 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs @@ -6,6 +6,7 @@ using Azure.Security.KeyVault.Keys; using Azure.Security.KeyVault.Keys.Cryptography; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; @@ -34,24 +35,24 @@ internal class AzureSqlKeyCryptographer /// /// A mapping of the KeyClient objects to the corresponding Azure Key Vault URI /// - private readonly Dictionary keyClientDictionary = new Dictionary(); + private readonly ConcurrentDictionary keyClientDictionary = new ConcurrentDictionary(); /// /// Holds references to the fetch key tasks and maps them to their corresponding Azure Key Vault Key Identifier (URI). /// These tasks will be used for returning the key in the event that the fetch task has not finished depositing the /// key into the key dictionary. /// - private readonly Dictionary>> _keyFetchTaskDictionary = new Dictionary>>(); + private readonly ConcurrentDictionary>> _keyFetchTaskDictionary = new ConcurrentDictionary>>(); /// /// Holds references to the Azure Key Vault keys and maps them to their corresponding Azure Key Vault Key Identifier (URI). /// - private readonly Dictionary _keyDictionary = new Dictionary(); + private readonly ConcurrentDictionary _keyDictionary = new ConcurrentDictionary(); /// /// Holds references to the Azure Key Vault CryptographyClient objects and maps them to their corresponding Azure Key Vault Key Identifier (URI). /// - private readonly Dictionary cryptoClientDictionary = new Dictionary(); + private readonly ConcurrentDictionary cryptoClientDictionary = new ConcurrentDictionary(); /// /// Constructs a new KeyCryptographer diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index a5d192cbac..96ba4106a7 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -288,10 +288,7 @@ public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string e // Construct the encryptedColumnEncryptionKey // Format is - // version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature - // - // We currently only support one version - byte[] version = new byte[] { s_firstVersion[0] }; + // s_firstVersion + keyPathLength + ciphertextLength + ciphertext + keyPath + signature // Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath byte[] masterKeyPathBytes = Encoding.Unicode.GetBytes(masterKeyPath.ToLowerInvariant()); @@ -308,7 +305,7 @@ public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string e // Compute hash // SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext) - byte[] hash = version.Concat(keyPathLength).Concat(cipherTextLength).Concat(masterKeyPathBytes).Concat(cipherText).ToArray(); + byte[] hash = s_firstVersion.Concat(keyPathLength).Concat(cipherTextLength).Concat(masterKeyPathBytes).Concat(cipherText).ToArray(); // Sign the hash byte[] signature = KeyCryptographer.SignData(hash, masterKeyPath); From 8b6d6aa0b36b6e0aeedf22d7f03eb45d1cc2b4b9 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Mon, 20 Jul 2020 13:27:08 -0700 Subject: [PATCH 05/17] Address review feedback + remove old constructors. --- .../AzureKeyVaultProviderTokenCredential.cs | 90 ------------------- .../AzureSqlKeyCryptographer.cs | 58 +++++------- .../AzureKeyVaultProvider/Constants.cs | 2 +- ...qlColumnEncryptionAzureKeyVaultProvider.cs | 59 +++--------- .../AlwaysEncrypted/AKVUnitTests.cs | 36 ++++++-- 5 files changed, 62 insertions(+), 183 deletions(-) delete mode 100644 src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs deleted file mode 100644 index f4d91cc3c0..0000000000 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureKeyVaultProviderTokenCredential.cs +++ /dev/null @@ -1,90 +0,0 @@ -// 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.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Azure.Core; - -namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider -{ - internal class AzureKeyVaultProviderTokenCredential : TokenCredential - { - private const string Bearer = "Bearer "; - - private AuthenticationCallback Callback { get; set; } - - private string Authority { get; set; } - - private string Resource { get; set; } - - internal AzureKeyVaultProviderTokenCredential(AuthenticationCallback authenticationCallback, string masterKeyPath) - { - Callback = authenticationCallback; - using (HttpClient httpClient = new HttpClient()) - { - HttpResponseMessage response = httpClient.GetAsync(masterKeyPath).GetAwaiter().GetResult(); - string challenge = response?.Headers.WwwAuthenticate.FirstOrDefault()?.ToString(); - string trimmedChallenge = ValidateChallenge(challenge); - string[] pairs = trimmedChallenge.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); - - if (pairs != null && pairs.Length > 0) - { - for (int i = 0; i < pairs.Length; i++) - { - string[] pair = pairs[i]?.Split('='); - - if (pair.Length == 2) - { - string key = pair[0]?.Trim().Trim(new char[] { '\"' }); - string value = pair[1]?.Trim().Trim(new char[] { '\"' }); - - if (!string.IsNullOrEmpty(key)) - { - if (key.Equals("authorization", StringComparison.InvariantCultureIgnoreCase)) - { - Authority = value; - } - else if (key.Equals("resource", StringComparison.InvariantCultureIgnoreCase)) - { - Resource = value; - } - } - } - } - } - } - } - - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - string token = Callback.Invoke(Authority, Resource, string.Empty).GetAwaiter().GetResult(); - return new AccessToken(token, DateTimeOffset.Now.AddHours(1)); - } - - public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - string token = Callback.Invoke(Authority, Resource, string.Empty).GetAwaiter().GetResult(); - Task task = Task.FromResult(new AccessToken(token, DateTimeOffset.Now.AddHours(1))); - return new ValueTask(task); - } - - private static string ValidateChallenge(string challenge) - { - if (string.IsNullOrEmpty(challenge)) - throw new ArgumentNullException(nameof(challenge)); - - string trimmedChallenge = challenge.Trim(); - - if (!trimmedChallenge.StartsWith(Bearer)) - throw new ArgumentException("Challenge is not Bearer", nameof(challenge)); - - return trimmedChallenge.Substring(Bearer.Length); - } - } -} diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs index 96efda7a03..fa99c44350 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs @@ -22,20 +22,10 @@ internal class AzureSqlKeyCryptographer /// private TokenCredential TokenCredential { get; set; } - /// - /// AuthenticationCallback to be used with the KeyClient for legacy support. - /// - private AuthenticationCallback AuthenticationCallback { get; set; } - - /// - /// A flag to determine whether to use AuthenticationCallback with the KeyClient for legacy support. - /// - private readonly bool isUsingLegacyAuthentication = false; - /// /// A mapping of the KeyClient objects to the corresponding Azure Key Vault URI /// - private readonly ConcurrentDictionary keyClientDictionary = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _keyClientDictionary = new ConcurrentDictionary(); /// /// Holds references to the fetch key tasks and maps them to their corresponding Azure Key Vault Key Identifier (URI). @@ -52,7 +42,7 @@ internal class AzureSqlKeyCryptographer /// /// Holds references to the Azure Key Vault CryptographyClient objects and maps them to their corresponding Azure Key Vault Key Identifier (URI). /// - private readonly ConcurrentDictionary cryptoClientDictionary = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _cryptoClientDictionary = new ConcurrentDictionary(); /// /// Constructs a new KeyCryptographer @@ -63,12 +53,6 @@ internal AzureSqlKeyCryptographer(TokenCredential tokenCredential) TokenCredential = tokenCredential; } - internal AzureSqlKeyCryptographer(AuthenticationCallback authenticationCallback) - { - AuthenticationCallback = authenticationCallback; - isUsingLegacyAuthentication = true; - } - /// /// Adds the key, specified by the Key Identifier URI, to the cache. /// @@ -77,11 +61,6 @@ internal void AddKey(string keyIdentifierUri) { if (TheKeyHasNotBeenCached(keyIdentifierUri)) { - if (isUsingLegacyAuthentication) - { - TokenCredential = new AzureKeyVaultProviderTokenCredential(AuthenticationCallback, keyIdentifierUri); - } - ParseAKVPath(keyIdentifierUri, out Uri vaultUri, out string keyName); CreateKeyClient(vaultUri); FetchKey(vaultUri, keyName, keyIdentifierUri); @@ -99,12 +78,14 @@ internal KeyVaultKey GetKey(string keyIdentifierUri) { if (_keyDictionary.ContainsKey(keyIdentifierUri)) { - return _keyDictionary[keyIdentifierUri]; + _keyDictionary.TryGetValue(keyIdentifierUri, out KeyVaultKey key); + return key; } if (_keyFetchTaskDictionary.ContainsKey(keyIdentifierUri)) { - return Task.Run(() => _keyFetchTaskDictionary[keyIdentifierUri]).Result; + _keyFetchTaskDictionary.TryGetValue(keyIdentifierUri, out Task> task); + return Task.Run(() => task).GetAwaiter().GetResult(); } // Not a public exception - not likely to occur. @@ -153,13 +134,14 @@ internal byte[] WrapKey(KeyWrapAlgorithm keyWrapAlgorithm, byte[] key, string ke private CryptographyClient GetCryptographyClient(string keyIdentifierUri) { - if (cryptoClientDictionary.ContainsKey(keyIdentifierUri)) + if (_cryptoClientDictionary.ContainsKey(keyIdentifierUri)) { - return cryptoClientDictionary[keyIdentifierUri]; + _cryptoClientDictionary.TryGetValue(keyIdentifierUri, out CryptographyClient client); + return client; } CryptographyClient cryptographyClient = new CryptographyClient(GetKey(keyIdentifierUri).Id, TokenCredential); - cryptoClientDictionary[keyIdentifierUri] = cryptographyClient; + _cryptoClientDictionary.AddOrUpdate(keyIdentifierUri, cryptographyClient, (k, v) => cryptographyClient); return cryptographyClient; } @@ -172,23 +154,27 @@ private CryptographyClient GetCryptographyClient(string keyIdentifierUri) /// The Azure Key Vault key identifier private void FetchKey(Uri vaultUri, string keyName, string keyResourceUri) { - var fetchKeyTask = FetchKeyFromKeyVault(vaultUri, keyName); - _keyFetchTaskDictionary[keyResourceUri] = fetchKeyTask; + Task> fetchKeyTask = FetchKeyFromKeyVault(vaultUri, keyName); + _keyFetchTaskDictionary.AddOrUpdate(keyResourceUri, fetchKeyTask, (k, v) => fetchKeyTask); fetchKeyTask - .ContinueWith(k => ValidateRsaKey(k.Result)) - .ContinueWith(k => _keyDictionary[keyResourceUri] = k.Result); + .ContinueWith(k => ValidateRsaKey(k.GetAwaiter().GetResult())) + .ContinueWith(k => _keyDictionary.AddOrUpdate(keyResourceUri, k.GetAwaiter().GetResult(), (key, v) => k.GetAwaiter().GetResult())); Task.Run(() => fetchKeyTask); } /// - /// Looks up the KeyClient object by it's URI and then fethces the key by name. + /// Looks up the KeyClient object by it's URI and then fetches the key by name. /// /// The Azure Key Vault URI /// Then name of the key /// - private Task> FetchKeyFromKeyVault(Uri vaultUri, string keyName) => keyClientDictionary[vaultUri].GetKeyAsync(keyName); + private Task> FetchKeyFromKeyVault(Uri vaultUri, string keyName) + { + _keyClientDictionary.TryGetValue(vaultUri, out KeyClient keyClient); + return keyClient.GetKeyAsync(keyName); + } /// /// Validates that a key is of type RSA @@ -211,9 +197,9 @@ private KeyVaultKey ValidateRsaKey(KeyVaultKey key) /// The Azure Key Vault URI private void CreateKeyClient(Uri vaultUri) { - if (!keyClientDictionary.ContainsKey(vaultUri)) + if (!_keyClientDictionary.ContainsKey(vaultUri)) { - keyClientDictionary[vaultUri] = new KeyClient(vaultUri, TokenCredential); + _keyClientDictionary.AddOrUpdate(vaultUri, new KeyClient(vaultUri, TokenCredential), (k, v) => new KeyClient(vaultUri, TokenCredential)); } } diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs index 0dde44a721..f3ea843576 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs @@ -28,7 +28,7 @@ internal static class Constants }; /// - /// Always Encrypted Param names for exec handling + /// Always Encrypted Parameter names for exec handling /// internal const string AeParamColumnEncryptionKey = "columnEncryptionKey"; internal const string AeParamEncryptionAlgorithm = "encryptionAlgorithm"; diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index 96ba4106a7..013c8e9cb0 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -78,43 +78,7 @@ public class SqlColumnEncryptionAzureKeyVaultProvider : SqlColumnEncryptionKeySt #endregion - /// - /// Constructor that takes a callback function to authenticate to AAD. This is used by KeyVaultClient at runtime - /// to authenticate to Azure Key Vault. - /// - /// Callback function used for authenticating to AAD. - public SqlColumnEncryptionAzureKeyVaultProvider(AuthenticationCallback authenticationCallback) : - this(authenticationCallback, Constants.AzureKeyVaultPublicDomainNames) - { } - - /// - /// Constructor that takes a callback function to authenticate to AAD and a trusted endpoint. - /// - /// Callback function used for authenticating to AAD. - /// TrustedEndpoint is used to validate the master key path - public SqlColumnEncryptionAzureKeyVaultProvider(AuthenticationCallback authenticationCallback, string trustedEndPoint) : - this(authenticationCallback, new[] { trustedEndPoint }) - { } - - /// - /// Constructor that takes a callback function to authenticate to AAD and an array of trusted endpoints. The callback function - /// is used by KeyVaultClient at runtime to authenticate to Azure Key Vault. - /// - /// Callback function used for authenticating to AAD. - /// TrustedEndpoints are used to validate the master key path - public SqlColumnEncryptionAzureKeyVaultProvider(AuthenticationCallback authenticationCallback, string[] trustedEndPoints) - { - ValidateNotNull(authenticationCallback, nameof(authenticationCallback)); - ValidateNotNull(trustedEndPoints, nameof(trustedEndPoints)); - ValidateNotEmpty(trustedEndPoints, nameof(trustedEndPoints)); - ValidateNotNullOrWhitespaceForEach(trustedEndPoints, nameof(trustedEndPoints)); - - KeyCryptographer = new AzureSqlKeyCryptographer(authenticationCallback); - TrustedEndPoints = trustedEndPoints; - } - - // New constructors - + #region Constructors /// /// Constructor that takes an implementation of Token Credential that is capable of providing an OAuth Token. /// @@ -148,6 +112,7 @@ public SqlColumnEncryptionAzureKeyVaultProvider(TokenCredential tokenCredential, KeyCryptographer = new AzureSqlKeyCryptographer(tokenCredential); TrustedEndPoints = trustedEndPoints; } + #endregion #region Public methods @@ -247,15 +212,15 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e // Get signature byte[] signature = encryptedColumnEncryptionKey.Skip(currentIndex).Take(signatureLength).ToArray(); - // Compute the hash to validate the signature - byte[] hash = encryptedColumnEncryptionKey.Take(encryptedColumnEncryptionKey.Length - signatureLength).ToArray(); + // Compute the message to validate the signature + byte[] message = encryptedColumnEncryptionKey.Take(encryptedColumnEncryptionKey.Length - signatureLength).ToArray(); - if (null == hash) + if (null == message) { throw new CryptographicException(Strings.NullHash); } - if (!KeyCryptographer.VerifyData(hash, signature, masterKeyPath)) + if (!KeyCryptographer.VerifyData(message, signature, masterKeyPath)) { throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureTemplate, masterKeyPath), @@ -303,21 +268,21 @@ public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string e throw new CryptographicException(Strings.CipherTextLengthMismatch); } - // Compute hash + // Compute message // SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext) - byte[] hash = s_firstVersion.Concat(keyPathLength).Concat(cipherTextLength).Concat(masterKeyPathBytes).Concat(cipherText).ToArray(); + byte[] message = s_firstVersion.Concat(keyPathLength).Concat(cipherTextLength).Concat(masterKeyPathBytes).Concat(cipherText).ToArray(); - // Sign the hash - byte[] signature = KeyCryptographer.SignData(hash, masterKeyPath); + // Sign the message + byte[] signature = KeyCryptographer.SignData(message, masterKeyPath); if (signature.Length != keySizeInBytes) { throw new CryptographicException(Strings.HashLengthMismatch); } - ValidateSignature(masterKeyPath, hash, signature); + ValidateSignature(masterKeyPath, message, signature); - return hash.Concat(signature).ToArray(); + return message.Concat(signature).ToArray(); } #endregion diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs index 7d117bc4ca..a7b4bf1d8c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs @@ -6,6 +6,10 @@ using Microsoft.IdentityModel.Clients.ActiveDirectory; using Azure.Identity; using Xunit; +using Azure.Core; +using System.Threading; +using System.Net.Http; +using System.Linq; namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted { @@ -17,7 +21,7 @@ public class AKVUnitTests [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] public void BackwardCompatibilityWithAuthenticationCallbackWorks() { - SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(AzureActiveDirectoryAuthenticationCallback); + SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new ChallengeBasedTokenCredential()); byte[] encryptedCek = akvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); byte[] decryptedCek = akvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCek); @@ -40,7 +44,7 @@ public void IsCompatibleWithProviderUsingLegacyClient() { ClientSecretCredential clientSecretCredential = new ClientSecretCredential(DataTestUtility.AKVTenantId, DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); SqlColumnEncryptionAzureKeyVaultProvider newAkvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); - SqlColumnEncryptionAzureKeyVaultProvider oldAkvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(AzureActiveDirectoryAuthenticationCallback); + SqlColumnEncryptionAzureKeyVaultProvider oldAkvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new ChallengeBasedTokenCredential()); byte[] encryptedCekWithNewProvider = newAkvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); byte[] decryptedCekWithOldProvider = oldAkvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCekWithNewProvider); @@ -51,17 +55,31 @@ public void IsCompatibleWithProviderUsingLegacyClient() Assert.Equal(ColumnEncryptionKey, decryptedCekWithNewProvider); } - public static async Task AzureActiveDirectoryAuthenticationCallback(string authority, string resource, string scope) + internal class ChallengeBasedTokenCredential : TokenCredential { - var authContext = new AuthenticationContext(authority); - ClientCredential clientCred = new ClientCredential(DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); - AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred); - if (result == null) + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) { - throw new InvalidOperationException($"Failed to retrieve an access token for {resource}"); + throw new NotImplementedException(); } - return result.AccessToken; + public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + private static string ValidateChallenge(string challenge) + { + string Bearer = "Bearer "; + if (string.IsNullOrEmpty(challenge)) + throw new ArgumentNullException(nameof(challenge)); + + string trimmedChallenge = challenge.Trim(); + + if (!trimmedChallenge.StartsWith(Bearer)) + throw new ArgumentException("Challenge is not Bearer", nameof(challenge)); + + return trimmedChallenge.Substring(Bearer.Length); + } } } } From 47f4a0fd14a7433e7541fa80387a0739610566d4 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 6 Aug 2020 19:14:47 -0700 Subject: [PATCH 06/17] Update design of AKV Provider with multi-user support example --- .../AzureKeyVaultProviderExample_2_0.cs | 248 ++++++++++++ .../AzureKeyVaultProviderLegacyExample_2_0.cs | 368 ++++++++++++++++++ .../AlwaysEncrypted/AKVUnitTests.cs | 71 +--- .../SQLSetupStrategyAzureKeyVault.cs | 2 +- .../ManualTests/DataCommon/AADUtility.cs | 26 -- .../SqlClientCustomTokenCredential.cs | 142 +++++++ ....Data.SqlClient.ManualTesting.Tests.csproj | 3 +- tools/props/Versions.props | 1 + 8 files changed, 781 insertions(+), 80 deletions(-) create mode 100644 doc/samples/AzureKeyVaultProviderExample_2_0.cs create mode 100644 doc/samples/AzureKeyVaultProviderLegacyExample_2_0.cs delete mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/AADUtility.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/SqlClientCustomTokenCredential.cs diff --git a/doc/samples/AzureKeyVaultProviderExample_2_0.cs b/doc/samples/AzureKeyVaultProviderExample_2_0.cs new file mode 100644 index 0000000000..1d56b5a35f --- /dev/null +++ b/doc/samples/AzureKeyVaultProviderExample_2_0.cs @@ -0,0 +1,248 @@ +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using Azure.Identity; +using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; + +namespace Microsoft.Data.SqlClient.Samples +{ + public class AzureKeyVaultProviderExample_2_0 + { + static readonly string s_algorithm = "RSA_OAEP"; + + // ********* Provide details here *********** + static readonly string s_akvUrl = "https://{KeyVaultName}.vault.azure.net/keys/{Key}/{KeyIdentifier}"; + static readonly string s_clientId = "{Application_Client_ID}"; + static readonly string s_clientSecret = "{Application_Client_Secret}"; + static readonly string s_tenantId = "{Azure_Key_Vault_Active_Directory_Tenant_Id}"; + static readonly string s_connectionString = "Server={Server}; Database={database}; Integrated Security=true; Column Encryption Setting=Enabled;"; + // ****************************************** + + public static void Main(string[] args) + { + // Initialize AKV provider + ClientSecretCredential clientSecretCredential = new ClientSecretCredential(s_tenantId, s_clientId, s_clientSecret); + SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); + + // Register AKV provider + SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customProviders: new Dictionary(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase) + { + { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, akvProvider} + }); + Console.WriteLine("AKV provider Registered"); + + // Create connection to database + using (SqlConnection sqlConnection = new SqlConnection(s_connectionString)) + { + string cmkName = "CMK_WITH_AKV"; + string cekName = "CEK_WITH_AKV"; + string tblName = "AKV_TEST_TABLE"; + + CustomerRecord customer = new CustomerRecord(1, @"Microsoft", @"Corporation"); + + try + { + sqlConnection.Open(); + + // Drop Objects if exists + dropObjects(sqlConnection, cmkName, cekName, tblName); + + // Create Column Master Key with AKV Url + createCMK(sqlConnection, cmkName); + Console.WriteLine("Column Master Key created."); + + // Create Column Encryption Key + createCEK(sqlConnection, cmkName, cekName, akvProvider); + Console.WriteLine("Column Encryption Key created."); + + // Create Table with Encrypted Columns + createTbl(sqlConnection, cekName, tblName); + Console.WriteLine("Table created with Encrypted columns."); + + // Insert Customer Record in table + insertData(sqlConnection, tblName, customer); + Console.WriteLine("Encryted data inserted."); + + // Read data from table + verifyData(sqlConnection, tblName, customer); + Console.WriteLine("Data validated successfully."); + } + finally + { + // Drop table and keys + dropObjects(sqlConnection, cmkName, cekName, tblName); + Console.WriteLine("Dropped Table, CEK and CMK"); + } + + Console.WriteLine("Completed AKV provider Sample."); + } + } + + private static void createCMK(SqlConnection sqlConnection, string cmkName) + { + string KeyStoreProviderName = SqlColumnEncryptionAzureKeyVaultProvider.ProviderName; + + string sql = + $@"CREATE COLUMN MASTER KEY [{cmkName}] + WITH ( + KEY_STORE_PROVIDER_NAME = N'{KeyStoreProviderName}', + KEY_PATH = N'{s_akvUrl}' + );"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static void createCEK(SqlConnection sqlConnection, string cmkName, string cekName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + string sql = + $@"CREATE COLUMN ENCRYPTION KEY [{cekName}] + WITH VALUES ( + COLUMN_MASTER_KEY = [{cmkName}], + ALGORITHM = '{s_algorithm}', + ENCRYPTED_VALUE = {GetEncryptedValue(sqlColumnEncryptionAzureKeyVaultProvider)} + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static string GetEncryptedValue(SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + byte[] plainTextColumnEncryptionKey = new byte[32]; + RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); + rngCsp.GetBytes(plainTextColumnEncryptionKey); + + byte[] encryptedColumnEncryptionKey = sqlColumnEncryptionAzureKeyVaultProvider.EncryptColumnEncryptionKey(s_akvUrl, s_algorithm, plainTextColumnEncryptionKey); + string EncryptedValue = string.Concat("0x", BitConverter.ToString(encryptedColumnEncryptionKey).Replace("-", string.Empty)); + return EncryptedValue; + } + + private static void createTbl(SqlConnection sqlConnection, string cekName, string tblName) + { + string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256"; + + string sql = + $@"CREATE TABLE [dbo].[{tblName}] + ( + [CustomerId] [int] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [FirstName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [LastName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}') + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static void insertData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + string insertSql = $"INSERT INTO [{tblName}] (CustomerId, FirstName, LastName) VALUES (@CustomerId, @FirstName, @LastName);"; + + using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction()) + using (SqlCommand sqlCommand = new SqlCommand(insertSql, + connection: sqlConnection, transaction: sqlTransaction, + columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) + { + sqlCommand.Parameters.AddWithValue(@"CustomerId", customer.Id); + sqlCommand.Parameters.AddWithValue(@"FirstName", customer.FirstName); + sqlCommand.Parameters.AddWithValue(@"LastName", customer.LastName); + + sqlCommand.ExecuteNonQuery(); + sqlTransaction.Commit(); + } + } + + private static void verifyData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + // Test INPUT parameter on an encrypted parameter + using (SqlCommand sqlCommand = new SqlCommand($"SELECT CustomerId, FirstName, LastName FROM [{tblName}] WHERE FirstName = @firstName", + sqlConnection)) + { + SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft"); + customerFirstParam.Direction = System.Data.ParameterDirection.Input; + customerFirstParam.ForceColumnEncryption = true; + + using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader()) + { + ValidateResultSet(sqlDataReader); + } + } + } + + private static void ValidateResultSet(SqlDataReader sqlDataReader) + { + Console.WriteLine(" * Row available: " + sqlDataReader.HasRows); + + while (sqlDataReader.Read()) + { + if (sqlDataReader.GetInt32(0) == 1) + { + Console.WriteLine(" * Employee Id received as sent: " + sqlDataReader.GetInt32(0)); + } + else + { + Console.WriteLine("Employee Id didn't match"); + } + + if (sqlDataReader.GetString(1) == @"Microsoft") + { + Console.WriteLine(" * Employee Firstname received as sent: " + sqlDataReader.GetString(1)); + } + else + { + Console.WriteLine("Employee FirstName didn't match."); + } + + if (sqlDataReader.GetString(2) == @"Corporation") + { + Console.WriteLine(" * Employee LastName received as sent: " + sqlDataReader.GetString(2)); + } + else + { + Console.WriteLine("Employee LastName didn't match."); + } + } + } + + private static void dropObjects(SqlConnection sqlConnection, string cmkName, string cekName, string tblName) + { + using (SqlCommand cmd = sqlConnection.CreateCommand()) + { + cmd.CommandText = $@"IF EXISTS (select * from sys.objects where name = '{tblName}') BEGIN DROP TABLE [{tblName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_encryption_keys where name = '{cekName}') BEGIN DROP COLUMN ENCRYPTION KEY [{cekName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_master_keys where name = '{cmkName}') BEGIN DROP COLUMN MASTER KEY [{cmkName}] END"; + cmd.ExecuteNonQuery(); + } + } + + private class CustomerRecord + { + internal int Id { get; set; } + internal string FirstName { get; set; } + internal string LastName { get; set; } + + public CustomerRecord(int id, string fName, string lName) + { + Id = id; + FirstName = fName; + LastName = lName; + } + } + } + + // + +} diff --git a/doc/samples/AzureKeyVaultProviderLegacyExample_2_0.cs b/doc/samples/AzureKeyVaultProviderLegacyExample_2_0.cs new file mode 100644 index 0000000000..e55e1f18c7 --- /dev/null +++ b/doc/samples/AzureKeyVaultProviderLegacyExample_2_0.cs @@ -0,0 +1,368 @@ +// +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using Azure.Identity; +using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; + +namespace Microsoft.Data.SqlClient.Samples +{ + public class AzureKeyVaultProviderLegacyExample_2_0 + { + const string s_algorithm = "RSA_OAEP"; + + // ********* Provide details here *********** + static readonly string s_akvUrl = "https://{KeyVaultName}.vault.azure.net/keys/{Key}/{KeyIdentifier}"; + static readonly string s_clientId = "{Application_Client_ID}"; + static readonly string s_clientSecret = "{Application_Client_Secret}"; + static readonly string s_connectionString = "Server={Server}; Database={database}; Integrated Security=true; Column Encryption Setting=Enabled;"; + // ****************************************** + + public static void Main() + { + // Initialize AKV provider + SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new LegacyAuthCallbackTokenCredential()); + + // Register AKV provider + SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customProviders: new Dictionary(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase) + { + { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, akvProvider} + }); + Console.WriteLine("AKV provider Registered"); + + // Create connection to database + using (SqlConnection sqlConnection = new SqlConnection(s_connectionString)) + { + string cmkName = "CMK_WITH_AKV"; + string cekName = "CEK_WITH_AKV"; + string tblName = "AKV_TEST_TABLE"; + + CustomerRecord customer = new CustomerRecord(1, @"Microsoft", @"Corporation"); + + try + { + sqlConnection.Open(); + + // Drop Objects if exists + dropObjects(sqlConnection, cmkName, cekName, tblName); + + // Create Column Master Key with AKV Url + createCMK(sqlConnection, cmkName); + Console.WriteLine("Column Master Key created."); + + // Create Column Encryption Key + createCEK(sqlConnection, cmkName, cekName, akvProvider); + Console.WriteLine("Column Encryption Key created."); + + // Create Table with Encrypted Columns + createTbl(sqlConnection, cekName, tblName); + Console.WriteLine("Table created with Encrypted columns."); + + // Insert Customer Record in table + insertData(sqlConnection, tblName, customer); + Console.WriteLine("Encryted data inserted."); + + // Read data from table + verifyData(sqlConnection, tblName, customer); + Console.WriteLine("Data validated successfully."); + } + finally + { + // Drop table and keys + dropObjects(sqlConnection, cmkName, cekName, tblName); + Console.WriteLine("Dropped Table, CEK and CMK"); + } + + Console.WriteLine("Completed AKV provider Sample."); + } + } + + private static void createCMK(SqlConnection sqlConnection, string cmkName) + { + string KeyStoreProviderName = SqlColumnEncryptionAzureKeyVaultProvider.ProviderName; + + string sql = + $@"CREATE COLUMN MASTER KEY [{cmkName}] + WITH ( + KEY_STORE_PROVIDER_NAME = N'{KeyStoreProviderName}', + KEY_PATH = N'{s_akvUrl}' + );"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static void createCEK(SqlConnection sqlConnection, string cmkName, string cekName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + string sql = + $@"CREATE COLUMN ENCRYPTION KEY [{cekName}] + WITH VALUES ( + COLUMN_MASTER_KEY = [{cmkName}], + ALGORITHM = '{s_algorithm}', + ENCRYPTED_VALUE = {GetEncryptedValue(sqlColumnEncryptionAzureKeyVaultProvider)} + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static string GetEncryptedValue(SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + byte[] plainTextColumnEncryptionKey = new byte[32]; + RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); + rngCsp.GetBytes(plainTextColumnEncryptionKey); + + byte[] encryptedColumnEncryptionKey = sqlColumnEncryptionAzureKeyVaultProvider.EncryptColumnEncryptionKey(s_akvUrl, s_algorithm, plainTextColumnEncryptionKey); + string EncryptedValue = string.Concat("0x", BitConverter.ToString(encryptedColumnEncryptionKey).Replace("-", string.Empty)); + return EncryptedValue; + } + + private static void createTbl(SqlConnection sqlConnection, string cekName, string tblName) + { + string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256"; + + string sql = + $@"CREATE TABLE [dbo].[{tblName}] + ( + [CustomerId] [int] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [FirstName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [LastName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = '{ColumnEncryptionAlgorithmName}') + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static void insertData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + string insertSql = $"INSERT INTO [{tblName}] (CustomerId, FirstName, LastName) VALUES (@CustomerId, @FirstName, @LastName);"; + + using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction()) + using (SqlCommand sqlCommand = new SqlCommand(insertSql, + connection: sqlConnection, transaction: sqlTransaction, + columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) + { + sqlCommand.Parameters.AddWithValue(@"CustomerId", customer.Id); + sqlCommand.Parameters.AddWithValue(@"FirstName", customer.FirstName); + sqlCommand.Parameters.AddWithValue(@"LastName", customer.LastName); + + sqlCommand.ExecuteNonQuery(); + sqlTransaction.Commit(); + } + } + + private static void verifyData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + // Test INPUT parameter on an encrypted parameter + using (SqlCommand sqlCommand = new SqlCommand($"SELECT CustomerId, FirstName, LastName FROM [{tblName}] WHERE FirstName = @firstName", + sqlConnection)) + { + SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft"); + customerFirstParam.Direction = System.Data.ParameterDirection.Input; + customerFirstParam.ForceColumnEncryption = true; + + using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader()) + { + ValidateResultSet(sqlDataReader); + } + } + } + + private static void ValidateResultSet(SqlDataReader sqlDataReader) + { + Console.WriteLine(" * Row available: " + sqlDataReader.HasRows); + + while (sqlDataReader.Read()) + { + if (sqlDataReader.GetInt32(0) == 1) + { + Console.WriteLine(" * Employee Id received as sent: " + sqlDataReader.GetInt32(0)); + } + else + { + Console.WriteLine("Employee Id didn't match"); + } + + if (sqlDataReader.GetString(1) == @"Microsoft") + { + Console.WriteLine(" * Employee Firstname received as sent: " + sqlDataReader.GetString(1)); + } + else + { + Console.WriteLine("Employee FirstName didn't match."); + } + + if (sqlDataReader.GetString(2) == @"Corporation") + { + Console.WriteLine(" * Employee LastName received as sent: " + sqlDataReader.GetString(2)); + } + else + { + Console.WriteLine("Employee LastName didn't match."); + } + } + } + + private static void dropObjects(SqlConnection sqlConnection, string cmkName, string cekName, string tblName) + { + using (SqlCommand cmd = sqlConnection.CreateCommand()) + { + cmd.CommandText = $@"IF EXISTS (select * from sys.objects where name = '{tblName}') BEGIN DROP TABLE [{tblName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_encryption_keys where name = '{cekName}') BEGIN DROP COLUMN ENCRYPTION KEY [{cekName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_master_keys where name = '{cmkName}') BEGIN DROP COLUMN MASTER KEY [{cmkName}] END"; + cmd.ExecuteNonQuery(); + } + } + + private class CustomerRecord + { + internal int Id { get; set; } + internal string FirstName { get; set; } + internal string LastName { get; set; } + + public CustomerRecord(int id, string fName, string lName) + { + Id = id; + FirstName = fName; + LastName = lName; + } + } + + private class LegacyAuthCallbackTokenCredential : TokenCredential + { + string _authority = ""; + string _resource = ""; + string _akvUrl = ""; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) => + AcquireTokenAsync().GetAwaiter().GetResult(); + + public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) => + await AcquireTokenAsync(); + + private async Task AcquireTokenAsync() + { + // Added to reduce HttpClient calls. + // For multi-user support, a better design can be implemented as needed. + if (_akvUrl != s_akvUrl) + { + using (HttpClient httpClient = new HttpClient()) + { + HttpResponseMessage response = await httpClient.GetAsync(s_akvUrl); + string challenge = response?.Headers.WwwAuthenticate.FirstOrDefault()?.ToString(); + string trimmedChallenge = ValidateChallenge(challenge); + string[] pairs = trimmedChallenge.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + if (pairs != null && pairs.Length > 0) + { + for (int i = 0; i < pairs.Length; i++) + { + string[] pair = pairs[i]?.Split('='); + + if (pair.Length == 2) + { + string key = pair[0]?.Trim().Trim(new char[] { '\"' }); + string value = pair[1]?.Trim().Trim(new char[] { '\"' }); + + if (!string.IsNullOrEmpty(key)) + { + if (key.Equals("authorization", StringComparison.InvariantCultureIgnoreCase)) + { + _authority = value; + } + else if (key.Equals("resource", StringComparison.InvariantCultureIgnoreCase)) + { + _resource = value; + } + } + } + } + } + } + _akvUrl = s_akvUrl; + } + + string strAccessToken = await AzureActiveDirectoryAuthenticationCallback(_authority, _resource); + DateTime expiryTime = InterceptAccessTokenForExpiry(strAccessToken); + return new AccessToken(strAccessToken, new DateTimeOffset(expiryTime)); + } + + private DateTime InterceptAccessTokenForExpiry(string accessToken) + { + if (null == accessToken) + { + throw new ArgumentNullException(accessToken); + } + + var jwtHandler = new JwtSecurityTokenHandler(); + var jwtOutput = string.Empty; + + // Check Token Format + if (!jwtHandler.CanReadToken(accessToken)) + throw new FormatException(accessToken); + + JwtSecurityToken token = jwtHandler.ReadJwtToken(accessToken); + + // Re-serialize the Token Headers to just Key and Values + var jwtHeader = JsonConvert.SerializeObject(token.Header.Select(h => new { h.Key, h.Value })); + jwtOutput = $"{{\r\n\"Header\":\r\n{JToken.Parse(jwtHeader)},"; + + // Re-serialize the Token Claims to just Type and Values + var jwtPayload = JsonConvert.SerializeObject(token.Claims.Select(c => new { c.Type, c.Value })); + jwtOutput += $"\r\n\"Payload\":\r\n{JToken.Parse(jwtPayload)}\r\n}}"; + + // Output the whole thing to pretty JSON object formatted. + string jToken = JToken.Parse(jwtOutput).ToString(Formatting.Indented); + JToken payload = JObject.Parse(jToken).GetValue("Payload"); + + return new DateTime(1970, 1, 1).AddSeconds((long)payload[4]["Value"]); + } + + private static string ValidateChallenge(string challenge) + { + string Bearer = "Bearer "; + if (string.IsNullOrEmpty(challenge)) + throw new ArgumentNullException(nameof(challenge)); + + string trimmedChallenge = challenge.Trim(); + + if (!trimmedChallenge.StartsWith(Bearer)) + throw new ArgumentException("Challenge is not Bearer", nameof(challenge)); + + return trimmedChallenge.Substring(Bearer.Length); + } + + /// + /// Legacy implementation of Authentication Callback, used by Azure Key Vault provider 1.0. + /// This can be leveraged to support multi-user authentication support in the same Azure Key Vault Provider. + /// + /// Authorization URL + /// Resource + /// + public static async Task AzureActiveDirectoryAuthenticationCallback(string authority, string resource) + { + var authContext = new AuthenticationContext(authority); + ClientCredential clientCred = new ClientCredential(s_clientId, s_clientSecret); + AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred); + if (result == null) + { + throw new InvalidOperationException($"Failed to retrieve an access token for {resource}"); + } + return result.AccessToken; + } + } + } +} +// diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs index a7b4bf1d8c..f7fd36fd8c 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs @@ -1,85 +1,52 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; -using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; using Azure.Identity; using Xunit; -using Azure.Core; -using System.Threading; -using System.Net.Http; -using System.Linq; namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted { - public class AKVUnitTests + public static class AKVUnitTests { const string EncryptionAlgorithm = "RSA_OAEP"; - public static readonly byte[] ColumnEncryptionKey = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 }; + public static readonly byte[] s_columnEncryptionKey = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 }; [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] - public void BackwardCompatibilityWithAuthenticationCallbackWorks() + public static void LegacyAuthenticationCallbackTest() { - SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new ChallengeBasedTokenCredential()); - byte[] encryptedCek = akvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); + // SqlClientCustomTokenCredential implements legacy authentication callback to request access token at client-side. + SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new SqlClientCustomTokenCredential()); + byte[] encryptedCek = akvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, s_columnEncryptionKey); byte[] decryptedCek = akvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCek); - Assert.Equal(ColumnEncryptionKey, decryptedCek); + Assert.Equal(s_columnEncryptionKey, decryptedCek); } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] - public void TokenCredentialWorks() + public static void TokenCredentialTest() { ClientSecretCredential clientSecretCredential = new ClientSecretCredential(DataTestUtility.AKVTenantId, DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); - byte[] encryptedCek = akvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); + byte[] encryptedCek = akvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, s_columnEncryptionKey); byte[] decryptedCek = akvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCek); - Assert.Equal(ColumnEncryptionKey, decryptedCek); + Assert.Equal(s_columnEncryptionKey, decryptedCek); } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] - public void IsCompatibleWithProviderUsingLegacyClient() + public static void TokenCredentialRotationTest() { + // SqlClientCustomTokenCredential implements legacy authentication callback to request access token at client-side. + SqlColumnEncryptionAzureKeyVaultProvider oldAkvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new SqlClientCustomTokenCredential()); + ClientSecretCredential clientSecretCredential = new ClientSecretCredential(DataTestUtility.AKVTenantId, DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); SqlColumnEncryptionAzureKeyVaultProvider newAkvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); - SqlColumnEncryptionAzureKeyVaultProvider oldAkvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new ChallengeBasedTokenCredential()); - byte[] encryptedCekWithNewProvider = newAkvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); + byte[] encryptedCekWithNewProvider = newAkvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, s_columnEncryptionKey); byte[] decryptedCekWithOldProvider = oldAkvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCekWithNewProvider); - Assert.Equal(ColumnEncryptionKey, decryptedCekWithOldProvider); + Assert.Equal(s_columnEncryptionKey, decryptedCekWithOldProvider); - byte[] encryptedCekWithOldProvider = oldAkvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, ColumnEncryptionKey); + byte[] encryptedCekWithOldProvider = oldAkvProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, s_columnEncryptionKey); byte[] decryptedCekWithNewProvider = newAkvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCekWithOldProvider); - Assert.Equal(ColumnEncryptionKey, decryptedCekWithNewProvider); - } - - internal class ChallengeBasedTokenCredential : TokenCredential - { - public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - private static string ValidateChallenge(string challenge) - { - string Bearer = "Bearer "; - if (string.IsNullOrEmpty(challenge)) - throw new ArgumentNullException(nameof(challenge)); - - string trimmedChallenge = challenge.Trim(); - - if (!trimmedChallenge.StartsWith(Bearer)) - throw new ArgumentException("Challenge is not Bearer", nameof(challenge)); - - return trimmedChallenge.Substring(Bearer.Length); - } + Assert.Equal(s_columnEncryptionKey, decryptedCekWithNewProvider); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyAzureKeyVault.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyAzureKeyVault.cs index 288e6f069a..a8eefa3969 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyAzureKeyVault.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/TestFixtures/SQLSetupStrategyAzureKeyVault.cs @@ -18,7 +18,7 @@ public class SQLSetupStrategyAzureKeyVault : SQLSetupStrategy public SQLSetupStrategyAzureKeyVault() : base() { - AkvStoreProvider = new SqlColumnEncryptionAzureKeyVaultProvider(authenticationCallback: AADUtility.AzureActiveDirectoryAuthenticationCallback); + AkvStoreProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new SqlClientCustomTokenCredential()); if (!isAKVProviderRegistered) { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/AADUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/AADUtility.cs deleted file mode 100644 index 5e437322fb..0000000000 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/AADUtility.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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.Threading.Tasks; -using Microsoft.IdentityModel.Clients.ActiveDirectory; - -namespace Microsoft.Data.SqlClient.ManualTesting.Tests -{ - public static class AADUtility - { - public static async Task AzureActiveDirectoryAuthenticationCallback(string authority, string resource, string scope) - { - var authContext = new AuthenticationContext(authority); - ClientCredential clientCred = new ClientCredential(DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); - AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred); - if (result == null) - { - throw new InvalidOperationException($"Failed to retrieve an access token for {resource}"); - } - - return result.AccessToken; - } - } -} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/SqlClientCustomTokenCredential.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/SqlClientCustomTokenCredential.cs new file mode 100644 index 0000000000..23ed76f81e --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/SqlClientCustomTokenCredential.cs @@ -0,0 +1,142 @@ +// 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.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class SqlClientCustomTokenCredential : TokenCredential + { + string _authority = ""; + string _resource = ""; + string _akvUrl = ""; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) => + AcquireTokenAsync().GetAwaiter().GetResult(); + + public override async ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) => + await AcquireTokenAsync(); + + private async Task AcquireTokenAsync() + { + // Added to reduce HttpClient calls. + // For multi-user support, a better design can be implemented as needed. + if (_akvUrl != DataTestUtility.AKVUrl) + { + using (HttpClient httpClient = new HttpClient()) + { + HttpResponseMessage response = await httpClient.GetAsync(DataTestUtility.AKVUrl); + string challenge = response?.Headers.WwwAuthenticate.FirstOrDefault()?.ToString(); + string trimmedChallenge = ValidateChallenge(challenge); + string[] pairs = trimmedChallenge.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries); + + if (pairs != null && pairs.Length > 0) + { + for (int i = 0; i < pairs.Length; i++) + { + string[] pair = pairs[i]?.Split('='); + + if (pair.Length == 2) + { + string key = pair[0]?.Trim().Trim(new char[] { '\"' }); + string value = pair[1]?.Trim().Trim(new char[] { '\"' }); + + if (!string.IsNullOrEmpty(key)) + { + if (key.Equals("authorization", StringComparison.InvariantCultureIgnoreCase)) + { + _authority = value; + } + else if (key.Equals("resource", StringComparison.InvariantCultureIgnoreCase)) + { + _resource = value; + } + } + } + } + } + } + // Since this is a test, we only create single-instance temp cache + _akvUrl = DataTestUtility.AKVUrl; + } + + string strAccessToken = await AzureActiveDirectoryAuthenticationCallback(_authority, _resource); + DateTime expiryTime = InterceptAccessTokenForExpiry(strAccessToken); + return new AccessToken(strAccessToken, new DateTimeOffset(expiryTime)); + } + + private DateTime InterceptAccessTokenForExpiry(string accessToken) + { + if (null == accessToken) + { + throw new ArgumentNullException(accessToken); + } + + var jwtHandler = new JwtSecurityTokenHandler(); + var jwtOutput = string.Empty; + + // Check Token Format + if (!jwtHandler.CanReadToken(accessToken)) + throw new FormatException(accessToken); + + JwtSecurityToken token = jwtHandler.ReadJwtToken(accessToken); + + // Re-serialize the Token Headers to just Key and Values + var jwtHeader = JsonConvert.SerializeObject(token.Header.Select(h => new { h.Key, h.Value })); + jwtOutput = $"{{\r\n\"Header\":\r\n{JToken.Parse(jwtHeader)},"; + + // Re-serialize the Token Claims to just Type and Values + var jwtPayload = JsonConvert.SerializeObject(token.Claims.Select(c => new { c.Type, c.Value })); + jwtOutput += $"\r\n\"Payload\":\r\n{JToken.Parse(jwtPayload)}\r\n}}"; + + // Output the whole thing to pretty JSON object formatted. + string jToken = JToken.Parse(jwtOutput).ToString(Formatting.Indented); + JToken payload = JObject.Parse(jToken).GetValue("Payload"); + + return new DateTime(1970, 1, 1).AddSeconds((long)payload[4]["Value"]); + } + + private static string ValidateChallenge(string challenge) + { + string Bearer = "Bearer "; + if (string.IsNullOrEmpty(challenge)) + throw new ArgumentNullException(nameof(challenge)); + + string trimmedChallenge = challenge.Trim(); + + if (!trimmedChallenge.StartsWith(Bearer)) + throw new ArgumentException("Challenge is not Bearer", nameof(challenge)); + + return trimmedChallenge.Substring(Bearer.Length); + } + + /// + /// Legacy implementation of Authentication Callback, used by Azure Key Vault provider 1.0. + /// This can be leveraged to support multi-user authentication support in the same Azure Key Vault Provider. + /// + /// Authorization URL + /// Resource + /// + public static async Task AzureActiveDirectoryAuthenticationCallback(string authority, string resource) + { + var authContext = new AuthenticationContext(authority); + ClientCredential clientCred = new ClientCredential(DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); + AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred); + if (result == null) + { + throw new InvalidOperationException($"Failed to retrieve an access token for {resource}"); + } + return result.AccessToken; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index b26d8c12b2..106ff17218 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -64,7 +64,7 @@ Common\System\Collections\DictionaryExtensions.cs - + @@ -297,6 +297,7 @@ + diff --git a/tools/props/Versions.props b/tools/props/Versions.props index 72fccd1a2a..e8e125db73 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -59,6 +59,7 @@ 4.5.0 4.6.0 4.3.0 + 5.6.0 2.4.1 5.0.0-beta.20206.4 2.0.8 From 8c9fefc62098f39e493a766919dd8fcf03c9e9da Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 6 Aug 2020 19:38:55 -0700 Subject: [PATCH 07/17] Minor edit --- .../Microsoft.Data.SqlClient.ManualTesting.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 106ff17218..7ac2eaa3c2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -18,6 +18,7 @@ + @@ -64,7 +65,6 @@ Common\System\Collections\DictionaryExtensions.cs - From 65d46969268886e34cd3789ab0665b2ff1cd39fd Mon Sep 17 00:00:00 2001 From: Johnny Pham <23270162+johnnypham@users.noreply.github.com> Date: Tue, 2 Feb 2021 14:13:59 -0800 Subject: [PATCH 08/17] fix key version bug --- .../AzureSqlKeyCryptographer.cs | 18 ++++--- ...qlColumnEncryptionAzureKeyVaultProvider.cs | 3 +- .../AlwaysEncrypted/AKVUnitTests.cs | 51 +++++++++++++++++++ .../ManualTests/DataCommon/DataTestUtility.cs | 9 ++-- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs index fa99c44350..a10e65c1cd 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs @@ -61,9 +61,9 @@ internal void AddKey(string keyIdentifierUri) { if (TheKeyHasNotBeenCached(keyIdentifierUri)) { - ParseAKVPath(keyIdentifierUri, out Uri vaultUri, out string keyName); + ParseAKVPath(keyIdentifierUri, out Uri vaultUri, out string keyName, out string keyVersion); CreateKeyClient(vaultUri); - FetchKey(vaultUri, keyName, keyIdentifierUri); + FetchKey(vaultUri, keyName, keyVersion, keyIdentifierUri); } bool TheKeyHasNotBeenCached(string k) => !_keyDictionary.ContainsKey(k) && !_keyFetchTaskDictionary.ContainsKey(k); @@ -151,10 +151,11 @@ private CryptographyClient GetCryptographyClient(string keyIdentifierUri) /// /// The Azure Key Vault URI /// The name of the Azure Key Vault key + /// The version of the Azure Key Vault key /// The Azure Key Vault key identifier - private void FetchKey(Uri vaultUri, string keyName, string keyResourceUri) + private void FetchKey(Uri vaultUri, string keyName, string keyVersion, string keyResourceUri) { - Task> fetchKeyTask = FetchKeyFromKeyVault(vaultUri, keyName); + Task> fetchKeyTask = FetchKeyFromKeyVault(vaultUri, keyName, keyVersion); _keyFetchTaskDictionary.AddOrUpdate(keyResourceUri, fetchKeyTask, (k, v) => fetchKeyTask); fetchKeyTask @@ -169,11 +170,12 @@ private void FetchKey(Uri vaultUri, string keyName, string keyResourceUri) /// /// The Azure Key Vault URI /// Then name of the key + /// Then version of the key /// - private Task> FetchKeyFromKeyVault(Uri vaultUri, string keyName) + private Task> FetchKeyFromKeyVault(Uri vaultUri, string keyName, string keyVersion) { _keyClientDictionary.TryGetValue(vaultUri, out KeyClient keyClient); - return keyClient.GetKeyAsync(keyName); + return keyClient.GetKeyAsync(keyName, keyVersion); } /// @@ -209,11 +211,13 @@ private void CreateKeyClient(Uri vaultUri) /// The Azure Key Vault key identifier /// The Azure Key Vault URI /// The name of the key - private void ParseAKVPath(string masterKeyPath, out Uri vaultUri, out string masterKeyName) + /// The version of the key + private void ParseAKVPath(string masterKeyPath, out Uri vaultUri, out string masterKeyName, out string masterKeyVersion) { Uri masterKeyPathUri = new Uri(masterKeyPath); vaultUri = new Uri(masterKeyPathUri.GetLeftPart(UriPartial.Authority)); masterKeyName = masterKeyPathUri.Segments[2]; + masterKeyVersion = masterKeyPathUri.Segments.Length > 3 ? masterKeyPathUri.Segments[3] : null; } } } diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index 013c8e9cb0..1a02edf163 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -310,8 +310,7 @@ internal void ValidateNonEmptyAKVPath(string masterKeyPath, bool isSystemOp) throw new ArgumentException(errorMessage, Constants.AeParamMasterKeyPath); } - - if (!Uri.TryCreate(masterKeyPath, UriKind.Absolute, out Uri parsedUri)) + if (!Uri.TryCreate(masterKeyPath, UriKind.Absolute, out Uri parsedUri) || parsedUri.Segments.Length < 3) { // Return an error indicating that the AKV url is invalid. throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvUrlTemplate, masterKeyPath), Constants.AeParamMasterKeyPath); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs index d58ee69281..c042339dc2 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs @@ -5,6 +5,10 @@ using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; using Azure.Identity; using Xunit; +using Azure.Security.KeyVault.Keys; +using Azure.Core; +using System.Reflection; +using System; namespace Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted { @@ -52,5 +56,52 @@ public static void TokenCredentialRotationTest() byte[] decryptedCekWithNewProvider = newAkvProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, EncryptionAlgorithm, encryptedCekWithOldProvider); Assert.Equal(s_columnEncryptionKey, decryptedCekWithNewProvider); } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] + public static void ReturnSpecifiedVersionOfKeyWhenItIsNotTheMostRecentVersion() + { + Uri keyPathUri = new Uri(DataTestUtility.AKVOriginalUrl); + Uri vaultUri = new Uri(keyPathUri.GetLeftPart(UriPartial.Authority)); + + //If key version is not specified then we cannot test. + if (KeyIsVersioned(keyPathUri)) + { + string keyName = keyPathUri.Segments[2]; + string keyVersion = keyPathUri.Segments[3]; + ClientSecretCredential clientSecretCredential = new ClientSecretCredential(DataTestUtility.AKVTenantId, DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); + KeyClient keyClient = new KeyClient(vaultUri, clientSecretCredential); + KeyVaultKey currentVersionKey = keyClient.GetKey(keyName); + KeyVaultKey specifiedVersionKey = keyClient.GetKey(keyName, keyVersion); + + //If specified versioned key is the most recent version of the key then we cannot test. + if (!KeyIsLatestVersion(specifiedVersionKey, currentVersionKey)) + { + SqlColumnEncryptionAzureKeyVaultProvider azureKeyProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); + // Perform an operation to initialize the internal caches + azureKeyProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVOriginalUrl, EncryptionAlgorithm, s_columnEncryptionKey); + + PropertyInfo keyCryptographerProperty = azureKeyProvider.GetType().GetProperty("KeyCryptographer", BindingFlags.NonPublic | BindingFlags.Instance); + var keyCryptographer = keyCryptographerProperty.GetValue(azureKeyProvider); + MethodInfo getKeyMethod = keyCryptographer.GetType().GetMethod("GetKey", BindingFlags.NonPublic | BindingFlags.Instance); + KeyVaultKey key = (KeyVaultKey)getKeyMethod.Invoke(keyCryptographer, new[] { DataTestUtility.AKVOriginalUrl }); + + Assert.Equal(keyVersion, key.Properties.Version); + } + } + } + + static bool KeyIsVersioned(Uri keyPath) => keyPath.Segments.Length > 3; + static bool KeyIsLatestVersion(KeyVaultKey specifiedVersionKey, KeyVaultKey currentVersionKey) => currentVersionKey.Properties.Version == specifiedVersionKey.Properties.Version; + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] + public static void ThrowWhenUrlHasLessThanThreeSegments() + { + SqlColumnEncryptionAzureKeyVaultProvider azureKeyProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new SqlClientCustomTokenCredential()); + string invalidKeyPath = "https://my-key-vault.vault.azure.net/keys"; + Exception ex1 = Assert.Throws(() => azureKeyProvider.EncryptColumnEncryptionKey(invalidKeyPath, EncryptionAlgorithm, s_columnEncryptionKey)); + Assert.Contains($"Invalid url specified: '{invalidKeyPath}'", ex1.Message); + Exception ex2 = Assert.Throws(() => azureKeyProvider.DecryptColumnEncryptionKey(invalidKeyPath, EncryptionAlgorithm, s_columnEncryptionKey)); + Assert.Contains($"Invalid url specified: '{invalidKeyPath}'", ex2.Message); + } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 0f9211ee3a..617f905ff7 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -15,6 +15,8 @@ using Microsoft.Identity.Client; using Microsoft.Data.SqlClient.TestUtilities; using Xunit; +using Azure.Security.KeyVault.Keys; +using Azure.Identity; namespace Microsoft.Data.SqlClient.ManualTesting.Tests { @@ -32,6 +34,7 @@ public static class DataTestUtility public static readonly string AADAccessToken = null; public static readonly string AKVBaseUrl = null; public static readonly string AKVUrl = null; + public static readonly string AKVOriginalUrl = null; public static readonly string AKVTenantId = null; public static readonly string AKVClientId = null; public static readonly string AKVClientSecret = null; @@ -103,14 +106,14 @@ static DataTestUtility() AADAccessToken = GenerateAccessToken(AADAuthorityURL, username, password); } - string url = c.AzureKeyVaultURL; - if (!string.IsNullOrEmpty(url) && Uri.TryCreate(url, UriKind.Absolute, out Uri AKVBaseUri)) + AKVOriginalUrl = c.AzureKeyVaultURL; + if (!string.IsNullOrEmpty(AKVOriginalUrl) && Uri.TryCreate(AKVOriginalUrl, UriKind.Absolute, out Uri AKVBaseUri)) { AKVBaseUri = new Uri(AKVBaseUri, "/"); AKVBaseUrl = AKVBaseUri.AbsoluteUri; AKVUrl = (new Uri(AKVBaseUri, $"/keys/{AKVKeyName}")).AbsoluteUri; } - + AKVTenantId = c.AzureKeyVaultTenantId; AKVClientId = c.AzureKeyVaultClientId; AKVClientSecret = c.AzureKeyVaultClientSecret; From 0c6c2f5c9f910320fc1cb9c84a9d013fc4a1808b Mon Sep 17 00:00:00 2001 From: Johnny Pham <23270162+johnnypham@users.noreply.github.com> Date: Thu, 4 Feb 2021 18:15:19 -0800 Subject: [PATCH 09/17] add enclave example using new akv. update public api comments --- ...oviderLegacyWithEnclaveProviderExample.cs} | 0 ...tProviderWithEnclaveProviderExample_2_0.cs | 253 ++++++++++++++++++ ...qlColumnEncryptionAzureKeyVaultProvider.cs | 14 +- 3 files changed, 260 insertions(+), 7 deletions(-) rename doc/samples/{AzureKeyVaultProviderWithEnclaveProviderExample.cs => AzureKeyVaultProviderLegacyWithEnclaveProviderExample.cs} (100%) create mode 100644 doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs diff --git a/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample.cs b/doc/samples/AzureKeyVaultProviderLegacyWithEnclaveProviderExample.cs similarity index 100% rename from doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample.cs rename to doc/samples/AzureKeyVaultProviderLegacyWithEnclaveProviderExample.cs diff --git a/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs b/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs new file mode 100644 index 0000000000..a4e288a1d9 --- /dev/null +++ b/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs @@ -0,0 +1,253 @@ +using System; +// +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Threading.Tasks; +using Azure.Identity; +using Microsoft.Data.SqlClient; +using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; + +namespace AKVEnclaveExample +{ + class Program + { + static readonly string s_algorithm = "RSA_OAEP"; + + // ********* Provide details here *********** + static readonly string s_akvUrl = "https://{KeyVaultName}.vault.azure.net/keys/{Key}/{KeyIdentifier}"; + static readonly string s_clientId = "{Application_Client_ID}"; + static readonly string s_clientSecret = "{Application_Client_Secret}"; + static readonly string s_tenantId = "{Azure_Key_Vault_Active_Directory_Tenant_Id}"; + static readonly string s_connectionString = "Server={Server}; Database={database}; Integrated Security=true; Column Encryption Setting=Enabled; Attestation Protocol=HGS; Enclave Attestation Url = {attestation_url_for_HGS};"; + // ****************************************** + + static void Main(string[] args) + { + // Initialize AKV provider + ClientSecretCredential clientSecretCredential = new ClientSecretCredential(s_tenantId, s_clientId, s_clientSecret); + SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); + + // Register AKV provider + SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customProviders: new Dictionary(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase) + { + { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, akvProvider} + }); + Console.WriteLine("AKV provider Registered"); + + // Create connection to database + using (SqlConnection sqlConnection = new SqlConnection(s_connectionString)) + { + string cmkName = "CMK_WITH_AKV"; + string cekName = "CEK_WITH_AKV"; + string tblName = "AKV_TEST_TABLE"; + + CustomerRecord customer = new CustomerRecord(1, @"Microsoft", @"Corporation"); + + try + { + sqlConnection.Open(); + + // Drop Objects if exists + dropObjects(sqlConnection, cmkName, cekName, tblName); + + // Create Column Master Key with AKV Url + createCMK(sqlConnection, cmkName, akvProvider); + Console.WriteLine("Column Master Key created."); + + // Create Column Encryption Key + createCEK(sqlConnection, cmkName, cekName, akvProvider); + Console.WriteLine("Column Encryption Key created."); + + // Create Table with Encrypted Columns + createTbl(sqlConnection, cekName, tblName); + Console.WriteLine("Table created with Encrypted columns."); + + // Insert Customer Record in table + insertData(sqlConnection, tblName, customer); + Console.WriteLine("Encryted data inserted."); + + // Read data from table + verifyData(sqlConnection, tblName, customer); + Console.WriteLine("Data validated successfully."); + } + finally + { + // Drop table and keys + dropObjects(sqlConnection, cmkName, cekName, tblName); + Console.WriteLine("Dropped Table, CEK and CMK"); + } + + Console.WriteLine("Completed AKV provider Sample."); + + Console.ReadKey(); + } + } + + private static void createCMK(SqlConnection sqlConnection, string cmkName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + string KeyStoreProviderName = SqlColumnEncryptionAzureKeyVaultProvider.ProviderName; + + byte[] cmkSign = sqlColumnEncryptionAzureKeyVaultProvider.SignColumnMasterKeyMetadata(s_akvUrl, true); + string cmkSignStr = string.Concat("0x", BitConverter.ToString(cmkSign).Replace("-", string.Empty)); + + string sql = + $@"CREATE COLUMN MASTER KEY [{cmkName}] + WITH ( + KEY_STORE_PROVIDER_NAME = N'{KeyStoreProviderName}', + KEY_PATH = N'{s_akvUrl}', + ENCLAVE_COMPUTATIONS (SIGNATURE = {cmkSignStr}) + );"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static void createCEK(SqlConnection sqlConnection, string cmkName, string cekName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + string sql = + $@"CREATE COLUMN ENCRYPTION KEY [{cekName}] + WITH VALUES ( + COLUMN_MASTER_KEY = [{cmkName}], + ALGORITHM = '{s_algorithm}', + ENCRYPTED_VALUE = {GetEncryptedValue(sqlColumnEncryptionAzureKeyVaultProvider)} + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static string GetEncryptedValue(SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + byte[] plainTextColumnEncryptionKey = new byte[32]; + RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); + rngCsp.GetBytes(plainTextColumnEncryptionKey); + + byte[] encryptedColumnEncryptionKey = sqlColumnEncryptionAzureKeyVaultProvider.EncryptColumnEncryptionKey(s_akvUrl, s_algorithm, plainTextColumnEncryptionKey); + string EncryptedValue = string.Concat("0x", BitConverter.ToString(encryptedColumnEncryptionKey).Replace("-", string.Empty)); + return EncryptedValue; + } + + private static void createTbl(SqlConnection sqlConnection, string cekName, string tblName) + { + string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256"; + + string sql = + $@"CREATE TABLE [dbo].[{tblName}] + ( + [CustomerId] [int] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [FirstName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [LastName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}') + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static void insertData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + string insertSql = $"INSERT INTO [{tblName}] (CustomerId, FirstName, LastName) VALUES (@CustomerId, @FirstName, @LastName);"; + + using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction()) + using (SqlCommand sqlCommand = new SqlCommand(insertSql, + connection: sqlConnection, transaction: sqlTransaction, + columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) + { + sqlCommand.Parameters.AddWithValue(@"CustomerId", customer.Id); + sqlCommand.Parameters.AddWithValue(@"FirstName", customer.FirstName); + sqlCommand.Parameters.AddWithValue(@"LastName", customer.LastName); + + sqlCommand.ExecuteNonQuery(); + sqlTransaction.Commit(); + } + } + + private static void verifyData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + // Test INPUT parameter on an encrypted parameter + using (SqlCommand sqlCommand = new SqlCommand($"SELECT CustomerId, FirstName, LastName FROM [{tblName}] WHERE FirstName = @firstName", + sqlConnection)) + { + SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft"); + customerFirstParam.Direction = System.Data.ParameterDirection.Input; + customerFirstParam.ForceColumnEncryption = true; + + using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader()) + { + ValidateResultSet(sqlDataReader); + } + } + } + + private static void ValidateResultSet(SqlDataReader sqlDataReader) + { + Console.WriteLine(" * Row available: " + sqlDataReader.HasRows); + + while (sqlDataReader.Read()) + { + if (sqlDataReader.GetInt32(0) == 1) + { + Console.WriteLine(" * Employee Id received as sent: " + sqlDataReader.GetInt32(0)); + } + else + { + Console.WriteLine("Employee Id didn't match"); + } + + if (sqlDataReader.GetString(1) == @"Microsoft") + { + Console.WriteLine(" * Employee Firstname received as sent: " + sqlDataReader.GetString(1)); + } + else + { + Console.WriteLine("Employee FirstName didn't match."); + } + + if (sqlDataReader.GetString(2) == @"Corporation") + { + Console.WriteLine(" * Employee LastName received as sent: " + sqlDataReader.GetString(2)); + } + else + { + Console.WriteLine("Employee LastName didn't match."); + } + } + } + + private static void dropObjects(SqlConnection sqlConnection, string cmkName, string cekName, string tblName) + { + using (SqlCommand cmd = sqlConnection.CreateCommand()) + { + cmd.CommandText = $@"IF EXISTS (select * from sys.objects where name = '{tblName}') BEGIN DROP TABLE [{tblName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_encryption_keys where name = '{cekName}') BEGIN DROP COLUMN ENCRYPTION KEY [{cekName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_master_keys where name = '{cmkName}') BEGIN DROP COLUMN MASTER KEY [{cmkName}] END"; + cmd.ExecuteNonQuery(); + } + } + + private class CustomerRecord + { + internal int Id { get; set; } + internal string FirstName { get; set; } + internal string LastName { get; set; } + + public CustomerRecord(int id, string fName, string lName) + { + Id = id; + FirstName = fName; + LastName = lName; + } + } + } +} +// diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index 1a02edf163..4c3c56cee2 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -35,7 +35,7 @@ namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider /// API only once in the lifetime of driver to register this custom provider by implementing a custom Authentication Callback mechanism. /// @@ -117,10 +117,10 @@ public SqlColumnEncryptionAzureKeyVaultProvider(TokenCredential tokenCredential, #region Public methods /// - /// Uses an asymmetric key identified by the key path to sign the masterkey metadata consisting of (masterKeyPath, allowEnclaveComputations bit, providerName). + /// Uses an asymmetric key identified by the key path to sign the master key metadata consisting of (masterKeyPath, allowEnclaveComputations bit, providerName). /// /// Complete path of an asymmetric key. Path format is specific to a key store provider. - /// Boolean indicating whether this key can be sent to trusted enclave + /// Boolean indicating whether this key can be sent to a trusted enclave /// Encrypted column encryption key public override byte[] SignColumnMasterKeyMetadata(string masterKeyPath, bool allowEnclaveComputations) { @@ -133,7 +133,7 @@ public override byte[] SignColumnMasterKeyMetadata(string masterKeyPath, bool al } /// - /// Uses an asymmetric key identified by the key path to verify the masterkey metadata consisting of (masterKeyPath, allowEnclaveComputations bit, providerName). + /// Uses an asymmetric key identified by the key path to verify the master key metadata consisting of (masterKeyPath, allowEnclaveComputations bit, providerName). /// /// Complete path of an asymmetric key. Path format is specific to a key store provider. /// Boolean indicating whether this key can be sent to trusted enclave @@ -153,7 +153,7 @@ public override bool VerifyColumnMasterKeyMetadata(string masterKeyPath, bool al /// This function uses the asymmetric key specified by the key path /// and decrypts an encrypted CEK with RSA encryption algorithm. /// - /// Complete path of an asymmetric key in AKV + /// Complete path of an asymmetric key in Azure Key Vault /// Asymmetric Key Encryption Algorithm /// Encrypted Column Encryption Key /// Plain text column encryption key @@ -234,7 +234,7 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e /// This function uses the asymmetric key specified by the key path /// and encrypts CEK with RSA encryption algorithm. /// - /// Complete path of an asymmetric key in AKV + /// Complete path of an asymmetric key in Azure Key Vault /// Asymmetric Key Encryption Algorithm /// Plain text column encryption key /// Encrypted column encryption key @@ -253,7 +253,7 @@ public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string e // Construct the encryptedColumnEncryptionKey // Format is - // s_firstVersion + keyPathLength + ciphertextLength + ciphertext + keyPath + signature + // s_firstVersion + keyPathLength + ciphertextLength + keyPath + ciphertext + signature // Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath byte[] masterKeyPathBytes = Encoding.Unicode.GetBytes(masterKeyPath.ToLowerInvariant()); From 0049b645cb456e8bdcecdc52a135cb162a8f6902 Mon Sep 17 00:00:00 2001 From: Johnny Pham <23270162+johnnypham@users.noreply.github.com> Date: Mon, 8 Feb 2021 11:32:26 -0800 Subject: [PATCH 10/17] update failing tests --- .../AzureKeyVaultProvider/Constants.cs | 6 ++++- ...qlColumnEncryptionAzureKeyVaultProvider.cs | 1 - .../AzureKeyVaultProvider/Strings.resx | 4 ++-- .../AzureKeyVaultProvider/Validator.cs | 6 ++--- .../AlwaysEncrypted/ExceptionTestAKVStore.cs | 23 +++++++++++-------- .../ManualTests/DataCommon/DataTestUtility.cs | 2 +- 6 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs index f3ea843576..8c6c34f9e0 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs @@ -24,7 +24,11 @@ internal static class Constants @"vault.azure.net", // default @"vault.azure.cn", // Azure China @"vault.usgovcloudapi.net", // US Government - @"vault.microsoftazure.de" // Azure Germany + @"vault.microsoftazure.de", // Azure Germany + @"managedhsm.azure.net", // public HSM vault + @"managedhsm.azure.cn", // Azure China HSM vault + @"managedhsm.usgovcloudapi.net", // US Government HSM vault + @"managedhsm.microsoftazure.de" // Azure Germany HSM vault }; /// diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index 4c3c56cee2..8e73ba443e 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -242,7 +242,6 @@ public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string e { // Validate the input parameters ValidateNonEmptyAKVPath(masterKeyPath, isSystemOp: true); - ValidateNotNullOrWhitespace(encryptionAlgorithm, nameof(encryptionAlgorithm)); ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: true); ValidateNotNull(columnEncryptionKey, nameof(columnEncryptionKey)); ValidateNotEmpty(columnEncryptionKey, nameof(columnEncryptionKey)); diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx index 7446876a52..e532f4a0f2 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Invalid trusted endpoint specified: '{0}'; a trusted endpoint must have a value. + Invalid trusted endpoint specified. A trusted endpoint must have a value. CipherText length does not match the RSA key size. @@ -174,4 +174,4 @@ Internal error. Key encryption algorithm cannot be null. - + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs index cca06fdea6..7b48878421 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs @@ -24,7 +24,7 @@ internal static void ValidateNotNullOrWhitespace(string parameter, string name) { if (string.IsNullOrWhiteSpace(parameter)) { - throw new ArgumentException(name, Strings.NullOrWhitespaceArgument); + throw new ArgumentException(string.Format(Strings.NullOrWhitespaceArgument, name)); } } @@ -32,7 +32,7 @@ internal static void ValidateNotEmpty(IList parameter, string name) { if (parameter.Count == 0) { - throw new ArgumentException(name, Strings.EmptyArgumentInternal); + throw new ArgumentException(string.Format(Strings.EmptyArgumentInternal, name)); } } @@ -42,7 +42,7 @@ internal static void ValidateNotNullOrWhitespaceForEach(string[] parameters, str { if (null == parameter) { - throw new ArgumentException(parameter, Strings.InvalidTrustedEndpointTemplate); + throw new ArgumentException(Strings.InvalidTrustedEndpointTemplate); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs index a9503d69de..d210cd562d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs @@ -45,7 +45,7 @@ public void NullEncryptionAlgorithm() Exception ex1 = Assert.Throws(() => fixture.AkvStoreProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, null, cek)); Assert.Matches($@"Internal error. Key encryption algorithm cannot be null.\s+\(?Parameter (name: )?'?encryptionAlgorithm('\))?", ex1.Message); Exception ex2 = Assert.Throws(() => fixture.AkvStoreProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, null, cek)); - Assert.Matches($@"Key encryption algorithm cannot be null.\s+\(?Parameter (name: )?'?encryptionAlgorithm('\))?", ex2.Message); + Assert.Matches($@"Internal error. Key encryption algorithm cannot be null.\s+\(?Parameter (name: )?'?encryptionAlgorithm('\))?", ex2.Message); } @@ -53,28 +53,28 @@ public void NullEncryptionAlgorithm() public void EmptyColumnEncryptionKey() { Exception ex1 = Assert.Throws(() => fixture.AkvStoreProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, MasterKeyEncAlgo, new byte[] { })); - Assert.Matches($@"Empty column encryption key specified.\s+\(?Parameter (name: )?'?columnEncryptionKey('\))?", ex1.Message); + Assert.Matches($@"Internal error. Empty columnEncryptionKey specified.", ex1.Message); } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] public void NullColumnEncryptionKey() { Exception ex1 = Assert.Throws(() => fixture.AkvStoreProvider.EncryptColumnEncryptionKey(DataTestUtility.AKVUrl, MasterKeyEncAlgo, null)); - Assert.Matches($@"Column encryption key cannot be null.\s+\(?Parameter (name: )?'?columnEncryptionKey('\))?", ex1.Message); + Assert.Matches($@"Value cannot be null..\s+\(?Parameter (name: )?'?columnEncryptionKey('\))?", ex1.Message); } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] public void EmptyEncryptedColumnEncryptionKey() { Exception ex1 = Assert.Throws(() => fixture.AkvStoreProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, MasterKeyEncAlgo, new byte[] { })); - Assert.Matches($@"Internal error. Empty encrypted column encryption key specified.\s+\(?Parameter (name: )?'?encryptedColumnEncryptionKey('\))?", ex1.Message); + Assert.Matches($@"Internal error. Empty encryptedColumnEncryptionKey specified", ex1.Message); } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] public void NullEncryptedColumnEncryptionKey() { Exception ex1 = Assert.Throws(() => fixture.AkvStoreProvider.DecryptColumnEncryptionKey(DataTestUtility.AKVUrl, MasterKeyEncAlgo, null)); - Assert.Matches($@"Internal error. Encrypted column encryption key cannot be null.\s+\(?Parameter (name: )?'?encryptedColumnEncryptionKey('\))?", ex1.Message); + Assert.Matches($@"Value cannot be null.\s+\(?Parameter (name: )?'?encryptedColumnEncryptionKey('\))?", ex1.Message); } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] @@ -152,15 +152,18 @@ public void NullAKVKeyPath() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] public void InvalidCertificatePath() { - string dummyPath = @"https://www.microsoft.com"; - string errorMessage = $@"Invalid Azure Key Vault key path specified: '{dummyPath}'. Valid trusted endpoints: vault.azure.net, vault.azure.cn, vault.usgovcloudapi.net, vault.microsoftazure.de.\s+\(?Parameter (name: )?'?masterKeyPath('\))?"; + string dummyPathWithOnlyHost = @"https://www.microsoft.com"; + string dummyPathWithInvalidKey = @"https://www.microsoft.vault.azure.com/keys/dummykey/dummykeyid"; + string errorMessage = $@"Invalid url specified: '{dummyPathWithOnlyHost}'"; + string errorMessage2 = $@"Invalid Azure Key Vault key path specified: '{dummyPathWithInvalidKey}'. Valid trusted endpoints: vault.azure.net, vault.azure.cn, vault.usgovcloudapi.net, vault.microsoftazure.de, managedhsm.azure.net, managedhsm.azure.cn, managedhsm.usgovcloudapi.net, managedhsm.microsoftazure.de.\s+\(?Parameter (name: )?'?masterKeyPath('\))?"; - Exception ex1 = Assert.Throws(() => fixture.AkvStoreProvider.EncryptColumnEncryptionKey(dummyPath, MasterKeyEncAlgo, cek)); + Exception ex1 = Assert.Throws( + () => fixture.AkvStoreProvider.EncryptColumnEncryptionKey(dummyPathWithOnlyHost, MasterKeyEncAlgo, cek)); Assert.Matches(errorMessage, ex1.Message); Exception ex2 = Assert.Throws( - () => fixture.AkvStoreProvider.DecryptColumnEncryptionKey(dummyPath, MasterKeyEncAlgo, encryptedCek)); - Assert.Matches(errorMessage, ex2.Message); + () => fixture.AkvStoreProvider.DecryptColumnEncryptionKey(dummyPathWithInvalidKey, MasterKeyEncAlgo, encryptedCek)); + Assert.Matches(errorMessage2, ex2.Message); } [InlineData(true)] diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index 617f905ff7..06c1b2794e 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -107,7 +107,7 @@ static DataTestUtility() } AKVOriginalUrl = c.AzureKeyVaultURL; - if (!string.IsNullOrEmpty(AKVOriginalUrl) && Uri.TryCreate(AKVOriginalUrl, UriKind.Absolute, out Uri AKVBaseUri)) + if (!string.IsNullOrEmpty(AKVOriginalUrl) && Uri.TryCreate(AKVOriginalUrl, UriKind.Absolute, out AKVBaseUri)) { AKVBaseUri = new Uri(AKVBaseUri, "/"); AKVBaseUrl = AKVBaseUri.AbsoluteUri; From d8278f8f8a8593465190b35e121a825fdd573558 Mon Sep 17 00:00:00 2001 From: Johnny Pham <23270162+johnnypham@users.noreply.github.com> Date: Mon, 8 Feb 2021 12:49:55 -0800 Subject: [PATCH 11/17] update tests --- ...qlColumnEncryptionAzureKeyVaultProvider.cs | 12 +++--- .../AzureKeyVaultProvider/Strings.Designer.cs | 4 +- .../AzureKeyVaultProvider/Strings.resx | 4 +- .../AzureKeyVaultProvider/Validator.cs | 7 +--- .../AlwaysEncrypted/ExceptionTestAKVStore.cs | 41 +++++++++++++++---- 5 files changed, 45 insertions(+), 23 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index 8e73ba443e..2d01efb26d 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -101,16 +101,16 @@ public SqlColumnEncryptionAzureKeyVaultProvider(TokenCredential tokenCredential, /// and an array of trusted endpoints. /// /// Instance of an implementation of Token Credential that is capable of providing an OAuth Token - /// TrustedEndpoints are used to validate the master key path - public SqlColumnEncryptionAzureKeyVaultProvider(TokenCredential tokenCredential, string[] trustedEndPoints) + /// TrustedEndpoints are used to validate the master key path + public SqlColumnEncryptionAzureKeyVaultProvider(TokenCredential tokenCredential, string[] trustedEndpoints) { ValidateNotNull(tokenCredential, nameof(tokenCredential)); - ValidateNotNull(trustedEndPoints, nameof(trustedEndPoints)); - ValidateNotEmpty(trustedEndPoints, nameof(trustedEndPoints)); - ValidateNotNullOrWhitespaceForEach(trustedEndPoints, nameof(trustedEndPoints)); + ValidateNotNull(trustedEndpoints, nameof(trustedEndpoints)); + ValidateNotEmpty(trustedEndpoints, nameof(trustedEndpoints)); + ValidateNotNullOrWhitespaceForEach(trustedEndpoints, nameof(trustedEndpoints)); KeyCryptographer = new AzureSqlKeyCryptographer(tokenCredential); - TrustedEndPoints = trustedEndPoints; + TrustedEndPoints = trustedEndpoints; } #endregion diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs index 97e15ebd56..29378fdac8 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs @@ -214,11 +214,11 @@ internal static string InvalidSignatureTemplate /// /// Looks up a localized string similar to Invalid trusted endpoint specified: '{0}'; a trusted endpoint must have a value.. /// - internal static string InvalidTrustedEndpointTemplate + internal static string NullOrWhitespaceForEach { get { - return ResourceManager.GetString("InvalidTrustedEndpointTemplate", resourceCulture); + return ResourceManager.GetString("NullOrWhitespaceForEach", resourceCulture); } } diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx index e532f4a0f2..5752720fb0 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx @@ -117,8 +117,8 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Invalid trusted endpoint specified. A trusted endpoint must have a value. + + One or more of the elements in {0} are null or empty or consist of only whitespace. CipherText length does not match the RSA key size. diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs index 7b48878421..f0611dd551 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs @@ -38,12 +38,9 @@ internal static void ValidateNotEmpty(IList parameter, string name) internal static void ValidateNotNullOrWhitespaceForEach(string[] parameters, string name) { - foreach (var parameter in parameters) + if (parameters.Any(s => string.IsNullOrWhiteSpace(s))) { - if (null == parameter) - { - throw new ArgumentException(Strings.InvalidTrustedEndpointTemplate); - } + throw new ArgumentException(string.Format(Strings.NullOrWhitespaceForEach, name)); } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs index d210cd562d..80c087ee77 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs @@ -4,6 +4,7 @@ using System; using System.Security.Cryptography; +using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; using Microsoft.Data.SqlClient.ManualTesting.Tests.AlwaysEncrypted.Setup; using Xunit; @@ -153,20 +154,30 @@ public void NullAKVKeyPath() public void InvalidCertificatePath() { string dummyPathWithOnlyHost = @"https://www.microsoft.com"; + string invalidUrlErrorMessage = $@"Invalid url specified: '{dummyPathWithOnlyHost}'"; string dummyPathWithInvalidKey = @"https://www.microsoft.vault.azure.com/keys/dummykey/dummykeyid"; - string errorMessage = $@"Invalid url specified: '{dummyPathWithOnlyHost}'"; - string errorMessage2 = $@"Invalid Azure Key Vault key path specified: '{dummyPathWithInvalidKey}'. Valid trusted endpoints: vault.azure.net, vault.azure.cn, vault.usgovcloudapi.net, vault.microsoftazure.de, managedhsm.azure.net, managedhsm.azure.cn, managedhsm.usgovcloudapi.net, managedhsm.microsoftazure.de.\s+\(?Parameter (name: )?'?masterKeyPath('\))?"; - - Exception ex1 = Assert.Throws( + string invalidTrustedEndpointErrorMessage = $@"Invalid Azure Key Vault key path specified: '{dummyPathWithInvalidKey}'. +Valid trusted endpoints: vault.azure.net, vault.azure.cn, vault.usgovcloudapi.net, vault.microsoftazure.de, managedhsm.azure.net, +managedhsm.azure.cn, managedhsm.usgovcloudapi.net, managedhsm.microsoftazure.de.\s+\(?Parameter (name: )?'?masterKeyPath('\))?"; + + Exception ex = Assert.Throws( () => fixture.AkvStoreProvider.EncryptColumnEncryptionKey(dummyPathWithOnlyHost, MasterKeyEncAlgo, cek)); - Assert.Matches(errorMessage, ex1.Message); + Assert.Matches(invalidUrlErrorMessage, ex.Message); - Exception ex2 = Assert.Throws( + ex = Assert.Throws( + () => fixture.AkvStoreProvider.EncryptColumnEncryptionKey(dummyPathWithInvalidKey, MasterKeyEncAlgo, cek)); + Assert.Matches(invalidTrustedEndpointErrorMessage, ex.Message); + + ex = Assert.Throws( + () => fixture.AkvStoreProvider.DecryptColumnEncryptionKey(dummyPathWithOnlyHost, MasterKeyEncAlgo, encryptedCek)); + Assert.Matches(invalidUrlErrorMessage, ex.Message); + + ex = Assert.Throws( () => fixture.AkvStoreProvider.DecryptColumnEncryptionKey(dummyPathWithInvalidKey, MasterKeyEncAlgo, encryptedCek)); - Assert.Matches(errorMessage2, ex2.Message); + Assert.Matches(invalidTrustedEndpointErrorMessage, ex.Message); } - [InlineData(true)] + [InlineData(true)] [InlineData(false)] [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] public void AkvStoreProviderVerifyFunctionWithInvalidSignature(bool fEnclaveEnabled) @@ -209,5 +220,19 @@ public void AkvStoreProviderVerifyFunctionWithInvalidSignature(bool fEnclaveEnab tamperedCmkSignature[startingByteIndex + randomIndexInCipherText[0]] = cmkSignature[startingByteIndex + randomIndexInCipherText[0]]; } } + + [InlineData(new object[] { new string[] { null } })] + [InlineData(new object[] { new string[] { "" } })] + [InlineData(new object[] { new string[] { " " } })] + [ConditionalTheory(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] + public void InvalidTrustedEndpoints(string[] trustedEndpoints) + { + Exception ex = Assert.Throws(() => + { + SqlColumnEncryptionAzureKeyVaultProvider azureKeyProvider = new SqlColumnEncryptionAzureKeyVaultProvider( + new SqlClientCustomTokenCredential(), trustedEndpoints); + }); + Assert.Matches("One or more of the elements in trustedEndpoints are null or empty or consist of only whitespace.", ex.Message); + } } } From cf8e1f7704dbec95241a39c7dccd22a7706bcbc5 Mon Sep 17 00:00:00 2001 From: Johnny Pham <23270162+johnnypham@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:09:10 -0800 Subject: [PATCH 12/17] resolve merge conflicts --- .../ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs | 4 +--- .../Microsoft.Data.SqlClient.ManualTesting.Tests.csproj | 1 + tools/props/Versions.props | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs index 80c087ee77..9b94aca8db 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/ExceptionTestAKVStore.cs @@ -156,9 +156,7 @@ public void InvalidCertificatePath() string dummyPathWithOnlyHost = @"https://www.microsoft.com"; string invalidUrlErrorMessage = $@"Invalid url specified: '{dummyPathWithOnlyHost}'"; string dummyPathWithInvalidKey = @"https://www.microsoft.vault.azure.com/keys/dummykey/dummykeyid"; - string invalidTrustedEndpointErrorMessage = $@"Invalid Azure Key Vault key path specified: '{dummyPathWithInvalidKey}'. -Valid trusted endpoints: vault.azure.net, vault.azure.cn, vault.usgovcloudapi.net, vault.microsoftazure.de, managedhsm.azure.net, -managedhsm.azure.cn, managedhsm.usgovcloudapi.net, managedhsm.microsoftazure.de.\s+\(?Parameter (name: )?'?masterKeyPath('\))?"; + string invalidTrustedEndpointErrorMessage = $@"Invalid Azure Key Vault key path specified: '{dummyPathWithInvalidKey}'. Valid trusted endpoints: vault.azure.net, vault.azure.cn, vault.usgovcloudapi.net, vault.microsoftazure.de, managedhsm.azure.net, managedhsm.azure.cn, managedhsm.usgovcloudapi.net, managedhsm.microsoftazure.de.\s+\(?Parameter (name: )?'?masterKeyPath('\))?"; Exception ex = Assert.Throws( () => fixture.AkvStoreProvider.EncryptColumnEncryptionKey(dummyPathWithOnlyHost, MasterKeyEncAlgo, cek)); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index df742bb6fb..03f42880fb 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -63,6 +63,7 @@ Common\System\Collections\DictionaryExtensions.cs + diff --git a/tools/props/Versions.props b/tools/props/Versions.props index cc9a08229b..7f9f5d5f79 100644 --- a/tools/props/Versions.props +++ b/tools/props/Versions.props @@ -61,7 +61,7 @@ 4.5.0 4.6.0 4.3.0 - 5.6.0 + 6.8.0 2.4.1 5.0.0-beta.20206.4 2.0.8 From 471d9e8e62dbd3d1a9b6ab56c4bbbca3e2153e62 Mon Sep 17 00:00:00 2001 From: Johnny Pham <23270162+johnnypham@users.noreply.github.com> Date: Wed, 10 Feb 2021 16:19:04 -0800 Subject: [PATCH 13/17] resolve merge conflicts --- .../tests/ManualTests/DataCommon/DataTestUtility.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs index deb23c14e9..01c6bad8ea 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/DataCommon/DataTestUtility.cs @@ -108,13 +108,6 @@ static DataTestUtility() Console.WriteLine($"App Context switch {ManagedNetworkingAppContextSwitch} enabled on {Environment.OSVersion}"); } - if (IsAADPasswordConnStrSetup() && IsAADAuthorityURLSetup()) - { - string username = RetrieveValueFromConnStr(AADPasswordConnectionString, new string[] { "User ID", "UID" }); - string password = RetrieveValueFromConnStr(AADPasswordConnectionString, new string[] { "Password", "PWD" }); - AADAccessToken = GenerateAccessToken(AADAuthorityURL, username, password); - } - AKVOriginalUrl = c.AzureKeyVaultURL; if (!string.IsNullOrEmpty(AKVOriginalUrl) && Uri.TryCreate(AKVOriginalUrl, UriKind.Absolute, out AKVBaseUri)) { From 47cbaf689c4eae720856aa27d27f6a22ae812e94 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 23 Feb 2021 12:51:52 -0800 Subject: [PATCH 14/17] Touch-ups and cleanups --- ...tProviderWithEnclaveProviderExample_2_0.cs | 423 +++++++++--------- .../AzureSqlKeyCryptographer.cs | 7 +- .../AzureKeyVaultProvider/Constants.cs | 6 - ...waysEncrypted.AzureKeyVaultProvider.csproj | 2 +- ...qlColumnEncryptionAzureKeyVaultProvider.cs | 43 +- .../AzureKeyVaultProvider/Strings.Designer.cs | 2 - .../AzureKeyVaultProvider/Strings.resx | 11 +- .../add-ons/AzureKeyVaultProvider/Utils.cs | 150 +++++++ .../AzureKeyVaultProvider/Validator.cs | 90 ---- 9 files changed, 380 insertions(+), 354 deletions(-) create mode 100644 src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Utils.cs delete mode 100644 src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs diff --git a/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs b/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs index a4e288a1d9..31f3249c4c 100644 --- a/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs +++ b/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs @@ -9,245 +9,242 @@ namespace AKVEnclaveExample { - class Program + class Program + { + static readonly string s_algorithm = "RSA_OAEP"; + + // ********* Provide details here *********** + static readonly string s_akvUrl = "https://{KeyVaultName}.vault.azure.net/keys/{Key}/{KeyIdentifier}"; + static readonly string s_clientId = "{Application_Client_ID}"; + static readonly string s_clientSecret = "{Application_Client_Secret}"; + static readonly string s_tenantId = "{Azure_Key_Vault_Active_Directory_Tenant_Id}"; + static readonly string s_connectionString = "Server={Server}; Database={database}; Integrated Security=true; Column Encryption Setting=Enabled; Attestation Protocol=HGS; Enclave Attestation Url = {attestation_url_for_HGS};"; + // ****************************************** + + static void Main(string[] args) { - static readonly string s_algorithm = "RSA_OAEP"; + // Initialize AKV provider + ClientSecretCredential clientSecretCredential = new ClientSecretCredential(s_tenantId, s_clientId, s_clientSecret); + SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); - // ********* Provide details here *********** - static readonly string s_akvUrl = "https://{KeyVaultName}.vault.azure.net/keys/{Key}/{KeyIdentifier}"; - static readonly string s_clientId = "{Application_Client_ID}"; - static readonly string s_clientSecret = "{Application_Client_Secret}"; - static readonly string s_tenantId = "{Azure_Key_Vault_Active_Directory_Tenant_Id}"; - static readonly string s_connectionString = "Server={Server}; Database={database}; Integrated Security=true; Column Encryption Setting=Enabled; Attestation Protocol=HGS; Enclave Attestation Url = {attestation_url_for_HGS};"; - // ****************************************** - - static void Main(string[] args) + // Register AKV provider + SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customProviders: new Dictionary(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase) { - // Initialize AKV provider - ClientSecretCredential clientSecretCredential = new ClientSecretCredential(s_tenantId, s_clientId, s_clientSecret); - SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); - - // Register AKV provider - SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customProviders: new Dictionary(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase) - { - { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, akvProvider} - }); - Console.WriteLine("AKV provider Registered"); - - // Create connection to database - using (SqlConnection sqlConnection = new SqlConnection(s_connectionString)) - { - string cmkName = "CMK_WITH_AKV"; - string cekName = "CEK_WITH_AKV"; - string tblName = "AKV_TEST_TABLE"; - - CustomerRecord customer = new CustomerRecord(1, @"Microsoft", @"Corporation"); - - try - { - sqlConnection.Open(); - - // Drop Objects if exists - dropObjects(sqlConnection, cmkName, cekName, tblName); - - // Create Column Master Key with AKV Url - createCMK(sqlConnection, cmkName, akvProvider); - Console.WriteLine("Column Master Key created."); - - // Create Column Encryption Key - createCEK(sqlConnection, cmkName, cekName, akvProvider); - Console.WriteLine("Column Encryption Key created."); - - // Create Table with Encrypted Columns - createTbl(sqlConnection, cekName, tblName); - Console.WriteLine("Table created with Encrypted columns."); - - // Insert Customer Record in table - insertData(sqlConnection, tblName, customer); - Console.WriteLine("Encryted data inserted."); - - // Read data from table - verifyData(sqlConnection, tblName, customer); - Console.WriteLine("Data validated successfully."); - } - finally - { - // Drop table and keys - dropObjects(sqlConnection, cmkName, cekName, tblName); - Console.WriteLine("Dropped Table, CEK and CMK"); - } - - Console.WriteLine("Completed AKV provider Sample."); - - Console.ReadKey(); - } - } + { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, akvProvider} + }); + Console.WriteLine("AKV provider Registered"); + + // Create connection to database + using (SqlConnection sqlConnection = new SqlConnection(s_connectionString)) + { + string cmkName = "CMK_WITH_AKV"; + string cekName = "CEK_WITH_AKV"; + string tblName = "AKV_TEST_TABLE"; - private static void createCMK(SqlConnection sqlConnection, string cmkName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + CustomerRecord customer = new CustomerRecord(1, @"Microsoft", @"Corporation"); + + try { - string KeyStoreProviderName = SqlColumnEncryptionAzureKeyVaultProvider.ProviderName; - - byte[] cmkSign = sqlColumnEncryptionAzureKeyVaultProvider.SignColumnMasterKeyMetadata(s_akvUrl, true); - string cmkSignStr = string.Concat("0x", BitConverter.ToString(cmkSign).Replace("-", string.Empty)); - - string sql = - $@"CREATE COLUMN MASTER KEY [{cmkName}] - WITH ( - KEY_STORE_PROVIDER_NAME = N'{KeyStoreProviderName}', - KEY_PATH = N'{s_akvUrl}', - ENCLAVE_COMPUTATIONS (SIGNATURE = {cmkSignStr}) - );"; - - using (SqlCommand command = sqlConnection.CreateCommand()) - { - command.CommandText = sql; - command.ExecuteNonQuery(); - } - } + sqlConnection.Open(); + + // Drop Objects if exists + dropObjects(sqlConnection, cmkName, cekName, tblName); - private static void createCEK(SqlConnection sqlConnection, string cmkName, string cekName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + // Create Column Master Key with AKV Url + createCMK(sqlConnection, cmkName, akvProvider); + Console.WriteLine("Column Master Key created."); + + // Create Column Encryption Key + createCEK(sqlConnection, cmkName, cekName, akvProvider); + Console.WriteLine("Column Encryption Key created."); + + // Create Table with Encrypted Columns + createTbl(sqlConnection, cekName, tblName); + Console.WriteLine("Table created with Encrypted columns."); + + // Insert Customer Record in table + insertData(sqlConnection, tblName, customer); + Console.WriteLine("Encryted data inserted."); + + // Read data from table + verifyData(sqlConnection, tblName, customer); + Console.WriteLine("Data validated successfully."); + } + finally { - string sql = - $@"CREATE COLUMN ENCRYPTION KEY [{cekName}] - WITH VALUES ( - COLUMN_MASTER_KEY = [{cmkName}], - ALGORITHM = '{s_algorithm}', - ENCRYPTED_VALUE = {GetEncryptedValue(sqlColumnEncryptionAzureKeyVaultProvider)} - )"; - - using (SqlCommand command = sqlConnection.CreateCommand()) - { - command.CommandText = sql; - command.ExecuteNonQuery(); - } + // Drop table and keys + dropObjects(sqlConnection, cmkName, cekName, tblName); + Console.WriteLine("Dropped Table, CEK and CMK"); } - private static string GetEncryptedValue(SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) - { - byte[] plainTextColumnEncryptionKey = new byte[32]; - RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); - rngCsp.GetBytes(plainTextColumnEncryptionKey); + Console.WriteLine("Completed AKV provider Sample."); + } + } - byte[] encryptedColumnEncryptionKey = sqlColumnEncryptionAzureKeyVaultProvider.EncryptColumnEncryptionKey(s_akvUrl, s_algorithm, plainTextColumnEncryptionKey); - string EncryptedValue = string.Concat("0x", BitConverter.ToString(encryptedColumnEncryptionKey).Replace("-", string.Empty)); - return EncryptedValue; - } + private static void createCMK(SqlConnection sqlConnection, string cmkName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + string KeyStoreProviderName = SqlColumnEncryptionAzureKeyVaultProvider.ProviderName; + + byte[] cmkSign = sqlColumnEncryptionAzureKeyVaultProvider.SignColumnMasterKeyMetadata(s_akvUrl, true); + string cmkSignStr = string.Concat("0x", BitConverter.ToString(cmkSign).Replace("-", string.Empty)); + + string sql = + $@"CREATE COLUMN MASTER KEY [{cmkName}] + WITH ( + KEY_STORE_PROVIDER_NAME = N'{KeyStoreProviderName}', + KEY_PATH = N'{s_akvUrl}', + ENCLAVE_COMPUTATIONS (SIGNATURE = {cmkSignStr}) + );"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } - private static void createTbl(SqlConnection sqlConnection, string cekName, string tblName) + private static void createCEK(SqlConnection sqlConnection, string cmkName, string cekName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + string sql = + $@"CREATE COLUMN ENCRYPTION KEY [{cekName}] + WITH VALUES ( + COLUMN_MASTER_KEY = [{cmkName}], + ALGORITHM = '{s_algorithm}', + ENCRYPTED_VALUE = {GetEncryptedValue(sqlColumnEncryptionAzureKeyVaultProvider)} + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static string GetEncryptedValue(SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + byte[] plainTextColumnEncryptionKey = new byte[32]; + RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); + rngCsp.GetBytes(plainTextColumnEncryptionKey); + + byte[] encryptedColumnEncryptionKey = sqlColumnEncryptionAzureKeyVaultProvider.EncryptColumnEncryptionKey(s_akvUrl, s_algorithm, plainTextColumnEncryptionKey); + string EncryptedValue = string.Concat("0x", BitConverter.ToString(encryptedColumnEncryptionKey).Replace("-", string.Empty)); + return EncryptedValue; + } + + private static void createTbl(SqlConnection sqlConnection, string cekName, string tblName) + { + string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256"; + + string sql = + $@"CREATE TABLE [dbo].[{tblName}] + ( + [CustomerId] [int] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [FirstName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), + [LastName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}') + )"; + + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static void insertData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + string insertSql = $"INSERT INTO [{tblName}] (CustomerId, FirstName, LastName) VALUES (@CustomerId, @FirstName, @LastName);"; + + using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction()) + using (SqlCommand sqlCommand = new SqlCommand(insertSql, + connection: sqlConnection, transaction: sqlTransaction, + columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) + { + sqlCommand.Parameters.AddWithValue(@"CustomerId", customer.Id); + sqlCommand.Parameters.AddWithValue(@"FirstName", customer.FirstName); + sqlCommand.Parameters.AddWithValue(@"LastName", customer.LastName); + + sqlCommand.ExecuteNonQuery(); + sqlTransaction.Commit(); + } + } + + private static void verifyData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + // Test INPUT parameter on an encrypted parameter + using (SqlCommand sqlCommand = new SqlCommand($"SELECT CustomerId, FirstName, LastName FROM [{tblName}] WHERE FirstName = @firstName", sqlConnection)) + { + SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft"); + customerFirstParam.Direction = System.Data.ParameterDirection.Input; + customerFirstParam.ForceColumnEncryption = true; + + using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader()) { - string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256"; - - string sql = - $@"CREATE TABLE [dbo].[{tblName}] - ( - [CustomerId] [int] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), - [FirstName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), - [LastName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}') - )"; - - using (SqlCommand command = sqlConnection.CreateCommand()) - { - command.CommandText = sql; - command.ExecuteNonQuery(); - } + ValidateResultSet(sqlDataReader); } + } + } + + private static void ValidateResultSet(SqlDataReader sqlDataReader) + { + Console.WriteLine(" * Row available: " + sqlDataReader.HasRows); - private static void insertData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + while (sqlDataReader.Read()) + { + if (sqlDataReader.GetInt32(0) == 1) { - string insertSql = $"INSERT INTO [{tblName}] (CustomerId, FirstName, LastName) VALUES (@CustomerId, @FirstName, @LastName);"; - - using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction()) - using (SqlCommand sqlCommand = new SqlCommand(insertSql, - connection: sqlConnection, transaction: sqlTransaction, - columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) - { - sqlCommand.Parameters.AddWithValue(@"CustomerId", customer.Id); - sqlCommand.Parameters.AddWithValue(@"FirstName", customer.FirstName); - sqlCommand.Parameters.AddWithValue(@"LastName", customer.LastName); - - sqlCommand.ExecuteNonQuery(); - sqlTransaction.Commit(); - } + Console.WriteLine(" * Employee Id received as sent: " + sqlDataReader.GetInt32(0)); } - - private static void verifyData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + else { - // Test INPUT parameter on an encrypted parameter - using (SqlCommand sqlCommand = new SqlCommand($"SELECT CustomerId, FirstName, LastName FROM [{tblName}] WHERE FirstName = @firstName", - sqlConnection)) - { - SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft"); - customerFirstParam.Direction = System.Data.ParameterDirection.Input; - customerFirstParam.ForceColumnEncryption = true; - - using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader()) - { - ValidateResultSet(sqlDataReader); - } - } + Console.WriteLine("Employee Id didn't match"); } - private static void ValidateResultSet(SqlDataReader sqlDataReader) + if (sqlDataReader.GetString(1) == @"Microsoft") { - Console.WriteLine(" * Row available: " + sqlDataReader.HasRows); - - while (sqlDataReader.Read()) - { - if (sqlDataReader.GetInt32(0) == 1) - { - Console.WriteLine(" * Employee Id received as sent: " + sqlDataReader.GetInt32(0)); - } - else - { - Console.WriteLine("Employee Id didn't match"); - } - - if (sqlDataReader.GetString(1) == @"Microsoft") - { - Console.WriteLine(" * Employee Firstname received as sent: " + sqlDataReader.GetString(1)); - } - else - { - Console.WriteLine("Employee FirstName didn't match."); - } - - if (sqlDataReader.GetString(2) == @"Corporation") - { - Console.WriteLine(" * Employee LastName received as sent: " + sqlDataReader.GetString(2)); - } - else - { - Console.WriteLine("Employee LastName didn't match."); - } - } + Console.WriteLine(" * Employee Firstname received as sent: " + sqlDataReader.GetString(1)); } - - private static void dropObjects(SqlConnection sqlConnection, string cmkName, string cekName, string tblName) + else { - using (SqlCommand cmd = sqlConnection.CreateCommand()) - { - cmd.CommandText = $@"IF EXISTS (select * from sys.objects where name = '{tblName}') BEGIN DROP TABLE [{tblName}] END"; - cmd.ExecuteNonQuery(); - cmd.CommandText = $@"IF EXISTS (select * from sys.column_encryption_keys where name = '{cekName}') BEGIN DROP COLUMN ENCRYPTION KEY [{cekName}] END"; - cmd.ExecuteNonQuery(); - cmd.CommandText = $@"IF EXISTS (select * from sys.column_master_keys where name = '{cmkName}') BEGIN DROP COLUMN MASTER KEY [{cmkName}] END"; - cmd.ExecuteNonQuery(); - } + Console.WriteLine("Employee FirstName didn't match."); } - private class CustomerRecord + if (sqlDataReader.GetString(2) == @"Corporation") { - internal int Id { get; set; } - internal string FirstName { get; set; } - internal string LastName { get; set; } - - public CustomerRecord(int id, string fName, string lName) - { - Id = id; - FirstName = fName; - LastName = lName; - } + Console.WriteLine(" * Employee LastName received as sent: " + sqlDataReader.GetString(2)); } + else + { + Console.WriteLine("Employee LastName didn't match."); + } + } + } + + private static void dropObjects(SqlConnection sqlConnection, string cmkName, string cekName, string tblName) + { + using (SqlCommand cmd = sqlConnection.CreateCommand()) + { + cmd.CommandText = $@"IF EXISTS (select * from sys.objects where name = '{tblName}') BEGIN DROP TABLE [{tblName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_encryption_keys where name = '{cekName}') BEGIN DROP COLUMN ENCRYPTION KEY [{cekName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_master_keys where name = '{cmkName}') BEGIN DROP COLUMN MASTER KEY [{cmkName}] END"; + cmd.ExecuteNonQuery(); + } + } + + private class CustomerRecord + { + internal int Id { get; set; } + internal string FirstName { get; set; } + internal string LastName { get; set; } + + public CustomerRecord(int id, string fName, string lName) + { + Id = id; + FirstName = fName; + LastName = lName; + } } + } } // diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs index a10e65c1cd..e9eb3b995d 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs @@ -7,12 +7,9 @@ using Azure.Security.KeyVault.Keys.Cryptography; using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; using System.Threading.Tasks; using static Azure.Security.KeyVault.Keys.Cryptography.SignatureAlgorithm; - namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider { internal class AzureSqlKeyCryptographer @@ -89,7 +86,7 @@ internal KeyVaultKey GetKey(string keyIdentifierUri) } // Not a public exception - not likely to occur. - throw new KeyNotFoundException($"The key with identifier {keyIdentifierUri} was not found."); + throw ADP.MasterKeyNotFound(keyIdentifierUri); } /// @@ -187,7 +184,7 @@ private KeyVaultKey ValidateRsaKey(KeyVaultKey key) { if (key.KeyType != KeyType.Rsa && key.KeyType != KeyType.RsaHsm) { - throw new FormatException(string.Format(CultureInfo.InvariantCulture, Strings.NonRsaKeyTemplate, key.KeyType)); + throw ADP.NonRsaKeyFormat(key.KeyType.ToString()); } return key; diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs index b3d74ebfde..310f97f944 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Constants.cs @@ -2,12 +2,6 @@ // 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.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider { internal static class Constants diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj index 211ee2d38e..644b641726 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj @@ -6,7 +6,7 @@ {9073ABEF-92E0-4702-BB23-2C99CEF9BDD7} netcoreapp netfx - Debug;Release;net46-Release;net46-Debug;netcoreapp2.1-Debug;netcoreapp2.1-Release;netcoreapp3.1-Debug;netcoreapp3.1-Release + Debug;Release;net461-Release;net461-Debug;netcoreapp2.1-Debug;netcoreapp2.1-Release;netcoreapp3.1-Debug;netcoreapp3.1-Release AnyCPU;x86;x64 $(ObjFolder)$(Configuration).$(Platform)\$(AddOnName) $(BinFolder)$(Configuration).$(Platform)\$(AddOnName) diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index 2d01efb26d..aebe42191e 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -3,10 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -using System.Diagnostics; -using System.Globalization; using System.Linq; -using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Azure.Core; @@ -187,22 +184,14 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e // validate the ciphertext length if (cipherTextLength != keySizeInBytes) { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidCiphertextLengthTemplate, - cipherTextLength, - keySizeInBytes, - masterKeyPath), - Constants.AeParamEncryptedCek); + throw ADP.InvalidCipherTextLength(cipherTextLength, keySizeInBytes, masterKeyPath); } // Validate the signature length int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength; if (signatureLength != keySizeInBytes) { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureLengthTemplate, - signatureLength, - keySizeInBytes, - masterKeyPath), - Constants.AeParamEncryptedCek); + throw ADP.InvalidSignatureLengthTemplate(signatureLength, keySizeInBytes, masterKeyPath); } // Get ciphertext @@ -217,14 +206,12 @@ public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string e if (null == message) { - throw new CryptographicException(Strings.NullHash); + throw ADP.NullHashFound(); } if (!KeyCryptographer.VerifyData(message, signature, masterKeyPath)) { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureTemplate, - masterKeyPath), - Constants.AeParamEncryptedCek); + throw ADP.InvalidSignatureTemplate(masterKeyPath); } return KeyCryptographer.UnwrapKey(s_keyWrapAlgorithm, cipherText, masterKeyPath); @@ -264,7 +251,7 @@ public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string e if (cipherText.Length != keySizeInBytes) { - throw new CryptographicException(Strings.CipherTextLengthMismatch); + throw ADP.CipherTextLengthMismatch(); } // Compute message @@ -276,7 +263,7 @@ public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string e if (signature.Length != keySizeInBytes) { - throw new CryptographicException(Strings.HashLengthMismatch); + throw ADP.HashLengthMismatch(); } ValidateSignature(masterKeyPath, message, signature); @@ -288,7 +275,6 @@ public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string e #region Private methods - /// /// Checks if the Azure Key Vault key path is Empty or Null (and raises exception if they are). /// @@ -297,22 +283,13 @@ internal void ValidateNonEmptyAKVPath(string masterKeyPath, bool isSystemOp) // throw appropriate error if masterKeyPath is null or empty if (string.IsNullOrWhiteSpace(masterKeyPath)) { - string errorMessage = null == masterKeyPath - ? Strings.NullAkvPath - : string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvPathTemplate, masterKeyPath); - - if (isSystemOp) - { - throw new ArgumentNullException(Constants.AeParamMasterKeyPath, errorMessage); - } - - throw new ArgumentException(errorMessage, Constants.AeParamMasterKeyPath); + ADP.InvalidAKVPath(masterKeyPath, isSystemOp); } if (!Uri.TryCreate(masterKeyPath, UriKind.Absolute, out Uri parsedUri) || parsedUri.Segments.Length < 3) { // Return an error indicating that the AKV url is invalid. - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvUrlTemplate, masterKeyPath), Constants.AeParamMasterKeyPath); + throw ADP.InvalidAKVUrl(masterKeyPath); } // A valid URI. @@ -326,14 +303,14 @@ internal void ValidateNonEmptyAKVPath(string masterKeyPath, bool isSystemOp) } // Return an error indicating that the AKV url is invalid. - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvKeyPathTrustedTemplate, masterKeyPath, string.Join(", ", TrustedEndPoints.ToArray())), Constants.AeParamMasterKeyPath); + throw ADP.InvalidAKVUrlTrustedEndpoints(masterKeyPath, string.Join(", ", TrustedEndPoints.ToArray())); } private void ValidateSignature(string masterKeyPath, byte[] message, byte[] signature) { if (!KeyCryptographer.VerifyData(message, signature, masterKeyPath)) { - throw new CryptographicException(Strings.InvalidSignature); + throw ADP.InvalidSignature(); } } diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs index 29378fdac8..60bf89df91 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.Designer.cs @@ -10,8 +10,6 @@ namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider { - using System; - /// /// A strongly-typed resource class, for looking up localized strings, etc. /// diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx index 5752720fb0..a90ecbeb99 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Strings.resx @@ -129,6 +129,9 @@ Internal error. Empty {0} specified. + + The key with identifier '{0}' was not found. + Signed hash length does not match the RSA key size. @@ -168,10 +171,10 @@ Key encryption algorithm cannot be null. - - Hash should not be null while decrypting encrypted column encryption key. - Internal error. Key encryption algorithm cannot be null. - \ No newline at end of file + + Hash should not be null while decrypting encrypted column encryption key. + + diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Utils.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Utils.cs new file mode 100644 index 0000000000..e0668450b5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Utils.cs @@ -0,0 +1,150 @@ +// 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; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; + +namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider +{ + internal static class Validator + { + internal static void ValidateNotNull(object parameter, string name) + { + if (null == parameter) + { + throw ADP.NullArgument(name); + } + } + + internal static void ValidateNotNullOrWhitespace(string parameter, string name) + { + if (string.IsNullOrWhiteSpace(parameter)) + { + throw ADP.NullOrWhitespaceArgument(name); + } + } + + internal static void ValidateNotEmpty(IList parameter, string name) + { + if (parameter.Count == 0) + { + throw ADP.EmptyArgument(name); + } + } + + internal static void ValidateNotNullOrWhitespaceForEach(string[] parameters, string name) + { + if (parameters.Any(s => string.IsNullOrWhiteSpace(s))) + { + throw ADP.NullOrWhitespaceForEach(name); + } + } + + internal static void ValidateEncryptionAlgorithm(string encryptionAlgorithm, bool isSystemOp) + { + // This validates that the encryption algorithm is RSA_OAEP + if (null == encryptionAlgorithm) + { + throw ADP.NullAlgorithm(isSystemOp); + } + + if (!encryptionAlgorithm.Equals("RSA_OAEP", StringComparison.OrdinalIgnoreCase) + && !encryptionAlgorithm.Equals("RSA-OAEP", StringComparison.OrdinalIgnoreCase)) + { + throw ADP.InvalidKeyAlgorithm(encryptionAlgorithm); + } + } + + internal static void ValidateVersionByte(byte encryptedByte, byte firstVersionByte) + { + // Validate and decrypt the EncryptedColumnEncryptionKey + // Format is + // version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature + // + // keyPath is present in the encrypted column encryption key for identifying the original source of the asymmetric key pair and + // we will not validate it against the data contained in the CMK metadata (masterKeyPath). + + // Validate the version byte + if (encryptedByte != firstVersionByte) + { + throw ADP.InvalidAlgorithmVersion(encryptedByte.ToString(@"X2"), firstVersionByte.ToString("X2")); + } + } + } + + internal static class ADP + { + internal static ArgumentNullException NullArgument(string name) => + new ArgumentNullException(name); + + internal static ArgumentException NullOrWhitespaceArgument(string name) => + new ArgumentException(string.Format(Strings.NullOrWhitespaceArgument, name)); + + internal static ArgumentException EmptyArgument(string name) => + new ArgumentException(string.Format(Strings.EmptyArgumentInternal, name)); + + internal static ArgumentException NullOrWhitespaceForEach(string name) => + new ArgumentException(string.Format(Strings.NullOrWhitespaceForEach, name)); + + internal static KeyNotFoundException MasterKeyNotFound(string masterKeyPath) => + new KeyNotFoundException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureTemplate, masterKeyPath)); + + internal static FormatException NonRsaKeyFormat(string keyType) => + new FormatException(string.Format(CultureInfo.InvariantCulture, Strings.NonRsaKeyTemplate, keyType)); + + internal static ArgumentException InvalidCipherTextLength(ushort cipherTextLength, int keySizeInBytes, string masterKeyPath) => + new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidCiphertextLengthTemplate, + cipherTextLength, keySizeInBytes, masterKeyPath), Constants.AeParamEncryptedCek); + + internal static ArgumentNullException NullAlgorithm(bool isSystemOp) => + new ArgumentNullException(Constants.AeParamEncryptionAlgorithm, (isSystemOp ? Strings.NullAlgorithmInternal : Strings.NullAlgorithm)); + + internal static ArgumentException InvalidKeyAlgorithm(string encryptionAlgorithm) => + new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidKeyAlgorithm, encryptionAlgorithm, + "RSA_OAEP' or 'RSA-OAEP")/* For supporting both algorithm formats.*/, Constants.AeParamEncryptionAlgorithm); + + internal static ArgumentException InvalidSignatureLengthTemplate(int signatureLength, int keySizeInBytes, string masterKeyPath) => + new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureLengthTemplate, + signatureLength, keySizeInBytes, masterKeyPath), Constants.AeParamEncryptedCek); + + internal static Exception InvalidAlgorithmVersion(string encryptedBytes, string firstVersionBytes) => + new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAlgorithmVersionTemplate, + encryptedBytes, firstVersionBytes), Constants.AeParamEncryptedCek); + + internal static ArgumentException InvalidSignatureTemplate(string masterKeyPath) => + new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidSignatureTemplate, masterKeyPath), + Constants.AeParamEncryptedCek); + + internal static CryptographicException InvalidSignature() => new CryptographicException(Strings.InvalidSignature); + + internal static CryptographicException NullHashFound() => new CryptographicException(Strings.NullHash); + + internal static CryptographicException CipherTextLengthMismatch() => new CryptographicException(Strings.CipherTextLengthMismatch); + + internal static CryptographicException HashLengthMismatch() => new CryptographicException(Strings.HashLengthMismatch); + + internal static ArgumentException InvalidAKVPath(string masterKeyPath, bool isSystemOp) + { + string errorMessage = null == masterKeyPath ? Strings.NullAkvPath + : string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvPathTemplate, masterKeyPath); + if (isSystemOp) + { + return new ArgumentNullException(Constants.AeParamMasterKeyPath, errorMessage); + } + + return new ArgumentException(errorMessage, Constants.AeParamMasterKeyPath); + } + + internal static ArgumentException InvalidAKVUrl(string masterKeyPath) => + new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvUrlTemplate, masterKeyPath), Constants.AeParamMasterKeyPath); + + internal static Exception InvalidAKVUrlTrustedEndpoints(string masterKeyPath, string endpoints) => + new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAkvKeyPathTrustedTemplate, masterKeyPath, endpoints), + Constants.AeParamMasterKeyPath); + } +} diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs deleted file mode 100644 index f0611dd551..0000000000 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Validator.cs +++ /dev/null @@ -1,90 +0,0 @@ -// 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; -using System.Globalization; -using System.Linq; -using System.Security.Cryptography; - -namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider -{ - internal static class Validator - { - internal static void ValidateNotNull(object parameter, string name) - { - if (null == parameter) - { - throw new ArgumentNullException(name); - } - } - - internal static void ValidateNotNullOrWhitespace(string parameter, string name) - { - if (string.IsNullOrWhiteSpace(parameter)) - { - throw new ArgumentException(string.Format(Strings.NullOrWhitespaceArgument, name)); - } - } - - internal static void ValidateNotEmpty(IList parameter, string name) - { - if (parameter.Count == 0) - { - throw new ArgumentException(string.Format(Strings.EmptyArgumentInternal, name)); - } - } - - internal static void ValidateNotNullOrWhitespaceForEach(string[] parameters, string name) - { - if (parameters.Any(s => string.IsNullOrWhiteSpace(s))) - { - throw new ArgumentException(string.Format(Strings.NullOrWhitespaceForEach, name)); - } - } - - internal static void ValidateEncryptionAlgorithm(string encryptionAlgorithm, bool isSystemOp) - { - // This validates that the encryption algorithm is RSA_OAEP - if (null == encryptionAlgorithm) - { - if (isSystemOp) - { - throw new ArgumentNullException(Constants.AeParamEncryptionAlgorithm, Strings.NullAlgorithmInternal); - } - else - { - throw new ArgumentNullException(Constants.AeParamEncryptionAlgorithm, Strings.NullAlgorithm); - } - } - - if (!encryptionAlgorithm.Equals("RSA_OAEP", StringComparison.OrdinalIgnoreCase) - && !encryptionAlgorithm.Equals("RSA-OAEP", StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidKeyAlgorithm, - encryptionAlgorithm, "RSA_OAEP' or 'RSA-OAEP"), // For supporting both algorithm formats. - Constants.AeParamEncryptionAlgorithm); - } - } - - internal static void ValidateVersionByte(byte encryptedByte, byte firstVersionByte) - { - // Validate and decrypt the EncryptedColumnEncryptionKey - // Format is - // version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature - // - // keyPath is present in the encrypted column encryption key for identifying the original source of the asymmetric key pair and - // we will not validate it against the data contained in the CMK metadata (masterKeyPath). - - // Validate the version byte - if (encryptedByte != firstVersionByte) - { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, Strings.InvalidAlgorithmVersionTemplate, - encryptedByte.ToString(@"X2"), - firstVersionByte.ToString("X2")), - Constants.AeParamEncryptedCek); - } - } - } -} From ffa98ad730702454ba3473cede8851b813c466e8 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Tue, 23 Feb 2021 12:57:06 -0800 Subject: [PATCH 15/17] Formatting fix --- ...tProviderWithEnclaveProviderExample_2_0.cs | 438 +++++++++--------- 1 file changed, 219 insertions(+), 219 deletions(-) diff --git a/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs b/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs index 31f3249c4c..081fdf13b9 100644 --- a/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs +++ b/doc/samples/AzureKeyVaultProviderWithEnclaveProviderExample_2_0.cs @@ -9,242 +9,242 @@ namespace AKVEnclaveExample { - class Program - { - static readonly string s_algorithm = "RSA_OAEP"; - - // ********* Provide details here *********** - static readonly string s_akvUrl = "https://{KeyVaultName}.vault.azure.net/keys/{Key}/{KeyIdentifier}"; - static readonly string s_clientId = "{Application_Client_ID}"; - static readonly string s_clientSecret = "{Application_Client_Secret}"; - static readonly string s_tenantId = "{Azure_Key_Vault_Active_Directory_Tenant_Id}"; - static readonly string s_connectionString = "Server={Server}; Database={database}; Integrated Security=true; Column Encryption Setting=Enabled; Attestation Protocol=HGS; Enclave Attestation Url = {attestation_url_for_HGS};"; - // ****************************************** - - static void Main(string[] args) - { - // Initialize AKV provider - ClientSecretCredential clientSecretCredential = new ClientSecretCredential(s_tenantId, s_clientId, s_clientSecret); - SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); - - // Register AKV provider - SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customProviders: new Dictionary(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase) - { - { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, akvProvider} - }); - Console.WriteLine("AKV provider Registered"); - - // Create connection to database - using (SqlConnection sqlConnection = new SqlConnection(s_connectionString)) - { - string cmkName = "CMK_WITH_AKV"; - string cekName = "CEK_WITH_AKV"; - string tblName = "AKV_TEST_TABLE"; - - CustomerRecord customer = new CustomerRecord(1, @"Microsoft", @"Corporation"); - - try - { - sqlConnection.Open(); - - // Drop Objects if exists - dropObjects(sqlConnection, cmkName, cekName, tblName); - - // Create Column Master Key with AKV Url - createCMK(sqlConnection, cmkName, akvProvider); - Console.WriteLine("Column Master Key created."); - - // Create Column Encryption Key - createCEK(sqlConnection, cmkName, cekName, akvProvider); - Console.WriteLine("Column Encryption Key created."); - - // Create Table with Encrypted Columns - createTbl(sqlConnection, cekName, tblName); - Console.WriteLine("Table created with Encrypted columns."); - - // Insert Customer Record in table - insertData(sqlConnection, tblName, customer); - Console.WriteLine("Encryted data inserted."); - - // Read data from table - verifyData(sqlConnection, tblName, customer); - Console.WriteLine("Data validated successfully."); - } - finally - { - // Drop table and keys - dropObjects(sqlConnection, cmkName, cekName, tblName); - Console.WriteLine("Dropped Table, CEK and CMK"); - } - - Console.WriteLine("Completed AKV provider Sample."); - } - } - - private static void createCMK(SqlConnection sqlConnection, string cmkName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) - { - string KeyStoreProviderName = SqlColumnEncryptionAzureKeyVaultProvider.ProviderName; - - byte[] cmkSign = sqlColumnEncryptionAzureKeyVaultProvider.SignColumnMasterKeyMetadata(s_akvUrl, true); - string cmkSignStr = string.Concat("0x", BitConverter.ToString(cmkSign).Replace("-", string.Empty)); - - string sql = - $@"CREATE COLUMN MASTER KEY [{cmkName}] + class Program + { + static readonly string s_algorithm = "RSA_OAEP"; + + // ********* Provide details here *********** + static readonly string s_akvUrl = "https://{KeyVaultName}.vault.azure.net/keys/{Key}/{KeyIdentifier}"; + static readonly string s_clientId = "{Application_Client_ID}"; + static readonly string s_clientSecret = "{Application_Client_Secret}"; + static readonly string s_tenantId = "{Azure_Key_Vault_Active_Directory_Tenant_Id}"; + static readonly string s_connectionString = "Server={Server}; Database={database}; Integrated Security=true; Column Encryption Setting=Enabled; Attestation Protocol=HGS; Enclave Attestation Url = {attestation_url_for_HGS};"; + // ****************************************** + + static void Main(string[] args) + { + // Initialize AKV provider + ClientSecretCredential clientSecretCredential = new ClientSecretCredential(s_tenantId, s_clientId, s_clientSecret); + SqlColumnEncryptionAzureKeyVaultProvider akvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(clientSecretCredential); + + // Register AKV provider + SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customProviders: new Dictionary(capacity: 1, comparer: StringComparer.OrdinalIgnoreCase) + { + { SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, akvProvider} + }); + Console.WriteLine("AKV provider Registered"); + + // Create connection to database + using (SqlConnection sqlConnection = new SqlConnection(s_connectionString)) + { + string cmkName = "CMK_WITH_AKV"; + string cekName = "CEK_WITH_AKV"; + string tblName = "AKV_TEST_TABLE"; + + CustomerRecord customer = new CustomerRecord(1, @"Microsoft", @"Corporation"); + + try + { + sqlConnection.Open(); + + // Drop Objects if exists + dropObjects(sqlConnection, cmkName, cekName, tblName); + + // Create Column Master Key with AKV Url + createCMK(sqlConnection, cmkName, akvProvider); + Console.WriteLine("Column Master Key created."); + + // Create Column Encryption Key + createCEK(sqlConnection, cmkName, cekName, akvProvider); + Console.WriteLine("Column Encryption Key created."); + + // Create Table with Encrypted Columns + createTbl(sqlConnection, cekName, tblName); + Console.WriteLine("Table created with Encrypted columns."); + + // Insert Customer Record in table + insertData(sqlConnection, tblName, customer); + Console.WriteLine("Encryted data inserted."); + + // Read data from table + verifyData(sqlConnection, tblName, customer); + Console.WriteLine("Data validated successfully."); + } + finally + { + // Drop table and keys + dropObjects(sqlConnection, cmkName, cekName, tblName); + Console.WriteLine("Dropped Table, CEK and CMK"); + } + + Console.WriteLine("Completed AKV provider Sample."); + } + } + + private static void createCMK(SqlConnection sqlConnection, string cmkName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + string KeyStoreProviderName = SqlColumnEncryptionAzureKeyVaultProvider.ProviderName; + + byte[] cmkSign = sqlColumnEncryptionAzureKeyVaultProvider.SignColumnMasterKeyMetadata(s_akvUrl, true); + string cmkSignStr = string.Concat("0x", BitConverter.ToString(cmkSign).Replace("-", string.Empty)); + + string sql = + $@"CREATE COLUMN MASTER KEY [{cmkName}] WITH ( KEY_STORE_PROVIDER_NAME = N'{KeyStoreProviderName}', KEY_PATH = N'{s_akvUrl}', ENCLAVE_COMPUTATIONS (SIGNATURE = {cmkSignStr}) );"; - using (SqlCommand command = sqlConnection.CreateCommand()) - { - command.CommandText = sql; - command.ExecuteNonQuery(); - } - } - - private static void createCEK(SqlConnection sqlConnection, string cmkName, string cekName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) - { - string sql = - $@"CREATE COLUMN ENCRYPTION KEY [{cekName}] + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static void createCEK(SqlConnection sqlConnection, string cmkName, string cekName, SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + string sql = + $@"CREATE COLUMN ENCRYPTION KEY [{cekName}] WITH VALUES ( COLUMN_MASTER_KEY = [{cmkName}], ALGORITHM = '{s_algorithm}', ENCRYPTED_VALUE = {GetEncryptedValue(sqlColumnEncryptionAzureKeyVaultProvider)} )"; - using (SqlCommand command = sqlConnection.CreateCommand()) - { - command.CommandText = sql; - command.ExecuteNonQuery(); - } - } - - private static string GetEncryptedValue(SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) - { - byte[] plainTextColumnEncryptionKey = new byte[32]; - RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); - rngCsp.GetBytes(plainTextColumnEncryptionKey); - - byte[] encryptedColumnEncryptionKey = sqlColumnEncryptionAzureKeyVaultProvider.EncryptColumnEncryptionKey(s_akvUrl, s_algorithm, plainTextColumnEncryptionKey); - string EncryptedValue = string.Concat("0x", BitConverter.ToString(encryptedColumnEncryptionKey).Replace("-", string.Empty)); - return EncryptedValue; - } - - private static void createTbl(SqlConnection sqlConnection, string cekName, string tblName) - { - string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256"; - - string sql = - $@"CREATE TABLE [dbo].[{tblName}] + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static string GetEncryptedValue(SqlColumnEncryptionAzureKeyVaultProvider sqlColumnEncryptionAzureKeyVaultProvider) + { + byte[] plainTextColumnEncryptionKey = new byte[32]; + RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); + rngCsp.GetBytes(plainTextColumnEncryptionKey); + + byte[] encryptedColumnEncryptionKey = sqlColumnEncryptionAzureKeyVaultProvider.EncryptColumnEncryptionKey(s_akvUrl, s_algorithm, plainTextColumnEncryptionKey); + string EncryptedValue = string.Concat("0x", BitConverter.ToString(encryptedColumnEncryptionKey).Replace("-", string.Empty)); + return EncryptedValue; + } + + private static void createTbl(SqlConnection sqlConnection, string cekName, string tblName) + { + string ColumnEncryptionAlgorithmName = @"AEAD_AES_256_CBC_HMAC_SHA_256"; + + string sql = + $@"CREATE TABLE [dbo].[{tblName}] ( [CustomerId] [int] ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), [FirstName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}'), [LastName] [nvarchar](50) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (COLUMN_ENCRYPTION_KEY = [{cekName}], ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = '{ColumnEncryptionAlgorithmName}') )"; - using (SqlCommand command = sqlConnection.CreateCommand()) - { - command.CommandText = sql; - command.ExecuteNonQuery(); - } - } - - private static void insertData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) - { - string insertSql = $"INSERT INTO [{tblName}] (CustomerId, FirstName, LastName) VALUES (@CustomerId, @FirstName, @LastName);"; - - using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction()) - using (SqlCommand sqlCommand = new SqlCommand(insertSql, - connection: sqlConnection, transaction: sqlTransaction, - columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) - { - sqlCommand.Parameters.AddWithValue(@"CustomerId", customer.Id); - sqlCommand.Parameters.AddWithValue(@"FirstName", customer.FirstName); - sqlCommand.Parameters.AddWithValue(@"LastName", customer.LastName); - - sqlCommand.ExecuteNonQuery(); - sqlTransaction.Commit(); - } - } - - private static void verifyData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) - { - // Test INPUT parameter on an encrypted parameter - using (SqlCommand sqlCommand = new SqlCommand($"SELECT CustomerId, FirstName, LastName FROM [{tblName}] WHERE FirstName = @firstName", sqlConnection)) - { - SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft"); - customerFirstParam.Direction = System.Data.ParameterDirection.Input; - customerFirstParam.ForceColumnEncryption = true; - - using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader()) - { - ValidateResultSet(sqlDataReader); - } - } - } - - private static void ValidateResultSet(SqlDataReader sqlDataReader) - { - Console.WriteLine(" * Row available: " + sqlDataReader.HasRows); - - while (sqlDataReader.Read()) - { - if (sqlDataReader.GetInt32(0) == 1) - { - Console.WriteLine(" * Employee Id received as sent: " + sqlDataReader.GetInt32(0)); - } - else - { - Console.WriteLine("Employee Id didn't match"); - } - - if (sqlDataReader.GetString(1) == @"Microsoft") - { - Console.WriteLine(" * Employee Firstname received as sent: " + sqlDataReader.GetString(1)); - } - else - { - Console.WriteLine("Employee FirstName didn't match."); - } - - if (sqlDataReader.GetString(2) == @"Corporation") - { - Console.WriteLine(" * Employee LastName received as sent: " + sqlDataReader.GetString(2)); - } - else - { - Console.WriteLine("Employee LastName didn't match."); - } - } - } - - private static void dropObjects(SqlConnection sqlConnection, string cmkName, string cekName, string tblName) - { - using (SqlCommand cmd = sqlConnection.CreateCommand()) - { - cmd.CommandText = $@"IF EXISTS (select * from sys.objects where name = '{tblName}') BEGIN DROP TABLE [{tblName}] END"; - cmd.ExecuteNonQuery(); - cmd.CommandText = $@"IF EXISTS (select * from sys.column_encryption_keys where name = '{cekName}') BEGIN DROP COLUMN ENCRYPTION KEY [{cekName}] END"; - cmd.ExecuteNonQuery(); - cmd.CommandText = $@"IF EXISTS (select * from sys.column_master_keys where name = '{cmkName}') BEGIN DROP COLUMN MASTER KEY [{cmkName}] END"; - cmd.ExecuteNonQuery(); - } - } - - private class CustomerRecord - { - internal int Id { get; set; } - internal string FirstName { get; set; } - internal string LastName { get; set; } - - public CustomerRecord(int id, string fName, string lName) - { - Id = id; - FirstName = fName; - LastName = lName; - } - } - } + using (SqlCommand command = sqlConnection.CreateCommand()) + { + command.CommandText = sql; + command.ExecuteNonQuery(); + } + } + + private static void insertData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + string insertSql = $"INSERT INTO [{tblName}] (CustomerId, FirstName, LastName) VALUES (@CustomerId, @FirstName, @LastName);"; + + using (SqlTransaction sqlTransaction = sqlConnection.BeginTransaction()) + using (SqlCommand sqlCommand = new SqlCommand(insertSql, + connection: sqlConnection, transaction: sqlTransaction, + columnEncryptionSetting: SqlCommandColumnEncryptionSetting.Enabled)) + { + sqlCommand.Parameters.AddWithValue(@"CustomerId", customer.Id); + sqlCommand.Parameters.AddWithValue(@"FirstName", customer.FirstName); + sqlCommand.Parameters.AddWithValue(@"LastName", customer.LastName); + + sqlCommand.ExecuteNonQuery(); + sqlTransaction.Commit(); + } + } + + private static void verifyData(SqlConnection sqlConnection, string tblName, CustomerRecord customer) + { + // Test INPUT parameter on an encrypted parameter + using (SqlCommand sqlCommand = new SqlCommand($"SELECT CustomerId, FirstName, LastName FROM [{tblName}] WHERE FirstName = @firstName", sqlConnection)) + { + SqlParameter customerFirstParam = sqlCommand.Parameters.AddWithValue(@"firstName", @"Microsoft"); + customerFirstParam.Direction = System.Data.ParameterDirection.Input; + customerFirstParam.ForceColumnEncryption = true; + + using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader()) + { + ValidateResultSet(sqlDataReader); + } + } + } + + private static void ValidateResultSet(SqlDataReader sqlDataReader) + { + Console.WriteLine(" * Row available: " + sqlDataReader.HasRows); + + while (sqlDataReader.Read()) + { + if (sqlDataReader.GetInt32(0) == 1) + { + Console.WriteLine(" * Employee Id received as sent: " + sqlDataReader.GetInt32(0)); + } + else + { + Console.WriteLine("Employee Id didn't match"); + } + + if (sqlDataReader.GetString(1) == @"Microsoft") + { + Console.WriteLine(" * Employee Firstname received as sent: " + sqlDataReader.GetString(1)); + } + else + { + Console.WriteLine("Employee FirstName didn't match."); + } + + if (sqlDataReader.GetString(2) == @"Corporation") + { + Console.WriteLine(" * Employee LastName received as sent: " + sqlDataReader.GetString(2)); + } + else + { + Console.WriteLine("Employee LastName didn't match."); + } + } + } + + private static void dropObjects(SqlConnection sqlConnection, string cmkName, string cekName, string tblName) + { + using (SqlCommand cmd = sqlConnection.CreateCommand()) + { + cmd.CommandText = $@"IF EXISTS (select * from sys.objects where name = '{tblName}') BEGIN DROP TABLE [{tblName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_encryption_keys where name = '{cekName}') BEGIN DROP COLUMN ENCRYPTION KEY [{cekName}] END"; + cmd.ExecuteNonQuery(); + cmd.CommandText = $@"IF EXISTS (select * from sys.column_master_keys where name = '{cmkName}') BEGIN DROP COLUMN MASTER KEY [{cmkName}] END"; + cmd.ExecuteNonQuery(); + } + } + + private class CustomerRecord + { + internal int Id { get; set; } + internal string FirstName { get; set; } + internal string LastName { get; set; } + + public CustomerRecord(int id, string fName, string lName) + { + Id = id; + FirstName = fName; + LastName = lName; + } + } + } } // From 68853b82e0301ff02a365f6713cd49737c547034 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Fri, 26 Feb 2021 13:53:55 -0800 Subject: [PATCH 16/17] Apply suggestions from code review Co-authored-by: Javad Co-authored-by: David Engel --- .../add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs | 2 +- .../SqlColumnEncryptionAzureKeyVaultProvider.cs | 4 ++-- .../tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs index e9eb3b995d..6659192574 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs @@ -172,7 +172,7 @@ private void FetchKey(Uri vaultUri, string keyName, string keyVersion, string ke private Task> FetchKeyFromKeyVault(Uri vaultUri, string keyName, string keyVersion) { _keyClientDictionary.TryGetValue(vaultUri, out KeyClient keyClient); - return keyClient.GetKeyAsync(keyName, keyVersion); + return keyClient?.GetKeyAsync(keyName, keyVersion); } /// diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs index aebe42191e..5f4f5693a0 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/SqlColumnEncryptionAzureKeyVaultProvider.cs @@ -32,9 +32,9 @@ namespace Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider /// API only once in the lifetime of driver to register this custom provider by implementing a custom Authentication Callback mechanism. + /// Client applications must call the API only once in the lifetime of the driver to register this custom provider by implementing a custom Authentication Callback mechanism. /// /// Once the provider is registered, it can used to perform Always Encrypted operations by creating Column Master Key using Azure Key Vault Key Identifier URL. /// diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs index c042339dc2..2fc97e46d6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/AlwaysEncrypted/AKVUnitTests.cs @@ -42,7 +42,7 @@ public static void TokenCredentialTest() [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.IsAKVSetupAvailable))] public static void TokenCredentialRotationTest() { - // SqlClientCustomTokenCredential implements legacy authentication callback to request access token at client-side. + // SqlClientCustomTokenCredential implements a legacy authentication callback to request the access token from the client-side. SqlColumnEncryptionAzureKeyVaultProvider oldAkvProvider = new SqlColumnEncryptionAzureKeyVaultProvider(new SqlClientCustomTokenCredential()); ClientSecretCredential clientSecretCredential = new ClientSecretCredential(DataTestUtility.AKVTenantId, DataTestUtility.AKVClientId, DataTestUtility.AKVClientSecret); From 405e564ebda65d8d657bd43911fbf8a9df19e1fe Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Fri, 26 Feb 2021 13:54:20 -0800 Subject: [PATCH 17/17] Apply Suggestions from Code Review --- .../AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs index e9eb3b995d..f2cc735067 100644 --- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs +++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/AzureSqlKeyCryptographer.cs @@ -73,15 +73,13 @@ internal void AddKey(string keyIdentifierUri) /// internal KeyVaultKey GetKey(string keyIdentifierUri) { - if (_keyDictionary.ContainsKey(keyIdentifierUri)) + if (_keyDictionary.TryGetValue(keyIdentifierUri, out KeyVaultKey key)) { - _keyDictionary.TryGetValue(keyIdentifierUri, out KeyVaultKey key); return key; } - if (_keyFetchTaskDictionary.ContainsKey(keyIdentifierUri)) + if (_keyFetchTaskDictionary.TryGetValue(keyIdentifierUri, out Task> task)) { - _keyFetchTaskDictionary.TryGetValue(keyIdentifierUri, out Task> task); return Task.Run(() => task).GetAwaiter().GetResult(); } @@ -131,9 +129,8 @@ internal byte[] WrapKey(KeyWrapAlgorithm keyWrapAlgorithm, byte[] key, string ke private CryptographyClient GetCryptographyClient(string keyIdentifierUri) { - if (_cryptoClientDictionary.ContainsKey(keyIdentifierUri)) + if (_cryptoClientDictionary.TryGetValue(keyIdentifierUri, out CryptographyClient client)) { - _cryptoClientDictionary.TryGetValue(keyIdentifierUri, out CryptographyClient client); return client; }