Phase 45: Arquitetura Zero-Knowledge E2EE

Arc OS — "Não conseguimos ler seus dados, mesmo que quiséssemos" Autor: Sentinel (Arquiteto de Segurança) | Data: 2026-04-24 Status: DONE ✅ (2026-04-28) — issues #16-#20 concluídas Aprovado por: CEO

Checklist de Implementação


O Problema

Estado atual (Phase 43): O servidor armazena dados do usuário em texto plano.

-- data/citadel.db (SQLite, arquivo não criptografado)
CREATE TABLE chat_messages (
  content TEXT NOT NULL  -- ❌ "sk-ant-abc123xyz (my API key)"
);

CREATE TABLE account_settings (
  anthropic_key TEXT,  -- ❌ "sk-ant-live-prod-key-123"
  openai_key TEXT      -- ❌ "sk-proj-sensitive-456"
);

CREATE TABLE users (
  totp_secret TEXT     -- ❌ "JBSWY3DPEHPK3PXP" (2FA seed)
);

Cenários de ataque:

  1. Vazamento de banco de dados — atacante baixa citadel.db → lê todas as mensagens, chaves de API, seeds TOTP
  2. Comprometimento do servidor — atacante obtém acesso SSH → sqlite3 citadel.db .dump → exportação completa dos dados
  3. Ameaça interna — admin com acesso root → lê conversas dos usuários
  4. Vazamento de backup — backup não criptografado enviado para a nuvem → exposto
  5. Compulsão legal — ordem judicial força entrega dos dados → todos os dados legíveis

Veredicto atual: Seguro para multi-tenancy (usuários não acessam dados uns dos outros), NÃO seguro para privacidade do usuário (servidor pode ler tudo).


A Visão: Arquitetura Zero-Knowledge

Princípio: O servidor é não confiável. Mesmo com acesso root SSH + acesso ao banco de dados, não conseguimos descriptografar os dados do usuário.

O que muda:

ANTES (Phase 43):
Usuário digita mensagem → servidor armazena em texto plano → admins podem ler ❌

DEPOIS (Phase 45):
Usuário digita mensagem → browser criptografa → servidor armazena blob → admins NÃO conseguem ler ✅

A promessa:

"Seus dados são criptografados com uma chave derivada da sua senha. Não temos sua senha, então não conseguimos descriptografar seus dados — mesmo que quiséssemos."

Modelo: Signal Protocol (simplificado), ProtonMail, 1Password.


Análise de Decisão: Modelos de Criptografia

Opção A: Criptografia Server-Side (Apenas em Repouso)

Usuário → servidor → criptografa com chave do servidor → armazena no DB
Critério Avaliação Detalhes
Segurança contra externos Média Arquivo de DB criptografado, mas servidor tem a chave → acesso root = pode descriptografar
Segurança contra admin ZERO Admin tem a chave do servidor → acesso total
Conformidade (GDPR) Fraca "Proteção de dados" mas não é zero-knowledge
Complexidade Baixa SQLCipher ou PRAGMA key
Recuperação de chave Fácil Servidor tem a chave → nenhuma ação do usuário necessária

Veredicto: Protege contra HD roubado, NÃO contra comprometimento do servidor ou ameaça interna.

Opção B: Criptografia na Camada de Aplicação (Server-Side)

Usuário → servidor → criptografa com chave do vault → armazena no DB
Servidor tem a chave do vault (AES-256-GCM em vault.json)
Critério Avaliação Detalhes
Segurança contra externos Média Melhor que texto plano, mas chave do vault no mesmo servidor
Segurança contra admin ZERO Admin tem vault.json → descriptografa tudo
Conformidade Fraca Ainda não é zero-knowledge
Complexidade Média Wrappers de criptografia por campo
Recuperação de chave Fácil Gerenciada pelo servidor

Veredicto: Marginalmente melhor que a Opção A. Ainda falha no teste de "ameaça interna".

Opção C: Criptografia End-to-End (Zero-Knowledge) — ESCOLHIDA

