Uso da criptografia com bancos de dados SQL

Adobe AIR 1.5 e posterior

Todos os aplicativos do Adobe AIR compartilham o mesmo mecanismo do banco de dados local. Consequentemente, qualquer aplicativo do AIR pode se conectar, ler e gravar em qualquer arquivo do banco de dados não criptografado. Começando com o Adobe AIR 1.5, o AIR inclui a capacidade de criar e se conectar a arquivos do banco de dados criptografado. Quando você usa um banco de dados criptografado, para se conectar a ele um aplicativo deve fornecer a chave de criptografia correta. Se a chave de criptografia incorreta (ou nenhuma chave) for fornecida, o aplicativo não poderá se conectar ao banco de dados. Consequentemente, o aplicativo não poderá ler, gravar ou alterar dados do banco de dados.

Para usar um banco de dados criptografado, você deve criar o banco de dados como banco de dados criptografado. Com um banco de dados criptografado existente, é possível abrir uma conexão com o banco de dados. Você também pode alterar a chave de criptografia de um banco de dados criptografado. Em vez de criar e estabelecer conexão com bancos de dados criptografados, as técnicas para trabalhar com um banco de dados criptografado são as mesmas que para um banco de dados não criptografado. Especificamente, a execução de instruções SQL ocorre da mesma forma em bancos de dados criptografados e não criptografados.

Uso de um banco de dados criptografado

A criptografia é útil sempre que você desejar restringir o acesso às informações armazenadas em um banco de dados. A funcionalidade de criptografia do banco de dados do Adobe AIR pode ser usada para várias finalidades. Estes são exemplos de casos em que você pode usar um banco de dados criptografado:

  • Um cache somente leitura de dados de aplicativos privados baixados de um servidor

  • Um armazenamento de aplicativo local para dados privados sincronizado com um servidor (os dados são enviados e carregados do servidor)

  • Arquivos criptografados usados como formato de arquivo para documentos criados e editados pelo aplicativo. Os arquivos podem ser privados para um usuário ou projetados para compartilhamento entre todos os usuários do aplicativo.

  • Qualquer outro uso de um armazenamento local de dados, como os descrito em Usos de bancos de dados SQL locais , onde os dados devem ser mantidos privados de pessoas com acesso ao computador ou aos arquivos do banco de dados.

Compreender o motivo pelo qual você deseja usar um banco de dados criptografado ajuda a decidir como arquitetar seu aplicativo. Especificamente, ele pode afetar a forma que seu aplicativo cria, obtém e armazena a chave de criptografia para o banco de dados. Para obter mais informações sobre essas considerações, consulte Considerações para usar criptografia com um banco de dados .

Além de um banco de dados criptografado, um mecanismo alternativo para manter dados confidenciais privados é o armazenamento local criptografado . Com o armazenamento local criptografado, você armazena um único valor ByteArray usando uma chave String. Apenas o aplicativo do AIR que armazena o valor pode acessá-lo, e somente no computador em que ele está armazenado. Com o armazenamento local criptografado, não é necessário criar sua própria chave de criptografia. Por esses motivos, o armazenamento local criptografado é mais adequado para armazenar facilmente um único valor ou um conjunto de valores que possa ser facilmente codificado em um ByteArray. Um banco de dados criptografado é mais adequado para conjuntos de dados maiores, onde o armazenamento de dados e consultas estruturadas são desejáveis. Para obter mais informações sobre o uso de armazenamento local criptografado, consulte Armazenamento local criptografado .

Criação de um banco de dados criptografado

Para usar um banco de dados criptografado, o arquivo do banco de dados deve ser criptografado quando ele for criado. Quando um banco de dados é criado como não criptografado, ele pode não ser criptografado posteriormente. Da mesma forma, um banco de dados criptografado não pode passar a não criptografado posteriormente. Se necessário, você também pode alterar a chave de criptografia de um banco de dados criptografado. Para obter detalhes, consulte Alteração da chave de criptografia de um banco de dados . Se você tiver um banco de dados existente que não seja criptografado e desejar usar a criptografia de banco de dados, poderá criar um novo banco de dados criptografado e copiar a estrutura de tabela e os dados existentes no novo banco de dados.

Criar um banco de dados criptografado é quase idêntico a criar um banco de dados não criptografado, como descrito em Criação de um banco de dados . Primeiro você cria uma instância do SQLConnection que representa a conexão ao banco de dados. Crie o banco de dados chamando o método open() ou o método openAsync() do objeto do SQLConnection, especificando para a localização do banco de dados um arquivo que ainda não exista. A única diferença na criação de um banco de dados criptografado é que você fornece um valor para o parâmetro encryptionKey (o quinto parâmetro do método open() e o sexto parâmetro do método openAsync() ).

Um valor válido do parâmetro encryptionKey é um objeto ByteArray que contém exatamente 16 bytes. http://help.adobe.com/pt_BR/Flash/CS5/AS3LR/flash/utils/ByteArray.html

Os exemplos a seguir demonstram a criação de um banco de dados criptografado. Para simplicidade, nestes exemplos, a chave de criptografia é codificada no código do aplicativo. No entanto, essa técnica é altamente desencorajada, pois não é segura.

var conn:SQLConnection = new SQLConnection(); 
     
var encryptionKey:ByteArray = new ByteArray(); 
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure! 
     
// Create an encrypted database in asynchronous mode 
conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey); 
     
// Create an encrypted database in synchronous mode 
conn.open(dbFile, SQLMode.CREATE, false, 1024, encryptionKey);

