1 //------------------------------------------------------------------------------
2 // <copyright file="SqlColumnEncryptionCspProvider.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 // <owner current="true" primary="true">balnee</owner>
6 // <owner current="true" primary="false">krishnib</owner>
7 //------------------------------------------------------------------------------
8 namespace System.Data.SqlClient
9 {
10     using System;
11     using System.Text;
12     using System.Data.Common;
13     using System.Diagnostics;
14     using System.Globalization;
15     using System.Security;
16     using System.Security.Cryptography;
17     using Microsoft.Win32;
18 
19     /// <summary>
20     /// Provides implementation similar to certificate store provider.
21     /// A CEK encrypted with certificate store provider should be decryptable by this provider and vice versa.
22     ///
23     /// Envolope Format for the encrypted column encryption key
24     ///           version + keyPathLength + ciphertextLength + keyPath + ciphertext +  signature
25     /// version: A single byte indicating the format version.
26     /// keyPathLength: Length of the keyPath.
27     /// ciphertextLength: ciphertext length
28     /// keyPath: keyPath used to encrypt the column encryption key. This is only used for troubleshooting purposes and is not verified during decryption.
29     /// ciphertext: Encrypted column encryption key
30     /// signature: Signature of the entire byte array. Signature is validated before decrypting the column encryption key.
31     /// </summary>
32     public class SqlColumnEncryptionCspProvider : SqlColumnEncryptionKeyStoreProvider
33     {
34         /// <summary>
35         /// Name for the CSP key store provider.
36         /// </summary>
37         public const string ProviderName = @"MSSQL_CSP_PROVIDER";
38 
39         /// <summary>
40         /// RSA_OAEP is the only algorithm supported for encrypting/decrypting column encryption keys using this provider.
41         /// For now, we are keeping all the providers in sync.
42         /// </summary>
43         private const string RSAEncryptionAlgorithmWithOAEP = @"RSA_OAEP";
44 
45         /// <summary>
46         /// Hashing algoirthm used for signing
47         /// </summary>
48         private const string HashingAlgorithm = @"SHA256";
49 
50         /// <summary>
51         /// Algorithm version
52         /// </summary>
53         private readonly byte[] _version = new byte[] { 0x01 };
54 
55         /// <summary>
56         /// This function uses the asymmetric key specified by the key path
57         /// and decrypts an encrypted CEK with RSA encryption algorithm.
58         /// </summary>
59         /// <param name="masterKeyPath">Complete path of an asymmetric key in CSP</param>
60         /// <param name="encryptionAlgorithm">Asymmetric Key Encryption Algorithm</param>
61         /// <param name="encryptedColumnEncryptionKey">Encrypted Column Encryption Key</param>
62         /// <returns>Plain text column encryption key</returns>
DecryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] encryptedColumnEncryptionKey)63         public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] encryptedColumnEncryptionKey)
64         {
65             // Validate the input parameters
66             ValidateNonEmptyCSPKeyPath(masterKeyPath, isSystemOp: true);
67 
68             if (null == encryptedColumnEncryptionKey)
69             {
70                 throw SQL.NullEncryptedColumnEncryptionKey();
71             }
72 
73             if (0 == encryptedColumnEncryptionKey.Length)
74             {
75                 throw SQL.EmptyEncryptedColumnEncryptionKey();
76             }
77 
78             // Validate encryptionAlgorithm
79             ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: true);
80 
81             // Create RSA Provider with the given CSP name and key name
82             RSACryptoServiceProvider rsaProvider = CreateRSACryptoProvider(masterKeyPath, isSystemOp: true);
83 
84             // Validate whether the key is RSA one or not and then get the key size
85             int keySizeInBytes = GetKeySize(rsaProvider);
86 
87             // Validate and decrypt the EncryptedColumnEncryptionKey
88             // Format is
89             //           version + keyPathLength + ciphertextLength + keyPath + ciphervtext +  signature
90             //
91             // keyPath is present in the encrypted column encryption key for identifying the original source of the asymmetric key pair and
92             // we will not validate it against the data contained in the CMK metadata (masterKeyPath).
93 
94             // Validate the version byte
95             if (encryptedColumnEncryptionKey[0] != _version[0])
96             {
97                 throw SQL.InvalidAlgorithmVersionInEncryptedCEK(encryptedColumnEncryptionKey[0], _version[0]);
98             }
99 
100             // Get key path length
101             int currentIndex = _version.Length;
102             UInt16 keyPathLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
103             currentIndex += sizeof(UInt16);
104 
105             // Get ciphertext length
106             UInt16 cipherTextLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
107             currentIndex += sizeof(UInt16);
108 
109             // Skip KeyPath
110             // KeyPath exists only for troubleshooting purposes and doesnt need validation.
111             currentIndex += keyPathLength;
112 
113             // validate the ciphertext length
114             if (cipherTextLength != keySizeInBytes)
115             {
116                 throw SQL.InvalidCiphertextLengthInEncryptedCEKCsp(cipherTextLength, keySizeInBytes, masterKeyPath);
117             }
118 
119             // Validate the signature length
120             // Signature length should be same as the key side for RSA PKCSv1.5
121             int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength;
122             if (signatureLength != keySizeInBytes)
123             {
124                 throw SQL.InvalidSignatureInEncryptedCEKCsp(signatureLength, keySizeInBytes, masterKeyPath);
125             }
126 
127             // Get ciphertext
128             byte[] cipherText = new byte[cipherTextLength];
129             Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, cipherText, 0, cipherText.Length);
130             currentIndex += cipherTextLength;
131 
132             // Get signature
133             byte[] signature = new byte[signatureLength];
134             Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, signature, 0, signature.Length);
135 
136             // Compute the hash to validate the signature
137             byte[] hash;
138             using (SHA256Cng sha256 = new SHA256Cng())
139             {
140                 sha256.TransformFinalBlock(encryptedColumnEncryptionKey, 0, encryptedColumnEncryptionKey.Length - signature.Length);
141                 hash = sha256.Hash;
142             }
143 
144             Debug.Assert(hash != null, @"hash should not be null while decrypting encrypted column encryption key.");
145 
146             // Validate the signature
147             if (!RSAVerifySignature(hash, signature, rsaProvider))
148             {
149                 throw SQL.InvalidSignature(masterKeyPath);
150             }
151 
152             // Decrypt the CEK
153             return RSADecrypt(rsaProvider, cipherText);
154         }
155 
156         /// <summary>
157         /// This function uses the asymmetric key specified by the key path
158         /// and encrypts CEK with RSA encryption algorithm.
159         /// </summary>
160         /// <param name="keyPath">Complete path of an asymmetric key in AKV</param>
161         /// <param name="encryptionAlgorithm">Asymmetric Key Encryption Algorithm</param>
162         /// <param name="columnEncryptionKey">Plain text column encryption key</param>
163         /// <returns>Encrypted column encryption key</returns>
EncryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey)164         public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey)
165         {
166             // Validate the input parameters
167             ValidateNonEmptyCSPKeyPath(masterKeyPath, isSystemOp: false);
168 
169             if (null == columnEncryptionKey)
170             {
171                 throw SQL.NullColumnEncryptionKey();
172             }
173             else if (0 == columnEncryptionKey.Length)
174             {
175                 throw SQL.EmptyColumnEncryptionKey();
176             }
177 
178             // Validate encryptionAlgorithm
179             ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: false);
180 
181             // Create RSA Provider with the given CSP name and key name
182             RSACryptoServiceProvider rsaProvider = CreateRSACryptoProvider(masterKeyPath, isSystemOp: false);
183 
184             // Validate whether the key is RSA one or not and then get the key size
185             int keySizeInBytes = GetKeySize(rsaProvider);
186 
187             // Construct the encryptedColumnEncryptionKey
188             // Format is
189             //          version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature
190             //
191             // We currently only support one version
192             byte[] version = new byte[] { _version[0] };
193 
194             // Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath
195             byte[] masterKeyPathBytes = Encoding.Unicode.GetBytes(masterKeyPath.ToLowerInvariant());
196             byte[] keyPathLength = BitConverter.GetBytes((Int16)masterKeyPathBytes.Length);
197 
198             // Encrypt the plain text
199             byte[] cipherText = RSAEncrypt(rsaProvider, columnEncryptionKey);
200             byte[] cipherTextLength = BitConverter.GetBytes((Int16)cipherText.Length);
201             Debug.Assert(cipherText.Length == keySizeInBytes, @"cipherText length does not match the RSA key size");
202 
203             // Compute hash
204             // SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext)
205             byte[] hash;
206             using (SHA256Cng sha256 = new SHA256Cng())
207             {
208                 sha256.TransformBlock(version, 0, version.Length, version, 0);
209                 sha256.TransformBlock(keyPathLength, 0, keyPathLength.Length, keyPathLength, 0);
210                 sha256.TransformBlock(cipherTextLength, 0, cipherTextLength.Length, cipherTextLength, 0);
211                 sha256.TransformBlock(masterKeyPathBytes, 0, masterKeyPathBytes.Length, masterKeyPathBytes, 0);
212                 sha256.TransformFinalBlock(cipherText, 0, cipherText.Length);
213                 hash = sha256.Hash;
214             }
215 
216             // Sign the hash
217             byte[] signedHash = RSASignHashedData(hash, rsaProvider);
218             Debug.Assert(signedHash.Length == keySizeInBytes, @"signed hash length does not match the RSA key size");
219             Debug.Assert(RSAVerifySignature(hash, signedHash, rsaProvider), @"Invalid signature of the encrypted column encryption key computed.");
220 
221             // Construct the encrypted column encryption key
222             // EncryptedColumnEncryptionKey = version + keyPathLength + ciphertextLength + keyPath + ciphertext +  signature
223             int encryptedColumnEncryptionKeyLength = version.Length + cipherTextLength.Length + keyPathLength.Length + cipherText.Length + masterKeyPathBytes.Length + signedHash.Length;
224             byte[] encryptedColumnEncryptionKey = new byte[encryptedColumnEncryptionKeyLength];
225 
226             // Copy version byte
227             int currentIndex = 0;
228             Buffer.BlockCopy(version, 0, encryptedColumnEncryptionKey, currentIndex, version.Length);
229             currentIndex += version.Length;
230 
231             // Copy key path length
232             Buffer.BlockCopy(keyPathLength, 0, encryptedColumnEncryptionKey, currentIndex, keyPathLength.Length);
233             currentIndex += keyPathLength.Length;
234 
235             // Copy ciphertext length
236             Buffer.BlockCopy(cipherTextLength, 0, encryptedColumnEncryptionKey, currentIndex, cipherTextLength.Length);
237             currentIndex += cipherTextLength.Length;
238 
239             // Copy key path
240             Buffer.BlockCopy(masterKeyPathBytes, 0, encryptedColumnEncryptionKey, currentIndex, masterKeyPathBytes.Length);
241             currentIndex += masterKeyPathBytes.Length;
242 
243             // Copy ciphertext
244             Buffer.BlockCopy(cipherText, 0, encryptedColumnEncryptionKey, currentIndex, cipherText.Length);
245             currentIndex += cipherText.Length;
246 
247             // copy the signature
248             Buffer.BlockCopy(signedHash, 0, encryptedColumnEncryptionKey, currentIndex, signedHash.Length);
249 
250             return encryptedColumnEncryptionKey;
251         }
252 
253         /// <summary>
254         /// This function validates that the encryption algorithm is RSA_OAEP and if it is not,
255         /// then throws an exception
256         /// </summary>
257         /// <param name="encryptionAlgorithm">Asymmetric key encryptio algorithm</param>
258         /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
ValidateEncryptionAlgorithm(string encryptionAlgorithm, bool isSystemOp)259         private void ValidateEncryptionAlgorithm(string encryptionAlgorithm, bool isSystemOp)
260         {
261             // This validates that the encryption algorithm is RSA_OAEP
262             if (null == encryptionAlgorithm)
263             {
264                 throw SQL.NullKeyEncryptionAlgorithm(isSystemOp);
265             }
266 
267             if (string.Equals(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, StringComparison.OrdinalIgnoreCase) != true)
268             {
269                 throw SQL.InvalidKeyEncryptionAlgorithm(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, isSystemOp);
270             }
271         }
272 
273 
274         /// <summary>
275         /// Checks if the CSP key path is Empty or Null (and raises exception if they are).
276         /// </summary>
277         /// <param name="masterKeyPath">CSP key path.</param>
278         /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
ValidateNonEmptyCSPKeyPath(string masterKeyPath, bool isSystemOp)279         private void ValidateNonEmptyCSPKeyPath(string masterKeyPath, bool isSystemOp)
280         {
281             if (string.IsNullOrWhiteSpace(masterKeyPath))
282             {
283                 if (null == masterKeyPath)
284                 {
285                     throw SQL.NullCspKeyPath(isSystemOp);
286                 }
287                 else
288                 {
289                     throw SQL.InvalidCspPath(masterKeyPath, isSystemOp);
290                 }
291             }
292         }
293 
294         /// <summary>
295         /// Encrypt the text using specified CSP key.
296         /// </summary>
297         /// <param name="masterKeyPath">CSP key path.</param>
298         /// <param name="encryptionAlgorithm">Encryption Algorithm.</param>
299         /// <param name="columnEncryptionKey">Plain text Column Encryption Key.</param>
300         /// <returns>Returns an encrypted blob or throws an exception if there are any errors.</returns>
RSAEncrypt(RSACryptoServiceProvider rscp, byte[] columnEncryptionKey)301         private byte[] RSAEncrypt(RSACryptoServiceProvider rscp, byte[] columnEncryptionKey)
302         {
303             Debug.Assert(columnEncryptionKey != null);
304             Debug.Assert(rscp != null);
305 
306             return rscp.Encrypt(columnEncryptionKey, fOAEP: true);
307         }
308 
309         /// <summary>
310         /// Decrypt the text using specified CSP key.
311         /// </summary>
312         /// <param name="masterKeyPath">CSP key url.</param>
313         /// <param name="encryptionAlgorithm">Encryption Algorithm.</param>
314         /// <param name="encryptedColumnEncryptionKey">Encrypted Column Encryption Key.</param>
315         /// <returns>Returns the decrypted plaintext Column Encryption Key or throws an exception if there are any errors.</returns>
RSADecrypt(RSACryptoServiceProvider rscp, byte[] encryptedColumnEncryptionKey)316         private byte[] RSADecrypt(RSACryptoServiceProvider rscp, byte[] encryptedColumnEncryptionKey)
317         {
318             Debug.Assert((encryptedColumnEncryptionKey != null) && (encryptedColumnEncryptionKey.Length != 0));
319             Debug.Assert(rscp != null);
320 
321             return rscp.Decrypt(encryptedColumnEncryptionKey, fOAEP: true);
322         }
323 
324         /// <summary>
325         /// Generates signature based on RSA PKCS#v1.5 scheme using a specified CSP Key URL.
326         /// </summary>
327         /// <param name="dataToSign">Text to sign.</param>
328         /// <param name="rscp">RSA Provider with a given key</param>
329         /// <returns>Signature</returns>
RSASignHashedData(byte[] dataToSign, RSACryptoServiceProvider rscp)330         private byte[] RSASignHashedData(byte[] dataToSign, RSACryptoServiceProvider rscp)
331         {
332             Debug.Assert((dataToSign != null) && (dataToSign.Length != 0));
333             Debug.Assert(rscp != null);
334 
335             return rscp.SignData(dataToSign, HashingAlgorithm);
336         }
337 
338         /// <summary>
339         /// Verifies the given RSA PKCSv1.5 signature.
340         /// </summary>
341         /// <param name="dataToVerify"></param>
342         /// <param name="signature"></param>
343         /// <param name="rscp">RSA Provider with a given key</param>
344         /// <returns>true if signature is valid, false if it is not valid</returns>
RSAVerifySignature(byte[] dataToVerify, byte[] signature, RSACryptoServiceProvider rscp)345         private bool RSAVerifySignature(byte[] dataToVerify, byte[] signature, RSACryptoServiceProvider rscp)
346         {
347             Debug.Assert((dataToVerify != null) && (dataToVerify.Length != 0));
348             Debug.Assert((signature != null) && (signature.Length != 0));
349             Debug.Assert(rscp != null);
350 
351             return rscp.VerifyData(dataToVerify, HashingAlgorithm, signature);
352         }
353 
354         /// <summary>
355         /// Gets the public Key size in bytes
356         /// </summary>
357         /// <param name="rscp">RSA Provider with a given key</param>
358         /// <returns>Key size in bytes</returns>
GetKeySize(RSACryptoServiceProvider rscp)359         private int GetKeySize(RSACryptoServiceProvider rscp)
360         {
361             Debug.Assert(rscp != null);
362 
363             return rscp.KeySize / 8;
364         }
365 
366         /// <summary>
367         /// Creates a RSACryptoServiceProvider from the given key path which contains both CSP name and key name
368         /// </summary>
369         /// <param name="keyPath">key path in the format of [CAPI provider name]\[key name]</param>
370         /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
371         /// <returns></returns>
CreateRSACryptoProvider(string keyPath, bool isSystemOp)372         private RSACryptoServiceProvider CreateRSACryptoProvider(string keyPath, bool isSystemOp)
373         {
374             // Get CNGProvider and the KeyID
375             string cspProviderName;
376             string keyName;
377             GetCspProviderAndKeyName(keyPath, isSystemOp, out cspProviderName, out keyName);
378 
379             // Verify the existence of CSP and then get the provider type
380             int providerType = GetProviderType(cspProviderName, keyPath, isSystemOp);
381 
382             // Create a new instance of CspParameters for an RSA container.
383             CspParameters cspParams = new CspParameters(providerType, cspProviderName, keyName);
384             cspParams.Flags = CspProviderFlags.UseExistingKey;
385 
386             RSACryptoServiceProvider rscp = null;
387 
388             try
389             {
390                 //Create a new instance of RSACryptoServiceProvider
391                 rscp = new RSACryptoServiceProvider(cspParams);
392             }
393             catch (CryptographicException e)
394             {
395                 const int KEYSETDOESNOTEXIST = -2146893802;
396                 if (e.HResult == KEYSETDOESNOTEXIST)
397                 {
398                     // Key does not exist
399                     throw SQL.InvalidCspKeyIdentifier(keyName, keyPath, isSystemOp);
400                 }
401                 else
402                 {
403                     // bubble up the exception
404                     throw;
405                 }
406             }
407 
408             return rscp;
409         }
410 
411         /// <summary>
412         /// Extracts the CSP provider name and key name from the given key path
413         /// </summary>
414         /// <param name="keyPath">key path in the format of [CSP provider name]\[key name]</param>
415         /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
416         /// <param name="cspProviderName">output containing the CSP provider name</param>
417         /// <param name="keyIdentifier">output containing the key name</param>
GetCspProviderAndKeyName(string keyPath, bool isSystemOp, out string cspProviderName, out string keyIdentifier)418         private void GetCspProviderAndKeyName(string keyPath, bool isSystemOp, out string cspProviderName, out string keyIdentifier)
419         {
420             int indexOfSlash = keyPath.IndexOf(@"/");
421             if (indexOfSlash == -1)
422             {
423                 throw SQL.InvalidCspPath(keyPath, isSystemOp);
424             }
425 
426             cspProviderName = keyPath.Substring(0, indexOfSlash);
427             keyIdentifier = keyPath.Substring(indexOfSlash + 1, keyPath.Length - (indexOfSlash + 1));
428 
429             if (cspProviderName.Length == 0)
430             {
431                 throw SQL.EmptyCspName(keyPath, isSystemOp);
432             }
433 
434             if (keyIdentifier.Length == 0)
435             {
436                 throw SQL.EmptyCspKeyId(keyPath, isSystemOp);
437             }
438         }
439 
440         /// <summary>
441         /// Gets the provider type from a given CAPI provider name
442         /// </summary>
443         /// <param name="providerName">CAPI provider name</param>
444         /// <param name="keyPath">key path in the format of [CSP provider name]\[key name]</param>
445         /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
446         /// <returns></returns>
GetProviderType(string providerName, string keyPath, bool isSystemOp)447         private int GetProviderType(string providerName, string keyPath, bool isSystemOp)
448         {
449             string keyName = String.Format(@"SOFTWARE\Microsoft\Cryptography\Defaults\Provider\{0}", providerName);
450             RegistryKey key = Registry.LocalMachine.OpenSubKey(keyName);
451             if (key == null)
452             {
453                 throw SQL.InvalidCspName(providerName, keyPath, isSystemOp);
454             }
455 
456             int providerType = (int)key.GetValue(@"Type");
457             key.Close();
458 
459             return providerType;
460         }
461     }
462 }
463