Skip to content

Commit af55cf4

Browse files
authored
Test safeStorage in Flatpak environments
1 parent ec7d072 commit af55cf4

File tree

4 files changed

+97
-4
lines changed

4 files changed

+97
-4
lines changed

_locales/en/messages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,18 @@
155155
"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}\"",
156156
"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"
157157
},
158+
"icu:systemEncryptionError": {
159+
"messageformat": "System encryption error",
160+
"description": "On Linux, title in a popup shown when the app detects that the system keyring encryption does not work correctly."
161+
},
162+
"icu:systemEncryptionError__linuxSafeStorageDecryptionError": {
163+
"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.",
164+
"description": "On Linux, text in a popup shown when the app detects that the system keyring encryption does not work correctly."
165+
},
166+
"icu:systemEncryptionError__continueWithPlaintextKey": {
167+
"messageformat": "Continue with plaintext key",
168+
"description": "On Linux, button in a popup shown when the app detects that the system keyring encryption does not work correctly."
169+
},
158170
"icu:mainMenuFile": {
159171
"messageformat": "&File",
160172
"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."

app/main.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ import { parseSignalRoute } from '../ts/util/signalRoutes.js';
123123
import * as dns from '../ts/util/dns.js';
124124
import { ZoomFactorService } from '../ts/services/ZoomFactorService.js';
125125
import { SafeStorageBackendChangeError } from '../ts/types/SafeStorageBackendChangeError.js';
126+
import { SafeStorageDecryptionError } from '../ts/types/SafeStorageDecryptionError.js';
126127
import { LINUX_PASSWORD_STORE_FLAGS } from '../ts/util/linuxPasswordStoreFlags.js';
127128
import { getOwn } from '../ts/util/getOwn.js';
128129
import { safeParseLoose, safeParseUnknown } from '../ts/util/schemas.js';
@@ -1650,9 +1651,20 @@ function getSQLKey(): string {
16501651
const encrypted = Buffer.from(modernKeyValue, 'hex');
16511652
key = safeStorage.decryptString(encrypted);
16521653

1653-
if (legacyKeyValue != null) {
1654-
log.info('getSQLKey: removing legacy key');
1655-
userConfig.set('key', undefined);
1654+
if (typeof legacyKeyValue === 'string') {
1655+
if (key === legacyKeyValue) {
1656+
// Confirmed roundtrip encryption, we can remove the legacy key
1657+
log.info('getSQLKey: removing legacy key');
1658+
userConfig.set('key', undefined);
1659+
} else {
1660+
log.warn('getSQLKey: decrypted modern key mismatch with legacy key');
1661+
const nextStep = handleSafeStorageDecryptionError();
1662+
if (nextStep === 'quit') {
1663+
throw new SafeStorageDecryptionError();
1664+
}
1665+
1666+
key = legacyKeyValue;
1667+
}
16561668
}
16571669

16581670
if (isLinux && previousBackend == null) {
@@ -1681,7 +1693,15 @@ function getSQLKey(): string {
16811693
log.info('getSQLKey: updating encrypted key in the config');
16821694
const encrypted = safeStorage.encryptString(key).toString('hex');
16831695
userConfig.set('encryptedKey', encrypted);
1684-
userConfig.set('key', undefined);
1696+
1697+
if (OS.isFlatpak()) {
1698+
log.info(
1699+
'getSQLKey: updating plaintext key in the config, will confirm decryption on next start'
1700+
);
1701+
userConfig.set('key', key);
1702+
} else {
1703+
userConfig.set('key', undefined);
1704+
}
16851705

16861706
if (isLinux && safeStorageBackend) {
16871707
log.info(`getSQLKey: saving safeStorageBackend: ${safeStorageBackend}`);
@@ -1695,6 +1715,41 @@ function getSQLKey(): string {
16951715
return key;
16961716
}
16971717

1718+
// In Flatpak, safeStorage encryption may appear to work on the first run but on
1719+
// subsequent starts the decrypted value may be incorrect.
1720+
function handleSafeStorageDecryptionError(): 'continue' | 'quit' {
1721+
const previousError = userConfig.get('safeStorageDecryptionError');
1722+
if (typeof previousError === 'string') {
1723+
return 'continue';
1724+
}
1725+
1726+
const { i18n } = getResolvedMessagesLocale();
1727+
const message = i18n('icu:systemEncryptionError');
1728+
const detail = i18n(
1729+
'icu:systemEncryptionError__linuxSafeStorageDecryptionError'
1730+
);
1731+
const buttons = [
1732+
i18n('icu:copyErrorAndQuit'),
1733+
i18n('icu:systemEncryptionError__continueWithPlaintextKey'),
1734+
];
1735+
const copyErrorAndQuitIndex = 0;
1736+
const resultIndex = dialog.showMessageBoxSync({
1737+
buttons,
1738+
defaultId: copyErrorAndQuitIndex,
1739+
cancelId: copyErrorAndQuitIndex,
1740+
message,
1741+
detail,
1742+
icon: getAppErrorIcon(),
1743+
noLink: true,
1744+
});
1745+
if (resultIndex === copyErrorAndQuitIndex) {
1746+
return 'quit';
1747+
}
1748+
1749+
userConfig.set('safeStorageDecryptionError', 'true');
1750+
return 'continue';
1751+
}
1752+
16981753
async function initializeSQL(
16991754
userDataPath: string
17001755
): Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> {
@@ -1818,6 +1873,12 @@ const onDatabaseInitializationError = async (error: Error) => {
18181873
buttons.push(i18n('icu:copyErrorAndQuit'));
18191874
copyErrorAndQuitButtonIndex = 0;
18201875
defaultButtonId = copyErrorAndQuitButtonIndex;
1876+
} else if (error instanceof SafeStorageDecryptionError) {
1877+
log.error(
1878+
'onDatabaseInitializationError: SafeStorageDecryptionError, user chose to quit'
1879+
);
1880+
app.exit(1);
1881+
return;
18211882
} else {
18221883
// Otherwise, this is some other kind of DB error, most likely broken safeStorage key.
18231884
// Let's give them the option to delete and show them the support guide.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright 2025 Signal Messenger, LLC
2+
// SPDX-License-Identifier: AGPL-3.0-only
3+
4+
export class SafeStorageDecryptionError extends Error {
5+
override name = 'SafeStorageDecryptionError';
6+
}

ts/util/os/osMain.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ function getLinuxName(): string | undefined {
2121
return match[1];
2222
}
2323

24+
function isFlatpak(): boolean {
25+
if (process.env.container === 'flatpak') {
26+
return true;
27+
}
28+
29+
const linuxName = getLinuxName();
30+
if (linuxName && linuxName.toLowerCase().includes('flatpak')) {
31+
return true;
32+
}
33+
34+
return false;
35+
}
36+
2437
function isWaylandEnabled(): boolean {
2538
return Boolean(process.env.WAYLAND_DISPLAY);
2639
}
@@ -32,6 +45,7 @@ function isLinuxUsingKDE(): boolean {
3245
const OS = {
3346
...getOSFunctions(os.release()),
3447
getLinuxName,
48+
isFlatpak,
3549
isLinuxUsingKDE,
3650
isWaylandEnabled,
3751
};

0 commit comments

Comments
 (0)