过去SQL Server有多种加密数据的方式,如透明数据加密(TDE)。这种技术是在数据库文件或者备份被盗用时,保护静态数据。然而对于可以访问数据库本身,或者任何拥有数据库的用户,可以获取秘钥、证书和密码(系统管理员、黑客诸如此类),是没有效果的。
SQL Server 2016新引入了Always Encrypted 功能,其设计的目的即时保护敏感数据,如手机号、身份证、银行卡号等等,可以同时加密静态和动态数据(内存中的数据也会被加密)。因此,这可以保护数据免受流氓管理员、备份窃贼和中间人攻击。和TDE不同,即Always Encrypted 允许你仅仅加密某些列,而不是整个数据库。
客户端库确保纯文本只在应用程序或中间层中显示,而不出现在应用程序和数据库之间。下面的说明中,我试图表明,无论是在数据库中,还是在应用程序和数据库之间的两个方向上(数据写入数据库,和数据库数据输出),数据都是密文形式:
这就带来了Always Encrypted的第一个限制:目前所有的客户端库都不支持它。事实上,目前,Always Encrypted 线程的提供者是ADO.NET 4.6,因此,你需要确保.NET Framework 4.6 被安装到任何需要和Always Encrypted 数据交互的客户端应用服务器上。
本文将介绍Always Encrypted的基础配置,给出一些示例,解释其所受限制。
使用Always Encrypted 有几个核心概念:
列主秘钥:这是用于保护列加密秘钥的秘钥。在加密任何列之前,你必须拥有至少一个列主秘钥。
列加密秘钥:这个加密秘钥是实际上保护加密列的。
列级别加密设置:必须将列设置为加密,并使用特定的列加密密钥、算法(目前只支持一种算法)和要使用的加密类型。
Deterministic:相同的字符串加密后为相同的密文,这可以用于特定的操作(如点查询,distinct查询,分组),可以创建索引
Randomized:更安全,但不能用于计量或任何操作(写/展示),并且不能创建索引
连接字符串:为了使得客户端驱动能够理解正在使用列加密,连接字符串必须有下面的属性:
Column Encryption Setting = enabled;
应用代码本身,除连接字符串设置外,没有任何改变,因为不需要知道哪个列真正的加密过。
数据库在组织内部,管理人员为组织外部人员
数据库搭建在云上,如Azure数据库等
数据库搭建在云上,管理人员亦是组织外部人员
Always Encrypted 示例
为了简化过程,我将在单个、本地服务器上展示示例。首先让我们创建一个数据库:
CREATE DATABASE AlwaysEncrypted
ON PRIMARY
(NAME='AlwaysEncrypted',FILENAME='D:\database\AlwaysEncrypted.mdf')
LOG ON
(NAME='AlwaysEncrypted_log',FILENAME='D:\database\AlwaysEncrypted_log.ldf')
现在我将创建一个列主秘钥和列加密秘钥。在对象资源管理器中,展开数据库→安全→始终加密的秘钥。在那里你将看到两个节点,你可以右击第一个“列主秘钥”,创建列主秘钥。
对话框中并没有给出太多的选项,给予一个名称,选择秘钥存储源。我选择当前用户。注意,你可以创建多个列主秘钥(用于支持秘钥轮换)。
列主秘钥存储的位置有四种:
当前用户
本地计算机
Azure 秘钥保管库
秘钥存储提供程序(CNG)
注意:在上面创建过程中,要生成证书,该证书可供拥有证书的账户通过客户端查看Always Encrypted 解密后的数据(后期文章将会阐述)。
接着,创建列加密秘钥:
类似的,这个对话框仅仅让你为列加密秘钥提供一个名称,并选择列主秘钥:
在我的机器,可以生成如下创建脚本(但是请不要试图拷贝这些脚本在你自己的机器上运行):
USE [AlwaysEncrypted]
/****** Object: ColumnMasterKey [ColumnMasterKey] Script Date: 2020/4/10 17:09:09 ******/
CREATE COLUMN MASTER KEY [ColumnMasterKey]
WITH
(
KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE',
KEY_PATH = N'LocalMachine/My/52B3AFBC309633E9256AFCA35D0D33203AC9BD51'
)
GO
USE [AlwaysEncrypted]
CREATE COLUMN ENCRYPTION KEY [ColumnEncryptedKey]
WITH VALUES
(
COLUMN_MASTER_KEY = [ColumnMasterKey],
ALGORITHM = 'RSA_OAEP',
ENCRYPTED_VALUE = 0x01700000016C006F00630061006C006D0061006300680069006E0065002F006D0079002F00350032006200330061006600620063003300300039003600330033006500390032003500360061006600630061003300350064003000640033003300320030003300610063003900620064003500310065F54E706262859512B9990D2FDA8CD0DA8B71120875AA1B096CECBCD14822B6E0931789DE37C670063E83F2067B57E6C7AFC8A9D5442DD3EA0EF4E2D22B606645682A3B638F2E1A8C4984188783FFCC5A8047CD87BF9709FFC5F191DED58DA5288F347D2EE70171BC4356111F76023C06E0D47291D7C9BADECAED976CBCA628029F80DAE0706D8FD3530F40BFE39D05B93AF01D930ED0665A2AD93DC65A8FDDAA9EC3E263FBCE20359F4A343E715CC43C81559DC60220C040B28BFC94A1CBB5EB5E000CAB43A65787F580DA73E34F03074BD8B8B30491742CCB7BE323A169D8120BB4045D3CD1407B4AD9F5B3ECBEB051D2270813E59A04C11CD524E9D79AFA07E350A1E0E67B6378EB7DECD66CF587D9BC7096E6EC61437D96F8B991D32F7FC07D85BADA7E79769A3BF1FBAF6525AD049EF99FD18EA3D9F9DA3BFA48E85F70596D203558517F17F0450C0D6C705980DF2D03430854C498FA9A2AF67FE866932EE8DE429C788CF2B3DDEC91AF9116A94D958F754F12562EAEDA2E6E1F8E1330C61CF8DC1BB99C07DA26D314D2A5BEFC9FF949FA1E02443CCA0FB78D2753CC7146DA37A04B838D2A6C8ED89960D561AC20E7D0055CBCDA6F157D0EF040F7608C2BDC92840B1EECFEDC95FE6B094E2E16DFB3702D3560697F77F5953ADB3BA07898DE5B81A48CBAB3E072387FE56299E472679D9A847846B8DAF2B38C18B05F39
)
GO
现在秘钥已经创建好,我们可以创建一个表来使用它们。假设我们有员工表,我们想对员工姓名和工资加密。
指定加密列的语法有点晦涩。如我前面提到的,仅仅只支持一个加密算法,引用有点拗口:AEAD_AES_256_CBC_HMAC_SHA_256。同时,任何字符型数据列决定使用DETERMINISTIC类型加密时,必须使用BIN2字符集。
CREATE TABLE Employee(
ID INT IDENTITY(1,1) PRIMARY KEY
,Name NVARCHAR(10) COLLATE Chinese_PRC_BIN2
ENCRYPTED WITH(
ENCRYPTION_TYPE=DETERMINISTIC
,ALGORITHM='AEAD_AES_256_CBC_HMAC_SHA_256'
,COLUMN_ENCRYPTION_KEY=ColumnEncryptedKey
)NOT NULL
,Salary INT
ENCRYPTED WITH(
ENCRYPTION_TYPE=RANDOMIZED
,ALGORITHM='AEAD_AES_256_CBC_HMAC_SHA_256'
,COLUMN_ENCRYPTION_KEY=ColumnEncryptedKey
) NOT NULL
);
GO
在客户端SSMS中执行新增员工信息语句:
INSERT dbo.Employee(Name,Salary) SELECT N'Jack',20000;
消息 206,级别 16,状态 2,第 70 行
操作数类型冲突: nvarchar 与 nvarchar(4000) encrypted with (encryption_type = 'DETERMINISTIC', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = 'ColumnEncryptedKey', column_encryption_key_database_name = 'AlwaysEncrypted') 不兼容
我修正临时ad hoc SQL中的数据类型,我得到了另外一个错误:
DECLARE @Name NVARCHAR(15)='Jack',@Salary int=20000
INSERT dbo.Employee(Name,Salary) SELECT @Name,@Salary;
消息 33299,级别 16,状态 6,第 73 行
加密方案不匹配列/变量 '@Name'。列/变量的加密方案为 (encryption_type = 'PLAINTEXT'),行“2”附近的表达式预期其为 (encryption_type = 'DETERMINISTIC', encryption_algorithm_name = 'AEAD_AES_256_CBC_HMAC_SHA_256', column_encryption_key_name = 'ColumnEncryptedKey', column_encryption_key_database_name = 'AlwaysEncrypted') (或更弱)。
调用上面创建的增加会员的存储过程,我们得到上面相同的错误:
DECLARE @Name NVARCHAR(15)='Jack',@Salary int=20000
EXEC dbo.AddEmployee @Name,@Salary
创建新增员工信息、员工信息查询过程,以供程序调用:
/*
新增员工信息
*/
CREATE PROC dbo.AddEmployee
@Name NVARCHAR(10)
,@Salary INT
AS
BEGIN
INSERT INTO dbo.Employee(Name,Salary)
SELECT @Name,@Salary
END
GO
/*
根据员工姓名获得员工信息
*/
CREATE PROC dbo.GetEmployeeByName
@Name NVARCHAR(10)
AS
BEGIN
SELECT
ID,Name,Salary
FROM DBO.Employee
WHERE Name=@Name COLLATE Chinese_PRC_BIN2
END
GO
在Visual Studio的上,我将创建一个非常简单的Windows窗体应用程序,它将允许我填充和查询这个表。首先需要包括列加密设置属性的连接字符串,因此我的App.Config 有如下内容:
在我的窗体上添加两个文本框和两个按钮,允许我输入姓名和薪资,并向数据库表中插入一行数据;或者输入姓名,检索展示其工资。
新增员工信息按钮的脚本:
private void button1_Click(object sender, EventArgs e)
{
using (SqlConnection con = new SqlConnection())
{
con.ConnectionString = ConfigurationManager.ConnectionStrings["AEDB"].ToString();
con.Open();
using (SqlCommand cmd = new SqlCommand("dbo.AddEmployee", con))
{
cmd.CommandType = CommandType.StoredProcedure;
SqlParameter Nm = new SqlParameter("@Name", SqlDbType.NVarChar, 10);
Nm.Value = textBox1.Text;
SqlParameter Sal = new SqlParameter("@Salary", SqlDbType.Int);
Sal.Value = Convert.ToInt32(textBox2.Text);
cmd.Parameters.Add(Nm);
cmd.Parameters.Add(Sal);
cmd.ExecuteNonQuery();
MessageBox.Show("新增员工成功。");
textBox1.Clear();textBox2.Clear();
}
}
}
查询员工薪资的脚本:
private void button2_Click(object sender, EventArgs e)
{
using(SqlConnection con=new SqlConnection())
{
con.ConnectionString = ConfigurationManager.ConnectionStrings["AEDB"].ToString();
con.Open();
using(SqlCommand cmd =new SqlCommand("dbo.GetEmployeeByName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
SqlParameter Nm = new SqlParameter("@Name", SqlDbType.NVarChar, 10);
Nm.Value = textBox1.Text;
cmd.Parameters.Add(Nm);
SqlDataReader rdr = cmd.ExecuteReader();
while (rdr.Read())
{
textBox2.Text = rdr["Salary"].ToString();
}
}
}
}
它非常粗糙和简陋,但它完成了工作:当您输入姓名和薪水并按下“新增员工信息”按钮时,它将其添加到数据库中,然后清除表单。如果只输入姓名按下另一个按钮,它将用该人员的薪水填充salary字段。
如果我们开启Profiler,我们可以看到下面的读写过程,参数在到达SQL Server之前已经加密了。
exec sp_describe_parameter_encryption N'EXEC [dbo].[AddEmployee] @Name=@Name, @Salary=@Salary',N'@Name nvarchar(10),@Salary int'
exec dbo.AddEmployee @Name=0x01BE14A713B21E6B357A0787010B45118E02D809C43CF75BA7E89EF00E08204709EA11B25936C51B97B6674B7846B9EFF0DF8EE3B66C97B798C3E2B6777D186ED4,@Salary=0x0118A0628238B637DCE8C041CCE05028743F5ADE119EED521AB75DF304CBEB5E43703A484FC3CA8E3DFC1D755A0160C32E9228C60671E9F912AF3981FDAAFBE52E
查询员工薪资
exec dbo.GetEmployeeByName @Name=0x01BE14A713B21E6B357A0787010B45118E02D809C43CF75BA7E89EF00E08204709EA11B25936C51B97B6674B7846B9EFF0DF8EE3B66C97B798C3E2B6777D186ED4
我们再回过头来查看数据库中的数据存储情况:
我们可以看到姓名列和薪资列都已经加密了。