Para obter um exemplo que demonstre uma forma recomendada de gerar uma chave de criptografia, consulte Exemplo: Geração e uso de uma chave de criptografia .

Conexão com um banco de dados criptografado

Da mesma forma que criar um banco de dados criptografado, o procedimento para abrir uma conexão com um banco de dados criptografado é como conectar-se a um banco de dados não criptografado. Esse procedimento é descrito com mais detalhes em Conexão com um banco de dados . Use o método open() para abrir uma conexão no modo de execução síncrona ou o método openAsync() para abri-la no modo de execução assíncrona . A única diferença é que para abrir um banco de dados criptografado, especifica o valor correto para o parâmetro encryptionKey (o quinto parâmetro do método open() e o sexto parâmetro do método openAsync() ).

Se a chave de criptografia fornecida não for a correta, ocorre um erro. Para o método open() , é lançada uma exceção SQLError . Para o método openAsync() , o objeto SQLConnection gera um SQLErrorEvent , cuja propriedade error contém um objeto SQLError . Em qualquer um dos casos, o objeto SQLError gerado pela exceção tem o valor 3138 na propriedade errorID . Este ID de erro corresponde à mensagem de erro “O arquivo aberto não corresponde a um arquivo de banco de dados.”

O exemplo a seguir demonstra a abertura de um banco de dados criptografado em modo de execução assíncrona. Para simplicidade, neste exemplo a chave de criptografia é codificada no código do aplicativo. No entanto, essa técnica é altamente desencorajada, pois não é segura.

import flash.data.SQLConnection; 
import flash.data.SQLMode; 
import flash.events.SQLErrorEvent; 
import flash.events.SQLEvent; 
import flash.filesystem.File; 
     
var conn:SQLConnection = new SQLConnection(); 
conn.addEventListener(SQLEvent.OPEN, openHandler); 
conn.addEventListener(SQLErrorEvent.ERROR, errorHandler); 
var dbFile:File = File.applicationStorageDirectory.resolvePath("DBSample.db"); 
     
var encryptionKey:ByteArray = new ByteArray(); 
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure! 
     
conn.openAsync(dbFile, SQLMode.UPDATE, null, false, 1024, encryptionKey); 
     
function openHandler(event:SQLEvent):void 
{ 
    trace("the database opened successfully"); 
} 
     
function errorHandler(event:SQLErrorEvent):void 
{ 
    if (event.error.errorID == 3138) 
    { 
        trace("Incorrect encryption key"); 
    } 
    else 
    { 
        trace("Error message:", event.error.message); 
        trace("Details:", event.error.details); 
    } 
} 

O exemplo a seguir demonstra a abertura de um banco de dados criptografado em modo de execução síncrona. Para simplicidade, neste exemplo a chave de criptografia é codificada no código do aplicativo. No entanto, essa técnica é altamente desencorajada, pois não é segura.

import flash.data.SQLConnection; 
import flash.data.SQLMode; 
import flash.filesystem.File; 
     
var conn:SQLConnection = new SQLConnection(); 
var dbFile:File = File.applicationStorageDirectory.resolvePath("DBSample.db"); 
     
var encryptionKey:ByteArray = new ByteArray(); 
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure! 
     
try 
{ 
    conn.open(dbFile, SQLMode.UPDATE, false, 1024, encryptionKey); 
    trace("the database was created successfully"); 
} 
catch (error:SQLError) 
{ 
    if (error.errorID == 3138) 
    { 
        trace("Incorrect encryption key"); 
    } 
    else 
    { 
        trace("Error message:", error.message); 
        trace("Details:", error.details); 
    } 
} 

Para obter um exemplo que demonstre uma forma recomendada de gerar uma chave de criptografia, consulte Exemplo: Geração e uso de uma chave de criptografia .

Alteração da chave de criptografia de um banco de dados

Quando um banco de dados é criptografado, você pode alterar a chave de criptografia do banco de dados posteriormente. Para alterar a chave criptográfica de um banco de dados, primeiro abra uma conexão com o banco de dados criando uma instância de SQLConnection e chamando seu método open() ou openAsync() . Quando o banco de dados for conectado, chame o método reencrypt() , enviando a nova chave de criptografia como um argumento.

Como a maioria das operações de banco de dados, o comportamento do método reencrypt() depende se a conexão do banco de dados usa modo de execução síncrona ou assíncrona. Se você usar o método open() para conectar-se ao banco de dados, a operação de reencrypt() será executada de forma síncrona. Quando a operação é concluída, a execução prossegue com a próxima linha do código.

var newKey:ByteArray = new ByteArray(); 
// ... generate the new key and store it in newKey 
conn.reencrypt(newKey);

Por outro lado, se a conexão do banco de dados for aberta com o método openAsync() , a operação de reencrypt() será assíncrona. Chamar reencrypt() inicia o processo de recriptografia. Quando a operação é concluída, o objeto SQLConnection despacha um evento reencrypt . Use um ouvinte de evento para determinar quando a recriptografia será concluída:

var newKey:ByteArray = new ByteArray(); 
// ... generate the new key and store it in newKey 
     
conn.addEventListener(SQLEvent.REENCRYPT, reencryptHandler); 
     
conn.reencrypt(newKey); 
     
function reencryptHandler(event:SQLEvent):void 
{ 
    // save the fact that the key changed 
}

A operação reencrypt() será executada na sua própria transação. Se a operação for interrompida ou falhar (por exemplo, se o aplicativo for fechado antes da conclusão da operação), a transação será revertida. Nesse caso, a chave de criptografia ainda será a chave de criptografia do banco de dados.

