Local Password Manager
Product lab · Security engineering
A personal security-engineering project exploring a product that keeps vault operations on the user's device.
The problem
A password manager’s browser extension needs convenient access to secrets that live in a desktop vault — and the moment the two talk over localhost, the vault has effectively opened a network service on the machine. Anything local can knock.
Local-first doesn’t remove the trust boundary; it moves it. The question becomes: how does the desktop app know a request really came from the paired extension, is fresh, isn’t a replay, and is for a site the credential actually belongs to?
The approach
CipherBox keeps the vault in a Tauri v2 + Rust desktop app: secrets encrypted with XChaCha20-Poly1305 under an Argon2id-derived key, in a locked-down SQLite file. The Chrome extension never stores or derives keys — it talks to the desktop over a loopback HTTP bridge where every request is HMAC-signed, timestamped and nonce-protected.
Autofill is two-phase by design: the extension may ask for matching item metadata, but an actual secret is released only after a real user click, and the desktop independently re-validates the requesting site before answering.
My role — Security architecture and product exploration; not represented as production-ready software.
How it works, step by step
Credential fill — the implemented flow
Every value shown is masked dummy data. No passwords appear anywhere on this page.
01Pair once
Pairing starts on the desktop, not in the browser: the user asks the unlocked app for a code, enters it in the extension, and the two are bound.
Select any node in the diagram to inspect it.
- Person
- Connected system
- Internal layer
- This project
- Data at rest
- Info
- Success
Architecture
Architecture
Local-first: the only implemented transport for secrets is a signed loopback hop on one machine.
- Connected system
- Internal layer
- This project
- Data at rest
- Partial (not built yet)
- Connected system
- Internal layer
- This project
- Data at rest
- Partial (not built yet)
Passkeys follow an even stricter path than passwords: P-256 private keys never leave Rust, and each WebAuthn request waits (max 60 seconds, in memory only) for explicit approval in the desktop UI, with the relying-party ID validated in Rust and again in the extension.
Key engineering decisions
- Local encrypted vault
- Loopback bridge
- Signed requests
- Replay protection
- Optional encrypted backup
Switch to the technical view for the engineering trade-offs behind these.
Challenges & limitations
- macOS only for now: OS secure storage and Touch ID are implemented against the macOS Keychain; other platforms return an explicit error rather than a weaker fallback.
- v1.0.0-rc.1 is a release candidate: the app and DMG build and run, but are ad-hoc signed — no Developer ID, no notarization, and the auto-updater is intentionally inert.
- Cloud backup is written and tested but deliberately not enabled (see architecture). Local encrypted .pvault files are the only shipping backup path.
- The security review is an internal, AI-assisted one — valuable, verified, and not a substitute for an independent external audit. This is a security-engineering lab project, not a product claiming production certification.
What I learned
Local-first doesn’t delete the trust boundary — it relocates it to process identity, IPC design and key lifecycle. The most instructive bugs were never in the cipher; they were in how a pairing code was issued, where a secret passed through argv, and which layer got to decide a domain matched.
Project status
Working vault, extension, TOTP, passkeys, Touch ID and encrypted local backup, with a built macOS app and DMG on disk. Held at release-candidate pending signing, notarization and real-device acceptance — and explicitly not marketed as audited or production-certified.