1 //------------------------------------------------------------------------------
2 // <copyright file="SqlColumnEncryptionCngProvider.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 // <owner current="true" primary="true">balnee</owner>
6 // <owner current="true" primary="false">krishnib</owner>
7 //------------------------------------------------------------------------------
8 namespace System.Data.SqlClient
12 using System.Data.Common;
13 using System.Diagnostics;
14 using System.Globalization;
15 using System.Security;
16 using System.Security.Cryptography;
19 /// Provides implementation similar to certificate store provider.
20 /// A CEK encrypted with certificate provider should be decryptable by this provider and vice versa.
22 /// Envolope Format for the encrypted column encryption key
23 /// version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature
24 /// version: A single byte indicating the format version.
25 /// keyPathLength: Length of the keyPath.
26 /// ciphertextLength: ciphertext length
27 /// keyPath: keyPath used to encrypt the column encryption key. This is only used for troubleshooting purposes and is not verified during decryption.
28 /// ciphertext: Encrypted column encryption key
29 /// signature: Signature of the entire byte array. Signature is validated before decrypting the column encryption key.
31 public class SqlColumnEncryptionCngProvider : SqlColumnEncryptionKeyStoreProvider
34 /// Name for the CNG key store provider.
36 public const string ProviderName = @"MSSQL_CNG_STORE";
39 /// RSA_OAEP is the only algorithm supported for encrypting/decrypting column encryption keys using this provider.
40 /// For now, we are keeping all the providers in [....].
42 private const string RSAEncryptionAlgorithmWithOAEP = @"RSA_OAEP";
47 private readonly byte[] _version = new byte[] { 0x01 };
50 /// This function uses the asymmetric key specified by the key path
51 /// and decrypts an encrypted CEK with RSA encryption algorithm.
53 /// <param name="masterKeyPath">Complete path of an asymmetric key in CNG</param>
54 /// <param name="encryptionAlgorithm">Asymmetric Key Encryption Algorithm</param>
55 /// <param name="encryptedColumnEncryptionKey">Encrypted Column Encryption Key</param>
56 /// <returns>Plain text column encryption key</returns>
57 public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] encryptedColumnEncryptionKey)
59 // Validate the input parameters
60 ValidateNonEmptyKeyPath(masterKeyPath, isSystemOp: true);
62 if (null == encryptedColumnEncryptionKey)
64 throw SQL.NullEncryptedColumnEncryptionKey();
67 if (0 == encryptedColumnEncryptionKey.Length)
69 throw SQL.EmptyEncryptedColumnEncryptionKey();
72 // Validate encryptionAlgorithm
73 ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: true);
75 // Create RSA Provider with the given CNG name and key name
76 RSACng rsaCngProvider = CreateRSACngProvider(masterKeyPath, isSystemOp: true);
78 // Validate whether the key is RSA one or not and then get the key size
79 int keySizeInBytes = GetKeySize(rsaCngProvider);
81 // Validate and decrypt the EncryptedColumnEncryptionKey
83 // version + keyPathLength + ciphertextLength + keyPath + ciphervtext + signature
85 // keyPath is present in the encrypted column encryption key for identifying the original source of the asymmetric key pair and
86 // we will not validate it against the data contained in the CMK metadata (masterKeyPath).
88 // Validate the version byte
89 if (encryptedColumnEncryptionKey[0] != _version[0])
91 throw SQL.InvalidAlgorithmVersionInEncryptedCEK(encryptedColumnEncryptionKey[0], _version[0]);
94 // Get key path length
95 int currentIndex = _version.Length;
96 UInt16 keyPathLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
97 currentIndex += sizeof(UInt16);
99 // Get ciphertext length
100 UInt16 cipherTextLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
101 currentIndex += sizeof(UInt16);
104 // KeyPath exists only for troubleshooting purposes and doesnt need validation.
105 currentIndex += keyPathLength;
107 // validate the ciphertext length
108 if (cipherTextLength != keySizeInBytes)
110 throw SQL.InvalidCiphertextLengthInEncryptedCEKCng(cipherTextLength, keySizeInBytes, masterKeyPath);
113 // Validate the signature length
114 // Signature length should be same as the key side for RSA PKCSv1.5
115 int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength;
116 if (signatureLength != keySizeInBytes)
118 throw SQL.InvalidSignatureInEncryptedCEKCng(signatureLength, keySizeInBytes, masterKeyPath);
122 byte[] cipherText = new byte[cipherTextLength];
123 Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, cipherText, 0, cipherText.Length);
124 currentIndex += cipherTextLength;
127 byte[] signature = new byte[signatureLength];
128 Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, signature, 0, signature.Length);
130 // Compute the hash to validate the signature
132 using (SHA256Cng sha256 = new SHA256Cng())
134 sha256.TransformFinalBlock(encryptedColumnEncryptionKey, 0, encryptedColumnEncryptionKey.Length - signature.Length);
138 Debug.Assert(hash != null, @"hash should not be null while decrypting encrypted column encryption key.");
140 // Validate the signature
141 if (!RSAVerifySignature(hash, signature, rsaCngProvider))
143 throw SQL.InvalidSignature(masterKeyPath);
147 return RSADecrypt(rsaCngProvider, cipherText);
151 /// This function uses the asymmetric key specified by the key path
152 /// and encrypts CEK with RSA encryption algorithm.
154 /// <param name="keyPath">Complete path of an asymmetric key in AKV</param>
155 /// <param name="encryptionAlgorithm">Asymmetric Key Encryption Algorithm</param>
156 /// <param name="columnEncryptionKey">Plain text column encryption key</param>
157 /// <returns>Encrypted column encryption key</returns>
158 public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey)
160 // Validate the input parameters
161 ValidateNonEmptyKeyPath(masterKeyPath, isSystemOp: false);
163 if (null == columnEncryptionKey)
165 throw SQL.NullColumnEncryptionKey();
167 else if (0 == columnEncryptionKey.Length)
169 throw SQL.EmptyColumnEncryptionKey();
172 // Validate encryptionAlgorithm
173 ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: false);
175 // CreateCNGProviderWithKey
176 RSACng rsaCngProvider = CreateRSACngProvider(masterKeyPath, isSystemOp: false);
178 // Validate whether the key is RSA one or not and then get the key size
179 int keySizeInBytes = GetKeySize(rsaCngProvider);
181 // Construct the encryptedColumnEncryptionKey
183 // version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature
185 // We currently only support one version
186 byte[] version = new byte[] { _version[0] };
188 // Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath
189 byte[] masterKeyPathBytes = Encoding.Unicode.GetBytes(masterKeyPath.ToLowerInvariant());
190 byte[] keyPathLength = BitConverter.GetBytes((Int16)masterKeyPathBytes.Length);
192 // Encrypt the plain text
193 byte[] cipherText = RSAEncrypt(rsaCngProvider, columnEncryptionKey);
194 byte[] cipherTextLength = BitConverter.GetBytes((Int16)cipherText.Length);
195 Debug.Assert(cipherText.Length == keySizeInBytes, @"cipherText length does not match the RSA key size");
198 // SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext)
200 using (SHA256Cng sha256 = new SHA256Cng())
202 sha256.TransformBlock(version, 0, version.Length, version, 0);
203 sha256.TransformBlock(keyPathLength, 0, keyPathLength.Length, keyPathLength, 0);
204 sha256.TransformBlock(cipherTextLength, 0, cipherTextLength.Length, cipherTextLength, 0);
205 sha256.TransformBlock(masterKeyPathBytes, 0, masterKeyPathBytes.Length, masterKeyPathBytes, 0);
206 sha256.TransformFinalBlock(cipherText, 0, cipherText.Length);
211 byte[] signedHash = RSASignHashedData(hash, rsaCngProvider);
212 Debug.Assert(signedHash.Length == keySizeInBytes, @"signed hash length does not match the RSA key size");
213 Debug.Assert(RSAVerifySignature(hash, signedHash, rsaCngProvider), @"Invalid signature of the encrypted column encryption key computed.");
215 // Construct the encrypted column encryption key
216 // EncryptedColumnEncryptionKey = version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature
217 int encryptedColumnEncryptionKeyLength = version.Length + cipherTextLength.Length + keyPathLength.Length + cipherText.Length + masterKeyPathBytes.Length + signedHash.Length;
218 byte[] encryptedColumnEncryptionKey = new byte[encryptedColumnEncryptionKeyLength];
221 int currentIndex = 0;
222 Buffer.BlockCopy(version, 0, encryptedColumnEncryptionKey, currentIndex, version.Length);
223 currentIndex += version.Length;
225 // Copy key path length
226 Buffer.BlockCopy(keyPathLength, 0, encryptedColumnEncryptionKey, currentIndex, keyPathLength.Length);
227 currentIndex += keyPathLength.Length;
229 // Copy ciphertext length
230 Buffer.BlockCopy(cipherTextLength, 0, encryptedColumnEncryptionKey, currentIndex, cipherTextLength.Length);
231 currentIndex += cipherTextLength.Length;
234 Buffer.BlockCopy(masterKeyPathBytes, 0, encryptedColumnEncryptionKey, currentIndex, masterKeyPathBytes.Length);
235 currentIndex += masterKeyPathBytes.Length;
238 Buffer.BlockCopy(cipherText, 0, encryptedColumnEncryptionKey, currentIndex, cipherText.Length);
239 currentIndex += cipherText.Length;
241 // copy the signature
242 Buffer.BlockCopy(signedHash, 0, encryptedColumnEncryptionKey, currentIndex, signedHash.Length);
244 return encryptedColumnEncryptionKey;
248 /// This function validates that the encryption algorithm is RSA_OAEP and if it is not,
249 /// then throws an exception
251 /// <param name="encryptionAlgorithm">Asymmetric key encryptio algorithm</param>
252 /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
253 private void ValidateEncryptionAlgorithm(string encryptionAlgorithm, bool isSystemOp)
255 // This validates that the encryption algorithm is RSA_OAEP
256 if (null == encryptionAlgorithm)
258 throw SQL.NullKeyEncryptionAlgorithm(isSystemOp);
261 if (!string.Equals(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, StringComparison.OrdinalIgnoreCase))
263 throw SQL.InvalidKeyEncryptionAlgorithm(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, isSystemOp);
268 /// Checks if the CNG key path is Empty or Null (and raises exception if they are).
270 /// <param name="masterKeyPath">keypath containing the CNG provider name and key name</param>
271 /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
272 private void ValidateNonEmptyKeyPath(string masterKeyPath, bool isSystemOp)
274 if (string.IsNullOrWhiteSpace(masterKeyPath))
276 if (null == masterKeyPath)
278 throw SQL.NullCngKeyPath(isSystemOp);
282 throw SQL.InvalidCngPath(masterKeyPath, isSystemOp);
288 /// Encrypt the text using specified CNG key.
290 /// <param name="rsaCngProvider">RSA CNG Provider.</param>
291 /// <param name="columnEncryptionKey">Plain text Column Encryption Key.</param>
292 /// <returns>Returns an encrypted blob or throws an exception if there are any errors.</returns>
293 private byte[] RSAEncrypt(RSACng rsaCngProvider, byte[] columnEncryptionKey)
295 Debug.Assert(columnEncryptionKey != null);
296 Debug.Assert(rsaCngProvider != null);
298 return rsaCngProvider.Encrypt(columnEncryptionKey, RSAEncryptionPadding.OaepSHA1);
302 /// Decrypt the text using the specified CNG key.
304 /// <param name="rsaCngProvider">RSA CNG Provider.</param>
305 /// <param name="encryptedColumnEncryptionKey">Encrypted Column Encryption Key.</param>
306 /// <returns>Returns the decrypted plaintext Column Encryption Key or throws an exception if there are any errors.</returns>
307 private byte[] RSADecrypt(RSACng rsaCngProvider, byte[] encryptedColumnEncryptionKey)
309 Debug.Assert((encryptedColumnEncryptionKey != null) && (encryptedColumnEncryptionKey.Length != 0));
310 Debug.Assert(rsaCngProvider != null);
312 return rsaCngProvider.Decrypt(encryptedColumnEncryptionKey, RSAEncryptionPadding.OaepSHA1);
316 /// Generates signature based on RSA PKCS#v1.5 scheme using a specified CNG Key.
318 /// <param name="dataToSign">Text to sign.</param>
319 /// <param name="rsaCngProvider">RSA CNG Provider.</param>
320 /// <returns>Signature</returns>
321 private byte[] RSASignHashedData(byte[] dataToSign, RSACng rsaCngProvider)
323 Debug.Assert((dataToSign != null) && (dataToSign.Length != 0));
324 Debug.Assert(rsaCngProvider != null);
326 return rsaCngProvider.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
330 /// Verifies the given RSA PKCSv1.5 signature.
332 /// <param name="dataToVerify"></param>
333 /// <param name="signature"></param>
334 /// <param name="rsaCngProvider">RSA CNG Provider.</param>
335 /// <returns>true if signature is valid, false if it is not valid</returns>
336 private bool RSAVerifySignature(byte[] dataToVerify, byte[] signature, RSACng rsaCngProvider)
338 Debug.Assert((dataToVerify != null) && (dataToVerify.Length != 0));
339 Debug.Assert((signature != null) && (signature.Length != 0));
340 Debug.Assert(rsaCngProvider != null);
342 return rsaCngProvider.VerifyData(dataToVerify, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
346 /// Gets the public Key size in bytes
348 /// <param name="rsaCngProvider">RSA CNG Provider.</param>
349 /// <returns>Key size in bytes</returns>
350 private int GetKeySize(RSACng rsaCngProvider)
352 Debug.Assert(rsaCngProvider != null);
354 return rsaCngProvider.KeySize / 8; // Convert from bits to byte
358 /// Creates a RSACng object from the given keyName
360 /// <param name="keyPath"></param>
361 /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
362 /// <returns></returns>
363 private RSACng CreateRSACngProvider(string keyPath, bool isSystemOp)
365 // Get CNGProvider and the KeyID
366 string cngProviderName;
367 string keyIdentifier;
368 GetCngProviderAndKeyId(keyPath, isSystemOp, out cngProviderName, out keyIdentifier);
370 CngProvider cngProvider = new CngProvider(cngProviderName);
375 cngKey = CngKey.Open(keyIdentifier, cngProvider);
377 catch (CryptographicException)
379 throw SQL.InvalidCngKey(keyPath, cngProviderName, keyIdentifier, isSystemOp);
382 return new RSACng(cngKey);
386 /// Extracts the CNG provider and key name from the key path
388 /// <param name="masterKeyPath">keypath in the format [CNG Provider]\[KeyName]</param>
389 /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
390 /// <param name="cngProvider">CNG Provider</param>
391 /// <param name="keyIdentifier">Key identifier inside the CNG provider</param>
392 private void GetCngProviderAndKeyId(string keyPath, bool isSystemOp, out string cngProvider, out string keyIdentifier)
394 int indexOfSlash = keyPath.IndexOf(@"/");
395 if (indexOfSlash == -1)
397 throw SQL.InvalidCngPath(keyPath, isSystemOp);
400 cngProvider = keyPath.Substring(0, indexOfSlash);
401 keyIdentifier = keyPath.Substring(indexOfSlash + 1, keyPath.Length - (indexOfSlash + 1));
403 if (cngProvider.Length == 0)
405 throw SQL.EmptyCngName(keyPath, isSystemOp);
408 if (keyIdentifier.Length == 0)
410 throw SQL.EmptyCngKeyId(keyPath, isSystemOp);