O método reencrypt() não pode ser usado para remover a criptografia de um banco de dados. Enviar um valor null ou chave de criptografia que não seja um ByteArray de 16 bytes para o método reencrypt() resulta em erro.

Considerações para usar criptografia com um banco de dados

A seção Uso de um banco de dados criptografado apresenta vários casos em que você pode usar um banco de dados criptografado. É óbvio que as situações de uso dos aplicativos diferentes (incluindo essas e outras situações) têm requisitos de privacidade diferentes. A forma que você arquiteta o uso de criptografia no seu aplicativo tem importante papel no controle do grau de privacidade dos dados de um banco de dados. Por exemplo, se você estiver usando um banco de dados criptografado para manter os dados pessoais privados, mesmo de usuários do mesmo computador, o banco de dados de cada usuário deverá ter a própria chave de criptografia. Para obter segurança máxima, seu aplicativo pode gerar a chave de uma senha digitada por um usuário. Basear a chave de criptografia em uma senha garante que os dados se mantenham preservados, mesmo se qualquer outra pessoa entrar na conta do usuário no computador. Por outro lado, suponha que você deseje que um arquivo do banco de dados seja legível para qualquer usuário do seu aplicativo, mas não para os demais aplicativos. Nesse caso, todas as cópias do aplicativo instaladas precisam de acesso a uma chave de criptografia compartilhada.

Você pode projetar seu aplicativo e, em particular, a técnica usada para gerar a chave de criptografia, de acordo com o nível de privacidade que você deseja para os dados do aplicativo. A lista a seguir fornece sugestões de design para vários níveis de privacidade de dados.

  • Para tornar um banco de dados acessível a qualquer usuário que tenha acesso ao aplicativo em qualquer computador, use uma única chave disponível em todas as instâncias do aplicativo. Por exemplo, na primeira vez que um aplicativo for executado, ele poderá baixar a chave de criptografia compartilhada de um servidor usando um protocolo seguro, como SSL. Em seguida, ele poderá salvar a chave no armazenamento local criptografado para uso futuro. Como alternativa, criptografe os dados por usuário no computador e sincronize os dados com um armazenamento de dados remoto, como servidor, para tornar os dados portáteis.

  • Para tornar um banco de dados acessível para um único usuário em qualquer computador, gere a chave de criptografia a partir de um segredo do usuário (como uma senha). Especificamente, não use nenhum valor associado a um computador específico (como um valor armazenado no armazenamento local criptografado) para gerar a chave. Como alternativa, criptografe os dados por usuário no computador e sincronize os dados com um armazenamento de dados remoto, como servidor, para tornar os dados portáteis.

  • Para tornar um banco de dados acessível somente para um único indivíduo em um único computador, gere a chave de uma senha e um salt gerado. Para obter um exemplo dessa técnica, consulte Exemplo: Geração e uso de uma chave de criptografia .

A seguir há algumas considerações adicionais sobre segurança que são importantes lembrar ao projetar um aplicativo para usar um banco de dados criptografado.

  • Um sistema só é seguro como seu link mais fraco. Se você estiver usando uma senha gerada pelo usuário para gerar uma chave de criptografia, considere impor o restrições de complexidade e tamanho mínimo às senhas. Uma senha curta que use somente caracteres básicos pode ser adivinhada rapidamente.

  • O código-fonte de um aplicativo do AIR é armazenado no computador de um usuário em texto simples (para conteúdo HTML) ou em um formato binário facilmente descompilável (para conteúdo SWF). Como o código-fonte é acessível, dois pontos a lembrar são:

    • Nunca codifique uma chave de criptografia no seu código-fonte

    • Sempre suponha que a técnica usada para gerar uma chave de criptografia (como gerador aleatório de caracteres ou um algoritmo de hash específico) possa ser facilmente descoberta por um invasor

  • A criptografia do banco de dados AIR usa o modo AES (padrão de criptografia avançado) com CCM (contador com CBC-MAC). Essa cifra de criptografia requer a combinação de uma chave inserida pelo usuário com um valor salt para ser segura. Para obter um exemplo, consulte Exemplo: Geração e uso de uma chave de criptografia .

  • Quando você opta por criptografar um banco de dados, todos os arquivos de discos usados pelo mecanismo de banco de dados juntamente com esse banco de dados são criptografados. No entanto, o mecanismo do banco de dados retém alguns dados temporariamente em um cache na memória para aprimorar o desempenho de tempos de leitura e gravação em transações. Todos os dados residentes na memória são criptografados. Se um invasor puder acessar a memória usada por um aplicativo do AIR, por exemplo usando um depurador, os dados do banco de dados aberto atualmente e não criptografado ficarão disponíveis.

Exemplo: Geração e uso de uma chave de criptografia

Esse aplicativo de exemplo demonstra uma técnica para gerar uma chave de criptografia. Esse aplicativo foi projetado para fornecer o nível mais alto de privacidade e segurança para dados do usuário. Um aspecto importante da segurança de dados privados é exigir que o usuário digite uma senha sempre que o aplicativo se conectar ao banco de dados. Consequentemente, conforme mostrado no exemplo, um aplicativo que requer este nível de privacidade nunca deverá armazenar diretamente a chave de criptografia do banco de dados.

O aplicativo consiste em duas partes: uma classe ActionScript que gera uma chave de criptografia (a classe EncryptionKeyGenerator) e uma interface do usuário básica que demonstra como usar a classe. Para obter o código-fonte completo, consulte Exemplo completo de código para gerar e usar uma chave de criptografia .

Usar a classe EncryptionKeyGenerator para obter uma cjave de criptografia segura

