在云计算大行其道的如今,有没有一种方法保证存储在云端的数据库中数据永远保持加密状态,即便是云服务提供商也看不到数据库中的明文数据,以此来保证客户云数据库中数据的绝对安全呢?答案是肯定的,就是我们今天将要谈到的SQL Server 2016引入的始终加密技术(Always Encrypted)。

使用SQL Server Always Encrypted,始终保持数据处于加密状态,只有调用SQL Server的应用才能读写和操作加密数据,如此您可以避免数据库或者操作系统管理员接触到客户应用程序敏感数据。SQL Server 2016 Always Encrypted通过验证加密密钥来实现了对客户端应用的控制,该加密密钥永远不会通过网络传递给远程的SQL Server服务端。因此,最大限度保证了云数据库客户数据安全,即使是云服务提供商也无法准确获知用户数据明文。

SQL Server 2016引入的新特性Always Encrypted让用户数据在应用端加密、解密,因此在云端始终处于加密状态存储和读写,最大限制保证用户数据安全,彻底解决客户对云服务提供商的信任问题。以下是SQL Server 2016 Always Encrypted技术的详细实现步骤。

为了测试方便,我们首先创建了测试数据库AlwaysEncrypted。

创建列主密钥

其次,在AlwaysEncrypted数据库中,我们创建列主密钥(Column Master Key,简写为CMK)。

  1. USE [AlwaysEncrypted]
  2. GO
  3. CREATE COLUMN MASTER KEY [AE_ColumnMasterKey]
  4. WITH
  5. (
  6. KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE',
  7. KEY_PATH = N'CurrentUser/My/C3C1AFCDA7F2486A9BBB16232A052A6A1431ACB0'
  8. )
  9. GO

然后,我们创建列加密密钥(Column Encryption Key,简写为CEK)。

  1. -- Step 3 - Create a column encryption key
  2. USE [AlwaysEncrypted]
  3. GO
  4. CREATE COLUMN ENCRYPTION KEY [AE_ColumnEncryptionKey]
  5. WITH VALUES
  6. (
  7. COLUMN_MASTER_KEY = [AE_ColumnMasterKey],
  8. ALGORITHM = 'RSA_OAEP',
  9. ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F006300330063003100610066006300640061003700660032003400380036006100390062006200620031003600320033003200610030003500320061003600610031003400330031006100630062003000956D4610BE7DAEFC2E1B08D557BFF9E33FF23896BD76BB33A84560F5E4BE174D8798D86CC963BA57867404945B166D756CE87AFC9EB29EEB9E26B08115724C1724DCD449D0D14D4D5C4601A631899C733C7646EB845A816A17DB1D400B7C341C2EF5838731583B1C51A457E14692532FD7059B7F0AFF3D89BDF86FB3BB18880F6B49CD2EA6F346BA5EE130FCFCA69A71523722F824CD14B3CE2C29C9E46074F2FE36265450A0424F390C2BC32B724FAB674E2B58DB16347B842597AFEBE983C7F4F51BCC088292219BD6F6E1F092BD77C5AD80331770E0B0B8BF6428D2719560AF56780ECE8805F7B425818F31CF54C84FF11114DB693B6CB7D499B1490B8E155749329C9A7AF4417E2A17D0EACA92CBB59A4EE314C54BCD83F80E8D6363F9CF66D8608772DCEB5D3FF4C8A131E21984C2370AB0788E38CB330C1D6190A7513BE1179432705C0C38B9430FC7A8D10BBDBDBA4AC7A7E24D2E257A0B8B79AC2B6D7E0C2F2056F58579E96009C488F2C1C691B3DC9E2F5D538D2E96BB4E8DB280F3C0461B18ADE30A3A5C5279C6861E3109C8EEFE4BC8192338137BBF7D5BFD64A689689B40B5E1FB7A157D06F6674C807515255C0F124ED866D9C0E5294759FECFF37AEEA672EF5C3A7649CAA8B55288526DF6EF8EB2D7485601E9A72CFA53D046E200320BAAD32AD559C644018964058BBE9BE5A2BAFB28E2FF7B37C85B49680F
  10. )
  11. GO

检查CMK和CEK

接下来,我们检查下刚才创建的列主密钥和列加密密钥,方法如下:

一切正常,如下截图所示:

当然,您也可以使用SSMS的IDE来查看Column Master Key和Column Encryption Key,方法是: 展开需要检查的数据库 -> Security -> Always Encrypted Keys -> 展开Column Master Keys和 Column Encryption Keys。如下图所示:

