Pular para o conteúdo principal

Zero-Knowledge Vault — E2EE opt-in

A partir da v4.8, o CipherVault entrega vaults end-to-end encrypted opt-in: o servidor nunca vê plaintext de secret values. Apenas metadados (name, path, tags) e o ciphertext opaco produzido pelo client.

Paridade com Bitwarden E2EE para tenants regulados / paranoid threat model.

Modelo

  • Flag por vault: vaults.is_zero_knowledge=true na criação (one-way, flip post-create recusado com ZK_FLAG_IMMUTABLE)
  • Encryption no client (browser via SDK, ou CLI cv-zk) com user passphrase + KDF
  • Server faz validação apenas de shape do envelope — nunca tenta decrypt

Wire envelope

zk:v1:<algo>:<kdf>:<salt_b64>:<iv_b64>:<ct_b64>:<tag_b64>

Algoritmos aceitos (allowlist):

AlgoNotas
aes-256-gcmAEAD, default
xchacha20-poly1305AEAD alternativo, IV 192-bit (long-lived keys)

KDFs aceitos:

KDFUse case
argon2idRecommended. User passphrase. Mem-hard.
hkdf-sha256Device-derived keys (sem passphrase user)

Trade-offs explícitos

O design acceita as seguintes limitações em troca de E2EE:

AspectoEm ZK vaultEm vault normal
Tokenization / FF1❌ Indisponível
Encryption-as-a-Service
Server-side encrypt
Search full-text❌ Só metadata✅ Cross-secret
Audit visibilitywho/when/op onlywho/when/op + diff
Recovery (passphrase perdida)Server NÃO recupera✅ Admin via dual-control
Rotation server-side❌ Re-encrypt no client + push✅ Rotação automática

Routes que precisem plaintext recusam ANTES de processar com HTTP 409 ZkBoundaryViolation (não tentam decrypt e falhar). Pin code path: backend/src/lib/zkVault.js — exported ZkBoundaryViolation class.

SDK cliente — @ciphervault/zk-sdk

Browser:

import { createZkClient } from '@ciphervault/zk-sdk';
import { browserKdf } from '@ciphervault/zk-sdk/kdf/browser';

const client = createZkClient({ kdf: browserKdf(), kdfName: 'argon2id' });

const envelope = await client.encrypt('my-secret-value', userPassphrase);
// → "zk:v1:aes-256-gcm:argon2id:<salt>:<iv>:<ct>:<tag>"

// Upload via API REST normal:
await fetch('/api/secrets', {
method: 'POST',
body: JSON.stringify({
name: 'db-password',
value: envelope,
vault_id: 42 // vault marcado is_zero_knowledge=true
})
});

// Mais tarde, depois de fetch:
const plaintext = await client.decrypt(envelope, userPassphrase);

Node CLI:

npm install -g @ciphervault/zk-sdk argon2

cv-zk encrypt --passphrase-env CV_ZK_PASSPHRASE \
--in plain.txt --out envelope.txt

cv-zk decrypt --passphrase-env CV_ZK_PASSPHRASE \
--in envelope.txt --out plain.txt

UI flow

  1. Vault create — toggle "Zero-Knowledge" → modal explicando trade-offs + confirm typed
  2. Vault unlockZkUnlockDialog ao acessar primeiro secret. Passphrase em memória (keystore) por TTL configurável
  3. Secret view — decrypt no client antes de render, auto-clear timer
  4. Secret create / edit — encrypt no client antes de POST
  5. useVaultForSecret hook — auto-resolve se secret pertence a ZK vault e dispara unlock se necessário

Boundary enforcement (server-side)

Ops que requerem plaintext:

secret.value.decrypt
secret.value.encrypt (server-side encryption)
secret.tokenize (FF1)
secret.eaas.encrypt|decrypt
secret.search.contains
secret.rotate (geração nova requer comparar)

Cada uma checa vault.is_zero_knowledge no início. Se true → throw ZkBoundaryViolation com HTTP 409 + body:

{
"error": "ZK_BOUNDARY_VIOLATION",
"op": "secret.tokenize",
"vault": 42,
"message": "Operation requires plaintext access; vault is zero-knowledge."
}

Recovery hint

vaults.zk_recovery_hint (campo opcional) — frase plain stored non-encrypted que pode ajudar o owner a lembrar a passphrase ("aniversário da Maria + lugar onde ela nasceu"). Não é segredo — visível na UI. Trade-off de UX: zero recovery sem passphrase, mas hint reduz lockout.

Audit

Audit log preserva:

  • ✅ Who (user_id, app_id)
  • ✅ When (timestamp)
  • ✅ Op (secret.view, secret.update)
  • ❌ What (value, diff antes/depois — server não tem)

Compliance auditor consegue auditar acesso, não conteúdo — trade-off explícito documentado em COMPLIANCE.md do produto.

Comparação vs Bitwarden / 1Password

CipherVault ZKBitwarden1Password
Opt-in por vault❌ Sempre ZK❌ Sempre ZK
Server-side ops em outros vaults
KMIP / FF1 / EaaS✅ (não-ZK vaults)
Audit diff✅ (não-ZK)
Recovery via admin❌ (ZK)Limited (Secret Key)

Filosofia: most users want server-side features, alguns subset querem E2EE. CipherVault oferece dois modos no mesmo tenant; Bitwarden/1Password forçam tudo-ou-nada.

Limitações conhecidas

  • No backup-by-admin — passphrase perdida = dados perdidos. Mitigation: encourage uso de Fortress com Shamir 3-of-5 pra threshold recovery (em vez de ZK puro)
  • No cross-device passphrase — keystore é per-browser/per-CLI session
  • SDK browser cost — argon2-wasm tem ~50-200ms de pause; aceitar pra security-sensitive flows

Referências

  • backend/src/lib/zkVault.js no repo do produto
  • sdks/zk-js/README.md — SDK docs
  • Issue #258 — design doc
  • Blog post v4.8