搭配 Microsoft .NET Data Provider for SQL Server 使用 Always Encrypted

適用於:.NET Framework .NET .NET Standard

此文章提供如何使用 Always Encrypted具有安全記憶體保護區的 Always EncryptedMicrosoft .NET Data Provider for SQL Server 來開發 .NET 應用程式的相關資訊。

[永遠加密] 可讓用戶端應用程式加密敏感性資料,且永遠不會顯示資料或 SQL Server 或 Azure SQL Database 的加密金鑰。 已啟用 Always Encrypted 的驅動程式 (例如 Microsoft .NET Data Provider for SQL Server) 會在用戶端應用程式中明確地加密及解密敏感性資料,以達成此安全性。 驅動程式會自動判斷哪些查詢參數對應至敏感性資料庫資料行 (使用 Always Encrypted 保護),並在將資料傳遞至伺服器前,先加密這些參數的值。 同樣地,驅動程式會以清晰簡明的方式,將擷取自查詢結果的加密資料庫資料行資料進行解密。 如需詳細資訊,請參閱使用 Always Encrypted 開發應用程式使用 Always Encrypted 搭配安全記憶體保護區開發應用程式

Prerequisites

  • 在資料庫中設定永遠加密。 此流程牽涉到佈建 Always Encrypted 金鑰,以及設定所選資料庫資料行的加密。 如果您沒有已設定 Always Encrypted 的資料庫,請遵循教學課程:開始使用 Always Encrypted 中的指示。
  • 如果您使用的 Always Encrypted 具有安全記憶體保護區,請參閱使用具有安全記憶體保護區的 Always Encrypted 開發應用程式,以了解其他必要條件。
  • 確定已在您的開發電腦上安裝必要的 .NET 平台。 透過 Microsoft.Data.SqlClient,.NET Framework 和 .NET Core 都支援 Always Encrypted 功能。 確定已在開發環境中,將 .NET Framework 4.6 或更新版本或是 .NET Core 2.1 或更新版本設定為目標 .NET 平台版本。 在 Microsoft.Data.SqlClient 2.1.0 版,.NET Standard 2.0 也支援 Always Encrypted 功能。 若要使用具有安全記憶體保護區的 Always Encrypted,需要 .NET Standard 2.1。 如果您想要在沒有證明的情況下使用 VBS 記憶體保護區,則需要 Microsoft.Data.SqlClient 4.1 版或更新版本。 如果您使用的是 Visual Studio,請參閱 Framework 目標概觀

下表摘要說明搭配 Microsoft.Data.SqlClient 使用 Always Encrypted 所需的 .NET 平台。

支援 Always Encrypted 支援具有安全記憶體保護區的 Always Encrypted 目標 Framework Microsoft.Data.SqlClient 版本 作業系統
.NET Framework 4.6+ 1.1.0+ Windows
.NET Core 2.1+ 2.1.0+1 Windows、Linux、macOS
.NET Standard 2.0 2.1.0+ Windows、Linux、macOS
Yes .NET Standard 2.1+ 2.1.0+ Windows、Linux、macOS

注意

1 在 Microsoft.Data.SqlClient 2.1.0 版之前,只有 Windows 上支援 Always Encrypted。

為應用程式查詢啟用 [永遠加密]

若要對以加密資料行為目標的參數進行加密,以及對查詢結果進行解密,最簡單的方式是將 Column Encryption Setting 連接字串關鍵字的值設定為 enabled

下列範例使用連接字串來啟用 Always Encrypted:

string connectionString = "Data Source=server63; Initial Catalog=Clinic; Integrated Security=true; Column Encryption Setting=enabled";
SqlConnection connection = new SqlConnection(connectionString);

下列程式碼片段為使用 SqlConnectionStringBuilder.ColumnEncryptionSetting 屬性的對等範例。

SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
builder.DataSource = "server63";
builder.InitialCatalog = "Clinic";
builder.IntegratedSecurity = true;
builder.ColumnEncryptionSetting = SqlConnectionColumnEncryptionSetting.Enabled;
SqlConnection connection = new SqlConnection(builder.ConnectionString);
connection.Open();

個別查詢也可以啟用 [永遠加密]。 請參閱以下的控制 Always Encrypted 的效能影響一節。 啟用 Always Encrypted 並不足以保證加密或解密成功。 您還需要確定︰

  • 應用程式要有 [檢視任何資料行的主要金鑰定義] 和 [檢視任何資料行的加密金鑰定義] 資料庫權限,才能存取資料庫中永遠加密金鑰的相關中繼資料。 如需詳細資料,請參閱 Always Encrypted (資料庫引擎) 中的「資料庫權限」一節
  • 應用程式可以存取保護資料行加密金鑰的資料行主要金鑰,這些加密金鑰會加密查詢的資料庫資料行。

啟用具有安全記憶體保護區的 Always Encrypted

從 Microsoft.Data.SqlClient 1.1.0 版開始,驅動程式支援具有安全記憶體保護區的 Always Encrypted

如需使用記憶體保護區開發應用程式的一般資訊,請參閱使用具有安全記憶體保護區的 Always Encrypted 開發應用程式

若要為資料庫連線啟用記憶體保護區計算,除了啟用 Always Encrypted 之外,您還必須設定下列連接字串關鍵字 (如上一節所述):

  • Attestation Protocol - 指定證明通訊協定。

    • 如果未指定此關鍵字,則會在連線停用安全記憶體保護區。
    • 如果您搭配虛擬化型安全性 (VBS) 記憶體保護區和主機守護者服務 (HGS) 使用 SQL Server,則此關鍵字的值應為 HGS
    • 如果您搭配 Intel SGX 記憶體保護區和 Microsoft Azure 證明使用 Azure SQL Database,則此關鍵字的值應為 AAS
    • 如果您搭配 VBS 記憶體保護區使用 Azure SQL Database 或 SQL Server,而且想要放棄證明,則此關鍵字的值應為 None。 需要 4.1 版或更新版本。

    注意

    'None' (沒有證明) 是 Azure SQL Database 中的 VBS 記憶體保護區唯一支援的選項。

  • Enclave Attestation URL - 指定證明 URL (證明服務端點)。 您必須從證明服務系統管理員取得環境的證明 URL。

如需逐步教學課程,請參閱教學課程:使用具有安全記憶體保護區的 Always Encrypted 開發 .NET 應用程式

擷取和修改加密資料行中的資料

一旦針對應用程式查詢啟用 Always Encrypted 之後,您就可以使用標準的 SqlClient API (請參閱在 ADO.NET 中擷取和修改資料 \(部分機器翻譯\)) 或 Microsoft.Data.SqlClient 命名空間 \(英文\) 中定義的 Microsoft .NET Data Provider for SQL Server API,來擷取或修改加密資料庫資料行中的資料。 如果您的應用程式具有必要的資料庫權限,而且可以存取資料行主要金鑰,則 Microsoft .NET Data Provider for SQL Server 會加密所有以加密資料行為目標的查詢參數,並解密擷取自加密資料行的資料,對應為資料庫結構描述中之資料行設定的 SQL Server 資料類型,傳回 .NET 類型純文字值。 若未啟用 Always Encrypted,使用目標加密資料行參數的查詢就會失敗。 只要查詢沒有以加密資料行為目標的參數,查詢就仍然可以從加密資料行擷取資料。 不過,Microsoft .NET Data Provider for SQL Server 將不會嘗試解密所有擷取自加密資料行的值,而應用程式將會接收二進位的加密資料 (作為位元組陣列)。

