5.2 KiB
5.2 KiB
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.tsowns state and key material;sync.tsowns git + conflict resolution;crypto.tsis Web Crypto wrappers;fs.tswraps lightning-fs;index.tsis the message router + alarms.src/popup/— main UI.popup.tsis the view;autofill.tsis injected into tabs viabrowser.scripting.executeScriptand must stay self-contained (serialized throughfunc).src/options/— setup and settings page.src/common/— shared types, the typedapiobject for background↔UI messages, andpolyfills.ts(Buffer re-export, injected by esbuild into the background bundle only).
Build & check
npm run build— esbuild bundles the three entry points intobuild/.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. TwoDANGEROUS_EVALwarnings come from bundled deps (pakonew 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 notrefs/heads/main, andpush('main')silently sends the stale ref. AlwayswriteRefmain first, thengit.checkout({ ref: 'main', force: true }). applyPut/applyDeletemust read the remote blob viagit.readBlob(remoteOid, filepath), not from the working tree.vault.put()pre-writes blobs to the workdir for immediate UI feedback, andcheckout -fdoes not remove untracked files. Reading the workdir would treat our own fresh-IV encryption as a divergent remote and spawn phantom conflict sidecars.createVaulthandles three remote states: empty (init + push), hasmainwithout 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 doesrequire('crypto')and breaks browser bundling. The config has a tiny plugin (isogitEsmPlugin) that redirects the bare specifierisomorphic-gittonode_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 usesBuffer.from/Buffer.alloc/Buffer.concatat module scope.
Crypto / session
deriveKeyreturns an extractable CryptoKey (extractable: true). We export raw bytes intobrowser.storage.sessionso 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:
idleCheckfires on a 30 s alarm. Any background message handler callsvault.touchActivity()on success — exceptlockandreset. 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— themodified_atof the version we edited from. If the remote's currentmodified_at === baseModifiedAt, it's a linear edit → overwrite, no sidecar. Only genuine divergence produces a<uuid>.conflict-<ts>.encsidecar. - 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.executeScriptfuncis typed as returning void in@types/firefox-webext-browser. The real API serialises return values intoInjectionResult.result. The wrapperexecInTabin popup.ts keeps a typed return path.
Style
- Keep popups type-checked under
noUncheckedIndexedAccess. When iteratingUint8Arrayvia 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
apiobject insrc/common/messages.tsis the only way the UI talks to background; keep message shapes insrc/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
↻ genbutton in the edit form. - TOTP, secure notes, attachments.
- Salt rotation / re-encryption.
- Chrome port (should mostly work; untested).
- SSH transport (impossible from a browser).