Wundervault Security White Paper
Abstract
Wundervault is a zero-knowledge secret sharing and persistent vault system. This document describes the cryptographic model, threat model, key management architecture, agent access control system, and the security properties that follow. The core design principle is simple: the server handles storage and routing but is architecturally incapable of reading secret content — for both ephemeral one-time secrets and persistent vault entries.
1. Threat Model
Wundervault is designed to be secure against the following threat classes:
1.1 Passive server compromise
An attacker who gains read access to the database (via SQL injection, backup exposure, or insider access) should be unable to recover plaintext secret content. This is the primary threat. Wundervault's client-side encryption model means a database dump contains only ciphertext, salts, nonces, and HMAC values — no plaintext, no keys.
1.2 Active server compromise
An attacker who can modify server responses could inject malicious JavaScript into the web application. This attack is partially mitigated by Content Security Policy headers. Subresource Integrity (SRI) ensures script files must match published hashes before executing (protects against cache/CDN tampering). SRI does not protect against an attacker who controls the origin server and can update both files and hashes — that remains the fundamental limitation of browser-based cryptography, documented in section 9.
1.3 Network interception
All production traffic is served over TLS. Because encryption occurs client-side before transmission, a passive network observer receives only ciphertext. An active TLS-terminating attacker who can serve a modified page falls under the active server compromise threat above.
1.4 Credential brute force
PBKDF2 with 600,000 iterations makes offline brute force of captured passphrase hashes computationally expensive. Rate limiting at the API layer prevents online brute force. Timing-safe comparisons prevent oracle attacks.
1.5 Agent credential theft
Agent API keys are provisioned via Wundervault's own one-time secret mechanism and burn after first use, preventing replay. The server stores only an HMAC of the API key — not the key itself — so a database breach cannot be used to reconstruct valid credentials. Revocation is immediate at the API layer.
1.6 Agent SSH access and host-level secret exposure
When an agent is granted SSH access to a remote machine (via a key path accessible on the agent's filesystem), the agent effectively has general shell access to that host. This means the agent can read any file accessible to the SSH user — including .env files, config files, and other plaintext secrets — and may inadvertently or intentionally leak their contents into conversation history or logs.
This is a known architectural limitation: vault-level controls cannot restrict what an agent reads once it has general shell access. Mitigations:
- Vault-managed SSH keys (recommended): Store the SSH private key itself as a vault entry and use
ssh_key_entry_idinvault_execremote_host config. The MCP server holds and uses the key without ever exposing it to the agent — the agent cannot SSH directly and all remote execution is forced through the controlledvault_execpath. - No plaintext secrets on agent-accessible hosts:
.envfiles on machines agents can reach should not contain plaintext secrets. Usevault_entry_inject_envat container startup to inject secrets into the process environment without writing to disk, or use a secrets manager that supports runtime injection. - OS-level permission scoping: The SSH user on the remote host should have only the permissions required for the agent's task — not broad read access to application config directories.
Out of scope: Endpoint compromise of the user's device (malware, keyloggers, screen capture after reveal). Wundervault cannot protect secrets already displayed on a compromised device. General shell access granted to agents via SSH keys stored outside the vault.
2. Encryption Architecture
2.1 Primitives
- Symmetric encryption: AES-256-GCM — provides confidentiality and authenticated integrity. Tampering is detected on decryption.
- Key derivation: PBKDF2-HMAC-SHA256 at 600,000 iterations (OWASP 2023 recommended minimum for SHA-256). Each secret has its own random 16-byte salt, ensuring key isolation between secrets.
- Nonce: 12-byte random nonce per encryption operation, never reused. Identical plaintexts produce different ciphertexts.
- HMAC: HMAC-SHA256 used for API key verification and directive signing.
- Retrieval verifier: SHA-256 of the client-derived content key. The server stores this one-way value to authorize retrieval — it cannot be reversed to recover the key or the unlock code.
- Runtime: Web Crypto API (browser) — uses platform-native cryptographic implementations, not JavaScript reimplementations.
2.2 One-Time Secrets
The one-time secret flow is fully server-blind:
- The user enters plaintext in the browser.
- The browser generates a random 16-byte salt and 12-byte nonce via
crypto.getRandomValues(). - A random passphrase is generated client-side.
- The passphrase is processed through PBKDF2-HMAC-SHA256 (600,000 iterations, per-secret salt) to derive a 256-bit AES-GCM key.
- The plaintext is encrypted with AES-256-GCM using the derived key and random nonce.
- The browser computes a one-way verifier =
SHA-256(content_key). - The server receives and stores:
{ciphertext, salt, nonce, verifier, ttl}. It never receives the passphrase, the derived key, or the plaintext. The verifier is one-way — it cannot be reversed to the key. - The link and passphrase are delivered to the recipient via separate channels.
- On retrieval the recipient's browser fetches the per-secret salt, re-derives the content key from the passphrase, and sends only the recomputed verifier. The server does a timing-safe comparison against the stored verifier, atomically marks the secret burned, and returns the ciphertext bundle — which the browser decrypts locally. The server never sees the passphrase or key at any point.
The server is cryptographically incapable of decrypting one-time secrets: it holds only ciphertext and a one-way verifier, never key material. Recovering a secret from a database leak requires brute-forcing the unlock code through the full KDF (600,000-iteration PBKDF2, or GPU-resistant Argon2id) — the verifier provides no shortcut. The one residual exposure is a deliberately weak user-chosen custom code; randomly generated codes are not feasibly brute-forceable, and custom codes are strength-checked client-side.
2.3 Persistent Vault
The vault introduces a vault key K — a random 32-byte key generated client-side during account setup. All vault secret values are encrypted with K before being stored.
The server never receives K. Instead, authentication works via a zero-knowledge proof chain:
- On setup, the browser derives an account secret (128-bit random value stored in localStorage, never sent to server) and runs
PBKDF2(passphrase, salt, 600,000) → base_key. - Then:
HKDF-Extract(salt=account_secret, ikm=base_key) → PRK(pseudorandom key). - Then:
HKDF-Expand(PRK, "wundervault-auth-v1") → auth_keyandHKDF-Expand(PRK, "wundervault-enc-v1") → vault_enc_key. - The server stores
SHA256(auth_key)— a one-way hash that cannot be reversed to recover the passphrase or account secret.
Account Secret: A 128-bit random value generated at registration and stored in your browser's localStorage. Never sent to the server. Even with the database and your passphrase, an attacker must enumerate 2^128 Account Secret possibilities — GPU cracking advantage is eliminated.
H(proof_key) — a SHA-256 hash of the proof key. This is the only passphrase-derived value the server ever sees.proof_key and sends it. The server hashes it and compares against the stored hash — constant-time comparison via HMAC.K is encrypted with the proof key (AES-GCM) and stored as encrypted_vault_key. The browser decrypts K locally using the proof key after authentication succeeds.K before upload. The server stores only ciphertext.A server breach exposes the hash of the proof key, not the proof key itself, and not K. Even with H(proof_key), an attacker cannot decrypt vault content — they would need proof_key to decrypt encrypted_vault_key, and K to decrypt vault secrets.
2.4 Recovery Codes
Recovery codes allow vault access if the passphrase is lost. Each recovery code contains 192 bits of entropy (128-bit Account Secret + 64-bit random padding) and is presented in Base32 format as 6 groups of 8 characters, hyphen-separated (e.g., ABCD-EFGH-IJKL-MNOP-QRST-UVWX). The vault key K is encrypted under the recovery code's derived key and stored server-side.
Security properties:
- Rate limiting: 5 attempts per minute per IP address, with lockout after 10 failures per hour.
- Cross-device usage: The recovery code is your portable credential — entering it on a new device extracts the Account Secret and stores it in localStorage.
- Phishing protection: Wundervault will only ask for your recovery code on the new device setup screen. If prompted elsewhere, it's a phishing attempt.
3. WebAuthn / Biometric Authentication
Wundervault supports WebAuthn for passwordless login and as a second-factor gate for Tier 2 vault secrets.
- Biometric data never leaves the user's device. The WebAuthn authenticator (Touch ID, Face ID, hardware key) signs a server-issued challenge with a device-resident private key.
- The server verifies the signature against the stored public key — it has no access to the biometric template or private key.
- The WebAuthn ceremony is distinct from the vault unlock ceremony. Having a valid session does not unlock Tier 2 secrets — a separate biometric challenge is required for each Tier 2 access session.
- The
tier_2_unlocked_atsession flag is set server-side only after successful WebAuthn verification. It cannot be set by the client.
4. Agent Security Model
4.1 Credential Provisioning
When an agent is registered, Wundervault generates an API key and an encryption key for that agent. These are delivered via Wundervault's own one-time secret mechanism — a setup URL that burns after the onboard script retrieves it. The onboard script (onboard.py) verifies its own Ed25519 signature before executing, exchanges credentials with the vault server, and registers the agent profile with the local daemon. This means:
- Credentials are never sent over email, chat, or any persistent channel.
- The setup URL cannot be replayed — a second attempt returns 404.
- The server stores
api_key_hmac = HMAC(encryption_key, api_key)— not the API key itself. A database breach cannot reconstruct the API key. - The onboard script registers credentials with the local daemon, which stores them in an AES-256-GCM encrypted profile file (
~/.wundervault/agent-profiles.enc) protected by a key derived from the machine ID and UID via HKDF. No plaintext credentials are written to disk; the daemon decrypts the profile at startup and serves credentials to MCP server processes over authenticated Unix domain sockets.
4.2 Request Authentication
Every agent API request must include:
Authorization: Bearer wv_agent_{agent_id}|{key_suffix}— the API key.X-Api-Key-Hmac: {hmac}—HMAC(encryption_key, api_key)computed by the agent.
The server verifies the HMAC against the stored value using a constant-time comparison. This means authentication requires both the API key and the encryption key — possession of either alone is insufficient.
4.3 Zero-Knowledge Agent Vault
The agent vault uses double-layer encryption:
- The human user generates a vault key for each agent (
agent_vault_key) client-side. agent_vault_keyis encrypted with the agent'sencryption_key(AES-GCM) and stored asvault_key_for_agent.- Each secret value sent to the agent's vault is encrypted with
agent_vault_keybefore upload. - On retrieval, the agent decrypts
vault_key_for_agentusing itsencryption_keyto obtainagent_vault_key, then decrypts each secret value. The server participates in neither decryption step.
The server stores only ciphertext it cannot decrypt. An agent database breach exposes ciphertext, vault_key_for_agent (which requires encryption_key to decrypt), and HMACs — not plaintext secret values.
4.4 Scope Isolation
- Agents can only access secrets explicitly sent to their vault via the dashboard. No agent can enumerate or access another agent's secrets.
- Agent API keys are scoped to
/agent/*endpoints only — they cannot access human vault endpoints or administrative functions. - The
mcp_onlyflag restricts an agent to MCP-protocol access only, rejecting direct REST API calls.
4.5 Tier 2 Biometric Gate
Vault secrets can be designated Tier 2. Agents calling GET /agent/vault/secrets/{id} on a Tier 2 secret receive 403 Forbidden unless the human owner has performed a WebAuthn biometric challenge in the dashboard within the current session. This creates a human-in-the-loop approval requirement that cannot be bypassed at the API layer.
4.6 MCP Server Isolation (CIP-017)
The @wundervault/mcp-server package provides MCP tool access with an additional isolation guarantee: secrets are decrypted inside the MCP server process and the plaintext is never returned to the agent. The tool returns only a confirmation string. This prevents secrets from appearing in conversation context, being logged by the agent runtime, or being included in model inputs.
vault_exec — tier-based free-form command execution. Agents provide any shell command string. No pre-approved template list is 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. The injection recipe (env var name, optional setup and teardown commands) lives on the vault entry as exec_config, set once by the user in the dashboard — the agent never needs to know how to wire a credential. Tier 1 secrets execute freely; Tier 2 secrets are gated by the session lock.
Subprocess isolation. The secret is injected as a named environment variable (never a command argument — it never appears in process listings). A fixed list of sensitive parent-process keys (ANTHROPIC_API_KEY, NODE_AUTH_TOKEN, RESTIC_PASSWORD, etc.) are stripped from the child environment so they cannot be inherited. The parent process copies the secret into a Buffer and calls buf.fill(0) in a finally block immediately after the subprocess spawns, zeroing the buffer bytes. Command output is scrubbed for the secret value before being returned to the agent.
Known limitation. JavaScript strings are immutable — the plaintext string produced by TextDecoder.decode() prior to the Buffer copy, and the string passed to the child environment, remain in the V8 heap until garbage-collected. The buf.fill(0) zeroes the Buffer copy only. This is an inherent limitation of the Node.js runtime and is documented as an accepted residual risk. The threat model assumes an attacker requires process memory access to exploit this, at which point the process is already fully compromised.
Tier 2 session lock. Tier 2 vault_exec calls 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 via vault_session_unlock is required to resume Tier 2 execution.
inject_env path allowlist. vault_entry_inject_env only writes secrets to a fixed allowlist of config file paths (~/.npmrc, ~/.netrc, ~/.docker/config.json, project .env files). Paths under /tmp and other temporary directories are rejected. This closes the side-channel where an agent could write a secret to a temp file and read it back with a separate file-read tool, effectively narrating the plaintext to the conversation.
4.7 Revocation
Revocation is immediate and enforced at the API layer on every request — there is no credential caching. A revoked agent receives 401 Unauthorized on its next request regardless of how recently it last authenticated. Individual secrets can also be removed from an agent's vault without revoking the agent entirely.
4.8 Audit Log
Every agent vault operation is recorded in an append-only audit log: agent identity, secret name, action (accessed, denied, registered, revoked), declared purpose, IP address, timestamp, and outcome. Audit log entries are never deleted, even if the agent or secret is subsequently removed.
5. Network and Transport Security
- TLS: All production traffic is served over TLS via Cloudflare tunnel. HTTP is not accepted.
- HSTS:
Strict-Transport-Security: max-age=31536000; includeSubDomains— browsers will not make non-HTTPS connections for at least one year after first visit. - CSP:
Content-Security-Policyrestricts script sources to'self'and Cloudflare Turnstile. Inline styles are permitted; inline scripts are not. This significantly limits XSS attack surface. - Frame protection:
X-Frame-Options: DENYandframe-ancestors 'none'— the application cannot be embedded in iframes, preventing clickjacking. - CORS: Restricted to
wundervault.com. Cross-origin requests from other origins are rejected. - COEP/COOP:
Cross-Origin-Opener-Policy: same-originandCross-Origin-Resource-Policy: same-origin— prevents cross-origin information leakage via shared browsing contexts. - Referrer:
Referrer-Policy: no-referrer— secret URLs are not leaked in HTTP Referer headers if a user navigates away from a secret view page.
6. Anti-Abuse Controls
6.1 Rate Limiting
All sensitive endpoints are rate-limited per IP address via slowapi:
| Endpoint | Limit |
|---|---|
Secret creation (POST /api/v1/secrets) | 20 / minute / IP |
Secret retrieval (POST /api/v1/secrets/{id}/unlock) | 30 / minute / IP |
| Agent secret creation | 30 / minute / IP |
| Agent secret retrieval | 30 / minute / IP |
| Login / registration | 20 / minute / IP |
| WebAuthn challenge | 10 / minute / IP |
| Recovery code use | 5 / minute / IP |
6.2 Timing Oracle Protection
When a secret lookup fails (not found, already burned, or expired), the server performs a dummy HMAC comparison against a fixed random value. This ensures that the response time for "wrong passphrase" and "secret not found" are indistinguishable to an observer. An attacker cannot determine whether a secret ID exists by measuring response latency.
6.3 Bot Protection
The public-facing signup flow is protected by Cloudflare Turnstile — a privacy-preserving CAPTCHA alternative that does not require users to solve challenges but provides strong bot detection signal server-side.
6.4 Automatic Expiry
One-time secrets are automatically purged at TTL expiry (minimum 60 seconds, maximum 7 days) even if never retrieved. A background cleanup task runs hourly in addition to TTL-based deletion at retrieval time. Expired sessions are also purged on the same schedule.
7. Data Storage
7.1 What is stored
| Data | Stored as | Server can decrypt? |
|---|---|---|
| One-time secret content | AES-256-GCM ciphertext + salt + nonce | No |
| Vault secret content | AES-256-GCM ciphertext (encrypted with vault key K) | No |
| Vault key K | AES-256-GCM encrypted with proof_key | No |
| User passphrase | SHA256(HKDF-Expand(HKDF-Extract(PBKDF2(passphrase), account_secret), "auth-v1")) | No — server cannot reverse to passphrase or account_secret |
| Account Secret | Not stored — lives in browser localStorage only. Never transmitted, never in DB. | N/A |
| Agent API key | HMAC(encryption_key, api_key) | No |
| Agent encryption key | Stored encrypted by local daemon in agent-profiles.enc (machine-id-derived key via HKDF); never stored server-side | Never exists server-side |
| Agent vault key | AES-256-GCM encrypted with agent's encryption_key | No |
| WebAuthn credentials | Public key + credential ID | N/A — public key only |
| Recovery code | First 8 chars of bcrypt hash (for lookup only) | No (vault key still requires recovery code) |
7.2 What is never stored
- Secret plaintext
- Unlock passphrases for one-time secrets
- User account passwords or passphrases in any recoverable form
- Vault key
Kin plaintext - Agent encryption keys in plaintext (stored only as AES-256-GCM ciphertext by the local daemon, never on the server)
- Agent API keys in plaintext (only HMAC is stored)
- Biometric data of any kind
8. Incident Response and Breach Scenario
In the event of a full database compromise:
- One-time secrets: Attacker obtains ciphertext, salt, and nonce. Without the passphrase, decryption requires brute force against PBKDF2 (600,000 iterations per attempt). Secrets with strong passphrases are not practically recoverable.
- Vault secrets: Attacker obtains
H(proof_key)and ciphertext. To decrypt vault content they would need to: crack the PBKDF2 hash to obtainproof_key, use it to decryptencrypted_vault_keyto obtainK, then useKto decrypt secret values. This is a two-layer attack against PBKDF2. Users should rotate vault passphrases after a confirmed breach. - Agent credentials: Attacker obtains
api_key_hmac. Without the agent'sencryption_key(stored only in the agent's memory, never on the server), the HMAC cannot be used to reconstruct the API key. Agent vault content is encrypted withagent_vault_key, which is encrypted withencryption_key— also not on the server. - Sessions: Active session tokens would be compromised. Users should be notified to log out and re-authenticate.
9. Known Limitations
- Browser trust: Client-side encryption depends on the integrity of the JavaScript served by the application. A compromised server could serve malicious JavaScript that exfiltrates plaintext before encryption. This is the fundamental limitation of browser-based cryptography. Mitigation: Subresource Integrity (SRI) for scripts; users can inspect source.
- localStorage: Clearing browser localStorage on a device removes the Account Secret from that device. A recovery code is required to restore access. This is by design.
- Post-reveal capture: Once a secret is revealed in the browser, it can be screenshot, copied, or photographed before it burns. The burn-after-read guarantee applies to the server record, not to information already visible on screen.
- Agent context persistence: Revoking an agent does not purge secrets already loaded into the agent's active context or memory. Treat revocation as forward-looking — it stops future access, not past access.
- No recipient authentication: One-time secrets have no built-in recipient identity verification. Anyone with the link and passphrase can read the secret. For high-value secrets, delivery of the passphrase should use an out-of-band channel with its own authentication (e.g., voice call).
- Metadata: Secret existence, TTL, creation timestamp, and access patterns are visible to the server. Only content is encrypted.
10. Compliance Notes
Wundervault's zero-knowledge architecture is well-suited for environments requiring secrets to be shared without exposing them to intermediary infrastructure. The server never processes plaintext credential data, which reduces the exposure of sensitive data and simplifies certain technical controls — organizations subject to SOC 2, PCI DSS, or HIPAA should evaluate their full obligations independently.
Audit logging of all agent access with declared purpose, IP, and timestamp supports compliance requirements for access control audit trails.
A weekly dispatch on agentic AI, security tooling, and building in public. No fluff, no sponsored content. Opt out any time.
Subscribe — it's free →