MSSQL-最佳實踐-Always Encrypted

摘要

在SQL Server安全系列專題月報分享中,往期咱們已經陸續分享了:如何使用對稱密鑰實現SQL Server列加密技術使用非對稱密鑰實現SQL Server列加密使用混合密鑰實現SQL Server列加密技術列加密技術帶來的查詢性能問題以及相應解決方案行級別安全解決方案SQL Server 2016 dynamic data masking實現隱私數據列打碼技術使用證書作數據庫備份加密這七篇文章,直接點擊以上文章前往查看詳情。本期月報咱們分享SQL Server 2016新特性Always Encrypted技術。mysql

問題引入

在雲計算大行其道的現在,有沒有一種方法保證存儲在雲端的數據庫中數據永遠保持加密狀態,即使是雲服務提供商也看不到數據庫中的明文數據,以此來保證客戶雲數據庫中數據的絕對安全呢?答案是確定的,就是咱們今天將要談到的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技術的詳細實現步驟。sql

建立測試數據庫

爲了測試方便,咱們首先建立了測試數據庫AlwaysEncrypted。數據庫

--Step 1 - Create MSSQL sample database
USE master
GO
IF DB_ID('AlwaysEncrypted') IS NULL
    CREATE DATABASE [AlwaysEncrypted];
GO

-- Not 100% require, but option adviced.
ALTER DATABASE [AlwaysEncrypted] COLLATE Latin1_General_BIN2;

建立列主密鑰

其次,在AlwaysEncrypted數據庫中,咱們建立列主密鑰(Column Master Key,簡寫爲CMK)。安全

-- Step 2 - Create a column master key
USE [AlwaysEncrypted]
GO
CREATE COLUMN MASTER KEY [AE_ColumnMasterKey]
WITH
(
    KEY_STORE_PROVIDER_NAME = N'MSSQL_CERTIFICATE_STORE',
    KEY_PATH = N'CurrentUser/My/C3C1AFCDA7F2486A9BBB16232A052A6A1431ACB0'
)

GO

建立列加密密鑰

而後,咱們建立列加密密鑰(Column Encryption Key,簡寫爲CEK)。服務器

-- Step 3 - Create a column encryption key
USE [AlwaysEncrypted]
GO

CREATE COLUMN ENCRYPTION KEY [AE_ColumnEncryptionKey]
WITH VALUES
(
    COLUMN_MASTER_KEY = [AE_ColumnMasterKey],
    ALGORITHM = 'RSA_OAEP',
    ENCRYPTED_VALUE = 0x016E000001630075007200720065006E00740075007300650072002F006D0079002F006300330063003100610066006300640061003700660032003400380036006100390062006200620031003600320033003200610030003500320061003600610031003400330031006100630062003000956D4610BE7DAEFC2E1B08D557BFF9E33FF23896BD76BB33A84560F5E4BE174D8798D86CC963BA57867404945B166D756CE87AFC9EB29EEB9E26B08115724C1724DCD449D0D14D4D5C4601A631899C733C7646EB845A816A17DB1D400B7C341C2EF5838731583B1C51A457E14692532FD7059B7F0AFF3D89BDF86FB3BB18880F6B49CD2EA6F346BA5EE130FCFCA69A71523722F824CD14B3CE2C29C9E46074F2FE36265450A0424F390C2BC32B724FAB674E2B58DB16347B842597AFEBE983C7F4F51BCC088292219BD6F6E1F092BD77C5AD80331770E0B0B8BF6428D2719560AF56780ECE8805F7B425818F31CF54C84FF11114DB693B6CB7D499B1490B8E155749329C9A7AF4417E2A17D0EACA92CBB59A4EE314C54BCD83F80E8D6363F9CF66D8608772DCEB5D3FF4C8A131E21984C2370AB0788E38CB330C1D6190A7513BE1179432705C0C38B9430FC7A8D10BBDBDBA4AC7A7E24D2E257A0B8B79AC2B6D7E0C2F2056F58579E96009C488F2C1C691B3DC9E2F5D538D2E96BB4E8DB280F3C0461B18ADE30A3A5C5279C6861E3109C8EEFE4BC8192338137BBF7D5BFD64A689689B40B5E1FB7A157D06F6674C807515255C0F124ED866D9C0E5294759FECFF37AEEA672EF5C3A7649CAA8B55288526DF6EF8EB2D7485601E9A72CFA53D046E200320BAAD32AD559C644018964058BBE9BE5A2BAFB28E2FF7B37C85B49680F
)