Usuário → browser criptografa com master key (NUNCA enviada ao servidor) → servidor armazena blob
Servidor não consegue descriptografar (sem a chave)
Critério Avaliação Detalhes
Segurança contra externos ALTA Dados criptografados + sem chave = inútil
Segurança contra admin ALTA Admin tem o banco de dados mas não consegue descriptografar
Conformidade (GDPR) EXCELENTE Verdadeira minimização de dados (Art. 25)
Complexidade ALTA WebCrypto API, gerenciamento de chaves, UX de recuperação
Recuperação de chave DIFÍCIL Usuário perde senha = dados perdidos (exige recovery key)

Veredicto: Única opção que entrega zero-knowledge. Custo de complexidade vale pela privacidade do usuário.


Visão Geral da Arquitetura

1. Derivação de Duas Chaves (Design Split-Brain)

Problema: Senha enviada ao servidor para auth. Como derivar chave de criptografia sem enviar a senha?

Solução: Derivar DUAS chaves da senha com salts diferentes.

Senha do Usuário: "MySecurePass123!"
      │
      ├─ PBKDF2(password, salt="citadel-auth-v1", 100k iter)
      │     ↓
      │  authBits (256-bit)
      │     ↓
      │  bcrypt(authBits, cost=12) → authHash
      │     ↓
      │  ENVIA AO SERVIDOR (para verificação de login)
      │
      └─ PBKDF2(password, salt="citadel-master-v1", 100k iter)
            ↓
         masterKey (chave de criptografia AES-256-GCM)
            ↓
         FICA NO BROWSER (sessionStorage)
         NUNCA enviada ao servidor

Propriedade de segurança: Comprometimento do servidor → atacante obtém authHashnão consegue derivar masterKey (salt diferente = saída diferente).

Código (frontend/src/crypto/e2ee.ts):

// Auth hash (sent to server)
async function deriveAuthHash(password: string): Promise<string> {
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(password),
    "PBKDF2",
    false,
    ["deriveBits"]
  );
  
  const bits = await crypto.subtle.deriveBits(
    {
      name: "PBKDF2",
      salt: new TextEncoder().encode("citadel-auth-v1"),
      iterations: 100000,
      hash: "SHA-256"
    },
    keyMaterial,
    256
  );
  
  // Server will bcrypt this again for storage
  return Buffer.from(bits).toString("hex");
}

// Master key (kept in browser)
async function deriveMasterKey(password: string): Promise<CryptoKey> {
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(password),
    "PBKDF2",
    false,
    ["deriveKey"]
  );
  
  const masterKey = await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: new TextEncoder().encode("citadel-master-v1"),
      iterations: 100000,
      hash: "SHA-256"
    },
    keyMaterial,
    { name: "AES-GCM", length: 256 },
    false,  // NOT extractable (cannot leak)
    ["encrypt", "decrypt"]
  );
  
  return masterKey;
}

2. Fluxo de Registro

┌─────────────────────────────────────────────────────────────┐
│ Registro do Usuário (browser)                               │
├─────────────────────────────────────────────────────────────┤
│  1. Usuário informa: email, senha                           │
│                                                              │
│  2. Browser deriva:                                         │
│     authHash = PBKDF2(password, "auth-salt") → bcrypt      │
│     masterKey = PBKDF2(password, "master-salt") → AES key  │
│                                                              │
│  3. POST /api/auth/register                                 │
│     {                                                        │
│       email: "[email protected]",                           │
│       authHash: "2a12...bcrypt..."  ← armazenado no DB     │
│     }                                                        │
│                                                              │
│  4. Servidor:                                               │
│     - bcrypt(authHash) → password_hash (hash duplo)        │
│     - INSERT INTO users (email, password_hash)             │
│     - Envia email de verificação                            │
│                                                              │
│  5. Browser:                                                │
│     - Armazena masterKey em sessionStorage                 │
│     - Gera recovery key (criptografa masterKey)            │
│     - Exibe recovery key ao usuário: ⚠️ SALVE ISTO!       │
│       "A83Z-KL9P-MM4X-VN2Q-8JC7"                           │
│     - Usuário baixa PDF                                    │
│                                                              │
│  6. POST /api/crm/account/recovery-key                      │
│     {                                                        │
│       encrypted_master_key: base64(...),  ← blob AES-GCM   │
│       recovery_key_iv: base64(...)                         │
│     }                                                        │
└─────────────────────────────────────────────────────────────┘

