Você tem um Web site ou outro sistema que lida com dados confidenciais de algum tipo? Parece que toda vez que inicio uma conversa sobre segurança, as pessoas perguntam como lidar com o problema recorrente sobre como armazenar dados confidenciais. Strings de conexão e senhas representam um problema óbvio. É melhor usar segurança integrada para resolver este problema, pelo menos com o SQL Server™ ou com um banco de dados Oracle. Mas o que dizer sobre números de cartão de crédito e outras informações financeiras ou pessoais? A encriptação pode nos ajudar?

Pensando Em Criptografia

Se estiver considerando usar criptografia, tenha em mente que se trata de uma técnica de compressão secreta ou de transferência. A encriptação não resolve totalmente o problema, mas pode converter dados confidenciais volumosos (tais como documentos confidenciais e outros), em objetos bem menores chamados chaves. Mas ainda teremos uma chave! O truque consiste em descobrir como separar a chave dos dados, para reduzir as chances de que um atacante possa obter os dados encriptados e a chave de decriptação.

Muitos sistemas que lidam com dados confidenciais (número de cartão de crédito, informações pessoais e outros) pode ser fatorados para permitir que máquinas altamente expostas, tais como servidores Web, aceitem dados confidenciais e os codifiquem sem a necessidade de uma chave secreta. Assim que o ciphertext esteja armazenado em um banco de dados ou em uma fila, um processo de negócio que roda em uma máquina dedicada e altamente segura, poderá recuperar o ciphertext e decifrá-lo.

Usando Criptografia de Chave Pública com Código Gerenciado

Existem várias formas de usar encriptação de chave pública no Microsoft® .NET Framework. Se estivermos trabalhando com uma infra-estrutura de chave pública (public key infrastructure - PKI), poderemos fazer uso do suporte rico para certificados da versão 2.0 da biblioteca de classes base (base class library - BCL). Mas nem todo o mundo quer gerenciar um PKI, e realmente isto não será necessário para implementar as idéias que apresentamos aqui. Para os casos mais simples, o BCL contém um par de classes de baixo nível, que permitem usar diretamente a encriptação de chave pública, sem o custo indireto dos certificados.

A classe que usaremos aqui é a RSACryptoServiceProvider. Esta classe permite gerar pares de chaves públicas/privadas e armazená-las em recipientes de chaves (key containers) CryptoAPI (CAPI), em um sistema de arquivos ou em um cartão inteligente (smart card). E, logicamente, também podemos usar esta classe para encriptar e decriptar dados.

Uma coisa que devemos manter em mente a respeito de chaves públicas, é que são muito boas para encriptar dados pequenos, tais como chaves secretas ou caches. Porem não foram projetadas para encriptar dados grandes. Portanto, nossa solução usará um cryptosystem híbrido típico: geraremos uma chave secreta aleatória e a usaremos o Padrão de Criptografia Avançado (Advanced Encryption Standard - AES), para encriptar os dados que desejamos proteger. Assim, usaremos o RSACryptoServiceProvider para encriptar a chave AES.

Quão Esperto é seu Recipiente de Chaves?

O RSACryptoServiceProvider é uma camada fina por cima do CAPI, onde as chaves são administradas dentro de um objeto chamado de recipiente de chaves. Esta abstração permite usar uma variedade de provedores de criptografia, inclusive alguns dispositivos de hardware, tal como cartões inteligentes, para armazenar o material chave. Cartões inteligentes (SmartCards) são um bom meio de armazenamento de chaves privadas, porque o material chave nunca deixa o cartão. O cartão tem sua própria memória embutida e um microprocessador, desta maneira, podemos gerar o material chave necessário, armazená-lo e usá-lo para encriptar e decriptar dados sem necessidade de evidenciar a chave privada da memória do computador principal.

Se houver alguma necessidade de manutenção do agente de decriptação (quer dizer, se este não rodar 24x7), o administrador simplesmente pode tirar o cartão inteligente, para reduzir a janela de exposição ao ataque. Quando a chave privada não estiver disponível no sistema, os dados encriptados estarão muito mais seguros!

Nossa solução usará o provedor de serviços criptográfico embutido (cryptographic service provider - CSP), portanto não será necessário algum hardware especial para usar o código deste artigo. Mas não seria difícil de conseguir o código para suportar um cartão inteligente, e cartões inteligentes não são caros. Poderemos obter um leitor de cartão inteligente e um conjunto de cinco cartões por uma quantia razoável, portanto, não permita que o custo do hardware o impeça de usá-lo.

Recipientes chave e o Key Manager

