Como parte do T-SQL Tuesday #69, escrevi no blog sobre as limitações do Always Encrypted e mencionei lá que o desempenho pode ser afetado negativamente pelo seu uso (como você pode esperar, uma segurança mais forte geralmente tem compensações). Neste post, eu queria dar uma olhada rápida nisso, tendo em mente (novamente) que esses resultados são baseados no código CTP 2.2, muito cedo no ciclo de desenvolvimento e não refletem necessariamente o desempenho que você ver vem RTM.
Primeiro, eu queria demonstrar que o Always Encrypted funciona a partir de aplicativos cliente, mesmo que a versão mais recente do SQL Server 2016 não esteja instalada lá. No entanto, você precisa instalar a visualização do .NET Framework 4.6 (versão mais recente aqui, e isso pode mudar) para oferecer suporte à
Column Encryption Setting
atributo de cadeia de conexão. Se você estiver executando o Windows 10 ou tiver instalado o Visual Studio 2015, esta etapa não é necessária, pois você já deve ter uma versão recente o suficiente do .NET Framework. Em seguida, você precisa verificar se o certificado Always Encrypted existe em todos os clientes. Você cria as chaves de criptografia mestre e de coluna no banco de dados, como qualquer tutorial do Always Encrypted mostrará, então você precisa exportar o certificado dessa máquina e importá-lo nas outras em que o código do aplicativo será executado. Abra
certmgr.msc
, e expanda Certificados – Usuário Atual> Pessoal> Certificados, e deve haver um chamado Always Encrypted Certificate
. Clique com o botão direito do mouse, escolha Todas as tarefas> Exportar e siga as instruções. Exportei a chave privada e forneci uma senha, que produziu um arquivo .pfx. Então você apenas repete o processo oposto nas máquinas clientes:Abra certmgr.msc
, expanda Certificados – Usuário Atual> Pessoal, clique com o botão direito do mouse em Certificados, escolha Todas as Tarefas> Importar e aponte para o arquivo .pfx que você criou acima. (Ajuda oficial aqui.) (Existem maneiras mais seguras de gerenciar esses certificados - não é provável que você queira apenas implantar o certificado assim em todas as máquinas, pois em breve você se perguntará qual era o objetivo? Eu estava fazendo isso apenas no meu ambiente isolado para os propósitos desta demonstração – eu queria ter certeza de que meu aplicativo estava recuperando dados pela rede e não apenas na memória local.)
Criamos dois bancos de dados, um com tabela criptografada e outro sem. Fazemos isso para isolar strings de conexão e também para medir o uso de espaço. Obviamente, existem maneiras mais granulares de controlar quais comandos precisam usar uma conexão habilitada para criptografia – consulte a nota intitulada "Controlando o impacto no desempenho..." neste artigo.
As tabelas ficam assim:
-- 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 );
Com essas tabelas em vigor, eu queria configurar um aplicativo de linha de comando muito simples para executar as seguintes tarefas nas versões criptografadas e não criptografadas da tabela:
- Insira 100.000 funcionários, um de cada vez
- Leia 100 linhas aleatórias, 1.000 vezes
- Carimbos de data e hora de saída antes e depois de cada etapa
Portanto, temos um procedimento armazenado em um banco de dados completamente separado usado para produzir números inteiros aleatórios para representar salários e strings Unicode aleatórias de comprimentos variados. Vamos fazer isso um de cada vez para simular melhor o uso real de 100.000 inserções acontecendo independentemente (embora não simultaneamente, pois não sou corajoso o suficiente para tentar desenvolver e gerenciar adequadamente um aplicativo C# multithread ou tentar coordenar e sincronizar várias instâncias de um único aplicativo).
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
Algumas linhas de saída de amostra (não nos importamos com o conteúdo real da string, apenas que varia):
酹2ዌ륒㦢㮧羮怰㉤盿⚉嗝䬴敏⽁캘♜鼹䓧 98600 贓峂쌄탠❼缉腱蛽☎뱶 72000
Em seguida, os procedimentos armazenados que o aplicativo finalmente chamará (são idênticos em ambos os bancos de dados, pois suas consultas não precisam ser alteradas para oferecer suporte ao Always Encrypted):
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
Agora, o código C#, começando com a parte connectionStrings de App.config. A parte importante é a
Column Encryption Setting
opção apenas para a base de dados com as colunas encriptadas (por questões de brevidade, suponha que todas as três cadeias de ligação contenham a mesma Data Source
, e a mesma autenticação SQL User ID
e 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>
E Program.cs (desculpe, para demos como essa, sou péssimo em entrar e renomear as coisas logicamente):
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")); } } } }
Então podemos chamar o .exe com as seguintes linhas de comando:
AEDemoConsole.exe "Normal" AEDemoConsole.exe "Encrypt"
E produzirá três linhas de saída para cada chamada:a hora de início, a hora após a inserção de 100.000 linhas e a hora após a leitura de 100 linhas 1.000 vezes. Aqui estão os resultados que vi no meu sistema, com média de 5 execuções cada:
Duração (segundos) de gravação e leitura de dados
Há um impacto claro na gravação dos dados – não exatamente 2X, mas mais de 1,5X. Havia um delta muito menor na leitura e descriptografia dos dados – pelo menos nesses testes – mas isso também não era gratuito.
No que diz respeito ao uso de espaço, há uma penalidade de aproximadamente 3 vezes para armazenar dados criptografados (dada a natureza da maioria dos algoritmos de criptografia, isso não deve ser chocante). Lembre-se de que isso estava em uma tabela com apenas uma única chave primária clusterizada. Aqui estavam os números:
Espaço (MB) usado para armazenar dados
Então, obviamente, há algumas penalidades com o uso do Always Encrypted, como normalmente ocorre com quase todas as soluções relacionadas à segurança (o ditado "sem almoço grátis" vem à mente). Vou repetir que esses testes foram executados no CTP 2.2, que pode ser radicalmente diferente da versão final do SQL Server 2016. Além disso, essas diferenças que observei podem refletir apenas a natureza dos testes que criei; obviamente, espero que você possa usar essa abordagem para testar seus resultados em relação ao seu esquema, em seu hardware e com seus padrões de acesso a dados.