Não é necessário compreender os detalhes de como a classe EncryptionKeyGenerator funciona para usá-la no seu aplicativo. Se estiver interessado nos detalhes de como a classe gera uma chave de criptografia para um banco de dados, consulte Noções básicas da classe EncryptionKeyGenerator .

Siga essas etapas para usar a classe EncryptionKeyGenerator no seu aplicativo:

  1. Baixe a classe EncryptionKeyGenerator como código-fonte ou um SWC compilado. A classe EncryptionKeyGenerator está incluída no projeto biblioteca principal (as3corelib) do ActionScript 3.0 de código-fonte aberto. Você pode baixar o pacote as3corelib incluindo o código-fonte e a documentação . Você também pode baixar os arquivos SWC ou de código-fonte na página do projeto.

  2. Coloque o código-fonte da classe EncryptionKeyGenerator (ou o SWC as3corelib) em um local onde o código-fonte do seu aplicativo possa encontrá-lo.

  3. No código-fonte do aplicativo, adicione uma instrução import para a classe EncryptionKeyGenerator.

    import com.adobe.air.crypto.EncryptionKeyGenerator;
  4. Antes do ponto onde o código cria o banco de dados ou abre uma conexão para ele, adicione o código para criar uma ocorrência EncryptionKeyGenerator chamando o construtor EncryptionKeyGenerator() .

    var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
  5. Obtenha uma senha do usuário:

    var password:String = passwordInput.text; 
     
    if (!keyGenerator.validateStrongPassword(password)) 
    { 
        // display an error message 
        return; 
    }

    A ocorrência EncryptionKeyGenerator usa essa senha como base da chave de criptografia (mostrado na próxima etapa). A ocorrência EncryptionKeyGenerator testa a senha em relação a alguns requisitos de validação de senha forte. Em caso de falha na validação, ocorre um erro. Como o código de exemplo mostra, você pode verificar a senha antecipadamente chamando o método validateStrongPassword() do objeto EncryptionKeyGenerator. Dessa forma, você pode determinar se a senha atende aos requisitos mínimos de uma senha forte e evita que haja erro.

  6. Gere a chave de criptografia da senha:

    var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);

    O método getEncryptionKey() gera e retorna a chave de criptografia (um ByteArray de 16 bytes). Em seguida, você pode usar a chave de criptografia para criar seu novo banco de dados criptografado ou abrir um já existente.

    O método getEncryptionKey() tem um parâmetro obrigatório, que é a senha obtida na etapa 5.

    Nota: Para manter o maior nível de segurança e privacidade dos dados, o aplicativo deve requerer que o usuário digite uma senha sempre que se conectar ao banco de dados. Não armazene diretamente a senha do usuário ou a chave de criptografia do banco de dados. Isso expõe a riscos de segurança. Em vez disso, conforme demonstrado nesse exemplo, o aplicativo deve usar a mesma técnica para derivar a chade de criptografia da senha, ao criar o banco de dados criptografado e quando se conectar a ele mais tarde.

    O método getEncryptionKey() também aceita um segundo parâmetro (opcional), o parâmetro overrideSaltELSKey . O EncryptionKeyGenerator cria um valor aleatório (conhecido como salt ) que é usado como parte da chave de criptografia. Para ser capaz de criar novamente a chave de criptografia, o valor salt é armazenado no armazenamento local criptografado (ELS) do seu aplicativo do AIR. Por padrão, a classe EncryptionKeyGenerator usa uma String específica como chave ELS. Embora improvável, é possível que a chave possa entrar em conflito com outra chave que o aplicativo usa. Em vez de usar a chave padrão, talvez você deseje especificar sua própria chave ELS. Nesse caso, especifique uma chave personalizada, passando-a como segundo parâmetro getEncryptionKey() , conforme mostrado aqui:

    var customKey:String = "My custom ELS salt key"; 
    var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password, customKey);
  7. Criar ou abrir o banco de dados

    Com a chave de criptografia retornada pelo método getEncryptionKey() , o código pode criar um novo banco de dados criptografado ou tentar abrir o banco de dados criptografado existente. Em ambos os casos, você usa o método open() ou openAsync() da classe SQLConnection, conforme descrito em Criação de um banco de dados criptografado e Conexão com um banco de dados criptografado .

    Nesse exemplo, o aplicativo foi projetado para abrir o banco de dados em modo de execução assíncrona. O código configura os ouvintes do evento apropriados e chama o método openAsync() , enviando a chave de criptografia como argumento final.

    conn.addEventListener(SQLEvent.OPEN, openHandler); 
    conn.addEventListener(SQLErrorEvent.ERROR, openError); 
     
    conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);

    Nos métodos do ouvinte, o código remove os registros do ouvinte do evento. Em seguida, ele exibe uma mensagem de status indicando se o banco de dados foi criado, aberto ou se ocorreu um erro. A parte mais notável desses manipuladores de eventos está no método openError() . Nesse método, a instrução if verifica se o banco de dados existe (o que significa que o código está tentando se conectar a um banco de dados existente) e se a ID do erro corresponde à constante EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID . Se ambas as condições forem verdadeiras, isso significa provavelmente que a senha inserida pelo usuário está incorreta. (Isso também pode significar que o arquivo especificado não é de maneira nenhuma um arquivo de banco de dados.) A seguir temos o código que verifica a ID do erro:

    if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID) 
    { 
        statusMsg.text = "Incorrect password!"; 
    } 
    else 
    { 
        statusMsg.text = "Error creating or opening database."; 
    }

    Para obter o código completo dos ouvintes de eventos de exemplo, consulte Exemplo completo de código para gerar e usar uma chave de criptografia .