Antes de realizar a encriptação dos dados usando uma chave pública, precisamos gerar um par de chaves, e isto implica aprender um pouco a respeito de recipientes de chaves. Cada recipiente chave CAPI pode ter até dois pares de chaves públicas/privadas. Uma é utilizada para "troca de chave” ("key exchange"), que é um termo rebuscado que significa que o par de chaves é usado para encriptar e decriptar chaves secretas como a chave AES, que usarei para proteger os dados sensíveis. O outro conjunto serve para criar assinaturas digitais que não serão utilizadas em este artigo.

Podemos ter tantos recipientes chave quantos forem necessários, a única limitação é o espaço de armazenamento disponível no dispositivo usado. A pesar de que tecnicamente só precisamos de um par de chaves para encriptar e decriptar dados, o agente de encriptação assume que poderá ser solicitado o uso de chaves diferentes para encriptar dados. Portanto, cada bit de dados encriptados será etiquetado com um nome chave, de modo que o agente de decriptação saberá qual chave usar para a decriptação. Este esquema de versionamento de chave simples permite facilmente trocar as chaves de vez em quando, no caso de uma chave ficar comprometida ou como uma melhor prática, pois quanto mais tempo uma única chave é usada, mais fraca e arriscada a mesma se torna.

Considerando que a administração das chaves é essencial, o primeiro programa que escrevi foi uma aplicação Windows® Forms chamada Key Manager, que permite ao administrador enumerar, criar e excluir recipientes chave em uma máquina (este programa poderá ser adaptado para administrar recipientes chave em um cartão inteligente). Cada vez que for criado um novo recipiente chave, o Key Manager gera uma nova chave pública/privada RSA para aquele recipiente e exibe os detalhes da chave pública em um formato fácil de recortar e colar na seção do arquivo config do agente de encriptação. Estas configurações não só incluem a própria chave pública, como também o nome do recipiente chave que as outras partes da solução usam para versionar a chave (como veremos depois).

Usar o Key Manager é fácil: basta rodá-lo na máquina onde será hospedado o agente de decriptação. Selecione um tamanho de chave e um nome para o recipiente chave, tal como ”MyApp-1” (podemos usar o número hifenizado para versionamento de chave). Uma vez criado o recipiente chave para o agente de decriptação, a chave privada será instalada e estará pronta para processar dados encriptados com a chave MyApp-1. Por padrão, os administradores da máquina e o usuário que criou o recipiente chave, terão permissão para usar a chave. Isto é reforçado pela lista de controle de acesso (access control list - ACL) no arquivo recipiente chave.

Clique no botão ”Security…” e o Key Manager abrirá o diálogo de configuração “file properties” de este ACL, para conceder acesso de leitura à conta do usuário que o agente de decriptação usará. Copie os dados de configuração de chave pública que o Key Manager provê, e cole-os no arquivo config do agente de encriptação, que contém o nome do recipiente chave e os detalhes da chave pública. Isto é tudo o que o agente de encriptação precisa para encriptar dados. A propósito, se quiser saber onde estão os arquivos de sistema em que são armazenados os recipientes chave, olhe dentro do seguinte diretório oculto:


\Documentos e Settings\All Users\ApplicationData\Microsoft\Crypto\RSA\MachineKeys

O CAPI também suporta recipientes em nível de usuário, porem, desde que podemos controlar o ACL em cada recipiente em nível de máquina, para simplificar usamos recipientes chave em este nível.

Tamanho da chave

O tamanho da chave gerada provoca impacto em nível de segurança e, inversamente, no desempenho que obteremos do cryptosystem. Também, quanto maior for a chave escolhida, mais tempo será gasto na sua geração pelo Key Manager. Em um ambiente de PC Virtual que roda em uma máquina razoavelmente rápida, uma chave de 4096 bits leva vários segundos para ser gerada, enquanto que uma de 16384 bits leva vários minutos! Por enquanto, o Key Manager cria estas chaves na thread da interface do usuário, o que é conveniente para uma aplicação de exemplo, porem uma melhoria óbvia seria gerar a chave assincronamente, para não congelar a IU. Independentemente do tempo usado para criar o par de chaves, nossa maior preocupação deverá ser a segurança versus o desempenho em tempo de execução. Portanto fiz alguns testes para cronometrar a encriptação e a decriptação, usando os cinco tamanhos de chave que o Key Manager suporta.

O tempo de encriptação é medido em mili-segundos, porem a decriptação está medida em segundos. Operações com chaves privadas não são baratas com o RSA. Se o desempenho for uma questão crítica, poderemos adquirir hardware especializado para aliviar a encriptação e/ou decriptação da CPU principal, obtendo um maior desempenho sem sacrificar a segurança. Isto requererá um CSP alternativo, que implicará em um bocado de adaptação no código, mas esse será um preço pequeno em relação à melhoria de desempenho. De uma perspectiva de segurança, recomendamos trabalhar com chaves de RSA com pelo menos 2048 bits como o mínimo absoluto; 4096 bits parecem ser atualmente o ponto de inflexão na curva de desempenho/segurança. No futuro, podemos imaginar ir além.