02.png

下一步,我们创建Always Encrypted测试表,代码如下:

  1. -- Step 5 - Create a table with an encrypted column
  2. USE [AlwaysEncrypted]
  3. GO
  4. IF OBJECT_ID('dbo.CustomerInfo', 'U') IS NOT NULL
  5. DROP TABLE dbo.CustomerInfo
  6. GO
  7. CREATE TABLE dbo.CustomerInfo
  8. (
  9. CustomerId INT IDENTITY(10000,1) NOT NULL PRIMARY KEY,
  10. CustomerName NVARCHAR(100) COLLATE Latin1_General_BIN2
  11. ENCRYPTED WITH (
  12. ENCRYPTION_TYPE = DETERMINISTIC,
  13. ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
  14. COLUMN_ENCRYPTION_KEY = AE_ColumnEncryptionKey
  15. ) NOT NULL,
  16. CustomerPhone NVARCHAR(11) COLLATE Latin1_General_BIN2
  17. ENCRYPTED WITH (
  18. ENCRYPTION_TYPE = RANDOMIZED,
  19. ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
  20. COLUMN_ENCRYPTION_KEY = AE_ColumnEncryptionKey
  21. ) NOT NULL
  22. )
  23. ;
  24. GO

在创建Always Encrypted测试表过程中,对于加密字段,我们指定了:

 加密类型:DETERMINISTIC和RANDOMIZED。

 加密密钥:创建的加密密钥名字。

导出服务器端证书

最后,我们将服务端的证书导出成文件,方法如下: Control Panel –> Internet Options -> Content -> Certificates -> Export。如下图所示:

导出向导中输入私钥保护密码。

04.png

选择存放路径。

最后导出成功。

SQL Server服务端配置完毕后,我们需要在测试应用程序端导入证书,然后测试应用程序。

客户端导入证书

客户端导入证书方法与服务端证书导出方法入口是一致的,方法是:Control Panel –> Internet Options -> Content -> Certificates -> Import。如下截图所示:

06.png

然后输入私钥文件加密密码,导入成功。

测试应用程序

