summaryrefslogtreecommitdiff
path: root/internal/migrate/kdbx_store.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/migrate/kdbx_store.go')
-rw-r--r--internal/migrate/kdbx_store.go105
1 files changed, 105 insertions, 0 deletions
diff --git a/internal/migrate/kdbx_store.go b/internal/migrate/kdbx_store.go
new file mode 100644
index 0000000..d2ad900
--- /dev/null
+++ b/internal/migrate/kdbx_store.go
@@ -0,0 +1,105 @@
+// Package migrate provides the geheim→KeePass migration logic for foostore.
+// The CLI layer (internal/cli) is responsible only for flag parsing and exit
+// codes; all migration business logic lives here.
+package migrate
+
+import (
+ "fmt"
+ "os"
+
+ gokeepasslib "github.com/tobischo/gokeepasslib/v3"
+
+ "codeberg.org/snonux/foostore/internal/keepass"
+)
+
+// KDBXStore is the minimal interface needed by the migrator to write entries
+// into a KeePass database. Keeping it small satisfies the Interface Segregation
+// Principle and allows easy substitution in tests.
+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
+}
+
+// kdbxStore wraps an in-memory gokeepasslib.Database and the file path it will
+// be written to on Save().
+type kdbxStore struct {
+ path string
+ db *gokeepasslib.Database
+}
+
+// OpenKDBXStore opens an existing KDBX database using password credentials,
+// decodes and unlocks protected entries, and returns a ready-to-use KDBXStore.
+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)
+ }
+
+ ensureRootGroup(db)
+ return &kdbxStore{path: dbPath, db: db}, nil
+}
+
+// ensureRootGroup guarantees that the database has a valid Content, Root, and
+// at least one top-level Group so callers never have to nil-check these fields.
+func ensureRootGroup(db *gokeepasslib.Database) {
+ 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)
+ }
+}
+
+// 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.
+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.
+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.
+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)
+}