Files
leetcrypt c2f181e56d passwords: Phase B browser/manager propagation engine (pwgen + pwstore + CLI)
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>
2026-06-19 13:02:18 -07:00

65 lines
2.3 KiB
Go

package pwstore
import (
"context"
"fmt"
"os"
"incredigo/internal/vault"
)
// FirefoxCSV adapts Firefox's about:logins store. Like Chrome it has no programmatic
// import API — the only path is the CSV the user re-imports through about:logins —
// so it is a BulkImporter routed through the same tmpfs + shred staging, gated behind
// the caller's --allow-csv opt-in (hard rule 3). The only difference from Chrome is
// the import column layout (Firefox keys on url/username/password), handled by
// firefoxLayout.
type FirefoxCSV struct {
ExportPath string // CSV the user exported from Firefox (read side)
importPath string // last staged path, for the "now re-import this" instruction
}
func init() { Register(&FirefoxCSV{}) }
func (f *FirefoxCSV) Name() string { return "firefox" }
// Available reports whether a Firefox export CSV was supplied to read from.
func (f *FirefoxCSV) Available() bool {
if f.ExportPath == "" {
return false
}
_, err := os.Stat(f.ExportPath)
return err == nil
}
// Export reads the user-provided Firefox export CSV into Accounts. Firefox's export
// header (url,username,password,…) is resolved by name in colsFromHeader, so the
// shared parseLoginCSV handles it unchanged.
func (f *FirefoxCSV) Export(ctx context.Context, v *vault.Vault) ([]Account, error) {
if f.ExportPath == "" {
return nil, fmt.Errorf("firefox: no export CSV path set")
}
data, err := os.ReadFile(f.ExportPath)
if err != nil {
return nil, fmt.Errorf("firefox: read export: %w", err)
}
return parseLoginCSV(v, data)
}
// Import refuses for the same reason as Chrome: a bare Import would shred the staged
// CSV before the human re-imports it. Use ImportStaged.
func (f *FirefoxCSV) Import(ctx context.Context, v *vault.Vault, accts []Account) error {
return fmt.Errorf("firefox: use ImportStaged — a bare Import would shred the CSV before the browser re-reads it")
}
// ImportStaged writes the new-password CSV (Firefox layout) to tmpfs and returns its
// path plus a cleanup func the caller MUST defer after the human confirms re-import.
func (f *FirefoxCSV) ImportStaged(v *vault.Vault, accts []Account, newPw map[int]*vault.Handle) (path string, cleanup func(), err error) {
path, cleanup, err = writeShreddableCSVLayout(v, accts, newPw, firefoxLayout)
if err != nil {
return "", nil, err
}
f.importPath = path
return path, cleanup, nil
}