Ao escrever esta aplicação, tentei usar o RSACryptoServiceProvider, apenas porque é o administrador de interface oficial a ser usado com recipientes chave CAPI. Mas, não me atendia totalmente, portanto executei um pouco da magia P/Invoke, para chegar diretamente no CAPI. Isto me permitiu enumerar recipientes chave para poder observar todos os recipientes chave rapidamente no provedor de RSA embutido na sua máquina. As duas funções CAPI importantes com as quais trabalhei, são CryptAcquireContext e CryptGetProvParam. Empacotei todos os detalhes em uma classe gerenciada chamada KeyContainerManager, a qual pode ser útil para você.

O Encriptador e Decriptador

Além da KeyContainerManager, há duas outras classes base na minha solução: Encryptor e Decryptor, que serão usadas respectivamente pelos agentes de encriptação e decriptação. O Decryptor precisa de uma chave privada instalada na máquina em um recipiente chave, enquanto que o Encryptor só precisa dos detalhes de chave pública providos via seu construtor. Um modo fácil de configurar isto é usando o arquivo de configuração do agente de encriptação. Em minha aplicação de exemplo, o agente de encriptação é uma aplicação ASP.NET Web, portanto simplesmente configuramos a informação de chave pública no arquivo Web.config.

O RSACryptoServiceProvider contém um par de métodos novos na versão 2.0 que permitem acessar facilmente informações de chave:


string ToXmlString(bool includePrivateParameters)

void FromXmlString(string xmlParameters)

Na realidade, o Key Manager chama o ToXmlString(false) para extrair os parâmetros chave públicos como um string XML, que será então encriptado e colocado em uma entrada de configuração , que poderemos colar no arquivo config do agente de encriptação. Minha classe Encryptor deve ser construída com um par de informações: um nome de chave e os parâmetros da chave pública, como strings XML:


public Encryptor(string keyName,

string publicKeyXmlParams) {

    this.keyName = keyName;

    rsa.FromXmlString(publicKeyXmlParams);

}

O versionamento de chave foi implementado com o argumento. Qualquer string colocado aqui será adicionado como um prefixo ao ciphertext produzido quando encriptarmos os dados. O Decryptor utilizará este prefixo como o recipiente chave ao qual será ligado ao decriptar os dados. Desejaremos configurar facilmente isto no agente de encriptação, razão pela qual o Key Manager também inclui este item de configuração. No arquivo Web.config da aplicação de exemplo, acharemos um elemento "" como a seguir:

Quando a aplicação de exemplo for encriptar algum dado, irá construir um Encryptor novo com estes dois pares de dados do Web.config e chamará o Encrypt. Quando quiser mudar chaves, precisará apenas atualizar o arquivo Web.config para indicar os novos dados de nome de chave e de chave pública, e a aplicação imediatamente passará a usar a nova chave. E como cada bit de dados encriptado inclui o nome da chave, o decriptador sempre sabe qual chave deverá usar para qualquer ciphertext específico. Apenas será necessário garantir que a máquina hospedeira do agente de decriptação, tem todas as chaves necessárias instaladas.

O método Encryptor.Encrypt UTF8, encripta o string passado, gerando uma chave secreta aleatória AES de 256 BITS e um vetor de inicialização (initialization vector - IV) aleatório DE 128 BITS, que são usados para encriptar os dados passados pela classe AES chamada RijndaelManaged (Rijndael é o nome do algoritmo que ganhou a competição para se tornar o padrão AES, portanto pode ser considerado como um sinônimo de AES. O seu nome é baseado nos sobrenomes dos criptógrafos que o inventaram e se pronuncia mais ou menos como"Rain-Doll").

A seguir, o Encryptor encripta a chave AES com a chave pública RSA, uma chave encriptada AES de tamanho pré-fixado, seguida pelo IV e os dados encriptados. Este blob binário de dados é encriptado em um string de base64, a qual é prefixada com o nome da chave que identifica qual chave RSA deverá ser usada para decriptar a chave AES.

Se isto parecer muito complicado, é bom saber que usando a classe Encryptor, simplesmente passamos um string a ser encriptado, e será retornado um string que contém toda a informação necessária para o Decryptor.Decrypt fazer o seu trabalho (bem, tudo menos o material chave privado, é claro). Isto torna a interface do Encryptor e Decryptor muito fácil de usar:


string Encryptor.Encrypt(string plaintext);

string Decryptor.Decrypt(string ciphertext);

