04b — Criptografia e Gestão de Chaves
Visão Geral
Esta seção detalha os controles criptográficos e de gestão de chaves para o stack Cataloga-ai, alinhados com LGPD Art. 46 (segurança técnica) e boas práticas de segurança da informação.
Stack em escopo:
- Supabase self-hosted (PostgreSQL + PostgREST)
- Vercel (Edge Functions + Secrets)
- Redis (cache)
- GPTMaker (IA externa)
- WhatsApp Cloud API (Meta)
1. Criptografia em Trânsito
1.1 TLS 1.3 — Configuração Supabase Self-Hosted
Objetivo: Garantir que todos os dados trafegando entre cliente-servidor e servidor-servidor usem TLS 1.3 com forward secrecy.
Nginx (se usado como reverse proxy no Supabase)
Arquivo: /etc/nginx/sites-available/supabase
server {
listen 443 ssl http2;
server_name api.cataloga-ai.com.br;
# TLS 1.3 apenas
ssl_protocols TLSv1.3;
# Cipher suites recomendadas (TLS 1.3)
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256';
ssl_prefer_server_ciphers off;
# Certificado (Let's Encrypt recomendado)
ssl_certificate /etc/letsencrypt/live/api.cataloga-ai.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.cataloga-ai.com.br/privkey.pem;
# HSTS (força HTTPS por 1 ano)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/api.cataloga-ai.com.br/chain.pem;
# Session tickets desabilitado (melhora forward secrecy)
ssl_session_tickets off;
location / {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Redirect HTTP → HTTPS
server {
listen 80;
server_name api.cataloga-ai.com.br;
return 301 https://$server_name$request_uri;
}
Validação:
# Testar configuração TLS
openssl s_client -connect api.cataloga-ai.com.br:443 -tls1_3
# Validar com SSL Labs
# https://www.ssllabs.com/ssltest/analyze.html?d=api.cataloga-ai.com.br
Caddy (alternativa mais simples)
Arquivo: /etc/caddy/Caddyfile
api.cataloga-ai.com.br {
tls {
protocols tls1.3
ciphers TLS_AES_128_GCM_SHA256 TLS_AES_256_GCM_SHA384 TLS_CHACHA20_POLY1305_SHA256
}
reverse_proxy localhost:8000
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
}
}
1.2 PostgreSQL — Conexões TLS Obrigatórias
Arquivo: /etc/postgresql/15/main/postgresql.conf
# Força TLS em todas as conexões
ssl = on
ssl_cert_file = '/etc/ssl/certs/postgresql.crt'
ssl_key_file = '/etc/ssl/private/postgresql.key'
ssl_ca_file = '/etc/ssl/certs/ca-bundle.crt'
# TLS 1.3 apenas
ssl_min_protocol_version = 'TLSv1.3'
# Cipher suites
ssl_ciphers = 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384'
Arquivo: /etc/postgresql/15/main/pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
# Força SSL/TLS em conexões remotas
hostssl all all 0.0.0.0/0 scram-sha-256
hostssl all all ::/0 scram-sha-256
# Bloqueia conexões não-TLS
hostnossl all all 0.0.0.0/0 reject
hostnossl all all ::/0 reject
Validação:
# Testar conexão TLS
psql "postgresql://user:pass@api.cataloga-ai.com.br:5432/cataloga?sslmode=require" \
-c "SELECT version();"
# Verificar TLS ativo
psql -c "SELECT datname, usename, ssl, client_addr FROM pg_stat_ssl JOIN pg_stat_activity ON pg_stat_ssl.pid = pg_stat_activity.pid;"
1.3 Redis — TLS para Conexões
Arquivo: /etc/redis/redis.conf
# TLS habilitado
tls-port 6380
port 0 # Desabilita porta não-TLS
# Certificados
tls-cert-file /etc/redis/certs/redis.crt
tls-key-file /etc/redis/certs/redis.key
tls-ca-cert-file /etc/redis/certs/ca.crt
# TLS 1.3 apenas
tls-protocols "TLSv1.3"
# Cipher suites
tls-ciphers "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384"
# Cliente deve apresentar certificado (mutual TLS opcional mas recomendado)
tls-auth-clients yes
Cliente Node.js (Vercel Edge Functions):
import Redis from 'ioredis';
const redis = new Redis({
host: 'redis.cataloga-ai.internal',
port: 6380,
tls: {
ca: process.env.REDIS_CA_CERT,
cert: process.env.REDIS_CLIENT_CERT,
key: process.env.REDIS_CLIENT_KEY,
minVersion: 'TLSv1.3',
},
});
1.4 Vercel Edge Functions — HTTPS Nativo
Vercel força HTTPS por padrão. Checklist:
- [ ] Custom domain configurado com DNS CAA record:
cataloga-ai.com.br. CAA 0 issue "letsencrypt.org"
cataloga-ai.com.br. CAA 0 issuewild "letsencrypt.org"
- [ ] HSTS header configurado em
vercel.json:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains; preload"
}
]
}
]
}
1.5 Certificate Pinning
Quando aplicar: Apps mobile nativos (iOS/Android) que se conectam à API.
Não aplicável para: Web apps (navegadores não suportam pinning custom de forma confiável).
Exemplo iOS (Swift):
import Alamofire
let evaluators: [String: ServerTrustEvaluating] = [
"api.cataloga-ai.com.br": PublicKeysTrustEvaluator(
keys: [
SecKey(base64Encoded: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...")!
],
performDefaultValidation: true,
validateHost: true
)
]
let session = Session(serverTrustManager: ServerTrustManager(evaluators: evaluators))
Manutenção:
- Renovação de certificado exige atualização do app (breaking change).
- Recomendação: Usar pinning apenas se risco de MitM for crítico; caso contrário, confiar na CA raiz do SO.
1.6 GPTMaker e WhatsApp Cloud API
GPTMaker: API externa — sem controle sobre TLS do lado deles. Validar certificado no cliente:
import https from 'https';
const agent = new https.Agent({
rejectUnauthorized: true, // Valida certificado
minVersion: 'TLSv1.3',
});
fetch('https://api.gptmaker.ai/v1/chat', {
method: 'POST',
agent,
body: JSON.stringify({ prompt: '...' }),
});
WhatsApp Cloud API (Meta): TLS 1.2+ garantido pela Meta. Checklist:
- [ ] Webhook endpoint (callback) usa HTTPS com certificado válido
- [ ] Validar assinatura
X-Hub-Signature-256em todos os webhooks recebidos
import crypto from 'crypto';
function validateWebhookSignature(req) {
const signature = req.headers['x-hub-signature-256'];
const expectedSignature = crypto
.createHmac('sha256', process.env.WHATSAPP_APP_SECRET)
.update(req.rawBody)
.digest('hex');
return signature === `sha256=${expectedSignature}`;
}
2. Criptografia em Repouso
2.1 PostgreSQL — Escolha de Estratégia
Opção A: Transparent Data Encryption (TDE) — Filesystem-Level
Recomendado para: Proteção contra acesso físico ao disco (ex: servidor comprometido, backup de disco roubado).
Ferramenta: LUKS (Linux Unified Key Setup)
Setup:
# 1. Criar partição criptografada (antes de instalar PostgreSQL)
cryptsetup luksFormat /dev/sdb1
cryptsetup luksOpen /dev/sdb1 postgres_data
# 2. Formatar e montar
mkfs.ext4 /dev/mapper/postgres_data
mount /dev/mapper/postgres_data /var/lib/postgresql
# 3. Configurar unlocking automático (keyfile em TPM ou HSM recomendado)
echo "postgres_data /dev/sdb1 /root/postgres.key luks" >> /etc/crypttab
echo "/dev/mapper/postgres_data /var/lib/postgresql ext4 defaults 0 2" >> /etc/fstab
Prós:
- Transparente para PostgreSQL
- Protege todos os arquivos (WAL, backups, logs)
- Baixo overhead de performance (~5%)
Contras:
- Chave em memória RAM quando volume está montado (vulnerável a cold boot attacks)
- Não protege contra acesso lógico (SQL injection com credenciais válidas)
Opção B: pgcrypto — Column-Level Encryption
Recomendado para: Proteção granular de PII específico (CPF, e-mail, telefone).
Setup:
-- 1. Habilitar extensão
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- 2. Criar tabela com coluna criptografada
CREATE TABLE lojistas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nome TEXT NOT NULL,
email_encrypted BYTEA NOT NULL, -- Email criptografado
cpf_encrypted BYTEA NOT NULL, -- CPF criptografado
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 3. Insert com criptografia (AES-256-GCM via chave em env)
INSERT INTO lojistas (nome, email_encrypted, cpf_encrypted)
VALUES (
'João Silva',
pgp_sym_encrypt('joao@loja.com', current_setting('app.encryption_key')),
pgp_sym_encrypt('12345678900', current_setting('app.encryption_key'))
);
-- 4. Select com decriptografia
SELECT
id,
nome,
pgp_sym_decrypt(email_encrypted::bytea, current_setting('app.encryption_key')) AS email,
pgp_sym_decrypt(cpf_encrypted::bytea, current_setting('app.encryption_key')) AS cpf
FROM lojistas;
Gestão de chave:
// Vercel Edge Function
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{
db: {
schema: 'public',
},
global: {
headers: {
// Injeta chave de criptografia via SET LOCAL
'x-encryption-key': process.env.DB_ENCRYPTION_KEY,
},
},
}
);
// Trigger PostgreSQL para setar chave por sessão
// CREATE OR REPLACE FUNCTION set_encryption_key()
// RETURNS void AS $$
// BEGIN
// PERFORM set_config('app.encryption_key', current_setting('request.headers')::json->>'x-encryption-key', true);
// END;
// $$ LANGUAGE plpgsql;
Prós:
- Granularidade por coluna
- Chave pode ser rotacionada por registro
- Protege contra SQL injection (atacante vê apenas ciphertext)
Contras:
- Overhead de performance (20-30% em queries que decriptam)
- Não permite índices ou
WHEREsobre dados criptografados (precisa usar hash separado)
Decisão Recomendada: TDE (LUKS) + pgcrypto para PII crítico
Raciocínio:
- TDE protege disco inteiro (compliance baseline)
- pgcrypto adiciona defense-in-depth para CPF, e-mail, telefone
- Não criptografar campos que precisam de busca rápida (ex: nome de loja — usar apenas TDE)
2.2 Redis — Encryption at Rest
Redis não possui encryption-at-rest nativo. Opções:
Opção A: LUKS no Volume Redis (recomendado)
Mesmo processo do PostgreSQL:
cryptsetup luksFormat /dev/sdc1
cryptsetup luksOpen /dev/sdc1 redis_data
mkfs.ext4 /dev/mapper/redis_data
mount /dev/mapper/redis_data /var/lib/redis
Opção B: Application-Level Encryption
Criptografar valores antes de gravar no Redis:
import crypto from 'crypto';
function encrypt(text, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(key, 'hex'), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return iv.toString('hex') + ':' + authTag + ':' + encrypted;
}
function decrypt(encryptedData, key) {
const [ivHex, authTagHex, encryptedHex] = encryptedData.split(':');
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
Buffer.from(key, 'hex'),
Buffer.from(ivHex, 'hex')
);
decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Uso
await redis.set('session:123', encrypt(JSON.stringify(sessionData), REDIS_ENCRYPTION_KEY));
const data = JSON.parse(decrypt(await redis.get('session:123'), REDIS_ENCRYPTION_KEY));
Decisão: LUKS (Opção A) é mais simples e performático.
2.3 Backups — Criptografia Obrigatória
PostgreSQL — Backup Criptografado com GPG
#!/bin/bash
# /usr/local/bin/backup-postgres-encrypted.sh
BACKUP_DIR="/var/backups/postgres"
DB_NAME="cataloga"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql.gz.gpg"
GPG_RECIPIENT="backup@cataloga-ai.com.br"
# 1. Dump + gzip + GPG encryption
pg_dump -U postgres "$DB_NAME" | \
gzip | \
gpg --encrypt --recipient "$GPG_RECIPIENT" --armor > "$BACKUP_FILE"
# 2. Validar backup
if [ $? -eq 0 ]; then
echo "Backup OK: $BACKUP_FILE"
# Upload para S3/Wasabi com encryption server-side
aws s3 cp "$BACKUP_FILE" \
"s3://cataloga-backups/postgres/" \
--server-side-encryption AES256
else
echo "ERRO no backup!" | mail -s "Backup failed" admin@cataloga-ai.com.br
fi
# 3. Retention: manter 30 dias local, 1 ano remoto
find "$BACKUP_DIR" -name "*.gpg" -mtime +30 -delete
Crontab:
0 2 * * * /usr/local/bin/backup-postgres-encrypted.sh >> /var/log/backup-postgres.log 2>&1
Restore:
# Download + decrypt + decompress + restore
aws s3 cp s3://cataloga-backups/postgres/cataloga_20260425_020000.sql.gz.gpg - | \
gpg --decrypt | \
gunzip | \
psql -U postgres cataloga
Redis — Backup RDB Criptografado
#!/bin/bash
# /usr/local/bin/backup-redis-encrypted.sh
REDIS_DATA="/var/lib/redis/dump.rdb"
BACKUP_DIR="/var/backups/redis"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/dump_${TIMESTAMP}.rdb.gpg"
GPG_RECIPIENT="backup@cataloga-ai.com.br"
# 1. Forçar save
redis-cli BGSAVE
sleep 5 # Aguardar conclusão
# 2. Criptografar RDB
gpg --encrypt --recipient "$GPG_RECIPIENT" --armor \
< "$REDIS_DATA" > "$BACKUP_FILE"
# 3. Upload
aws s3 cp "$BACKUP_FILE" \
"s3://cataloga-backups/redis/" \
--server-side-encryption AES256
# 4. Retention
find "$BACKUP_DIR" -name "*.gpg" -mtime +7 -delete
3. Gestão de Secrets e Chaves
3.1 Escolha de Vault
Comparação
| Solução | Custo/mês | Complexidade | Auditoria | Rotação Automática | Recomendação |
|---|---|---|---|---|---|
| Vercel Env | $0 | Baixa | Básica | Manual | MVP / early-stage |
| Doppler | $0-$39 | Baixa | Completa | Automática | Recomendado (melhor custo-benefício) |
| HashiCorp Vault | Self-hosted | Alta | Completa | Automática | Enterprise com compliance rigoroso |
Decisão: Doppler (justificativa)
Vantagens:
- Free tier generoso (até 5 usuários)
- Auditoria completa (quem acessou qual secret quando)
- Rotação automática para AWS, Supabase, Stripe
- Sync nativo com Vercel
- Secrets versionados (rollback fácil)
Setup Doppler:
# 1. Instalar CLI
curl -Ls https://cli.doppler.com/install.sh | sh
# 2. Login
doppler login
# 3. Setup projeto
doppler setup --project cataloga-ai --config production
# 4. Adicionar secrets
doppler secrets set DB_ENCRYPTION_KEY=$(openssl rand -hex 32)
doppler secrets set SUPABASE_SERVICE_ROLE_KEY=xxx
doppler secrets set WHATSAPP_APP_SECRET=xxx
doppler secrets set REDIS_PASSWORD=$(openssl rand -base64 32)
# 5. Integrar com Vercel
doppler integrations add vercel
Vercel Function usando Doppler:
// Edge Function automaticamente recebe env vars do Doppler
export default async function handler(req) {
// process.env.DB_ENCRYPTION_KEY já disponível
const key = process.env.DB_ENCRYPTION_KEY;
// ...
}
3.2 Hierarquia de Chaves (DEK/KEK)
Conceito:
- KEK (Key Encryption Key): Master key que criptografa outras chaves. Armazenada em HSM ou KMS.
- DEK (Data Encryption Key): Chave que criptografa dados. Gerada por demanda, criptografada pela KEK.
Implementação no Cataloga-ai:
// kms.js — Wrapper AWS KMS ou GCP KMS
import { KMSClient, EncryptCommand, DecryptCommand } from '@aws-sdk/client-kms';
const kms = new KMSClient({ region: 'us-east-1' });
const KEK_ARN = 'arn:aws:kms:us-east-1:123456789012:key/abcd-1234';
// Gerar DEK criptografada
export async function generateDEK() {
const dek = crypto.randomBytes(32); // 256 bits
const encryptedDEK = await kms.send(new EncryptCommand({
KeyId: KEK_ARN,
Plaintext: dek,
}));
return {
plaintext: dek,
ciphertext: encryptedDEK.CiphertextBlob,
};
}
// Descriptografar DEK
export async function unwrapDEK(encryptedDEK) {
const result = await kms.send(new DecryptCommand({
KeyId: KEK_ARN,
CiphertextBlob: encryptedDEK,
}));
return result.Plaintext;
}
Uso no PostgreSQL:
-- Tabela de DEKs por tenant (se multi-tenant)
CREATE TABLE encryption_keys (
tenant_id UUID PRIMARY KEY,
dek_encrypted BYTEA NOT NULL, -- DEK criptografada pela KEK
created_at TIMESTAMPTZ DEFAULT NOW(),
rotated_at TIMESTAMPTZ
);
-- Aplicação busca DEK, descriptografa com KEK, usa para criptografar dados do tenant
3.3 Rotação de Chaves
3.3.1 Frequência Recomendada
| Tipo de Chave | Rotação | Justificativa |
|---|---|---|
| KEK (KMS master key) | Anual | Raramente exposta |
| DEK (database encryption key) | Trimestral | Reduz janela de comprometimento |
| API keys (Vercel, Supabase) | Semestral | Auditoria de acessos |
| Passwords (humanos) | 90 dias | LGPD Art. 46 § 1º |
| TLS certificates | Automático | Let's Encrypt renova a cada 60 dias |
3.3.2 Rotação Zero-Downtime — PostgreSQL DEK
Estratégia: Dual-encryption durante transição.
-- 1. Adicionar coluna para nova DEK
ALTER TABLE lojistas ADD COLUMN email_encrypted_v2 BYTEA;
-- 2. Background job: re-encriptar com nova chave
DO $$
DECLARE
r RECORD;
old_key TEXT := current_setting('app.encryption_key_v1');
new_key TEXT := current_setting('app.encryption_key_v2');
BEGIN
FOR r IN SELECT id, email_encrypted FROM lojistas WHERE email_encrypted_v2 IS NULL LOOP
UPDATE lojistas
SET email_encrypted_v2 = pgp_sym_encrypt(
pgp_sym_decrypt(r.email_encrypted::bytea, old_key),
new_key
)
WHERE id = r.id;
-- Rate limit para não travar DB
PERFORM pg_sleep(0.01);
END LOOP;
END $$;
-- 3. Após conclusão: swap colunas
ALTER TABLE lojistas DROP COLUMN email_encrypted;
ALTER TABLE lojistas RENAME COLUMN email_encrypted_v2 TO email_encrypted;
-- 4. Atualizar aplicação para usar nova chave
-- Deploy com feature flag ou blue-green
Automação via Doppler:
# Agendar rotação trimestral
doppler secrets set DB_ENCRYPTION_KEY_V2=$(openssl rand -hex 32)
# Trigger migration job
curl -X POST https://admin.cataloga-ai.com.br/api/internal/rotate-dek \
-H "Authorization: Bearer $ADMIN_API_KEY"
3.4 Revogação de Chaves Comprometidas — Runbook
Cenário: DEK vazada (ex: acesso não-autorizado ao Doppler, leak em logs).
Procedimento:
#!/bin/bash
# emergency-key-rotation.sh
set -e
echo "=== EMERGENCY KEY ROTATION ==="
echo "Timestamp: $(date)"
# 1. Gerar nova DEK
NEW_KEY=$(openssl rand -hex 32)
echo "Nova chave gerada: ${NEW_KEY:0:8}..."
# 2. Atualizar Doppler (produção bloqueada temporariamente)
doppler secrets set DB_ENCRYPTION_KEY_EMERGENCY="$NEW_KEY" --config production
# 3. Notificar SRE
curl -X POST https://hooks.slack.com/services/T00/B00/XXX \
-d "{\"text\":\"🚨 Key rotation iniciada - DB encryption key\"}"
# 4. Trigger re-encryption job
psql $DATABASE_URL <<SQL
-- Bloquear novas escritas (opcional se criticidade for extrema)
-- ALTER TABLE lojistas ADD CHECK (false);
-- Re-encriptar em batch
UPDATE lojistas
SET email_encrypted = pgp_sym_encrypt(
pgp_sym_decrypt(email_encrypted::bytea, '$OLD_KEY'),
'$NEW_KEY'
)
WHERE id IN (
SELECT id FROM lojistas
WHERE email_encrypted IS NOT NULL
ORDER BY updated_at DESC
LIMIT 1000
);
SQL
# 5. Validar
psql $DATABASE_URL -c "SELECT COUNT(*) FROM lojistas WHERE email_encrypted IS NOT NULL;"
# 6. Rollback antigo secret no Doppler (tornar inacessível)
doppler secrets delete DB_ENCRYPTION_KEY --config production
echo "=== ROTATION COMPLETE ==="
echo "Action items:"
echo "1. Auditar logs de acesso à chave antiga (Doppler > Activity)"
echo "2. Investigar vetor de comprometimento"
echo "3. Post-mortem em 24h"
Tempo estimado: 30-60 minutos dependendo do volume de dados.
Comunicação ANPD: Se a chave vazada permitiu acesso real a PII, disparar protocolo de comunicação de incidente (ver 05-resposta-incidentes.md).
4. Dados Enviados ao GPTMaker
4.1 Avaliação de Risco
Contexto: GPTMaker processa prompts com contexto de clientes (ex: histórico de compras para gerar recomendações de upsell).
Riscos identificados:
| Risco | Probabilidade | Impacto | Severidade |
|---|---|---|---|
| PII exposta em training data do GPTMaker | Baixa | Alto | Médio |
| Vazamento por falha de segurança GPTMaker | Média | Alto | Alto |
| Subprocessadores não-LGPD (servidores EUA) | Alta | Médio | Alto |
| Logs GPTMaker retêm PII indefinidamente | Alta | Médio | Alto |
Conclusão: Tratamento de alto risco — exige RIPD (ver 02-ripd.md) + DPA com GPTMaker + medidas técnicas de minimização.
4.2 Recomendações Técnicas
4.2.1 Anonimização Antes do Envio
Estratégia: Substituir PII por tokens opacos antes de enviar ao GPTMaker.
Exemplo:
// anonymizer.js
import crypto from 'crypto';
const PII_MAP = new Map(); // Em produção, usar Redis com TTL curto
function anonymize(text, piiFields) {
let anonymized = text;
piiFields.forEach(({ type, value }) => {
const token = crypto.randomBytes(8).toString('hex');
PII_MAP.set(token, value);
// Substituir com placeholder tipado
anonymized = anonymized.replace(
new RegExp(escapeRegex(value), 'g'),
`[${type.toUpperCase()}_${token}]`
);
});
return anonymized;
}
function deanonymize(response) {
let original = response;
PII_MAP.forEach((value, token) => {
original = original.replace(
new RegExp(`\\[\\w+_${token}\\]`, 'g'),
value
);
});
return original;
}
// Uso
const customerName = 'João Silva';
const customerEmail = 'joao@email.com';
const anonymizedPrompt = anonymize(
`Criar email de upsell para ${customerName} (${customerEmail}) que comprou produto X.`,
[
{ type: 'name', value: customerName },
{ type: 'email', value: customerEmail },
]
);
// Enviar para GPTMaker
const response = await fetch('https://api.gptmaker.ai/v1/chat', {
method: 'POST',
body: JSON.stringify({ prompt: anonymizedPrompt }),
});
// Restaurar PII na resposta
const finalResponse = deanonymize(await response.text());
Resultado:
- Prompt enviado:
"Criar email de upsell para [NAME_a3f9b2c1] ([EMAIL_d4e8f1a9]) que comprou produto X." - GPTMaker nunca vê PII real
- Resposta é de-anonimizada antes de entregar ao lojista
4.2.2 Pseudonimização (Alternativa)
Se contexto semântico for necessário (ex: "cliente do setor farmacêutico"):
function pseudonymize(profile) {
return {
segment: profile.industry, // "farmacêutico"
purchaseHistory: profile.purchases.map(p => ({
category: p.category,
priceRange: bucketize(p.price), // "R$100-500"
// Remove: SKU exato, timestamp preciso, loja específica
})),
// Remove: nome, email, CPF, telefone, endereço
};
}
const pseudonymizedContext = pseudonymize(customerProfile);
const prompt = `Sugerir produto complementar para cliente do setor ${pseudonymizedContext.segment} que já comprou ${pseudonymizedContext.purchaseHistory[0].category}.`;
4.2.3 Controles de Minimização
Checklist obrigatório antes de enviar ao GPTMaker:
- [ ] PII foi anonimizado ou pseudonimizado?
- [ ] Dados sensíveis (Art. 11 LGPD: raça, religião, política, saúde, vida sexual) foram removidos?
- [ ] Timestamp foi generalizado (ex: "abril/2026" em vez de "2026-04-25 14:32:01")?
- [ ] Geolocalização foi generalizada (cidade em vez de lat/lon exato)?
- [ ] Dados de menores de idade (< 18 anos) foram explicitamente excluídos?
Implementação:
function validateGPTPayload(payload) {
const piiRegex = /\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b|\b\d{3}\.\d{3}\.\d{3}-\d{2}\b|\b\(\d{2}\)\s?\d{4,5}-\d{4}\b/g;
if (piiRegex.test(payload.prompt)) {
throw new Error('PII detectado em payload GPTMaker — abortando');
}
// Log para auditoria
console.log('[GPTMaker] Payload validado:', {
timestamp: new Date().toISOString(),
promptLength: payload.prompt.length,
containsPII: false,
});
return true;
}
4.3 DPA (Data Processing Agreement) com GPTMaker
Cláusulas obrigatórias:
- Finalidade restrita: GPTMaker só pode usar dados para gerar resposta; não pode treinar modelos com dados do Cataloga-ai.
- Retenção: Logs devem ser deletados em 30 dias. Sem retenção indefinida.
- Subprocessadores: GPTMaker deve listar todos (ex: OpenAI, Anthropic, AWS) e notificar mudanças.
- Localização: Dados devem permanecer em servidores com adequação LGPD (Brasil, UE, ou com cláusulas contratuais padrão).
- Auditoria: Cataloga-ai pode auditar compliance do GPTMaker anualmente.
- Notificação de incidente: GPTMaker deve notificar em 24h qualquer vazamento.
Status: Solicitar DPA assinado antes de produção. Se GPTMaker não assinar, considerar alternativas (ex: hosting próprio do LLM via Hugging Face ou Replicate).
5. Monitoramento e Auditoria
5.1 Logs de Auditoria — Acesso a Dados Criptografados
Objetivo: Rastrear quem descriptografou qual PII e quando.
Implementação PostgreSQL:
-- Tabela de auditoria
CREATE TABLE audit_decryption (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
table_name TEXT NOT NULL,
record_id UUID NOT NULL,
column_name TEXT NOT NULL,
decrypted_at TIMESTAMPTZ DEFAULT NOW(),
ip_address INET,
user_agent TEXT
);
-- Trigger em funções de decriptografia
CREATE OR REPLACE FUNCTION log_decryption()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_decryption (user_id, table_name, record_id, column_name, ip_address)
VALUES (
current_setting('app.user_id')::UUID,
TG_TABLE_NAME,
NEW.id,
'email', -- ou dinâmico via TG_ARGV
current_setting('app.client_ip')::INET
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Aplicar em queries sensíveis via application-level trigger
Alertas:
-- Detectar acesso anômalo (>100 decriptações em 1 hora por mesmo usuário)
SELECT user_id, COUNT(*) as decryptions
FROM audit_decryption
WHERE decrypted_at > NOW() - INTERVAL '1 hour'
GROUP BY user_id
HAVING COUNT(*) > 100;
5.2 Checklist de Validação Semestral
- [ ] TLS 1.3 ativo em todos os endpoints (validar com
nmap --script ssl-enum-ciphers) - [ ] Certificados TLS não expirados (monitorar com Zabbix ou Nagios)
- [ ] LUKS encryption ativa nos volumes PostgreSQL/Redis
- [ ] Backups criptografados e testados (restore test trimestral)
- [ ] Secrets no Doppler sem exposição em logs (auditoria de access logs)
- [ ] DEK rotacionada no prazo (verificar
encryption_keys.rotated_at) - [ ] DPA assinado com GPTMaker e subprocessadores
- [ ] Logs de auditoria de decriptografia retidos por 1 ano
6. Custos Estimados
| Item | Custo/mês | Observações |
|---|---|---|
| Let's Encrypt (TLS certs) | $0 | Free |
| Doppler (até 5 usuários) | $0 | Free tier |
| AWS KMS (KEK) | $1 | $1/chave + $0.03/10k requests |
| Storage adicional backups | ~$10 | S3 Standard-IA |
| HSM (opcional, enterprise) | $1.500+ | Apenas se auditoria exigir |
| TOTAL baseline | ~$11 | Operação normal sem HSM |
7. Responsabilidades
| Área | Responsável | Revisão |
|---|---|---|
| Configuração TLS Supabase | SRE / DevOps | Cyber Head |
| Rotação de DEKs | Backend Lead | Cyber Head |
| Auditoria DPA GPTMaker | DPO | Jurídico |
| Testes de restore backups | SRE | Cyber Head |
| Monitoramento logs decriptografia | Security Analyst | Cyber Head |
8. Próximos Passos
- Implementar TLS 1.3 em Supabase (SRE — prazo: 1 semana)
- Ativar LUKS em volumes PostgreSQL/Redis (SRE — prazo: 2 semanas)
- Setup Doppler e migrar secrets do Vercel (Backend — prazo: 3 dias)
- Implementar anonimização GPTMaker (Backend — prazo: 1 semana)
- Solicitar DPA GPTMaker (DPO — prazo: 2 semanas)
- Configurar auditoria de decriptografia (Backend — prazo: 1 semana)
- Teste de restore backup criptografado (SRE — prazo: 1 dia)
Documento mantido por: Cryptography Counsel (Paperclip)
Última atualização: 2026-04-25
Próxima revisão: 2026-07-25 (trimestral)