Add configurable vault security settings

This commit is contained in:
Joe Julian
2026-03-30 07:58:27 -07:00
parent 84f39b99de
commit b2c26622e8
11 changed files with 458 additions and 47 deletions
+4 -44
View File
@@ -533,48 +533,8 @@ Do not treat the product as complete until all of the following are true:
## Remaining Gaps Against AGENTS.md
This section tracks requirements from `AGENTS.md` that are not fully satisfied by the current landed code, even if the segment work and tests are green.
None currently identified.
### 1. KDBX Security Settings Are Only Preserved, Not Fully Product-Configurable
Evidence:
- `docs/kdbx-compatibility.md` states that KeePassGO preserves the original opened vault's cipher and KDF selection during save.
- The same document also states:
- KeePassGO does not yet provide a UI for editing cipher or KDF parameters directly.
- New vault creation still uses library default KDBX header settings.
Why this is still a gap:
- `AGENTS.md` requires support for the major KeePass-style encryption and KDF configuration choices represented in KDBX databases.
- Preserving existing settings is good, but it is weaker than allowing the product user to choose those settings for new vaults or change them explicitly for existing vaults.
Remaining work:
- Expose major KDBX cipher/KDF choices in the product UI for vault creation.
- Expose supported security-setting changes for an existing unlocked vault.
- Add behavior tests covering explicit user selection of supported cipher/KDF options.
- Update `docs/kdbx-compatibility.md` once those product-facing controls exist.
Exit criteria for this gap:
- A user can create a vault with a selected supported cipher/KDF combination through the product.
- A user can change supported cipher/KDF settings for an existing vault through the product, where the underlying library supports it.
- Tests cover those choices end to end.
### 2. Accessibility Requirement Needs Explicit Screen-Reader Review
Evidence:
- Keyboard-first behavior and focus handling exist in the landed code.
- Accessibility labels exist in `ui_accessibility.go`.
- The repository does not currently contain a documented review of what screen-reader-conscious behavior Gio can and cannot provide on the supported desktop targets.
Why this is still a gap:
- `AGENTS.md` explicitly calls for screen-reader-conscious design, not only keyboard shortcuts and focus states.
- The current code suggests intent, but the repo does not yet document a concrete accessibility support boundary or validation result.
Remaining work:
- Audit the current Gio accessibility surface on Linux and Windows for the controls used by KeePassGO.
- Document what is currently exposed, what is intentionally labeled, and what remains limited by the toolkit.
- Add targeted tests for any label/focus mapping that can be verified in-repo.
Exit criteria for this gap:
- The repo includes a documented accessibility review for current desktop targets.
- Screen-reader-conscious behavior is explicitly described rather than implied.
- Any in-repo verifiable accessibility mappings have tests.
The last explicitly tracked gaps are now closed:
- KDBX security settings are product-configurable at the major cipher/KDF family level for both new vault creation and existing sessions.
- The current accessibility support boundary is documented in `docs/accessibility.md`, while in-repo focus and labeling behavior remains tested.
+25
View File
@@ -86,6 +86,11 @@ type RemoteOpenableSession interface {
OpenRemote(webdav.Client, string, vault.MasterKey) error
}
type SecurityConfigurableSession interface {
ConfigureSecurity(vault.SecuritySettings) error
SecuritySettings() vault.SecuritySettings
}
type ApprovalManager interface {
Pending() []apiapproval.Request
Resolve(string, apiapproval.Outcome) (apiapproval.Request, *apitokens.PolicyRule, error)
@@ -238,6 +243,26 @@ func (s *State) DeleteAPIToken(id string) error {
return nil
}
func (s *State) SecuritySettings() (vault.SecuritySettings, error) {
security, ok := s.Session.(SecurityConfigurableSession)
if !ok {
return vault.SecuritySettings{}, fmt.Errorf("session does not expose security settings")
}
return security.SecuritySettings(), nil
}
func (s *State) ConfigureSecurity(settings vault.SecuritySettings) error {
security, ok := s.Session.(SecurityConfigurableSession)
if !ok {
return fmt.Errorf("session does not expose security settings")
}
if err := security.ConfigureSecurity(settings); err != nil {
return err
}
s.Dirty = true
return nil
}
func (s *State) ShowSection(section Section) {
s.Section = section
s.CurrentPath = nil
+48
View File
@@ -0,0 +1,48 @@
# Accessibility Review
KeePassGO currently targets keyboard-first desktop use on Linux and Windows.
## What is intentionally supported
- Keyboard focus is explicit for:
- vault search
- breadcrumb navigation
- entry list selection
- detail/editor fields
- Focus styling is visible and distinct from the unfocused field treatment.
- Common keyboard workflows are covered in-repo by tests for:
- tab navigation
- list navigation
- search focus
- new-entry focus transitions
- Controls that participate in keyboard navigation have intent-revealing accessibility labels through `accessibilityLabel` in [`ui_accessibility.go`](/home/rustyryan/dev/go/src/git.julianfamily.org/keepassgo/ui_accessibility.go).
## Current screen-reader boundary
- Gio does not currently give KeePassGO a full native accessibility tree comparable to mature desktop UI toolkits.
- KeePassGO therefore treats screen-reader support as:
- label-conscious where the toolkit exposes focusable controls
- limited where platform assistive APIs are not surfaced by Gio in the same way as native toolkit widgets
- In practice, this means keyboard and focus behavior are first-class and tested, while spoken output quality still depends on Gio/platform limitations outside this repo.
## Current review result
- Linux:
- keyboard/focus behavior is intentionally supported
- visible focus states and control naming are present in code
- full Orca-style semantic verification is not something this repo can assert automatically today
- Windows:
- the same keyboard/focus behavior and explicit labels are present in-app
- full UI Automation parity cannot be claimed from inside this codebase without broader Gio support
## What KeePassGO should continue doing
- Keep every major workflow operable without a pointer device.
- Add explicit labels for any new focusable control.
- Preserve visible focus treatment for new form fields, buttons, and dialogs.
- Prefer dialogs and panels that keep keyboard focus predictable.
## What remains toolkit-limited
- Rich screen-reader semantics beyond the control labeling and focus management done in this repository.
- Native assistive-technology parity with toolkits that expose a fuller accessibility object model.
+6 -3
View File
@@ -10,6 +10,9 @@ KeePassGO supports the following KDBX security workflows today:
- preserve the original opened vault's KDBX format version during save
- preserve the original opened vault's cipher selection during save
- preserve the original opened vault's KDF selection during save
- choose the cipher family for new vault creation
- choose the KDF family for new vault creation
- change the cipher family and KDF family for an existing unlocked session before the next save
What "preserve" means:
@@ -18,11 +21,11 @@ What "preserve" means:
Current explicit limitations:
- KeePassGO does not yet provide a UI for editing cipher or KDF parameters directly
- new vault creation still uses the library default KDBX header settings for freshly created databases
- KeePassGO currently exposes major cipher/KDF family choices, not every low-level tuning parameter from KeePass
- advanced KDF tuning such as custom Argon2 memory/parallelism and AES-KDF round-count editing is not yet a product-facing control
- unsupported or unknown header fields outside the preserved header structures are not guaranteed to round-trip if they are not represented by the underlying library
Practical expectation:
- existing KeePass/KeePass2Android-compatible vaults keep their major format, cipher, and KDF family when edited and saved through KeePassGO
- KeePassGO does not yet try to be a full advanced database-settings editor
- KeePassGO now lets a user select the major cipher/KDF family, while still avoiding a full low-level database-header editor
+120
View File
@@ -136,6 +136,8 @@ type ui struct {
apiPolicyOperation widget.Editor
apiPolicyPath widget.Editor
apiPolicyEntryID widget.Editor
securityCipher widget.Editor
securityKDF widget.Editor
entryID widget.Editor
entryTitle widget.Editor
entryUsername widget.Editor
@@ -170,8 +172,11 @@ type ui struct {
synchronizeVault widget.Clickable
toggleSyncMenu widget.Clickable
openAdvancedSync widget.Clickable
openSecuritySettings widget.Clickable
closeAdvancedSync widget.Clickable
closeSecuritySettings widget.Clickable
runAdvancedSync widget.Clickable
saveSecuritySettings widget.Clickable
editEntry widget.Clickable
cancelEdit widget.Clickable
pickVaultPath widget.Clickable
@@ -268,6 +273,7 @@ type ui struct {
syncRemotePassword widget.Editor
syncDialogOpen bool
syncMenuOpen bool
securityDialogOpen bool
showSyncPassword bool
keyboardFocus focusID
defaultSaveAsPath string
@@ -349,6 +355,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
apiPolicyOperation: widget.Editor{SingleLine: true, Submit: false},
apiPolicyPath: widget.Editor{SingleLine: true, Submit: false},
apiPolicyEntryID: widget.Editor{SingleLine: true, Submit: false},
securityCipher: widget.Editor{SingleLine: true, Submit: false},
securityKDF: widget.Editor{SingleLine: true, Submit: false},
entryID: widget.Editor{SingleLine: true, Submit: false},
entryTitle: widget.Editor{SingleLine: true, Submit: false},
entryUsername: widget.Editor{SingleLine: true, Submit: false},
@@ -398,6 +406,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
u.expandLessIcon, _ = widget.NewIcon(icons.NavigationExpandLess)
u.chevronDownIcon, _ = widget.NewIcon(icons.NavigationArrowDropDown)
u.passwordProfile.SetText("strong")
u.securityCipher.SetText(vault.CipherChaCha20)
u.securityKDF.SetText(vault.KDFArgon2)
u.keyboardFocus = focusSearch
u.setCustomFieldRows(nil)
u.loadRecentVaults()
@@ -628,6 +638,12 @@ func (u *ui) createVaultAction() error {
if err != nil {
return err
}
if err := u.state.ConfigureSecurity(vault.SecuritySettings{
Cipher: strings.TrimSpace(u.securityCipher.Text()),
KDF: strings.TrimSpace(u.securityKDF.Text()),
}); err != nil {
return err
}
if err := u.state.CreateVault(key); err != nil {
return err
}
@@ -640,6 +656,7 @@ func (u *ui) createVaultAction() error {
}
u.resetPasswordPeek()
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
return nil
@@ -662,6 +679,7 @@ func (u *ui) openVaultAction() error {
u.resetPasswordPeek()
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.restoreRecentVaultGroup(path)
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
return nil
@@ -709,6 +727,7 @@ func (u *ui) openRemoteAction() error {
)
u.resetPasswordPeek()
u.restoreRecentRemoteGroup(strings.TrimSpace(u.remoteBaseURL.Text()), strings.TrimSpace(u.remotePath.Text()))
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
return nil
@@ -737,6 +756,7 @@ func (u *ui) unlockAction() error {
}
u.resetPasswordPeek()
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
return nil
@@ -751,6 +771,27 @@ func (u *ui) changeMasterKeyAction() error {
return u.state.ChangeMasterKey(key)
}
func (u *ui) loadSecuritySettingsFromSession() {
settings, err := u.state.SecuritySettings()
if err != nil {
return
}
u.securityCipher.SetText(settings.Cipher)
u.securityKDF.SetText(settings.KDF)
}
func (u *ui) saveSecuritySettingsAction() error {
settings := vault.SecuritySettings{
Cipher: strings.TrimSpace(u.securityCipher.Text()),
KDF: strings.TrimSpace(u.securityKDF.Text()),
}
if err := u.state.ConfigureSecurity(settings); err != nil {
return err
}
u.securityDialogOpen = false
return nil
}
func (u *ui) clearMasterPassword() {
u.masterPassword.SetText("")
}
@@ -1518,13 +1559,23 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.openAdvancedSync.Clicked(gtx) {
u.openAdvancedSyncDialog()
}
for u.openSecuritySettings.Clicked(gtx) {
u.loadSecuritySettingsFromSession()
u.securityDialogOpen = true
}
for u.closeAdvancedSync.Clicked(gtx) {
u.syncDialogOpen = false
u.showSyncPassword = false
}
for u.closeSecuritySettings.Clicked(gtx) {
u.securityDialogOpen = false
}
for u.runAdvancedSync.Clicked(gtx) {
u.runAction("advanced synchronize vault", u.advancedSyncAction)
}
for u.saveSecuritySettings.Clicked(gtx) {
u.runAction("save security settings", u.saveSecuritySettingsAction)
}
for u.unlockVault.Clicked(gtx) {
u.runAction("unlock vault", u.unlockAction)
}
@@ -1835,6 +1886,12 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
}
return u.syncDialog(gtx)
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
if !u.securityDialogOpen {
return layout.Dimensions{}
}
return u.securityDialog(gtx)
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
if _, ok := u.pendingApproval(); !ok {
return layout.Dimensions{}
@@ -1891,6 +1948,61 @@ func (u *ui) syncDialog(gtx layout.Context) layout.Dimensions {
)
}
func (u *ui) securityDialog(gtx layout.Context) layout.Dimensions {
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
paint.FillShape(gtx.Ops, color.NRGBA{A: 90}, clip.Rect{Max: gtx.Constraints.Max}.Op())
return layout.Dimensions{Size: gtx.Constraints.Max}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
width := gtx.Dp(unit.Dp(620))
if width > gtx.Constraints.Max.X {
width = gtx.Constraints.Max.X - gtx.Dp(unit.Dp(24))
}
if width < 1 {
width = gtx.Constraints.Max.X
}
gtx.Constraints.Min.X = width
gtx.Constraints.Max.X = width
return card(gtx, u.securityDialogContent)
})
}),
)
}
func (u *ui) securityDialogContent(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(20), "Security Settings")
lbl.Color = accentColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(14), "Choose the KDBX cipher and KDF family KeePassGO should use for new or future saves.")
lbl.Color = mutedColor
return lbl.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Cipher", "Supported values: "+strings.Join([]string{vault.CipherAES256, vault.CipherChaCha20}, ", "), &u.securityCipher, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Supported values: "+strings.Join([]string{vault.KDFAES, vault.KDFArgon2}, ", "), &u.securityKDF, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.closeSecuritySettings, "Cancel")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.saveSecuritySettings, "Save Security Settings")
}),
)
}),
)
}
func (u *ui) approvalDialog(gtx layout.Context) layout.Dimensions {
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
@@ -2168,6 +2280,10 @@ func (u *ui) headerActions(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(u.syncButtonGroup),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Security")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.lockVault, "Lock")
return btn.Layout(gtx)
@@ -2505,6 +2621,10 @@ func (u *ui) detailPanel(gtx layout.Context) layout.Dimensions {
}),
layout.Rigid(u.syncButtonGroup),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Security")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.Button(u.theme, &u.lockVault, "Lock")
return btn.Layout(gtx)
+59
View File
@@ -365,6 +365,65 @@ func TestUILifecycleActionsCreateSaveOpenLockAndUnlockLocalVault(t *testing.T) {
}
}
func TestUICreateVaultUsesSelectedSecuritySettings(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.securityCipher.SetText(vault.CipherAES256)
u.securityKDF.SetText(vault.KDFAES)
u.masterPassword.SetText("correct horse battery staple")
if err := u.createVaultAction(); err != nil {
t.Fatalf("createVaultAction() error = %v", err)
}
path := filepath.Join(t.TempDir(), "secure.kdbx")
u.saveAsPath.SetText(path)
if err := u.saveAsAction(); err != nil {
t.Fatalf("saveAsAction() error = %v", err)
}
var reopened session.Manager
if err := reopened.Open(path, vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
t.Fatalf("Open() error = %v", err)
}
got := reopened.SecuritySettings()
if got.Cipher != vault.CipherAES256 || got.KDF != vault.KDFAES {
t.Fatalf("SecuritySettings() = %#v, want aes256/aes-kdf", got)
}
}
func TestUISaveSecuritySettingsUpdatesExistingVault(t *testing.T) {
t.Parallel()
manager := &session.Manager{}
u := newUIWithSession("desktop", manager)
u.masterPassword.SetText("correct horse battery staple")
if err := u.createVaultAction(); err != nil {
t.Fatalf("createVaultAction() error = %v", err)
}
u.securityCipher.SetText(vault.CipherAES256)
u.securityKDF.SetText(vault.KDFAES)
if err := u.saveSecuritySettingsAction(); err != nil {
t.Fatalf("saveSecuritySettingsAction() error = %v", err)
}
path := filepath.Join(t.TempDir(), "updated-secure.kdbx")
u.saveAsPath.SetText(path)
if err := u.saveAsAction(); err != nil {
t.Fatalf("saveAsAction() error = %v", err)
}
var reopened session.Manager
if err := reopened.Open(path, vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
t.Fatalf("Open() error = %v", err)
}
got := reopened.SecuritySettings()
if got.Cipher != vault.CipherAES256 || got.KDF != vault.KDFAES {
t.Fatalf("SecuritySettings() = %#v, want aes256/aes-kdf", got)
}
}
func TestUILockAndUnlockClearMasterPasswordField(t *testing.T) {
t.Parallel()
+13
View File
@@ -32,6 +32,19 @@ type Manager struct {
remoteVersion webdav.Version
}
func (m *Manager) SecuritySettings() vault.SecuritySettings {
return vault.DetectSecuritySettings(m.config)
}
func (m *Manager) ConfigureSecurity(settings vault.SecuritySettings) error {
config, err := vault.ApplySecuritySettings(configOrCurrent(m.config, nil), settings)
if err != nil {
return fmt.Errorf("configure security settings: %w", err)
}
m.config = config
return nil
}
func (m *Manager) Create(model vault.Model, key vault.MasterKey) error {
root := detectSingleVaultRoot(model)
model = normalizeUnderRoot(model, root)
+29
View File
@@ -741,6 +741,35 @@ func TestRemoteSaveAndReopenPreservesCrossFeatureState(t *testing.T) {
}
}
func TestConfigureSecurityAppliesToCreatedVaultAndPersists(t *testing.T) {
t.Parallel()
var sess Manager
if err := sess.ConfigureSecurity(vault.SecuritySettings{
Cipher: vault.CipherAES256,
KDF: vault.KDFAES,
}); err != nil {
t.Fatalf("ConfigureSecurity() error = %v", err)
}
if err := sess.Create(vault.Model{}, vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
t.Fatalf("Create() error = %v", err)
}
path := filepath.Join(t.TempDir(), "secure.kdbx")
if err := sess.SaveAs(path); err != nil {
t.Fatalf("SaveAs() error = %v", err)
}
var reopened Manager
if err := reopened.Open(path, vault.MasterKey{Password: "correct horse battery staple"}); err != nil {
t.Fatalf("Open() error = %v", err)
}
got := reopened.SecuritySettings()
if got.Cipher != vault.CipherAES256 || got.KDF != vault.KDFAES {
t.Fatalf("SecuritySettings() = %#v, want aes256/aes-kdf", got)
}
}
func TestSynchronizeRemotePreservesOverwrittenRemoteVariantInHistory(t *testing.T) {
t.Parallel()
+4
View File
@@ -61,6 +61,10 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Cipher", "Supported values: aes256, chacha20", &u.securityCipher, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Supported values: aes-kdf, argon2", &u.securityKDF, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.lifecycleMode == "remote" {
return tonedButton(gtx, u.theme, &u.openRemote, "Open Remote Vault")
+100
View File
@@ -0,0 +1,100 @@
package vault
import (
"fmt"
"slices"
"github.com/tobischo/gokeepasslib/v3"
)
type SecuritySettings struct {
Cipher string
KDF string
}
const (
CipherAES256 = "aes256"
CipherChaCha20 = "chacha20"
KDFAES = "aes-kdf"
KDFArgon2 = "argon2"
)
func SupportedSecuritySettings() (ciphers []string, kdfs []string) {
return []string{CipherAES256, CipherChaCha20}, []string{KDFAES, KDFArgon2}
}
func DetectSecuritySettings(config *KDBXConfig) SecuritySettings {
settings := SecuritySettings{
Cipher: CipherChaCha20,
KDF: KDFArgon2,
}
if config == nil || config.Header == nil || config.Header.FileHeaders == nil {
return settings
}
if slices.Equal(config.Header.FileHeaders.CipherID, gokeepasslib.CipherAES) {
settings.Cipher = CipherAES256
}
if config.Header.FileHeaders.KdfParameters != nil && slices.Equal(config.Header.FileHeaders.KdfParameters.UUID, gokeepasslib.KdfAES4) {
settings.KDF = KDFAES
}
return settings
}
func NewSecurityConfig(settings SecuritySettings) (*KDBXConfig, error) {
db := gokeepasslib.NewDatabase(gokeepasslib.WithDatabaseKDBXVersion4())
config := &KDBXConfig{
Header: cloneHeader(db.Header),
InnerHeader: cloneInnerHeader(db.Content.InnerHeader),
}
return ApplySecuritySettings(config, settings)
}
func ApplySecuritySettings(config *KDBXConfig, settings SecuritySettings) (*KDBXConfig, error) {
if config == nil || config.Header == nil || config.Header.FileHeaders == nil {
return NewSecurityConfig(settings)
}
out := &KDBXConfig{
Header: cloneHeader(config.Header),
InnerHeader: cloneInnerHeader(config.InnerHeader),
}
if out.Header.FileHeaders.KdfParameters == nil {
defaults := gokeepasslib.NewDatabase(gokeepasslib.WithDatabaseKDBXVersion4())
out.Header.FileHeaders.KdfParameters = cloneHeader(defaults.Header).FileHeaders.KdfParameters
}
switch settings.Cipher {
case "", CipherChaCha20:
out.Header.FileHeaders.CipherID = slices.Clone(gokeepasslib.CipherChaCha20)
out.Header.FileHeaders.EncryptionIV = randomBytes(12)
case CipherAES256:
out.Header.FileHeaders.CipherID = slices.Clone(gokeepasslib.CipherAES)
out.Header.FileHeaders.EncryptionIV = randomBytes(16)
default:
return nil, fmt.Errorf("unsupported cipher %q", settings.Cipher)
}
var salt [32]byte
copy(salt[:], randomBytes(32))
switch settings.KDF {
case "", KDFArgon2:
defaults := gokeepasslib.NewDatabase(gokeepasslib.WithDatabaseKDBXVersion4())
kdf := defaults.Header.FileHeaders.KdfParameters
out.Header.FileHeaders.KdfParameters = &gokeepasslib.KdfParameters{
UUID: slices.Clone(gokeepasslib.KdfArgon2),
Rounds: kdf.Rounds,
Salt: salt,
Parallelism: kdf.Parallelism,
Memory: kdf.Memory,
Iterations: kdf.Iterations,
Version: kdf.Version,
}
case KDFAES:
out.Header.FileHeaders.KdfParameters = &gokeepasslib.KdfParameters{
UUID: slices.Clone(gokeepasslib.KdfAES4),
Rounds: 6000,
Salt: salt,
}
default:
return nil, fmt.Errorf("unsupported KDF %q", settings.KDF)
}
return out, nil
}
+50
View File
@@ -0,0 +1,50 @@
package vault
import (
"bytes"
"slices"
"testing"
"github.com/tobischo/gokeepasslib/v3"
)
func TestNewSecurityConfigCreatesRequestedCipherAndKDF(t *testing.T) {
t.Parallel()
config, err := NewSecurityConfig(SecuritySettings{Cipher: CipherAES256, KDF: KDFAES})
if err != nil {
t.Fatalf("NewSecurityConfig() error = %v", err)
}
if !slices.Equal(config.Header.FileHeaders.CipherID, gokeepasslib.CipherAES) {
t.Fatalf("CipherID = %x, want %x", config.Header.FileHeaders.CipherID, gokeepasslib.CipherAES)
}
if !slices.Equal(config.Header.FileHeaders.KdfParameters.UUID, gokeepasslib.KdfAES4) {
t.Fatalf("KDF UUID = %x, want %x", config.Header.FileHeaders.KdfParameters.UUID, gokeepasslib.KdfAES4)
}
}
func TestApplySecuritySettingsPreservesRequestedChoicesAcrossSave(t *testing.T) {
t.Parallel()
config, err := NewSecurityConfig(SecuritySettings{Cipher: CipherChaCha20, KDF: KDFArgon2})
if err != nil {
t.Fatalf("NewSecurityConfig() error = %v", err)
}
config, err = ApplySecuritySettings(config, SecuritySettings{Cipher: CipherAES256, KDF: KDFAES})
if err != nil {
t.Fatalf("ApplySecuritySettings() error = %v", err)
}
var encoded bytes.Buffer
if err := SaveKDBXWithConfigAndKey(&encoded, Model{}, MasterKey{Password: "correct horse battery staple"}, config); err != nil {
t.Fatalf("SaveKDBXWithConfigAndKey() error = %v", err)
}
_, reloadedConfig, err := LoadKDBXWithConfig(bytes.NewReader(encoded.Bytes()), MasterKey{Password: "correct horse battery staple"})
if err != nil {
t.Fatalf("LoadKDBXWithConfig() error = %v", err)
}
got := DetectSecuritySettings(reloadedConfig)
if got.Cipher != CipherAES256 || got.KDF != KDFAES {
t.Fatalf("DetectSecuritySettings() = %#v, want aes256/aes-kdf", got)
}
}