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
|
package cli
import (
"fmt"
"os"
gokeepasslib "github.com/tobischo/gokeepasslib/v3"
"codeberg.org/snonux/foostore/internal/keepass"
)
// KDBXStore is the minimal interface needed by migrate-kdbx.
type KDBXStore interface {
UpsertTextEntry(groupPath []string, title, password, notes string) (overwrote bool, err error)
UpsertBinaryEntry(groupPath []string, title, filename string, content []byte) (overwrote bool, err error)
Save() error
}
type kdbxStore struct {
path string
db *gokeepasslib.Database
}
// OpenKDBXStore opens an existing KDBX database using password credentials.
func OpenKDBXStore(dbPath, password string) (KDBXStore, error) {
f, err := os.Open(dbPath)
if err != nil {
return nil, fmt.Errorf("opening kdbx %q: %w", dbPath, err)
}
defer f.Close()
db := gokeepasslib.NewDatabase()
db.Credentials = gokeepasslib.NewPasswordCredentials(password)
if err := gokeepasslib.NewDecoder(f).Decode(db); err != nil {
return nil, fmt.Errorf("decoding kdbx %q: %w", dbPath, err)
}
if err := db.UnlockProtectedEntries(); err != nil {
return nil, fmt.Errorf("unlocking kdbx %q: %w", dbPath, err)
}
if db.Content == nil {
db.Content = gokeepasslib.NewContent()
}
if db.Content.Root == nil {
db.Content.Root = gokeepasslib.NewRootData()
}
if len(db.Content.Root.Groups) == 0 {
root := gokeepasslib.NewGroup()
root.Name = "Root"
db.Content.Root.Groups = append(db.Content.Root.Groups, root)
}
return &kdbxStore{
path: dbPath,
db: db,
}, nil
}
// UpsertTextEntry creates or updates a text entry in groupPath with the given
// title, password, and notes. Delegates field manipulation to keepass.SetEntryField
// and group navigation to keepass.EnsureGroup to avoid duplication.
func (s *kdbxStore) UpsertTextEntry(groupPath []string, title, password, notes string) (bool, error) {
g := keepass.EnsureGroup(&s.db.Content.Root.Groups[0], groupPath)
entry, overwrote := keepass.UpsertEntryByTitle(g, title)
keepass.SetEntryField(entry, "Title", title)
keepass.SetEntryField(entry, "Password", password)
keepass.SetEntryField(entry, "Notes", notes)
return overwrote, nil
}
// UpsertBinaryEntry creates or updates a binary attachment entry in groupPath.
// Delegates field manipulation to keepass.SetEntryField and group navigation
// to keepass.EnsureGroup to avoid duplication.
func (s *kdbxStore) UpsertBinaryEntry(groupPath []string, title, filename string, content []byte) (bool, error) {
g := keepass.EnsureGroup(&s.db.Content.Root.Groups[0], groupPath)
entry, overwrote := keepass.UpsertEntryByTitle(g, title)
keepass.SetEntryField(entry, "Title", title)
keepass.SetEntryField(entry, "Password", "")
b := s.db.AddBinary(content)
entry.Binaries = []gokeepasslib.BinaryReference{b.CreateReference(filename)}
// Keep notes concise for binary-only entries.
keepass.SetEntryField(entry, "Notes", fmt.Sprintf("Migrated binary attachment: %s", filename))
return overwrote, nil
}
// Save locks protected entries and atomically writes the database to disk.
// Delegates the tmp→encode→rename sequence to keepass.AtomicSave to avoid
// duplicating that logic here (keepass.Backend.save() uses the same helper).
func (s *kdbxStore) Save() error {
if err := s.db.LockProtectedEntries(); err != nil {
return fmt.Errorf("locking kdbx entries: %w", err)
}
return keepass.AtomicSave(s.db, s.path)
}
|