下表摘要說明查詢的行為,視 Always Encrypted 是否啟用而定:

查詢特性 Always Encrypted 已啟用,且應用程式可以存取金鑰和金鑰中繼資料 Always Encrypted 已啟用,且應用程式可以存取金鑰或金鑰中繼資料 [永遠加密] 已停用
有以加密資料行為目標之參數的查詢。 以清晰簡明的方式加密參數值。 錯誤 錯誤
查詢從加密的資料行擷取資料,沒有任何以加密資料行為目標的參數。 以清晰簡明的方式解密來自加密資料行的結果。 應用程式會接收 .NET 資料類型的純文字值,其對應到為加密資料行所設定的 SQL Server 類型。 錯誤 不會解密來自加密資料行的結果。 應用程式收到位元組陣列 (byte[]) 形態的加密值。

以下範例將說明擷取和修改加密資料行中的資料。 這些範例假設目標資料表具有下列結構描述。 SSNBirthDate 資料行均已加密。

CREATE TABLE [dbo].[Patients]([PatientId] [int] IDENTITY(1,1),
 [SSN] [char](11) COLLATE Latin1_General_BIN2
 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC,
 ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
 COLUMN_ENCRYPTION_KEY = CEK1) NOT NULL,
 [FirstName] [nvarchar](50) NULL,
 [LastName] [nvarchar](50) NULL,
 [BirthDate] [date]
 ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED,
 ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
 COLUMN_ENCRYPTION_KEY = CEK1) NOT NULL
 PRIMARY KEY CLUSTERED ([PatientId] ASC) ON [PRIMARY])
 GO

插入資料範例

本例會將資料列插入病患資料表。 請注意下列詳細資料:

  • 範例程式碼中沒有任何需要加密的項目。 Microsoft .NET Data Provider for SQL Server 會自動偵測並加密以加密資料行為目標的 paramSSNparamBirthdate 參數。 此行為可讓加密對應用程式變得透明化。
  • 插入至資料庫資料行的值,包括加密的資料行,會傳遞為 SqlParameter 物件。 雖然將值傳送到未加密的資料行時,使用 SqlParameter 為選擇性 (但強烈建議使用,因有利於防止 SQL 插入式攻擊),但對以加密資料行為目標的值為必要。 如果將插入 資料行中的值當作內嵌於查詢陳述式的常值傳遞,查詢就會失敗,因為 SSNMicrosoft .NET Data Provider for SQL ServerBirthDate 無法判斷目標加密資料行的值,所以不會加密值。 結果,伺服器會因與加密資料行不相容而拒絕它們。
  • SSN 資料行為目標的參數資料類型是設定為 ANSI (非 Unicode) 字串,這會對應到 char/varchar SQL Server 資料類型。 如果參數類型先前設為 Unicode 字串 (String),並對應至 nchar/nvarchar,則查詢會失敗,因為 Always Encrypted 不支援從已加密 nchar/nvarchar 值轉換成已加密 char/varchar 值。 如需資料類型對應的相關資訊,請參閱 SQL Server 資料型別對應
  • 使用 SqlParameter.SqlDbType 屬性,明確地將插入到 BirthDate 資料行的參數資料類型設定為目標 SQL Server 資料類型,而不依賴隱含地將 .NET 類型對應到使用 SqlParameter.DbType 屬性時所套用的 SQL Server 資料類型。 DateTime 結構 \(部分機器翻譯\) 預設會對應到日期時間 SQL Server 資料類型。 因為 BirthDate 資料行的資料類型是日期,而且 Always Encrypted 不支援將加密的日期時間值轉換成加密的日期值,所以使用預設的對應會造成錯誤。
string connectionString = "Data Source=server63; Initial Catalog=Clinic; Integrated Security=true; Column Encryption Setting=enabled";

using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
using (SqlCommand cmd = connection.CreateCommand())
{
    connection.Open();
    cmd.CommandText = @"INSERT INTO [dbo].[Patients] ([SSN], [FirstName], [LastName], [BirthDate]) VALUES (@SSN, @FirstName, @LastName, @BirthDate);";

    SqlParameter paramSSN = cmd.CreateParameter();
    paramSSN.ParameterName = @"@SSN";
    paramSSN.DbType = DbType.AnsiStringFixedLength;
    paramSSN.Direction = ParameterDirection.Input;
    paramSSN.Value = "795-73-9838";
    paramSSN.Size = 11;
    cmd.Parameters.Add(paramSSN);

    SqlParameter paramFirstName = cmd.CreateParameter();
    paramFirstName.ParameterName = @"@FirstName";
    paramFirstName.DbType = DbType.String;
    paramFirstName.Direction = ParameterDirection.Input;
    paramFirstName.Value = "Catherine";
    paramFirstName.Size = 50;
    cmd.Parameters.Add(paramFirstName);

    SqlParameter paramLastName = cmd.CreateParameter();
    paramLastName.ParameterName = @"@LastName";
    paramLastName.DbType = DbType.String;
    paramLastName.Direction = ParameterDirection.Input;
    paramLastName.Value = "Abel";
    paramLastName.Size = 50;
    cmd.Parameters.Add(paramLastName);

    SqlParameter paramBirthdate = cmd.CreateParameter();
    paramBirthdate.ParameterName = @"@BirthDate";
    paramBirthdate.SqlDbType = SqlDbType.Date;
    paramBirthdate.Direction = ParameterDirection.Input;
    paramBirthdate.Value = new DateTime(1996, 09, 10);
    cmd.Parameters.Add(paramBirthdate);

    cmd.ExecuteNonQuery();
}

擷取純文字資料範例

下列範例會示範根據加密值篩選資料,以及從加密資料行擷取純文字資料。

string connectionString = "Data Source=server63; Initial Catalog=Clinic; Integrated Security=true; Column Encryption Setting=enabled";
using (SqlConnection connection = new SqlConnection(builder.ConnectionString))
using (SqlCommand cmd = connection.CreateCommand())
{
    connection.Open();
    cmd.CommandText = @"SELECT [SSN], [FirstName], [LastName], [BirthDate] FROM [dbo].[Patients] WHERE SSN=@SSN";

    SqlParameter paramSSN = cmd.CreateParameter();
    paramSSN.ParameterName = @"@SSN";
    paramSSN.DbType = DbType.AnsiStringFixedLength;
    paramSSN.Direction = ParameterDirection.Input;
    paramSSN.Value = "795-73-9838";
    paramSSN.Size = 11;
    cmd.Parameters.Add(paramSSN);
    using (SqlDataReader reader = cmd.ExecuteReader())
    {
        if (reader.HasRows)
        {
            while (reader.Read())
            {
                Console.WriteLine(@"{0}, {1}, {2}, {3}", reader[0], reader[1], reader[2], ((DateTime)reader[3]).ToShortDateString());
            }
        }
    }
}

