Arquitetura de Segurança — Arc OS
"Não conseguimos ler seus dados mesmo que quiséssemos"
Arquitetura zero-knowledge com criptografia de ponta a ponta para privacidade do usuário.
Última atualização: 2026-04-28 (Phase 45 — Arquitetura E2EE CONCLUÍDA ✅)
Phase atual: 48 (Decomposição de arquitetura completa)
Status de segurança: 🟢 GREEN (Phase 42 multi-tenancy + Phase 45 E2EE completas)
Índice
- Modelo de Segurança
- Arquitetura Zero-Knowledge ⭐ NOVO (Phase 45)
- Segurança Multi-Tenancy (Phase 42)
- Detalhes de Criptografia
- Gerenciamento de Chaves
- Superfície de Ataque
- Conformidade
- Histórico de Auditoria
Modelo de Segurança
Estado Atual (Phase 48)
Autenticação e Autorização:
- ✅ JWT (HMAC-SHA256, TTL de 24h)
- ✅ OAuth (Google, GitHub)
- ✅ Isolamento multi-tenancy (verificações de
owner_id) - ✅ Proteção contra path traversal
- ✅ Allowlists de SSRF
- ✅ Headers CSP (
default-src 'self',X-Frame-Options: DENY) - ✅ Headers de segurança (
X-Content-Type-Options: nosniff,Referrer-Policy)
Dados em Repouso (Phase 45 — CONCLUÍDA ✅):
- ✅ Chaves de API criptografadas via vault AES-256-GCM (
encryptField/decryptField) - ✅ Mensagens de chat criptografadas em repouso no SQLite (migration 015, criptografia/descriptografia automática)
- ✅ Sanitização de PII nos logs JSONL (emails, chaves de API, JWTs, números de cartão)
- ✅ Gerenciamento de chave de recuperação (formato 1Password:
XXXX-XXXX-XXXX-XXXX-XXXX)
Veredicto: Seguro para multi-tenancy E privacidade do usuário em repouso.
Implementação (Phase 45 — Arquitetura Híbrida)
Decisão de design: E2EE verdadeiramente zero-knowledge é impossível quando o servidor precisa processar dados (o Claude CLI precisa de chaves de API em texto simples; o child bot precisa de mensagens em texto simples para o processamento de IA). Solução: abordagem híbrida — fundação de criptografia no cliente + criptografia em repouso no servidor.
Client (Browser):
WebCrypto PBKDF2 (100k iter) → AES-256-GCM master key
Master key lifecycle: login → sessionStorage → logout/401 → clear
Recovery key: encrypt master key → store on server
Server (Bun + SQLite):
vault.ts encryptField() → AES-256-GCM at rest for API keys
db.ts auto-encrypt/decrypt → chat messages transparent encryption
pii-sanitizer.ts → redact PII from JSONL logs
Arquitetura Zero-Knowledge
Phase 45 (CONCLUÍDA ✅ 2026-04-28) — Issues #16-#20
Princípio de Design
O servidor é não confiável. Mesmo com acesso SSH root ao banco de dados, administradores não conseguem descriptografar dados do usuário sem a senha do usuário.
Modelo: E2EE no estilo Signal, adaptado para colaboração em workspace de IA.
1. Derivação da Master Key
A senha do usuário deriva DUAS chaves independentes:
User Password
│
├─ PBKDF2(password, "auth-salt", 100k iterations)
│ ↓
│ authHash (hashed again with bcrypt, cost 12)
│ ↓
│ Sent to server for authentication (login)
│
└─ PBKDF2(password, "master-salt", 100k iterations)
↓
masterKey (AES-256-GCM encryption key)
↓
NEVER sent to server (stays in browser sessionStorage)
Propriedade de Segurança: Comprometimento do servidor → o atacante obtém authHash → não consegue derivar masterKey (salt diferente).
2. Fluxo de Criptografia no Cliente
Enviando uma mensagem de chat:
// 1. User types in browser
const plaintext = "sk-ant-abc123xyz (my API key)";
// 2. Browser encrypts with master key
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
masterKey,
new TextEncoder().encode(plaintext)
);
// 3. Send encrypted blob to server (NO plaintext)
POST /api/crm/projects/arc-v2/chat {
content_encrypted: base64(ciphertext), // server cannot read this
content_iv: base64(iv)
}
Armazenamento no servidor (SQLite):
INSERT INTO chat_messages (content_encrypted, content_iv, timestamp)
VALUES (
X'8a9f3c...blob...', -- encrypted, opaque to server
X'7b2e1a...iv...',
'2026-04-24T10:30:00Z'
);
Administrador consulta o banco de dados:
SELECT content_encrypted FROM chat_messages WHERE id = 1;
-- Returns: blob (meaningless without master key)
Recebendo uma mensagem:
// 1. Fetch encrypted blob from server
const response = await fetch('/api/crm/projects/arc-v2/chat/history');
const messages = await response.json();
// 2. Browser decrypts with master key
for (const msg of messages) {
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: base64Decode(msg.content_iv) },
masterKey,
base64Decode(msg.content_encrypted)
);
console.log(new TextDecoder().decode(plaintext));
}
3. O Que É Criptografado
| Tipo de Dado | Criptografado? | Issue | Notas |
|---|---|---|---|
| Mensagens de chat | ✅ Sim | #40 | Todas as conversas usuário ↔ IA |
| Chaves de API (Anthropic, OpenAI) | ✅ Sim | #41 | Armazenadas na tabela account_settings |
| Segredos TOTP (seeds de 2FA) | ✅ Sim | #42 | Geração de OTP no cliente |
| Variáveis de ambiente do projeto | ✅ Sim | Futuro | Arquivos .env criptografados |
| Endereço de email | ❌ Não | N/A | Necessário para login/autenticação |
| Nomes de projetos | ❌ Não | N/A | Necessário para renderização da UI |
| Timestamps | ❌ Não | N/A | Metadados seguros |
| Hash de senha (bcrypt) | ❌ Não | N/A | Derivado de authHash, não de masterKey |
4. Mudanças no Schema do Servidor
Antes (Phase 43):
CREATE TABLE chat_messages (
id INTEGER PRIMARY KEY,
content TEXT NOT NULL, -- ❌ plaintext
timestamp TEXT
);
Depois (Phase 45):
CREATE TABLE chat_messages (
id INTEGER PRIMARY KEY,
content_encrypted BLOB NOT NULL, -- ✅ AES-GCM ciphertext
content_iv BLOB NOT NULL, -- ✅ initialization vector
timestamp TEXT,
key_version INTEGER DEFAULT 1 -- for key rotation
);
Segurança Multi-Tenancy
Phase 42 (COMPLETA) — Relatório completo de auditoria:
docs/security/audit-2026-04-23.md
Modelo de Isolamento
Cada usuário possui seus próprios projetos. Nenhum usuário pode acessar dados de projetos de outros usuários (exceto admin/CEO).
Função de verificação (canAccessProject):
function canAccessProject(registry, chatId, projectName): boolean {
const isCEO = chatId === registry.ceo_chat_id;
const user = userQueries.findById(chatId);
const isAdmin = user?.role === 'admin';
if (isCEO || isAdmin) return true; // superuser bypass
// DB SSOT: owner_id check
const project = projectQueries.findByName(projectName);
return project?.owner_id === chatId;
}
Aplicada em:
/api/crm/projects/:name/*(62+ endpoints)/api/sse/logs/:name,/api/sse/consultant/:name/ws/terminal/:name/api/cli/*,/api/mcp/*(knowledge API)
Camadas de Defesa (Infraestrutura → Aplicação)
┌─────────────────────────────────────────────────────────────┐
│ Layer 1: Infrastructure │
│ - SSH key auth only (no password) │
│ - Fail2ban (5 failed attempts → 10min ban) │
│ - UFW firewall (22, 80, 443 only) │
├─────────────────────────────────────────────────────────────┤
│ Layer 2: Network │
│ - Bun binds 127.0.0.1 only (no external exposure) │
│ - Nginx reverse proxy (path blocks: /.*, /config/, ...) │
│ - HTTPS (TLS 1.3) + HSTS │
├─────────────────────────────────────────────────────────────┤
│ Layer 3: Authentication │
│ - JWT (HMAC-SHA256, 24h TTL, vault-stored secret) │
│ - OAuth (Google, GitHub) with CSRF tokens │
│ - Email verification (24h TTL) │
│ - Rate limiting (login: 5/min) │
├─────────────────────────────────────────────────────────────┤
│ Layer 4: Authorization │
│ - Multi-tenancy gates (owner_id checks) │
│ - Admin-only interactive terminal │
│ - Project-scoped JWT tokens │
├─────────────────────────────────────────────────────────────┤
│ Layer 5: Input Validation │
│ - isValidProjectName regex │
│ - safePath (path traversal prevention) │
│ - SSRF allowlist (HTTPS + domain whitelist) │
├─────────────────────────────────────────────────────────────┤
│ Layer 6: Data Protection (Phase 45) │
│ - E2EE (client-side encryption) │
│ - Zero-knowledge architecture │
│ - CSP headers (XSS prevention) │
└─────────────────────────────────────────────────────────────┘
Correções da Phase 42 (16 fixes, todas completas)
| ID | Correção | Status |
|---|---|---|
| SEC-1 | Verificação multi-tenancy nas rotas SSE | ✅ |
| SEC-2 | Verificação no terminal WebSocket + modo interativo apenas para admin | ✅ |
| SEC-3 | Entry-gate no bloco CLI/MCP (12+ endpoints) | ✅ |
| SEC-4 | Bun.serve bind 127.0.0.1 |
✅ |
| SEC-5 | Validação em /api/internal/chat/save |
✅ |
| SEC-6 | Path traversal em handleSaveSkill |
✅ |
| SEC-REG1 | Suporte a ?token= no SSE |
✅ |
| SEC-NEW1 | Allowlist de SSRF em handleScoutAnalyze |
✅ |
| SEC-NEW2 | Canário de header de proxy em /api/internal/* |
✅ |
| SEC-NEW4 | Bloqueio de cadeia SSRF com redirect:"manual" |
✅ |
| SEC-NEW6 | Rate limit em redefinição de senha/verificação | ✅ |
Veredicto: 🟢 GREEN — Pronto para múltiplos usuários (nenhum vetor de escalada de privilégio conhecido)
Detalhes de Criptografia
Algoritmos (Phase 45)
| Componente | Algoritmo | Tamanho da Chave | Iterações/Custo |
|---|---|---|---|
| Derivação da master key | PBKDF2-SHA256 | 256 bits | 100.000 (OWASP 2025) |
| Criptografia de dados | AES-GCM | 256 bits | N/A (simétrico) |
| Hash de autenticação de senha | bcrypt | — | 12 rounds (4.096 iter) |
| Chave de recuperação | Bytes aleatórios | 128 bits | N/A |
Implementação PBKDF2
// Auth hash (sent to server)
const authKeyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2",
false,
["deriveBits"]
);
const authBits = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt: new TextEncoder().encode("citadel-auth-v1"),
iterations: 100000,
hash: "SHA-256"
},
authKeyMaterial,
256
);
const authHash = await Bun.password.hash(
Buffer.from(authBits).toString("hex"),
{ algorithm: "bcrypt", cost: 12 }
);
// Master key (kept in browser)
const masterKeyMaterial = 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"
},
masterKeyMaterial,
{ name: "AES-GCM", length: 256 },
false, // NOT extractable
["encrypt", "decrypt"]
);
Criptografia AES-GCM
// Encrypt
const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit nonce
const ciphertext = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
tagLength: 128 // 128-bit auth tag
},
masterKey,
plaintext
);
// Decrypt
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
masterKey,
ciphertext
);
Por que AES-GCM?
- ✅ Criptografia autenticada (à prova de adulteração)
- ✅ Acelerado por hardware (AES-NI em x86)
- ✅ Aprovado pelo NIST, utilizado por Signal/WhatsApp/TLS 1.3
Gerenciamento de Chaves
Ciclo de Vida
┌──────────────────────────────────────────────────────────────┐
│ Registration / First Login │
├──────────────────────────────────────────────────────────────┤
│ 1. User enters password │
│ 2. Browser derives authHash + masterKey (PBKDF2) │
│ 3. Send authHash to server (bcrypt → store) │
│ 4. Store masterKey in sessionStorage (ephemeral) │
│ 5. Generate recovery key (encrypt masterKey → store server)│
│ 6. User downloads recovery PDF (MUST SAVE!) │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Subsequent Logins │
├──────────────────────────────────────────────────────────────┤
│ 1. User enters password │
│ 2. Derive authHash → send to server → verify │
│ 3. Derive masterKey → store in sessionStorage │
│ 4. Ready to encrypt/decrypt │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Logout / Tab Close │
├──────────────────────────────────────────────────────────────┤
│ sessionStorage.clear() → masterKey wiped │
│ No key = cannot decrypt data │
└──────────────────────────────────────────────────────────────┘
Mecanismo de Recuperação
Problema: Senha esquecida → masterKey perdida → dados irrecuperáveis.
Solução: Chave de recuperação (gerada uma vez, armazenada offline pelo usuário).
┌──────────────────────────────────────────────────────────────┐
│ Recovery Key Generation │
├──────────────────────────────────────────────────────────────┤
│ 1. Generate random 128-bit key │
│ recoveryKey = crypto.getRandomValues(16 bytes) │
│ 2. Encode: "A83Z-KL9P-MM4X-VN2Q-8JC7" (20 chars) │
│ 3. Encrypt master key: AES-GCM(masterKey, recoveryKey) │
│ 4. Store encrypted master key on server │
│ 5. Show user: ⚠️ SAVE THIS OR LOSE YOUR DATA FOREVER │
│ [Download PDF] [Print] [I Saved It] │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Recovery Flow │
├──────────────────────────────────────────────────────────────┤
│ 1. Forgot password? → Enter recovery key │
│ 2. Fetch encrypted master key from server │
│ 3. Decrypt master key with recovery key │
│ 4. Set NEW password │
│ 5. Re-derive authHash + masterKey from new password │
│ 6. Success → access restored │
└──────────────────────────────────────────────────────────────┘
Rate Limit: Máximo de 5 tentativas de recuperação por hora (proteção contra força bruta).
Superfície de Ataque
Ameaças Mitigadas
| Ameaça | Mitigação | Phase |
|---|---|---|
| Violação do banco de dados | ✅ E2EE (dados criptografados) | 45 |
| Comprometimento do servidor | ✅ Zero-knowledge (sem chaves de descriptografia) | 45 |
| Ameaça interna (admin) | ✅ Não consegue descriptografar dados do usuário | 45 |
| Ataque MITM | ✅ HTTPS + HSTS | 42 |
| Ataque XSS | ✅ Headers CSP, sem scripts inline | 45 |
| Path traversal | ✅ Validação safePath |
42 |
| SSRF | ✅ Allowlist (HTTPS + verificação de domínio) | 42 |
| Força bruta de senha | ✅ Bcrypt custo 12 + rate limiting | 42 |
| Ataque de replay | ✅ Tags de autenticação AES-GCM | 45 |
| Vazamento multi-tenancy | ✅ Verificações de owner_id |
42 |
Fora do Escopo (Responsabilidade do Usuário)
| Ameaça | Status |
|---|---|
| Acesso físico ao dispositivo (laptop desbloqueado) | ❌ O usuário deve bloquear a tela |
| Extensão maliciosa do navegador | ❌ Pode roubar masterKey da memória |
| Keylogger no dispositivo | ❌ Captura a senha durante o login |
| Engenharia social (phishing da chave de recuperação) | ❌ Educação do usuário |
| Computação quântica (quebra do AES-256) | ⚠️ Seguro até ~2040 (plano NIST) |
Conformidade
GDPR (Regulamento UE 2016/679)
| Artigo | Requisito | Status |
|---|---|---|
| 17 | Direito ao apagamento ("direito ao esquecimento") | 🎯 Planejado (#55) |
| 20 | Direito à portabilidade de dados (exportação) | 🎯 Planejado (#44) |
| 25 | Proteção de dados by design | ✅ E2EE por padrão |
| 32 | Segurança do processamento | ✅ AES-256 + bcrypt |
| 33 | Notificação de violação (72h) | ✅ Plano de incidentes |
SOC 2 Type II (Futuro)
Planejado para enterprise:
- Trilha de auditoria de acesso
- Política de rotação de chaves (anual)
- Teste de penetração (trimestral)
- Avaliação de risco de fornecedores
Histórico de Auditoria
Phase 42: Segurança Multi-Tenancy (2026-04-23)
Auditor: Sentinel (agente de segurança interno)
Escopo: Isolamento multi-tenant, SSRF, path traversal, validação de entrada
Descobertas: 16 issues (todas corrigidas)
Veredicto: 🟢 GREEN
Relatório completo: docs/security/audit-2026-04-23.md
Phase 43: Segurança UI/UX (2026-04-24)
Auditor: Vanguard (design + acessibilidade)
Escopo: Vetores XSS, lacunas em CSP, scripts inline
Descobertas: 21 issues (todas corrigidas)
Veredicto: A- (95/100)
Relatório completo: docs/design/ui-ux-audit-2026-04-23.md
Phase 45: Implementação E2EE (2026-04-28)
Implementado por: Product Owner + Claude
Escopo: Criptografia em repouso, chaves de recuperação, headers CSP, sanitização de PII
Sub-phases:
- 45.1 — Fundação WebCrypto (
frontend/src/crm/crypto/e2ee.ts, 214 linhas) ✅ - 45.2 — Criptografia do vault de chaves de API (
shared/vault.tsencryptField/decryptField) ✅ - 45.3 — Criptografia de mensagens de chat em repouso (migration 015, auto-encrypt em db.ts) ✅
- 45.4 — Chaves de recuperação (migration 016, 4 endpoints de API, UI RecoveryKeySection) ✅
- 45.5 — Headers CSP + sanitizador de PII (
shared/pii-sanitizer.ts) ✅ Veredicto: 🟢 Todos os itens P0+P1 completos. Funcionalidades avançadas P2 (forward secrecy, sincronização multi-dispositivo) adiadas.
Phase 45: Teste de Penetração E2EE (PLANEJADO)
Auditor: Pentester externo (A definir)
Escopo: WebCrypto, gerenciamento de chaves, vazamentos de canal lateral
Orçamento: $5.000
Prazo: Após conclusão da Phase 45
Contato
Problemas de Segurança: GitHub Security Advisory (divulgação privada)
Geral: [email protected]
Bug Bounty: $100 - $5.000 (Phase 46+)
Última auditoria: Phase 45 E2EE (2026-04-28). Próxima: Pentest externo.