Add UI master key setup and change flows
This commit is contained in:
@@ -5,7 +5,8 @@ KeePassGO is a Go-based KeePass-compatible password manager prototype targeting
|
|||||||
## Current Capabilities
|
## Current Capabilities
|
||||||
|
|
||||||
- KDBX load and save
|
- KDBX load and save
|
||||||
- password, key-file, and composite master-key support in the storage layer
|
- password-only, key-file-only, and composite master-key flows through the desktop product UI
|
||||||
|
- master-key changes for existing vault sessions
|
||||||
- WebDAV-backed open and save support in the session layer
|
- WebDAV-backed open and save support in the session layer
|
||||||
- password generation profiles
|
- password generation profiles
|
||||||
- gRPC integration surface for trusted automation
|
- gRPC integration surface for trusted automation
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ type LockableSession interface {
|
|||||||
Unlock(vault.MasterKey) error
|
Unlock(vault.MasterKey) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MasterKeyChangeableSession interface {
|
||||||
|
CurrentSession
|
||||||
|
ChangeMasterKey(vault.MasterKey) error
|
||||||
|
}
|
||||||
|
|
||||||
type SaveableSession interface {
|
type SaveableSession interface {
|
||||||
CurrentSession
|
CurrentSession
|
||||||
Save() error
|
Save() error
|
||||||
@@ -419,6 +424,20 @@ func (s *State) Unlock(key vault.MasterKey) error {
|
|||||||
return session.Unlock(key)
|
return session.Unlock(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *State) ChangeMasterKey(key vault.MasterKey) error {
|
||||||
|
session, ok := s.Session.(MasterKeyChangeableSession)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("session does not support master key changes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := session.ChangeMasterKey(key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Dirty = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *State) EnterGroup(name string) {
|
func (s *State) EnterGroup(name string) {
|
||||||
s.CurrentPath = append(append([]string(nil), s.CurrentPath...), name)
|
s.CurrentPath = append(append([]string(nil), s.CurrentPath...), name)
|
||||||
s.SelectedEntryID = ""
|
s.SelectedEntryID = ""
|
||||||
|
|||||||
@@ -696,6 +696,25 @@ func TestUnlockRestoresVaultVisibility(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChangeMasterKeyMarksStateDirty(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
sess := &lifecycleStubSession{}
|
||||||
|
state := State{Session: sess}
|
||||||
|
|
||||||
|
key := vault.MasterKey{Password: "correct horse battery staple"}
|
||||||
|
if err := state.ChangeMasterKey(key); err != nil {
|
||||||
|
t.Fatalf("ChangeMasterKey() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := sess.changedKey; got.Password != key.Password {
|
||||||
|
t.Fatalf("changedKey = %#v, want %#v", got, key)
|
||||||
|
}
|
||||||
|
if !state.Dirty {
|
||||||
|
t.Fatal("Dirty = false, want true after ChangeMasterKey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEnterGroupAppendsPathAndClearsSelection(t *testing.T) {
|
func TestEnterGroupAppendsPathAndClearsSelection(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -929,6 +948,7 @@ type lifecycleStubSession struct {
|
|||||||
openPath string
|
openPath string
|
||||||
saveAsPath string
|
saveAsPath string
|
||||||
remotePath string
|
remotePath string
|
||||||
|
changedKey vault.MasterKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *lifecycleStubSession) Current() (vault.Model, error) {
|
func (s *lifecycleStubSession) Current() (vault.Model, error) {
|
||||||
@@ -954,3 +974,8 @@ func (s *lifecycleStubSession) OpenRemote(_ webdav.Client, path string, _ vault.
|
|||||||
s.remotePath = path
|
s.remotePath = path
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *lifecycleStubSession) ChangeMasterKey(key vault.MasterKey) error {
|
||||||
|
s.changedKey = key
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ KeePassGO supports the following KDBX security workflows today:
|
|||||||
- open and save password-only vaults
|
- open and save password-only vaults
|
||||||
- open and save key-file-only vaults
|
- open and save key-file-only vaults
|
||||||
- open and save composite password-plus-key-file vaults
|
- open and save composite password-plus-key-file vaults
|
||||||
|
- select the active master-key mode in the product UI for create, open, and unlock flows
|
||||||
|
- change an existing session to a new master-key mode before saving
|
||||||
- preserve the original opened vault's KDBX format version during save
|
- 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 cipher selection during save
|
||||||
- preserve the original opened vault's KDF selection during save
|
- preserve the original opened vault's KDF selection during save
|
||||||
|
|||||||
@@ -1,111 +1,116 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"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/appstate"
|
||||||
"git.julianfamily.org/keepassgo/clipboard"
|
"git.julianfamily.org/keepassgo/clipboard"
|
||||||
"git.julianfamily.org/keepassgo/session"
|
"git.julianfamily.org/keepassgo/session"
|
||||||
"git.julianfamily.org/keepassgo/vault"
|
"git.julianfamily.org/keepassgo/vault"
|
||||||
"git.julianfamily.org/keepassgo/webdav"
|
"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"
|
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||||
)
|
)
|
||||||
|
|
||||||
type entry = vault.Entry
|
type entry = vault.Entry
|
||||||
|
|
||||||
type ui struct {
|
type ui struct {
|
||||||
mode string
|
mode string
|
||||||
theme *material.Theme
|
theme *material.Theme
|
||||||
search widget.Editor
|
search widget.Editor
|
||||||
vaultPath widget.Editor
|
vaultPath widget.Editor
|
||||||
saveAsPath widget.Editor
|
saveAsPath widget.Editor
|
||||||
remoteBaseURL widget.Editor
|
remoteBaseURL widget.Editor
|
||||||
remotePath widget.Editor
|
remotePath widget.Editor
|
||||||
remoteUsername widget.Editor
|
remoteUsername widget.Editor
|
||||||
remotePassword widget.Editor
|
remotePassword widget.Editor
|
||||||
masterPassword widget.Editor
|
masterPassword widget.Editor
|
||||||
keyFilePath widget.Editor
|
keyFilePath widget.Editor
|
||||||
entryID widget.Editor
|
entryID widget.Editor
|
||||||
entryTitle widget.Editor
|
entryTitle widget.Editor
|
||||||
entryUsername widget.Editor
|
entryUsername widget.Editor
|
||||||
entryPassword widget.Editor
|
entryPassword widget.Editor
|
||||||
entryURL widget.Editor
|
entryURL widget.Editor
|
||||||
entryNotes widget.Editor
|
entryNotes widget.Editor
|
||||||
entryTags widget.Editor
|
entryTags widget.Editor
|
||||||
entryPath widget.Editor
|
entryPath widget.Editor
|
||||||
entryFields widget.Editor
|
entryFields widget.Editor
|
||||||
historyIndex widget.Editor
|
historyIndex widget.Editor
|
||||||
groupName widget.Editor
|
groupName widget.Editor
|
||||||
passwordProfile widget.Editor
|
passwordProfile widget.Editor
|
||||||
attachmentName widget.Editor
|
attachmentName widget.Editor
|
||||||
attachmentPath widget.Editor
|
attachmentPath widget.Editor
|
||||||
exportAttachmentPath widget.Editor
|
exportAttachmentPath widget.Editor
|
||||||
list widget.List
|
list widget.List
|
||||||
detailList widget.List
|
detailList widget.List
|
||||||
copyUser widget.Clickable
|
copyUser widget.Clickable
|
||||||
copyPass widget.Clickable
|
copyPass widget.Clickable
|
||||||
copyURL widget.Clickable
|
copyURL widget.Clickable
|
||||||
openURL widget.Clickable
|
openURL widget.Clickable
|
||||||
lockVault widget.Clickable
|
lockVault widget.Clickable
|
||||||
unlockVault widget.Clickable
|
unlockVault widget.Clickable
|
||||||
createVault widget.Clickable
|
createVault widget.Clickable
|
||||||
openVault widget.Clickable
|
openVault widget.Clickable
|
||||||
saveVault widget.Clickable
|
saveVault widget.Clickable
|
||||||
saveAsVault widget.Clickable
|
saveAsVault widget.Clickable
|
||||||
openRemote widget.Clickable
|
openRemote widget.Clickable
|
||||||
addEntry widget.Clickable
|
changeMasterKey widget.Clickable
|
||||||
saveEntry widget.Clickable
|
addEntry widget.Clickable
|
||||||
duplicateEntry widget.Clickable
|
saveEntry widget.Clickable
|
||||||
deleteEntry widget.Clickable
|
duplicateEntry widget.Clickable
|
||||||
restoreEntry widget.Clickable
|
deleteEntry widget.Clickable
|
||||||
saveTemplate widget.Clickable
|
restoreEntry widget.Clickable
|
||||||
deleteTemplate widget.Clickable
|
saveTemplate widget.Clickable
|
||||||
instantiateTemplate widget.Clickable
|
deleteTemplate widget.Clickable
|
||||||
addAttachment widget.Clickable
|
instantiateTemplate widget.Clickable
|
||||||
removeAttachment widget.Clickable
|
addAttachment widget.Clickable
|
||||||
exportAttachment widget.Clickable
|
removeAttachment widget.Clickable
|
||||||
restoreHistory widget.Clickable
|
exportAttachment widget.Clickable
|
||||||
generatePassword widget.Clickable
|
restoreHistory widget.Clickable
|
||||||
createGroup widget.Clickable
|
generatePassword widget.Clickable
|
||||||
renameGroup widget.Clickable
|
createGroup widget.Clickable
|
||||||
deleteGroup widget.Clickable
|
renameGroup widget.Clickable
|
||||||
togglePasswordInline widget.Clickable
|
deleteGroup widget.Clickable
|
||||||
showEntries widget.Clickable
|
togglePasswordInline widget.Clickable
|
||||||
showTemplates widget.Clickable
|
showEntries widget.Clickable
|
||||||
showRecycle widget.Clickable
|
showTemplates widget.Clickable
|
||||||
entryClicks []widget.Clickable
|
showRecycle widget.Clickable
|
||||||
breadcrumbs []widget.Clickable
|
masterKeyPasswordOnly widget.Clickable
|
||||||
groupClicks []widget.Clickable
|
masterKeyKeyFileOnly widget.Clickable
|
||||||
state appstate.State
|
masterKeyComposite widget.Clickable
|
||||||
visible []entry
|
entryClicks []widget.Clickable
|
||||||
currentPath []string
|
breadcrumbs []widget.Clickable
|
||||||
showPassword bool
|
groupClicks []widget.Clickable
|
||||||
togglePassword widget.Clickable
|
state appstate.State
|
||||||
phoneSplit widget.Float
|
masterKeyMode vault.MasterKeyMode
|
||||||
splitDrag gesture.Drag
|
visible []entry
|
||||||
splitBase float32
|
currentPath []string
|
||||||
splitStartY float32
|
showPassword bool
|
||||||
phoneSpan int
|
togglePassword widget.Clickable
|
||||||
eyeIcon *widget.Icon
|
phoneSplit widget.Float
|
||||||
eyeOffIcon *widget.Icon
|
splitDrag gesture.Drag
|
||||||
copyIcon *widget.Icon
|
splitBase float32
|
||||||
statusMessage string
|
splitStartY float32
|
||||||
errorMessage string
|
phoneSpan int
|
||||||
|
eyeIcon *widget.Icon
|
||||||
|
eyeOffIcon *widget.Icon
|
||||||
|
copyIcon *widget.Icon
|
||||||
|
statusMessage string
|
||||||
|
errorMessage string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -143,28 +148,28 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui {
|
|||||||
SingleLine: true,
|
SingleLine: true,
|
||||||
Submit: false,
|
Submit: false,
|
||||||
},
|
},
|
||||||
vaultPath: widget.Editor{SingleLine: true, Submit: false},
|
vaultPath: widget.Editor{SingleLine: true, Submit: false},
|
||||||
saveAsPath: widget.Editor{SingleLine: true, Submit: false},
|
saveAsPath: widget.Editor{SingleLine: true, Submit: false},
|
||||||
remoteBaseURL: widget.Editor{SingleLine: true, Submit: false},
|
remoteBaseURL: widget.Editor{SingleLine: true, Submit: false},
|
||||||
remotePath: widget.Editor{SingleLine: true, Submit: false},
|
remotePath: widget.Editor{SingleLine: true, Submit: false},
|
||||||
remoteUsername: widget.Editor{SingleLine: true, Submit: false},
|
remoteUsername: widget.Editor{SingleLine: true, Submit: false},
|
||||||
remotePassword: widget.Editor{SingleLine: true, Submit: false},
|
remotePassword: widget.Editor{SingleLine: true, Submit: false},
|
||||||
masterPassword: widget.Editor{SingleLine: true, Submit: false},
|
masterPassword: widget.Editor{SingleLine: true, Submit: false},
|
||||||
keyFilePath: widget.Editor{SingleLine: true, Submit: false},
|
keyFilePath: widget.Editor{SingleLine: true, Submit: false},
|
||||||
entryID: widget.Editor{SingleLine: true, Submit: false},
|
entryID: widget.Editor{SingleLine: true, Submit: false},
|
||||||
entryTitle: widget.Editor{SingleLine: true, Submit: false},
|
entryTitle: widget.Editor{SingleLine: true, Submit: false},
|
||||||
entryUsername: widget.Editor{SingleLine: true, Submit: false},
|
entryUsername: widget.Editor{SingleLine: true, Submit: false},
|
||||||
entryPassword: widget.Editor{SingleLine: true, Submit: false},
|
entryPassword: widget.Editor{SingleLine: true, Submit: false},
|
||||||
entryURL: widget.Editor{SingleLine: true, Submit: false},
|
entryURL: widget.Editor{SingleLine: true, Submit: false},
|
||||||
entryNotes: widget.Editor{SingleLine: false, Submit: false},
|
entryNotes: widget.Editor{SingleLine: false, Submit: false},
|
||||||
entryTags: widget.Editor{SingleLine: true, Submit: false},
|
entryTags: widget.Editor{SingleLine: true, Submit: false},
|
||||||
entryPath: widget.Editor{SingleLine: true, Submit: false},
|
entryPath: widget.Editor{SingleLine: true, Submit: false},
|
||||||
entryFields: widget.Editor{SingleLine: false, Submit: false},
|
entryFields: widget.Editor{SingleLine: false, Submit: false},
|
||||||
historyIndex: widget.Editor{SingleLine: true, Submit: false},
|
historyIndex: widget.Editor{SingleLine: true, Submit: false},
|
||||||
groupName: widget.Editor{SingleLine: true, Submit: false},
|
groupName: widget.Editor{SingleLine: true, Submit: false},
|
||||||
passwordProfile: widget.Editor{SingleLine: true, Submit: false},
|
passwordProfile: widget.Editor{SingleLine: true, Submit: false},
|
||||||
attachmentName: widget.Editor{SingleLine: true, Submit: false},
|
attachmentName: widget.Editor{SingleLine: true, Submit: false},
|
||||||
attachmentPath: widget.Editor{SingleLine: true, Submit: false},
|
attachmentPath: widget.Editor{SingleLine: true, Submit: false},
|
||||||
exportAttachmentPath: widget.Editor{SingleLine: true, Submit: false},
|
exportAttachmentPath: widget.Editor{SingleLine: true, Submit: false},
|
||||||
list: widget.List{
|
list: widget.List{
|
||||||
List: layout.List{Axis: layout.Vertical},
|
List: layout.List{Axis: layout.Vertical},
|
||||||
@@ -172,7 +177,8 @@ func newUIWithState(mode string, sess appstate.CurrentSession) *ui {
|
|||||||
detailList: widget.List{
|
detailList: widget.List{
|
||||||
List: layout.List{Axis: layout.Vertical},
|
List: layout.List{Axis: layout.Vertical},
|
||||||
},
|
},
|
||||||
state: appstate.State{},
|
state: appstate.State{},
|
||||||
|
masterKeyMode: vault.MasterKeyModePasswordOnly,
|
||||||
}
|
}
|
||||||
u.state.Session = sess
|
u.state.Session = sess
|
||||||
u.phoneSplit.Value = 0.46
|
u.phoneSplit.Value = 0.46
|
||||||
@@ -263,19 +269,44 @@ func (u *ui) selectedEntry() (entry, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) currentMasterKey() (vault.MasterKey, error) {
|
func (u *ui) currentMasterKey() (vault.MasterKey, error) {
|
||||||
key := vault.MasterKey{Password: u.masterPassword.Text()}
|
password := u.masterPassword.Text()
|
||||||
|
|
||||||
path := strings.TrimSpace(u.keyFilePath.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)
|
content, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vault.MasterKey{}, fmt.Errorf("read key file: %w", err)
|
return vault.MasterKey{}, fmt.Errorf("read key file: %w", err)
|
||||||
}
|
}
|
||||||
key.KeyFileData = content
|
if len(content) == 0 {
|
||||||
return key, nil
|
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 {
|
func (u *ui) createVaultAction() error {
|
||||||
@@ -359,6 +390,14 @@ func (u *ui) unlockAction() error {
|
|||||||
return nil
|
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) {
|
func (u *ui) runAction(label string, action func() error) {
|
||||||
if err := action(); err != nil {
|
if err := action(); err != nil {
|
||||||
u.errorMessage = err.Error()
|
u.errorMessage = err.Error()
|
||||||
@@ -392,9 +431,21 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
for u.openRemote.Clicked(gtx) {
|
for u.openRemote.Clicked(gtx) {
|
||||||
u.runAction("open remote vault", u.openRemoteAction)
|
u.runAction("open remote vault", u.openRemoteAction)
|
||||||
}
|
}
|
||||||
|
for u.changeMasterKey.Clicked(gtx) {
|
||||||
|
u.runAction("change master key", u.changeMasterKeyAction)
|
||||||
|
}
|
||||||
for u.unlockVault.Clicked(gtx) {
|
for u.unlockVault.Clicked(gtx) {
|
||||||
u.runAction("unlock vault", u.unlockAction)
|
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) {
|
for u.showEntries.Clicked(gtx) {
|
||||||
u.showEntriesSection()
|
u.showEntriesSection()
|
||||||
}
|
}
|
||||||
@@ -757,7 +808,7 @@ func (u *ui) phoneSlider(gtx layout.Context) layout.Dimensions {
|
|||||||
y := (gtx.Constraints.Min.Y - handleH) / 2
|
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, 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{
|
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,
|
NE: 2, NW: 2, SE: 2, SW: 2,
|
||||||
}.Op(gtx.Ops))
|
}.Op(gtx.Ops))
|
||||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||||
|
|||||||
+253
@@ -154,6 +154,258 @@ func TestUILifecycleActionsCreateSaveOpenLockAndUnlockLocalVault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIMasterKeyModesCreateOpenAndUnlockLocalVault(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode vault.MasterKeyMode
|
||||||
|
password string
|
||||||
|
keyFileData []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "password only",
|
||||||
|
mode: vault.MasterKeyModePasswordOnly,
|
||||||
|
password: "correct horse battery staple",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key file only",
|
||||||
|
mode: vault.MasterKeyModeKeyFileOnly,
|
||||||
|
keyFileData: []byte("key-file-only-material"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "composite",
|
||||||
|
mode: vault.MasterKeyModePasswordAndKeyFile,
|
||||||
|
password: "correct horse battery staple",
|
||||||
|
keyFileData: []byte("composite-key-material"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
keyFile := ""
|
||||||
|
if len(tt.keyFileData) > 0 {
|
||||||
|
keyFile = filepath.Join(t.TempDir(), "master.key")
|
||||||
|
if err := os.WriteFile(keyFile, tt.keyFileData, 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile(master.key) error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.setMasterKeyMode(tt.mode)
|
||||||
|
u.masterPassword.SetText(tt.password)
|
||||||
|
u.keyFilePath.SetText(keyFile)
|
||||||
|
|
||||||
|
if err := u.createVaultAction(); err != nil {
|
||||||
|
t.Fatalf("createVaultAction() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := u.state.UpsertEntry(vault.Entry{
|
||||||
|
ID: "vault-console",
|
||||||
|
Title: "Vault Console",
|
||||||
|
Username: "dannyocean",
|
||||||
|
Password: "token-1",
|
||||||
|
URL: "https://vault.crew.example.invalid",
|
||||||
|
Path: []string{"Root", "Internet"},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("UpsertEntry() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "keepassgo.kdbx")
|
||||||
|
u.saveAsPath.SetText(path)
|
||||||
|
if err := u.saveAsAction(); err != nil {
|
||||||
|
t.Fatalf("saveAsAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.lockAction(); err != nil {
|
||||||
|
t.Fatalf("lockAction() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := u.unlockAction(); err != nil {
|
||||||
|
t.Fatalf("unlockAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reopened := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
reopened.setMasterKeyMode(tt.mode)
|
||||||
|
reopened.masterPassword.SetText(tt.password)
|
||||||
|
reopened.keyFilePath.SetText(keyFile)
|
||||||
|
reopened.vaultPath.SetText(path)
|
||||||
|
if err := reopened.openVaultAction(); err != nil {
|
||||||
|
t.Fatalf("openVaultAction() error = %v", err)
|
||||||
|
}
|
||||||
|
reopened.currentPath = []string{"Root", "Internet"}
|
||||||
|
reopened.filter()
|
||||||
|
if got := reopened.filteredTitles(); !slices.Equal(got, []string{"Vault Console"}) {
|
||||||
|
t.Fatalf("reopened filteredTitles() = %v, want [Vault Console]", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIChangeMasterKeyModeForExistingVault(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
updated := filepath.Join(t.TempDir(), "updated.key")
|
||||||
|
if err := os.WriteFile(updated, []byte("updated-key"), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile(updated.key) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.setMasterKeyMode(vault.MasterKeyModePasswordOnly)
|
||||||
|
u.masterPassword.SetText("old-password")
|
||||||
|
if err := u.createVaultAction(); err != nil {
|
||||||
|
t.Fatalf("createVaultAction() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := u.state.UpsertEntry(vault.Entry{
|
||||||
|
ID: "vault-console",
|
||||||
|
Title: "Vault Console",
|
||||||
|
Path: []string{"Root", "Internet"},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("UpsertEntry() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "keepassgo.kdbx")
|
||||||
|
u.saveAsPath.SetText(path)
|
||||||
|
if err := u.saveAsAction(); err != nil {
|
||||||
|
t.Fatalf("saveAsAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
|
||||||
|
u.masterPassword.SetText("new-password")
|
||||||
|
u.keyFilePath.SetText(updated)
|
||||||
|
if err := u.changeMasterKeyAction(); err != nil {
|
||||||
|
t.Fatalf("changeMasterKeyAction() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := u.saveAction(); err != nil {
|
||||||
|
t.Fatalf("saveAction() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := u.lockAction(); err != nil {
|
||||||
|
t.Fatalf("lockAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.masterPassword.SetText("old-password")
|
||||||
|
u.keyFilePath.SetText("")
|
||||||
|
u.setMasterKeyMode(vault.MasterKeyModePasswordOnly)
|
||||||
|
u.runAction("unlock vault", u.unlockAction)
|
||||||
|
if u.errorMessage == "" {
|
||||||
|
t.Fatal("errorMessage = empty, want visible invalid master key error")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
|
||||||
|
u.masterPassword.SetText("new-password")
|
||||||
|
u.keyFilePath.SetText(updated)
|
||||||
|
if err := u.unlockAction(); err != nil {
|
||||||
|
t.Fatalf("unlockAction() with updated key error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reopened := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
reopened.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
|
||||||
|
reopened.masterPassword.SetText("new-password")
|
||||||
|
reopened.keyFilePath.SetText(updated)
|
||||||
|
reopened.vaultPath.SetText(path)
|
||||||
|
if err := reopened.openVaultAction(); err != nil {
|
||||||
|
t.Fatalf("openVaultAction() with updated key error = %v", err)
|
||||||
|
}
|
||||||
|
reopened.currentPath = []string{"Root", "Internet"}
|
||||||
|
reopened.filter()
|
||||||
|
if got := reopened.filteredTitles(); !slices.Equal(got, []string{"Vault Console"}) {
|
||||||
|
t.Fatalf("reopened filteredTitles() = %v, want [Vault Console]", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIMasterKeyValidationErrorsAreVisible(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode vault.MasterKeyMode
|
||||||
|
password string
|
||||||
|
keyFile string
|
||||||
|
wantError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "password mode requires password",
|
||||||
|
mode: vault.MasterKeyModePasswordOnly,
|
||||||
|
wantError: "master password is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key file mode requires path",
|
||||||
|
mode: vault.MasterKeyModeKeyFileOnly,
|
||||||
|
wantError: "key file is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "composite mode requires password",
|
||||||
|
mode: vault.MasterKeyModePasswordAndKeyFile,
|
||||||
|
keyFile: filepath.Join("/tmp", "ignored.key"),
|
||||||
|
wantError: "master password is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.setMasterKeyMode(tt.mode)
|
||||||
|
u.masterPassword.SetText(tt.password)
|
||||||
|
u.keyFilePath.SetText(tt.keyFile)
|
||||||
|
|
||||||
|
u.runAction("create vault", u.createVaultAction)
|
||||||
|
|
||||||
|
if got := u.errorMessage; got != tt.wantError {
|
||||||
|
t.Fatalf("errorMessage = %q, want %q", got, tt.wantError)
|
||||||
|
}
|
||||||
|
if got := u.statusMessage; got != "" {
|
||||||
|
t.Fatalf("statusMessage = %q, want empty on validation error", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIUnreadableAndInvalidMasterKeyErrorsAreVisible(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
keyFile := filepath.Join(t.TempDir(), "master.key")
|
||||||
|
if err := os.WriteFile(keyFile, []byte("key-material"), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile(master.key) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
create := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
create.setMasterKeyMode(vault.MasterKeyModeKeyFileOnly)
|
||||||
|
create.keyFilePath.SetText(keyFile)
|
||||||
|
if err := create.createVaultAction(); err != nil {
|
||||||
|
t.Fatalf("createVaultAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "keepassgo.kdbx")
|
||||||
|
create.saveAsPath.SetText(path)
|
||||||
|
if err := create.saveAsAction(); err != nil {
|
||||||
|
t.Fatalf("saveAsAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unreadable := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
unreadable.setMasterKeyMode(vault.MasterKeyModeKeyFileOnly)
|
||||||
|
unreadable.keyFilePath.SetText(filepath.Join(t.TempDir(), "missing.key"))
|
||||||
|
unreadable.runAction("open vault", unreadable.openVaultAction)
|
||||||
|
if got := unreadable.errorMessage; got == "" || got[:14] != "read key file:" {
|
||||||
|
t.Fatalf("errorMessage = %q, want read key file error", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
wrong := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
wrong.setMasterKeyMode(vault.MasterKeyModeKeyFileOnly)
|
||||||
|
wrong.keyFilePath.SetText(filepath.Join(t.TempDir(), "wrong.key"))
|
||||||
|
if err := os.WriteFile(wrong.keyFilePath.Text(), []byte("wrong-key"), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile(wrong.key) error = %v", err)
|
||||||
|
}
|
||||||
|
wrong.vaultPath.SetText(path)
|
||||||
|
wrong.runAction("open vault", wrong.openVaultAction)
|
||||||
|
if got := wrong.errorMessage; got == "" || !bytes.Contains([]byte(got), []byte(vault.ErrInvalidMasterKey.Error())) {
|
||||||
|
t.Fatalf("errorMessage = %q, want invalid master key error", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIOpenRemoteAndSaveThroughConfiguredWebDAVTarget(t *testing.T) {
|
func TestUIOpenRemoteAndSaveThroughConfiguredWebDAVTarget(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -230,6 +482,7 @@ func TestUIMasterKeyInputSupportsKeyFileAndCompositeKeys(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
u := newUIWithSession("desktop", &session.Manager{})
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
|
||||||
u.masterPassword.SetText("correct horse battery staple")
|
u.masterPassword.SetText("correct horse battery staple")
|
||||||
u.keyFilePath.SetText(keyFile)
|
u.keyFilePath.SetText(keyFile)
|
||||||
|
|
||||||
|
|||||||
+55
-11
@@ -99,17 +99,17 @@ func (m *Manager) SaveRemote() error {
|
|||||||
return ErrNoPath
|
return ErrNoPath
|
||||||
}
|
}
|
||||||
|
|
||||||
var encoded bytes.Buffer
|
encoded, err := m.persistableBytes()
|
||||||
if err := vault.SaveKDBXWithConfigAndKey(&encoded, m.model, m.key, m.config); err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encode vault: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
version, err := m.remoteClient.Save(m.remotePath, bytes.NewReader(encoded.Bytes()), m.remoteVersion)
|
version, err := m.remoteClient.Save(m.remotePath, bytes.NewReader(encoded), m.remoteVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("save remote %s: %w", m.remotePath, err)
|
return fmt.Errorf("save remote %s: %w", m.remotePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.encoded = encoded.Bytes()
|
m.encoded = encoded
|
||||||
m.remoteVersion = version
|
m.remoteVersion = version
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -165,16 +165,60 @@ func (m *Manager) Unlock(key vault.MasterKey) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) saveToPath(path string) error {
|
func (m *Manager) ChangeMasterKey(key vault.MasterKey) error {
|
||||||
var encoded bytes.Buffer
|
var (
|
||||||
if err := vault.SaveKDBXWithConfigAndKey(&encoded, m.model, m.key, m.config); err != nil {
|
model vault.Model
|
||||||
return fmt.Errorf("encode vault: %w", err)
|
config *vault.KDBXConfig
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if m.locked {
|
||||||
|
model, config, err = vault.LoadKDBXWithConfig(bytes.NewReader(m.encoded), m.key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decode locked vault: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model = m.model
|
||||||
|
config = m.config
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(path, encoded.Bytes(), 0o600); err != nil {
|
var encoded bytes.Buffer
|
||||||
|
if err := vault.SaveKDBXWithConfigAndKey(&encoded, model, key, config); err != nil {
|
||||||
|
return fmt.Errorf("encode vault with updated master key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.key = key
|
||||||
|
m.config = config
|
||||||
|
m.encoded = encoded.Bytes()
|
||||||
|
if !m.locked {
|
||||||
|
m.model = model
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) saveToPath(path string) error {
|
||||||
|
encoded, err := m.persistableBytes()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, encoded, 0o600); err != nil {
|
||||||
return fmt.Errorf("write %s: %w", path, err)
|
return fmt.Errorf("write %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.encoded = encoded.Bytes()
|
m.encoded = encoded
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) persistableBytes() ([]byte, error) {
|
||||||
|
if m.locked {
|
||||||
|
return append([]byte(nil), m.encoded...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoded bytes.Buffer
|
||||||
|
if err := vault.SaveKDBXWithConfigAndKey(&encoded, m.model, m.key, m.config); err != nil {
|
||||||
|
return nil, fmt.Errorf("encode vault: %w", err)
|
||||||
|
}
|
||||||
|
return encoded.Bytes(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -391,6 +391,68 @@ func TestSaveUsesRemoteTargetWhenVaultWasOpenedFromWebDAV(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChangeMasterKeyReencryptsSavedAndLockedVault(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
originalKey := vault.MasterKey{Password: "old-password"}
|
||||||
|
updatedKey := vault.MasterKey{
|
||||||
|
Password: "new-password",
|
||||||
|
KeyFileData: []byte("updated-key-file"),
|
||||||
|
}
|
||||||
|
model := vault.Model{
|
||||||
|
Entries: []vault.Entry{
|
||||||
|
{
|
||||||
|
ID: "entry-1",
|
||||||
|
Title: "Vault Console",
|
||||||
|
Username: "dannyocean",
|
||||||
|
Password: "token-1",
|
||||||
|
URL: "https://vault.crew.example.invalid",
|
||||||
|
Path: []string{"Root", "Internet"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "keepassgo.kdbx")
|
||||||
|
|
||||||
|
var sess Manager
|
||||||
|
if err := sess.Create(model, originalKey); err != nil {
|
||||||
|
t.Fatalf("Create() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := sess.SaveAs(path); err != nil {
|
||||||
|
t.Fatalf("SaveAs() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := sess.Lock(); err != nil {
|
||||||
|
t.Fatalf("Lock() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := sess.ChangeMasterKey(updatedKey); err != nil {
|
||||||
|
t.Fatalf("ChangeMasterKey() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := sess.Save(); err != nil {
|
||||||
|
t.Fatalf("Save() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sess.Unlock(updatedKey); err != nil {
|
||||||
|
t.Fatalf("Unlock(updatedKey) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
current, err := sess.Current()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Current() error = %v", err)
|
||||||
|
}
|
||||||
|
got := current.EntriesInPath([]string{"Root", "Internet"})
|
||||||
|
if len(got) != 1 || got[0].Title != "Vault Console" {
|
||||||
|
t.Fatalf("Current() entries = %#v, want Vault Console entry after ChangeMasterKey", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reopened Manager
|
||||||
|
if err := reopened.Open(path, updatedKey); err != nil {
|
||||||
|
t.Fatalf("Open(updatedKey) error = %v", err)
|
||||||
|
}
|
||||||
|
if err := reopened.Open(path, originalKey); !errors.Is(err, vault.ErrInvalidMasterKey) {
|
||||||
|
t.Fatalf("Open(originalKey) error = %v, want ErrInvalidMasterKey", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSavePreservesOpenedKDBXSecuritySettings(t *testing.T) {
|
func TestSavePreservesOpenedKDBXSecuritySettings(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
+75
-17
@@ -3,15 +3,37 @@ package main
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.julianfamily.org/keepassgo/appstate"
|
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"gioui.org/widget"
|
"gioui.org/widget"
|
||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
|
"git.julianfamily.org/keepassgo/appstate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(12), "MASTER KEY MODE")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.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.masterKeyPasswordOnly, "Password Only")
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.masterKeyKeyFileOnly, "Key File Only")
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.masterKeyComposite, "Password + Key File")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(labeledEditor(u.theme, "Master Password", &u.masterPassword, true)),
|
layout.Rigid(labeledEditor(u.theme, "Master Password", &u.masterPassword, true)),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(labeledEditor(u.theme, "Key File", &u.keyFilePath, false)),
|
layout.Rigid(labeledEditor(u.theme, "Key File", &u.keyFilePath, false)),
|
||||||
@@ -36,9 +58,17 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.saveVault, "Save") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.saveVault, "Save") }),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.saveAsVault, "Save As") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.saveAsVault, "Save As")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.openRemote, "Open Remote") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.openRemote, "Open Remote")
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.changeMasterKey, "Change Master Key")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.unlockVault, "Unlock") }),
|
||||||
)
|
)
|
||||||
@@ -55,11 +85,17 @@ func (u *ui) groupControls(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.createGroup, "Create Group") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.createGroup, "Create Group")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.renameGroup, "Rename Group") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.renameGroup, "Rename Group")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.deleteGroup, "Delete Group") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.deleteGroup, "Delete Group")
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -93,25 +129,39 @@ func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
switch u.state.Section {
|
switch u.state.Section {
|
||||||
case appstate.SectionTemplates:
|
case appstate.SectionTemplates:
|
||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.saveTemplate, "Save Template") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.saveTemplate, "Save Template")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.deleteTemplate, "Delete Template") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.deleteTemplate, "Delete Template")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.instantiateTemplate, "Instantiate") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.instantiateTemplate, "Instantiate")
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
case appstate.SectionRecycleBin:
|
case appstate.SectionRecycleBin:
|
||||||
return tonedButton(gtx, u.theme, &u.restoreEntry, "Restore Entry")
|
return tonedButton(gtx, u.theme, &u.restoreEntry, "Restore Entry")
|
||||||
default:
|
default:
|
||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.saveEntry, "Save Entry") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.saveEntry, "Save Entry")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.duplicateEntry, "Duplicate") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.duplicateEntry, "Duplicate")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.deleteEntry, "Delete") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.deleteEntry, "Delete") }),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.generatePassword, "Generate Password") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.generatePassword, "Generate Password")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.restoreHistory, "Restore History") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.restoreHistory, "Restore History")
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -120,7 +170,9 @@ func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.copyUser, "Copy User") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.copyUser, "Copy User") }),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.copyPass, "Copy Password") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.copyPass, "Copy Password")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.copyURL, "Copy URL") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.copyURL, "Copy URL") }),
|
||||||
)
|
)
|
||||||
@@ -134,11 +186,17 @@ func (u *ui) entryEditorPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.addAttachment, "Add Attachment") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.addAttachment, "Add Attachment")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.removeAttachment, "Remove Attachment") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.removeAttachment, "Remove Attachment")
|
||||||
|
}),
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.exportAttachment, "Export Attachment") }),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.exportAttachment, "Export Attachment")
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package vault
|
||||||
|
|
||||||
|
// MasterKeyMode identifies which key material the user intends to provide.
|
||||||
|
type MasterKeyMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MasterKeyModePasswordOnly requires a master password and no key file.
|
||||||
|
MasterKeyModePasswordOnly MasterKeyMode = "password-only"
|
||||||
|
// MasterKeyModeKeyFileOnly requires a key file and no password.
|
||||||
|
MasterKeyModeKeyFileOnly MasterKeyMode = "key-file-only"
|
||||||
|
// MasterKeyModePasswordAndKeyFile requires both password and key file.
|
||||||
|
MasterKeyModePasswordAndKeyFile MasterKeyMode = "password-and-key-file"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user