Add the secondary-persona path from docs/BROWSER-ROTATION.md: ingest a password manager / browser store, take a mandatory verified sealed backup, generate fresh strong passwords, hand the human a ready-to-finish task at the MFA wall, and commit the new value into the manager only after the site change is confirmed. - internal/pwgen: crypto/rand generator (rejection sampling, per-class guarantee, vault-native — never returns plaintext as a Go string). - internal/pwstore: Manager/ItemUpdater/BulkImporter contracts, secrets-free RedactIdentity, and five adapters — bitwarden (bw, stdin), keepassxc (keepassxc-cli, stdin), 1password (op, argv assignment with documented caveat), chrome/firefox (tmpfs CSV ingest-and-shred). All real code; validation status is data (MOCK-ONLY against fake binaries/CSVs, recorded in the design doc). - internal/pwstore/stage.go: age-sealed staged-list (WriteStaged/ReadStaged) + StagedImporter — the only artifacts crossing the plan->commit gap, never plaintext. - cmd/incredigo: passwords scan|plan|guide|commit. Backup gate seals + round-trip verifies before any commit; guide is interactive verify-before-commit; commit is headless over a sealed stage. Browser CSV writes are gated behind --allow-csv. Hard rules honored: backup-before-commit, verify-before-commit, no plaintext on disk (browser CSV is the flag-gated tmpfs+shred exception), MFA always a human handoff. go vet + -race clean; end-to-end verified against a fake bw. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 KiB
Browser / password-manager rotation — design spec
This is the design for incredigo's secondary-persona path: rotating the passwords a non-technical user has reused across dozens of sites and saved in a browser or password manager. It extends the same safe spine as the developer-credential drivers (discover → custody → mandatory backup → rotate/verify/commit → guide) — it is not a new product.
Status: Phase B (propagation engine) — built. pwgen + pwstore (all five adapters) +
the sealed staged-list + the incredigo passwords scan|plan|guide|commit surface are
implemented and tested (adapters MOCK-ONLY against fake binaries/CSVs — see §8). Phases
A-tier-1 / A-tier-2 (site automation) are specified here but not yet built. Nothing in this
doc changes a password at a website automatically — the human still performs the site
change + MFA; incredigo only propagates the new value into the manager afterward.
1. The reframe: two sub-problems, very different difficulty
A naïve "automate browser password rotation" conflates two jobs. Keeping them separate is the whole reason this design is tractable where Dashlane's was not.
| A — the site-side change | B — the manager-side propagation | |
|---|---|---|
| What | Go to each website, authenticate, find the change-password form, submit old→new | Update the password manager's stored entry with the new value |
| Why hard | MFA, CAPTCHA, email/SMS confirmation, per-site DOM variance, lockout risk | Mostly a solved problem via the manager's CLI/API |
| Automatable? | Partially, with a hard human-handoff ceiling | Yes, safely |
The throughput of the whole pipeline is capped by A, not B. So Phase B builds B end-to-end (high value, safe) with the human as the site+MFA actor, and later phases chip away at A behind explicit opt-in. We never let B's success imply A is solved.
2. The user-visible model: "everything up to MFA, then make MFA easy"
incredigo does every safe, deterministic step and then hands a ready-to-finish task to the human at exactly the MFA wall:
- Ingest the existing store (manager CLI/API or browser CSV) into the RAM vault.
- Backup (mandatory, verified, sealed) the old store before anything changes.
- Generate a fresh strong password per account → the staged "separate list" (held in the vault, never written to disk in plaintext).
- Guided handoff — for each account present: the change-password link, the new password (revealed/copied locally on demand), and per-site MFA instructions. The human performs the site change + completes MFA/CAPTCHA. incredigo never bypasses either.
- Commit/merge — only after the change succeeds, write the new password back into the manager: in-place by item ID for CLI managers; CSV re-import (flag-gated) for browser stores.
In Phase B step 4 is fully manual (incredigo opens the link + reveals the new password). In later phases step 4 is automated up to the MFA wall, where it still hands off to the human.
3. Hard-rule reconciliation (this path touches real passwords)
The project's hard rules constrain this design tightly. The collisions and resolutions:
Rule 3 — "No plaintext secrets on disk. Ever." (escape hatch: flag-gated raw export)
- CLI managers (Bitwarden
bw, 1Passwordop, KeePassXCkeepassxc-cli) read and write individual items over stdin/stdout — secrets stream as bytes into the RAM vault and back out per item by ID. No plaintext file is ever created. This is the preferred path. - Browser-native stores (Chrome, Firefox) have no import API; the only path is a
plaintext CSV the browser reads off disk. This is the one unavoidable plaintext-on-disk
moment, so it is:
- flag-gated + loudly warned (rule 3's sanctioned escape hatch),
- materialized on tmpfs (
/dev/shm) so it never lands on stable storage, - securely shredded (random+zero overwrite → fsync → unlink) the instant the browser finishes, with the window kept as narrow as possible,
- audited (the act, never the value).
- Honesty note: shred is best-effort on SSD/journaling/CoW filesystems; tmpfs avoids the durability problem entirely, which is why we prefer it.
- The staged "separate list" of new passwords lives only in the vault for the session. If it
must survive an interactive session it is sealed with the existing
Sealer(age), never written as plaintext — same rule as backups. - The worklist
.mdstays secrets-free (site, username, link, status only). New passwords appear only in the interactive TUI / clipboard, never in any file.
Rule 1 — backup before rotate
The mandatory rotate.Snapshot gate runs over the ingested store before any commit. No
verified sealed backup → no commit. The backup is also the rollback if a half-rotation occurs.
Rule 2 — verify-new-before-revoke-old, and the browser-specific lockout hazard
For website passwords the site itself invalidates the old password the instant the new one is set — we don't control that ordering. So the safe sequence per account is:
change at site → re-login with NEW pw in the same session to verify → only then commit to manager
If the change can't be verified (MFA loop, confirmation email, ambiguous result) incredigo keeps the old value in the manager and flags "needs human" — it never leaves an account half-rotated, and never risks locking the user out (do-no-harm, rule 5/6). In Phase B the human asserts success explicitly ("the site accepted it") before commit.
Rule 5/6 — self-owned only, never bypass MFA/CAPTCHA, audited, reversible
Only the user's own accounts, with their authorization. MFA/CAPTCHA are always a human handoff. Every action is dry-run-able and audited (redacted).
4. Package layout (one-file-each, mirrors discover/sink)
internal/pwgen/ strong password generator (crypto/rand, policy, vault-native)
pwgen.go
internal/pwstore/ password-manager adapters + the propagation engine
pwstore.go Manager interface, Account, registry, CSV + shred + redaction helpers
bitwarden.go bw CLI — in-place UpdatePassword by item id
onepassword.go op CLI — in-place UpdatePassword by item id
keepassxc.go keepassxc-cli — in-place UpdatePassword by entry path
chromecsv.go Chrome CSV ingest-and-shred (BulkImporter)
firefoxcsv.go Firefox CSV ingest-and-shred (BulkImporter)
The Manager contract
// Account is a single login. The password is a vault handle — never plaintext.
type Account struct {
ID string // manager-native item id / entry path ("" for CSV rows)
Site string // hostname used for the change-password link
URL string // full login URL if the store has one
Username string
Secret *vault.Handle // OLD password, in the RAM vault
Meta map[string]string // non-secret extras (folder, note title, …)
}
type Manager interface {
Name() string
Available() bool // is the CLI installed / the export present?
// Export reads all logins into the vault, passwords as handles.
Export(ctx context.Context, v *vault.Vault) ([]Account, error)
}
// In-place updaters (CLI managers) implement this — the safe, no-plaintext-file path.
type ItemUpdater interface {
UpdatePassword(ctx context.Context, v *vault.Vault, acct Account, newPassword *vault.Handle) error
}
// Bulk importers (browser CSV stores) implement this instead — flag-gated, shredded.
type BulkImporter interface {
Import(ctx context.Context, v *vault.Vault, accts []Account) error
}
Every adapter takes an injectable Bin (CLI managers) or Path (CSV stores) so it is unit
tested against a fake binary / temp CSV — identical to the sink.Gopass{Bin:…} and
driver HTTPClient test pattern. Adapters contain only real code; validation status
(does this work against the real bw/op/Chrome?) is tracked as data, exactly like the
rotation drivers' proofs.go.
The generator
type Policy struct {
Length int // default 20
Upper, Lower bool // default true
Digits, Symbols bool // default true
ExcludeAmbiguous bool // drop O/0/l/1/I to survive hand-retyping
SymbolSet string // override per-site (some sites reject specific symbols)
}
// Generate builds a password with crypto/rand (rejection sampling, no modulo bias),
// guarantees ≥1 char from each enabled class, and stores it straight into the vault —
// it never returns the plaintext as a Go string.
func Generate(v *vault.Vault, p Policy) (*vault.Handle, error)
5. Command surface (implemented)
incredigo passwords scan # list logins (redacted) + change-link per account; no secrets, no commit
incredigo passwords plan # ingest + verified backup + generate + seal staged list (--stage-out) + secrets-free worklist (--worklist-out)
incredigo passwords guide # interactive (TTY): per account → link + reveal new pw + MFA handoff → confirm → commit
incredigo passwords commit # headless: open sealed --stage-in, re-verify backup, commit --verified <1,2,…|all>
Shared flags: --manager bitwarden|1password|keepassxc|chrome|firefox selects the backend;
--export-path <csv> feeds chrome/firefox; --kdbx <db> feeds keepassxc (passphrase from
$INCREDIGO_KDBX_PASSPHRASE or a no-echo prompt); --allow-csv is the loud opt-in required
for the browser-CSV plaintext path; --length/--exclude-ambiguous/--symbols tune the generator;
$INCREDIGO_PASSPHRASE enables the headless (no-TTY) backup/seal flows.
Both the backup (pwBackupGate) and the staged new-password list are sealed with the
existing Sealer (age by default) and round-trip-verified — they are the only artifacts that
persist across the plan→commit gap, and they are never plaintext on disk. --verified indexes
are 1-based and match the worklist row numbers / the staged-list order from the same plan.
6. Audit
New redacted audit.Entry.Action values: pw-scan, pw-export, pw-stage, pw-commit,
pw-import. Identity is the redacted site / us…@…. No password, ever. The CSV-path
entries additionally record that a flag-gated plaintext file was created and shredded.
7. Honest ceiling (carried from the strategy discussion)
- MFA is on every account that matters → the realistic fully-automated set (later phases) is low-value, no-MFA, simple-form sites. Phase B sidesteps this by keeping the human in the loop for the site change.
- Chrome CSV import duplicates rather than updates (it appends), so the browser-CSV merge produces dupes a human must reconcile; the clean in-place merge is the CLI-manager path. This is documented, not hidden.
- Fully-silent mass browser rotation stays a non-goal (VISION) — the known Dashlane tar pit. incredigo automates up to the MFA wall and guides the rest.
8. Validation status (DATA, mirrors rotate/proofs.go philosophy)
The adapters contain ONLY real CLI/CSV code — no test branches. What differs is how each
write path has been validated. As of the Phase-B build every adapter is MOCK-ONLY:
exercised against a fake bw/op/keepassxc-cli binary (state-dir scripts) or a fake
export/import CSV, never yet against the real manager binary or a real browser re-import.
| Adapter | Write shape | Secret path off-argv? | Validated against |
|---|---|---|---|
| bitwarden | bw edit item (stdin) |
yes — base64 on stdin | fake bw (MOCK-ONLY) |
| keepassxc | keepassxc-cli edit -p |
yes — db+new pass on stdin | fake keepassxc-cli (MOCK-ONLY) |
| 1password | op item edit pw=… |
no — argv assignment¹ | fake op (MOCK-ONLY) |
| chrome | tmpfs CSV → human re-import | n/a (no live secret to a child) | fake CSV (MOCK-ONLY) |
| firefox | tmpfs CSV → human re-import | n/a | fake CSV (MOCK-ONLY) |
¹ 1Password argv caveat: op item edit has no stdin-per-field path (only op item create
does), and the JSON-template alternative would write plaintext to disk (hard rule 3). The
remaining real path is op item edit <id> password=<new>, so the new value is visible to
same-uid processes via /proc/<pid>/cmdline for the brief op exec. This is strictly weaker
than the Bitwarden/KeePassXC stdin paths; it is in-RAM, transient, and never touches disk. The
choice is documented here rather than hidden in the driver. Promoting any adapter to a
real-binary proof level requires a sandbox-VM POC against the genuine CLI / browser import.
9. Roadmap
- Phase B (now):
pwgen+pwstore(interface + Bitwarden/1Password/KeePassXC in-place + Chrome/Firefox CSV ingest-and-shred) + the staged-list propagation engine +passwordscommands. Human does the site change + MFA; incredigo does everything else. - Phase A-tier-1: deterministic
go-rod/playwright-godriving the curated change-URL table for a handful of high-frequency, no-MFA sites, with verify-before-commit. Measure the real success rate before promising anything. - Phase A-tier-2: Computer-Use / AI-in-browser vision fallback for DOM variance, with secrets kept out of the model loop (the model decides navigation; the Go harness injects the actual password via CDP, redacted from the screenshot stream). Opt-in, measured.