注意

  • WHERE 子句中用於篩選 SSN 資料行的值需要使用 SqlParameter 來傳遞,因此 Microsoft .NET Data Provider for SQL Server 能夠明確地將其加密,然後傳送到資料庫。

  • 此程式將以純文字形式列印所有值,因為 Microsoft .NET Data Provider for SQL Server 會明確地解密擷取自 SSNBirthDate 資料行的資料。

  • 如果查詢使用了確定性加密來進行加密,則可以執行資料行的相等比較。

擷取加密的資料範例

若未啟用 Always Encrypted,只要查詢沒有以加密資料行為目標的參數,查詢就仍然可以從加密資料行擷取資料。

下例會示範如何從加密資料行擷取二進位的加密資料。

string connectionString = "Data Source=server63; Initial Catalog=Clinic; Integrated Security=true";

using (SqlConnection connection = new SqlConnection(connectionString))
using (SqlCommand cmd = connection.CreateCommand())
{
    connection.Open();
    cmd.CommandText = @"SELECT [SSN], [FirstName], [LastName], [BirthDate] FROM [dbo].[Patients] WHERE [LastName]=@LastName";

    SqlParameter paramLastName = cmd.CreateParameter();
    paramLastName.ParameterName = @"@LastName";
    paramLastName.DbType = DbType.String;
    paramLastName.Direction = ParameterDirection.Input;
    paramLastName.Value = "Abel";
    paramLastName.Size = 50;
    cmd.Parameters.Add(paramLastName);
    using (SqlDataReader reader = cmd.ExecuteReader())
    {
        if (reader.HasRows)
        {
            while (reader.Read())
            {
                Console.WriteLine(@"{0}, {1}, {2}, {3}", BitConverter.ToString((byte[])reader[0]), reader[1], reader[2], BitConverter.ToString((byte[])reader[3]));
            }
        }
    }
}

注意

  • 由於連接字串中並未啟用 Always Encrypted,因此查詢將會以位元組陣列 (程式會將值轉換為字串) 傳回加密的 SSNBirthDate 值。

  • 從加密資料行擷取資料但停用 [永遠加密] 的查詢可以有參數,只要沒有任何參數以加密資料行為目標。 上述依 LastName 篩選的查詢,在資料庫中未加密。 如果依 SSNBirthDate 篩選查詢,查詢就會失敗。

避免常見的加密資料行查詢問題

本節說明從 .NET 應用程式查詢加密資料行時常見的錯誤類別,以及如何避免的一些指導方針。

不支援的資料類型轉換錯誤

[永遠加密] 支援極少數的加密資料類型轉換。 如需支援的類型轉換詳細清單,請參閱 Always Encrypted。 請執行下列作業以免發生資料型別轉換錯誤︰

  • 設定以加密資料行為目標的參數類型,如此一來,參數的 SQL Server 資料類型就會與目標資料行的類型完全相同,或是支援將參數的 SQL Server 資料類型轉換成資料行的目標類型。 您可以使用 SqlParameter.SqlDbType 屬性,強制執行 .NET 資料類型到特定 SQL Server 資料類型的必要對應。
  • 確認小數和數值 SQL Server 資料類型資料行為目標之參數的有效位數和小數位數,和為目標資料行設定的有效位數和小數位數相同。
  • 驗證在修改目標資料行值的查詢中,以 datetime2、datetimeoffset 或 time SQL Server 資料類型資料行為目標之參數的有效位數,不大於目標資料行的有效位數。

因為傳送純文字,而不是傳送加密值所造成的錯誤。

任何以加密資料行為目標的值都需要在應用程式內加密。 嘗試對加密資料行插入/修改或以純文字值篩選,會造成類似錯誤如下:

Microsoft.Data.SqlClient.SqlException (0x80131904): 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 = 'CEK_Auto1', column_encryption_key_database_name = 'Clinic') collation_name = 'SQL_Latin1_General_CP1_CI_AS'

若要避免這類錯誤,請確定︰

  • (針對連接字串或特定查詢的 SqlCommand 物件) 以加密資料行為目標的應用程式查詢已啟用 [永遠加密]。
  • 您可以使用 SqlParameter 傳送以加密資料行為目標的資料。 下列範例示範以常值/常數錯誤篩選加密資料行 (SSN) 的查詢 (而不是在 SqlParameter 物件內傳遞常值)。
using (SqlCommand cmd = connection.CreateCommand())
{
    cmd.CommandText = @"SELECT [SSN], [FirstName], [LastName], [BirthDate] FROM [dbo].[Patients] WHERE SSN = '795-73-9838'";
    cmd.ExecuteNonQuery();
}

使用資料行主要金鑰存放區

若要加密參數值或解密查詢結果的資料,Microsoft .NET Data Provider for SQL Server 需要取得為目標資料行設定的資料行加密金鑰。 資料行加密金鑰是以加密形式存放在資料庫中繼資料。 每個資料行加密金鑰都有對應的資料行主要金鑰,主要金鑰是用於加密資料行加密金鑰。 資料庫中繼資料不會儲存資料行主要金鑰,而是只包含金鑰存放區的相關資訊,金鑰存放區包含特定的資料行主要金鑰,以及金鑰在金鑰存放區中的位置。

為取得資料行加密金鑰的純文字值,Microsoft .NET Data Provider for SQL Server 需先取得有關資料行加密金鑰及其對應資料行主要金鑰的中繼資料。 其接著會使用中繼資料中的資訊來連絡包含資料行主要金鑰的金鑰存放區,並將加密的資料行加密金鑰解密。 Microsoft .NET Data Provider for SQL Server 會使用資料行主要金鑰存放區提供者來與金鑰存放區通訊,這是衍生自 SqlColumnEncryptionKeyStoreProvider 類別 \(英文\) 的類別執行個體。

取得資料行加密金鑰的程序︰

  1. 若已針對查詢啟用 Always Encrypted,則當查詢具有參數時,Microsoft .NET Data Provider for SQL Server 就會明確呼叫 sys.sp_describe_parameter_encryption,來擷取以加密資料行為目標之參數的加密中繼資料。 針對查詢結果所包含的加密資料,SQL Server 會自動附加加密中繼資料。 資料行主要金鑰的資訊包括:

    • 套件金鑰存放區的金鑰存放區提供者名稱,而該存放區包含資料行主要金鑰。
    • 金鑰路徑,其指定金鑰存放區中資料行主要金鑰的位置。

    資料行加密金鑰的資訊包括:

    • 資料行加密金鑰的加密值。
    • 用來將資料行加密金鑰加密的演算法名稱。
  2. Microsoft .NET Data Provider for SQL Server 會使用資料行主要金鑰存放區提供者的名稱,在內部資料結構中查閱提供者物件,該物件為衍生自 SqlColumnEncryptionKeyStoreProvider 類別的類別執行個體。

  3. 若要將資料行加密金鑰解密,Microsoft .NET Data Provider for SQL Server 會呼叫 SqlColumnEncryptionKeyStoreProvider.DecryptColumnEncryptionKey() 方法,來傳遞資料行主要金鑰路徑、資料行加密金鑰的加密值及加密演算法的名稱,用以產生加密的資料行加密金鑰。

