Support Android share-driven credential lookup

This commit is contained in:
Joe Julian
2026-04-23 20:51:39 -07:00
parent 515eb730f0
commit 14c9bc72f6
6 changed files with 229 additions and 2 deletions
+6
View File
@@ -140,6 +140,7 @@ type statePaths struct {
AutofillCachePath string
PendingSharedVaultPath string
PendingSharedVaultNamePath string
PendingSharedLookupPath string
}
type recentVaultRecord struct {
@@ -474,6 +475,8 @@ type ui struct {
autofillCachePath string
pendingSharedVaultPath string
pendingSharedVaultNamePath string
pendingSharedLookupPath string
pendingSharedLookupQuery string
editingEntry bool
syncDefaultSourceMode syncSourceMode
syncDefaultDirection syncDirection
@@ -656,6 +659,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
autofillCachePath: paths.AutofillCachePath,
pendingSharedVaultPath: paths.PendingSharedVaultPath,
pendingSharedVaultNamePath: paths.PendingSharedVaultNamePath,
pendingSharedLookupPath: paths.PendingSharedLookupPath,
recentVaultGroups: map[string][]string{},
recentVaultUsedAt: map[string]time.Time{},
lifecycleAdvancedHidden: true,
@@ -704,6 +708,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
u.showStatusMessage("Some saved remote sign-ins came from an older KeePassGO build. Reopen those remotes and save them in the vault to migrate them.")
}
u.consumePendingSharedVaultImport()
u.consumePendingSharedLookup()
u.restoreStartupLifecycleTarget()
u.requestMasterPassFocus = u.hasSelectedLifecycleTarget()
u.loadUIPreferences()
@@ -785,6 +790,7 @@ func defaultStatePaths(stateDir string) statePaths {
AutofillCachePath: filepath.Join(baseDir, "autofill-cache.json"),
PendingSharedVaultPath: filepath.Join(baseDir, "pending-shared-vault.kdbx"),
PendingSharedVaultNamePath: filepath.Join(baseDir, "pending-shared-vault-name.txt"),
PendingSharedLookupPath: filepath.Join(baseDir, "pending-shared-lookup.txt"),
}
}
+49
View File
@@ -4,8 +4,10 @@ import (
"errors"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
@@ -17,6 +19,8 @@ import (
"git.julianfamily.org/keepassgo/internal/webdav"
)
var pendingSharedLookupURLPattern = regexp.MustCompile(`https?://[^\s<>"']+`)
func (u *ui) createVaultAction() error {
key, err := u.currentMasterKey()
defer u.clearMasterPassword()
@@ -78,6 +82,7 @@ func (u *ui) openVaultAction() error {
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
u.applyPendingSharedLookup()
u.applyPendingLifecycleOpenIntent()
return nil
}
@@ -120,6 +125,7 @@ func (u *ui) startOpenVaultAction() {
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
u.applyPendingSharedLookup()
u.applyPendingLifecycleOpenIntent()
return nil
}, nil
@@ -741,6 +747,49 @@ func (u *ui) consumePendingSharedVaultImport() {
}
}
func normalizePendingSharedLookupQuery(raw string) string {
value := strings.TrimSpace(raw)
if value == "" {
return ""
}
if match := pendingSharedLookupURLPattern.FindString(value); match != "" {
value = match
}
if parsed, err := url.Parse(value); err == nil && strings.TrimSpace(parsed.Hostname()) != "" {
return strings.ToLower(strings.TrimSpace(parsed.Hostname()))
}
return value
}
func (u *ui) consumePendingSharedLookup() {
path := strings.TrimSpace(u.pendingSharedLookupPath)
if path == "" {
return
}
data, err := os.ReadFile(path)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
u.state.ErrorMessage = fmt.Sprintf("shared lookup: %v", err)
}
return
}
_ = os.Remove(path)
u.pendingSharedLookupQuery = normalizePendingSharedLookupQuery(string(data))
u.applyPendingSharedLookup()
}
func (u *ui) applyPendingSharedLookup() {
query := strings.TrimSpace(u.pendingSharedLookupQuery)
status, ok := u.state.Session.(sessionStatus)
if query == "" || !ok || !status.HasVault() || status.IsLocked() {
return
}
u.pendingSharedLookupQuery = ""
u.state.Section = appstate.SectionEntries
u.search.SetText(query)
u.filter()
}
func (u *ui) importSharedVaultBytesAction(name string, content []byte) error {
target := u.importedVaultDestination(name)
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
+92
View File
@@ -8390,6 +8390,9 @@ func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) {
if got := paths.PendingSharedVaultNamePath; got != filepath.Join(base, "pending-shared-vault-name.txt") {
t.Fatalf("PendingSharedVaultNamePath = %q, want %q", got, filepath.Join(base, "pending-shared-vault-name.txt"))
}
if got := paths.PendingSharedLookupPath; got != filepath.Join(base, "pending-shared-lookup.txt") {
t.Fatalf("PendingSharedLookupPath = %q, want %q", got, filepath.Join(base, "pending-shared-lookup.txt"))
}
}
func TestImportedVaultDestinationUsesIncomingFilenameInsideDefaultDirectory(t *testing.T) {
@@ -8520,6 +8523,95 @@ func TestUIConsumesPendingSharedVaultImportOnStartup(t *testing.T) {
}
}
func TestUIConsumesPendingSharedLookupOnStartupWhenVaultIsAlreadyOpen(t *testing.T) {
t.Parallel()
dir := t.TempDir()
paths := statePaths{
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
PendingSharedLookupPath: filepath.Join(dir, "pending-shared-lookup.txt"),
}
if err := os.WriteFile(paths.PendingSharedLookupPath, []byte("https://bellagio.example.invalid/login\n"), 0o600); err != nil {
t.Fatalf("WriteFile(PendingSharedLookupPath) error = %v", err)
}
u := newUIWithSession("phone", &uiSession{model: vault.Model{
Entries: []vault.Entry{
{ID: "bellagio-login", Title: "Bellagio", URL: "https://bellagio.example.invalid/login", Path: []string{"Crew", "Internet"}},
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.example.invalid", Path: []string{"Crew", "Internet"}},
},
}}, paths)
if got := u.search.Text(); got != "bellagio.example.invalid" {
t.Fatalf("search after pending shared lookup = %q, want %q", got, "bellagio.example.invalid")
}
if got := u.filteredTitles(); !slices.Equal(got, []string{"Bellagio"}) {
t.Fatalf("filteredTitles() after pending shared lookup = %v, want [Bellagio]", got)
}
if _, err := os.Stat(paths.PendingSharedLookupPath); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("Stat(PendingSharedLookupPath) error = %v, want not exist", err)
}
}
func TestNormalizePendingSharedLookupQueryExtractsURLFromTextSnippet(t *testing.T) {
t.Parallel()
raw := "Meet the crew at https://bellagio.example.invalid/login before the vault opens."
if got := normalizePendingSharedLookupQuery(raw); got != "bellagio.example.invalid" {
t.Fatalf("normalizePendingSharedLookupQuery() = %q, want %q", got, "bellagio.example.invalid")
}
}
func TestUIAppliesPendingSharedLookupAfterOpeningVault(t *testing.T) {
t.Parallel()
dir := t.TempDir()
paths := statePaths{
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
PendingSharedLookupPath: filepath.Join(dir, "pending-shared-lookup.txt"),
}
if err := os.WriteFile(paths.PendingSharedLookupPath, []byte("https://bellagio.example.invalid/login\n"), 0o600); err != nil {
t.Fatalf("WriteFile(PendingSharedLookupPath) error = %v", err)
}
key := vault.MasterKey{Password: "correct horse battery staple"}
vaultPath := filepath.Join(dir, "bellagio.kdbx")
var encoded bytes.Buffer
if err := vault.SaveKDBXWithKey(&encoded, vault.Model{
Entries: []vault.Entry{
{ID: "bellagio-login", Title: "Bellagio", URL: "https://bellagio.example.invalid/login", Path: []string{"Crew", "Internet"}},
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.example.invalid", Path: []string{"Crew", "Internet"}},
},
}, key); err != nil {
t.Fatalf("SaveKDBXWithKey() error = %v", err)
}
if err := os.WriteFile(vaultPath, encoded.Bytes(), 0o600); err != nil {
t.Fatalf("WriteFile(vaultPath) error = %v", err)
}
u := newUIWithState("phone", &session.Manager{}, paths)
if got := u.search.Text(); got != "" {
t.Fatalf("search before open with pending shared lookup = %q, want empty", got)
}
u.vaultPath.SetText(vaultPath)
u.masterPassword.SetText(key.Password)
if err := u.openVaultAction(); err != nil {
t.Fatalf("openVaultAction() with pending shared lookup error = %v", err)
}
if got := u.search.Text(); got != "bellagio.example.invalid" {
t.Fatalf("search after open with pending shared lookup = %q, want %q", got, "bellagio.example.invalid")
}
if got := u.filteredTitles(); !slices.Equal(got, []string{"Bellagio"}) {
t.Fatalf("filteredTitles() after open with pending shared lookup = %v, want [Bellagio]", got)
}
}
func TestUICurrentShareableVaultPathUsesSelectedVaultPath(t *testing.T) {
t.Parallel()