docs: add README and project notes
This commit is contained in:
parent
0340a7f8b7
commit
0a8ead0f85
60
CLAUDE.md
Normal file
60
CLAUDE.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Passchmop — working notes
|
||||
|
||||
Firefox MV3 extension. Git-backed, client-side-encrypted password vault. TypeScript, esbuild, isomorphic-git. No server — the remote is a plain git repo.
|
||||
|
||||
## Layout
|
||||
|
||||
- `src/background/` — event page. `vault.ts` owns state and key material; `sync.ts` owns git + conflict resolution; `crypto.ts` is Web Crypto wrappers; `fs.ts` wraps lightning-fs; `index.ts` is the message router + alarms.
|
||||
- `src/popup/` — main UI. `popup.ts` is the view; `autofill.ts` is injected into tabs via `browser.scripting.executeScript` and must stay self-contained (serialized through `func`).
|
||||
- `src/options/` — setup and settings page.
|
||||
- `src/common/` — shared types, the typed `api` object for background↔UI messages, and `polyfills.ts` (Buffer re-export, injected by esbuild into the background bundle only).
|
||||
|
||||
## Build & check
|
||||
|
||||
- `npm run build` — esbuild bundles the three entry points into `build/`.
|
||||
- `npm run watch` — same, in watch mode.
|
||||
- `npm run typecheck` — `tsc --noEmit`. Run before committing non-trivial changes; esbuild does not type-check.
|
||||
- `npx web-ext run` — launches Firefox with the extension loaded.
|
||||
- `npx web-ext lint --source-dir=.` — 0 errors expected. Two `DANGEROUS_EVAL` warnings come from bundled deps (pako `new Function`) and are acceptable for local loading.
|
||||
|
||||
## Non-obvious invariants — do not regress these
|
||||
|
||||
### Git / isomorphic-git
|
||||
|
||||
- **Never `git.checkout({ ref: <SHA> })`.** Checkout by SHA leaves HEAD detached; `commit()` then advances HEAD but not `refs/heads/main`, and `push('main')` silently sends the stale ref. Always `writeRef` main first, then `git.checkout({ ref: 'main', force: true })`.
|
||||
- **`applyPut` / `applyDelete` must read the remote blob via `git.readBlob(remoteOid, filepath)`, not from the working tree.** `vault.put()` pre-writes blobs to the workdir for immediate UI feedback, and `checkout -f` does not remove untracked files. Reading the workdir would treat our own fresh-IV encryption as a divergent remote and spawn phantom conflict sidecars.
|
||||
- **`createVault` handles three remote states**: empty (init + push), has `main` without a vault (base our init commit on it — e.g. auto-created README), has a vault (refuse, point user at "Join existing").
|
||||
- **esbuild → isomorphic-git ESM**: the `.` subpath export only points at the CJS entry, which does `require('crypto')` and breaks browser bundling. The config has a tiny plugin (`isogitEsmPlugin`) that redirects the bare specifier `isomorphic-git` to `node_modules/isomorphic-git/index.js`. Don't drop it.
|
||||
- **Buffer polyfill** is injected only into the background bundle via `inject: [src/common/polyfills.ts]`. isomorphic-git's index-file code uses `Buffer.from` / `Buffer.alloc` / `Buffer.concat` at module scope.
|
||||
|
||||
### Crypto / session
|
||||
|
||||
- **`deriveKey` returns an extractable CryptoKey (`extractable: true`).** We export raw bytes into `browser.storage.session` so the vault stays unlocked across Firefox MV3 event-page suspensions. storage.session is in-memory only; the trade-off is a live memory artifact, not disk persistence.
|
||||
- **Idle lock**: `idleCheck` fires on a 30 s alarm. Any background message handler calls `vault.touchActivity()` on success — *except* `lock` and `reset`. Do not bump activity from those.
|
||||
- **Master password → PBKDF2-SHA256, 600 000 iterations, 16-byte random salt.** Salt is in `vault-meta.json` (not secret). A verifier blob in the same file lets unlock detect the wrong password before touching entries.
|
||||
|
||||
### Conflict resolution
|
||||
|
||||
- Entries live at `entries/<uuid>.enc`; UUIDs are random, so add/add is impossible.
|
||||
- A PUT op carries `baseModifiedAt` — the `modified_at` of the version we edited from. If the remote's current `modified_at === baseModifiedAt`, it's a linear edit → overwrite, no sidecar. Only genuine divergence produces a `<uuid>.conflict-<ts>.enc` sidecar.
|
||||
- Edit vs delete: edit wins (no silent data loss). Delete vs delete: no-op. Sidecar delete is always permitted (it's how the user resolves a conflict from the UI).
|
||||
|
||||
### Transport
|
||||
|
||||
- Extension background has `host_permissions: ["https://*/*"]` and bypasses CORS. No git host sets CORS headers on smart-HTTP endpoints; this is the workaround. A user-configurable CORS proxy URL exists as a fallback.
|
||||
- Firefox `browser.scripting.executeScript` `func` is typed as returning void in `@types/firefox-webext-browser`. The real API serialises return values into `InjectionResult.result`. The wrapper `execInTab` in popup.ts keeps a typed return path.
|
||||
|
||||
## Style
|
||||
|
||||
- Keep popups type-checked under `noUncheckedIndexedAccess`. When iterating `Uint8Array` via index, use `!` or restructure.
|
||||
- Don't add content scripts unless there's a reason — v1 autofill is explicitly popup-driven (user must click Autofill button), not passive.
|
||||
- The `api` object in `src/common/messages.ts` is the only way the UI talks to background; keep message shapes in `src/common/types.ts`.
|
||||
|
||||
## Things that are out of scope (v1)
|
||||
|
||||
- Content-script autofill on page load, context-menu fill, form heuristics beyond the simple one in `autofill.ts`.
|
||||
- Password generator UI beyond the `↻ gen` button in the edit form.
|
||||
- TOTP, secure notes, attachments.
|
||||
- Salt rotation / re-encryption.
|
||||
- Chrome port (should mostly work; untested).
|
||||
- SSH transport (impossible from a browser).
|
||||
113
README.md
Normal file
113
README.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# 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.
|
||||
Loading…
Reference in New Issue
Block a user