Test safeStorage in Flatpak environments

This commit is contained in:
ayumi-signal
2025-09-29 15:58:14 -07:00
committed by GitHub
parent ec7d07269d
commit af55cf4682
4 changed files with 97 additions and 4 deletions

View File

@@ -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-<letter> combination."

View File

@@ -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) {
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);
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.

View File

@@ -0,0 +1,6 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export class SafeStorageDecryptionError extends Error {
override name = 'SafeStorageDecryptionError';
}

View File

@@ -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,
};