使用內建資料行主要金鑰存放區提供者

Microsoft .NET Data Provider for SQL Server 隨附於下列內建資料行主要金鑰存放區提供者,它們都會以特定提供者名稱 (用以查閱提供者) 預先註冊。 只有 Windows 支援這些內建的金鑰存放區提供者。

類別 描述 提供者 (查閱) 名稱 平台
SqlColumnEncryptionCertificateStoreProvider 類別 Windows 憑證存放區的提供者。 MSSQL_CERTIFICATE_STORE Windows
SqlColumnEncryptionCngProvider 類別 支援以下功能的金鑰存放區提供者:Microsoft Cryptography API:新一代 (CNG) API。 一般而言,這類型的存放區是硬體安全性模組,可保護和管理數位金鑰,並提供密碼編譯處理的實體裝置。 MSSQL_CNG_STORE Windows
SqlColumnEncryptionCspProvider 類別 支援 Microsoft Cryptography API (CAPI)的金鑰存放區提供者。 一般而言,這類型的存放區是硬體安全性模組,可保護和管理數位金鑰,並提供密碼編譯處理的實體裝置。 MSSQL_CSP_PROVIDER Windows

您不需進行任何應用程式程式碼變更,即可使用這些提供者,但請注意下列細節:

  • 您 (或您的 DBA) 需要確認設定在資料行主要金鑰中繼資料的提供者名稱是否正確,且資料行主要金鑰路徑符合指定提供者有效的金鑰路徑格式。 建議您使用 SQL Server Management Studio 等工具設定金鑰,這樣在發出 CREATE COLUMN MASTER KEY (Transact-SQL) 陳述式時,會自動產生有效的提供者名稱和金鑰路徑。 如需詳細資訊,請參閱 使用 SQL Server Management Studio 設定永遠加密使用 PowerShell 設定永遠加密
  • 確定應用程式可以存取金鑰存放區中的金鑰。 此流程可能需要金鑰和 (或) 金鑰存放區的存取權 (視金鑰存放區而定) 授與應用程式,或執行其他金鑰存放區專屬的組態步驟。 例如,若要存取實作 CNG 或 CAPI 的金鑰存放區 (例如硬體安全性模組),您需要確定應用程式機器上已安裝存放區實作 CNG 或 CAPI 的程式庫。 如需詳細資料,請參閱建立及儲存 Always Encrypted 的資料行主要金鑰

使用 Azure Key Vault 提供者

Azure 金鑰保存庫是存放和管理永遠加密資料行主要金鑰的方便選項 (尤其是當應用程式裝載在 Azure 時)。 Microsoft .NET Data Provider for SQL Server 不包含適用於 Azure Key Vault 的內建資料行主要金鑰存放區提供者,但可以 NuGet 套件形式提供 (Microsoft.Data.SqLClient.AlwaysEncrypted.AzureKeyVaultProvider),讓您輕鬆整合應用程式。 如需詳細資訊,請參閱 一律加密 - 透過資料加密並將您的加密金鑰儲存在 Azure 金鑰保存庫,來保護 SQL Database 中的機密資料

類別 描述 提供者 (查閱) 名稱 平台
SqlColumnEncryptionAzureKeyVaultProvider 類別 (英文) Azure Key Vault 的提供者。 AZURE_KEY_VAULT Windows、Linux、macOS

.NET 支援能力

版本 Microsoft.Data.SqlClient 版本 .NET 平台
3.0.0 3.0.0+ .NET Framework 4.6.1+、.NET Core 2.1+、.NET Standard 2.0+
2.0.0 1.1.3+
2.1.0+
.NET Framework 4.6.1+、.NET Core 2.1+
.NET Standard 2.0+
1.2.0 1.0.19269.1+
2.1.0+
.NET Framework 4.6+、.NET Core 2.1+
.NET Standard 2.0+
1.1.0 1.0.19269.1+ .NET Framework 4.6+、.NET Core 2.1+
1.0.0 1.0.19269.1+ .NET Framework 4.6+、.NET Core 2.1+

v3.0.0 起,Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 支援在使用 SqlConnection.RegisterColumnEncryptionKeyStoreProvidersOnConnectionSqlCommand.RegisterColumnEncryptionKeyStoreProvidersOnCommand API 註冊提供者時,執行資料行加密金鑰快取功能。

v2.0.0 起,Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 支援新的 Azure.CoreAzure.Identity API 透過 Azure Key Vault 執行驗證。 TokenCredential 實作的執行個體現在可以傳遞至 SqlColumnEncryptionAzureKeyVaultProvider 建構函式,以初始化 Azure Key Vault 提供者物件。

注意

Microsoft.Data.SqLClient.AlwaysEncrypted.AzureKeyVaultProvider 支援 Azure Key Vault 中的保存庫和受控 HSM

如需示範使用 Azure Key Vault 加密/解密的範例,請參閱使用 Always Encrypted 的 Azure Key Vault使用具有安全記憶體保護區的 Always Encrypted 的 Azure Key Vault

實作自訂資料行主要金鑰存放區提供者

如果您想要將資料行主要金鑰儲存於現有提供者不支援的金鑰存放區,可以透過擴充 SqlColumnEncryptionKeyStoreProvider 類別,並使用下列其中一種方法註冊自訂提供者,以實作該提供者:

public class MyCustomKeyStoreProvider : SqlColumnEncryptionKeyStoreProvider
{
    public const string ProviderName = "MY_CUSTOM_STORE";

    public override byte[] EncryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] columnEncryptionKey)
    {
        // Logic for encrypting a column encrypted key.
    }
    public override byte[] DecryptColumnEncryptionKey(string masterKeyPath, string encryptionAlgorithm, byte[] EncryptedColumnEncryptionKey)
    {
        // Logic for decrypting a column encrypted key.
    }
}  
class Program
{
    static void Main(string[] args)
    {
        Dictionary<string, SqlColumnEncryptionKeyStoreProvider> providers =
            new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>();
        providers.Add(MyCustomKeyStoreProvider.ProviderName, new MyCustomKeyStoreProvider());
        SqlConnection.RegisterColumnEncryptionKeyStoreProviders(providers);
        // ...
    }
}

資料行加密金鑰快取優先順序

本節適用於 Microsoft .NET Data Provider for SQL Server 3.0 版和更新版本。

Microsoft .NET Data Provider for SQL Server 不會快取在連線或命令執行個體上註冊之自訂金鑰存放區提供者所解密的資料行加密金鑰 (CEK)。 自訂金鑰存放區提供者應實作自己的 CEK 快取機制。

Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProviderv3.0.0 起,每個 Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 執行個體都有自己的 CEK 快取實作。 在連線或命令執行個體上註冊時,Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 執行個體所解密的 CEK 會在該執行個體超出範圍時清除:

