From 03ea2247733fb5c37b35e296ba02f2b0b55b9d0c Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Mon, 6 Apr 2026 17:31:58 -0700 Subject: [PATCH] Add legacy remote credential migration notices --- main.go | 25 ++++++++++++++++++++++++ main_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/main.go b/main.go index ce4615c..04d4286 100644 --- a/main.go +++ b/main.go @@ -159,8 +159,11 @@ type recentRemoteRecord struct { LocalVaultPath string `json:"localVaultPath,omitempty"` RemoteProfileID string `json:"remoteProfileId,omitempty"` CredentialEntryID string `json:"credentialEntryId,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` LastGroup []string `json:"lastGroup,omitempty"` UsedAt string `json:"usedAt,omitempty"` + NeedsMigration bool `json:"-"` } type uiPreferences struct { @@ -668,6 +671,9 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) u.setCustomFieldRows(nil) u.loadRecentVaults() u.loadRecentRemotes() + if u.hasLegacyRecentRemoteCredentialMigration() { + 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.restoreStartupLifecycleTarget() u.requestMasterPassFocus = u.hasSelectedLifecycleTarget() @@ -1695,9 +1701,16 @@ func (u *ui) loadRecentRemotes() { record.LocalVaultPath = strings.TrimSpace(record.LocalVaultPath) record.RemoteProfileID = strings.TrimSpace(record.RemoteProfileID) record.CredentialEntryID = strings.TrimSpace(record.CredentialEntryID) + record.Username = strings.TrimSpace(record.Username) + record.Password = strings.TrimSpace(record.Password) if record.BaseURL == "" || record.Path == "" { continue } + if record.Username != "" || record.Password != "" { + record.NeedsMigration = true + record.Username = "" + record.Password = "" + } key := record.BaseURL + "|" + record.Path if seen[key] { continue @@ -1715,6 +1728,15 @@ func (u *ui) loadRecentRemotes() { } } +func (u *ui) hasLegacyRecentRemoteCredentialMigration() bool { + for _, record := range u.recentRemotes { + if record.NeedsMigration { + return true + } + } + return false +} + func (u *ui) saveRecentVaults() { if strings.TrimSpace(u.recentVaultsPath) == "" { return @@ -2092,6 +2114,9 @@ func (u *ui) applyRecentRemoteRecord(record recentRemoteRecord) { u.selectedVaultRemoteCredentialEntryID = strings.TrimSpace(record.CredentialEntryID) u.remotePassword.Mask = '•' u.selectedRemoteConnection = true + if record.NeedsMigration && strings.TrimSpace(record.RemoteProfileID) == "" && strings.TrimSpace(record.CredentialEntryID) == "" { + u.showStatusMessage("This saved remote came from an older local-sign-in format. Open it again, then save the remote in the vault to migrate it.") + } } func (u *ui) remotePreferencesCurrentSummary() string { diff --git a/main_test.go b/main_test.go index eccb42f..8e94abd 100644 --- a/main_test.go +++ b/main_test.go @@ -4560,6 +4560,44 @@ func TestUILoadRecentRemotesIgnoresLegacySavedCredentials(t *testing.T) { if got := u.recentRemotes[0]; got.BaseURL != "https://dav.example.com" || got.Path != "vaults/home.kdbx" { t.Fatalf("recentRemotes[0] = %#v, want location-only record", got) } + if !u.recentRemotes[0].NeedsMigration { + t.Fatal("recentRemotes[0].NeedsMigration = false, want true for legacy saved credentials") + } + if got := u.recentRemotes[0].Username; got != "" { + t.Fatalf("recentRemotes[0].Username = %q, want empty after migration strip", got) + } + if got := u.recentRemotes[0].Password; got != "" { + t.Fatalf("recentRemotes[0].Password = %q, want empty after migration strip", got) + } +} + +func TestUINewUIShowsMigrationStatusForLegacyRecentRemoteCredentials(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + recentRemotesPath := filepath.Join(dir, "recent-remotes.json") + content := `[ + { + "baseUrl": "https://dav.example.com", + "path": "vaults/home.kdbx", + "username": "alice", + "password": "secret-1" + } +]` + if err := os.WriteFile(recentRemotesPath, []byte(content), 0o600); err != nil { + t.Fatalf("WriteFile(recent-remotes.json) error = %v", err) + } + + u := newUIWithState("desktop", &session.Manager{}, statePaths{ + DefaultSaveAsPath: filepath.Join(dir, "default.kdbx"), + RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"), + RecentRemotesPath: recentRemotesPath, + UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"), + }) + + if got := u.state.StatusMessage; got != "This saved remote came from an older local-sign-in format. Open it again, then save the remote in the vault to migrate it." { + t.Fatalf("StatusMessage = %q, want legacy recent-remote migration notice for the selected startup remote", got) + } } func TestUIApplyRecentRemoteRecordRestoresVaultBindingSelection(t *testing.T) { @@ -4586,6 +4624,22 @@ func TestUIApplyRecentRemoteRecordRestoresVaultBindingSelection(t *testing.T) { } } +func TestUIApplyRecentRemoteRecordShowsMigrationNoticeForLegacySavedCredentials(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", &session.Manager{}) + + u.applyRecentRemoteRecord(recentRemoteRecord{ + BaseURL: "https://dav.example.com", + Path: "vaults/home.kdbx", + NeedsMigration: true, + }) + + if got := u.state.StatusMessage; got != "This saved remote came from an older local-sign-in format. Open it again, then save the remote in the vault to migrate it." { + t.Fatalf("StatusMessage = %q, want legacy per-record migration notice", got) + } +} + func TestUIStartupPreselectsNewestTargetAcrossLocalAndRemote(t *testing.T) { t.Parallel()