Security model
import { Aside } from ‘@astrojs/starlight/components’;
OpaqueVault is built around two non-negotiable guarantees:
- The server cannot decrypt your secrets — ever, under any circumstances, including a subpoena
- AI models cannot see your secret values — not in responses, not in logs, not accidentally
Everything in the architecture follows from these two rules.
What the server knows
Section titled “What the server knows”The API (api.opaquevault.com) stores:
- Your email address
- Your Argon2id salt (not secret — used to derive your KEK client-side)
- Encrypted blobs:
ciphertext_b64,nonce_b64,dek_encrypted_b64,dek_nonce_b64 - Secret names in plaintext (post-MVP hardening will encrypt these too)
- HMAC-hashed secret IDs in the audit log
The server does not store:
- Your master password
- Your KEK (Key Encryption Key)
- Any DEK (Data Encryption Key) in plaintext
- Any secret value in plaintext
What the AI sees
Section titled “What the AI sees”Claude Code (or any MCP client) receives:
- Secret names via
vault_list_secrets - Command output (stdout/stderr/exit code) via
vault_run - Session status via
vault_status
Claude never receives:
- Secret values
- DEKs
- Any intermediate decryption material
The interceptor enforces this at the transport layer — even if a bug in OpaqueVault accidentally returned a secret value in a response, the interceptor would catch and block it.
Key derivation
Section titled “Key derivation”master password + argon2_salt ↓ Argon2id(time=1, mem=64MB, threads=4, keyLen=32)KEK (32 bytes)The Argon2id parameters are hardcoded client-side. The server does not negotiate them. A compromised server cannot weaken your KDF by serving different parameters.
The KEK lives only in memory inside ov mcp serve. It is never written to disk, never transmitted to the server, and is zeroed when the process exits.
Secret encryption
Section titled “Secret encryption”Each secret has its own randomly generated DEK:
random(32 bytes) → DEKDEK encrypted with KEK → dek_encrypted (stored on server)secret value encrypted with DEK → ciphertext (stored on server)Both encryptions use AES-256-GCM with a random 12-byte nonce. Nonces are never reused — each encryption operation generates a fresh nonce via crypto/rand.
Transport security
Section titled “Transport security”All communication between ov mcp serve and api.opaquevault.com uses HTTPS with a post-quantum hybrid KEM:
- X25519 — classical Diffie-Hellman key exchange
- ML-KEM-768 — NIST FIPS 203 post-quantum KEM (formerly Kyber)
The shared secret is derived by XOR-combining the outputs of both KEMs. If X25519 is broken by a classical attack, ML-KEM-768 still protects you. If ML-KEM-768 is broken by a quantum attack, X25519 still protects you.
There is no X25519-only fallback. No downgrade. No --no-pqc flag.
Audit log
Section titled “Audit log”Every operation is logged:
2026-04-09 14:32:11 | read | secret:a3f9b2... | api_key:ci-runner | ip:1.2.3.42026-04-09 14:33:02 | intercept| pattern:aws-key | mode:block | ip:127.0.0.1Secret IDs in the audit log are HMAC-SHA256 hashed with a server-side key. The raw log is less sensitive than plaintext IDs, but the server can verify which secret was accessed by re-computing the HMAC.
What a compromised server reveals
Section titled “What a compromised server reveals”If api.opaquevault.com were fully compromised:
- Attacker gets: encrypted blobs, salts, secret names, HMAC-hashed audit entries
- Attacker cannot get: plaintext secret values (requires KEK, which never left the client)
- Attacker cannot get: your master password (never transmitted)
- Attacker cannot get: your KEK (derived locally, never stored or sent)
The only way to decrypt your secrets is to have your master password and run the same Argon2id derivation locally. This is by design.