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