GO

檢查CMK和CEK

接下來,咱們檢查下剛纔建立的列主密鑰和列加密密鑰,方法以下:網絡

-- Step 4 - CMK & CEK Checking
select * from sys.column_master_keys
select * from sys.column_encryption_keys
select * from sys.column_encryption_key_values

一切正常,以下截圖所示:app

固然,您也可使用SSMS的IDE來查看Column Master Key和Column Encryption Key,方法是:
展開須要檢查的數據庫 -> Security -> Always Encrypted Keys -> 展開Column Master Keys和 Column Encryption Keys。以下圖所示:
工具

建立Always Encryped測試表

下一步,咱們建立Always Encrypted測試表,代碼以下:性能

-- Step 5 -  Create a table with an encrypted column

USE [AlwaysEncrypted]
GO
IF OBJECT_ID('dbo.CustomerInfo', 'U') IS NOT NULL
    DROP TABLE dbo.CustomerInfo
GO
CREATE TABLE dbo.CustomerInfo
(
CustomerId        INT IDENTITY(10000,1)    NOT NULL PRIMARY KEY,
CustomerName    NVARCHAR(100) COLLATE Latin1_General_BIN2 
    ENCRYPTED WITH (
        ENCRYPTION_TYPE = DETERMINISTIC, 
        ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', 
        COLUMN_ENCRYPTION_KEY = AE_ColumnEncryptionKey
    ) NOT NULL,
CustomerPhone    NVARCHAR(11)  COLLATE Latin1_General_BIN2
    ENCRYPTED WITH (
    ENCRYPTION_TYPE = RANDOMIZED, 
    ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', 
    COLUMN_ENCRYPTION_KEY = AE_ColumnEncryptionKey
    ) NOT NULL
 )
;
GO

在建立Always Encrypted測試表過程當中,對於加密字段,咱們指定了:
 加密類型:DETERMINISTIC和RANDOMIZED。
 算法:AEAD_AES_256_CBC_HMAC_SHA_256是Always Encrypted專有算法。
 加密密鑰:建立的加密密鑰名字。

導出服務器端證書

最後,咱們將服務端的證書導出成文件,方法以下:
Control Panel –> Internet Options -> Content -> Certificates -> Export。以下圖所示:

導出嚮導中輸入私鑰保護密碼。

選擇存放路徑。

最後導出成功。

應用程序端測試

SQL Server服務端配置完畢後,咱們須要在測試應用程序端導入證書,而後測試應用程序。

客戶端導入證書

客戶端導入證書方法與服務端證書導出方法入口是一致的,方法是:Control Panel –> Internet Options -> Content -> Certificates -> Import。以下截圖所示:

而後輸入私鑰文件加密密碼,導入成功。

測試應用程序

咱們使用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。

爲了簡單方便,咱們直接在SQL Server服務端測試應用程序,所以您看到的鏈接字符串是鏈接本地SQL Server服務。若是您須要測試遠程SQL Server,修改鏈接字符串便可。整個測試應用程序代碼以下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Dapper;
using System.Data;
using System.Data.SqlClient;

namespace AlwaysEncryptedExample
{
    public class AlwaysEncrypted
    {
        public static readonly string CONN_STRING = "Column Encryption Setting = Enabled;Server=.,1433;Initial Catalog=AlwaysEncrypted;Trusted_Connection=Yes;MultipleActiveResultSets=True;";
        public static void Main(string[] args)
        {
            List<Customer> Customers = QueryCustomerList<Customer>(@"SELECT TOP 3 * FROM dbo.CustomerInfo WITH(NOLOCK)");

            // there is no record
            if(Customers.Count == 0)
            {
                Console.WriteLine("************There is no record.************");
                string execSql = @"INSERT INTO dbo.CustomerInfo VALUES (@customerName, @cellPhone);";

                Console.WriteLine("************Insert some records.************");

                DynamicParameters dp = new DynamicParameters();
                dp.Add("@customerName", "CustomerA", dbType: DbType.String, direction: ParameterDirection.Input, size: 100);
                dp.Add("@cellPhone", "13402871524", dbType: DbType.String, direction: ParameterDirection.Input, size: 11);

                DoExecuteSql(execSql, dp);

                Console.WriteLine("************re-generate records.************");
                Customers = QueryCustomerList<Customer>(@"SELECT TOP 3 * FROM dbo.CustomerInfo WITH(NOLOCK)");
            }
            else
            {
                Console.WriteLine("************There are a couple of records.************");
            }

            foreach(Customer cus in Customers)
            {
                Console.WriteLine(string.Format("Customer name is {0} and cell phone is {1}.", cus.CustomerName, cus.CustomerPhone));
            }

            Console.ReadKey();
        }

