Add UI master key setup and change flows

This commit is contained in:
Joe Julian
2026-03-29 11:23:54 -07:00
parent e15cfb1535
commit 44bba18149
10 changed files with 675 additions and 147 deletions
+169 -118
View File
@@ -1,111 +1,116 @@
package main
import (
"fmt"
"flag"
"fmt"
"image"
"image/color"
"os"
"strings"
"gioui.org/app"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"git.julianfamily.org/keepassgo/appstate"
"git.julianfamily.org/keepassgo/clipboard"
"git.julianfamily.org/keepassgo/session"
"git.julianfamily.org/keepassgo/vault"
"git.julianfamily.org/keepassgo/webdav"
"gioui.org/app"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons"
)
type entry = vault.Entry
type ui struct {
mode string
theme *material.Theme
search widget.Editor
vaultPath widget.Editor
saveAsPath widget.Editor
remoteBaseURL widget.Editor
remotePath widget.Editor
remoteUsername widget.Editor
remotePassword widget.Editor
masterPassword widget.Editor
keyFilePath widget.Editor
entryID widget.Editor
entryTitle widget.Editor
entryUsername widget.Editor
entryPassword widget.Editor
entryURL widget.Editor
entryNotes widget.Editor
entryTags widget.Editor
entryPath widget.Editor
entryFields widget.Editor
historyIndex widget.Editor
groupName widget.Editor
passwordProfile widget.Editor
attachmentName widget.Editor
attachmentPath widget.Editor
exportAttachmentPath widget.Editor
list widget.List
detailList widget.List
copyUser widget.Clickable
copyPass widget.Clickable
copyURL widget.Clickable
openURL widget.Clickable
lockVault widget.Clickable
unlockVault widget.Clickable
createVault widget.Clickable
openVault widget.Clickable
saveVault widget.Clickable
saveAsVault widget.Clickable
openRemote widget.Clickable
addEntry widget.Clickable
saveEntry widget.Clickable
duplicateEntry widget.Clickable
deleteEntry widget.Clickable
restoreEntry widget.Clickable
saveTemplate widget.Clickable
deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable
addAttachment widget.Clickable
removeAttachment widget.Clickable
exportAttachment widget.Clickable
restoreHistory widget.Clickable
generatePassword widget.Clickable
createGroup widget.Clickable
renameGroup widget.Clickable
deleteGroup widget.Clickable
togglePasswordInline widget.Clickable
showEntries widget.Clickable
showTemplates widget.Clickable
showRecycle widget.Clickable
entryClicks []widget.Clickable
breadcrumbs []widget.Clickable
groupClicks []widget.Clickable
state appstate.State
visible []entry
currentPath []string
showPassword bool
togglePassword widget.Clickable
phoneSplit widget.Float
splitDrag gesture.Drag
splitBase float32
splitStartY float32
phoneSpan int
eyeIcon *widget.Icon
eyeOffIcon *widget.Icon
copyIcon *widget.Icon
statusMessage string
errorMessage string
mode string
theme *material.Theme
search widget.Editor
vaultPath widget.Editor
saveAsPath widget.Editor
remoteBaseURL widget.Editor
remotePath widget.Editor
remoteUsername widget.Editor
remotePassword widget.Editor
masterPassword widget.Editor
keyFilePath widget.Editor
entryID widget.Editor
entryTitle widget.Editor
entryUsername widget.Editor
entryPassword widget.Editor
entryURL widget.Editor
entryNotes widget.Editor
entryTags widget.Editor
entryPath widget.Editor
entryFields widget.Editor
historyIndex widget.Editor
groupName widget.Editor
passwordProfile widget.Editor
attachmentName widget.Editor
attachmentPath widget.Editor
exportAttachmentPath widget.Editor
list widget.List
detailList widget.List
copyUser widget.Clickable
copyPass widget.Clickable
copyURL widget.Clickable
openURL widget.Clickable
lockVault widget.Clickable
unlockVault widget.Clickable
createVault widget.Clickable
openVault widget.Clickable
saveVault widget.Clickable
saveAsVault widget.Clickable
openRemote widget.Clickable
changeMasterKey widget.Clickable
addEntry widget.Clickable
saveEntry widget.Clickable
duplicateEntry widget.Clickable
deleteEntry widget.Clickable
restoreEntry widget.Clickable
saveTemplate widget.Clickable
deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable
addAttachment widget.Clickable
removeAttachment widget.Clickable
exportAttachment widget.Clickable
restoreHistory widget.Clickable
generatePassword widget.Clickable
createGroup widget.Clickable
renameGroup widget.Clickable
deleteGroup widget.Clickable
togglePasswordInline widget.Clickable
showEntries widget.Clickable
showTemplates widget.Clickable
showRecycle widget.Clickable
masterKeyPasswordOnly widget.Clickable
masterKeyKeyFileOnly widget.Clickable
masterKeyComposite widget.Clickable
entryClicks []widget.Clickable
breadcrumbs []widget.Clickable
groupClicks []widget.Clickable
state appstate.State
masterKeyMode vault.MasterKeyMode
visible []entry
currentPath []string
showPassword bool
togglePassword widget.Clickable
phoneSplit widget.Float
splitDrag gesture.Drag
splitBase float32
splitStartY float32
phoneSpan int
eyeIcon *widget.Icon
eyeOffIcon *widget.Icon
copyIcon *widget.Icon
statusMessage string
errorMessage string
}
var (
@@ -143,28 +148,28 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui {
SingleLine: true,
Submit: false,
},
vaultPath: widget.Editor{SingleLine: true, Submit: false},
saveAsPath: widget.Editor{SingleLine: true, Submit: false},
remoteBaseURL: widget.Editor{SingleLine: true, Submit: false},
remotePath: widget.Editor{SingleLine: true, Submit: false},
remoteUsername: widget.Editor{SingleLine: true, Submit: false},
remotePassword: widget.Editor{SingleLine: true, Submit: false},
masterPassword: widget.Editor{SingleLine: true, Submit: false},
keyFilePath: 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},
entryPassword: widget.Editor{SingleLine: true, Submit: false},
entryURL: widget.Editor{SingleLine: true, Submit: false},
entryNotes: widget.Editor{SingleLine: false, Submit: false},
entryTags: widget.Editor{SingleLine: true, Submit: false},
entryPath: widget.Editor{SingleLine: true, Submit: false},
entryFields: widget.Editor{SingleLine: false, Submit: false},
historyIndex: widget.Editor{SingleLine: true, Submit: false},
groupName: widget.Editor{SingleLine: true, Submit: false},
passwordProfile: widget.Editor{SingleLine: true, Submit: false},
attachmentName: widget.Editor{SingleLine: true, Submit: false},
attachmentPath: widget.Editor{SingleLine: true, Submit: false},
vaultPath: widget.Editor{SingleLine: true, Submit: false},
saveAsPath: widget.Editor{SingleLine: true, Submit: false},
remoteBaseURL: widget.Editor{SingleLine: true, Submit: false},
remotePath: widget.Editor{SingleLine: true, Submit: false},
remoteUsername: widget.Editor{SingleLine: true, Submit: false},
remotePassword: widget.Editor{SingleLine: true, Submit: false},
masterPassword: widget.Editor{SingleLine: true, Submit: false},
keyFilePath: 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},
entryPassword: widget.Editor{SingleLine: true, Submit: false},
entryURL: widget.Editor{SingleLine: true, Submit: false},
entryNotes: widget.Editor{SingleLine: false, Submit: false},
entryTags: widget.Editor{SingleLine: true, Submit: false},
entryPath: widget.Editor{SingleLine: true, Submit: false},
entryFields: widget.Editor{SingleLine: false, Submit: false},
historyIndex: widget.Editor{SingleLine: true, Submit: false},
groupName: widget.Editor{SingleLine: true, Submit: false},
passwordProfile: widget.Editor{SingleLine: true, Submit: false},
attachmentName: widget.Editor{SingleLine: true, Submit: false},
attachmentPath: widget.Editor{SingleLine: true, Submit: false},
exportAttachmentPath: widget.Editor{SingleLine: true, Submit: false},
list: widget.List{
List: layout.List{Axis: layout.Vertical},
@@ -172,7 +177,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui {
detailList: widget.List{
List: layout.List{Axis: layout.Vertical},
},
state: appstate.State{},
state: appstate.State{},
masterKeyMode: vault.MasterKeyModePasswordOnly,
}
u.state.Session = sess
u.phoneSplit.Value = 0.46
@@ -263,19 +269,44 @@ func (u *ui) selectedEntry() (entry, bool) {
}
func (u *ui) currentMasterKey() (vault.MasterKey, error) {
key := vault.MasterKey{Password: u.masterPassword.Text()}
password := u.masterPassword.Text()
path := strings.TrimSpace(u.keyFilePath.Text())
if path == "" {
return key, nil
switch u.masterKeyMode {
case vault.MasterKeyModeKeyFileOnly:
if path == "" {
return vault.MasterKey{}, fmt.Errorf("key file is required")
}
case vault.MasterKeyModePasswordAndKeyFile:
if password == "" {
return vault.MasterKey{}, fmt.Errorf("master password is required")
}
if path == "" {
return vault.MasterKey{}, fmt.Errorf("key file is required")
}
default:
if password == "" {
return vault.MasterKey{}, fmt.Errorf("master password is required")
}
return vault.MasterKey{Password: password}, nil
}
content, err := os.ReadFile(path)
if err != nil {
return vault.MasterKey{}, fmt.Errorf("read key file: %w", err)
}
key.KeyFileData = content
return key, nil
if len(content) == 0 {
return vault.MasterKey{}, fmt.Errorf("key file is empty")
}
return vault.MasterKey{
Password: password,
KeyFileData: content,
}, nil
}
func (u *ui) setMasterKeyMode(mode vault.MasterKeyMode) {
u.masterKeyMode = mode
}
func (u *ui) createVaultAction() error {
@@ -359,6 +390,14 @@ func (u *ui) unlockAction() error {
return nil
}
func (u *ui) changeMasterKeyAction() error {
key, err := u.currentMasterKey()
if err != nil {
return err
}
return u.state.ChangeMasterKey(key)
}
func (u *ui) runAction(label string, action func() error) {
if err := action(); err != nil {
u.errorMessage = err.Error()
@@ -392,9 +431,21 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.openRemote.Clicked(gtx) {
u.runAction("open remote vault", u.openRemoteAction)
}
for u.changeMasterKey.Clicked(gtx) {
u.runAction("change master key", u.changeMasterKeyAction)
}
for u.unlockVault.Clicked(gtx) {
u.runAction("unlock vault", u.unlockAction)
}
for u.masterKeyPasswordOnly.Clicked(gtx) {
u.setMasterKeyMode(vault.MasterKeyModePasswordOnly)
}
for u.masterKeyKeyFileOnly.Clicked(gtx) {
u.setMasterKeyMode(vault.MasterKeyModeKeyFileOnly)
}
for u.masterKeyComposite.Clicked(gtx) {
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
}
for u.showEntries.Clicked(gtx) {
u.showEntriesSection()
}
@@ -757,7 +808,7 @@ func (u *ui) phoneSlider(gtx layout.Context) layout.Dimensions {
y := (gtx.Constraints.Min.Y - handleH) / 2
paint.FillShape(gtx.Ops, color.NRGBA{R: 214, G: 208, B: 197, A: 255}, clip.Rect{Min: image.Pt(0, y+1), Max: image.Pt(gtx.Constraints.Min.X, y+2)}.Op())
paint.FillShape(gtx.Ops, accentColor, clip.RRect{
Rect: image.Rectangle{Min: image.Pt(x, y), Max: image.Pt(x + handleW, y + handleH)},
Rect: image.Rectangle{Min: image.Pt(x, y), Max: image.Pt(x+handleW, y+handleH)},
NE: 2, NW: 2, SE: 2, SW: 2,
}.Op(gtx.Ops))
return layout.Dimensions{Size: gtx.Constraints.Min}