Encrypting sensitive data no banco de dados

1. Por que criptografar dados sensíveis?

A criptografia de dados sensíveis no banco de dados deixou de ser uma opção e se tornou uma exigência para aplicações modernas. Legislações como a LGPD (Lei Geral de Proteção de Dados) no Brasil, o GDPR na Europa e o PCI-DSS para dados de cartão de crédito impõem penalidades severas para vazamentos de informações pessoais.

Quando um invasor obtém acesso ao banco de dados, dados criptografados se tornam inúteis sem a chave correspondente. Isso reduz drasticamente o impacto de um vazamento. É crucial entender a diferença entre hash e criptografia: hash é unidirecional e ideal para senhas (não podem ser recuperadas), enquanto criptografia é bidirecional e permite recuperar o dado original — essencial para informações como CPF, cartões de crédito ou endereços que precisam ser exibidos novamente ao usuário.

2. Estratégias de criptografia: aplicação vs banco de dados

Existem duas abordagens principais:

Criptografia no lado da aplicação oferece controle total sobre o processo. A chave fica sob gestão do desenvolvedor, geralmente em variáveis de ambiente ou serviços como AWS KMS. A aplicação criptografa antes de enviar ao banco e descriptografa ao ler.

Criptografia nativa do banco utiliza funções como AES_ENCRYPT no MySQL ou pgcrypto no PostgreSQL. Embora pareça conveniente, essa abordagem expõe a chave nos logs do banco e dificulta a migração entre fornecedores.

Para a maioria dos casos, recomendo a criptografia no lado da aplicação: maior portabilidade, controle granular e segurança contra ataques ao próprio banco de dados.

3. Implementando criptografia com PHP e OpenSSL

O PHP oferece as funções openssl_encrypt e openssl_decrypt para criptografia simétrica. O algoritmo recomendado atualmente é AES-256-GCM, que fornece autenticação integrada (evita adulteração dos dados).

<?php

function encryptData(string $data, string $key): string
{
    $iv = openssl_random_pseudo_bytes(12); // 12 bytes para GCM
    $tag = '';

    $ciphertext = openssl_encrypt(
        $data,
        'aes-256-gcm',
        $key,
        OPENSSL_RAW_DATA,
        $iv,
        $tag,
        '',
        16
    );

    // Armazenamos IV + tag + ciphertext em base64
    return base64_encode($iv . $tag . $ciphertext);
}

function decryptData(string $encryptedData, string $key): string
{
    $decoded = base64_decode($encryptedData);

    $iv = substr($decoded, 0, 12);
    $tag = substr($decoded, 12, 16);
    $ciphertext = substr($decoded, 28);

    $plaintext = openssl_decrypt(
        $ciphertext,
        'aes-256-gcm',
        $key,
        OPENSSL_RAW_DATA,
        $iv,
        $tag
    );

    if ($plaintext === false) {
        throw new \RuntimeException('Falha na descriptografia ou dados adulterados');
    }

    return $plaintext;
}

// Exemplo de uso
$key = hex2bin('sua_chave_hexadecimal_de_32_bytes_aqui');
$encrypted = encryptData('123.456.789-00', $key);
echo $encrypted; // string base64 segura para banco

$decrypted = decryptData($encrypted, $key);
echo $decrypted; // 123.456.789-00
?>

A chave deve ter exatamente 32 bytes para AES-256. Armazene-a em variáveis de ambiente, nunca no código fonte.

4. Criptografia de atributos no Laravel (Eloquent)

No Laravel, podemos criar um cast personalizado para criptografar/descriptografar automaticamente ao acessar atributos:

<?php

namespace App\Casts;

use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class SensitiveDataCast implements CastsAttributes
{
    private string $key;

    public function __construct()
    {
        $this->key = hex2bin(config('app.encryption_key'));
    }

    public function get($model, string $key, $value, array $attributes): ?string
    {
        if ($value === null) {
            return null;
        }

        return decryptData($value, $this->key);
    }

    public function set($model, string $key, $value, array $attributes): ?string
    {
        if ($value === null) {
            return null;
        }

        return encryptData($value, $this->key);
    }
}

No modelo:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Casts\SensitiveDataCast;

class User extends Model
{
    protected $casts = [
        'cpf' => SensitiveDataCast::class,
        'credit_card' => SensitiveDataCast::class,
    ];
}

Agora, ao salvar $user->cpf = '123.456.789-00', o Laravel automaticamente criptografa. Ao ler, descriptografa. Importante: campos criptografados não podem ser usados em buscas WHERE tradicionais. Para buscas, considere índices determinísticos ou hashes separados.

5. Criptografia de dados em massa e migrações seguras

Ao criptografar dados existentes, evite downtime com uma migração gradual:

<?php

use App\Models\User;
use Illuminate\Support\Facades\DB;

// Passo 1: Adicionar coluna temporária
// ALTER TABLE users ADD COLUMN cpf_encrypted TEXT NULL;

// Passo 2: Migrar dados em lotes
User::whereNull('cpf_encrypted')
    ->chunk(100, function ($users) {
        foreach ($users as $user) {
            DB::table('users')
                ->where('id', $user->id)
                ->update([
                    'cpf_encrypted' => encryptData($user->cpf, $key)
                ]);
        }
    });

// Passo 3: Verificar integridade
$failed = User::whereNull('cpf_encrypted')->count();
if ($failed === 0) {
    // Passo 4: Remover coluna original
    // ALTER TABLE users DROP COLUMN cpf;
    // ALTER TABLE users RENAME COLUMN cpf_encrypted TO cpf;
}

Sempre valide a integridade antes de remover dados originais.

6. Boas práticas de chave e rotação

A segurança da criptografia depende exclusivamente da chave. Siga estas práticas:

Envelope encryption: use uma chave mestra (armazenada em cofre) para criptografar chaves derivadas por registro:

<?php

function generateRecordKey(string $masterKey, string $recordId): string
{
    // Deriva uma chave única por registro usando HKDF
    return hash_hkdf('sha256', $masterKey, 32, 'record-key-' . $recordId);
}

Rotação periódica: implemente versionamento de chaves. Ao descriptografar, identifique qual chave foi usada e re-criptografe com a nova:

<?php

function rotateKey(string $encryptedData, string $oldKey, string $newKey): string
{
    $plaintext = decryptData($encryptedData, $oldKey);
    return encryptData($plaintext, $newKey);
}

Armazene chaves em serviços gerenciados como AWS KMS, HashiCorp Vault ou ao menos em .env com permissões restritas (600).

7. Monitoramento e logging seguro

Nunca registre dados descriptografados em logs. Em vez disso, registre metadados:

<?php

try {
    $cpf = decryptData($encryptedCpf, $key);
} catch (\RuntimeException $e) {
    Log::warning('Falha na descriptografia', [
        'user_id' => $userId,
        'ip' => request()->ip(),
        'timestamp' => now()
    ]);
    // Não logar o dado criptografado ou a chave
}

Integre com ferramentas de auditoria para detectar acessos indevidos. Monitore tentativas de descriptografia com falha — podem indicar ataques de força bruta ou adulteração de dados.


Referências