Exemplo completo de código para gerar e usar uma chave de criptografia

A seguir há um exemplo completo de um código para a aplicação de “Geração e uso da chave de criptografia”. O código consiste em duas partes.

O exemplo usa a classe EncryptionKeyGenerator para criar uma chave de criptografia a partir de uma senha. A classe EncryptionKeyGenerator está incluída no projeto biblioteca principal (as3corelib) do ActionScript 3.0 de código-fonte aberto. Você pode baixar o pacote as3corelib incluindo o código-fonte e a documentação . Você também pode baixar os arquivos SWC ou de código-fonte na página do projeto.

Exemplo do Flex

O arquivo do aplicativo MXML contém o código-fonte de um aplicativo simples que cria ou abre uma conexão para um banco de dados criptografado:

<?xml version="1.0" encoding="utf-8"?> 
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init();"> 
    <mx:Script> 
        <![CDATA[ 
            import com.adobe.air.crypto.EncryptionKeyGenerator; 
             
            private const dbFileName:String = "encryptedDatabase.db"; 
             
            private var dbFile:File; 
            private var createNewDB:Boolean = true; 
            private var conn:SQLConnection; 
             
            // ------- Event handling ------- 
             
            private function init():void 
            { 
                conn = new SQLConnection(); 
                dbFile = File.applicationStorageDirectory.resolvePath(dbFileName); 
                if (dbFile.exists) 
                { 
                    createNewDB = false; 
                    instructions.text = "Enter your database password to open the encrypted database."; 
                    openButton.label = "Open Database"; 
                } 
            } 
             
            private function openConnection():void 
            { 
                var password:String = passwordInput.text; 
                 
                var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); 
                 
                if (password == null || password.length <= 0) 
                { 
                    statusMsg.text = "Please specify a password."; 
                    return; 
                } 
                 
                if (!keyGenerator.validateStrongPassword(password)) 
                { 
                    statusMsg.text = "The password must be 8-32 characters long. It must contain at least one lowercase letter, at least one uppercase letter, and at least one number or symbol."; 
                    return; 
                } 
                 
                passwordInput.text = ""; 
                passwordInput.enabled = false; 
                openButton.enabled = false; 
                 
                var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password); 
                 
                conn.addEventListener(SQLEvent.OPEN, openHandler); 
                conn.addEventListener(SQLErrorEvent.ERROR, openError); 
                  
                conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey); 
            } 
             
            private function openHandler(event:SQLEvent):void 
            { 
                conn.removeEventListener(SQLEvent.OPEN, openHandler); 
                conn.removeEventListener(SQLErrorEvent.ERROR, openError); 
                  
                statusMsg.setStyle("color", 0x009900); 
                if (createNewDB) 
                { 
                    statusMsg.text = "The encrypted database was created successfully."; 
                } 
                else 
                { 
                    statusMsg.text = "The encrypted database was opened successfully."; 
                } 
            } 
              
            private function openError(event:SQLErrorEvent):void 
            { 
                conn.removeEventListener(SQLEvent.OPEN, openHandler); 
                conn.removeEventListener(SQLErrorEvent.ERROR, openError); 
     
                if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID) 
                { 
                    statusMsg.text = "Incorrect password!"; 
                } 
                else 
                { 
                    statusMsg.text = "Error creating or opening database."; 
                } 
            } 
        ]]> 
    </mx:Script> 
    <mx:Text id="instructions" text="Enter a password to create an encrypted database. The next time you open the application, you will need to re-enter the password to open the database again." width="75%" height="65"/> 
    <mx:HBox> 
        <mx:TextInput id="passwordInput" displayAsPassword="true"/> 
        <mx:Button id="openButton" label="Create Database" click="openConnection();"/> 
    </mx:HBox> 
    <mx:Text id="statusMsg" color="#990000" width="75%"/> 
</mx:WindowedApplication>

Exemplo do Flash Professional

O arquivo FLA do aplicativo contém o código fonte de um aplicativo simples que cria ou abre uma conexão para um banco de dados criptografado. O arquivo FLA tem quatro componentes colocados no palco:

Nome da ocorrência

Tipo do componente

Descrição

instruções

Label

Contém as instruções fornecidas ao usuário

passwordInput

TextInput

Campo de entrada onde o usuário digita a senha

openButton

Button

Botão em que o usuário clica após digitar a senha

statusMsg

Label

Exibe mensagens de status (êxito ou falha)

O código do aplicativo é definido em um quadro-chave no quadro 1 da linha do tempo principal. Este é o código do aplicativo:

import com.adobe.air.crypto.EncryptionKeyGenerator; 
     
const dbFileName:String = "encryptedDatabase.db"; 
     
var dbFile:File; 
var createNewDB:Boolean = true; 
var conn:SQLConnection; 
     
init(); 
     
// ------- Event handling ------- 
     
function init():void 
{ 
    passwordInput.displayAsPassword = true; 
    openButton.addEventListener(MouseEvent.CLICK, openConnection); 
    statusMsg.setStyle("textFormat", new TextFormat(null, null, 0x990000)); 
     
    conn = new SQLConnection(); 
    dbFile = File.applicationStorageDirectory.resolvePath(dbFileName); 
     
    if (dbFile.exists) 
    { 
        createNewDB = false; 
        instructions.text = "Enter your database password to open the encrypted database."; 
        openButton.label = "Open Database"; 
    } 
    else 
    { 
        instructions.text = "Enter a password to create an encrypted database. The next time you open the application, you will need to re-enter the password to open the database again."; 
        openButton.label = "Create Database"; 
    } 
} 
     
