MSSQL - Best Practices - Always Encrypted

Keywords: Programming SQL Database network Attribute

abstract

In the sharing of SQL Server Security Series monthly bulletins, we have shared one after another in the past: How to Use Symmetric Key to Implement SQL Server Column Encryption Technology,Using Asymmetric Key to Encrypt SQL Server Columns,Using Hybrid Key to Encrypt SQL Server Columns,Query Performance Problems Caused by Column Encryption Technology and Relevant Solutions,Row-level security solutions,Implementation of Privacy Data Column Coding Technology by SQL Server 2016 dynamic data masking and Using Certificate to Backup and Encrypt Database For these seven articles, click directly on the above articles to see the details. In this monthly report, we share the new features of SQL Server 2016, Always Encrypted technology.

Problem introduction

With the popularity of cloud computing, is there a way to ensure that the data stored in the cloud database will always be encrypted, even if the cloud service provider can not see the plaintext data in the database, in order to ensure the absolute security of data in the customer cloud database? The answer is yes, Always Encrypted, introduced by SQL Server 2016, which we will talk about today.
With SQL Server Always Encrypted, data is always encrypted. Only applications calling SQL Server can read, write and operate encrypted data, so that you can avoid database or operating system administrators from touching sensitive data of client applications. SQL Server 2016 Always Encrypted controls the client application by verifying the encryption key, which will never be transmitted to the remote SQL Server server server through the network. Therefore, the security of cloud database customer data is guaranteed to the greatest extent, even cloud service providers can not accurately know user data plaintext.

Concrete realization

Always Encrypted, a new feature introduced by SQL Server 2016, enables user data to be encrypted and decrypted on the application side. Therefore, it is always stored and read and written in the cloud side in an encrypted state, which maximizes user data security and completely solves the trust problem of customers to cloud service providers. Following are the detailed implementation steps of SQL Server 2016 Always Encrypted technology.

Create a test database

For testing convenience, we first created the test database Always Encrypted.

--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;

Create column master key

Secondly, in the Always Encrypted database, we create the 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

Create column encryption keys

Then, we create the Column Encryption Key (abbreviated as 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

Check CMK and CEK

Next, we examine the column master key and the column encryption key that we just created, as follows:

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

Everything is normal, as shown in the following screenshot:

Of course, you can also use SSMS IDE to view Column Master Key and Column Encryption Key by:
Expand the database that needs to be checked - > Security - > Always Encrypted Keys - > Expand Column Master Keys and Column Encryption Keys. As shown in the following figure:

Create Always Encryped Test Table

Next, we create the Always Encrypted test table, which is coded as follows:

-- 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

In the process of creating the Always Encrypted test table, for the encrypted fields, we specify:
Encryption types: DETERMINISTIC and RANDOMIZED.
Algorithms: AEAD_AES_256_CBC_HMAC_SHA_256 is a proprietary algorithm of Always Encrypted.
Encryption key: The name of the encryption key created.

Export server-side certificates

Finally, we export the certificate of the server to a file, as follows:
Control Panel -> Internet Options - > Content - > Certificates - > Export. As shown in the following figure:

Enter the private key protection password in the export wizard.

Select the storage path.

Finally, the derivation is successful.

Application-side testing

After the SQL Server server is configured, we need to import certificates at the test application side and then test the application.

Client Import Certificate

The client-side import certificate method is consistent with the server-side certificate export method entry by: Control Panel -> Internet Options -> Content -> Certificates -> Import. As shown in the following screenshot:

Then enter the encryption password of the private key file and import successfully.

Test application

We use VS to create a C
Note: Only. NET 4.6 and above support the SQL Server driver with Always Encrypted features, so make sure that your project Target framework is at least version. NET 4.6 by right-clicking on your project - > Properties - > in Application and switching your Target framework to. NET Framework 4.6.

For simplicity and convenience, we test the application directly on the SQL Server server side, so the connection string you see is the connection to the local SQL Server service. If you need to test remote SQL Server, modify the connection string. The entire test application code is as follows:

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;
                }
            }
        }
    }
}

In our application code, we only need to add Column Encryption Setting = Enabled to the connection string; attribute configuration, which supports the new feature Always Encrypted of SQL Server 2016, is very simple. For your convenience, I put this property configuration in the first place of the connection string, as shown in the following figure:

Run our test application and show the results as follows:

From the test results of the application, we can read and write Always Encrypted test tables normally, and the application works well. So, if we leave aside the application and use other ways to read and write the test table, what kind of data results can we see?

Test SSMS

Suppose we use SSMS as a test tool. First read the data in the Always Encrypted test table:

-- 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)

The results are shown in the following screenshots:

Then, use SSMS to insert data directly into the test table:

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

The following errors will be reported:

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'

The following screenshot:

Thus, we can't read and manipulate the plaintext data of the Always Encrypted table using methods other than the test application.

Analysis of test results

According to the test results of application reading and writing test and using SSMS to read and write Always Encrypted table directly, users can use the former to read and write the test table normally and work well, while the latter can not read the test manifestation text, only can see the encrypted ciphertext data of the test table, plus write operation to report errors directly.

Test application source code

If you need the source code for the test application in this article, please Click to download.

Final conclusion

In this monthly report, we share the principle and implementation of Always Encrypted, a new feature of SQL Server 2016, in order to ensure that the data stored in the cloud database will always be encrypted. Even cloud service providers can not see the plaintext data in the database, so as to ensure the absolute security of data in the client cloud database and solve the most important users in the cloud database scenario. Trust in cloud service providers.


Original link
This article is the original content of Yunqi Community, which can not be reproduced without permission.

Posted by yacaph on Sat, 27 Apr 2019 15:24:35 -0700