diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 71f484db25e1..74f2397682cc 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -131,6 +131,14 @@ "messageformat": "Recover data", "description": "(Deleted 2024/07/11) Text of a button shown in a popup if the database cannot start up properly; allows user to attempt data recovery of their database" }, + "icu:databaseError__safeStorageBackendChange": { + "messageformat": "Unable to access the database encryption key because the OS encryption keyring backend has changed from {previousBackend} to {currentBackend}. This can occur if the desktop environment changes, for example between GNOME and KDE.\n\nPlease switch to the previous desktop environment.", + "description": "On Linux, text in a popup shown if the app cannot start because the system's keyring encryption backend has changed. Example values: previousBackend=gnome_libsecret, currentBackend=kwallet5" + }, + "icu:databaseError__safeStorageBackendChangeWithPreviousFlag": { + "messageformat": "Unable to access the database encryption key because the OS encryption keyring backend has changed from {previousBackend} to {currentBackend}. This can occur if the desktop environment changes, for example between GNOME and KDE.\n\nPlease switch to the previous desktop environment or try to run signal with the command line flag --password-store=\"{previousBackendFlag}\"", + "description": "On Linux, text in a popup shown if the app cannot start because the system's keyring encryption backend has changed. We suggest a command line flag they can use to recover the app. Example values: previousBackend=gnome_libsecret, currentBackend=kwallet5, previousBackendFlag: gnome-libsecret" + }, "icu:mainMenuFile": { "messageformat": "&File", "description": "The label that is used for the File menu in the program main menu. The '&' indicates that the following letter will be used as the keyboard 'shortcut letter' for accessing the menu with the Alt- combination." diff --git a/app/main.ts b/app/main.ts index 2f2464a643b2..7ca8bba577fa 100644 --- a/app/main.ts +++ b/app/main.ts @@ -122,6 +122,9 @@ import type { ParsedSignalRoute } from '../ts/util/signalRoutes'; import { parseSignalRoute } from '../ts/util/signalRoutes'; import * as dns from '../ts/util/dns'; import { ZoomFactorService } from '../ts/services/ZoomFactorService'; +import { SafeStorageBackendChangeError } from '../ts/types/SafeStorageBackendChangeError'; +import { LINUX_PASSWORD_STORE_FLAGS } from '../ts/util/linuxPasswordStoreFlags'; +import { getOwn } from '../ts/util/getOwn'; const animationSettings = systemPreferences.getAnimationSettings(); @@ -1600,7 +1603,7 @@ const runSQLCorruptionHandler = async () => { `Restarting the application immediately. Error: ${error.message}` ); - await onDatabaseError(Errors.toLogFormat(error)); + await onDatabaseError(error); }; const runSQLReadonlyHandler = async () => { @@ -1627,12 +1630,35 @@ function generateSQLKey(): string { function getSQLKey(): string { let update = false; + const isLinux = OS.isLinux(); const legacyKeyValue = userConfig.get('key'); const modernKeyValue = userConfig.get('encryptedKey'); + const previousBackend = isLinux + ? userConfig.get('safeStorageBackend') + : undefined; + const safeStorageBackend: string | undefined = isLinux + ? safeStorage.getSelectedStorageBackend() + : undefined; const isEncryptionAvailable = safeStorage.isEncryptionAvailable() && - (!OS.isLinux() || safeStorage.getSelectedStorageBackend() !== 'basic_text'); + (!isLinux || safeStorageBackend !== 'basic_text'); + + // On Linux the backend can change based on desktop environment and command line flags. + // If the backend changes we won't be able to decrypt the key. + if ( + isLinux && + typeof previousBackend === 'string' && + previousBackend !== safeStorageBackend + ) { + console.error( + `Detected change in safeStorage backend, can't decrypt DB key (previous: ${previousBackend}, current: ${safeStorageBackend})` + ); + throw new SafeStorageBackendChangeError({ + currentBackend: String(safeStorageBackend), + previousBackend, + }); + } let key: string; if (typeof modernKeyValue === 'string') { @@ -1648,6 +1674,13 @@ function getSQLKey(): string { getLogger().info('getSQLKey: removing legacy key'); userConfig.set('key', undefined); } + + if (isLinux && previousBackend == null) { + getLogger().info( + `getSQLKey: saving safeStorageBackend: ${safeStorageBackend}` + ); + userConfig.set('safeStorageBackend', safeStorageBackend); + } } else if (typeof legacyKeyValue === 'string') { key = legacyKeyValue; update = isEncryptionAvailable; @@ -1671,6 +1704,13 @@ function getSQLKey(): string { const encrypted = safeStorage.encryptString(key).toString('hex'); userConfig.set('encryptedKey', encrypted); userConfig.set('key', undefined); + + if (isLinux && safeStorageBackend) { + getLogger().info( + `getSQLKey: saving safeStorageBackend: ${safeStorageBackend}` + ); + userConfig.set('safeStorageBackend', safeStorageBackend); + } } else { getLogger().info('getSQLKey: updating plaintext key in the config'); userConfig.set('key', key); @@ -1740,7 +1780,7 @@ async function initializeSQL( return { ok: true, error: undefined }; } -const onDatabaseError = async (error: string) => { +const onDatabaseError = async (error: Error) => { // Prevent window from re-opening ready = false; @@ -1758,11 +1798,27 @@ const onDatabaseError = async (error: string) => { const copyErrorAndQuitButtonIndex = 0; const SIGNAL_SUPPORT_LINK = 'https://support.signal.org/error'; - if (error.includes(DBVersionFromFutureError.name)) { + if (error instanceof DBVersionFromFutureError) { // If the DB version is too new, the user likely opened an older version of Signal, // and they would almost never want to delete their data as a result, so we don't show // that option messageDetail = i18n('icu:databaseError__startOldVersion'); + } else if (error instanceof SafeStorageBackendChangeError) { + const { currentBackend, previousBackend } = error; + const previousBackendFlag = getOwn( + LINUX_PASSWORD_STORE_FLAGS, + previousBackend + ); + messageDetail = previousBackendFlag + ? i18n('icu:databaseError__safeStorageBackendChangeWithPreviousFlag', { + currentBackend, + previousBackend, + previousBackendFlag, + }) + : i18n('icu:databaseError__safeStorageBackendChange', { + currentBackend, + previousBackend, + }); } else { // Otherwise, this is some other kind of DB error, let's give them the option to // delete. @@ -1787,7 +1843,9 @@ const onDatabaseError = async (error: string) => { }); if (buttonIndex === copyErrorAndQuitButtonIndex) { - clipboard.writeText(`Database startup error:\n\n${redactAll(error)}`); + clipboard.writeText( + `Database startup error:\n\n${redactAll(Errors.toLogFormat(error))}` + ); } else if ( typeof deleteAllDataButtonIndex === 'number' && buttonIndex === deleteAllDataButtonIndex @@ -1827,10 +1885,6 @@ let sqlInitPromise: | Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> | undefined; -ipc.on('database-error', (_event: Electron.Event, error: string) => { - drop(onDatabaseError(error)); -}); - ipc.on('database-readonly', (_event: Electron.Event, error: string) => { // Just let global_errors.ts handle it throw new Error(error); @@ -2171,7 +2225,7 @@ app.on('ready', async () => { if (sqlError) { getLogger().error('sql.initialize was unsuccessful; returning early'); - await onDatabaseError(Errors.toLogFormat(sqlError)); + await onDatabaseError(sqlError); return; } diff --git a/ts/types/SafeStorageBackendChangeError.ts b/ts/types/SafeStorageBackendChangeError.ts new file mode 100644 index 000000000000..884257166a02 --- /dev/null +++ b/ts/types/SafeStorageBackendChangeError.ts @@ -0,0 +1,24 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export class SafeStorageBackendChangeError extends Error { + override name = 'SafeStorageBackendChangeError'; + + public readonly currentBackend: string; + public readonly previousBackend: string; + + constructor({ + currentBackend, + previousBackend, + }: { + currentBackend: string; + previousBackend: string; + }) { + super( + `Detected change in safeStorage backend, can't decrypt DB key (previous: ${previousBackend}, current: ${currentBackend})` + ); + + this.currentBackend = currentBackend; + this.previousBackend = previousBackend; + } +} diff --git a/ts/util/linuxPasswordStoreFlags.ts b/ts/util/linuxPasswordStoreFlags.ts new file mode 100644 index 000000000000..27c1c0ad75f5 --- /dev/null +++ b/ts/util/linuxPasswordStoreFlags.ts @@ -0,0 +1,11 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +// Mapping of safeStorage backends to flags used to activate them. +// See https://www.electronjs.org/docs/latest/api/safe-storage +export const LINUX_PASSWORD_STORE_FLAGS: Record = { + basic_text: 'basic', + gnome_libsecret: 'gnome-libsecret', + kwallet: 'kwallet', + kwallet5: 'kwallet5', +};