Files
incredigo/docs/BROWSER-ROTATION.md
T
leetcrypt 71f1aa4c6f pwstore: keepassxc LIVE-VM proof + root-group entry-path fix
Proven end-to-end (scan→plan→commit→restore-from-backup) against the real
keepassxc-cli 2.7.6 in the incredigo-sbx sandbox VM. The live run surfaced an
adapter bug the fake CSV masked: KeePassXC's CSV "Group" column is the full path
including the root group name (default root is "Passwords"), but keepassxc-cli
addresses entries relative to root (/GitHub, not /Passwords/GitHub). kpEntryPath
now drops the leading root segment; the unit test that encoded the buggy
/Root/GitHub form is corrected and a nested-group table test added.

Records the proof as data: docs/BROWSER-ROTATION.md promotes keepassxc
MOCK-ONLY -> LIVE-VM (the others stay MOCK-ONLY).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-19 16:57:30 -07:00

14 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 (keepassxc is LIVE-VM proven; the rest MOCK-ONLY — 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:

  1. Ingest the existing store (manager CLI/API or browser CSV) into the RAM vault.
  2. Backup (mandatory, verified, sealed) the old store before anything changes.
  3. Generate a fresh strong password per account → the staged "separate list" (held in the vault, never written to disk in plaintext).
  4. 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.
  5. 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, 1Password op, KeePassXC keepassxc-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 .md stays 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. Most adapters are still MOCK-ONLY (exercised against a fake bw/op binary or a fake export/import CSV); keepassxc is LIVE-VM — proven end to end (scan → plan → commit → restore-from-backup) against the genuine keepassxc-cli 2.7.6 in the incredigo-sbx sandbox VM (lab-provision-keepass.sh).

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 real keepassxc-cli 2.7.6 (LIVE-VM)
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)

keepassxc LIVE-VM finding: the CSV Group column is the FULL group path including the root group name (the default DB names its root Passwords), but keepassxc-cli addresses entries relative to root — a top-level entry is /GitHub, not /Passwords/GitHub. The mock CSV (root group Root) never exposed this; the real CLI 404'd until kpEntryPath was fixed to drop the leading root segment. The recoverability leg (feeding the sealed backup.age back through commit --stage-in) restored both original passwords, proving the backup is a usable restore artifact, not just a sealed blob.

¹ 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 + passwords commands. Human does the site change + MFA; incredigo does everything else.
  • Phase A-tier-1: deterministic go-rod/playwright-go driving 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.