class Program
{
    static void Main()
    {
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            using (SqlCommand command = connection.CreateCommand())
            {
                Dictionary<string, SqlColumnEncryptionKeyStoreProvider> customKeyStoreProviders = new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>();
                SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider();
                customKeyStoreProviders.Add(SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider);
                command.RegisterColumnEncryptionKeyStoreProvidersOnCommand(customKeyStoreProviders);
                // Perform database operation using Azure Key Vault Provider
                // Any decrypted column encryption keys will be cached
            } // Column encryption key cache of "azureKeyVaultProvider" is cleared when "azureKeyVaultProvider" goes out of scope
        }
    }
}

注意

如果使用 SqlConnection.RegisterColumnEncryptionKeyStoreProviders 方法在驅動程式全域註冊金鑰存放區提供者執行個體,驅動程式將會停用自訂金鑰存放區提供者實作的 CEK 快取。 任何 CEK 快取實作都應該在快取 CEK 之前參考 SqlColumnEncryptionKeyStoreProvider.ColumnEncryptionKeyCacheTtl 的值;如果值為零,則不快取。 這可避免在嘗試設定金鑰快取時重複快取及造成使用者混淆。

註冊自訂資料行主要金鑰存放區提供者

本章節適用於提供者的 3.0 版和更新版本。

您可以在三個不同層級向驅動程式註冊自訂主要金鑰存放區提供者。 三種註冊的優先順序如下:

  • 檢查依據命令的註冊是否為空。
  • 如果依據命令的註冊是空的,則檢查依據連線的註冊是否為空。
  • 如果依據連線的註冊是空的,則檢查全域註冊。

在註冊層級找到任何金鑰存放區提供者後,驅動程式就不會回復至其他註冊來搜尋提供者。 如果已註冊提供者,但在層級中找不到適當的提供者,則會擲回例外狀況,只包含檢查過的註冊中已註冊的提供者。

Windows 憑證存放區、CNG Store 和 CSP 可用的內建資料行主要金鑰存放區提供者會預先註冊。

這三個註冊層級支援查詢加密資料時的不同案例。 適當的方法可用來確保應用程式使用者在可提供必要的資料行主要金鑰時,能夠向包含資料行主要金鑰的金鑰存放區進行驗證,以存取純文字資料。

在多個使用者之間共用 SqlConnection 執行個體的應用程式,適合使用 SqlCommand.RegisterColumnEncryptionKeyStoreProvidersOnCommand。 每個使用者都必須在 SqlCommand 執行個體上註冊金鑰存放區提供者,才能執行查詢以存取加密的資料行。 如果金鑰存放區提供者能夠透過使用者獲得的認證存取金鑰存放區中的必要資料行主要金鑰,查詢就會成功。

為每個使用者建立一個 SqlConnection 執行個體的應用程式,適合使用 SqlConnection.RegisterColumnEncryptionKeyStoreProvidersOnConnection。 透過這個方法註冊的金鑰存放區提供者,可供連線用於存取加密資料的任何查詢。

使用 SqlConnection.RegisterColumnEncryptionKeyStoreProviders 註冊的金鑰存放區提供者,會在向金鑰存放區進行驗證時,使用應用程式提供的身分識別。

下列範例顯示在連線執行個體上註冊的自訂資料行主要金鑰存放區提供者優先順序:

class Program
{
    static void Main()
    {
        Dictionary<string, SqlColumnEncryptionKeyStoreProvider> customKeyStoreProviders = new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>();
        MyCustomKeyStoreProvider myProvider = new MyCustomKeyStoreProvider();
        customKeyStoreProviders.Add("MY_CUSTOM_STORE", myProvider);
        // Registers the provider globally
        SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customKeyStoreProviders);

        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            customKeyStoreProviders.Clear();
            SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider();
            customKeyStoreProviders.Add(SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider);
            // Registers the provider on the connection
            // These providers will take precedence over globally registered providers
            connection.RegisterColumnEncryptionKeyStoreProvidersOnConnection(customKeyStoreProviders);
        }
    }
}

下列範例顯示在命令執行個體上註冊的自訂資料行主要金鑰存放區提供者的優先順序:

class Program
{
    static void Main()
    {
        Dictionary<string, SqlColumnEncryptionKeyStoreProvider> customKeyStoreProviders = new Dictionary<string, SqlColumnEncryptionKeyStoreProvider>();
        MyCustomKeyStoreProvider firstProvider = new MyCustomKeyStoreProvider();
        customKeyStoreProviders.Add("FIRST_CUSTOM_STORE", firstProvider);
        // Registers the provider globally
        SqlConnection.RegisterColumnEncryptionKeyStoreProviders(customKeyStoreProviders);

        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            customKeyStoreProviders.Clear();
            MyCustomKeyStoreProvider secondProvider = new MyCustomKeyStoreProvider();
            customKeyStoreProviders.Add("SECOND_CUSTOM_STORE", secondProvider);
            // Registers the provider on the connection
            connection.RegisterColumnEncryptionKeyStoreProvidersOnConnection(customKeyStoreProviders);

            using (SqlCommand command = connection.CreateCommand())
            {
                customKeyStoreProviders.Clear();
                SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider();
                customKeyStoreProviders.Add(SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider);
                // Registers the provider on the command
                // These providers will take precedence over connection-level providers and globally registered providers
                command.RegisterColumnEncryptionKeyStoreProvidersOnCommand(customKeyStoreProviders);
            }
        }
    }
}

使用資料行主要金鑰存放區提供者以程式設計方式佈建金鑰

Microsoft .NET Data Provider for SQL Server 存取加密資料行時,會明確尋找並呼叫正確的資料行主要金鑰存放區提供者,以解密資料行加密金鑰。 一般來說,一般應用程式程式碼不會直接呼叫資料行主要金鑰存放區提供者。 但是,您可以明確具現化和呼叫提供者,以程式設計方式建立和管理 Always Encrypted 金鑰︰產生加密資料行加密金鑰,及解密資料行加密金鑰 (例如在資料行主要金鑰輪替過程中)。 如需詳細資訊,請參閱 Always Encrypted 的金鑰管理概觀。 只有使用自訂的金鑰存放區提供者時,才可能需要實作您自己的金鑰管理工具。 使用儲存在金鑰存放區的金鑰時,凡是有內建提供者或位在 Azure Key Vault 中的金鑰,皆可使用 SQL Server Management Studio 或 PowerShell 等現有工具,來管理和佈建金鑰。 下列範例示範如何產生資料行加密金鑰,以及使用 SqlColumnEncryptionCertificateStoreProvider 類別 \(英文\),利用憑證來加密金鑰。

using System.Security.Cryptography;
static void Main(string[] args)
{
    byte[] EncryptedColumnEncryptionKey = GetEncryptedColumnEncryptonKey();
    Console.WriteLine("0x" + BitConverter.ToString(EncryptedColumnEncryptionKey).Replace("-", ""));
    Console.ReadKey();
}

static byte[]  GetEncryptedColumnEncryptonKey()
{
    int cekLength = 32;
    String certificateStoreLocation = "CurrentUser";
    String certificateThumbprint = "698C7F8E21B2158E9AED4978ADB147CF66574180";
    // Generate the plaintext column encryption key.
    byte[] columnEncryptionKey = new byte[cekLength];
    RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider();
    rngCsp.GetBytes(columnEncryptionKey);

    // Encrypt the column encryption key with a certificate.
    string keyPath = String.Format(@"{0}/My/{1}", certificateStoreLocation, certificateThumbprint);
    SqlColumnEncryptionCertificateStoreProvider provider = new SqlColumnEncryptionCertificateStoreProvider();
    return provider.EncryptColumnEncryptionKey(keyPath, @"RSA_OAEP", columnEncryptionKey);
}

