摘要问题引入

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

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

具体实现

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

创建测试数据库

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

--Step1-CreateMSSQLsampledatabaseUSEmasterGOIFDB_ID('AlwaysEncrypted')ISNULLCREATEDATABASE[AlwaysEncrypted];GO--Not100%require,[AlwaysEncrypted]COLLATELatin1_General_BIN2;

创建列主密钥

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

--Step2-CreateacolumnmasterkeyUSE[AlwaysEncrypted]GOCREATECOLUMNMASTERKEY[AE_ColumnMasterKey]WITH(KEY_STORE_PROVIDER_NAME=N'MSSQL_CERTIFICATE_STORE',KEY_PATH=N'CurrentUser/My/C3C1AFCDA7F2486A9BBB16232A052A6A1431ACB0')GO

创建列加密密钥

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

--Step3-CreateacolumnencryptionkeyUSE[AlwaysEncrypted]GOCREATECOLUMNENCRYPTIONKEY[AE_ColumnEncryptionKey]WITHVALUES(COLUMN_MASTER_KEY=[AE_ColumnMasterKey],ALGORITHM='RSA_OAEP',ENCRYPTED_VALUE=0x016E000001630075007200720065006E0070650072002F006D0079002F006300330063003100610066006300606600320030610039006200620062003100360032003300320061003000350032006100360061003100306100630062003000956D4610BE7DAEFC2E1B08D557BFF9E33FF23896BD76BB33A84560F5E4BE174D8798D86CC963BA57867404945B166D756CE87AFC9EB29EEB9E26B08115724C1724DCD449D0D14D4D5C4601A631899C733C7646EB845A816A17DB1D400B7C341C2EF5838731583B1C51A457E14692532FD7059B7F0AFF3D89BDF86FB3BB18880F6B49CD2EA6F346BA5EE130FCFCA69A71523722F824CD14B3CE2C29C9E46074F2FE36265450A0424F390C2BC32B724FAB674E2B58DB16347B842597AFEBE983C7F4F51BCC088292219BD6F6E1F092BD77C5AD80331770E0B0B8BF6428D2719560AF56780ECE8805F7B425818F31CF54C84FF11114DB693B6CB7D499B1490B8E155749329C9A7AF4417E2A17D0EACA92CBB59A4EE314C54BCD83F80E8D6363F9CF66D8608772DCEB5D3FF4C8A131E21984C2370AB0788E38CB330C1D6190A7513BE1179432705C0C38B9430FC7A8D10BBDBDBA4AC7A7E24D2E257A0B8B79AC2B6D7E0C2F2056F58579E96009C488F2C1C691B3DC9E2F5D538D2E96BB4E8DB280F3C0461B18ADE30A3A5C5279C6861E3109C8EEFE4BC8192338137BBF7D5BFD64A689689B40B5E1FB7A157D06F6674C807515255C0F124ED866D9C0E5294759FECFF37AEEA672EF5C3A7649CAA8B55288526DF6EF8EB2D7485601E9A72CFA53D046E200320BAAD32AD559C644018964058BBE9BE5A2BAFB28E2FF7B37C85B49680F)GO

检查CMK和CEK

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

--Step4-CMKCEKCheckingselect*_master_keysselect*_encryption_keysselect*_encryption_key_values

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


当然,您也可以使用SSMS的IDE来查看ColumnMasterKey和ColumnEncryptionKey,方法是:

展开需要检查的数据库-Security-AlwaysEncryptedKeys-展开ColumnMasterKeys和ColumnEncryptionKeys。如下图所示:


创建AlwaysEncryped测试表

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

--Step5-CreateatablewithanencryptedcolumnUSE[AlwaysEncrypted]GOIFOBJECT_ID('','U')(CustomerIdINTIDENTITY(10000,1)NOTNULLPRIMARYKEY,CustomerNameNVARCHAR(100)COLLATELatin1_General_BIN2ENCRYPTEDWITH(ENCRYPTION_TYPE=DETERMINISTIC,ALGORITHM='AEAD_AES_256_CBC_HMAC_SHA_256',COLUMN_ENCRYPTION_KEY=AE_ColumnEncryptionKey)NOTNULL,CustomerPhoneNVARCHAR(11)COLLATELatin1_General_BIN2ENCRYPTEDWITH(ENCRYPTION_TYPE=RANDOMIZED,ALGORITHM='AEAD_AES_256_CBC_HMAC_SHA_256',COLUMN_ENCRYPTION_KEY=AE_ColumnEncryptionKey)NOTNULL);GO

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