É claro que este não é o único modo de empacotar o blob encriptado. Poderíamos achar mais conveniente separar o nome chave do blob, ou trabalhar com arranjos de byte ao invés de com strings, mas essas são mudanças fáceis de fazer, uma vez que seja entendida a arquitetura global desta solução. Outra coisa que deveríamos considerar futuramente em uma aplicação real, consiste no formato de versionamento. Não espere até a versão 2 para colocar um número de versão nestes blobs encriptados!

O Decryptor.Decrypt extrai a estrutura de dados recuperando primeiro o nome do recipiente chave e depois construindo um instância de RSACryptoServiceProvider ligada ao recipiente chave. Decripta a encriptação em base64 e recupera então a chave encriptada AES, realizando a decriptação com o RSACryptoServiceProvider.Decrypt. Este último passo, é provavelmente a parte mais intensiva em CPU do processo inteiro, se estivermos encriptação strings relativamente pequenos. Neste momento uma chave AES, representada por uma instância da classe RijndaelManaged, é inicializada com a chave secreta decriptada e com o IV, e os dados encriptados são decriptados. Finalmente, o arranjo de bytes decriptado é decodificado em um string via UTF8 e retornado como o resultado de Decryptor.Decrypt.

O cipher mode da minha solução é o encadeamento de bloco de cifra (cipher block chaining - CBC), um esquema muito popular que é padrão dentro do .NET Framework. Este modo de cifra não provê proteção de integridade da carga útil. Quer dizer, quando decriptarmos os dados, não há garantias de que seremos notificados, caso o ciphertext for falsificado. A pesar de que a proteção de integridade não é o objetivo de esta solução particular, com base na minha experiência, muitas pessoas acreditam que obtém proteção de integridade automaticamente quando encriptam os dados. Acontece com freqüência que, ao decriptar ciphertext encriptado CBC que foi modificado, obtermos uma exceção quando o preenchimento final não é o que está sendo esperado, mas isto não constitui uma garantia real de integridade. Não deterá um atacante determinado.

O Exemplo do Agente de Encriptação (TAke Orders)

Reuni estas classes em uma aplicação de exemplo que pode ser construída com o Visual Studio 2005, e recomendo que experimente. Já vimos a primeira parte; o Key Manager; que é usado para administrar as chaves da aplicação. Também construímos um exemplo que encripta o agente chamado Take Orders, uma aplicação Web que aceita alguns dados privados em uma interface do usuário simples: um endereço de e-mail, o número do cartão de crédito, e a data de expiração. O Take Orders coloca estes dados em um string XML e usa uma instância de Encryptor para encriptar o string inteiro.

Agora, poderíamos enfileirar cada ordem encriptada em uma fila de mensagens ou banco de dados, mas quis fazer uma aplicação fácil de distribuir, portanto, escolhi escrever os dados em um arquivo para ser colocado em um diretório de sua escolha. Precisará configurar um diretório para o Take Orders. Coloque o ACL no diretório, de forma que a aplicação Web tenha permissão para escrever nele. O arquivo Web.config para Take Orders, inclui um elemento chamado dataDir, que deverá apontar para este diretório.

O Exemplo do Agente de Decriptação (Process Orders)

Na a aplicação Process Orders, uma aplicação Windows Forms que exibe cada ordem baseada na data de criação do arquivo. Selecionando uma ordem, será exibido o conteúdo do arquivo encriptado. O arquivo pode ser então decriptado, clicando-se no botão Decrypt Order que usa uma instância da classe Decryptor para extrair o arquivo order e decriptar a mensagem. O Process Orders exibirá então os detalhes de order, inclusive o endereço de e-mail, o número de cartão de crédito e a data de expiração providos pelo usuário.

Neste momento, podemos clicar no botão Process Order, para enviar para um e-mail e comunicar que a ordem foi processada. Também remove do disco o arquivo order encriptado, fazendo com que este sistema se comporte de forma semelhante a uma fila. Se usar seu próprio endereço de e-mail e servidor de SMTP, obterá um e-mail. Estes pequenos usos extras da classe SmtpClient recentemente refatorada, pode ser achada no novo namespace System.Net.Mail do .NET Framework 2.0.

Indo em Frente

Pense em novos modos de lidar com dados sensíveis. Fatorando o sistema em agentes que encriptam e decriptam dados independentemente um do outro, é possível limitar significativamente a exposição de dados sensíveis. É claro que a ameaça da revelação de informações nunca será completamente neutralizada. Se o agente de decriptação estiver comprometido, assim o estarão seus dados sensíveis (ou pelo menos os dados que são encriptados usando as chaves contidas no agente de decriptação).

Lembre-se que um cartão inteligente torna mais fácil a remoção completa de material chave privado, quando o agente de decriptação não estiver ativo, limitando mesmo ainda mais a exposição. Portanto, brinque com esta aplicação e pense em modos de pôr em uso as idéias por trás da mesma.