//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // // balnee // krishnib //------------------------------------------------------------------------------ namespace System.Data.SqlClient { using System; using System.Text; using System.Data.Common; using System.Diagnostics; using System.Globalization; using System.Security; using System.Security.Cryptography; using Microsoft.Win32; /// /// Provides implementation similar to certificate store provider. /// A CEK encrypted with certificate store provider should be decryptable by this provider and vice versa. /// /// Envolope Format for the encrypted column encryption key /// version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature /// version: A single byte indicating the format version. /// keyPathLength: Length of the keyPath. /// ciphertextLength: ciphertext length /// keyPath: keyPath used to encrypt the column encryption key. This is only used for troubleshooting purposes and is not verified during decryption. /// ciphertext: Encrypted column encryption key /// signature: Signature of the entire byte array. Signature is validated before decrypting the column encryption key. /// public class SqlColumnEncryptionCspProvider : SqlColumnEncryptionKeyStoreProvider { /// /// Name for the CSP key store provider. /// public const string ProviderName = @"MSSQL_CSP_PROVIDER"; /// /// RSA_OAEP is the only algorithm supported for encrypting/decrypting column encryption keys using this provider. /// For now, we are keeping all the providers in [....]. /// private const string RSAEncryptionAlgorithmWithOAEP = @"RSA_OAEP"; /// /// Hashing algoirthm used for signing /// private const string HashingAlgorithm = @"SHA256"; /// /// Algorithm version /// private readonly byte[] _version = new byte[] { 0x01 }; /// /// 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 CSP /// Asymmetric Key Encryption Algorithm /// Encrypted Column Encryption Key /// Plain text column encryption key public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] encryptedColumnEncryptionKey) { // Validate the input parameters ValidateNonEmptyCSPKeyPath(masterKeyPath, isSystemOp: true); if (null == encryptedColumnEncryptionKey) { throw SQL.NullEncryptedColumnEncryptionKey(); } if (0 == encryptedColumnEncryptionKey.Length) { throw SQL.EmptyEncryptedColumnEncryptionKey(); } // Validate encryptionAlgorithm ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: true); // Create RSA Provider with the given CSP name and key name RSACryptoServiceProvider rsaProvider = CreateRSACryptoProvider(masterKeyPath, isSystemOp: true); // Validate whether the key is RSA one or not and then get the key size int keySizeInBytes = GetKeySize(rsaProvider); // Validate and decrypt the EncryptedColumnEncryptionKey // Format is // version + keyPathLength + ciphertextLength + keyPath + ciphervtext + 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] != _version[0]) { throw SQL.InvalidAlgorithmVersionInEncryptedCEK(encryptedColumnEncryptionKey[0], _version[0]); } // Get key path length int currentIndex = _version.Length; UInt16 keyPathLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex); currentIndex += sizeof(UInt16); // Get ciphertext length UInt16 cipherTextLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex); currentIndex += sizeof(UInt16); // Skip KeyPath // KeyPath exists only for troubleshooting purposes and doesnt need validation. currentIndex += keyPathLength; // validate the ciphertext length if (cipherTextLength != keySizeInBytes) { throw SQL.InvalidCiphertextLengthInEncryptedCEKCsp(cipherTextLength, keySizeInBytes, masterKeyPath); } // Validate the signature length // Signature length should be same as the key side for RSA PKCSv1.5 int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength; if (signatureLength != keySizeInBytes) { throw SQL.InvalidSignatureInEncryptedCEKCsp(signatureLength, keySizeInBytes, masterKeyPath); } // Get ciphertext byte[] cipherText = new byte[cipherTextLength]; Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, cipherText, 0, cipherText.Length); currentIndex += cipherTextLength; // Get signature byte[] signature = new byte[signatureLength]; Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, signature, 0, signature.Length); // Compute the hash to validate the signature byte[] hash; using (SHA256Cng sha256 = new SHA256Cng()) { sha256.TransformFinalBlock(encryptedColumnEncryptionKey, 0, encryptedColumnEncryptionKey.Length - signature.Length); hash = sha256.Hash; } Debug.Assert(hash != null, @"hash should not be null while decrypting encrypted column encryption key."); // Validate the signature if (!RSAVerifySignature(hash, signature, rsaProvider)) { throw SQL.InvalidSignature(masterKeyPath); } // Decrypt the CEK return RSADecrypt(rsaProvider, cipherText); } /// /// 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 /// Asymmetric Key Encryption Algorithm /// Plain text column encryption key /// Encrypted column encryption key public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey) { // Validate the input parameters ValidateNonEmptyCSPKeyPath(masterKeyPath, isSystemOp: false); if (null == columnEncryptionKey) { throw SQL.NullColumnEncryptionKey(); } else if (0 == columnEncryptionKey.Length) { throw SQL.EmptyColumnEncryptionKey(); } // Validate encryptionAlgorithm ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: false); // Create RSA Provider with the given CSP name and key name RSACryptoServiceProvider rsaProvider = CreateRSACryptoProvider(masterKeyPath, isSystemOp: false); // Validate whether the key is RSA one or not and then get the key size int keySizeInBytes = GetKeySize(rsaProvider); // Construct the encryptedColumnEncryptionKey // Format is // version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature // // We currently only support one version byte[] version = new byte[] { _version[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); // Encrypt the plain text byte[] cipherText = RSAEncrypt(rsaProvider, columnEncryptionKey); byte[] cipherTextLength = BitConverter.GetBytes((Int16)cipherText.Length); Debug.Assert(cipherText.Length == keySizeInBytes, @"cipherText length does not match the RSA key size"); // Compute hash // SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext) byte[] hash; using (SHA256Cng sha256 = new SHA256Cng()) { 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; } // Sign the hash byte[] signedHash = RSASignHashedData(hash, rsaProvider); Debug.Assert(signedHash.Length == keySizeInBytes, @"signed hash length does not match the RSA key size"); Debug.Assert(RSAVerifySignature(hash, signedHash, rsaProvider), @"Invalid signature of the encrypted column encryption key computed."); // 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; // 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; } /// /// This function validates that the encryption algorithm is RSA_OAEP and if it is not, /// then throws an exception /// /// Asymmetric key encryptio algorithm /// Indicates if ADO.NET calls or the customer calls the API private void ValidateEncryptionAlgorithm(string encryptionAlgorithm, bool isSystemOp) { // This validates that the encryption algorithm is RSA_OAEP if (null == encryptionAlgorithm) { throw SQL.NullKeyEncryptionAlgorithm(isSystemOp); } if (string.Equals(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, StringComparison.OrdinalIgnoreCase) != true) { throw SQL.InvalidKeyEncryptionAlgorithm(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, isSystemOp); } } /// /// Checks if the CSP key path is Empty or Null (and raises exception if they are). /// /// CSP key path. /// Indicates if ADO.NET calls or the customer calls the API private void ValidateNonEmptyCSPKeyPath(string masterKeyPath, bool isSystemOp) { if (string.IsNullOrWhiteSpace(masterKeyPath)) { if (null == masterKeyPath) { throw SQL.NullCspKeyPath(isSystemOp); } else { throw SQL.InvalidCspPath(masterKeyPath, isSystemOp); } } } /// /// Encrypt the text using specified CSP key. /// /// CSP key path. /// Encryption Algorithm. /// Plain text Column Encryption Key. /// Returns an encrypted blob or throws an exception if there are any errors. private byte[] RSAEncrypt(RSACryptoServiceProvider rscp, byte[] columnEncryptionKey) { Debug.Assert(columnEncryptionKey != null); Debug.Assert(rscp != null); return rscp.Encrypt(columnEncryptionKey, fOAEP: true); } /// /// Decrypt the text using specified CSP key. /// /// CSP 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[] RSADecrypt(RSACryptoServiceProvider rscp, byte[] encryptedColumnEncryptionKey) { Debug.Assert((encryptedColumnEncryptionKey != null) && (encryptedColumnEncryptionKey.Length != 0)); Debug.Assert(rscp != null); return rscp.Decrypt(encryptedColumnEncryptionKey, fOAEP: true); } /// /// Generates signature based on RSA PKCS#v1.5 scheme using a specified CSP Key URL. /// /// Text to sign. /// RSA Provider with a given key /// Signature private byte[] RSASignHashedData(byte[] dataToSign, RSACryptoServiceProvider rscp) { Debug.Assert((dataToSign != null) && (dataToSign.Length != 0)); Debug.Assert(rscp != null); return rscp.SignData(dataToSign, HashingAlgorithm); } /// /// Verifies the given RSA PKCSv1.5 signature. /// /// /// /// RSA Provider with a given key /// true if signature is valid, false if it is not valid private bool RSAVerifySignature(byte[] dataToVerify, byte[] signature, RSACryptoServiceProvider rscp) { Debug.Assert((dataToVerify != null) && (dataToVerify.Length != 0)); Debug.Assert((signature != null) && (signature.Length != 0)); Debug.Assert(rscp != null); return rscp.VerifyData(dataToVerify, HashingAlgorithm, signature); } /// /// Gets the public Key size in bytes /// /// RSA Provider with a given key /// Key size in bytes private int GetKeySize(RSACryptoServiceProvider rscp) { Debug.Assert(rscp != null); return rscp.KeySize / 8; } /// /// Creates a RSACryptoServiceProvider from the given key path which contains both CSP name and key name /// /// key path in the format of [CAPI provider name]\[key name] /// Indicates if ADO.NET calls or the customer calls the API /// private RSACryptoServiceProvider CreateRSACryptoProvider(string keyPath, bool isSystemOp) { // Get CNGProvider and the KeyID string cspProviderName; string keyName; GetCspProviderAndKeyName(keyPath, isSystemOp, out cspProviderName, out keyName); // Verify the existence of CSP and then get the provider type int providerType = GetProviderType(cspProviderName, keyPath, isSystemOp); // Create a new instance of CspParameters for an RSA container. CspParameters cspParams = new CspParameters(providerType, cspProviderName, keyName); cspParams.Flags = CspProviderFlags.UseExistingKey; RSACryptoServiceProvider rscp = null; try { //Create a new instance of RSACryptoServiceProvider rscp = new RSACryptoServiceProvider(cspParams); } catch (CryptographicException e) { const int KEYSETDOESNOTEXIST = -2146893802; if (e.HResult == KEYSETDOESNOTEXIST) { // Key does not exist throw SQL.InvalidCspKeyIdentifier(keyName, keyPath, isSystemOp); } else { // bubble up the exception throw; } } return rscp; } /// /// Extracts the CSP provider name and key name from the given key path /// /// key path in the format of [CSP provider name]\[key name] /// Indicates if ADO.NET calls or the customer calls the API /// output containing the CSP provider name /// output containing the key name private void GetCspProviderAndKeyName(string keyPath, bool isSystemOp, out string cspProviderName, out string keyIdentifier) { int indexOfSlash = keyPath.IndexOf(@"/"); if (indexOfSlash == -1) { throw SQL.InvalidCspPath(keyPath, isSystemOp); } cspProviderName = keyPath.Substring(0, indexOfSlash); keyIdentifier = keyPath.Substring(indexOfSlash + 1, keyPath.Length - (indexOfSlash + 1)); if (cspProviderName.Length == 0) { throw SQL.EmptyCspName(keyPath, isSystemOp); } if (keyIdentifier.Length == 0) { throw SQL.EmptyCspKeyId(keyPath, isSystemOp); } } /// /// Gets the provider type from a given CAPI provider name /// /// CAPI provider name /// key path in the format of [CSP provider name]\[key name] /// Indicates if ADO.NET calls or the customer calls the API /// private int GetProviderType(string providerName, string keyPath, bool isSystemOp) { string keyName = String.Format(@"SOFTWARE\Microsoft\Cryptography\Defaults\Provider\{0}", providerName); RegistryKey key = Registry.LocalMachine.OpenSubKey(keyName); if (key == null) { throw SQL.InvalidCspName(providerName, keyPath, isSystemOp); } int providerType = (int)key.GetValue(@"Type"); key.Close(); return providerType; } } }