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
+13 -6
View File
@@ -35,11 +35,13 @@ func colsFromHeader(header []string) csvCols {
}
return csvCols{
name: idx("name", "title"),
url: idx("url", "login_uri", "web site", "website"),
username: idx("username", "login_username", "user"),
password: idx("password", "login_password", "pass"),
note: idx("note", "notes"),
group: idx("group"), // KeePassXC export column; absent elsewhere (-1)
url: idx("url", "login_uri", "web site", "website", "matchurl"),
username: idx("username", "login_username", "user", "login"),
password: idx("password", "login_password", "pass", "pwd"),
note: idx("note", "notes", "extra"),
// group: KeePassXC "group"; LastPass "grouping"; Dashlane "category";
// NordPass/RoboForm "folder"; Proton "vault". Absent elsewhere (-1).
group: idx("group", "grouping", "category", "folder", "vault"),
}
}
@@ -85,6 +87,11 @@ func parseLoginCSV(v *vault.Vault, data []byte) ([]Account, error) {
if g := get(rec, cols.group); g != "" {
a.Meta["group"] = g
}
// Preserve notes so a manager round-trip (export -> import CSV) does not wipe
// the user's existing note on the entry. Notes are non-secret metadata.
if nt := get(rec, cols.note); nt != "" {
a.Meta["note"] = nt
}
pw := get(rec, cols.password)
a.Secret = v.Store([]byte(pw))
out = append(out, a)
@@ -110,7 +117,7 @@ type csvLayout struct {
var chromeLayout = csvLayout{
header: chromeCSVHeader,
record: func(a Account, pw string) []string {
return []string{a.Meta["name"], a.URL, a.Username, pw, ""}
return []string{a.Meta["name"], a.URL, a.Username, pw, a.Meta["note"]}
},
}