pwstore: generic csvManager for 8 password-manager CSV adapters

One data-driven csvManager (layout = header + record func) covers
edge/brave (chrome layout), safari, lastpass, dashlane, protonpass,
nordpass, roboform, and read/guide-only enpass — replacing 8 near-identical
files. Export is header-name driven (parseLoginCSV + new aliases); import
stages a per-manager-layout CSV on tmpfs and shreds it, gated by --allow-csv.
Notes now round-trip through Meta. Reimporter exposes per-manager re-import
hints. Column layouts are MOCK-ONLY (unit-validated) on the same LIVE-VM
staging machinery; recorded as DATA in BROWSER-ROTATION.md §8.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-20 11:31:28 -07:00
parent 6bd6aefbe8
commit 4f5939a3e3
6 changed files with 467 additions and 21 deletions
+28 -9
View File
@@ -53,15 +53,19 @@ func passwordsCmd() *cobra.Command {
"passwords -> guided handoff (you change the site + complete MFA) -> commit the new\n" +
"value into the manager ONLY after you confirm the site accepted it. MFA/CAPTCHA are\n" +
"always your job; incredigo never bypasses them.\n\n" +
"--manager bitwarden|1password|keepassxc|chrome|firefox selects the backend.\n" +
"CLI managers update one item in place over stdin (no plaintext file). Browser stores\n" +
"can only be re-imported via a CSV, so chrome/firefox require --allow-csv (the file is\n" +
"written to tmpfs and securely shredded).",
"--manager selects the backend:\n" +
" in-place (CLI, no plaintext file): bitwarden | 1password | keepassxc\n" +
" CSV import/export (tmpfs + shred, needs --allow-csv + --export-path):\n" +
" chrome | firefox | edge | brave | safari | lastpass | dashlane |\n" +
" protonpass | nordpass | roboform | enpass(read-only)\n" +
"CLI managers update one item in place over stdin (no plaintext file). CSV stores can\n" +
"only be re-imported via a CSV, so they require --allow-csv (the file is written to\n" +
"tmpfs and securely shredded after you confirm re-import).",
}
pf := c.PersistentFlags()
pf.StringVar(&flagPwManager, "manager", "", "backend: bitwarden|1password|keepassxc|chrome|firefox")
pf.BoolVar(&flagAllowCSV, "allow-csv", false, "allow the flag-gated browser-CSV plaintext path (tmpfs + shred)")
pf.StringVar(&flagExportPath, "export-path", "", "chrome/firefox: CSV you exported from the browser")
pf.StringVar(&flagPwManager, "manager", "", "backend: bitwarden|1password|keepassxc|chrome|firefox|edge|brave|safari|lastpass|dashlane|protonpass|nordpass|roboform|enpass")
pf.BoolVar(&flagAllowCSV, "allow-csv", false, "allow the flag-gated browser/manager-CSV plaintext path (tmpfs + shred)")
pf.StringVar(&flagExportPath, "export-path", "", "CSV managers: the CSV you exported from the browser/manager")
pf.StringVar(&flagKdbxPath, "kdbx", "", "keepassxc: path to the .kdbx database")
c.AddCommand(pwScanCmd(), pwPlanCmd(), pwGuideCmd(), pwCommitCmd())
return c
@@ -106,8 +110,17 @@ func buildManager(v *vault.Vault) (pwstore.Manager, error) {
}
return &pwstore.FirefoxCSV{ExportPath: flagExportPath}, nil
case "":
return nil, fmt.Errorf("--manager is required (bitwarden|1password|keepassxc|chrome|firefox)")
return nil, fmt.Errorf("--manager is required (bitwarden|1password|keepassxc|chrome|firefox|lastpass|dashlane|protonpass|nordpass|roboform|safari|edge|brave|enpass)")
default:
// CSV-based managers (lastpass, dashlane, protonpass, nordpass, roboform, safari,
// edge, brave, enpass) all share the generic csvManager + tmpfs/shred staging.
name := strings.ToLower(strings.TrimSpace(flagPwManager))
if pwstore.IsCSV(name) {
if flagExportPath == "" {
return nil, fmt.Errorf("%s requires --export-path <csv> exported from %s", name, name)
}
return pwstore.NewCSV(name, flagExportPath)
}
return nil, fmt.Errorf("unknown --manager %q", flagPwManager)
}
}
@@ -522,7 +535,13 @@ func pwCommitCmd() *cobra.Command {
defer cleanup()
log.Write(audit.Entry{Action: "pw-import", Source: m.Name(), Location: path,
Outcome: fmt.Sprintf("staged %d account(s); plaintext CSV on tmpfs, will shred", len(sub))})
fmt.Printf("Re-import this CSV into the browser, then press Enter to shred it:\n %s\n", path)
where := "Re-import this CSV into the browser/manager"
if r, ok := m.(pwstore.Reimporter); ok {
if h := r.ReimportHint(); h != "" {
where = h
}
}
fmt.Printf("%s, then press Enter to shred it:\n %s\n", where, path)
if term.IsTerminal(int(os.Stdin.Fd())) {
in := bufio.NewScanner(os.Stdin)
in.Scan()