3. Fluxo de Login

┌─────────────────────────────────────────────────────────────┐
│ Login do Usuário (browser)                                  │
├─────────────────────────────────────────────────────────────┤
│  1. Usuário informa: email, senha                           │
│                                                              │
│  2. Browser deriva:                                         │
│     authHash = PBKDF2(password, "auth-salt") → bcrypt      │
│     masterKey = PBKDF2(password, "master-salt") → AES key  │
│                                                              │
│  3. POST /api/auth/login                                    │
│     {                                                        │
│       email: "[email protected]",                           │
│       authHash: "2a12..."                                  │
│     }                                                        │
│                                                              │
│  4. Servidor:                                               │
│     - Busca user.password_hash do DB                       │
│     - bcrypt.compare(authHash, password_hash)              │
│     - Se coincidir: gera token JWT                         │
│     - Retorna { token, user_id }                           │
│                                                              │
│  5. Browser:                                                │
│     - Armazena JWT em localStorage (para auth de API)      │
│     - Armazena masterKey em sessionStorage (para decrypt)  │
│     - Pronto para criptografar/descriptografar dados       │
└─────────────────────────────────────────────────────────────┘

4. Criptografar e Enviar Mensagem

┌─────────────────────────────────────────────────────────────┐
│ Enviar Mensagem de Chat (browser)                          │
├─────────────────────────────────────────────────────────────┤
│  1. Usuário digita: "Deploy to production with sk-ant-xyz123"│
│                                                              │
│  2. Browser criptografa:                                    │
│     plaintext = "Deploy to production with sk-ant-xyz123"  │
│     iv = crypto.getRandomValues(12 bytes)  ← nonce aleatório│
│     ciphertext = AES-GCM.encrypt(plaintext, masterKey, iv) │
│                                                              │
│  3. POST /api/crm/projects/arc-v2/chat                      │
│     {                                                        │
│       worker_id: "developer",                              │
│       content_encrypted: "8a9f3c...",  ← blob base64       │
│       content_iv: "7b2e1a..."          ← IV base64         │
│       // SEM campo content em texto plano                  │
│     }                                                        │
│                                                              │
│  4. Servidor:                                               │
│     - Verifica JWT (usuário autorizado)                    │
│     - INSERT INTO chat_messages (                          │
│         project_name,                                       │
│         worker_id,                                          │
│         role = 'user',                                      │
│         content_encrypted = BLOB,  ← opaco para o servidor │
│         content_iv = BLOB,                                  │
│         timestamp                                           │
│       )                                                      │
│     - Retorna { message_id }                               │
│                                                              │
│  5. Servidor NÃO consegue ler "sk-ant-xyz123" — criptografado│
└─────────────────────────────────────────────────────────────┘

5. Receber e Descriptografar Mensagem

┌─────────────────────────────────────────────────────────────┐
│ Buscar Histórico de Chat (browser)                         │
├─────────────────────────────────────────────────────────────┤
│  1. GET /api/crm/projects/arc-v2/chat/history               │
│     Authorization: Bearer <JWT>                             │
│                                                              │
│  2. Servidor:                                               │
│     - Verifica JWT + ownership (canAccessProject)          │
│     - SELECT content_encrypted, content_iv, timestamp      │
│       FROM chat_messages                                    │
│       WHERE project_name = 'arc-v2'                        │
│       ORDER BY timestamp DESC                               │
│       LIMIT 50                                              │
│     - Retorna [{ content_encrypted, content_iv, ... }]     │
│                                                              │
│  3. Browser descriptografa CADA mensagem:                   │
│     for (const msg of messages) {                          │
│       const ciphertext = base64Decode(msg.content_encrypted);│
│       const iv = base64Decode(msg.content_iv);             │
│       const plaintext = AES-GCM.decrypt(                   │
│         ciphertext,                                         │
│         masterKey,  ← do sessionStorage                    │
│         iv                                                  │
│       );                                                    │
│       displayMessage(plaintext);                           │
│     }                                                        │
│                                                              │
│  4. Usuário vê: "Deploy to production with sk-ant-xyz123"  │
│     Servidor viu: blob sem sentido (8a9f3c...)             │
└─────────────────────────────────────────────────────────────┘