控制 Always Encrypted 的效能影響

由於 Always Encrypted 是一項用戶端加密技術,因此,所觀察到的效能額外負荷大部分都是在用戶端,而非資料庫中。 除了加密和解密作業成本之外,用戶端上效能額外負荷的其他來源如下:

  • 額外反覆存取資料庫以擷取查詢參數的中繼資料。
  • 呼叫資料行主要金鑰存放區以存取資料行主要金鑰。

此節描述 Microsoft .NET Data Provider for SQL Server 的內建效能最佳化,以及如何控制上述兩個因素對效能的影響。

控制反覆存取以擷取查詢參數的中繼資料

如果已針對連線啟用 Always Encrypted,Microsoft .NET Data Provider for SQL Server 預設將針對每個參數化查詢呼叫 sys.sp_describe_parameter_encryption,以將查詢陳述式 (不含任何參數值) 傳遞至 SQL Server。 sys.sp_describe_parameter_encryption 會分析查詢陳述式,以找出是否有任何參數需要加密;如果有,則會對每個需要加密的參數傳回加密相關資訊,讓 Microsoft .NET Data Provider for SQL Server 能夠加密參數值。 上述行為可對用戶端應用程式確保高透明度。 只要在 SqlParameter 物件中將以加密資料行為目標的值傳遞至 Microsoft .NET Data Provider for SQL Server,應用程式 (及應用程式開發人員) 就不需要留意哪些查詢會存取加密資料行。

查詢中繼資料快取

Microsoft .NET Data Provider for SQL Server 會快取每個查詢陳述式的 sys.sp_describe_parameter_encryption 結果。 因此,如果多次執行同一個查詢陳述式,驅動程式就只會呼叫一次 sys.sp_describe_parameter_encryption。 查詢陳述式的加密中繼資料快取大幅減少了從資料庫擷取中繼資料的效能成本。 預設啟用快取。 您可以將 SqlConnection.ColumnEncryptionQueryMetadataCacheEnabled 屬性設定為 false,以停用參數中繼資料快取,但除非是如下所述的罕見情況,否則不建議這樣做:

請考慮具有兩個不同結構描述的資料庫︰s1s2。 每個結構描述都具有相同名稱的資料表︰ts1.ts2.t 資料表的定義相同,但與加密相關的屬性除外:名為 c 的資料行在 s1.t 中未加密,但在 s2.t 中加密。 資料庫有兩個使用者︰u1u2u1 使用者的預設結構描述是 s1u2 的預設結構描述是 s2。 .NET 應用程式會開啟兩個資料庫連線,在一個連線上模擬 u1 使用者在一個連線,並在另一個連線上模擬 u2。 應用程式會在使用者 u1 的連線上,傳送目標為 c 資料行之參數的查詢 (此查詢並未指定結構描述,因此會假設使用預設的使用者結構描述)。 接下來,應用程式會在 u2 使用者的連線上傳送相同的查詢。 如果已啟用查詢中繼資料快取,則在第一次查詢後,將在快取中填入中繼資料,以指出 c 資料行 (查詢參數的目標) 並未加密。 當第二次查詢有相同的查詢陳述式時,就會使用儲存在快取中的資訊。 因此,驅動程式將在未加密參數的情況下傳送查詢 (這不正確,因為已將目標資料行 s2.t.c 加密),以將參數的純文字值洩漏給伺服器。 伺服器會偵測到不相容,並強制驅動程式重新整理快取,因此應用程式會明確重新傳送具有正確加密參數值的查詢。 這種情況應該停用快取,以免向伺服器洩漏機密值。

在查詢層級設定 [永遠加密]

若要控制擷取加密中繼資料對參數化查詢的效能影響,您可以針對個別查詢啟用 Always Encrypted,而不是針對連線來設定它。 如此一來,您可以確保只有您知道有以加密資料行為目標之參數的查詢可以叫用 sys.sp_describe_parameter_encryption 。 不過請注意,如此一來就會降低加密的透明度︰如果您變更資料庫資料行的加密屬性,可能就需要變更應用程式的程式碼,以配合結構描述變更。

注意

在查詢層級設定 Always Encrypted,會限制參數加密中繼資料快取的效能優勢。

若要控制個別查詢的 [永遠加密] 行為,您需要使用 SqlCommandSqlCommandColumnEncryptionSetting的這個建構函式。 以下是一些實用的方針:

  • 如果用戶端應用程式的大多數查詢都會存取加密資料行:
    • 資料行加密設定連接字串關鍵字設為 [啟用] 。
    • 針對不會存取任何加密資料行的個別查詢,將 SqlCommandColumnEncryptionSetting 設為 Disabled。 此設定會禁止呼叫 sys.sp_describe_parameter_encryption 及嘗試解密結果集內的任何值。
    • 針對不含任何需要加密的參數,但會從加密資料行擷取資料的個別查詢,將 SqlCommandColumnEncryptionSetting 設為 ResultSetOnly。 此設定會禁止呼叫 sys.sp_describe_parameter_encryption 和參數加密。 查詢將能夠解密來自加密資料行的結果。
  • 如果用戶端應用程式的大多數查詢不會存取加密資料行:
    • 資料行加密設定連接字串關鍵字設為 [停用] 。
    • 針對有任何參數需要加密的個別查詢,將 SqlCommandColumnEncryptionSetting 設為 Enabled。 此設定會允許呼叫 sys.sp_describe_parameter_encryption 及解密擷取自加密資料行的任何查詢結果。
    • 針對不含任何需要加密的參數,但會從加密資料行擷取資料的查詢,將 SqlCommandColumnEncryptionSetting 設為 ResultSetOnly。 此設定會禁止呼叫 sys.sp_describe_parameter_encryption 和參數加密。 查詢將能夠解密來自加密資料行的結果。

下例中,資料庫連接已停用 [永遠加密]。 應用程式發出的查詢具有以 LastName 資料行為目標的參數,而此資料行未加密。 查詢會從加密的 SSNBirthDate 資料行擷取資料。 在這種情況下,不需要呼叫 sys.sp_describe_parameter_encryption 以擷取加密中繼資料。 不過,需要啟用查詢結果的解密,以使應用程式從兩個加密資料行接收純文字值。 SqlCommandColumnEncryptionSettinglyResultSetOn 設定會用於確認此項。

