passchmop/README.md

114 lines
6.8 KiB
Markdown

# 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>.enc` sidecar 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 visible `input[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
1. Click the Passchmop toolbar icon → "Open setup".
2. Enter your git repo URL (HTTPS), a username, and a personal access token with write access to the repo.
3. 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).
4. Choose a master password. It never leaves this device.
5. The extension commits `vault-meta.json` and an empty `entries/` 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`:
```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.session` so the key survives Firefox MV3's event-page suspensions. `storage.session` is 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's `fetch` must 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` — the `modified_at` of the version you started editing from. On sync, if `remote.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 a `conflict` badge).
- 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 `↻ gen` button on the edit form.
- No TOTP / secure notes / attachments in v1. The free-text `notes` field 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.