Mudanças no Schema do Banco de Dados

Migration 010: Campos E2EE

-- Migration: Add encrypted columns, deprecate plaintext

-- 1. Chat messages
ALTER TABLE chat_messages 
  ADD COLUMN content_encrypted BLOB,
  ADD COLUMN content_iv BLOB,
  ADD COLUMN key_version INTEGER DEFAULT 1;

-- Mark old content as deprecated (will be re-encrypted on next access)
-- DO NOT drop `content` column yet (backwards compat during migration)

-- 2. Account settings (API keys)
ALTER TABLE account_settings
  ADD COLUMN anthropic_key_encrypted BLOB,
  ADD COLUMN anthropic_key_iv BLOB,
  ADD COLUMN openai_key_encrypted BLOB,
  ADD COLUMN openai_key_iv BLOB;

-- 3. Users (TOTP secrets)
ALTER TABLE users
  ADD COLUMN totp_secret_encrypted BLOB,
  ADD COLUMN totp_secret_iv BLOB;

-- 4. Recovery keys
CREATE TABLE key_recovery (
  user_id TEXT PRIMARY KEY,
  encrypted_master_key BLOB NOT NULL,
  recovery_key_iv BLOB NOT NULL,
  recovery_key_hint TEXT,  -- optional hint (first 4 chars: "A83Z-****-****")
  created_at TEXT NOT NULL,
  last_used_at TEXT,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 5. Encryption key rotation log
CREATE TABLE key_versions (
  version INTEGER PRIMARY KEY,
  created_at TEXT NOT NULL,
  deprecated_at TEXT,
  notes TEXT  -- e.g., "Annual rotation", "Security incident"
);

INSERT INTO key_versions (version, created_at) VALUES (1, datetime('now'));

Gerenciamento de Chaves

Geração de Recovery Key

Problema: Usuário esquece a senha → masterKey perdida → todos os dados irrecuperáveis.

Solução: Recovery key (gerada uma vez, armazenada offline pelo usuário).

// Generate recovery key (browser)
async function generateRecoveryKey(masterKey: CryptoKey): Promise<string> {
  // 1. Generate random 128-bit key
  const recoveryBytes = crypto.getRandomValues(new Uint8Array(16));
  
  // 2. Encode as human-readable string (Base32, no ambiguous chars)
  // Result: "A83Z-KL9P-MM4X-VN2Q-8JC7" (5 groups of 4 chars)
  const recoveryKey = base32Encode(recoveryBytes, { groups: 5 });
  
  // 3. Derive AES key from recovery bytes
  const recoveryKeyMaterial = await crypto.subtle.importKey(
    "raw",
    recoveryBytes,
    "PBKDF2",
    false,
    ["deriveKey"]
  );
  
  const recoveryAESKey = await crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: new TextEncoder().encode("citadel-recovery-v1"),
      iterations: 10000,  // lower iterations (recovery is already random)
      hash: "SHA-256"
    },
    recoveryKeyMaterial,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt"]
  );
  
  // 4. Export master key (for encryption)
  const masterKeyExport = await crypto.subtle.exportKey("raw", masterKey);
  
  // 5. Encrypt master key with recovery key
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encryptedMasterKey = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    recoveryAESKey,
    masterKeyExport
  );
  
  // 6. Send to server for storage
  await fetch('/api/crm/account/recovery-key', {
    method: 'POST',
    body: JSON.stringify({
      encrypted_master_key: base64Encode(encryptedMasterKey),
      recovery_key_iv: base64Encode(iv)
    })
  });
  
  // 7. Return recovery key to user (SHOW ONLY ONCE)
  return recoveryKey;  // "A83Z-KL9P-MM4X-VN2Q-8JC7"
}

Fluxo de recuperação (esqueceu a senha):

