1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
|
// Package keepass provides a read-only backend.Backend implementation that
// reads secrets from a KeePass (.kdbx) database. It flattens KeePass groups
// into Description="Group/Subgroup/Title" entries and formats content as
// "Password:...\nUser:...\nURL:...\nNotes:\n..." text.
package keepass
import (
"regexp"
"strings"
)
// Content field patterns — compiled once at package init to avoid
// per-call regex allocation.
var (
passwordPattern = regexp.MustCompile(`(?i)^\s*pass(?:word)?\s*:\s*(.*)\s*$`)
userPattern = regexp.MustCompile(`(?i)^\s*user(?:name)?\s*:\s*(.*)\s*$`)
urlPattern = regexp.MustCompile(`(?i)^\s*url\s*:\s*(.*)\s*$`)
// notesPattern matches the "Notes:" header with nothing after the colon.
// This is intentional: formatContent always emits "Notes:\n" on its own
// line so that everything following the header is treated as multi-line
// notes body. A "Notes: value" form on the same line is not produced by
// formatContent and would be silently ignored during parseContent; that
// case is explicitly unsupported to keep the parser simple.
notesPattern = regexp.MustCompile(`(?i)^\s*notes\s*:\s*$`)
)
// formatContent builds the canonical multi-line text representation for a
// KeePass entry. The stable field order (Password / User / URL / Notes)
// ensures round-trips through parseContent are lossless.
//
// Output format:
//
// Password: <value>
// User: <value>
// URL: <value>
// Notes:
// <notes lines>
func formatContent(password, user, url, notes string) []byte {
var b strings.Builder
b.WriteString("Password: ")
b.WriteString(password)
b.WriteByte('\n')
b.WriteString("User: ")
b.WriteString(user)
b.WriteByte('\n')
b.WriteString("URL: ")
b.WriteString(url)
b.WriteByte('\n')
b.WriteString("Notes:\n")
if notes != "" {
b.WriteString(notes)
if !strings.HasSuffix(notes, "\n") {
b.WriteByte('\n')
}
}
return []byte(b.String())
}
// parseContent is the inverse of formatContent. It tolerates missing or
// reordered fields and handles a multi-line Notes section. Lines that appear
// before a "Notes:" header are matched against the password/user/url patterns;
// everything after "Notes:" is collected verbatim.
//
// This generalises extractPasswordFromContent from internal/cli/migrate_kdbx.go
// to also handle User: and URL: fields.
func parseContent(content []byte) (password, user, url, notes string) {
lines := strings.Split(string(content), "\n")
inNotes := false
var notesLines []string
for _, line := range lines {
if inNotes {
notesLines = append(notesLines, line)
continue
}
if notesPattern.MatchString(line) {
inNotes = true
continue
}
if m := passwordPattern.FindStringSubmatch(line); len(m) == 2 && password == "" {
password = strings.TrimSpace(m[1])
continue
}
if m := userPattern.FindStringSubmatch(line); len(m) == 2 && user == "" {
user = strings.TrimSpace(m[1])
continue
}
if m := urlPattern.FindStringSubmatch(line); len(m) == 2 && url == "" {
url = strings.TrimSpace(m[1])
continue
}
}
notes = strings.TrimRight(strings.Join(notesLines, "\n"), "\n")
return password, user, url, notes
}
|