function openConnection(event:MouseEvent):void 
{ 
    var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); 
     
    var password:String = passwordInput.text; 
     
    if (password == null || password.length <= 0) 
    { 
        statusMsg.text = "Please specify a password."; 
        return; 
    } 
     
    if (!keyGenerator.validateStrongPassword(password)) 
    { 
        statusMsg.text = "The password must be 8-32 characters long. It must contain at least one lowercase letter, at least one uppercase letter, and at least one number or symbol."; 
        return; 
    } 
     
    passwordInput.text = ""; 
    passwordInput.enabled = false; 
    openButton.enabled = false; 
     
    var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password); 
     
    conn.addEventListener(SQLEvent.OPEN, openHandler); 
    conn.addEventListener(SQLErrorEvent.ERROR, openError); 
     
    conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey); 
} 
     
function openHandler(event:SQLEvent):void 
{ 
    conn.removeEventListener(SQLEvent.OPEN, openHandler); 
    conn.removeEventListener(SQLErrorEvent.ERROR, openError); 
     
    statusMsg.setStyle("textFormat", new TextFormat(null, null, 0x009900)); 
    if (createNewDB) 
    { 
        statusMsg.text = "The encrypted database was created successfully."; 
    } 
    else 
    { 
        statusMsg.text = "The encrypted database was opened successfully."; 
    } 
} 
 
function openError(event:SQLErrorEvent):void 
{ 
    conn.removeEventListener(SQLEvent.OPEN, openHandler); 
    conn.removeEventListener(SQLErrorEvent.ERROR, openError); 
     
    if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID) 
    { 
        statusMsg.text = "Incorrect password!"; 
    } 
    else 
    { 
        statusMsg.text = "Error creating or opening database."; 
    } 
}

Noções básicas da classe EncryptionKeyGenerator

Não é necessário compreender o funcionamento interno da classe EncryptionKeyGenerator para usá-la com o fim de criar uma chave de criptografia segura para o banco de dados do aplicativo. O processo para usar a classe está explicado em Usar a classe EncryptionKeyGenerator para obter uma cjave de criptografia segura . No entanto, pode ser conveniente entender as técnicas que a classe utiliza. Por exemplo, pode ser conveniente adaptar a classe ou incorporar algumas de suas técnicas em situações nas quais um nível diferente de privacidade de dados é desejado.

A classe EncryptionKeyGenerator está incluída no projeto biblioteca principal (as3corelib) do ActionScript 3.0 de código-fonte aberto. Você pode fazer o download do pacote as3corelib, incluindo o código-fonte e a documentação . Também é possível exibir o código-fonte no site do projeto ou baixá-lo para acompanhar juntamente com as explicações.

Quando o código cria uma ocorrência EncryptionKeyGenerator e chama o respectivo método getEncryptionKey() , várias etapas são realizadas para garantir que apenas o usuário com permissão possa acessar os dados. O processo é o mesmo para gerar uma chave de criptografia de uma senha inserida pelo usuário antes de o banco de dados ter sido criado, bem como criar novamente a chave de criptografia para abrir o banco de dados.

Obter e validar uma senha forte

Quando o código chama o método getEncryptionKey() , ele passa uma senha como um parâmetro. A senha é usada como base para a chave de criptografia. Usando informações que somente o usuário conheça, esse design garante que somente o usuário que saiba a senha possa acessar os dados do banco de dados. Mesmo se um invasor acessar a conta do usuário no computador, não poderá entrar no banco de dados sem saber a senha. Para fornecer máxima proteção, o aplicativo nunca armazena a senha.

O código de um aplicativo cria uma instância EncryptionKeyGenerator e chama o método getEncryptionKey() , passando uma senha inserida pelo usuário como argumento (a variável password neste exemplo):

var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator(); 
var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);

A primeira etapa que a classe EncryptionKeyGenerator executa quando o método getEncryptionKey() é chamado, é verificar a senha digitada pelo usuário para garantir que ela atende aos requisitos de segurança da senha. A classe EncryptionKeyGenerator exige que a senha tenha de 8 a 32 caracteres. A senha deve conter letras maiúsculas e minúsculas e pelo menos um número ou caractere simbólico.

A expressão regular que marca esse padrão é definida como uma constante chamada de STRONG_PASSWORD_PATTERN :

private static const STRONG_PASSWORD_PATTERN:RegExp = /(?=^.{8,32}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/;

O código que verifica a senha está no método validateStrongPassword() da classe EncryptionKeyGenerator. O código é o seguinte:

public function vaidateStrongPassword(password:String):Boolean 
{ 
    if (password == null || password.length <= 0) 
    { 
        return false; 
    } 
     
    return STRONG_PASSWORD_PATTERN.test(password)) 
}

Internamente, o método getEncryptionKey() chama o método validateStrongPassword() da classe EncryptionKeyGenerator e, se a senha não é válida, lança uma exceção. O método validateStrongPassword() é um método público para que o código do aplicativo possa verificar a senha sem chamar o método getEncryptionKey() , para evitar um erro.

Expandir a senha para 256 bits

Mais tarde no processo, a senha deverá ter 256 bits. Em vez de exigir que cada usuário insira uma senha com exatamente 256 bits (32 caracteres), o código cria uma senha maior, repetindo os caracteres da senha.

O método getEncryptionKey() chama o método concatenatePassword() para executar a tarefa de criar a senha longa.

var concatenatedPassword:String = concatenatePassword(password);

A seguir, temos o código do método concatenatePassword() :

