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=truena criação (one-way, flip post-create recusado comZK_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):
| Algo | Notas |
|---|---|
aes-256-gcm | AEAD, default |
xchacha20-poly1305 | AEAD alternativo, IV 192-bit (long-lived keys) |
KDFs aceitos:
| KDF | Use case |
|---|---|
argon2id | Recommended. User passphrase. Mem-hard. |
hkdf-sha256 | Device-derived keys (sem passphrase user) |
Trade-offs explícitos
O design acceita as seguintes limitações em troca de E2EE:
| Aspecto | Em ZK vault | Em vault normal |
|---|---|---|
| Tokenization / FF1 | ❌ Indisponível | ✅ |
| Encryption-as-a-Service | ❌ | ✅ |
| Server-side encrypt | ❌ | ✅ |
| Search full-text | ❌ Só metadata | ✅ Cross-secret |
| Audit visibility | who/when/op only | who/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
- Vault create — toggle "Zero-Knowledge" → modal explicando trade-offs + confirm typed
- Vault unlock —
ZkUnlockDialogao acessar primeiro secret. Passphrase em memória (keystore) por TTL configurável - Secret view — decrypt no client antes de render, auto-clear timer
- Secret create / edit — encrypt no client antes de POST
useVaultForSecrethook — 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 ZK | Bitwarden | 1Password | |
|---|---|---|---|
| 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.jsno repo do produtosdks/zk-js/README.md— SDK docs- Issue #258 — design doc
- Blog post v4.8