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 // 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"); 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. // Disable hardware acceleration if the setting has been set.
if (store.get("disableHardwareAcceleration") === true) { if (store.get("disableHardwareAcceleration") === true) {
console.log("Disabling hardware acceleration."); console.log("Disabling hardware acceleration.");

View File

@ -19,6 +19,8 @@
"zoom_out": "Zoom Out" "zoom_out": "Zoom Out"
}, },
"common": { "common": {
"yes": "Yes",
"no": "No",
"about": "About", "about": "About",
"brand_help": "%(brand)s Help", "brand_help": "%(brand)s Help",
"help": "Help", "help": "Help",
@ -59,5 +61,12 @@
"bring_all_to_front": "Bring All to Front", "bring_all_to_front": "Bring All to Front",
"label": "Window", "label": "Window",
"zoom": "Zoom" "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 ElectronStore from "electron-store";
import keytar from "keytar-forked"; 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. * Legacy keytar service names for storing secrets.
@ -24,6 +26,20 @@ import { app, safeStorage } from "electron";
const KEYTAR_SERVICE = "element.io"; const KEYTAR_SERVICE = "element.io";
const LEGACY_KEYTAR_SERVICE = "riot.im"; 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. * JSON-backed store for settings which need to be accessible by the main process.
* Secrets are stored within the `safeStorage` object, encrypted with safeStorage. * Secrets are stored within the `safeStorage` object, encrypted with safeStorage.
@ -37,6 +53,10 @@ class Store extends ElectronStore<{
locale?: string | string[]; locale?: string | string[];
disableHardwareAcceleration: boolean; disableHardwareAcceleration: boolean;
safeStorage?: Record<string, string>; 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() { public constructor() {
super({ super({
@ -69,37 +89,147 @@ class Store extends ElectronStore<{
safeStorage: { safeStorage: {
type: "object", type: "object",
}, },
safeStorageBackend: {
type: "string",
},
safeStorageBackendOverride: {
type: "boolean",
},
}, },
}); });
} }
private whenSafeStorageReadyPromise?: Promise<unknown>; /**
public async safeStorageReady(): Promise<void> { * Prepare the store, does not prepare safeStorage, which needs to be done after the app is ready.
if (!this.whenSafeStorageReadyPromise) { * Must be executed in the first tick of the event loop so that it can call Electron APIs before ready state.
this.whenSafeStorageReadyPromise = Promise.allSettled([app.whenReady().then(() => this.migrateSecrets())]); */
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 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, * Migrates keytar data to safeStorage,
* deletes data from legacy keytar but keeps it in the new keytar for downgrade compatibility. * 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> { private async migrateSecrets(): Promise<void> {
if (this.has("safeStorage")) return; if (this.has("safeStorage")) return; // already migrated
console.info("Store migration: started"); console.info("Store migration: started");
if (
!safeStorage.isEncryptionAvailable() && if (process.platform === "linux" && safeStorage.getSelectedStorageBackend() === "basic_text") {
!(process.platform === "linux" && safeStorage.getSelectedStorageBackend() === "basic_text") console.warn("Store migration: safeStorage is using basic text encryption");
) {
console.error(
"Store migration: safeStorage is not available with backend",
safeStorage.getSelectedStorageBackend(),
);
throw new Error("safeStorage is not available");
} }
try { try {