FoxVault Encryption
Protocol v1
A detailed technical overview of how FoxVault protects your data using a 5-layer key hierarchy with per-item encryption, Trapdoor-Delay AEAD, memory-hard entanglement, and Web Worker crypto isolation.
Last updated: April 2026
1. Overview
FoxVault is a zero-knowledge password manager where all encryption and decryption happens exclusively on your device. The server is an untrusted storage layer that only sees ciphertext.
Design goals:
- One password — used for both authentication and encryption
- Optional secret key as second factor for encryption
- Per-item keys (PIK) for granular sharing and compromise isolation
- Web Worker isolation — key material never touches the main thread (hard requirement)
- Sensitivity tiers — PIN or hardware key for critical items
- Encrypted session persistence — seamless refresh, PIN-gated new tabs
- Tamper-evident audit chain for compartment-protected items
- Zero-knowledge — server stores only ciphertext
- Asymmetric decryption cost — TD-AEAD makes brute force 500× slower and non-parallelizable
- Vault-size security — MH-AONT entanglement scales brute-force cost with item count
- Key-committing AEAD — prevents invisible salamander attacks across key boundaries
- Distributed key verification — no single item can confirm a key guess
2. Threat Model
- Key derivation & encryption
- Web Worker key storage
- Session management
- Audit chain verification
- Stores ciphertext only
- Auth hash verification
- Row-level security (RLS)
- Cannot read or forge data
What FoxVault does NOT protect against:
- Compromised master password + secret key + trapdoor
- Malware on your device (keyloggers, memory readers)
- Physical coercion — cannot protect against forced unlock
3. Key Hierarchy
FoxVault uses a 4-layer key hierarchy. Each layer limits the blast radius of key compromise:
With secret key (v1 protocol): 1. password_key = PBKDF2-SHA256(password, kek_salt, 600,000) 2. root_key = HKDF-SHA256(password_key, secret_key, "foxvault-root") 3. KEK = AES-256-GCM-decrypt(root_key, encrypted_kek) + verify commitment 4. vault_DEK = AES-256-GCM-decrypt(KEK, encrypted_dek) + verify commitment 5. Disentangle wrapped PIKs via MH-AONT (Balloon hash chain) 6. Load TD-AEAD trapdoor φ(N) from secret keystore 7. For each item: resolve time-lock via trapdoor (~2ms), decrypt Without secret key (password-only): 1. password_key = PBKDF2-SHA256(password, kek_salt, 600,000) 2. KEK = AES-256-GCM-decrypt(password_key, encrypted_kek) + verify commitment 3. vault_DEK = AES-256-GCM-decrypt(KEK, encrypted_dek) + verify commitment 4–7. Same as above
4. Algorithms
Core algorithms are NIST-approved standards via the Web Crypto API. TD-AEAD and MH-AONT extend these with number-theoretic and memory-hard constructions based on well-studied assumptions.
| Purpose | Algorithm | Parameters |
|---|---|---|
| Password derivation | PBKDF2-SHA256 | 600,000 iterations, 16-byte salt |
| Key combination | HKDF-SHA256 | Password key + secret key |
| Auth hash | PBKDF2-SHA256 | 600,000 iterations, email salt |
| Symmetric encryption | AES-256-GCM | 12-byte IV, 128-bit auth tag |
| Key commitment | HMAC-SHA256 | HMAC(key, iv || tag || ct) per blob |
| TD-AEAD time-lock | RSA squaring | 2048-bit N, T=2²⁵ sequential squarings |
| TD-AEAD trapdoor | Euler totient | φ(N) in secret keystore |
| MH-AONT entanglement | Balloon hash | 1MB/step, N sequential evals |
| DKV vault tag | HMAC-SHA256 | XOR-fragmented across items |
| Blind index | HMAC-SHA256 | Truncated to 16 bytes |
| PIN compartment | PBKDF2-SHA256 | 600,000 iterations, compartment salt |
| Hardware compartment | WebAuthn PRF | 32-byte hardware-derived secret |
| Audit hash | SHA-256 | Hash chain linking entries |
| Recovery phrase | BIP39-style | 12 words, 132-bit entropy |
| Secret key | CSPRNG | 256-bit random |
5. Per-Item Key Isolation (PIK)
Every vault item has its own unique 256-bit AES key called a Per-Item Key (PIK). Most password managers use a single key for the entire vault.
- Generate random 256-bit PIK
- Encrypt item: AES-GCM(PIK, plaintext) + key commitment
- Apply TD-AEAD masking (time-lock puzzle binding)
- Wrap PIK: AES-GCM(vault_DEK, PIK) + key commitment
- Entangle wrapped PIK via MH-AONT
- Generate blind index tokens (tier 1 only)
- Generate DKV tag fragment
- Zero PIK from Worker memory immediately
- Store all blobs on server
| Feature | FoxVault | Bitwarden | 1Password |
|---|---|---|---|
| Encryption granularity | Per item (unique PIK) | Per vault | Per vault |
| Single key compromise | 1 item exposed | Entire vault | Entire vault |
| Item-level sharing | Native (share PIK) | Vault-level | Vault-level |
| Key zeroing | Immediate (Worker) | N/A | N/A |
6. Key-Committing AEAD
AES-256-GCM is not key-committing: different keys can decrypt the same ciphertext to different valid plaintexts without authentication failure. This enables “invisible salamander” attacks in multi-key systems. FoxVault v1 adds an HMAC-SHA256 commitment to every encrypted blob.
Encrypt:
1. ct, tag = AES-256-GCM(key, iv, plaintext)
2. commitment = HMAC-SHA256(key, iv || tag || ct)
3. Store: { iv, authTag, data, commitment }
Decrypt:
1. expected = HMAC-SHA256(key, iv || authTag || data)
2. if expected ≠ commitment → KEY_COMMITMENT_FAILED (reject)
3. plaintext = AES-256-GCM-decrypt(key, iv, data)Applied at every key boundary: Root → KEK, KEK → DEK, DEK → PIK, PIK → data, and compartment key → PIK. Cost: +32 bytes per blob, ~1µs compute time. Legacy blobs without commitment are accepted for reading but rewritten on next save.
7. Trapdoor-Delay AEAD (TD-AEAD)
TD-AEAD introduces asymmetric decryption cost. Legitimate users decrypt in O(1) via a number-theoretic trapdoor. Attackers without the trapdoor pay O(T) sequential computation per item. Based on Rivest, Shamir, and Wagner’s time-lock puzzles (1996).
Decryption cost per item
100x asymmetry per item — non-parallelizable
Setup (once per vault): Generate RSA primes p, q → N = p·q (2048-bit) φ(N) = (p-1)(q-1) — the trapdoor Store p, q in secret keystore (NEVER on server) Store N in vault metadata (public) T = 2^25 (difficulty: ~33 million sequential squarings) Encrypt (with trapdoor, ~2ms): 1. ct, tag = AES-256-GCM(PIK, plaintext) + commitment 2. seed = SHA-256(PIK || ct || iv) 3. a = seed mod N 4. e = 2^T mod φ(N) // trapdoor shortcut 5. puzzle_solution = a^e mod N // trapdoor shortcut 6. mask = HKDF(puzzle_solution, "td-aead-mask") 7. ct′ = ct ⊕ mask Decrypt without trapdoor (attacker, ~200ms): Must compute a^(2^T) mod N by repeated squaring: a → a² → (a²)² → ... (T sequential squarings) Cannot parallelize. Cannot shortcut without factoring N.
Security reduces to three assumptions: AES-256-GCM security (IND-CCA2), hardness of factoring N (RSA), and the sequential squaring assumption (held since 1996, underpins VDFs in Ethereum and Chia). The trapdoor is stored in the secret keystore and transferred to new devices via the recovery phrase or encrypted P2P.
8. Memory-Hard Entanglement (MH-AONT)
Wrapped PIKs within a vault are entangled via a Memory-Hard All-or-Nothing Transform. Recovering any single PIK requires processing all N entangled blocks sequentially. Vault size becomes a security parameter.
Based on Rivest’s AONT (FSE 1997) extended with Balloon hashing (Corrigan-Gibbs & Kogan, Asiacrypt 2016) for provable memory-hardness.
Entangle (on vault lock/sync):
1. ent_key = CSPRNG(32)
2. s₀ = ent_key
3. For i = 1 to N:
sᵢ = Balloon(sᵢ₋₁ || i, m_cost=1MB) // memory-hard, sequential
eᵢ = wrapped_pik[i] ⊕ sᵢ
4. sentinel = ent_key ⊕ SHA-256(e₁ || ... || eₙ)
Disentangle (on vault unlock):
1. ent_key = sentinel ⊕ SHA-256(e₁ || ... || eₙ)
2. Walk the Balloon chain to recover each wrapped PIK
3. Cache disentangled PIKs in Worker memorySequential entanglement chain
New items are buffered without entanglement and folded into the entangled set on next vault lock or sync (max 10 pending). Editing an item’s data does not affect entanglement — only wrapped PIKs are entangled.
9. Distributed Key Verification (DKV)
DKV removes the per-item verification oracle. Instead of each item’s AES-GCM auth tag independently confirming a key guess, a single vault-wide integrity tag is fragmented across all items via XOR secret sharing. Verifying a key requires decrypting every item.
Construct: 1. vault_tag = HMAC-SHA256(DEK, ct₁ || ct₂ || ... || ctₙ) 2. Fragment: f₁ = random, f₂ = random, ..., fₙ = vault_tag ⊕ f₁ ⊕ ... ⊕ fₙ₋₁ 3. Encrypt each fragment with its item’s PIK 4. Store encrypted_tag_fragment alongside each item Verify (on vault load): 1. Decrypt all N fragments 2. XOR together → recovered vault_tag 3. Compare to HMAC-SHA256(DEK, all ciphertexts) 4. Match → all items authentic. Mismatch → wrong key or tampering.
Tag fragmentation via XOR secret sharing
Each fragment encrypted with its item's PIK — must decrypt ALL to reconstruct tag
An attacker trying a candidate key must decrypt ALL N items and reconstruct the full tag before they can determine if the key is correct. Per-item auth tags still protect individual item integrity — DKV adds vault-wide verification on top.
10. Web Worker Crypto Isolation
All key material lives exclusively inside a dedicated Web Worker. The main thread communicates via postMessage — sending ciphertext, receiving plaintext. The Worker also holds the TD-AEAD trapdoor φ(N) and the Blind Index Key.
- UI rendering
- Sends encrypted blobs
- Receives plaintext only
- No key material
- KEK (raw bytes)
- Vault DEKs (CryptoKey)
- Audit key (CryptoKey)
- Compartment key (30s TTL)
If the Worker fails to load, FoxVault will not function. The Worker is a hard security requirement — the vault remains locked and the user is shown an error. No fallback to main-thread crypto.
11. Encryption Compartments
Three sensitivity tiers with independent encryption layers. Compartment keys are independent of the master password.
- Compartment keys live in Worker with 30-second TTL
- PIN only required to view secrets — not to create or delete items
- encrypted_preview field stores title with vault DEK only (visible without PIN)
- PIN verifier: AES-encrypt(key, "foxvault-pin-ok") — validates without storing PIN
12. Blind Indexing
Server-side search over encrypted vault items without revealing plaintext. Each searchable field is hashed with a vault-specific Blind Index Key (BIK) via HMAC-SHA256. The server matches hashes — never sees site names or usernames.
On save (in Worker):
tokens = normalize(site_name, username) → lowercase, split
hashes = tokens.map(t → HMAC-SHA256(BIK, t).slice(0, 16))
Store hashes in blind_index column
On search (in Worker):
query_hash = HMAC-SHA256(BIK, normalize("chase")).slice(0, 16)
Server: SELECT * WHERE query_hash = ANY(blind_index)
Client decrypts only matching itemsTier 2+ items are excluded from blind indexing — these require full client-side decryption to search. The BIK is a 256-bit random key wrapped with the vault DEK.
13. Session Management
Encrypted session persistence balances security with usability. The Worker’s key state is serialized, encrypted with a random session key, and stored in browser storage.
Session state machine
14. Cryptographic Audit Chain
Append-only, tamper-evident log of decryption events for compartment-protected items (tier 2+). Encrypted with a dedicated audit key, hash-chained via SHA-256.
- Only tier 2+ items audited — standard items excluded to prevent log growth
- Tamper-evident — modifying any entry breaks the chain forward
- Encrypted — server stores only ciphertext, cannot read or forge entries
- Non-blocking — batched inserts flush every 5s or 10 entries
- Append-only at DB level — RLS allows INSERT and SELECT only
The audit key is a 256-bit AES key generated on first use and wrapped with the user’s KEK. It is unwrapped in the Worker on every unlock.
Hash-chained audit entries
Entry #1
VIEW item_a3f
prev_hash: SHA-256(...)
Entry #2
VIEW item_7b2
prev_hash: SHA-256(...)
Entry #3
VIEW item_c19
prev_hash: SHA-256(...)
Each entry's prev_hash chains to the previous — modifying any entry breaks the chain forward
15. Vault Integrity (Merkle Tree)
A client-side Merkle tree detects server-side tampering of vault data. On every vault load, the client builds a SHA-256 Merkle tree from encrypted rows and compares the root hash against a locally stored reference.
- Leaf hashes — SHA-256(id + encrypted_data + encrypted_key) per vault item
- Tamper detection — detects item modification, deletion, injection, and rollback
- Root stored locally — IndexedDB, never sent to the server
- Optimistic updates — root updated after local mutations to prevent false positives
- Graceful reset — first load or storage clear initializes the root without warning
leaves = vault_items.map(row => SHA-256(row.id + row.encrypted_data + row.encrypted_key)) tree = buildMerkleTree(leaves) // pairwise SHA-256 up to root root = tree[0] if (stored_root && root !== stored_root) → TAMPERING DETECTED else → store root in IndexedDB
Binary Merkle tree structure
Any change to a leaf propagates to root — stored root in IndexedDB detects tampering
If a mismatch is detected, a warning banner is shown on the vault page. The user can dismiss and re-anchor to the current state.
16. Secret Key
An optional secret key combines with your password via HKDF. When enabled, your password alone cannot decrypt the vault.
New device flow:
- Sign in → no secret key found → "New device detected"
- Upload Secret Key file or enter recovery phrase
- Generate new secret key, re-derive root key, re-wrap KEK
- Save Secret Key file for future devices
17. Recovery
A 12-word BIP39 recovery phrase (132-bit entropy) derives an independent recovery key that wraps the KEK separately, bypassing both password and secret key.
1. recovery_key = PBKDF2-SHA256(phrase, recovery_salt, 600,000) 2. KEK = AES-256-GCM-decrypt(recovery_key, encrypted_kek_recovery) 3. DEK = AES-256-GCM-decrypt(KEK, encrypted_dek) 4. Vault unlocked — set new password, generate new recovery phrase
Shown once during setup. Store offline. FoxVault never stores the phrase. Regeneration available in Settings (invalidates previous phrase immediately).
18. Authentication
Your master password is never sent to the server in plaintext:
auth_hash = PBKDF2-SHA256(password, "foxvault-auth:" + email, 600,000) Client sends: auth_hash (not password) Server stores: bcrypt(auth_hash) (double hashed) Auth and encryption use DIFFERENT salts: Auth: "foxvault-auth:" + email Encryption: random 16-byte kek_salt → Auth hash cannot derive encryption keys
MFA: TOTP-based with AAL2 enforcement at Supabase Auth and database RLS level. Recovery codes hashed before storage.
19. Data at Rest
All sensitive data is encrypted client-side before reaching the server:
| Data | Encrypted with | Server sees |
|---|---|---|
| Vault items | Per-item PIK (AES-256-GCM + commitment + TD-AEAD) | Masked ciphertext blob |
| Wrapped PIK | Vault DEK (AES-256-GCM + commitment + MH-AONT) | Entangled ciphertext blob |
| DKV tag fragments | Per-item PIK | Encrypted fragments |
| Blind index tokens | BIK (HMAC-SHA256, truncated) | Opaque hashes (tier 1 only) |
| Activity metadata | Vault DEK | Event type + encrypted data |
| User settings | Vault DEK | Key + encrypted value |
| Audit chain | Audit key + commitment | Ciphertext + hash |
| KEK / DEKs | Root key / KEK + commitment | Ciphertext blobs |
| TD-AEAD trapdoor | Secret keystore / recovery key | Not stored on server |
Blob format: JSON with base64 iv (12 bytes), authTag (16 bytes), data (ciphertext), and commitment (32 bytes, HMAC-SHA256 key binding). TD-AEAD blobs additionally include puzzle_a and puzzle_T. Unique IV per encryption operation.
20. Data in Transit
- TLS 1.2+ enforced on all connections (Cloudflare)
- HSTS with 1-year max-age, includeSubDomains
- CSP headers restrict connections to self + Supabase
- No third-party scripts — no analytics, tracking, or ads
- WebSocket (Realtime) — TLS-encrypted, JWT-authenticated
All data encrypted client-side before transmission. TLS is defense-in-depth.
21. Brute-Force Cost Analysis
FoxVault v1’s layered hardening creates a multiplicative cost increase for brute-force attacks that cannot be reduced with additional hardware.
| Layer | Cost per guess | Parallelizable? |
|---|---|---|
| KDF (PBKDF2 600k) | ~200ms | GPU-parallelizable |
| MH-AONT (N=500) | ~500ms | No — sequential Balloon chain |
| TD-AEAD (N=500) | ~100 seconds | No — sequential squarings per item |
| DKV verification | ~1ms | Requires all N items |
| Total per guess | ~101 seconds | Non-parallelizable |
Cost per password guess — legacy vs v1
500x multiplier — immune to GPU/ASIC parallelization
Standard vault (PBKDF2 only): 50-bit password: 2^50 × 0.2s ≈ 35,700 years (1 core) With 10,000 GPUs: ~3.6 years FoxVault v1 (KDF + MH-AONT + TD-AEAD): 50-bit password: 2^50 × 101s ≈ 3,600,000 years (1 core) With 10,000 GPUs: CANNOT parallelize TD-AEAD Effective: ~3,600,000 years regardless of hardware Equivalent entropy gain: ~9 bits from architecture alone A 50-bit password behaves like a 59-bit password.
The critical insight: throwing more hardware at the problem does not help. The RSA sequential squaring assumption (held since 1996) guarantees that each squaring depends on the previous result. 10,000 GPUs attack at the same speed as 1 CPU.
22. Comparison with Competitors
| Feature | FoxVault v1 | Bitwarden | 1Password | LastPass |
|---|---|---|---|---|
| Encryption | AES-256-GCM | AES-256-CBC | AES-256-GCM | AES-256-CBC |
| Key commitment | HMAC-SHA256 | No | No | No |
| KDF | PBKDF2 + HKDF | PBKDF2 600k | HKDF | PBKDF2 600k |
| Device factor | Secret key | None | Secret Key | None |
| Per-item keys | Yes (PIK) | No | No | No |
| Asymmetric decrypt cost | TD-AEAD (100×) | No | No | No |
| Vault-size security | MH-AONT | No | No | No |
| Distributed verification | DKV | No | No | No |
| Worker isolation | Yes (hard req) | No | No | No |
| Sensitivity tiers | PIN / HWK | No | No | No |
| Audit chain | Hash-chained | No | No | No |
| Encrypted search | Blind indexing | No | No | No |
| Sharing | Per-item | Per-vault | Per-vault | Per-folder |
| Recovery | 12-word BIP39 | Emergency Kit |
23. References
- Rivest, Shamir, Wagner, “Time-Lock Puzzles and Timed-Release Crypto” (1996)
- Rivest, “All-Or-Nothing Encryption and The Package Transform” (FSE 1997)
- Corrigan-Gibbs & Kogan, “Balloon Hashing: Provable Memory-Hard Functions” (Asiacrypt 2016)
- Wesolowski, “Efficient Verifiable Delay Functions” (Eurocrypt 2019)
- Pietrzak, “Simple Verifiable Delay Functions” (ITCS 2019)
- Boneh, Bonneau, Bünz, Fisch, “Verifiable Delay Functions” (Crypto 2018)
- Juels & Ristenpart, “Honey Encryption: Security Beyond the Brute-Force Bound” (Eurocrypt 2014)
- Chatterjee & Ristenpart, “Multi-Message Honey Encryption for Password Stores” (2019)
- Alwen & Serbinenko, “High Parallel Complexity Graphs and Memory-Hard Functions” (STOC 2015)
- Shoup, “Sequences of Games: A Tool for Taming Complexity” (2004)
24. Security Practices
25. Responsible Disclosure
If you discover a security vulnerability, please report it responsibly.
We will not take legal action against researchers who report vulnerabilities in good faith.
Ready to secure your passwords?
Free forever for individuals. No credit card required.
Get Started Free