Files
leetcrypt 851890b119 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>
2026-06-20 11:39:20 -07:00

155 lines
4.5 KiB
Go

package pwstore
import (
"context"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"incredigo/internal/vault"
)
// fakePassBin writes a `pass`/`gopass` stand-in backed by a store directory of
// `<entry>.gpg` files whose plaintext content IS the entry body (line 1 = password).
// It implements just the three subcommands the adapter uses: `show <entry>` (cat the
// file), `insert -m -f <entry>` (overwrite the file from stdin), and `ls --flat` (list
// entry names). The store dir is passed via $PASSWORD_STORE_DIR so both flavors find it.
func fakePassBin(t *testing.T) (bin, store string) {
t.Helper()
if runtime.GOOS == "windows" {
t.Skip("fake pass binary is a POSIX shell script")
}
store = filepath.Join(t.TempDir(), "store")
if err := os.MkdirAll(filepath.Join(store, "web"), 0o700); err != nil {
t.Fatal(err)
}
// github.com/alice: password + login + url metadata lines.
body := "old-secret\nlogin: alice@example.com\nurl: https://github.com/login\n"
if err := os.WriteFile(filepath.Join(store, "web", "github.com.gpg"), []byte(body), 0o600); err != nil {
t.Fatal(err)
}
bin = filepath.Join(t.TempDir(), "passbin")
script := `#!/usr/bin/env bash
set -euo pipefail
STORE="${PASSWORD_STORE_DIR:?need PASSWORD_STORE_DIR}"
cmd="${1:-}"
case "$cmd" in
show)
entry="$2"
cat "$STORE/$entry.gpg"
;;
insert)
# insert -m -f <entry> ; body on stdin
entry="${@: -1}"
mkdir -p "$(dirname "$STORE/$entry.gpg")"
cat > "$STORE/$entry.gpg"
;;
ls)
# ls --flat
( cd "$STORE" && find . -name '*.gpg' | sed -e 's#^\./##' -e 's#\.gpg$##' )
;;
*)
echo "fake pass: unknown command: $*" >&2; exit 2
;;
esac
`
if err := os.WriteFile(bin, []byte(script), 0o700); err != nil {
t.Fatal(err)
}
return bin, store
}
func TestPassStoreExport(t *testing.T) {
for _, flavor := range []string{"pass", "gopass"} {
t.Run(flavor, func(t *testing.T) {
bin, store := fakePassBin(t)
t.Setenv("PASSWORD_STORE_DIR", store) // the fake bin (and pass walk) read this
v := vault.New()
defer v.Purge()
p := &PassStore{Bin: bin, Flavor: flavor, Store: store}
if !p.Available() {
t.Fatal("Available should be true with injected bin")
}
accts, err := p.Export(context.Background(), v)
if err != nil {
t.Fatalf("Export: %v", err)
}
if len(accts) != 1 {
t.Fatalf("got %d accounts, want 1", len(accts))
}
a := accts[0]
if a.ID != "web/github.com" {
t.Errorf("entry path = %q, want web/github.com", a.ID)
}
if a.Username != "alice@example.com" {
t.Errorf("username = %q, want alice@example.com", a.Username)
}
if a.Site != "github.com" {
t.Errorf("site = %q, want github.com", a.Site)
}
if a.Meta["group"] != "web" {
t.Errorf("group = %q, want web", a.Meta["group"])
}
if pw := handleStr(t, v, a.Secret); pw != "old-secret" {
t.Errorf("password = %q, want old-secret", pw)
}
})
}
}
func TestPassStoreUpdatePreservesMetadata(t *testing.T) {
bin, store := fakePassBin(t)
t.Setenv("PASSWORD_STORE_DIR", store)
v := vault.New()
defer v.Purge()
p := &PassStore{Bin: bin, Flavor: "pass", Store: store}
accts, err := p.Export(context.Background(), v)
if err != nil {
t.Fatalf("Export: %v", err)
}
newPw := v.Store([]byte("NEW-pass-pw-7"))
if err := p.UpdatePassword(context.Background(), v, accts[0], newPw); err != nil {
t.Fatalf("UpdatePassword: %v", err)
}
raw, err := os.ReadFile(filepath.Join(store, "web", "github.com.gpg"))
if err != nil {
t.Fatal(err)
}
s := string(raw)
// New password on line 1, OLD gone, metadata lines preserved.
if got := s[:len("NEW-pass-pw-7")]; got != "NEW-pass-pw-7" {
t.Errorf("first line = %q, want the new password", got)
}
if want := "login: alice@example.com"; !strings.Contains(s, want) {
t.Errorf("metadata dropped: %q missing from:\n%s", want, s)
}
if strings.Contains(s, "old-secret") {
t.Errorf("old password still present:\n%s", s)
}
}
func TestPassStorePrefixScope(t *testing.T) {
bin, store := fakePassBin(t)
t.Setenv("PASSWORD_STORE_DIR", store)
// Add an out-of-scope entry at the root.
if err := os.WriteFile(filepath.Join(store, "root-note.gpg"), []byte("x\n"), 0o600); err != nil {
t.Fatal(err)
}
v := vault.New()
defer v.Purge()
p := &PassStore{Bin: bin, Flavor: "pass", Store: store, Prefix: "web"}
accts, err := p.Export(context.Background(), v)
if err != nil {
t.Fatalf("Export: %v", err)
}
if len(accts) != 1 || accts[0].ID != "web/github.com" {
t.Fatalf("prefix scope failed: got %+v", accts)
}
}