Preserve single-root KDBX group trees
This commit is contained in:
+87
-7
@@ -24,6 +24,7 @@ type Manager struct {
|
||||
config *vault.KDBXConfig
|
||||
path string
|
||||
key vault.MasterKey
|
||||
vaultRoot string
|
||||
locked bool
|
||||
encoded []byte
|
||||
remoteClient *webdav.Client
|
||||
@@ -32,6 +33,8 @@ type Manager struct {
|
||||
}
|
||||
|
||||
func (m *Manager) Create(model vault.Model, key vault.MasterKey) error {
|
||||
root := detectSingleVaultRoot(model)
|
||||
model = normalizeUnderRoot(model, root)
|
||||
var encoded bytes.Buffer
|
||||
if err := vault.SaveKDBXWithConfigAndKey(&encoded, model, key, m.config); err != nil {
|
||||
return fmt.Errorf("encode new vault: %w", err)
|
||||
@@ -39,6 +42,7 @@ func (m *Manager) Create(model vault.Model, key vault.MasterKey) error {
|
||||
|
||||
m.model = model
|
||||
m.key = key
|
||||
m.vaultRoot = root
|
||||
m.encoded = encoded.Bytes()
|
||||
m.locked = false
|
||||
return nil
|
||||
@@ -71,6 +75,7 @@ func (m *Manager) Open(path string, key vault.MasterKey) error {
|
||||
m.config = config
|
||||
m.path = path
|
||||
m.key = key
|
||||
m.vaultRoot = detectSingleVaultRoot(model)
|
||||
m.encoded = content
|
||||
m.locked = false
|
||||
return nil
|
||||
@@ -102,6 +107,7 @@ func (m *Manager) OpenRemote(client webdav.Client, path string, key vault.Master
|
||||
m.model = model
|
||||
m.config = config
|
||||
m.key = key
|
||||
m.vaultRoot = detectSingleVaultRoot(model)
|
||||
m.encoded = content
|
||||
m.locked = false
|
||||
m.remoteClient = &client
|
||||
@@ -162,10 +168,11 @@ func (m *Manager) SynchronizeToLocal(path string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
merged = normalizeUnderRoot(merged, m.vaultRoot)
|
||||
if err := saveModelToLocal(path, merged, m.key, configOrCurrent(config, m.config)); err != nil {
|
||||
return err
|
||||
}
|
||||
m.model = merged
|
||||
m.model = normalizeUnderRoot(merged, m.vaultRoot)
|
||||
m.locked = false
|
||||
return nil
|
||||
}
|
||||
@@ -191,10 +198,11 @@ func (m *Manager) SynchronizeToRemote(client webdav.Client, path string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
merged = normalizeUnderRoot(merged, m.vaultRoot)
|
||||
if err := saveModelToRemote(client, path, merged, m.key, configOrCurrent(config, m.config), version); err != nil {
|
||||
return err
|
||||
}
|
||||
m.model = merged
|
||||
m.model = normalizeUnderRoot(merged, m.vaultRoot)
|
||||
m.locked = false
|
||||
return nil
|
||||
}
|
||||
@@ -209,7 +217,12 @@ func (m *Manager) SaveAs(path string) error {
|
||||
}
|
||||
|
||||
func (m *Manager) Replace(model vault.Model) {
|
||||
m.model = model
|
||||
root := m.vaultRoot
|
||||
if root == "" {
|
||||
root = detectSingleVaultRoot(model)
|
||||
}
|
||||
m.model = normalizeUnderRoot(model, root)
|
||||
m.vaultRoot = root
|
||||
m.locked = false
|
||||
}
|
||||
|
||||
@@ -227,7 +240,8 @@ func (m *Manager) Lock() error {
|
||||
}
|
||||
|
||||
var encoded bytes.Buffer
|
||||
if err := vault.SaveKDBXWithConfigAndKey(&encoded, m.model, m.key, m.config); err != nil {
|
||||
model := normalizeUnderRoot(m.model, m.vaultRoot)
|
||||
if err := vault.SaveKDBXWithConfigAndKey(&encoded, model, m.key, m.config); err != nil {
|
||||
return fmt.Errorf("encode vault for lock: %w", err)
|
||||
}
|
||||
|
||||
@@ -246,6 +260,7 @@ func (m *Manager) Unlock(key vault.MasterKey) error {
|
||||
m.model = model
|
||||
m.config = config
|
||||
m.key = key
|
||||
m.vaultRoot = detectSingleVaultRoot(model)
|
||||
m.locked = false
|
||||
return nil
|
||||
}
|
||||
@@ -303,9 +318,13 @@ func (m *Manager) persistableBytes() ([]byte, error) {
|
||||
if m.locked {
|
||||
return append([]byte(nil), m.encoded...), nil
|
||||
}
|
||||
model, err := m.currentModelForPersistence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var encoded bytes.Buffer
|
||||
if err := vault.SaveKDBXWithConfigAndKey(&encoded, m.model, m.key, m.config); err != nil {
|
||||
if err := vault.SaveKDBXWithConfigAndKey(&encoded, model, m.key, m.config); err != nil {
|
||||
return nil, fmt.Errorf("encode vault: %w", err)
|
||||
}
|
||||
return encoded.Bytes(), nil
|
||||
@@ -336,6 +355,7 @@ func (m *Manager) synchronizeLocal() error {
|
||||
}
|
||||
|
||||
merged := mergeModels(base, current, latest)
|
||||
merged = normalizeUnderRoot(merged, m.vaultRoot)
|
||||
var encoded bytes.Buffer
|
||||
if err := vault.SaveKDBXWithConfigAndKey(&encoded, merged, m.key, config); err != nil {
|
||||
return fmt.Errorf("encode synchronized vault: %w", err)
|
||||
@@ -373,6 +393,7 @@ func (m *Manager) synchronizeRemote() error {
|
||||
}
|
||||
|
||||
merged := mergeModels(base, current, latest)
|
||||
merged = normalizeUnderRoot(merged, m.vaultRoot)
|
||||
var encoded bytes.Buffer
|
||||
if err := vault.SaveKDBXWithConfigAndKey(&encoded, merged, m.key, config); err != nil {
|
||||
return fmt.Errorf("encode synchronized remote vault: %w", err)
|
||||
@@ -393,9 +414,13 @@ func (m *Manager) synchronizeRemote() error {
|
||||
|
||||
func (m *Manager) currentModelForPersistence() (vault.Model, error) {
|
||||
if m.locked {
|
||||
return vault.LoadKDBXWithKey(bytes.NewReader(m.encoded), m.key)
|
||||
model, err := vault.LoadKDBXWithKey(bytes.NewReader(m.encoded), m.key)
|
||||
if err != nil {
|
||||
return vault.Model{}, err
|
||||
}
|
||||
return normalizeUnderRoot(model, m.vaultRoot), nil
|
||||
}
|
||||
return m.model, nil
|
||||
return normalizeUnderRoot(m.model, m.vaultRoot), nil
|
||||
}
|
||||
|
||||
func (m *Manager) baseModel() (vault.Model, error) {
|
||||
@@ -418,6 +443,7 @@ func (m *Manager) mergedWithPeer(other vault.Model) (vault.Model, error) {
|
||||
}
|
||||
|
||||
func (m *Manager) persistMergedToCurrentSource(merged vault.Model) error {
|
||||
merged = normalizeUnderRoot(merged, m.vaultRoot)
|
||||
switch {
|
||||
case m.remoteClient != nil && m.remotePath != "":
|
||||
if err := saveModelToRemote(*m.remoteClient, m.remotePath, merged, m.key, configOrCurrent(m.config, nil), m.remoteVersion); err != nil {
|
||||
@@ -435,17 +461,22 @@ func (m *Manager) persistMergedToCurrentSource(merged vault.Model) error {
|
||||
}
|
||||
|
||||
func (m *Manager) reloadCurrentLocal(merged vault.Model) error {
|
||||
merged = normalizeUnderRoot(merged, m.vaultRoot)
|
||||
encoded, err := encodeModelWithConfig(merged, m.key, configOrCurrent(m.config, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.model = merged
|
||||
if root := detectSingleVaultRoot(merged); root != "" {
|
||||
m.vaultRoot = root
|
||||
}
|
||||
m.encoded = encoded
|
||||
m.locked = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) reloadCurrentRemote(merged vault.Model) error {
|
||||
merged = normalizeUnderRoot(merged, m.vaultRoot)
|
||||
encoded, err := encodeModelWithConfig(merged, m.key, configOrCurrent(m.config, nil))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -455,6 +486,9 @@ func (m *Manager) reloadCurrentRemote(merged vault.Model) error {
|
||||
return fmt.Errorf("reopen remote %s after synchronize: %w", m.remotePath, err)
|
||||
}
|
||||
m.model = merged
|
||||
if root := detectSingleVaultRoot(merged); root != "" {
|
||||
m.vaultRoot = root
|
||||
}
|
||||
m.encoded = encoded
|
||||
m.remoteVersion = version
|
||||
m.locked = false
|
||||
@@ -716,6 +750,52 @@ func mergePeerGroups(primary, secondary [][]string) [][]string {
|
||||
return out
|
||||
}
|
||||
|
||||
func detectSingleVaultRoot(model vault.Model) string {
|
||||
if len(model.EntriesInPath(nil)) != 0 {
|
||||
return ""
|
||||
}
|
||||
groups := model.ChildGroups(nil)
|
||||
if len(groups) != 1 {
|
||||
return ""
|
||||
}
|
||||
return groups[0]
|
||||
}
|
||||
|
||||
func normalizeUnderRoot(model vault.Model, root string) vault.Model {
|
||||
if root == "" {
|
||||
return model
|
||||
}
|
||||
|
||||
out := cloneModel(model)
|
||||
normalizePath := func(path []string) []string {
|
||||
switch {
|
||||
case len(path) == 0:
|
||||
return []string{root}
|
||||
case path[0] == root:
|
||||
return path
|
||||
default:
|
||||
return append([]string{root}, path...)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range out.Entries {
|
||||
out.Entries[i].Path = normalizePath(out.Entries[i].Path)
|
||||
for j := range out.Entries[i].History {
|
||||
out.Entries[i].History[j].Path = normalizePath(out.Entries[i].History[j].Path)
|
||||
}
|
||||
}
|
||||
for i := range out.RecycleBin {
|
||||
out.RecycleBin[i].Path = normalizePath(out.RecycleBin[i].Path)
|
||||
for j := range out.RecycleBin[i].History {
|
||||
out.RecycleBin[i].History[j].Path = normalizePath(out.RecycleBin[i].History[j].Path)
|
||||
}
|
||||
}
|
||||
for i := range out.Groups {
|
||||
out.Groups[i] = normalizePath(out.Groups[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func loadLocalSource(path string, key vault.MasterKey) (vault.Model, *vault.KDBXConfig, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user