Cipherkeep is a local-first, zero-knowledge password manager: a vanilla JS web app plus a companion Manifest V3 browser extension, both encrypting every vault item individually with AES-256-GCM. No server, no account, no telemetry — your master password never leaves the device. Includes a password generator, breach checking via HaveIBeenPwned's k-anonymity API, WebAuthn/passkey unlock, and same-browser sync between the app and extension.
A local-first password manager built with the Web Crypto API. Two pieces:
app/— the main vault: a single-page web app (index.html+app.js, no build step, no dependencies).extension/— a Manifest V3 browser extension with its own lightweight local vault, autofill, and generator.
🌐 Web app: open app/index.html directly, or serve the app/ folder with any static file server:
cd app && python3 -m http.server 8080Then visit http://localhost:8080. (WebAuthn/passkeys require a secure context — localhost or https:// — a plain file:// URL won't support them, though the rest of the app still works.)
🧩 Extension: open chrome://extensions, enable Developer Mode, click "Load unpacked," and select the extension/ folder.
They start out as two independent vaults. Two options, easiest first:
- Export from one, import into the other (Settings → Backup in the app; the extension can import the same file format via its own settings). Fully manual, works across any device or browser, and is the only option if you're moving to a different machine.
- "Sync with app" (extension popup, Vault tab): with the web app tab open and unlocked in the same browser, click it and the extension reconciles both vaults item-by-item over
postMessage/chrome.runtimemessaging — nothing leaves the browser, no server involved, and it only runs when you ask it to. Conflicts resolve by whichever side edited that login more recently. It needs the extension to actually run on the app's page: that's automatic if you serve the app (e.g.python3 -m http.server), but if you're openingapp/index.htmldirectly as afile://URL, enable "Allow access to file URLs" for the extension inchrome://extensions.
A real cloud sync (accounts, a relay/server, background sync across devices) is a much bigger lift — it's the natural "next" option beyond these two, but it means standing up a server and is out of scope for this local-first build.
- Argon2id replaces PBKDF2 as the master-password KDF in the web app (via the
argon2-browserWASM build, loaded from a CDN inindex.html). Argon2id is memory-hard (~64MB, time cost 3), which is far more expensive to parallelize on GPUs/ASICs than PBKDF2's pure iteration count. Vaults created before this change still open fine — the first unlock verifies the old PBKDF2 key, then transparently re-keys the vault onto Argon2id.- The extension stays on PBKDF2-SHA256 (250,000 iterations), not Argon2id. Chrome's Manifest V3 flatly forbids remotely-hosted code in extension pages — no CDN, allowlisted or not — so loading the WASM build the way the web app does isn't possible there. Argon2id in the extension would need the WASM binary vendored directly into the extension's files instead of fetched at runtime; that's not done in this build. PBKDF2 at 250k iterations is still a legitimate KDF, just a weaker offline-cracking defense than Argon2id.
- Password-derived key now wraps a separate vault key instead of encrypting entries directly, in both the app and the extension. This is what makes the app's KDF migration above possible without re-typing every entry by hand, and it's also why a wrong password fails cleanly: unwrapping just throws (AES-GCM's auth tag), so a separate "verifier" ciphertext is no longer needed for new vaults.
- Master password re-prompt before exporting a backup, wiping the vault, or deleting an item — closes the gap where anyone at an unlocked, not-yet-auto-locked session could exfiltrate or nuke the vault in one click.
- Rate-limited unlock attempts: 3 free tries, then an increasing delay (2s → 4s → 8s → 16s → 30s → capped at 60s) tracked per-device in storage, so it survives a page reload. This slows this app's own retry loop; it isn't a substitute for a slow KDF's actual cracking-cost defense against someone attacking the stored ciphertext directly and offline.
- Backup staleness warning: a banner (in Settings, and across the app once you have items in your vault) appears if you've never exported a backup, or it's been 14+ days since your last one.
| Feature | Implementation |
|---|---|
| AES-256 encryption | AES-256-GCM via crypto.subtle, unique random IV per vault item |
| Zero-knowledge architecture | Master password never stored or transmitted; a PBKDF2-SHA256 (250,000 iterations) key is derived locally each unlock and held only in memory. All persisted data is ciphertext (IndexedDB / chrome.storage.local). No server component in this build. |
| Password generator | CSPRNG-based (crypto.getRandomValues), random-character and passphrase (word-list) modes, live entropy estimate |
| Password health checker | Local scoring (length, character variety, common-password/pattern matching), reuse detection, staleness (90+ days unchanged) |
| Breach detection | Live client-side calls to the HaveIBeenPwned Pwned Passwords k-anonymity API — only a 5-character SHA-1 prefix ever leaves the device, the full hash and password never do |
| Browser extension | MV3 extension with its own encrypted local vault, a content script that detects and fills login forms, a "save current page login" action, and a same-browser "Sync with app" action that reconciles its vault with the web app tab (see Sync section below) |
| Passkey / WebAuthn | Real navigator.credentials.create/get calls using the PRF extension: the platform authenticator's PRF output is fed through HKDF to derive an AES key that wraps the vault key, so unlocking never touches the master password |
| Biometric login | Comes from the above — whatever biometric/PIN check the OS's platform authenticator uses (Touch ID, Windows Hello, Android biometrics) gates the WebAuthn assertion |
This was built as a serious, working demonstration of the architecture, not an audited product:
- No independent security audit. Don't use it as your daily password manager for high-value accounts without one.
- No cloud sync/server. The web app and extension each keep their own local vault. Two ways to move data between them:
- Encrypted export/import (Settings) — works across devices and browsers, fully offline, but manual.
- Live sync between tab and popup (Settings in the app / "Sync with app" in the popup) — when the web app tab and the extension are open in the same browser, the extension can reconcile the two vaults directly via
postMessage/chrome.runtimemessaging, no server involved. This only works while that tab is open and both vaults are unlocked, and it doesn't help across devices — for that, use the export/import backup. There is still no cloud backup: losing the device without an exported backup means losing the vault.
- No password reset. By design — that's what makes it zero-knowledge. Forgetting the master password makes the vault permanently unreadable.
- WebAuthn PRF support is inconsistent across browsers/authenticators as of early 2026. The app feature-detects and falls back to the master password when PRF isn't available; check current support before relying on it.
- The browser extension's autofill uses straightforward DOM heuristics (first password field, nearest username-like field) rather than the sophisticated per-site heuristics and form-detection heuristics that production managers maintain — it will miss unusual login forms.
- Clipboard auto-clear depends on the Clipboard API's read permission being granted; if it's denied, copied passwords will simply remain on the clipboard until overwritten.
- This is JavaScript in a browser tab/extension, not a hardened native app — it inherits whatever the browser's security model gives you (e.g., a malicious extension with broad permissions or compromised OS could still observe memory in principle). Real products add anti-tampering, code signing, and platform keychains on top of this.
- Every vault item is encrypted individually with its own random IV, rather than one big encrypted blob — so editing one item never requires decrypting/re-encrypting the whole vault.
- Unlock verification uses a small "verifier" ciphertext (a known plaintext string encrypted with the derived key) rather than comparing raw keys, so an incorrect password fails cleanly.
- Passkey unlock wraps the same AES-256 vault key rather than deriving a separate one, so switching between master-password and passkey unlock is always operating on identical vault data.