        public static List<T> QueryCustomerList<T>(string queryText)
        {
            // input variable checking
            if (queryText == null || queryText == "")
            {
                return new List<T>();
            }
            try
            {
                using (IDbConnection dbConn = new SqlConnection(CONN_STRING))
                {
                    // if connection is closed, open it
                    if (dbConn.State == ConnectionState.Closed)
                    {
                        dbConn.Open();
                    }

                    // return the query result data set to list.
                    return dbConn.Query<T>(queryText, commandTimeout: 120).ToList();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to execute {0} with error message : {1}, StackTrace: {2}.", queryText, ex.Message, ex.StackTrace);
                // return empty list
                return new List<T>();
            }
        }

        public static bool DoExecuteSql(String execSql, object parms)
        {
            bool rt = false;

            // input parameters checking
            if (string.IsNullOrEmpty(execSql))
            {
                return rt;
            }

            if (!string.IsNullOrEmpty(CONN_STRING))
            {
                // try to add event file target
                try
                {
                    using (IDbConnection dbConn = new SqlConnection(CONN_STRING))
                    {
                        // if connection is closed, open it
                        if (dbConn.State == ConnectionState.Closed)
                        {
                            dbConn.Open();
                        }

                        var affectedRows = dbConn.Execute(execSql, parms);

                        rt = (affectedRows > 0);
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Failed to execute {0} with error message : {1}, StackTrace: {2}.", execSql, ex.Message, ex.StackTrace);
                }
            }

            return rt;
        }

        public class Customer
        {
            private int customerId;
            private string customerName;
            private string customerPhone;

            public Customer(int customerId, string customerName, string customerPhone)
            {
                this.customerId = customerId;
                this.customerName = customerName;
                this.customerPhone = customerPhone;
            }

            public int CustomerId
            {
                get
                {
                    return customerId;
                }

                set
                {
                    customerId = value;
                }
            }

            public string CustomerName
            {
                get
                {
                    return customerName;
                }

                set
                {
                    customerName = value;
                }
            }

            public string CustomerPhone
            {
                get
                {
                    return customerPhone;
                }

                set
                {
                    customerPhone = value;
                }
            }
        }
    }
}

咱們在應用程序代碼中,僅須要在鏈接字符串中添加Column Encryption Setting = Enabled;屬性配置,便可支持SQL Server 2016新特性Always Encrypted,很是簡單。爲了方便你們觀察,我把這個屬性配置放到了鏈接字符串的第一個位置,以下圖所示:

運行咱們的測試應用程序,展現結果以下圖所示:

從應用程序的測試結果來看,咱們能夠正常讀、寫Always Encrypted測試表,應用程序工做良好。那麼,假如咱們拋開應用程序使用其它方式可否讀寫該測試表,看到又是什麼樣的數據結果呢?

測試SSMS

假設,咱們使用SSMS作爲測試工具。首先讀取Always Encrypted測試表中的數據:

-- try to read Always Encrypted table and it'll show us encrypted data instead of the plaintext.
USE [AlwaysEncrypted]
GO
SELECT * FROM dbo.CustomerInfo WITH(NOLOCK)

展現結果以下截圖:

而後,使用SSMS直接往測試表中插入數據:

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

會報告以下錯誤:

Msg 206, Level 16, State 2, Line 74
Operand type clash: varchar is incompatible with varchar(8000) encrypted with (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'

以下截圖:

因而可知,咱們沒法使用測試應用程序之外的方法讀取和操做Always Encrypted表的明文數據。

測試結果分析

從應用程序讀寫測試和使用SSMS直接讀寫Always Encrypted表的測試結果來看,用戶可使用前者正常讀寫測試表,工做良好;然後者沒法讀取測試代表文,僅可查看測試表的加密後的密文數據,加之寫入操做直接報錯。

測試應用源代碼

若是您須要本文的測試應用程序源代碼,請點擊下載

最後總結

本期月報,咱們分享了SQL Server 2016新特性Always Encrypted的原理及實現方法,以此來保證存儲在雲端的數據庫中數據永遠保持加密狀態,即使是雲服務提供商也看不到數據庫中的明文數據,以此來保證客戶雲數據庫的數據絕對安全,解決了雲數據庫場景中最重要的用戶對雲服務提供商信任問題。


原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索