779 lines
22 KiB
Go
779 lines
22 KiB
Go
package appui
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"gioui.org/layout"
|
|
"gioui.org/x/explorer"
|
|
"git.julianfamily.org/keepassgo/internal/appstate"
|
|
"git.julianfamily.org/keepassgo/internal/session"
|
|
"git.julianfamily.org/keepassgo/internal/vault"
|
|
"git.julianfamily.org/keepassgo/internal/webdav"
|
|
)
|
|
|
|
func (u *ui) createVaultAction() error {
|
|
key, err := u.currentMasterKey()
|
|
defer u.clearMasterPassword()
|
|
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
|
|
}
|
|
if u.lifecycleMode == "local" {
|
|
u.selectedVaultRemoteProfileID = ""
|
|
u.selectedVaultRemoteCredentialEntryID = ""
|
|
u.selectedVaultRemoteSyncMode = appstate.SyncModeManual
|
|
u.remoteBaseURL.SetText("")
|
|
u.remotePath.SetText("")
|
|
u.remoteUsername.SetText("")
|
|
u.remotePassword.SetText("")
|
|
if err := u.state.SaveAs(u.saveAsTargetPath()); err != nil {
|
|
return err
|
|
}
|
|
u.vaultPath.SetText(u.saveAsTargetPath())
|
|
u.noteRecentVault(u.saveAsTargetPath())
|
|
}
|
|
u.resetPasswordPeek()
|
|
u.adoptStateCurrentPath()
|
|
u.loadSecuritySettingsFromSession()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) openVaultAction() error {
|
|
key, err := u.currentMasterKey()
|
|
defer u.clearMasterPassword()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
path := strings.TrimSpace(u.vaultPath.Text())
|
|
if path == "" {
|
|
return errors.New(errVaultPathRequired)
|
|
}
|
|
if err := u.state.OpenVault(path, key); err != nil {
|
|
return err
|
|
}
|
|
u.noteRecentVault(path)
|
|
u.resetPasswordPeek()
|
|
u.adoptStateCurrentPath()
|
|
u.restoreRecentVaultGroup(path)
|
|
u.syncSavedRemoteBindingSelection()
|
|
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
|
|
u.showStatusMessage("Remote sync on open failed: " + err.Error())
|
|
}
|
|
u.loadSecuritySettingsFromSession()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
u.applyPendingLifecycleOpenIntent()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) startOpenVaultAction() {
|
|
manager, ok := u.state.Session.(*session.Manager)
|
|
if !ok {
|
|
u.runAction("open vault", u.openVaultAction)
|
|
return
|
|
}
|
|
key, err := u.currentMasterKey()
|
|
u.clearMasterPassword()
|
|
if err != nil {
|
|
u.state.ErrorMessage = u.describeActionError("open vault", err)
|
|
u.requestMasterPassFocus = true
|
|
return
|
|
}
|
|
path := strings.TrimSpace(u.vaultPath.Text())
|
|
if path == "" {
|
|
u.state.ErrorMessage = u.describeActionError("open vault", errors.New(errVaultPathRequired))
|
|
u.requestMasterPassFocus = true
|
|
return
|
|
}
|
|
u.lastLifecycleAction = "open vault"
|
|
u.runBackgroundAction("open vault", func() (func() error, error) {
|
|
prepared, err := session.PrepareLocalOpen(path, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func() error {
|
|
manager.ApplyPreparedLocalOpen(prepared)
|
|
u.noteRecentVault(path)
|
|
u.resetPasswordPeek()
|
|
u.adoptStateCurrentPath()
|
|
u.restoreRecentVaultGroup(path)
|
|
u.syncSavedRemoteBindingSelection()
|
|
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
|
|
u.showStatusMessage("Remote sync on open failed: " + err.Error())
|
|
}
|
|
u.loadSecuritySettingsFromSession()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
u.applyPendingLifecycleOpenIntent()
|
|
return nil
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
func (u *ui) shouldShowLifecycleRemoteSyncAction() bool {
|
|
return strings.TrimSpace(u.vaultPath.Text()) != ""
|
|
}
|
|
|
|
func (u *ui) lifecycleRemoteSyncActionLabel() string {
|
|
path := strings.TrimSpace(u.vaultPath.Text())
|
|
if path == "" {
|
|
return "Open Vault And Set Up Remote Sync"
|
|
}
|
|
if hasBoundRecentRemote(u.recentRemotes, path) {
|
|
return "Open Vault And Open Remote Sync Settings"
|
|
}
|
|
return "Open Vault And Set Up Remote Sync"
|
|
}
|
|
|
|
func (u *ui) beginLifecycleRemoteSyncOpen() {
|
|
path := strings.TrimSpace(u.vaultPath.Text())
|
|
switch {
|
|
case path == "":
|
|
u.pendingLifecycleOpenIntent = lifecycleOpenIntentNone
|
|
case hasBoundRecentRemote(u.recentRemotes, path):
|
|
u.pendingLifecycleOpenIntent = lifecycleOpenIntentRemoteSyncSettings
|
|
default:
|
|
u.pendingLifecycleOpenIntent = lifecycleOpenIntentRemoteSyncSetup
|
|
}
|
|
u.startOpenVaultAction()
|
|
}
|
|
|
|
func (u *ui) applyPendingLifecycleOpenIntent() {
|
|
intent := u.pendingLifecycleOpenIntent
|
|
u.pendingLifecycleOpenIntent = lifecycleOpenIntentNone
|
|
switch intent {
|
|
case lifecycleOpenIntentRemoteSyncSetup, lifecycleOpenIntentRemoteSyncSettings:
|
|
u.openRemoteSyncSetupDialog()
|
|
}
|
|
}
|
|
|
|
func (u *ui) saveAction() error {
|
|
if err := u.state.Save(); err != nil {
|
|
return err
|
|
}
|
|
if err := u.synchronizeSelectedRemoteBindingOnSave(); err != nil {
|
|
return err
|
|
}
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) saveAsAction() error {
|
|
path := u.saveAsTargetPath()
|
|
if err := u.state.SaveAs(path); err != nil {
|
|
return err
|
|
}
|
|
u.vaultPath.SetText(path)
|
|
u.noteRecentVault(path)
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) openRemoteAction() error {
|
|
key, err := u.currentMasterKey()
|
|
defer u.clearMasterPassword()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if binding, resolved, ok, err := u.bootstrapSelectedVaultRemoteBinding(key); err != nil {
|
|
return err
|
|
} else if ok {
|
|
if err := u.state.OpenBoundRemoteVault(binding, key); err != nil {
|
|
return err
|
|
}
|
|
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
|
u.remotePath.SetText(resolved.Profile.Path)
|
|
u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path)
|
|
u.resetPasswordPeek()
|
|
u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path)
|
|
u.loadSecuritySettingsFromSession()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
return nil
|
|
}
|
|
client := webdav.Client{
|
|
BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
|
Username: strings.TrimSpace(u.remoteUsername.Text()),
|
|
Password: u.remotePassword.Text(),
|
|
}
|
|
if err := u.state.OpenRemoteVault(client, strings.TrimSpace(u.remotePath.Text()), key); err != nil {
|
|
return err
|
|
}
|
|
if err := u.materializeCurrentRemoteCache(); err != nil {
|
|
return err
|
|
}
|
|
u.noteRecentRemote(
|
|
strings.TrimSpace(u.remoteBaseURL.Text()),
|
|
strings.TrimSpace(u.remotePath.Text()),
|
|
)
|
|
u.resetPasswordPeek()
|
|
u.restoreRecentRemoteGroup(strings.TrimSpace(u.remoteBaseURL.Text()), strings.TrimSpace(u.remotePath.Text()))
|
|
u.loadSecuritySettingsFromSession()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) startOpenRemoteAction() {
|
|
manager, ok := u.state.Session.(*session.Manager)
|
|
if !ok {
|
|
u.runAction("open remote vault", u.openRemoteAction)
|
|
return
|
|
}
|
|
key, err := u.currentMasterKey()
|
|
u.clearMasterPassword()
|
|
if err != nil {
|
|
u.state.ErrorMessage = u.describeActionError("open remote vault", err)
|
|
u.requestMasterPassFocus = true
|
|
return
|
|
}
|
|
client := webdav.Client{
|
|
BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
|
Username: strings.TrimSpace(u.remoteUsername.Text()),
|
|
Password: u.remotePassword.Text(),
|
|
}
|
|
remotePath := strings.TrimSpace(u.remotePath.Text())
|
|
u.lastLifecycleAction = "open remote vault"
|
|
u.runBackgroundAction("open remote vault", func() (func() error, error) {
|
|
binding, bindingOK := u.selectedVaultRemoteBinding()
|
|
if bindingOK && !u.hasOpenVault() && strings.TrimSpace(binding.LocalVaultPath) != "" {
|
|
preparedLocal, err := session.PrepareLocalOpen(binding.LocalVaultPath, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resolved, err := binding.Resolve(preparedLocal.Model)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
preparedRemote, err := session.PrepareRemoteOpen(webdav.Client{
|
|
BaseURL: resolved.Profile.BaseURL,
|
|
Username: resolved.Credentials.Username,
|
|
Password: resolved.Credentials.Password,
|
|
}, resolved.Profile.Path, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func() error {
|
|
manager.ApplyPreparedLocalOpen(preparedLocal)
|
|
u.vaultPath.SetText(binding.LocalVaultPath)
|
|
u.noteRecentVault(binding.LocalVaultPath)
|
|
u.restoreRecentVaultGroup(binding.LocalVaultPath)
|
|
manager.ApplyPreparedRemoteOpen(preparedRemote)
|
|
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
|
u.remotePath.SetText(resolved.Profile.Path)
|
|
u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path)
|
|
u.resetPasswordPeek()
|
|
u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path)
|
|
u.loadSecuritySettingsFromSession()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
return nil
|
|
}, nil
|
|
}
|
|
if u.hasOpenVault() {
|
|
if _, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil {
|
|
return nil, err
|
|
} else if ok {
|
|
client = webdav.Client{
|
|
BaseURL: resolved.Profile.BaseURL,
|
|
Username: resolved.Credentials.Username,
|
|
Password: resolved.Credentials.Password,
|
|
}
|
|
remotePath = resolved.Profile.Path
|
|
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
|
u.remotePath.SetText(resolved.Profile.Path)
|
|
}
|
|
}
|
|
prepared, err := session.PrepareRemoteOpen(client, remotePath, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func() error {
|
|
manager.ApplyPreparedRemoteOpen(prepared)
|
|
if err := u.materializeCurrentRemoteCache(); err != nil {
|
|
return err
|
|
}
|
|
u.noteRecentRemote(
|
|
strings.TrimSpace(u.remoteBaseURL.Text()),
|
|
remotePath,
|
|
)
|
|
u.resetPasswordPeek()
|
|
u.restoreRecentRemoteGroup(strings.TrimSpace(u.remoteBaseURL.Text()), remotePath)
|
|
u.loadSecuritySettingsFromSession()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
return nil
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
func (u *ui) lockAction() error {
|
|
u.clearMasterPassword()
|
|
if err := u.state.Lock(); err != nil {
|
|
return err
|
|
}
|
|
u.requestMasterPassFocus = true
|
|
u.adoptStateCurrentPath()
|
|
u.resetPasswordPeek()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) unlockAction() error {
|
|
key, err := u.currentMasterKey()
|
|
defer u.clearMasterPassword()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := u.state.Unlock(key); err != nil {
|
|
return err
|
|
}
|
|
u.resetPasswordPeek()
|
|
u.adoptStateCurrentPath()
|
|
u.loadSecuritySettingsFromSession()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) startUnlockAction() {
|
|
manager, ok := u.state.Session.(*session.Manager)
|
|
if !ok {
|
|
u.runAction("unlock vault", u.unlockAction)
|
|
return
|
|
}
|
|
key, err := u.currentMasterKey()
|
|
u.clearMasterPassword()
|
|
if err != nil {
|
|
u.state.ErrorMessage = u.describeActionError("unlock vault", err)
|
|
u.requestMasterPassFocus = true
|
|
return
|
|
}
|
|
encoded := append([]byte(nil), manager.EncodedBytes()...)
|
|
u.runBackgroundAction("unlock vault", func() (func() error, error) {
|
|
prepared, err := session.PrepareUnlock(encoded, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func() error {
|
|
manager.ApplyPreparedUnlock(prepared)
|
|
u.resetPasswordPeek()
|
|
u.adoptStateCurrentPath()
|
|
u.loadSecuritySettingsFromSession()
|
|
u.editingEntry = false
|
|
u.filter()
|
|
return nil
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
func (u *ui) changeMasterKeyAction() error {
|
|
key, err := u.currentMasterKey()
|
|
defer u.clearMasterPassword()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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) clearMasterPassword() {
|
|
u.masterPassword.SetText("")
|
|
}
|
|
|
|
func (u *ui) synchronizeAction() error {
|
|
if err := u.state.Synchronize(); err != nil {
|
|
return err
|
|
}
|
|
u.syncMenuOpen = false
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) openAdvancedSyncDialog() {
|
|
u.syncDialogOpen = true
|
|
u.syncMenuOpen = false
|
|
u.showSyncPassword = false
|
|
u.syncDialogList.Position = layout.Position{}
|
|
u.syncDialogPurpose = syncDialogPurposeAdvanced
|
|
u.syncSourceMode = u.syncDefaultSourceMode
|
|
u.syncDirection = u.syncDefaultDirection
|
|
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
|
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
|
}
|
|
u.syncSavedRemoteBindingSelection()
|
|
u.prefillAdvancedSyncRemoteFromSavedBinding()
|
|
}
|
|
|
|
func (u *ui) openRemoteSyncSetupDialog() {
|
|
u.syncDialogOpen = true
|
|
u.syncMenuOpen = false
|
|
u.showSyncPassword = false
|
|
u.syncDialogList.Position = layout.Position{}
|
|
u.syncDialogPurpose = syncDialogPurposeRemoteSetup
|
|
u.syncSourceMode = syncSourceRemote
|
|
u.syncDirection = syncDirectionPush
|
|
u.syncSetupAutomatic.Value = true
|
|
if strings.TrimSpace(u.syncLocalPath.Text()) == "" {
|
|
u.syncLocalPath.SetText(strings.TrimSpace(u.vaultPath.Text()))
|
|
}
|
|
u.syncSavedRemoteBindingSelection()
|
|
u.prefillAdvancedSyncRemoteFromSavedBinding()
|
|
if _, ok := u.selectedVaultRemoteBinding(); ok && u.selectedVaultRemoteSyncMode == appstate.SyncModeManual {
|
|
u.syncSetupAutomatic.Value = false
|
|
}
|
|
}
|
|
|
|
func (u *ui) clearSyncLocalImport() {
|
|
u.syncLocalImportName = ""
|
|
u.syncLocalImportContent = nil
|
|
}
|
|
|
|
func (u *ui) selectedSyncLocalImport() (string, []byte, bool) {
|
|
name := strings.TrimSpace(u.syncLocalImportName)
|
|
if name == "" || name != strings.TrimSpace(u.syncLocalPath.Text()) || len(u.syncLocalImportContent) == 0 {
|
|
return "", nil, false
|
|
}
|
|
return name, append([]byte(nil), u.syncLocalImportContent...), true
|
|
}
|
|
|
|
func sanitizeSyncSourceMode(mode syncSourceMode) syncSourceMode {
|
|
switch mode {
|
|
case syncSourceRemote:
|
|
return syncSourceRemote
|
|
default:
|
|
return syncSourceLocal
|
|
}
|
|
}
|
|
|
|
func sanitizeSyncDirection(direction syncDirection) syncDirection {
|
|
switch direction {
|
|
case syncDirectionPush:
|
|
return syncDirectionPush
|
|
default:
|
|
return syncDirectionPull
|
|
}
|
|
}
|
|
|
|
func (u *ui) advancedSyncAction() error {
|
|
switch u.syncDirection {
|
|
case syncDirectionPush:
|
|
return u.advancedSyncToAction()
|
|
default:
|
|
return u.advancedSyncFromAction()
|
|
}
|
|
}
|
|
|
|
func (u *ui) advancedSyncFromAction() error {
|
|
switch u.syncSourceMode {
|
|
case syncSourceRemote:
|
|
client := webdav.Client{
|
|
BaseURL: strings.TrimSpace(u.syncRemoteBaseURL.Text()),
|
|
Username: strings.TrimSpace(u.syncRemoteUsername.Text()),
|
|
Password: u.syncRemotePassword.Text(),
|
|
}
|
|
if err := u.state.SynchronizeFromRemote(client, strings.TrimSpace(u.syncRemotePath.Text())); err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
if name, content, ok := u.selectedSyncLocalImport(); ok {
|
|
if err := u.state.SynchronizeFromLocalBytes(name, content); err != nil {
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
path := strings.TrimSpace(u.syncLocalPath.Text())
|
|
if path == "" {
|
|
return errors.New(errVaultPathRequired)
|
|
}
|
|
if err := u.state.SynchronizeFromLocal(path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
u.syncDialogOpen = false
|
|
u.showSyncPassword = false
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) startChooseSyncLocalSourceAction() {
|
|
if runtime.GOOS != "android" || u.fileExplorer == nil {
|
|
u.runAction("choose sync path", func() error {
|
|
u.clearSyncLocalImport()
|
|
return u.chooseExistingFileAction(&u.syncLocalPath)
|
|
})
|
|
return
|
|
}
|
|
u.runBackgroundAction("choose sync file", func() (func() error, error) {
|
|
file, err := u.fileExplorer.ChooseFile(".kdbx")
|
|
if err != nil {
|
|
if errors.Is(err, explorer.ErrUserDecline) {
|
|
return func() error { return nil }, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
content, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
label := "Selected Android vault"
|
|
return func() error {
|
|
u.syncLocalImportName = label
|
|
u.syncLocalImportContent = append([]byte(nil), content...)
|
|
u.syncLocalPath.SetText(label)
|
|
return nil
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
func pickedDocumentName(file io.ReadCloser, fallback string) string {
|
|
if named, ok := file.(interface{ Name() string }); ok {
|
|
if base := filepath.Base(strings.TrimSpace(named.Name())); base != "" && base != "." && base != string(filepath.Separator) {
|
|
return base
|
|
}
|
|
}
|
|
fallback = filepath.Base(strings.TrimSpace(fallback))
|
|
if fallback == "" || fallback == "." || fallback == string(filepath.Separator) {
|
|
return "selected-vault.kdbx"
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func (u *ui) startChooseVaultPathAction() {
|
|
if runtime.GOOS != "android" || u.fileExplorer == nil {
|
|
u.runAction("choose vault path", func() error { return u.chooseExistingFileAction(&u.vaultPath) })
|
|
return
|
|
}
|
|
u.runBackgroundAction("choose vault file", func() (func() error, error) {
|
|
file, err := u.fileExplorer.ChooseFile(".kdbx")
|
|
if err != nil {
|
|
if errors.Is(err, explorer.ErrUserDecline) {
|
|
return func() error { return nil }, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
content, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
name := pickedDocumentName(file, "selected-vault.kdbx")
|
|
return func() error {
|
|
return u.importSharedVaultBytesAction(name, content)
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
func (u *ui) startImportSharedVaultAction() {
|
|
if !supportsSharedVaultImport(runtime.GOOS) || u.fileExplorer == nil {
|
|
return
|
|
}
|
|
u.runBackgroundAction("import shared vault", func() (func() error, error) {
|
|
file, err := u.fileExplorer.ChooseFile(".kdbx")
|
|
if err != nil {
|
|
if errors.Is(err, explorer.ErrUserDecline) {
|
|
return func() error { return nil }, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
content, err := io.ReadAll(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return func() error {
|
|
return u.importSharedVaultBytesAction("shared-vault.kdbx", content)
|
|
}, nil
|
|
})
|
|
}
|
|
|
|
func (u *ui) advancedSyncToAction() error {
|
|
switch u.syncSourceMode {
|
|
case syncSourceRemote:
|
|
baseURL := strings.TrimSpace(u.syncRemoteBaseURL.Text())
|
|
remotePath := strings.TrimSpace(u.syncRemotePath.Text())
|
|
client := webdav.Client{
|
|
BaseURL: baseURL,
|
|
Username: strings.TrimSpace(u.syncRemoteUsername.Text()),
|
|
Password: u.syncRemotePassword.Text(),
|
|
}
|
|
if err := u.state.SynchronizeToRemote(client, remotePath); err != nil {
|
|
return err
|
|
}
|
|
if u.syncDialogPurpose == syncDialogPurposeRemoteSetup {
|
|
if err := u.persistSyncDialogRemoteBinding(baseURL, remotePath); err != nil {
|
|
return err
|
|
}
|
|
u.showStatusMessage("Remote sync is set up for this vault.")
|
|
}
|
|
default:
|
|
path := strings.TrimSpace(u.syncLocalPath.Text())
|
|
if path == "" {
|
|
return errors.New(errVaultPathRequired)
|
|
}
|
|
if err := u.state.SynchronizeToLocal(path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
u.syncDialogOpen = false
|
|
u.showSyncPassword = false
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) persistSyncDialogRemoteBinding(baseURL, remotePath string) error {
|
|
baseURL = strings.TrimSpace(baseURL)
|
|
remotePath = strings.TrimSpace(remotePath)
|
|
if baseURL == "" || remotePath == "" {
|
|
return fmt.Errorf("remote setup requires base URL and path")
|
|
}
|
|
input := appstate.RemoteBindingInput{
|
|
LocalVaultPath: strings.TrimSpace(u.vaultPath.Text()),
|
|
RemoteProfileID: "remote-profile-" + remoteBindingSuffix(baseURL, remotePath, strings.TrimSpace(u.syncRemoteUsername.Text())),
|
|
RemoteProfileName: friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: remotePath}),
|
|
BaseURL: baseURL,
|
|
RemotePath: remotePath,
|
|
CredentialEntryID: "remote-credential-" + remoteBindingSuffix(baseURL, remotePath, strings.TrimSpace(u.syncRemoteUsername.Text())),
|
|
CredentialTitle: "WebDAV Sign-In" + func() string {
|
|
if user := strings.TrimSpace(u.syncRemoteUsername.Text()); user != "" {
|
|
return " · " + user
|
|
}
|
|
return ""
|
|
}(),
|
|
Username: strings.TrimSpace(u.syncRemoteUsername.Text()),
|
|
Password: u.syncRemotePassword.Text(),
|
|
CredentialPath: append([]string(nil), u.currentPath...),
|
|
SyncMode: u.syncSetupMode(),
|
|
}
|
|
binding, err := u.state.ConfigureRemoteBinding(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := u.state.Save(); err != nil {
|
|
return err
|
|
}
|
|
u.selectedVaultRemoteProfileID = binding.RemoteProfileID
|
|
u.selectedVaultRemoteCredentialEntryID = binding.CredentialEntryID
|
|
u.selectedVaultRemoteSyncMode = binding.SyncMode
|
|
u.remoteBaseURL.SetText(baseURL)
|
|
u.remotePath.SetText(remotePath)
|
|
u.remoteUsername.SetText(strings.TrimSpace(u.syncRemoteUsername.Text()))
|
|
u.remotePassword.SetText(u.syncRemotePassword.Text())
|
|
u.noteRecentRemote(baseURL, remotePath)
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) saveAsTargetPath() string {
|
|
path := strings.TrimSpace(u.saveAsPath.Text())
|
|
if path != "" {
|
|
return path
|
|
}
|
|
return u.defaultSaveAsPath
|
|
}
|
|
|
|
func (u *ui) importedVaultDestination(name string) string {
|
|
baseTarget := u.saveAsTargetPath()
|
|
baseDir := filepath.Dir(baseTarget)
|
|
baseName := filepath.Base(strings.TrimSpace(name))
|
|
switch {
|
|
case baseName == "" || baseName == "." || baseName == string(filepath.Separator):
|
|
return baseTarget
|
|
case strings.HasSuffix(strings.ToLower(baseName), ".kdbx"):
|
|
return filepath.Join(baseDir, baseName)
|
|
default:
|
|
return baseTarget
|
|
}
|
|
}
|
|
|
|
func (u *ui) consumePendingSharedVaultImport() {
|
|
path := strings.TrimSpace(u.pendingSharedVaultPath)
|
|
if path == "" {
|
|
return
|
|
}
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
u.state.ErrorMessage = fmt.Sprintf("import shared vault: %v", err)
|
|
}
|
|
return
|
|
}
|
|
name := "shared-vault.kdbx"
|
|
if namePath := strings.TrimSpace(u.pendingSharedVaultNamePath); namePath != "" {
|
|
if rawName, err := os.ReadFile(namePath); err == nil {
|
|
if trimmed := strings.TrimSpace(string(rawName)); trimmed != "" {
|
|
name = trimmed
|
|
}
|
|
}
|
|
}
|
|
if err := u.importSharedVaultBytesAction(name, content); err != nil {
|
|
u.state.ErrorMessage = fmt.Sprintf("import shared vault: %v", err)
|
|
return
|
|
}
|
|
_ = os.Remove(path)
|
|
if namePath := strings.TrimSpace(u.pendingSharedVaultNamePath); namePath != "" {
|
|
_ = os.Remove(namePath)
|
|
}
|
|
}
|
|
|
|
func (u *ui) importSharedVaultBytesAction(name string, content []byte) error {
|
|
target := u.importedVaultDestination(name)
|
|
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(target, append([]byte(nil), content...), 0o600); err != nil {
|
|
return err
|
|
}
|
|
u.lifecycleMode = "local"
|
|
u.vaultPath.SetText(target)
|
|
u.noteRecentVault(target)
|
|
u.state.ErrorMessage = ""
|
|
u.state.StatusMessage = ""
|
|
u.requestMasterPassFocus = true
|
|
u.filter()
|
|
return nil
|
|
}
|
|
|
|
func (u *ui) currentShareableVaultPath() string {
|
|
return strings.TrimSpace(u.vaultPath.Text())
|
|
}
|
|
|
|
func (u *ui) shareCurrentVaultAction() error {
|
|
if u.vaultSharer == nil {
|
|
return fmt.Errorf("vault sharing is not available on this platform")
|
|
}
|
|
path := u.currentShareableVaultPath()
|
|
if path == "" {
|
|
return errors.New(errVaultPathRequired)
|
|
}
|
|
if err := u.state.Save(); err != nil {
|
|
return err
|
|
}
|
|
return u.vaultSharer.ShareVault(path, friendlyRecentVaultLabel(path))
|
|
}
|