Security
Wundervault is designed around a single goal: secrets that can't be recovered after being read. Here's how it works technically.
Encryption Model
Wundervault uses AES-256-GCM with per-secret unique nonces. This means:
- Every secret has its own encryption key — compromising one secret doesn't affect others
- The nonce (12 bytes) is never reused — the same plaintext encrypts differently each time
- AES-256-GCM provides both confidentiality and integrity — tampering is detected on decryption
The unlock passphrase is processed with PBKDF2-HMAC-SHA256 at 600,000 iterations (OWASP 2023 recommended) to derive the Master Key. Each secret also has its own random salt (16 bytes). This means a weak passphrase is computationally expensive to crack, and a breach of one secret's salt reveals nothing about others.
What the Server Never Sees
One-time secrets (created via the homepage form): Encryption happens in your browser before data is transmitted. The server receives and stores only:
- The encrypted ciphertext
- The salt (16 bytes) and nonce (12 bytes)
- A one-way verifier —
SHA-256of the content key, used only to authorize retrieval. It cannot be reversed to the key or the passphrase.
The unlock passphrase is derived client-side and never transmitted — not at creation, not at retrieval (the browser sends only the recomputed verifier, then decrypts the returned ciphertext locally). The server cannot decrypt the content — not now, not ever. A database leak yields only ciphertext and a one-way verifier; recovering a secret means brute-forcing the unlock code through 600,000-iteration PBKDF2 / Argon2id, with no shortcut. Randomly generated codes are infeasible to brute-force; user-chosen custom codes are strength-checked client-side.
Persistent vault (dashboard, requiring login): True zero-knowledge. The vault key K is protected by both your passphrase and a device-local Account Secret stored in your browser's localStorage. To derive K, your passphrase is first processed through PBKDF2-HMAC-SHA256 to produce a Master Key, which is then combined with the Account Secret via HKDF to produce K. The server stores only a hash commitment of the Master Key — not the Account Secret, not K. A breached server has neither factor needed to derive your vault key.
This is true zero-knowledge: even a fully compromised server cannot decrypt your vault. Recovery codes store an encrypted copy of K — recoverable with the code, but still server-blind if the server is breached.
WebAuthn / biometric login: Your biometric data never leaves your device. The WebAuthn authenticator signs a challenge with your private key. The server verifies the signature — it has no access to biometric templates.
Tier 2 Biometric Gate
Some vault secrets are marked Tier 2. These require a separate biometric unlock via WebAuthn — even if you're already logged in.
- The vault unlock and the biometric unlock are separate operations — vault open does not mean Tier 2 is unlocked
- A
tier_2_unlocked_atsession flag tracks biometric unlock state - The Agent API returns
403 Forbiddenon Tier 2 secrets unlesstier_2_unlocked_atis set
Agent Vault Security
Agents (via the Agent API) get a scoped API key that only works on /agent/* endpoints. Security is layered:
- API key provisioned via an encrypted blob:
{api_key, api_key_hmac, encryption_key}— passed through Wundervault's own one-time secret mechanism; the setup link burns after first use - Credentials are stored by the local daemon in an encrypted profile file (
~/.wundervault/agent-profiles.enc), protected by a machine-id-derived key — no plaintext credentials exist on disk - The agent decrypts
vault_key_for_agentclient-side usingencryption_key— the server never sees vault key material - Each agent sees only the specific secrets explicitly sent to its vault (via the dashboard 📨 SEND button)
- Tier 2 secrets require biometric unlock even for agents
- Full audit log: every agent access is logged with agent name, secret name, timestamp, purpose, and IP address
- MCP server: secrets never reach the agent. When using the
@wundervault/mcp-serverpackage, secrets are decrypted inside the MCP process and never sent to the agent — the tool returns only a confirmation. Secrets cannot be echoed in chat or stored in conversation memory. - vault_exec: tier-based command execution (CIP-017). Agents provide any shell command — no template list required. Shell escape patterns ($(), backticks,
bash -c,sh -c,eval) are hard-blocked before the secret is decrypted, eliminating prompt injection as a path to arbitrary execution with secret privileges. The injection recipe (env var name, setup command, teardown command) lives on the vault entry asexec_config, set once by the user in the dashboard — the agent never needs to know how to wire a credential. The secret is injected as a named environment variable (never a command argument), the Buffer is zeroed immediately after spawn, and sensitive parent-process keys are stripped from the child's environment. - Tier 2 session lock. Tier 2
vault_execcalls lock after a configurable number of uses or minutes of inactivity (default: 3 uses or 5 minutes). Tier 1 executes freely with no lock. Session lock state is HMAC-signed on disk — a tampered state file is detected on load and resets to locked. Re-authentication viavault_session_unlockis required to resume Tier 2 execution. - inject_env path allowlist.
vault_entry_inject_envonly writes to a fixed allowlist of config files (~/.npmrc,~/.netrc,~/.docker/config.json, project.envfiles). Paths under/tmpand other temp directories are rejected, preventing agents from using temp files as a side-channel to exfiltrate secrets.
Timing Oracle Protection
Wundervault protects against timing attacks. When a secret is not found or has already been burned, the server still runs a dummy HMAC comparison against a random stored code. This means response timing doesn't leak whether a secret exists — an attacker cannot distinguish "secret not found" from "wrong passphrase" by measuring response time.
Rate Limiting
Both the create and retrieve endpoints are rate-limited via slowapi:
- Create: 20 requests per IP per minute
- Retrieve: 30 requests per IP per minute
These limits are per-IP, which prevents both automated brute-force attacks and denial-of-service attempts.
Data Expiry
One-time secrets: Auto-deleted by TTL (1 hour to 7 days) or burned on first read — whichever comes first. Once burned, the record is gone.
Vault secrets: No TTL — they persist until manually deleted or rotated by the owner.
Network Security
TLS is required in production. Client-side encryption protects the secret content even over HTTP, but an active man-in-the-middle could inject JavaScript to capture the passphrase before it's used — always use HTTPS in production.
API Authentication
Dashboard: Session-based, with optional WebAuthn/biometric second factor.
Agent API: Bearer token in the format wv_agent_<agent_id>|<key_suffix>. The plain API key is never stored — only api_key_hmac (HMAC) in the database. Revocation is immediate; a revoked agent gets 401 Unauthorized on next request.
Known Limitations
- Screen capture: Once revealed, the recipient can screenshot, copy, or photograph the secret before it burns
- Agent context: Revocation cannot reach secrets already loaded into an agent's context or memory — act accordingly
- Client-side encryption: One-time secrets are fully server-blind. Persistent vault is true zero-knowledge — your vault key
Kis protected by both your passphrase and a device-local Account Secret. A breached server has neither. - No per-recipient authentication: One-time secrets have no recipient-level auth — anyone with the link + passphrase can read
- New device or cleared browser data: the Account Secret lives in localStorage. A recovery code is required to restore access on a new device or after clearing browser data.