c2f181e56d
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>
75 lines
2.1 KiB
Go
75 lines
2.1 KiB
Go
package pwstore
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"incredigo/internal/vault"
|
|
)
|
|
|
|
func TestFirefoxExportFromCSV(t *testing.T) {
|
|
v := vault.New()
|
|
defer v.Purge()
|
|
// Firefox export header order differs from Chrome; colsFromHeader maps by name.
|
|
path := writeTempCSV(t,
|
|
"url,username,password,httpRealm,formActionOrigin,guid,timeCreated,timeLastUsed,timePasswordChanged\n"+
|
|
"https://github.com/login,alice@example.com,old1,,https://github.com,{g1},0,0,0\n")
|
|
f := &FirefoxCSV{ExportPath: path}
|
|
|
|
if !f.Available() {
|
|
t.Fatal("Available should be true when export file exists")
|
|
}
|
|
accts, err := f.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))
|
|
}
|
|
if accts[0].Site != "github.com" || accts[0].Username != "alice@example.com" {
|
|
t.Errorf("account = %+v", accts[0])
|
|
}
|
|
if pw := handleStr(t, v, accts[0].Secret); pw != "old1" {
|
|
t.Errorf("password = %q, want old1", pw)
|
|
}
|
|
}
|
|
|
|
func TestFirefoxBareImportRefuses(t *testing.T) {
|
|
f := &FirefoxCSV{}
|
|
if err := f.Import(context.Background(), vault.New(), nil); err == nil {
|
|
t.Error("bare Import must refuse and direct callers to ImportStaged")
|
|
}
|
|
}
|
|
|
|
func TestFirefoxImportStagedFirefoxLayout(t *testing.T) {
|
|
v := vault.New()
|
|
defer v.Purge()
|
|
f := &FirefoxCSV{}
|
|
accts := []Account{
|
|
{URL: "https://a.test/", Username: "u1", Secret: v.Store([]byte("old1")), Meta: map[string]string{"name": "A"}},
|
|
}
|
|
newPw := map[int]*vault.Handle{0: v.Store([]byte("NEW-ff-1"))}
|
|
|
|
path, cleanup, err := f.ImportStaged(v, accts, newPw)
|
|
if err != nil {
|
|
t.Fatalf("ImportStaged: %v", err)
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read staged csv: %v", err)
|
|
}
|
|
s := string(raw)
|
|
if !strings.HasPrefix(s, "url,username,password") {
|
|
t.Errorf("staged csv should use Firefox header:\n%s", s)
|
|
}
|
|
if !strings.Contains(s, "NEW-ff-1") {
|
|
t.Errorf("staged csv missing new password:\n%s", s)
|
|
}
|
|
cleanup()
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
t.Errorf("staged csv should be shredded+removed, stat err = %v", err)
|
|
}
|
|
}
|