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>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user