From 7d11ca31369e316cd1b179765baf1d635c69fbd7 Mon Sep 17 00:00:00 2001 From: jonasnobile Date: Fri, 15 May 2026 16:44:02 +0200 Subject: [PATCH] fix: keep listing connections when password decryption fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the local encryption key changed (keychain rotation, app reinstall, key file deletion) the stored ciphertext for older connections can't be decrypted anymore. decryptConfig() used to throw in that case, which took down every caller — listConnections, the periodic idle-transaction check, the whole UI on startup. Switch to tryDecryptLocalPassword and fall back to an empty password when decryption fails. The connect flow already prompts for a password when one isn't available, so the user can re-enter it and move on instead of being stuck with a crashed connection list. Co-Authored-By: Claude Opus 4.7 --- src/backend-shared/storage/app-db.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/backend-shared/storage/app-db.ts b/src/backend-shared/storage/app-db.ts index 657bbf8..43aedf3 100644 --- a/src/backend-shared/storage/app-db.ts +++ b/src/backend-shared/storage/app-db.ts @@ -3,7 +3,7 @@ import { isServerConfig } from '@dotaz/shared/types/connection' import type { QueryHistoryEntry, QueryHistoryStatus } from '@dotaz/shared/types/query' import type { HistoryListParams, QueryBookmark, SavedView, SavedViewConfig } from '@dotaz/shared/types/rpc' import Database from 'bun:sqlite' -import { decryptLocalPassword, encryptLocalPassword, isEncryptedPassword, tryDecryptLocalPassword } from '../services/encryption' +import { encryptLocalPassword, isEncryptedPassword, tryDecryptLocalPassword } from '../services/encryption' import { runMigrations } from './migrations' /** Default settings values — returned when a key has not been explicitly set. */ @@ -112,15 +112,22 @@ export class AppDatabase { private decryptConfig(config: ConnectionConfig): ConnectionConfig { if (this.localKey && isServerConfig(config) && isEncryptedPassword(config.password)) { - let decrypted: ConnectionConfig = { ...config, password: decryptLocalPassword(config.password, this.localKey) } + // If the local key changed (keychain rotation, app reinstall, etc.) the + // stored ciphertext can't be decrypted anymore. Fall back to an empty + // password and keep the connection visible — the connect flow already + // prompts the user for a password when none is available. Throwing here + // would take down listConnections() and stop the entire UI. + const safeDecrypt = (value: string): string => tryDecryptLocalPassword(value, this.localKey!) ?? '' + + let decrypted: ConnectionConfig = { ...config, password: safeDecrypt(config.password) } // Decrypt SSH tunnel secrets if present if (config.type === 'postgresql' && config.sshTunnel) { const tunnel = { ...config.sshTunnel } if (tunnel.password && isEncryptedPassword(tunnel.password)) { - tunnel.password = decryptLocalPassword(tunnel.password, this.localKey!) + tunnel.password = safeDecrypt(tunnel.password) } if (tunnel.keyPassphrase && isEncryptedPassword(tunnel.keyPassphrase)) { - tunnel.keyPassphrase = decryptLocalPassword(tunnel.keyPassphrase, this.localKey!) + tunnel.keyPassphrase = safeDecrypt(tunnel.keyPassphrase) } decrypted = { ...decrypted, sshTunnel: tunnel } as ConnectionConfig }