6.8 KiB
Passchmop
A Firefox password manager whose vault is a plain git repository. Every entry is a client-side-encrypted blob; the remote only ever sees ciphertext. Multiple devices sync by pushing to the same repo — conflicts resolve deterministically on-device.
No server. No cloud account. You bring the repo.
Features
- Your repo is the backend. Any HTTPS-accessible git remote works — GitHub, GitLab, Gitea, Forgejo, self-hosted. One encrypted file per entry under
entries/<uuid>.enc. - Your device holds the key. Master password → PBKDF2-SHA256 (600 k iterations, 16-byte random salt) → AES-GCM-256. The remote sees only ciphertext; no verifier for the password is ever transmitted in the clear.
- Cross-device sync. Sync on popup open, on every add/edit/delete, and on a 2-minute background timer. The vault stays unlocked for 5 minutes of inactivity (timer resets on use).
- Deterministic conflict resolution. Edit/edit produces a
<uuid>.conflict-<ts>.encsidecar the user resolves in the UI. Edit/delete always keeps the edit (no silent data loss). Add/add is impossible because entry IDs are random UUIDs. - Origin-aware list. Entries matching the active tab's origin surface to the top; URL prefill on "+ Add" uses just the origin.
- Autofill button. Click ✎ Autofill on an expanded entry — the popup injects a small form-heuristic script into the active tab via
browser.scripting.executeScript, finds the first visibleinput[type=password], walks back for the nearest username-ish input in the same form, and fills both using the prototype setter so React/Vue/Angular controlled inputs observe the change. - Clipboard auto-clear. Copied credentials blank out of the clipboard after 30 s.
Install from source
git clone …/passchmop.git
cd passchmop
npm install
npm run build
npx web-ext run
web-ext run launches a temporary Firefox instance with the extension loaded. For permanent install, package with npm run package and install the resulting .xpi via about:debugging → This Firefox → Load Temporary Add-on (or sign it for proper installation).
Firefox 115+ required — we rely on Manifest V3 features including browser.storage.session.
First-run setup
- Click the Passchmop toolbar icon → "Open setup".
- Enter your git repo URL (HTTPS), a username, and a personal access token with write access to the repo.
- Pick Create new vault (empty repo, or a repo that only contains an auto-README) or Join existing vault (a repo that was set up by another Passchmop device).
- Choose a master password. It never leaves this device.
- The extension commits
vault-meta.jsonand an emptyentries/directory, then pushes. On a repo with an existing non-Passchmop commit (e.g. an auto-created README) it rebases the init commit on top; on a repo that already contains a vault it refuses and directs you to "Join existing".
Your master password is unrecoverable. There is no reset flow on the server side. Lose it and the vault is ciphertext bricks.
Dev workflow
npm run build # one-shot bundle into build/
npm run watch # rebuild on file change
npm run typecheck # tsc --noEmit (strict + noUncheckedIndexedAccess)
npx web-ext run # launch Firefox dev instance
npx web-ext lint # sanity check the manifest + bundle
src/background/— the event page (vault state, git sync, crypto, message router).src/popup/— toolbar popup UI.src/options/— first-run setup and settings.src/common/— shared types + typed message API.- Build output lands in
build/(gitignored).
Architecture and non-obvious invariants are documented in CLAUDE.md.
Vault layout on disk
your-repo/
vault-meta.json # KDF params + verifier. Not secret.
entries/
<uuid>.enc # iv(12B) || ciphertext || gcm-tag(16B)
<uuid>.conflict-<ts>.enc # conflict sidecar, if any
vault-meta.json:
{
"version": 1,
"kdf": {
"name": "PBKDF2",
"hash": "SHA-256",
"iterations": 600000,
"salt": "<base64 16B>"
},
"verifier": "<base64: AES-GCM-encrypted known plaintext>"
}
Each entry's plaintext is UTF-8 JSON: { id, title, url, username, password, notes, created_at, modified_at, device_id }.
Security model
- The master password derives the AES-GCM key. The derived key is held in memory while the vault is unlocked; its raw bytes are also mirrored into
browser.storage.sessionso the key survives Firefox MV3's event-page suspensions.storage.sessionis in-memory only and is wiped on browser restart. - The idle-lock alarm clears both the in-memory key and the session copy after 5 minutes of inactivity, or immediately when you click Lock.
- Repo credentials (username + PAT) are encrypted with the master key before hitting
storage.local. Without the master password, a compromised extension profile yields only ciphertext + KDF parameters. - The remote is assumed hostile-adjacent: it sees the number of entries, their sizes, and commit timing, but not their contents. Commit author is
passchmop-<deviceIdPrefix>@passchmop.local, not your real identity. host_permissions: ["https://*/*"]is broad. It's needed because the background script'sfetchmust bypass CORS against arbitrary git hosts (none of them set CORS headers on smart-HTTP endpoints). Narrow it after first-run configuration if you prefer — requires an extension reload.
Conflict resolution, concretely
- Each PUT op carries a
baseModifiedAt— themodified_atof the version you started editing from. On sync, ifremote.modified_at === baseModifiedAt, it's a linear update; the remote is overwritten with no sidecar. Anything else is genuine divergence. - Divergence → the newer version becomes
<uuid>.enc, the loser is written as<uuid>.conflict-<ts>.enc. Both show up in the list (the sidecar is flagged with aconflictbadge). - Resolve a conflict by picking which entry to keep and deleting the other.
- Edit vs delete → edit wins. Delete vs delete → clean no-op.
Known limitations
- No SSH transport. Browser fetch can't speak ssh; HTTPS + PAT is the only option.
- No content-script autofill on page load. Autofill is explicit (you click the button), by design.
- No password generator UI beyond the
↻ genbutton on the edit form. - No TOTP / secure notes / attachments in v1. The free-text
notesfield can hold whatever. - Background sync is best-effort: Firefox MV3 event pages can be suspended. Sync always runs when you open the popup, so incoming changes from other devices appear then.
- Salt is fixed at vault creation; rotation would require re-encrypting every entry (not in v1).
- Chrome / other-browser support is untested. The manifest is MV3 and mostly portable, but
browser.*globals and some Firefox specifics would need work.
License
TBD.