Use electron's safeStorage API

This commit is contained in:
Fedor Indutny 2024-07-08 16:41:26 -07:00 committed by GitHub
parent c68aac7401
commit e87eaff948
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 128 additions and 33 deletions

View file

@ -123,6 +123,14 @@
"messageformat": "The version of your database does not match this version of Signal. Make sure you are opening the newest version of Signal on your computer.",
"description": "Text in a popup shown if the app cannot start because the user started an older version of Signal"
},
"icu:databaseError__recover__detail": {
"messageformat": "A database error occurred. You can copy the error and contact Signal support to help fix the issue. If you need to use Signal right away, you can attempt to recover your data.\n\nContact support by visiting: {link}",
"description": "Description shown in a popup if the database key cannot be read, but can be recovered"
},
"icu:databaseError__recover__button": {
"messageformat": "Recover data",
"description": "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: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

@ -28,6 +28,7 @@ import {
shell,
systemPreferences,
Notification,
safeStorage,
} from 'electron';
import type { MenuItemConstructorOptions, Settings } from 'electron';
import { z } from 'zod';
@ -1583,28 +1584,105 @@ const runSQLReadonlyHandler = async () => {
throw error;
};
async function initializeSQL(
userDataPath: string
): Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> {
let key: string | undefined;
const keyFromConfig = userConfig.get('key');
if (typeof keyFromConfig === 'string') {
key = keyFromConfig;
} else if (keyFromConfig) {
getLogger().warn(
"initializeSQL: got key from config, but it wasn't a string"
);
function generateSQLKey(): string {
getLogger().info(
'key/initialize: Generating new encryption key, since we did not find it on disk'
);
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
return randomBytes(32).toString('hex');
}
function getSQLKey(): string {
let update = false;
const legacyKeyValue = userConfig.get('key');
const modernKeyValue = userConfig.get('encryptedKey');
const isEncryptionAvailable =
safeStorage.isEncryptionAvailable() &&
(!OS.isLinux() || safeStorage.getSelectedStorageBackend() !== 'basic_text');
let key: string;
if (typeof modernKeyValue === 'string') {
if (!isEncryptionAvailable) {
throw new Error("Can't decrypt database key");
}
getLogger().info('getSQLKey: decrypting key');
const encrypted = Buffer.from(modernKeyValue, 'hex');
key = safeStorage.decryptString(encrypted);
} else if (typeof legacyKeyValue === 'string') {
key = legacyKeyValue;
update = isEncryptionAvailable;
if (update) {
getLogger().info('getSQLKey: migrating key');
} else {
getLogger().info('getSQLKey: using legacy key');
}
} else {
getLogger().warn("getSQLKey: got key from config, but it wasn't a string");
key = generateSQLKey();
update = true;
}
if (!key) {
getLogger().info(
'key/initialize: Generating new encryption key, since we did not find it on disk'
);
// https://www.zetetic.net/sqlcipher/sqlcipher-api/#key
key = randomBytes(32).toString('hex');
if (!update) {
return key;
}
if (isEncryptionAvailable) {
getLogger().info('getSQLKey: updating encrypted key in the config');
const encrypted = safeStorage.encryptString(key).toString('hex');
userConfig.set('encryptedKey', encrypted);
userConfig.set('key', key);
} else {
getLogger().info('getSQLKey: updating plaintext key in the config');
userConfig.set('key', key);
}
return key;
}
async function initializeSQL(
userDataPath: string
): Promise<{ ok: true; error: undefined } | { ok: false; error: Error }> {
sqlInitTimeStart = Date.now();
let key: string;
try {
key = getSQLKey();
} catch (error: unknown) {
const SIGNAL_SUPPORT_LINK = 'https://support.signal.org/error';
const { i18n } = getResolvedMessagesLocale();
const buttonIndex = dialog.showMessageBoxSync({
buttons: [i18n('icu:cancel'), i18n('icu:databaseError__recover__button')],
defaultId: 1,
cancelId: 1,
message: i18n('icu:databaseError'),
detail: i18n('icu:databaseError__recover__detail', {
link: SIGNAL_SUPPORT_LINK,
}),
noLink: true,
type: 'error',
});
const copyErrorAndQuitButtonIndex = 0;
if (buttonIndex === copyErrorAndQuitButtonIndex) {
clipboard.writeText(
`Database startup error:\n\n${redactAll(Errors.toLogFormat(error))}`
);
getLogger().error('onDatabaseError: Quitting application');
app.exit(1);
// Don't let go through, while `app.exit()` is finalizing asynchronously
await new Promise(noop);
}
getLogger().error('onDatabaseError: Removing malformed key');
userConfig.set('encryptedKey', undefined);
key = getSQLKey();
}
try {
// This should be the first awaited call in this function, otherwise
// `sql.sqlCall` will throw an uninitialized error instead of waiting for
@ -1773,9 +1851,6 @@ const featuresToDisable = `HardwareMediaKeyHandling,${app.commandLine.getSwitchV
)}`;
app.commandLine.appendSwitch('disable-features', featuresToDisable);
// If we don't set this, Desktop will ask for access to keychain/keyring on startup
app.commandLine.appendSwitch('password-store', 'basic');
// <canvas/> rendering is often utterly broken on Linux when using GPU
// acceleration.
if (DISABLE_GPU) {
@ -1824,8 +1899,6 @@ app.on('ready', async () => {
);
await EmojiService.create(resourceService);
sqlInitPromise = initializeSQL(userDataPath);
if (!resolvedTranslationsLocale) {
preferredSystemLocales = resolveCanonicalLocales(
loadPreferredSystemLocales()
@ -1850,6 +1923,8 @@ app.on('ready', async () => {
});
}
sqlInitPromise = initializeSQL(userDataPath);
// First run: configure Signal to minimize to tray. Additionally, on Windows
// enable auto-start with start-in-tray so that starting from a Desktop icon
// would still show the window.
@ -2251,11 +2326,19 @@ async function requestShutdown() {
function getWindowDebugInfo() {
const windows = BrowserWindow.getAllWindows();
return {
windowCount: windows.length,
mainWindowExists: windows.some(win => win === mainWindow),
mainWindowIsFullScreen: mainWindow?.isFullScreen(),
};
try {
return {
windowCount: windows.length,
mainWindowExists: windows.some(win => win === mainWindow),
mainWindowIsFullScreen: mainWindow?.isFullScreen(),
};
} catch {
return {
windowCount: 0,
mainWindowExists: false,
mainWindowIsFullScreen: false,
};
}
}
app.on('before-quit', e => {
@ -2614,11 +2697,6 @@ ipc.handle('DebugLogs.upload', async (_event, content: string) => {
});
});
ipc.on('user-config-key', event => {
// eslint-disable-next-line no-param-reassign
event.returnValue = userConfig.get('key');
});
ipc.on('get-user-data-path', event => {
// eslint-disable-next-line no-param-reassign
event.returnValue = app.getPath('userData');

View file

@ -4,6 +4,8 @@ import type { BrowserWindow } from 'electron';
import { ipcMain } from 'electron';
import EventEmitter from 'events';
import * as log from '../logging/log';
const DEFAULT_ZOOM_FACTOR = 1.0;
// https://chromium.googlesource.com/chromium/src/+/938b37a6d2886bf8335fc7db792f1eb46c65b2ae/third_party/blink/common/page/page_zoom.cc
@ -99,7 +101,14 @@ export class ZoomFactorService extends EventEmitter {
window.webContents.send('zoomFactorChanged', zoomFactor);
};
const initialZoomFactor = await this.getZoomFactor();
let initialZoomFactor: number;
try {
initialZoomFactor = await this.getZoomFactor();
} catch (error) {
log.error('Failed to get zoom factor', error);
initialZoomFactor = DEFAULT_ZOOM_FACTOR;
}
window.once('ready-to-show', () => {
// Workaround to apply zoomFactor because webPreferences does not handle it
// https://github.com/electron/electron/issues/10572