加密类型:DETERMINISTIC和RANDOMIZED。

算法:AEAD_AES_256_CBC_HMAC_SHA_256是AlwaysEncrypted专有算法。

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

导出服务器端证书

最后,我们将服务端的证书导出成文件,方法如下:

ControlPanel–InternetOptions-Content-Certificates-Export。如下图所示:


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


选择存放路径。


最后导出成功。

应用程序端测试

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

客户端导入证书

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


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

测试应用程序

我们使用VS创建一个C#的ConsoleApplication做为测试应用程序,使用NuGetPackage功能安装Dapper,做为我们SQLServer数据库操作的工具。


为了简单方便,我们直接在SQLServer服务端测试应用程序,因此您看到的连接字符串是连接本地SQLServer服务。如果您需要测试远程SQLServer,修改连接字符串即可。整个测试应用程序代码如下:

usingSystem;;;;;usingDapper;;;namespaceAlwaysEncryptedExample{publicclassAlwaysEncrypted{publicstaticreadonlystringCONN_STRING="ColumnEncryptionSetting=Enabled;Server=.,1433;InitialCatalog=AlwaysEncrypted;Trusted_Connection=Yes;MultipleActiveResultSets=True;";publicstaticvoidMain(string[]args){ListCustomerCustomers=QueryCustomerListCustomer(@"SELECTTOP3*(NOLOCK)");//thereisnorecordif(==0){("************Thereisnorecord.************");stringexecSql=@"(@customerName,@cellPhone);";("************Insertsomerecords.************");DynamicParametersdp=newDynamicParameters();("@customerName","CustomerA",dbType:,direction:,size:100);("@cellPhone","",dbType:,direction:,size:11);DoExecuteSql(execSql,dp);("************re-generaterecords.************");Customers=QueryCustomerListCustomer(@"SELECTTOP3*(NOLOCK)");}else{("************Thereareacoupleofrecords.************");}foreach(CustomercusinCustomers){(("Customernameis{0}andcellphoneis{1}.",,));}();}publicstaticListTQueryCustomerListT(stringqueryText){//inputvariablecheckingif(queryText==null||queryText==""){returnnewListT();}try{using(IDbConnectiondbConn=newSqlConnection(CONN_STRING)){//ifconnectionisclosed,openitif(==){();}//(queryText,commandTimeout:120).ToList();}}catch(Exceptionex){("Failedtoexecute{0}witherrormessage:{1},StackTrace:{2}.",queryText,,);//returnemptylistreturnnewListT();}}publicstaticboolDoExecuteSql(StringexecSql,objectparms){boolrt=false;//inputparameterscheckingif((execSql)){returnrt;}if(!(CONN_STRING)){//trytoaddeventfiletargettry{using(IDbConnectiondbConn=newSqlConnection(CONN_STRING)){//ifconnectionisclosed,openitif(==){();}varaffectedRows=(execSql,parms);rt=(affectedRows0);}}catch(Exceptionex){("Failedtoexecute{0}witherrormessage:{1},StackTrace:{2}.",execSql,,);}}returnrt;}publicclassCustomer{privateintcustomerId;privatestringcustomerName;privatestringcustomerPhone;publicCustomer(intcustomerId,stringcustomerName,stringcustomerPhone){=customerId;=customerName;=customerPhone;}publicintCustomerId{get{returncustomerId;}set{customerId=value;}}publicstringCustomerName{get{returncustomerName;}set{customerName=value;}}publicstringCustomerPhone{get{returncustomerPhone;}set{customerPhone=value;}}}}}

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


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


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

测试SSMS

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

--trytoreadAlwaysEncryptedtableandit'[AlwaysEncrypted]GOSELECT*(NOLOCK)

展示结果如下截图:


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

--trytoinsertrecordstoencryptedtable,[AlwaysEncrypted]('CustomerA',''),('CustomerB','')GO

会报告如下错误:

Msg206,Level16,State2,Line74Operandtypeclash:varcharisincompatiblewithvarchar(8000)encryptedwith(encryption_type='DETERMINISTIC',encryption_algorithm_name='AEAD_AES_256_CBC_HMAC_SHA_256',column_encryption_key_name='AE_ColumnEncryptionKey',column_encryption_key_database_name='AlwaysEncrypted')collation_name='Chinese_PRC_CI_AS'

如下截图:

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

测试结果分析

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

测试应用源代码

最后总结

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