Als onderdeel van T-SQL Tuesday #69 heb ik geblogd over de beperkingen van Always Encrypted, en ik heb daar vermeld dat de prestaties negatief kunnen worden beïnvloed door het gebruik ervan (zoals je zou verwachten, heeft sterkere beveiliging vaak nadelen). In dit bericht wilde ik hier snel naar kijken, in gedachten houdend (opnieuw) dat deze resultaten zijn gebaseerd op CTP 2.2-code, dus heel vroeg in de ontwikkelingscyclus, en niet noodzakelijk een afspiegeling zijn van de prestaties die u zult zie komen RTM.
Ten eerste wilde ik aantonen dat Always Encrypted werkt vanuit clienttoepassingen, zelfs als de nieuwste versie van SQL Server 2016 daar niet is geïnstalleerd. U moet echter wel de preview van .NET Framework 4.6 installeren (meest recente versie hier, en dat kan veranderen) om de Column Encryption Setting
te ondersteunen kenmerk verbindingsreeks. Als je Windows 10 gebruikt of Visual Studio 2015 hebt geïnstalleerd, is deze stap niet nodig, omdat je al een voldoende recente versie van het .NET Framework moet hebben.
Vervolgens moet u ervoor zorgen dat het Always Encrypted-certificaat op alle clients bestaat. U maakt de hoofd- en kolomcoderingssleutels in de database, zoals elke Always Encrypted-zelfstudie u zal laten zien, dan moet u het certificaat van die machine exporteren en het importeren op de andere waar de toepassingscode zal worden uitgevoerd. Open certmgr.msc
, en vouw Certificaten – Huidige gebruiker> Persoonlijk> Certificaten uit, en daar zou er een moeten staan met de naam Always Encrypted Certificate
. Klik daar met de rechtermuisknop op, kies Alle taken> Exporteren en volg de aanwijzingen. Ik heb de privésleutel geëxporteerd en een wachtwoord opgegeven, wat een .pfx-bestand opleverde. Dan herhaal je gewoon het tegenovergestelde proces op de clientcomputers:Open certmgr.msc
, vouw Certificaten – Huidige gebruiker> Persoonlijk uit, klik met de rechtermuisknop op Certificaten, kies Alle taken> Importeren en wijs naar het .pfx-bestand dat u hierboven hebt gemaakt. (Officiële hulp hier.)
(Er zijn veiligere manieren om deze certificaten te beheren - het is niet waarschijnlijk dat u het certificaat op deze manier op alle machines wilt implementeren, aangezien u zich al snel zult afvragen wat het nut was? Ik deed dit alleen in mijn geïsoleerde omgeving voor de doeleinden van deze demo - ik wilde er zeker van zijn dat mijn toepassing gegevens via de draad ophaalde en niet alleen in het lokale geheugen.)
We creëren twee databases, een met een versleutelde tabel en een zonder. We doen dit om verbindingsreeksen te isoleren en ook om het ruimtegebruik te meten. Natuurlijk zijn er meer gedetailleerde manieren om te bepalen welke commando's een versleutelde verbinding moeten gebruiken - zie de opmerking met de titel "Controle van de prestatie-impact..." in dit artikel.
De tabellen zien er als volgt uit:
-- encrypted copy, in database Encrypted CREATE TABLE dbo.Employees ( ID INT IDENTITY(1,1) PRIMARY KEY, LastName NVARCHAR(32) COLLATE Latin1_General_BIN2 ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = ColumnKey) NOT NULL, Salary INT ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = ColumnKey) NOT NULL ); -- unencrypted copy, in database Normal CREATE TABLE dbo.Employees ( ID INT IDENTITY(1,1) PRIMARY KEY, LastName NVARCHAR(32) COLLATE Latin1_General_BIN2 NOT NULL, Salary INT NOT NULL );
Met deze tabellen op hun plaats, wilde ik een zeer eenvoudige opdrachtregeltoepassing opzetten om de volgende taken uit te voeren tegen zowel de versleutelde als niet-versleutelde versies van de tabel:
- Voeg 100.000 werknemers toe, één voor één
- Lees 100 willekeurige rijen, 1000 keer door
- Tijdstempels voor en na elke stap uitvoeren
We hebben dus een opgeslagen procedure in een volledig aparte database die wordt gebruikt om willekeurige gehele getallen te produceren om salarissen weer te geven, en willekeurige Unicode-reeksen van verschillende lengtes. We gaan dit één voor één doen om het werkelijke gebruik van 100.000 inserts die onafhankelijk van elkaar plaatsvinden beter te simuleren (hoewel niet gelijktijdig, omdat ik niet dapper genoeg ben om een multi-threaded C#-toepassing op de juiste manier te ontwikkelen en te beheren, of om te proberen te coördineren en synchroniseer meerdere instanties van een enkele applicatie).
CREATE DATABASE Utility; GO USE Utility; GO CREATE PROCEDURE dbo.GenerateNameAndSalary @Name NVARCHAR(32) OUTPUT, @Salary INT OUTPUT AS BEGIN SET NOCOUNT ON; SELECT @Name = LEFT(CONVERT(NVARCHAR(32), CRYPT_GEN_RANDOM(64)), RAND() * 32 + 1); SELECT @Salary = CONVERT(INT, RAND()*100000)/100*100; END GO
Een paar rijen voorbeelduitvoer (we geven niet om de daadwerkelijke inhoud van de tekenreeks, alleen dat deze varieert):
酹2ዌ륒㦢㮧羮怰㉤盿⚉嗝䬴敏⽁캘♜鼹䓧 98600 贓峂쌄탠❼缉腱蛽☎뱶 72000
Dan de opgeslagen procedures die de toepassing uiteindelijk zal aanroepen (deze zijn identiek in beide databases, aangezien uw zoekopdrachten niet gewijzigd hoeven te worden om Always Encrypted te ondersteunen):
CREATE PROCEDURE dbo.AddPerson @LastName NVARCHAR(32), @Salary INT AS BEGIN SET NOCOUNT ON; INSERT dbo.Employees(LastName, Salary) SELECT @LastName, @Salary; END GO CREATE PROCEDURE dbo.RetrievePeople AS BEGIN SET NOCOUNT ON; SELECT TOP (100) ID, LastName, Salary FROM dbo.Employees ORDER BY NEWID(); END GO
Nu de C#-code, te beginnen met het gedeelte connectionStrings van App.config. Het belangrijkste onderdeel is de Column Encryption Setting
optie voor alleen de database met de versleutelde kolommen (neem voor de beknoptheid aan dat alle drie de verbindingsreeksen dezelfde Data Source
bevatten , en dezelfde SQL-authenticatie User ID
en Password
):
<connectionStrings> <add name="Utility" connectionString="Initial Catalog=Utility;..."/> <add name="Normal" connectionString="Initial Catalog=Normal;..."/> <add name="Encrypt" connectionString="Initial Catalog=Encrypted; Column Encryption Setting=Enabled;..."/> </connectionStrings>
En Program.cs (sorry, voor demo's als deze, ik ben verschrikkelijk in het binnengaan en logisch hernoemen van dingen):
using System; using System.Collections.Generic; using System.Text; using System.Configuration; using System.Data; using System.Data.SqlClient; namespace AEDemo { class Program { static void Main(string[] args) { using (SqlConnection con1 = new SqlConnection()) { Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff")); string name; string EmptyString = ""; int salary; int i = 1; while (i <= 100000) { con1.ConnectionString = ConfigurationManager.ConnectionStrings["Utility"].ToString(); using (SqlCommand cmd1 = new SqlCommand("dbo.GenerateNameAndSalary", con1)) { cmd1.CommandType = CommandType.StoredProcedure; SqlParameter n = new SqlParameter("@Name", SqlDbType.NVarChar, 32) { Direction = ParameterDirection.Output }; SqlParameter s = new SqlParameter("@Salary", SqlDbType.Int) { Direction = ParameterDirection.Output }; cmd1.Parameters.Add(n); cmd1.Parameters.Add(s); con1.Open(); cmd1.ExecuteNonQuery(); name = n.Value.ToString(); salary = Convert.ToInt32(s.Value); con1.Close(); } using (SqlConnection con2 = new SqlConnection()) { con2.ConnectionString = ConfigurationManager.ConnectionStrings[args[0]].ToString(); using (SqlCommand cmd2 = new SqlCommand("dbo.AddPerson", con2)) { cmd2.CommandType = CommandType.StoredProcedure; SqlParameter n = new SqlParameter("@LastName", SqlDbType.NVarChar, 32); SqlParameter s = new SqlParameter("@Salary", SqlDbType.Int); n.Value = name; s.Value = salary; cmd2.Parameters.Add(n); cmd2.Parameters.Add(s); con2.Open(); cmd2.ExecuteNonQuery(); con2.Close(); } } i++; } Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff")); i = 1; while (i <= 1000) { using (SqlConnection con3 = new SqlConnection()) { con3.ConnectionString = ConfigurationManager.ConnectionStrings[args[0]].ToString(); using (SqlCommand cmd3 = new SqlCommand("dbo.RetrievePeople", con3)) { cmd3.CommandType = CommandType.StoredProcedure; con3.Open(); SqlDataReader rdr = cmd3.ExecuteReader(); while (rdr.Read()) { EmptyString += rdr[0].ToString(); } con3.Close(); } } i++; } Console.WriteLine(DateTime.UtcNow.ToString("hh:mm:ss.fffffff")); } } } }
Dan kunnen we de .exe aanroepen met de volgende commandoregels:
AEDemoConsole.exe "Normal" AEDemoConsole.exe "Encrypt"
En het zal drie regels output produceren voor elke oproep:de starttijd, de tijd nadat 100.000 rijen zijn ingevoegd en de tijd nadat 100 rijen 1000 keer zijn gelezen. Dit waren de resultaten die ik op mijn systeem zag, gemiddeld over 5 runs elk:
Duur (seconden) van het schrijven en lezen van gegevens
Er is een duidelijke impact op het schrijven van de gegevens - niet helemaal 2X, maar meer dan 1,5X. Er was een veel lagere delta bij het lezen en decoderen van de gegevens - althans in deze tests - maar dat was ook niet gratis.
Wat het ruimtegebruik betreft, is er ruwweg een boete van 3x voor het opslaan van versleutelde gegevens (gezien de aard van de meeste versleutelingsalgoritmen, zou dit niet schokkend moeten zijn). Houd er rekening mee dat dit een tabel was met slechts één geclusterde primaire sleutel. Dit waren de cijfers:
Space (MB) gebruikt om gegevens op te slaan
Er zijn dus duidelijk enkele sancties verbonden aan het gebruik van Always Encrypted, zoals meestal het geval is met zowat alle beveiligingsgerelateerde oplossingen (het gezegde "geen gratis lunch" komt in me op). Ik herhaal dat deze tests werden uitgevoerd tegen CTP 2.2, die radicaal anders kan zijn dan de definitieve release van SQL Server 2016. Ook kunnen deze verschillen die ik heb waargenomen alleen de aard van de tests weerspiegelen die ik heb verzonnen; Uiteraard hoop ik dat u deze aanpak kunt gebruiken om uw resultaten te testen tegen uw schema, op uw hardware en met uw gegevenstoegangspatronen.