SqlColumnEncryptionCngProvider.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. //------------------------------------------------------------------------------
  2. // <copyright file="SqlColumnEncryptionCngProvider.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. /// <summary>
  18. /// Provides implementation similar to certificate store provider.
  19. /// A CEK encrypted with certificate provider should be decryptable by this provider and vice versa.
  20. ///
  21. /// Envolope Format for the encrypted column encryption key
  22. /// version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature
  23. /// version: A single byte indicating the format version.
  24. /// keyPathLength: Length of the keyPath.
  25. /// ciphertextLength: ciphertext length
  26. /// keyPath: keyPath used to encrypt the column encryption key. This is only used for troubleshooting purposes and is not verified during decryption.
  27. /// ciphertext: Encrypted column encryption key
  28. /// signature: Signature of the entire byte array. Signature is validated before decrypting the column encryption key.
  29. /// </summary>
  30. public class SqlColumnEncryptionCngProvider : SqlColumnEncryptionKeyStoreProvider
  31. {
  32. /// <summary>
  33. /// Name for the CNG key store provider.
  34. /// </summary>
  35. public const string ProviderName = @"MSSQL_CNG_STORE";
  36. /// <summary>
  37. /// RSA_OAEP is the only algorithm supported for encrypting/decrypting column encryption keys using this provider.
  38. /// For now, we are keeping all the providers in [....].
  39. /// </summary>
  40. private const string RSAEncryptionAlgorithmWithOAEP = @"RSA_OAEP";
  41. /// <summary>
  42. /// Algorithm version
  43. /// </summary>
  44. private readonly byte[] _version = new byte[] { 0x01 };
  45. /// <summary>
  46. /// This function uses the asymmetric key specified by the key path
  47. /// and decrypts an encrypted CEK with RSA encryption algorithm.
  48. /// </summary>
  49. /// <param name="masterKeyPath">Complete path of an asymmetric key in CNG</param>
  50. /// <param name="encryptionAlgorithm">Asymmetric Key Encryption Algorithm</param>
  51. /// <param name="encryptedColumnEncryptionKey">Encrypted Column Encryption Key</param>
  52. /// <returns>Plain text column encryption key</returns>
  53. public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] encryptedColumnEncryptionKey)
  54. {
  55. // Validate the input parameters
  56. ValidateNonEmptyKeyPath(masterKeyPath, isSystemOp: true);
  57. if (null == encryptedColumnEncryptionKey)
  58. {
  59. throw SQL.NullEncryptedColumnEncryptionKey();
  60. }
  61. if (0 == encryptedColumnEncryptionKey.Length)
  62. {
  63. throw SQL.EmptyEncryptedColumnEncryptionKey();
  64. }
  65. // Validate encryptionAlgorithm
  66. ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: true);
  67. // Create RSA Provider with the given CNG name and key name
  68. RSACng rsaCngProvider = CreateRSACngProvider(masterKeyPath, isSystemOp: true);
  69. // Validate whether the key is RSA one or not and then get the key size
  70. int keySizeInBytes = GetKeySize(rsaCngProvider);
  71. // Validate and decrypt the EncryptedColumnEncryptionKey
  72. // Format is
  73. // version + keyPathLength + ciphertextLength + keyPath + ciphervtext + signature
  74. //
  75. // keyPath is present in the encrypted column encryption key for identifying the original source of the asymmetric key pair and
  76. // we will not validate it against the data contained in the CMK metadata (masterKeyPath).
  77. // Validate the version byte
  78. if (encryptedColumnEncryptionKey[0] != _version[0])
  79. {
  80. throw SQL.InvalidAlgorithmVersionInEncryptedCEK(encryptedColumnEncryptionKey[0], _version[0]);
  81. }
  82. // Get key path length
  83. int currentIndex = _version.Length;
  84. UInt16 keyPathLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
  85. currentIndex += sizeof(UInt16);
  86. // Get ciphertext length
  87. UInt16 cipherTextLength = BitConverter.ToUInt16(encryptedColumnEncryptionKey, currentIndex);
  88. currentIndex += sizeof(UInt16);
  89. // Skip KeyPath
  90. // KeyPath exists only for troubleshooting purposes and doesnt need validation.
  91. currentIndex += keyPathLength;
  92. // validate the ciphertext length
  93. if (cipherTextLength != keySizeInBytes)
  94. {
  95. throw SQL.InvalidCiphertextLengthInEncryptedCEKCng(cipherTextLength, keySizeInBytes, masterKeyPath);
  96. }
  97. // Validate the signature length
  98. // Signature length should be same as the key side for RSA PKCSv1.5
  99. int signatureLength = encryptedColumnEncryptionKey.Length - currentIndex - cipherTextLength;
  100. if (signatureLength != keySizeInBytes)
  101. {
  102. throw SQL.InvalidSignatureInEncryptedCEKCng(signatureLength, keySizeInBytes, masterKeyPath);
  103. }
  104. // Get ciphertext
  105. byte[] cipherText = new byte[cipherTextLength];
  106. Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, cipherText, 0, cipherText.Length);
  107. currentIndex += cipherTextLength;
  108. // Get signature
  109. byte[] signature = new byte[signatureLength];
  110. Buffer.BlockCopy(encryptedColumnEncryptionKey, currentIndex, signature, 0, signature.Length);
  111. // Compute the hash to validate the signature
  112. byte[] hash;
  113. using (SHA256Cng sha256 = new SHA256Cng())
  114. {
  115. sha256.TransformFinalBlock(encryptedColumnEncryptionKey, 0, encryptedColumnEncryptionKey.Length - signature.Length);
  116. hash = sha256.Hash;
  117. }
  118. Debug.Assert(hash != null, @"hash should not be null while decrypting encrypted column encryption key.");
  119. // Validate the signature
  120. if (!RSAVerifySignature(hash, signature, rsaCngProvider))
  121. {
  122. throw SQL.InvalidSignature(masterKeyPath);
  123. }
  124. // Decrypt the CEK
  125. return RSADecrypt(rsaCngProvider, cipherText);
  126. }
  127. /// <summary>
  128. /// This function uses the asymmetric key specified by the key path
  129. /// and encrypts CEK with RSA encryption algorithm.
  130. /// </summary>
  131. /// <param name="keyPath">Complete path of an asymmetric key in AKV</param>
  132. /// <param name="encryptionAlgorithm">Asymmetric Key Encryption Algorithm</param>
  133. /// <param name="columnEncryptionKey">Plain text column encryption key</param>
  134. /// <returns>Encrypted column encryption key</returns>
  135. public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey)
  136. {
  137. // Validate the input parameters
  138. ValidateNonEmptyKeyPath(masterKeyPath, isSystemOp: false);
  139. if (null == columnEncryptionKey)
  140. {
  141. throw SQL.NullColumnEncryptionKey();
  142. }
  143. else if (0 == columnEncryptionKey.Length)
  144. {
  145. throw SQL.EmptyColumnEncryptionKey();
  146. }
  147. // Validate encryptionAlgorithm
  148. ValidateEncryptionAlgorithm(encryptionAlgorithm, isSystemOp: false);
  149. // CreateCNGProviderWithKey
  150. RSACng rsaCngProvider = CreateRSACngProvider(masterKeyPath, isSystemOp: false);
  151. // Validate whether the key is RSA one or not and then get the key size
  152. int keySizeInBytes = GetKeySize(rsaCngProvider);
  153. // Construct the encryptedColumnEncryptionKey
  154. // Format is
  155. // version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature
  156. //
  157. // We currently only support one version
  158. byte[] version = new byte[] { _version[0] };
  159. // Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath
  160. byte[] masterKeyPathBytes = Encoding.Unicode.GetBytes(masterKeyPath.ToLowerInvariant());
  161. byte[] keyPathLength = BitConverter.GetBytes((Int16)masterKeyPathBytes.Length);
  162. // Encrypt the plain text
  163. byte[] cipherText = RSAEncrypt(rsaCngProvider, columnEncryptionKey);
  164. byte[] cipherTextLength = BitConverter.GetBytes((Int16)cipherText.Length);
  165. Debug.Assert(cipherText.Length == keySizeInBytes, @"cipherText length does not match the RSA key size");
  166. // Compute hash
  167. // SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext)
  168. byte[] hash;
  169. using (SHA256Cng sha256 = new SHA256Cng())
  170. {
  171. sha256.TransformBlock(version, 0, version.Length, version, 0);
  172. sha256.TransformBlock(keyPathLength, 0, keyPathLength.Length, keyPathLength, 0);
  173. sha256.TransformBlock(cipherTextLength, 0, cipherTextLength.Length, cipherTextLength, 0);
  174. sha256.TransformBlock(masterKeyPathBytes, 0, masterKeyPathBytes.Length, masterKeyPathBytes, 0);
  175. sha256.TransformFinalBlock(cipherText, 0, cipherText.Length);
  176. hash = sha256.Hash;
  177. }
  178. // Sign the hash
  179. byte[] signedHash = RSASignHashedData(hash, rsaCngProvider);
  180. Debug.Assert(signedHash.Length == keySizeInBytes, @"signed hash length does not match the RSA key size");
  181. Debug.Assert(RSAVerifySignature(hash, signedHash, rsaCngProvider), @"Invalid signature of the encrypted column encryption key computed.");
  182. // Construct the encrypted column encryption key
  183. // EncryptedColumnEncryptionKey = version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature
  184. int encryptedColumnEncryptionKeyLength = version.Length + cipherTextLength.Length + keyPathLength.Length + cipherText.Length + masterKeyPathBytes.Length + signedHash.Length;
  185. byte[] encryptedColumnEncryptionKey = new byte[encryptedColumnEncryptionKeyLength];
  186. // Copy version byte
  187. int currentIndex = 0;
  188. Buffer.BlockCopy(version, 0, encryptedColumnEncryptionKey, currentIndex, version.Length);
  189. currentIndex += version.Length;
  190. // Copy key path length
  191. Buffer.BlockCopy(keyPathLength, 0, encryptedColumnEncryptionKey, currentIndex, keyPathLength.Length);
  192. currentIndex += keyPathLength.Length;
  193. // Copy ciphertext length
  194. Buffer.BlockCopy(cipherTextLength, 0, encryptedColumnEncryptionKey, currentIndex, cipherTextLength.Length);
  195. currentIndex += cipherTextLength.Length;
  196. // Copy key path
  197. Buffer.BlockCopy(masterKeyPathBytes, 0, encryptedColumnEncryptionKey, currentIndex, masterKeyPathBytes.Length);
  198. currentIndex += masterKeyPathBytes.Length;
  199. // Copy ciphertext
  200. Buffer.BlockCopy(cipherText, 0, encryptedColumnEncryptionKey, currentIndex, cipherText.Length);
  201. currentIndex += cipherText.Length;
  202. // copy the signature
  203. Buffer.BlockCopy(signedHash, 0, encryptedColumnEncryptionKey, currentIndex, signedHash.Length);
  204. return encryptedColumnEncryptionKey;
  205. }
  206. /// <summary>
  207. /// This function validates that the encryption algorithm is RSA_OAEP and if it is not,
  208. /// then throws an exception
  209. /// </summary>
  210. /// <param name="encryptionAlgorithm">Asymmetric key encryptio algorithm</param>
  211. /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
  212. private void ValidateEncryptionAlgorithm(string encryptionAlgorithm, bool isSystemOp)
  213. {
  214. // This validates that the encryption algorithm is RSA_OAEP
  215. if (null == encryptionAlgorithm)
  216. {
  217. throw SQL.NullKeyEncryptionAlgorithm(isSystemOp);
  218. }
  219. if (!string.Equals(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, StringComparison.OrdinalIgnoreCase))
  220. {
  221. throw SQL.InvalidKeyEncryptionAlgorithm(encryptionAlgorithm, RSAEncryptionAlgorithmWithOAEP, isSystemOp);
  222. }
  223. }
  224. /// <summary>
  225. /// Checks if the CNG key path is Empty or Null (and raises exception if they are).
  226. /// </summary>
  227. /// <param name="masterKeyPath">keypath containing the CNG provider name and key name</param>
  228. /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
  229. private void ValidateNonEmptyKeyPath(string masterKeyPath, bool isSystemOp)
  230. {
  231. if (string.IsNullOrWhiteSpace(masterKeyPath))
  232. {
  233. if (null == masterKeyPath)
  234. {
  235. throw SQL.NullCngKeyPath(isSystemOp);
  236. }
  237. else
  238. {
  239. throw SQL.InvalidCngPath(masterKeyPath, isSystemOp);
  240. }
  241. }
  242. }
  243. /// <summary>
  244. /// Encrypt the text using specified CNG key.
  245. /// </summary>
  246. /// <param name="rsaCngProvider">RSA CNG Provider.</param>
  247. /// <param name="columnEncryptionKey">Plain text Column Encryption Key.</param>
  248. /// <returns>Returns an encrypted blob or throws an exception if there are any errors.</returns>
  249. private byte[] RSAEncrypt(RSACng rsaCngProvider, byte[] columnEncryptionKey)
  250. {
  251. Debug.Assert(columnEncryptionKey != null);
  252. Debug.Assert(rsaCngProvider != null);
  253. return rsaCngProvider.Encrypt(columnEncryptionKey, RSAEncryptionPadding.OaepSHA1);
  254. }
  255. /// <summary>
  256. /// Decrypt the text using the specified CNG key.
  257. /// </summary>
  258. /// <param name="rsaCngProvider">RSA CNG Provider.</param>
  259. /// <param name="encryptedColumnEncryptionKey">Encrypted Column Encryption Key.</param>
  260. /// <returns>Returns the decrypted plaintext Column Encryption Key or throws an exception if there are any errors.</returns>
  261. private byte[] RSADecrypt(RSACng rsaCngProvider, byte[] encryptedColumnEncryptionKey)
  262. {
  263. Debug.Assert((encryptedColumnEncryptionKey != null) && (encryptedColumnEncryptionKey.Length != 0));
  264. Debug.Assert(rsaCngProvider != null);
  265. return rsaCngProvider.Decrypt(encryptedColumnEncryptionKey, RSAEncryptionPadding.OaepSHA1);
  266. }
  267. /// <summary>
  268. /// Generates signature based on RSA PKCS#v1.5 scheme using a specified CNG Key.
  269. /// </summary>
  270. /// <param name="dataToSign">Text to sign.</param>
  271. /// <param name="rsaCngProvider">RSA CNG Provider.</param>
  272. /// <returns>Signature</returns>
  273. private byte[] RSASignHashedData(byte[] dataToSign, RSACng rsaCngProvider)
  274. {
  275. Debug.Assert((dataToSign != null) && (dataToSign.Length != 0));
  276. Debug.Assert(rsaCngProvider != null);
  277. return rsaCngProvider.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
  278. }
  279. /// <summary>
  280. /// Verifies the given RSA PKCSv1.5 signature.
  281. /// </summary>
  282. /// <param name="dataToVerify"></param>
  283. /// <param name="signature"></param>
  284. /// <param name="rsaCngProvider">RSA CNG Provider.</param>
  285. /// <returns>true if signature is valid, false if it is not valid</returns>
  286. private bool RSAVerifySignature(byte[] dataToVerify, byte[] signature, RSACng rsaCngProvider)
  287. {
  288. Debug.Assert((dataToVerify != null) && (dataToVerify.Length != 0));
  289. Debug.Assert((signature != null) && (signature.Length != 0));
  290. Debug.Assert(rsaCngProvider != null);
  291. return rsaCngProvider.VerifyData(dataToVerify, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
  292. }
  293. /// <summary>
  294. /// Gets the public Key size in bytes
  295. /// </summary>
  296. /// <param name="rsaCngProvider">RSA CNG Provider.</param>
  297. /// <returns>Key size in bytes</returns>
  298. private int GetKeySize(RSACng rsaCngProvider)
  299. {
  300. Debug.Assert(rsaCngProvider != null);
  301. return rsaCngProvider.KeySize / 8; // Convert from bits to byte
  302. }
  303. /// <summary>
  304. /// Creates a RSACng object from the given keyName
  305. /// </summary>
  306. /// <param name="keyPath"></param>
  307. /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
  308. /// <returns></returns>
  309. private RSACng CreateRSACngProvider(string keyPath, bool isSystemOp)
  310. {
  311. // Get CNGProvider and the KeyID
  312. string cngProviderName;
  313. string keyIdentifier;
  314. GetCngProviderAndKeyId(keyPath, isSystemOp, out cngProviderName, out keyIdentifier);
  315. CngProvider cngProvider = new CngProvider(cngProviderName);
  316. CngKey cngKey;
  317. try
  318. {
  319. cngKey = CngKey.Open(keyIdentifier, cngProvider);
  320. }
  321. catch (CryptographicException)
  322. {
  323. throw SQL.InvalidCngKey(keyPath, cngProviderName, keyIdentifier, isSystemOp);
  324. }
  325. return new RSACng(cngKey);
  326. }
  327. /// <summary>
  328. /// Extracts the CNG provider and key name from the key path
  329. /// </summary>
  330. /// <param name="masterKeyPath">keypath in the format [CNG Provider]\[KeyName]</param>
  331. /// <param name="isSystemOp">Indicates if ADO.NET calls or the customer calls the API</param>
  332. /// <param name="cngProvider">CNG Provider</param>
  333. /// <param name="keyIdentifier">Key identifier inside the CNG provider</param>
  334. private void GetCngProviderAndKeyId(string keyPath, bool isSystemOp, out string cngProvider, out string keyIdentifier)
  335. {
  336. int indexOfSlash = keyPath.IndexOf(@"/");
  337. if (indexOfSlash == -1)
  338. {
  339. throw SQL.InvalidCngPath(keyPath, isSystemOp);
  340. }
  341. cngProvider = keyPath.Substring(0, indexOfSlash);
  342. keyIdentifier = keyPath.Substring(indexOfSlash + 1, keyPath.Length - (indexOfSlash + 1));
  343. if (cngProvider.Length == 0)
  344. {
  345. throw SQL.EmptyCngName(keyPath, isSystemOp);
  346. }
  347. if (keyIdentifier.Length == 0)
  348. {
  349. throw SQL.EmptyCngKeyId(keyPath, isSystemOp);
  350. }
  351. }
  352. }
  353. }