string connectionString = "Data Source=server63; Initial Catalog=Clinic; Integrated Security=true";
using (SqlConnection connection = new SqlConnection(connectionString))
using (SqlCommand cmd = new SqlCommand(@"SELECT [SSN], [FirstName], [LastName], [BirthDate] FROM [dbo].[Patients] WHERE [LastName]=@LastName",
connection, null, SqlCommandColumnEncryptionSetting.ResultSetOnly))
{
    connection.Open();
    SqlParameter paramLastName = cmd.CreateParameter();
    paramLastName.ParameterName = @"@LastName";
    paramLastName.DbType = DbType.String;
    paramLastName.Direction = ParameterDirection.Input;
    paramLastName.Value = "Abel";
    paramLastName.Size = 50;
    cmd.Parameters.Add(paramLastName);
    using (SqlDataReader reader = cmd.ExecuteReader())
    {
        if (reader.HasRows)
        {
            while (reader.Read())
            {
                Console.WriteLine(@"{0}, {1}, {2}, {3}", reader[0], reader[1], reader[2], ((DateTime)reader[3]).ToShortDateString());
            }
        }
    }
}

資料行加密金鑰快取

為了減少呼叫資料行主要金鑰存放區來解密資料行加密金鑰的次數,Microsoft .NET Data Provider for SQL Server 會將純文字資料行加密金鑰快取於記憶體中。 提供者從資料庫中繼資料中接收加密資料行加密金鑰值之後,驅動程式會先嘗試尋找對應加密金鑰值的純文字資料行加密金鑰。 只有在快取中找不到加的資料行加密金鑰值時,驅動程式才會呼叫包含資料行主要金鑰的金鑰存放區。

基於安全性考量,會在可設定的存留時間間隔之後收回快取項目。 預設的存留時間值是 2 小時。 如果您對於應用程式中以純文字快取的資料行加密金鑰存留時間有更嚴格的安全性需求,則可使用 SqlConnection.ColumnEncryptionKeyCacheTtl 屬性 \(英文\) 來變更它。

若金鑰存放區提供者是使用 SqlConnection.RegisterColumnEncryptionKeyStoreProvidersOnConnectionSqlCommand.RegisterColumnEncryptionKeyStoreProvidersOnCommand 註冊,Microsoft .NET Data Provider for SQL Server 不會快取其解密資料行加密金鑰。 反之,自訂金鑰存放區提供者必須實作自己的快取機制。 Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProviderv3.0.0 和更新版本隨附自己的快取實作。

為了支援相同應用程式的不同使用者可能執行多個查詢的案例,自訂金鑰存放區提供者可以對應至使用者,並在該使用者專屬的連線或命令執行個體上註冊。 下列範例示範如何在相同使用者的不同 SqlCommand 物件之間,重複使用 Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider 的執行個體。 其資料行加密金鑰快取會在多個查詢之間保存,以減少金鑰存放區的往返次數:

using Microsoft.Data.SqlClient;
using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider;
using System.Collections.Generic;

class Program
{
    // Maps a SqlColumnEncryptionAzureKeyVaultProvider to some object that represents a user
    static Dictionary<object, SqlColumnEncryptionAzureKeyVaultProvider> providerByUser = new();

    void ExecuteSelectQuery(object user, SqlConnection connection)
    {
        // Check if the user already has a SqlColumnEncryptionAzureKeyVaultProvider
        SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = providerByUser[user];
        if (azureKeyVaultProvider is null)
        {
            // Create a new SqlColumnEncryptionAzureKeyVaultProvider with the user's credentials and save it for future use
            azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider();
            providerByUser[user] = azureKeyVaultProvider;
        }

        Dictionary<string, SqlColumnEncryptionKeyStoreProvider> customProviders = new();
        customProviders.Add(SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider);

        using SqlCommand command = new("SELECT * FROM Customers", connection);
        command.RegisterColumnEncryptionKeyStoreProvidersOnCommand(customProviders);
        // Perform database operations
        // Any decrypted column encryption keys will be cached by azureKeyVaultProvider
    }

    void ExecuteUpdateQuery(object user, SqlConnection connection)
    {
        // Check if the user already has a SqlColumnEncryptionAzureKeyVaultProvider
        SqlColumnEncryptionAzureKeyVaultProvider azureKeyVaultProvider = providerByUser[user];
        if (azureKeyVaultProvider is null)
        {
            // Create a new SqlColumnEncryptionAzureKeyVaultProvider with the user's credentials and save it for future use
            azureKeyVaultProvider = new SqlColumnEncryptionAzureKeyVaultProvider();
            providerByUser[user] = azureKeyVaultProvider;
        }

        Dictionary<string, SqlColumnEncryptionKeyStoreProvider> customProviders = new();
        customProviders.Add(SqlColumnEncryptionAzureKeyVaultProvider.ProviderName, azureKeyVaultProvider);

        using SqlCommand command = new("UPDATE Customers SET Name = 'NewName' WHERE CustomerId = 1", connection);
        command.RegisterColumnEncryptionKeyStoreProvidersOnCommand(customProviders);
        // Perform database operations
        // Any decrypted column encryption keys will be cached by azureKeyVaultProvider
    }
}

對遭入侵的 SQL Server 啟用額外保護

根據預設,Microsoft .NET Data Provider for SQL Server 依賴資料庫系統 (SQL Server 或 Azure SQL Database) 提供有關資料庫中哪些資料行已加密及加密方法的中繼資料。 加密中繼資料可讓 Microsoft .NET Data Provider for SQL Server 加密查詢參數及解密查詢結果,而不需任何應用程式輸入,這可大幅降低需要在應用程式中進行變更的次數。 不過,如果 SQL Server 處理序遭到入侵,且攻擊者竄改了 SQL Server 傳送到 Microsoft .NET Data Provider for SQL Server 的中繼資料,該攻擊者或許就能竊取敏感性資訊。 本章節描述針對此類攻擊提供多一層防護的 API,代價是降低透明度。

強制參數加密

Microsoft .NET Data Provider for SQL Server 在將參數化查詢傳送到 SQL Server 之前,會先要求 SQL Server (藉由呼叫 sys.sp_describe_parameter_encryption) 分析查詢陳述式,並提供查詢中哪些參數應該加密的相關資訊。 即使資料庫的資料行已加密,但遭入侵的 SQL Server 執行個體可能會傳送指出參數並未以加密資料行為目標的中繼資料,而誤導 Microsoft .NET Data Provider for SQL Server。 因此,Microsoft .NET Data Provider for SQL Server 不會加密參數值,並會將其以純文字形式傳送到遭入侵的 SQL Server 執行個體。

為避免這類的攻擊,應用程式可將參數的 SqlParameter.ForceColumnEncryption 屬性 設為 true。 如果 Microsoft .NET Data Provider for SQL Server 從伺服器接收到的中繼資料指出參數不需要加密,則此設定將導致其擲回例外狀況。

雖然使用 SqlParameter.ForceColumnEncryption 屬性有助於改善安全性,但也會降低對用戶端應用程式加密的透明度。 如果更新資料庫結構描述來變更加密的資料行集合,您可能也需要變更應用程式。

下列程式碼範例示範如何使用 SqlParameter.ForceColumnEncryption 屬性,來防止將社會安全號碼以純文字形式傳送到資料庫。

