Add regression coverage for KDBX reopen cycles

This commit is contained in:
Joe Julian
2026-03-29 11:26:14 -07:00
parent d3be07f252
commit b043ecdc83
3 changed files with 330 additions and 34 deletions
+92 -34
View File
@@ -23,8 +23,8 @@ type KDBXConfig struct {
var ErrInvalidMasterKey = errors.New("invalid master key")
const (
templatesRoot = "Templates"
recycleBinRoot = "Recycle Bin"
templatesRoot = "Templates"
recycleBinRoot = "Recycle Bin"
keepassGOIDField = "KeePassGO-ID"
)
@@ -46,33 +46,29 @@ func SaveKDBXWithConfigAndKey(wr io.Writer, model Model, key MasterKey, config *
return err
}
header := gokeepasslib.NewHeader()
db := gokeepasslib.NewDatabase(gokeepasslib.WithDatabaseKDBXVersion4())
db.Credentials = credentials
db.Content.Meta = gokeepasslib.NewMetaData()
db.Content.Root = &gokeepasslib.RootData{}
if config != nil && config.Header != nil {
header = cloneHeader(config.Header)
db.Header = cloneHeader(config.Header)
db.Hashes = gokeepasslib.NewHashes(db.Header)
}
content := &gokeepasslib.DBContent{
Meta: gokeepasslib.NewMetaData(),
Root: &gokeepasslib.RootData{},
}
if header.IsKdbx4() {
if db.Header.IsKdbx4() {
if config != nil && config.InnerHeader != nil {
content.InnerHeader = cloneInnerHeader(config.InnerHeader)
} else {
content.InnerHeader = &gokeepasslib.InnerHeader{
db.Content.InnerHeader = cloneInnerHeader(config.InnerHeader)
db.Content.InnerHeader.Binaries = nil
} else if db.Content.InnerHeader == nil {
db.Content.InnerHeader = &gokeepasslib.InnerHeader{
InnerRandomStreamID: gokeepasslib.ChaChaStreamID,
InnerRandomStreamKey: randomBytes(64),
}
}
} else {
db.Content.InnerHeader = nil
}
db := &gokeepasslib.Database{
Header: header,
Credentials: credentials,
Content: content,
Hashes: gokeepasslib.NewHashes(header),
}
db.Content.Root.Groups = buildGroupTree(db, entriesForPersistence(model))
db.Content.Root.DeletedObjects = marshalDeletedObjects(model.RecycleBin)
db.Content.Root.Groups = buildGroupTree(db, model)
db.Content.Root.DeletedObjects = nil
if err := db.LockProtectedEntries(); err != nil {
return fmt.Errorf("lock protected entries: %w", err)
@@ -87,20 +83,21 @@ func SaveKDBXWithConfigAndKey(wr io.Writer, model Model, key MasterKey, config *
func appendGroupEntries(model *Model, db *gokeepasslib.Database, group gokeepasslib.Group, path []string) {
path = append(clonePath(path), group.Name)
model.CreateGroup(path[:len(path)-1], group.Name)
for _, entry := range group.Entries {
appendModelEntry(model, Entry{
ID: extractEntryID(entry),
Title: entry.GetTitle(),
Username: entry.GetContent("UserName"),
Password: entry.GetPassword(),
URL: entry.GetContent("URL"),
Notes: entry.GetContent("Notes"),
Tags: splitTags(entry.Tags),
Fields: extractCustomFields(entry),
ID: extractEntryID(entry),
Title: entry.GetTitle(),
Username: entry.GetContent("UserName"),
Password: entry.GetPassword(),
URL: entry.GetContent("URL"),
Notes: entry.GetContent("Notes"),
Tags: splitTags(entry.Tags),
Fields: extractCustomFields(entry),
Attachments: extractAttachments(db, entry),
History: extractHistory(db, entry, path),
Path: clonePath(path),
History: extractHistory(db, entry, path),
Path: clonePath(path),
})
}
@@ -207,7 +204,7 @@ func extractHistory(db *gokeepasslib.Database, entry gokeepasslib.Entry, path []
for _, item := range entry.Histories {
for _, historical := range item.Entries {
history = append(history, Entry{
ID: marshalUUID(historical.UUID),
ID: extractEntryID(historical),
Title: historical.GetTitle(),
Username: historical.GetContent("UserName"),
Password: historical.GetPassword(),
@@ -235,7 +232,8 @@ type MasterKey struct {
KeyFileData []byte
}
func buildGroupTree(db *gokeepasslib.Database, entries []Entry) []gokeepasslib.Group {
func buildGroupTree(db *gokeepasslib.Database, model Model) []gokeepasslib.Group {
entries := entriesForPersistence(model)
root := &groupNode{children: map[string]*groupNode{}}
for _, entry := range entries {
node := root
@@ -250,6 +248,18 @@ func buildGroupTree(db *gokeepasslib.Database, entries []Entry) []gokeepasslib.G
}
node.entries = append(node.entries, entry)
}
for _, path := range groupPathsForPersistence(model, entries) {
node := root
for _, segment := range path {
if node.children[segment] == nil {
node.children[segment] = &groupNode{
name: segment,
children: map[string]*groupNode{},
}
}
node = node.children[segment]
}
}
groups := marshalGroups(db, root)
if len(groups) > 0 {
@@ -261,6 +271,31 @@ func buildGroupTree(db *gokeepasslib.Database, entries []Entry) []gokeepasslib.G
return []gokeepasslib.Group{group}
}
func groupPathsForPersistence(model Model, entries []Entry) [][]string {
seen := map[string]bool{}
var groups [][]string
appendPath := func(path []string) {
key := strings.Join(path, "\x00")
if seen[key] {
return
}
seen[key] = true
groups = append(groups, slices.Clone(path))
}
for _, entry := range entries {
for i := 1; i <= len(entry.Path); i++ {
appendPath(entry.Path[:i])
}
}
for _, path := range model.Groups {
for i := 1; i <= len(path); i++ {
appendPath(path[:i])
}
}
return groups
}
func LoadKDBXWithKey(r io.Reader, key MasterKey) (Model, error) {
model, _, err := LoadKDBXWithConfig(r, key)
return model, err
@@ -407,7 +442,7 @@ func isInvalidCredentialError(err error) bool {
func marshalGroups(db *gokeepasslib.Database, node *groupNode) []gokeepasslib.Group {
names := slices.Collect(maps.Keys(node.children))
slices.Sort(names)
slices.SortFunc(names, compareGroupNames)
var groups []gokeepasslib.Group
for _, name := range names {
@@ -422,6 +457,29 @@ func marshalGroups(db *gokeepasslib.Database, node *groupNode) []gokeepasslib.Gr
return groups
}
func compareGroupNames(a, b string) int {
switch {
case a == b:
return 0
case a == "Root":
return -1
case b == "Root":
return 1
case a == templatesRoot:
return -1
case b == templatesRoot:
return 1
case a == recycleBinRoot:
return 1
case b == recycleBinRoot:
return -1
case a < b:
return -1
default:
return 1
}
}
func marshalEntries(db *gokeepasslib.Database, entries []Entry) []gokeepasslib.Entry {
slices.SortFunc(entries, func(a, b Entry) int {
switch {