Linux: Detect changes in safeStorage backend

Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
ayumi-signal 2024-07-24 09:23:17 -07:00 committed by GitHub
parent 233a18bc81
commit 16864e381a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 107 additions and 10 deletions

View file

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

View file

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

View file

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

View file

@ -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<string, string> = {
basic_text: 'basic',
gnome_libsecret: 'gnome-libsecret',
kwallet: 'kwallet',
kwallet5: 'kwallet5',
};