From af55cf468237260b87693ffa354931c6b763ef91 Mon Sep 17 00:00:00 2001 From: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:58:14 -0700 Subject: [PATCH] Test safeStorage in Flatpak environments --- _locales/en/messages.json | 12 +++++ app/main.ts | 69 ++++++++++++++++++++++++-- ts/types/SafeStorageDecryptionError.ts | 6 +++ ts/util/os/osMain.ts | 14 ++++++ 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 ts/types/SafeStorageDecryptionError.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 78f54ca34c..303df5b196 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -155,6 +155,18 @@ "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:systemEncryptionError": { + "messageformat": "System encryption error", + "description": "On Linux, title in a popup shown when the app detects that the system keyring encryption does not work correctly." + }, + "icu:systemEncryptionError__linuxSafeStorageDecryptionError": { + "messageformat": "Unable to decrypt the database encryption key using the OS encryption keyring. This can occur in certain containerized environments such as Flatpak.\n\nWe recommend quitting and checking your OS encryption backend such as gnome-libsecret or kwallet, then trying again.\n\nYou may choose to continue anyway, however the database key will be stored in plaintext on the filesystem and other apps may be able to access it.", + "description": "On Linux, text in a popup shown when the app detects that the system keyring encryption does not work correctly." + }, + "icu:systemEncryptionError__continueWithPlaintextKey": { + "messageformat": "Continue with plaintext key", + "description": "On Linux, button in a popup shown when the app detects that the system keyring encryption does not work correctly." + }, "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 b3f8350004..9ec0f5fef1 100644 --- a/app/main.ts +++ b/app/main.ts @@ -123,6 +123,7 @@ import { parseSignalRoute } from '../ts/util/signalRoutes.js'; import * as dns from '../ts/util/dns.js'; import { ZoomFactorService } from '../ts/services/ZoomFactorService.js'; import { SafeStorageBackendChangeError } from '../ts/types/SafeStorageBackendChangeError.js'; +import { SafeStorageDecryptionError } from '../ts/types/SafeStorageDecryptionError.js'; import { LINUX_PASSWORD_STORE_FLAGS } from '../ts/util/linuxPasswordStoreFlags.js'; import { getOwn } from '../ts/util/getOwn.js'; import { safeParseLoose, safeParseUnknown } from '../ts/util/schemas.js'; @@ -1650,9 +1651,20 @@ function getSQLKey(): string { const encrypted = Buffer.from(modernKeyValue, 'hex'); key = safeStorage.decryptString(encrypted); - if (legacyKeyValue != null) { - log.info('getSQLKey: removing legacy key'); - userConfig.set('key', undefined); + if (typeof legacyKeyValue === 'string') { + if (key === legacyKeyValue) { + // Confirmed roundtrip encryption, we can remove the legacy key + log.info('getSQLKey: removing legacy key'); + userConfig.set('key', undefined); + } else { + log.warn('getSQLKey: decrypted modern key mismatch with legacy key'); + const nextStep = handleSafeStorageDecryptionError(); + if (nextStep === 'quit') { + throw new SafeStorageDecryptionError(); + } + + key = legacyKeyValue; + } } if (isLinux && previousBackend == null) { @@ -1681,7 +1693,15 @@ function getSQLKey(): string { log.info('getSQLKey: updating encrypted key in the config'); const encrypted = safeStorage.encryptString(key).toString('hex'); userConfig.set('encryptedKey', encrypted); - userConfig.set('key', undefined); + + if (OS.isFlatpak()) { + log.info( + 'getSQLKey: updating plaintext key in the config, will confirm decryption on next start' + ); + userConfig.set('key', key); + } else { + userConfig.set('key', undefined); + } if (isLinux && safeStorageBackend) { log.info(`getSQLKey: saving safeStorageBackend: ${safeStorageBackend}`); @@ -1695,6 +1715,41 @@ function getSQLKey(): string { return key; } +// In Flatpak, safeStorage encryption may appear to work on the first run but on +// subsequent starts the decrypted value may be incorrect. +function handleSafeStorageDecryptionError(): 'continue' | 'quit' { + const previousError = userConfig.get('safeStorageDecryptionError'); + if (typeof previousError === 'string') { + return 'continue'; + } + + const { i18n } = getResolvedMessagesLocale(); + const message = i18n('icu:systemEncryptionError'); + const detail = i18n( + 'icu:systemEncryptionError__linuxSafeStorageDecryptionError' + ); + const buttons = [ + i18n('icu:copyErrorAndQuit'), + i18n('icu:systemEncryptionError__continueWithPlaintextKey'), + ]; + const copyErrorAndQuitIndex = 0; + const resultIndex = dialog.showMessageBoxSync({ + buttons, + defaultId: copyErrorAndQuitIndex, + cancelId: copyErrorAndQuitIndex, + message, + detail, + icon: getAppErrorIcon(), + noLink: true, + }); + if (resultIndex === copyErrorAndQuitIndex) { + return 'quit'; + } + + userConfig.set('safeStorageDecryptionError', 'true'); + return 'continue'; +} + async function initializeSQL( userDataPath: string ): Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> { @@ -1818,6 +1873,12 @@ const onDatabaseInitializationError = async (error: Error) => { buttons.push(i18n('icu:copyErrorAndQuit')); copyErrorAndQuitButtonIndex = 0; defaultButtonId = copyErrorAndQuitButtonIndex; + } else if (error instanceof SafeStorageDecryptionError) { + log.error( + 'onDatabaseInitializationError: SafeStorageDecryptionError, user chose to quit' + ); + app.exit(1); + return; } else { // Otherwise, this is some other kind of DB error, most likely broken safeStorage key. // Let's give them the option to delete and show them the support guide. diff --git a/ts/types/SafeStorageDecryptionError.ts b/ts/types/SafeStorageDecryptionError.ts new file mode 100644 index 0000000000..4cecd4df04 --- /dev/null +++ b/ts/types/SafeStorageDecryptionError.ts @@ -0,0 +1,6 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +export class SafeStorageDecryptionError extends Error { + override name = 'SafeStorageDecryptionError'; +} diff --git a/ts/util/os/osMain.ts b/ts/util/os/osMain.ts index fa19438bf0..64d6d84674 100644 --- a/ts/util/os/osMain.ts +++ b/ts/util/os/osMain.ts @@ -21,6 +21,19 @@ function getLinuxName(): string | undefined { return match[1]; } +function isFlatpak(): boolean { + if (process.env.container === 'flatpak') { + return true; + } + + const linuxName = getLinuxName(); + if (linuxName && linuxName.toLowerCase().includes('flatpak')) { + return true; + } + + return false; +} + function isWaylandEnabled(): boolean { return Boolean(process.env.WAYLAND_DISPLAY); } @@ -32,6 +45,7 @@ function isLinuxUsingKDE(): boolean { const OS = { ...getOSFunctions(os.release()), getLinuxName, + isFlatpak, isLinuxUsingKDE, isWaylandEnabled, };