using (SqlCommand cmd = _sqlconn.CreateCommand())
{
    // Use parameterized queries to access Always Encrypted data.

    cmd.CommandText = @"SELECT [SSN], [FirstName], [LastName], [BirthDate] FROM [dbo].[Patients] WHERE [SSN] = @SSN;";

    SqlParameter paramSSN = cmd.CreateParameter();
    paramSSN.ParameterName = @"@SSN";
    paramSSN.DbType = DbType.AnsiStringFixedLength;
    paramSSN.Direction = ParameterDirection.Input;
    paramSSN.Value = ssn;
    paramSSN.Size = 11;
    paramSSN.ForceColumnEncryption = true;
    cmd.Parameters.Add(paramSSN);

    using (SqlDataReader reader = cmd.ExecuteReader())
    {
        // Do something.
    }
}

設定受信任的資料行主要金鑰路徑

SQL Server 針對以加密資料行為目標的查詢參數及擷取自加密資料行的結果所傳回的加密中繼資料,包括識別金鑰存放區之資料行主要金鑰的金鑰路徑,以及金鑰存放區的金鑰位置。 如果 SQL Server 執行個體遭到入侵,它可能會傳送金鑰路徑,以將 Microsoft .NET Data Provider for SQL Server 導向到受攻擊者控制的位置。 如果金鑰存放區要求應用程式驗證,此流程可能會洩漏金鑰存放區認證。

為了避免此類攻擊,應用程式可以使用 SqlConnection.ColumnEncryptionTrustedMasterKeyPaths 屬性 \(英文\),為指定的伺服器指定受信任的金鑰路徑清單。 如果 Microsoft .NET Data Provider for SQL Server 接收到受信任金鑰路徑清單以外的金鑰路徑,就會擲回例外狀況。

雖然設定信任的金鑰路徑可提升應用程式安全性,但您每次變換資料行主要金鑰 (每次資料行主要金鑰路徑變更) 時,都必須變更應用程式的程式碼和 (或) 設定。

下例示範如何設定受信任的資料行主要金鑰路徑︰

// Configure trusted key paths to protect against fake key paths sent by a compromised SQL Server instance
// First, create a list of trusted key paths for your server
List<string> trustedKeyPathList = new List<string>();
trustedKeyPathList.Add("CurrentUser/my/425CFBB9DDDD081BB0061534CE6AB06CB5283F5Ea");

// Register the trusted key path list for your server
SqlConnection.ColumnEncryptionTrustedMasterKeyPaths.Add(serverName, trustedKeyPathList);

使用 SqlBulkCopy 複製加密的資料

使用 SqlBulkCopy,您可以將已加密並儲存在某份資料表的資料,複製到另一份資料表,而不需要解密資料。 若要這樣做:

  • 請確定目標資料表的加密組態與來源資料表的組態完全一致。 特別是,這兩份資料表都必須加密相同的資料行,且這些資料行必須使用相同的加密類型和相同的加密金鑰來加密。 如果任一目標資料行的加密方式不同於其對應的來源資料行,您將無法在複製作業後,解密目標資料表中的資料。 資料會損毀。
  • 設定這兩個資料庫對來源資料表和目標資料表的連線,但不啟用 Always Encrypted。
  • 設定 AllowEncryptedValueModifications 選項 (請參閱 SqlBulkCopyOptions \(英文\))。

注意

請謹慎指定 AllowEncryptedValueModifications。 此設定可能會導致資料庫損毀,因為 Microsoft .NET Data Provider for SQL Server 不會檢查資料是否確實加密,或是否使用與目標資料行相同的加密類型、演算法和金鑰正確加密。

以下是將資料從某份資料表複製到另一份資料表的範例。 假設 SSNBirthDate 資料行均已加密。

static public void CopyTablesUsingBulk(string sourceTable, string targetTable)
{
    string sourceConnectionString = "Data Source=server63; Initial Catalog=Clinic; Integrated Security=true";
    string targetConnectionString = "Data Source=server64; Initial Catalog=Clinic; Integrated Security=true";
    using (SqlConnection connSource = new SqlConnection(sourceConnectionString))
    {
        connSource.Open();
        using (SqlCommand cmd = new SqlCommand(string.Format("SELECT [PatientID], [SSN], [FirstName], [LastName], [BirthDate] FROM {0}", sourceTable), connSource))
        {
            using (SqlDataReader reader = cmd.ExecuteReader())
            using (SqlBulkCopy copy = new SqlBulkCopy(targetConnectionString, SqlBulkCopyOptions.KeepIdentity | SqlBulkCopyOptions.AllowEncryptedValueModifications))
            {
                copy.EnableStreaming = true;
                copy.DestinationTableName = targetTable;
                copy.WriteToServer(reader);
            }
        }
    }
}

一律加密 API 參考

Namespace:Microsoft.Data.SqlClient

組件: Microsoft.Data.SqlClient.dll

名稱 描述
SqlColumnEncryptionCertificateStoreProvider 類別 Windows 憑證存放區的金鑰存放區提供者。
SqlColumnEncryptionCngProvider 類別 Microsoft 密碼編譯 API 的金鑰存放區提供者:新一代 (CNG)。
SqlColumnEncryptionCspProvider 類別 Microsoft CAPI 型密碼編譯服務提供者 (CSP) 的金鑰存放區提供者。
SqlColumnEncryptionKeyStoreProvider 類別 金鑰存放區提供者的基底類別。
SqlCommandColumnEncryptionSetting 列舉 可對個別查詢控制永遠加密行為的設定。
SqlConnectionAttestationProtocol Enumeration 使用具有安全記憶體保護區的 Always Encrypted 時,請指定證明通訊協定的值
SqlConnectionColumnEncryptionSetting 列舉 讓資料庫連接得以加密和解密的設定。
SqlConnectionStringBuilder.ColumnEncryptionSetting 屬性 在連接字串中取得及設定 [永遠加密]。
SqlConnection.ColumnEncryptionQueryMetadataCacheEnabled 屬性 啟用和停用加密查詢中繼資料快取。
SqlConnection.ColumnEncryptionKeyCacheTtl 屬性 取得及設定資料行加密金鑰快取項目的存留時間。
SqlConnection.ColumnEncryptionTrustedMasterKeyPaths 屬性 可讓您為資料庫伺服器設定受信任的金鑰路徑清單。 如果驅動程式在處理應用程式查詢時,接收到不在清單上的金鑰路徑,則查詢會失敗。 此屬性會提供額外保護,防範安全性攻擊入侵 SQL Server 並提供假的金鑰路徑,從而導致金鑰存放區認證外洩。
SqlConnection.RegisterColumnEncryptionKeyStoreProviders 方法 可讓您註冊自訂金鑰存放區提供者。 它是一個字典,會將金鑰存放區提供者名稱對應至金鑰存放區提供者實作。
SqlCommand 建構函式 (String、SqlConnection、SqlTransaction、SqlCommandColumnEncryptionSetting) 可讓您對個別查詢控制永遠加密的行為。
SqlParameter.ForceColumnEncryption 屬性 強制將參數加密。 如果 SQL Server 向驅動程式告知參數不需要加密,使用該參數的查詢就會失敗。 此屬性會提供額外保護,防範安全性攻擊入侵 SQL Server 並向用戶端提供不正確的加密中繼資料,從而導致資料暴露。
連接字串 \(英文\) 關鍵字:Column Encryption Setting=enabled 啟用或停用連線的永遠加密功能。

另請參閱