Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2025-04-16 12:49:53 +01:00
parent bec721cf57
commit 4889634f0b
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
3 changed files with 158 additions and 17 deletions

View File

@ -364,6 +364,8 @@ app.enableSandbox();
// We disable media controls here. We do this because calls use audio and video elements and they sometimes capture the media keys. See https://github.com/vector-im/element-web/issues/15704
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
store.prepare(); // must be called before any async actions
// Disable hardware acceleration if the setting has been set.
if (store.get("disableHardwareAcceleration") === true) {
console.log("Disabling hardware acceleration.");

View File

@ -19,6 +19,8 @@
"zoom_out": "Zoom Out"
},
"common": {
"yes": "Yes",
"no": "No",
"about": "About",
"brand_help": "%(brand)s Help",
"help": "Help",
@ -59,5 +61,12 @@
"bring_all_to_front": "Bring All to Front",
"label": "Window",
"zoom": "Zoom"
},
"store": {
"error": {
"title": "Failed to load configuration",
"unsupported_backend_override": "TODO",
"unknown_backend_override": "TODO"
}
}
}

View File

@ -16,7 +16,9 @@ limitations under the License.
import ElectronStore from "electron-store";
import keytar from "keytar-forked";
import { app, safeStorage } from "electron";
import { app, safeStorage, dialog, type SafeStorage } from "electron";
import { _t } from "./language-helper.js";
/**
* Legacy keytar service names for storing secrets.
@ -24,6 +26,20 @@ import { app, safeStorage } from "electron";
const KEYTAR_SERVICE = "element.io";
const LEGACY_KEYTAR_SERVICE = "riot.im";
type SafeStorageBackend = ReturnType<SafeStorage["getSelectedStorageBackend"]>;
/**
* Map of safeStorage backends to their command line arguments.
* kwallet6 cannot be specified via command line
* https://www.electronjs.org/docs/latest/api/safe-storage#safestoragegetselectedstoragebackend-linux
*/
const safeStorageBackendMap: Omit<Record<SafeStorageBackend, string>, "unknown" | "kwallet6"> = {
basic_text: "basic",
gnome_libsecret: "gnome-libsecret",
kwallet: "kwallet",
kwallet5: "kwallet5",
};
/**
* JSON-backed store for settings which need to be accessible by the main process.
* Secrets are stored within the `safeStorage` object, encrypted with safeStorage.
@ -37,6 +53,10 @@ class Store extends ElectronStore<{
locale?: string | string[];
disableHardwareAcceleration: boolean;
safeStorage?: Record<string, string>;
// Only known for Linux - the safeStorage backend used for the safeStorage data as written
safeStorageBackend?: SafeStorageBackend;
// Only valid for Linux - whether to override the safeStorage backend via commandLine
safeStorageBackendOverride?: boolean;
}> {
public constructor() {
super({
@ -69,37 +89,147 @@ class Store extends ElectronStore<{
safeStorage: {
type: "object",
},
safeStorageBackend: {
type: "string",
},
safeStorageBackendOverride: {
type: "boolean",
},
},
});
}
private whenSafeStorageReadyPromise?: Promise<unknown>;
public async safeStorageReady(): Promise<void> {
if (!this.whenSafeStorageReadyPromise) {
this.whenSafeStorageReadyPromise = Promise.allSettled([app.whenReady().then(() => this.migrateSecrets())]);
/**
* Prepare the store, does not prepare safeStorage, which needs to be done after the app is ready.
* Must be executed in the first tick of the event loop so that it can call Electron APIs before ready state.
*/
public prepare(): void {
if (process.platform === "linux") {
if (this.get("safeStorageBackendOverride")) {
const backend = this.get("safeStorageBackend")!;
if (backend in safeStorageBackendMap) {
app.commandLine.appendSwitch(
"password-store",
safeStorageBackendMap[backend as keyof typeof safeStorageBackendMap],
);
} else {
// This case should never happen, but could due to a downgrade or a modified store.
dialog.showErrorBox(_t("store|error|title"), _t("store|error|unsupported_backend_override"));
throw new Error("safeStorage backend override is not supported");
}
await this.whenSafeStorageReadyPromise;
}
}
}
private safeStorageReadyPromise?: Promise<unknown>;
public async safeStorageReady(): Promise<void> {
if (!this.safeStorageReadyPromise) {
this.safeStorageReadyPromise = this.prepareSafeStorage();
}
await this.safeStorageReadyPromise;
}
private getSecretStorageKey = (key: string) => `safeStorage.${key}` as const;
private async prepareSafeStorage(): Promise<void> {
await app.whenReady();
if (process.platform === "linux") {
// Linux safeStorage support is hellish, the support varies on the Desktop Environment used rather than the store itself.
// https://github.com/electron/electron/issues/39789 https://github.com/microsoft/vscode/issues/185212
let safeStorageBackend = this.get("safeStorageBackend");
const selectedSafeStorageBackend = safeStorage.getSelectedStorageBackend();
if (selectedSafeStorageBackend === "unknown") {
// This should never happen but good to be safe
dialog.showErrorBox(_t("store|error|title"), _t("store|error|unknown_backend_override"));
throw new Error("safeStorage backend unknown");
}
if (!safeStorageBackend) {
if (selectedSafeStorageBackend === "basic_text") {
// Ask the user if they want to use plain text encryption
// TODO should we only do this if they have existing data
const { response } = await dialog.showMessageBox({
// TODO
title: "Error 1",
message: "Message",
// detail: _t(""),
type: "question",
buttons: [_t("common|no"), _t("common|yes")],
defaultId: 0,
cancelId: 0,
});
if (response === 0) {
throw new Error("safeStorage backend basic_text and user rejected it");
}
}
// Store the backend used for the safeStorage data so we can detect if it changes
this.set("safeStorageBackend", selectedSafeStorageBackend);
safeStorageBackend = selectedSafeStorageBackend;
} else if (safeStorageBackend !== selectedSafeStorageBackend) {
console.warn(`safeStorage backend changed from ${safeStorageBackend} to ${selectedSafeStorageBackend}`);
if (safeStorageBackend === "basic_text") {
console.info(`Migrating safeStorage from basic_text to ${selectedSafeStorageBackend}`);
const data = this.get("safeStorage");
if (data) {
for (const key in data) {
const plaintext = data[key];
await this.setSecret(key, plaintext);
}
}
} else if (safeStorageBackend in safeStorageBackendMap) {
// Warn the user that the backend has changed and ask if they wish to use the old one
const { response } = await dialog.showMessageBox({
// TODO
title: "Error 2",
message: "Message",
// detail: _t(""),
type: "question",
buttons: [_t("common|no"), _t("common|yes")],
defaultId: 0,
cancelId: 0,
});
if (response === 0) {
throw new Error("safeStorage backend changed and user rejected mitigation");
}
this.set("safeStorageBackendOverride", true);
app.relaunch();
} else {
// Warn the user that the backend has changed and tell them that we cannot migrate
// dialog.showErrorBox(_t(""), _t("")); TODO
throw new Error("safeStorage backend changed and cannot migrate");
}
}
if (safeStorageBackend === "basic_text" && selectedSafeStorageBackend === safeStorageBackend) {
// TODO verify if this even works, the docstring makes it sound ephemeral!
safeStorage.setUsePlainTextEncryption(true);
}
}
if (!safeStorage.isEncryptionAvailable()) {
console.error("Store migration: safeStorage is not available");
throw new Error(`safeStorage is not available`);
// TODO fatal error
}
await this.migrateSecrets();
}
/**
* Migrates keytar data to safeStorage,
* deletes data from legacy keytar but keeps it in the new keytar for downgrade compatibility.
* @throws if safeStorage is not available.
* @throws if safeStorage is not available. TODO
*/
private async migrateSecrets(): Promise<void> {
if (this.has("safeStorage")) return;
if (this.has("safeStorage")) return; // already migrated
console.info("Store migration: started");
if (
!safeStorage.isEncryptionAvailable() &&
!(process.platform === "linux" && safeStorage.getSelectedStorageBackend() === "basic_text")
) {
console.error(
"Store migration: safeStorage is not available with backend",
safeStorage.getSelectedStorageBackend(),
);
throw new Error("safeStorage is not available");
if (process.platform === "linux" && safeStorage.getSelectedStorageBackend() === "basic_text") {
console.warn("Store migration: safeStorage is using basic text encryption");
}
try {