我们使用VS创建一个C#的Console Application做为测试应用程序,使用NuGet Package功能安装Dapper,做为我们SQL Server数据库操作的工具。 注意:仅.NET 4.6及以上版本支持Always Encrypted特性的SQL Server driver,因此,请确保您的项目Target framework至少是.NET 4.6版本,方法如下:右键点击您的项目 -> Properties -> 在Application中,切换你的Target framework为.NET Framework 4.6。

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using Dapper;
  7. using System.Data;
  8. using System.Data.SqlClient;
  9. namespace AlwaysEncryptedExample
  10. {
  11. public class AlwaysEncrypted
  12. {
  13. public static readonly string CONN_STRING = "Column Encryption Setting = Enabled;Server=.,1433;Initial Catalog=AlwaysEncrypted;Trusted_Connection=Yes;MultipleActiveResultSets=True;";
  14. public static void Main(string[] args)
  15. List<Customer> Customers = QueryCustomerList<Customer>(@"SELECT TOP 3 * FROM dbo.CustomerInfo WITH(NOLOCK)");
  16. // there is no record
  17. if(Customers.Count == 0)
  18. {
  19. Console.WriteLine("************There is no record.************");
  20. string execSql = @"INSERT INTO dbo.CustomerInfo VALUES (@customerName, @cellPhone);";
  21. Console.WriteLine("************Insert some records.************");
  22. dp.Add("@customerName", "CustomerA", dbType: DbType.String, direction: ParameterDirection.Input, size: 100);
  23. dp.Add("@cellPhone", "13402871524", dbType: DbType.String, direction: ParameterDirection.Input, size: 11);
  24. DoExecuteSql(execSql, dp);
  25. Console.WriteLine("************re-generate records.************");
  26. Customers = QueryCustomerList<Customer>(@"SELECT TOP 3 * FROM dbo.CustomerInfo WITH(NOLOCK)");
  27. }
  28. else
  29. {
  30. Console.WriteLine("************There are a couple of records.************");
  31. }
  32. foreach(Customer cus in Customers)
  33. {
  34. Console.WriteLine(string.Format("Customer name is {0} and cell phone is {1}.", cus.CustomerName, cus.CustomerPhone));
  35. }
  36. Console.ReadKey();
  37. }
  38. public static List<T> QueryCustomerList<T>(string queryText)
  39. {
  40. // input variable checking
  41. if (queryText == null || queryText == "")
  42. {
  43. return new List<T>();
  44. }
  45. try
  46. {
  47. using (IDbConnection dbConn = new SqlConnection(CONN_STRING))
  48. {
  49. // if connection is closed, open it
  50. if (dbConn.State == ConnectionState.Closed)
  51. {
  52. dbConn.Open();
  53. }
  54. // return the query result data set to list.
  55. return dbConn.Query<T>(queryText, commandTimeout: 120).ToList();
  56. }
  57. }
  58. catch (Exception ex)
  59. {
  60. Console.WriteLine("Failed to execute {0} with error message : {1}, StackTrace: {2}.", queryText, ex.Message, ex.StackTrace);
  61. // return empty list
  62. return new List<T>();
  63. }
  64. }
  65. public static bool DoExecuteSql(String execSql, object parms)
  66. {
  67. bool rt = false;
  68. // input parameters checking
  69. if (string.IsNullOrEmpty(execSql))
  70. {
  71. return rt;
  72. }
  73. if (!string.IsNullOrEmpty(CONN_STRING))
  74. {
  75. // try to add event file target
  76. try
  77. {
  78. using (IDbConnection dbConn = new SqlConnection(CONN_STRING))
  79. {
  80. // if connection is closed, open it
  81. {
  82. dbConn.Open();
  83. }
  84. var affectedRows = dbConn.Execute(execSql, parms);
  85. rt = (affectedRows > 0);
  86. }
  87. }
  88. catch (Exception ex)
  89. {
  90. Console.WriteLine("Failed to execute {0} with error message : {1}, StackTrace: {2}.", execSql, ex.Message, ex.StackTrace);
  91. }
  92. }
  93. return rt;
  94. }
  95. public class Customer
  96. {
  97. private int customerId;
  98. private string customerName;
  99. private string customerPhone;
  100. public Customer(int customerId, string customerName, string customerPhone)
  101. {
  102. this.customerId = customerId;
  103. this.customerName = customerName;
  104. this.customerPhone = customerPhone;
  105. }
  106. public int CustomerId
  107. {
  108. get
  109. {
  110. return customerId;
  111. }
  112. set
  113. {
  114. customerId = value;
  115. }
  116. }
  117. public string CustomerName
  118. {
  119. get
  120. {
  121. return customerName;
  122. }
  123. set
  124. {
  125. customerName = value;
  126. }
  127. }
  128. public string CustomerPhone
  129. {
  130. get
  131. {
  132. return customerPhone;
  133. }
  134. set
  135. {
  136. customerPhone = value;
  137. }
  138. }
  139. }
  140. }
  141. }

我们在应用程序代码中,仅需要在连接字符串中添加Column Encryption Setting = Enabled;属性配置,即可支持SQL Server 2016新特性Always Encrypted,非常简单。为了方便大家观察,我把这个属性配置放到了连接字符串的第一个位置,如下图所示:

08.png

运行我们的测试应用程序,展示结果如下图所示:

从应用程序的测试结果来看,我们可以正常读、写Always Encrypted测试表,应用程序工作良好。那么,假如我们抛开应用程序使用其它方式能否读写该测试表,看到又是什么样的数据结果呢?

测试SSMS

假设,我们使用SSMS做为测试工具。首先读取Always Encrypted测试表中的数据:

展示结果如下截图:

10.png

然后,使用SSMS直接往测试表中插入数据:

  1. -- try to insert records to encrypted table, will be fail.
  2. USE [AlwaysEncrypted]
  3. GO
  4. INSERT INTO dbo.CustomerInfo
  5. VALUES ('CustomerA','13402872514'),('CustomerB','13880674722')
  6. GO

会报告如下错误:

  1. Msg 206, Level 16, State 2, Line 74

如下截图:

由此可见,我们无法使用测试应用程序以外的方法读取和操作Always Encrypted表的明文数据。

测试结果分析

从应用程序读写测试和使用SSMS直接读写Always Encrypted表的测试结果来看,用户可以使用前者正常读写测试表,工作良好;而后者无法读取测试表明文,仅可查看测试表的加密后的密文数据,加之写入操作直接报错。

测试应用源代码

本期月报,我们分享了SQL Server 2016新特性Always Encrypted的原理及实现方法,以此来保证存储在云端的数据库中数据永远保持加密状态,即便是云服务提供商也看不到数据库中的明文数据,以此来保证客户云数据库的数据绝对安全,解决了云数据库场景中最重要的用户对云服务提供商信任问题。