private function concatenatePassword(pwd:String):String 
{ 
    var len:int = pwd.length; 
    var targetLength:int = 32; 
     
    if (len == targetLength) 
    { 
        return pwd; 
    } 
     
    var repetitions:int = Math.floor(targetLength / len); 
    var excess:int = targetLength % len; 
     
    var result:String = ""; 
     
    for (var i:uint = 0; i < repetitions; i++) 
    { 
        result += pwd; 
    } 
     
    result += pwd.substr(0, excess); 
     
    return result; 
}

Se a senha tem menos de 256 bits, o código concatena a senha com ele mesmo para torná-la 256 bits. Se o comprimento não funcionar exatamente, a última repetição será reduzida para obter exatamente 256 bits.

Gerar ou recuperar um valor de salt de 256 bits

A próxima etapa é obter um valor de salt de 256 bits que, posteriormente, será combinado com a senha. Um salt é um valor aleatório adicionado ou combinado com um valor inserido pelo usuário para formar uma senha. Usar um salt com uma senha garante que mesmo se um usuário escolher uma palavra real ou termo comum como senha, a combinação senha mais salt que o sistema usa será um valor aleatório. Isso, de maneira não aleatória, ajuda a proteger contra um ataque ao dicionário, em que um invasor usa uma lista de palavras para tentar adivinhar uma senha. Além disso, gerando o valor salt e armazenando-o no armazenamento local criptografado, ele é associado à conta do usuário no computador em que o arquivo do banco de dados está localizado.

Se o aplicativo estiver chamando o método getEncryptionKey() pela primeira vez, o código criará um valor salt aleatório de 256 bits. Caso contrário, o código carregará o valor de salt do armazenamento local criptografado.

O salt é armazenado em uma variável chamada de salt . O código determina se já foi criado um salt, ao tentar carregar o salt do armazenamento local criptografado:

var salt:ByteArray = EncryptedLocalStore.getItem(saltKey); 
if (salt == null) 
{ 
    salt = makeSalt(); 
    EncryptedLocalStore.setItem(saltKey, salt); 
}

Se o código estiver criando um novo valor salt, o método makeSalt() gera um valor aleatório de 256 bits. Como o valor é eventualmente armazenado no armazenamento local criptografado, ele é gerado como um objeto ByteArray. O método makeSalt() usa o método Math.random() para gerar aleatoriamente o valor. O método Math.random() não pode gerar 256 bits de uma vez. Em vez disso, o código usa um loop para chamar o Math.random() oito vezes. A cada vez, um valor uint aleatório entre 0 e 4294967295 (o valor uint máximo) é gerado. Um valor uint é usado por conveniência, pois um uint usa exatamente 32 bits. Quando oito valores uint são gravados no ByteArray, é gerado um valor de 256 bits. Este é o código para o método makeSalt() :

private function makeSalt():ByteArray 
{ 
    var result:ByteArray = new ByteArray; 
     
    for (var i:uint = 0; i < 8; i++) 
    { 
        result.writeUnsignedInt(Math.round(Math.random() * uint.MAX_VALUE)); 
    } 
     
    return result; 
}

Quando o código está salvando o salt no armazenamento local criptografado (ELS) ou recuperando o salt do ELS, ele precisa da chave String com a qual o salt é salvo. Sem saber a chave, o valor salt não pode ser recuperado. Nesse caso, a chave de criptografia não pode ser recriada todas as vezes para abrir novamente o banco de dados. Por padrão, o EncryptionKeyGenerator usa uma chave ELS predefinida que é definida na constante SALT_ELS_KEY . Em vez de usar a chave padrão, o código do aplicativo também pode especificar uma chave ELS para usar na chamada do método getEncryptionKey() . A Chave ELS padrão ou a chave ELS salt especificada pelo aplicativo é armazenada em uma variável chamada de saltKey . Essa variável é usada nas chamadas a EncryptedLocalStore.setItem() e EncryptedLocalStore.getItem() , como mostrado antes.

Combinar a senha de 256 bits e o salt usando o operador XOR

O código agora tem uma senha de 256 bits e um valor salt de 256 bits. Em seguida, ele usa uma operação XOR em nível de bits para combinar o salt e a senha concatenada em um único valor. Efetivamente, essa técnica cria um senha de 256 bits que consiste em caracteres do intervalo inteiro de caracteres possíveis. Esse princípio é verdadeiro, embora muito provavelmente a entrada de senha real consista principalmente em caracteres alfanuméricos. Essa não aleatoriedade elevada oferece o benefício de tornar o conjunto de senhas possíveis maior, sem exigir que o usuário digite uma senha longa e complexa.

O resultado da operação XOR é armazenado na variável unhashedKey . O processo real de execução de um XOR em nível de bits nos dois valores ocorre no método xorBytes() :

var unhashedKey:ByteArray = xorBytes(concatenatedPassword, salt);

O operador XOR em nível de bits ( ^ ) retira dois valores uint e retorna um valor uint. (Um valor uint contém 32 bits.) Os valores de entrada enviados como argumentos para o método xorBytes() são uma String (a senha) e um ByteArray (o salt). Consequentemente, o código usa um loop para extrair 32 bits de uma vez de cada entrada para combinar usando o operador XOR.

private function xorBytes(passwordString:String, salt:ByteArray):ByteArray 
{ 
    var result:ByteArray = new ByteArray(); 
     
    for (var i:uint = 0; i < 32; i += 4) 
    { 
        // ... 
    } 
     
    return result; 
}

