pwstore: pass/gopass + macOS Keychain in-place adapters

Group 2 of the manager wire-up — two CLI ItemUpdaters:

  pass/gopass: the user's own password-store as a rotation source. Reads
  line-1=password + key:value metadata via `<bin> show`; updates in place by
  piping the full body to `<bin> insert -m -f` (off-argv), preserving metadata
  across the password swap. gopass enumerates via `ls --flat`, pass by walking
  the store dir; --pass-prefix scopes a subtree.

  keychain: macOS internet passwords via the `security` CLI. Pure-exec (no
  build tag) so it unit-tests cross-platform via a fake bin; Available() is
  false off darwin. Read off-argv (find-internet-password -w); write is
  delete+add with -w on argv (CLI limitation, documented like 1password).

Both MOCK-ONLY (fake-binary unit tests + leak checks); recorded as DATA in
BROWSER-ROTATION.md §8. 153 tests green, vet clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-20 11:39:20 -07:00
parent 4f5939a3e3
commit 851890b119
6 changed files with 796 additions and 3 deletions
+10 -3
View File
@@ -34,6 +34,7 @@ var (
flagAllowCSV bool
flagExportPath string
flagKdbxPath string
flagPassPrefix string
flagPwLength int
flagExcludeAmbig bool
flagSymbolSet string
@@ -54,7 +55,8 @@ func passwordsCmd() *cobra.Command {
"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 selects the backend:\n" +
" in-place (CLI, no plaintext file): bitwarden | 1password | keepassxc\n" +
" in-place (CLI, no plaintext file): bitwarden | 1password | keepassxc |\n" +
" pass | gopass | keychain(macOS)\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" +
@@ -63,10 +65,11 @@ func passwordsCmd() *cobra.Command {
"tmpfs and securely shredded after you confirm re-import).",
}
pf := c.PersistentFlags()
pf.StringVar(&flagPwManager, "manager", "", "backend: bitwarden|1password|keepassxc|chrome|firefox|edge|brave|safari|lastpass|dashlane|protonpass|nordpass|roboform|enpass")
pf.StringVar(&flagPwManager, "manager", "", "backend: bitwarden|1password|keepassxc|pass|gopass|keychain|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")
pf.StringVar(&flagPassPrefix, "pass-prefix", "", "pass/gopass: only rotate entries under this subtree (e.g. logins/)")
c.AddCommand(pwScanCmd(), pwPlanCmd(), pwGuideCmd(), pwCommitCmd())
return c
}
@@ -109,8 +112,12 @@ func buildManager(v *vault.Vault) (pwstore.Manager, error) {
return nil, fmt.Errorf("firefox requires --export-path <csv> (export from about:logins)")
}
return &pwstore.FirefoxCSV{ExportPath: flagExportPath}, nil
case "pass", "gopass":
return &pwstore.PassStore{Flavor: strings.ToLower(strings.TrimSpace(flagPwManager)), Prefix: flagPassPrefix}, nil
case "keychain":
return &pwstore.Keychain{}, nil
case "":
return nil, fmt.Errorf("--manager is required (bitwarden|1password|keepassxc|chrome|firefox|lastpass|dashlane|protonpass|nordpass|roboform|safari|edge|brave|enpass)")
return nil, fmt.Errorf("--manager is required (bitwarden|1password|keepassxc|chrome|firefox|pass|gopass|keychain|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.