passvault.
A personal, cross-device password vault: master password plus TOTP, zero-knowledge encryption, and a phone-approval login flow built to survive a hostile network, not just a friendly one.
- Role
- Author
- Timeline
- Web app 2026-06-24 to 08-27 (git-tracked), Android client through 09-02 (untracked)
- Live URL
- secrets.visioncrafters.ro ↗ (login screen only, publicly)
01 / THE REQUEST
A vault the servercan't read.
A personal, cross-device password keeper: master password plus TOTP for auth, installable as a PWA, and specifically zero-knowledge, meaning the server storing the vault should never see plaintext passwords, only ciphertext it cannot decrypt.
02 / THE PROBLEM
Logging in froma phone you trust less.
Zero-knowledge encryption solves storage, not login. A cross-device flow where a phone approves a desktop session has to move the master password and a TOTP code across a channel without exposing them to a relay attacker, and without letting a compromised database hand out live session tokens.
03 / THE CHALLENGE
Encrypted, bound,and unreadable to the server.
The vault key is derived from the master password with PBKDF2, 210,000 iterations, SHA-256, a 16-byte random salt, into a non-extractable AES-256-GCM CryptoKey that cannot be pulled out of the browser even from DevTools (src/lib/crypto.ts:16-19,59-80). Every encrypt call generates a fresh random 12-byte IV, since IV reuse breaks GCM's guarantees entirely. A wrong password or tampered ciphertext throws on decrypt rather than returning garbage (crypto.ts:111-113). Supabase, the server, only ever stores ciphertext plus a salt and an IV, both non-secret, never plaintext and never the derived key.
Desktop generates a fresh RSA-OAEP-2048 keypair per login attempt and sends only the public key plus a random poll token to a Supabase Edge Function, which stores a hash of the poll token, never the raw value (supabase/functions/passvault-login/index.ts:77). Desktop and phone independently derive the same human-checkable 3-digit code from the public key (src/lib/phoneLoginCrypto.ts:47-52), so a relay attempting to substitute a different key produces a visibly different code the user can catch before approving.
The phone seals the master password, TOTP code and factor ID with a fresh AES-GCM content key, itself wrapped with the desktop's RSA public key, and binds Additional Authenticated Data of request ID, fingerprint, kind, attempt to the encryption (phoneLoginCrypto.ts:54-61). Decryption fails outright if any of those four fields don't match, so a sealed blob from one login attempt cannot be replayed against a different request or a retried attempt. Decrypted plaintext is explicitly zeroed after use in a finally block, not left to garbage collection (PhoneApprovalLogin.tsx:122).
The server enforces a constant 250ms response floor and returns an identically shaped response whether an account exists or not, so timing and response shape can't be used to enumerate accounts (passvault-login/index.ts:38-41,59-64). TOTP retries are capped at two attempts before a forced sign-out (PhoneApprovalLogin.tsx:113-116).
04 / THE SOLUTION
What gotbuilt.
Non-extractable AES-256-GCM vault key
PBKDF2 at 210,000 iterations derives a key the browser will not let JavaScript read back out.
Visually verified handshake
A shared 3-digit code, derived independently on both devices, catches a relayed public key before approval.
Request-bound encryption
AAD ties every sealed login payload to one request, attempt and device fingerprint, so replay fails by design.
Enumeration-resistant server
A constant response floor and uniform response shape hide whether an account exists at all.
05 / THE RESULT
Live,in daily use.
- Livesecrets.visioncrafters.ro, login screen public, vault behind auth
- Build windowWeb app 2026-06-24 to 08-27, Android client through 09-02, derived, git-tracked for the web app only
- ClientsWeb PWA, Android app, browser extension, sharing one crypto protocol
- Owns the codeBilal, personal tool
BUILT WITH
Need security done properly?
Next project
Layla Voice PWA