No loop, os primeiros 32 bits (4 bytes) são extraídos do parâmetro passwordString . Esses bits são extraídos e convertidos em um uint ( o1 ) em um processo de duas partes. Primeiro, o método charCodeAt() obtém cada valor numérico do caractere. Depois, esse valor é desviado para a posição apropriada na unidade usando o operador de desvio à esquerda em nível de bits ( << ) e o valor desviado é adicionado ao o1 . Por exemplo, o primeiro caractere ( i ) se torna os primeiros 8 bits usando o operador de desvio à esquerda em nível de bits ( << ) para desviar os bits deixados por 24 bits e atribuindo esse valor ao o1 . O segundo caractere (i + 1 ) se torna os segundos 8 bits desviando seu valor 16 bits à esquerda e adicionando o resultado a o1 . Os valores do terceiro e do quarto caracteres são adicionados da mesma forma.

        // ... 
         
        // Extract 4 bytes from the password string and convert to a uint 
        var o1:uint = passwordString.charCodeAt(i) << 24; 
        o1 += passwordString.charCodeAt(i + 1) << 16; 
        o1 += passwordString.charCodeAt(i + 2) << 8; 
        o1 += passwordString.charCodeAt(i + 3); 
         
        // ...

A variável o1 agora contém 32 bits do parâmetro passwordString . Em seguida, 32 bits são extraídos do parâmetro salt , chamando seu método readUnsignedInt() . Os 32 bits são armazenados em uma variável uint o2 .

        // ... 
         
        salt.position = i; 
        var o2:uint = salt.readUnsignedInt(); 
         
        // ...

Finalmente, os dois valores de 32 bits (uint) são combinados usando o operador XOR, e o resultado é gravado em um ByteArray chamado de result .

        // ... 
         
        var xor:uint = o1 ^ o2; 
        result.writeUnsignedInt(xor); 
        // ...

Quando o loop é concluído, o ByteArray contendo o resultado XOR é retornado.

        // ... 
    } 
     
    return result; 
}

Hash da chave

Após a senha concatenada e o salt serem combinados, a próxima etapa é oferecer proteção adicional a esse valor, fazendo hash dele por meio do algoritmo de hash SHA-256. O processo de hash dificulta para um invasor realizar a engenharia reversa no valor.

Nesse ponto, o código tem um ByteArray chamado de unhashedKey , que contém a senha concatenada combinada com o salt. O projeto de biblioteca central (as3corelib) do ActionScript 3.0 inclui uma classe SHA256 no pacote com.adobe.crypto. O método SHA256.hashBytes() , que realiza um hash de SHA-256 em um ByteArray e retorna uma String contendo o resultado do hash de 256 bits como um número decimal. A classe EncryptionKeyGenerator usa a classe SHA256 para aplicar hash à chave:

var hashedKey:String = SHA256.hashBytes(unhashedKey);

Extrair a chave de criptografia do hash

A chave de criptografia deve ser um ByteArray com exatamente 16 bytes (128 bits). O resultado do algoritmo de hash SHA-256 tem sempre 256 bits. Consequentemente, a etapa final é selecionar 128 bits do resultado com hash para usar como chave de criptografia real.

Na classe EncryptionKeyGenerator, o código reduz a chave para 128 bits chamando o método generateEncryptionKey() . Em seguida, ele retorna esse resultado do método como o resultado do método getEncryptionKey() :

var encryptionKey:ByteArray = generateEncryptionKey(hashedKey); 
return encryptionKey;

É necessário usar os primeiros 128 bits como a chave de criptografia. Você pode selecionar um intervalo de bits iniciando em algum ponto arbitrário, pode selecionar todos os outros bits ou usar alguma outra maneira de selecionar bits. O importante é que o código selecione 128 bits diferentes e que os mesmos 128 bits sejam usados a cada vez.

Nesse caso, o método generateEncryptionKey() usa o intervalo de bits que começa no 18º byte como a chave de criptografia. Como mencionado antes, a classe SHA256 retorna uma String contendo um hash de 256 bits como número hexadecimal. Um único bloco de 128 bits não tem muitos bytes a acrescentar a um ByteArray de uma vez. Consequentemente, o código usa um loop for para extrair caracteres da String hexadecimal, convertendo-os em valores numéricos reais e adicionando-os ao ByteArray. A String de resultado SHA-256 tem 64 caracteres. Um intervalo de 128 bits equivale a 32 caracteres na String, e cada caractere representa 4 bits. O menor incremento de dados que você pode adicionar a um ByteArray é um byte (8 bits), o que é equivalente a dois caracteres na String hash . Consequentemente, o loop conta de 0 a 31 (32 caracteres) em incrementos de 2 caracteres.

Dentro do loop, o código determina primeiro a posição inicial do par de caracteres atual. Como o intervalo desejado começa na posição de indexação 17 (o 18º byte) do caractere, a variável position é atribuída ao valor iterador da corrente ( i ) mais 17. O código usa o método substr() do objeto da String para extrair os dois caracteres na posição atual. Esses caracteres são armazenados na variável hex . Em seguida, o código usa o método parseInt() para converter a String hex a um valor inteiro decimal. Ele armazena esse valor na variável byte . Finalmente, o código adiciona o valor de byte ao ByteArray result usando o método writeByte() . Quando o loop é concluído, o ByteArray result contém 16 bytes e está pronto para ser usado como chave de criptografia do banco de dados.

private function generateEncryptionKey(hash:String):ByteArray 
{ 
    var result:ByteArray = new ByteArray(); 
     
    for (var i:uint = 0; i < 32; i += 2) 
    { 
        var position:uint = i + 17; 
        var hex:String = hash.substr(position, 2); 
        var byte:int = parseInt(hex, 16); 
        result.writeByte(byte); 
    } 
     
    return result; 
}