114 lines
6.8 KiB
Markdown
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.
|