┌─────────────────────────────────────────────────────────────┐
│ Recuperação de Senha                                        │
├─────────────────────────────────────────────────────────────┤
│  1. Usuário clica em "Esqueci a Senha"                      │
│                                                              │
│  2. Browser exibe:                                          │
│     "Informe sua recovery key:"                             │
│     [____-____-____-____-____]                             │
│                                                              │
│  3. Usuário informa: A83Z-KL9P-MM4X-VN2Q-8JC7              │
│                                                              │
│  4. Busca master key criptografada no servidor:            │
│     GET /api/crm/account/[email protected]     │
│     → { encrypted_master_key, recovery_key_iv }            │
│                                                              │
│  5. Descriptografa master key:                              │
│     recoveryAESKey = deriveKey(recoveryKey)                │
│     masterKey = AES-GCM.decrypt(                           │
│       encrypted_master_key,                                 │
│       recoveryAESKey,                                       │
│       recovery_key_iv                                       │
│     )                                                        │
│                                                              │
│  6. Solicita NOVA senha:                                    │
│     "Defina uma nova senha:"                                │
│     [________] (deve ser diferente da antiga)              │
│                                                              │
│  7. Deriva novo authHash da nova senha                      │
│                                                              │
│  8. Atualiza no servidor:                                   │
│     PUT /api/auth/reset-password                            │
│     { recovery_key, new_auth_hash }                        │
│                                                              │
│  9. Servidor atualiza password_hash                         │
│                                                              │
│ 10. Browser armazena masterKey em sessionStorage            │
│     → Usuário recupera acesso aos dados criptografados     │
└─────────────────────────────────────────────────────────────┘

Rate limiting: Máx. 5 tentativas de recuperação por hora (previne força bruta).


Propriedades de Segurança

Contra o que Nos Protegemos

Ameaça Mitigação Nível
Vazamento de banco de dados (arquivo .db roubado) ✅ Dados criptografados, sem chaves Completo
Comprometimento do servidor (root SSH) ✅ Sem master keys no servidor Completo
Ameaça interna (abuso de admin) ✅ Admin não consegue descriptografar Completo
Ataque MITM (interceptação de rede) ✅ HTTPS + payload criptografado Completo
Vazamento de backup (S3, Drive) ✅ Backups contêm apenas blobs Completo
Compulsão legal (ordem judicial) ✅ Não é possível descriptografar sem a senha do usuário Completo
Ataque XSS (roubo de master key) ✅ Headers CSP + sem JS inline Parcial
Força bruta de senha ✅ bcrypt + PBKDF2 100k iterações Forte

Contra o que NÃO Nos Protegemos (Fora do Escopo)

Ameaça Responsabilidade do Usuário
Roubo de dispositivo físico (laptop desbloqueado) Bloqueio de tela, criptografia de disco completo
Extensão de browser maliciosa Revisar extensões, usar apenas confiáveis
Keylogger no dispositivo Antivírus, dispositivo seguro
Engenharia social (phishing da recovery key) Treinamento de conscientização de segurança
Computação quântica (quebra do AES-256) Inviável até ~2040 (estimativa NIST)

Roadmap de Implementação

Phase 45.1: Fundação (issues #39) — 2 semanas

Objetivo: Wrapper WebCrypto, derivação de chave funcionando.

Aceite: Login deriva ambas as chaves, master key em sessionStorage.

Phase 45.2: Criptografia de Chat (issue #40) — 1 semana

Objetivo: Mensagens de chat com E2EE.

Aceite: Chat funciona, servidor não consegue ler mensagens (query SQL mostra blobs).

Phase 45.3: Criptografia de Secrets (issues #41-#42) — 1 semana

Objetivo: Chaves de API + seeds TOTP criptografados.

Aceite: UI de configurações funciona, servidor não consegue ler chaves.

Phase 45.4: Recuperação de Chave (issues #43-#44) — 1 semana

Objetivo: Recovery key + exportação GDPR.

Aceite: Usuário consegue recuperar a conta com a recovery key.

Phase 45.5: Hardening (issues #45-#46) — 3 dias

Objetivo: CSP, sanitização de logs.

Aceite: Violações CSP = 0, logs limpos.


Estratégia de Testes

Testes Unitários (Jest)

describe('E2EE', () => {
  test('deriveAuthHash is deterministic', async () => {
    const hash1 = await deriveAuthHash('password123');
    const hash2 = await deriveAuthHash('password123');
    expect(hash1).toBe(hash2);
  });

  test('deriveMasterKey is deterministic', async () => {
    const key1 = await deriveMasterKey('password123');
    const key2 = await deriveMasterKey('password123');
    const exported1 = await crypto.subtle.exportKey('raw', key1);
    const exported2 = await crypto.subtle.exportKey('raw', key2);
    expect(Buffer.from(exported1)).toEqual(Buffer.from(exported2));
  });

  test('encrypt → decrypt round-trip', async () => {
    const masterKey = await deriveMasterKey('test');
    const plaintext = 'Secret message';
    const { ciphertext, iv } = await encrypt(plaintext, masterKey);
    const decrypted = await decrypt(ciphertext, iv, masterKey);
    expect(decrypted).toBe(plaintext);
  });

  test('wrong key cannot decrypt', async () => {
    const key1 = await deriveMasterKey('password1');
    const key2 = await deriveMasterKey('password2');
    const { ciphertext, iv } = await encrypt('Secret', key1);
    await expect(decrypt(ciphertext, iv, key2)).rejects.toThrow();
  });

  test('tampered ciphertext fails (GCM auth tag)', async () => {
    const key = await deriveMasterKey('test');
    const { ciphertext, iv } = await encrypt('Secret', key);
    ciphertext[0] ^= 0xFF;  // flip one bit
    await expect(decrypt(ciphertext, iv, key)).rejects.toThrow();
  });
});

Testes de Integração

  1. Registro → Login → Descriptografar:

    • Registra com senha → sai → faz login → verifica se consegue descriptografar mensagens antigas
  2. Fluxo de recuperação:

    • Registra → salva recovery key → sai → esquece senha → recupera → verifica dados acessíveis
  3. Simulação multi-dispositivo:

    • Login no "dispositivo 1" (Chrome) → envia mensagem
    • Login no "dispositivo 2" (Firefox) → verifica se consegue descriptografar a mesma mensagem

Teste de Penetração (Externo, orçamento $5k)

Cenários:

  1. Injeção de payload XSS → tentativa de exfiltrar master key
  2. Dump do banco de dados → verificar ausência de conteúdo de chat em texto plano
  3. Ataque MITM → verificar blobs criptografados à prova de adulteração (tag GCM)
  4. Força bruta de recovery key → verificar eficácia do rate limiting
  5. Ataque de timing de canal lateral → verificar tempo constante do PBKDF2

Cronograma: Após a conclusão das issues #39-#42 (Q3 2026).


Estratégia de Migração

Compatibilidade com Versões Anteriores

Problema: Usuários existentes têm dados em texto plano. Não dá pra quebrar o acesso deles.

Solução: Migração gradual.

Passo 1: Dual-Write (Phase 45.2)

// Backend: write BOTH plaintext and encrypted
async function handlePostMessage(req) {
  const { content, content_encrypted, content_iv } = req.body;
  
  db.run(`
    INSERT INTO chat_messages (
      content,             -- old plaintext (deprecated)
      content_encrypted,   -- new E2EE blob
      content_iv
    ) VALUES (?, ?, ?)
  `, [
    content || null,  // null if E2EE client
    content_encrypted,
    content_iv
  ]);
}

// Frontend: send BOTH (during transition)
const { ciphertext, iv } = await encrypt(plaintext, masterKey);
await fetch('/api/chat', {
  body: JSON.stringify({
    content: plaintext,        // for old API
    content_encrypted: ciphertext,  // for new E2EE
    content_iv: iv
  })
});

Passo 2: Preferência de Leitura (Phase 45.2)

// Backend: return encrypted if available, else plaintext
const messages = db.query(`SELECT * FROM chat_messages`).all();
for (const msg of messages) {
  if (msg.content_encrypted) {
    // E2EE message (preferred)
    yield { content_encrypted: msg.content_encrypted, content_iv: msg.content_iv };
  } else {
    // Legacy plaintext (deprecated, warn user)
    yield { content: msg.content, legacy: true };
  }
}

// Frontend: decrypt if E2EE, else show plaintext with warning
if (msg.content_encrypted) {
  const plaintext = await decrypt(msg.content_encrypted, msg.content_iv, masterKey);
  displayMessage(plaintext);
} else {
  displayMessage(msg.content);
  showWarning('This message was sent before E2EE was enabled. Re-encrypt it?');
}

Passo 3: Ferramenta de Re-criptografia (Phase 45.3)

// Settings → Security → "Encrypt Old Messages"
async function reEncryptOldMessages() {
  const masterKey = getMasterKeyFromSession();
  const oldMessages = await fetch('/api/chat/history?legacy=true');
  
  for (const msg of oldMessages) {
    const { ciphertext, iv } = await encrypt(msg.content, masterKey);
    await fetch(`/api/chat/messages/${msg.id}/encrypt`, {
      method: 'PUT',
      body: JSON.stringify({ content_encrypted: ciphertext, content_iv: iv })
    });
  }
  
  showSuccess('All messages encrypted!');
}

// Backend: update message, WIPE plaintext
app.put('/api/chat/messages/:id/encrypt', (req) => {
  db.run(`
    UPDATE chat_messages
    SET content_encrypted = ?,
        content_iv = ?,
        content = NULL  ← APAGA texto plano
    WHERE id = ?
  `, [req.content_encrypted, req.content_iv, req.params.id]);
});

Passo 4: Remover Coluna Plaintext (Phase 46)

-- After 90 days, verify 0 plaintext messages
SELECT COUNT(*) FROM chat_messages WHERE content IS NOT NULL;
-- If 0, drop column

ALTER TABLE chat_messages DROP COLUMN content;

Considerações de UX

Avisos Assustadores mas Honestos

Geração de recovery key:

┌────────────────────────────────────────────────────────────┐
│  ⚠️ CRÍTICO: Salve Sua Recovery Key                       │
├────────────────────────────────────────────────────────────┤
│  Seus dados são criptografados com uma chave que apenas    │
│  VOCÊ tem. Se você perder a senha E esta recovery key,     │
│  seus dados serão PERMANENTEMENTE PERDIDOS. NÃO conseguimos│
│  recuperá-los para você.                                    │
│                                                             │
│  Recovery Key:                                             │
│  ┌──────────────────────────────────────────────────────┐ │
│  │  A83Z-KL9P-MM4X-VN2Q-8JC7                            │ │
│  └──────────────────────────────────────────────────────┘ │
│                                                             │
│  [ Baixar PDF ]  [ Imprimir ]  [ Copiar ]                  │
│                                                             │
│  ☐ Salvei esta recovery key em um lugar seguro             │
│                                                             │
│  [ Continuar ]  ← desabilitado até o checkbox ser marcado  │
└────────────────────────────────────────────────────────────┘

Primeiro login após o E2EE ser ativado:

┌────────────────────────────────────────────────────────────┐
│  🔒 Criptografia End-to-End Ativada                        │
├────────────────────────────────────────────────────────────┤
│  Suas mensagens, chaves de API e secrets agora são         │
│  criptografados no seu dispositivo antes de serem enviados │
│  aos nossos servidores.                                     │
│                                                             │
│  ✓ NÃO conseguimos ler seus dados (mesmo que quiséssemos) │
│  ✓ Vazamento de banco de dados = atacante obtém blobs inúteis│
│  ✗ Esqueceu a senha + perdeu a recovery key = dados perdidos│
│                                                             │
│  [ Saiba Mais ]  [ Entendido ]                             │
└────────────────────────────────────────────────────────────┘

Feedback de Performance

// Show encryption indicator while typing (subtle)
<textarea 
  placeholder="Type a message... 🔒 (encrypted)"
  onChange={handleChange}
/>

// Show "Encrypting..." spinner before send (< 10ms, barely visible)
<button onClick={handleSend}>
  {encrypting ? '🔐 Encrypting...' : 'Send'}
</button>

Impacto de Conformidade

Artigos do GDPR

Artigo Antes (Phase 43) Depois (Phase 45) Melhoria
Art. 25 (Proteção de dados por design) Armazenamento em texto plano E2EE por padrão ✅ Conforme
Art. 32 (Segurança do processamento) JWT + bcrypt + AES-256 E2EE ✅ Aprimorado
Art. 17 (Direito ao apagamento) Excluir do DB + criptografado (já inutilizável) ✅ Mais forte
Art. 20 (Portabilidade de dados) Exportação pelo servidor Descriptografia client-side + exportação ✅ Controlado pelo usuário

Afirmações de Marketing (Revisão Jurídica Necessária)

Pode dizer:

Não pode dizer:

Recomendado:

"Seus dados são criptografados end-to-end usando AES-256-GCM, padrão da indústria. Usamos uma arquitetura zero-knowledge: a criptografia acontece no seu dispositivo e nunca temos acesso às suas chaves de descriptografia. Nem os nossos administradores conseguem ler suas mensagens criptografadas, chaves de API ou secrets."


Análise de Custo-Benefício

Custos

Item Esforço Risco
Implementação WebCrypto 2 semanas de dev Médio (criptografia é difícil)
UX de gerenciamento de chaves 1 semana de design + dev Alto (confusão do usuário)
Mecanismo de recuperação 1 semana de dev Alto (UX errada = perda de dados)
Migração (dual-write) 3 dias de dev Baixo (compat. com versões anteriores)
Teste de penetração $5.000
Carga de suporte +20% de tickets Médio ("perdi minha recovery key")

Total: ~6 semanas de dev + $5k de auditoria externa.

Benefícios

Benefício Impacto
Confiança do usuário Alto — "privacidade de dados" é a principal preocupação de empresas
Conformidade regulatória Conformidade com GDPR Art. 25 (obrigatória para clientes da UE)
Proteção contra violação Mesmo violação catastrófica → blobs criptografados (dano mínimo)
Vantagem competitiva Poucas plataformas de IA oferecem E2EE (Notion, ClickUp, Monday = texto plano)
Vendas enterprise Obrigatório para setores de saúde, finanças, jurídico (HIPAA, SOC 2)

Veredicto: Benefícios superam os custos. E2EE é requisito básico para plataformas de IA enterprise.


Métricas de Sucesso

Phase 45.1 (Fundação)

Phase 45.2 (Criptografia de Chat)

Phase 45.3 (Secrets)

Phase 45.4 (Recuperação)

Phase 45.5 (Hardening)


Riscos e Mitigação

Risco Probabilidade Impacto Mitigação
Usuário esquece senha + perde recovery key → dados perdidos Média CRÍTICO 1. Aviso assustador durante setup
2. Enviar PDF de recuperação por email automaticamente
3. Opcional: Imprimir QR code da recovery key
Bug no WebCrypto → dados corrompidos Baixa ALTO 1. Testes unitários extensivos
2. Dual-write durante rollout (mantém backup em texto plano)
3. Auditoria de segurança externa
Degradação de performance (PBKDF2 lento em dispositivos antigos) Média Médio 1. Iterações adaptativas (detecta velocidade do dispositivo)
2. Web Worker para derivação não-bloqueante
Incompatibilidade de browser (bugs do Safari) Baixa Médio 1. Testar no Safari 15+, Chrome, Firefox
2. Fallback: polyfill para browsers antigos (ou bloqueá-los)
Carga de suporte ("não consigo acessar meus dados") Alta Médio 1. Docs abrangentes
2. UI do wizard de recuperação
3. Email proativo: "Você salvou sua recovery key?"

Apêndice: Primitivos Criptográficos

PBKDF2-SHA256

Propósito: Desacelerar ataques de força bruta em senhas.

Parâmetros:

Segurança: Com 100k iterações, o atacante consegue tentar ~1000 senhas/seg em GPU de ponta (vs. 1M/seg para SHA256 puro).

AES-GCM

Propósito: Criptografia autenticada (confidencialidade + integridade).

Parâmetros:

Segurança: O AES-256 é resistente a computação quântica até ~2040 (estimativa NIST). O modo GCM previne adulteração (qualquer flip de bit → falha na descriptografia).

bcrypt

Propósito: Hashing de senha (server-side).

Parâmetros:

Por que hash duplo? O browser envia bcrypt(PBKDF2(password)) → o servidor armazena bcrypt(receivedHash). Mesmo que o DB do servidor vaze, o atacante precisa reverter DOIS hashes bcrypt.


Elaborado por Sentinel (Arquiteto de Segurança). Aprovado para implementação: CEO (2026-04-24).