Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2025-03-28 22:45:13 +00:00
parent ef4b41dac5
commit d509da2b09
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
7 changed files with 203 additions and 193 deletions

View File

@ -7,10 +7,9 @@ Please see LICENSE files in the repository root for full details.
import { type BrowserWindow } from "electron";
import type Store from "electron-store";
import type AutoLaunch from "auto-launch";
import { type AppLocalization } from "../language-helper.js";
import { type StoreData } from "../electron-main.js";
import { type Store } from "../store.js";
// global type extensions need to use var for whatever reason
/* eslint-disable no-var */
@ -25,6 +24,6 @@ declare global {
icon_path: string;
brand: string;
};
var store: Store<StoreData>;
var store: Store;
}
/* eslint-enable no-var */

View File

@ -16,7 +16,6 @@ import * as Sentry from "@sentry/electron/main";
import AutoLaunch from "auto-launch";
import path, { dirname } from "node:path";
import windowStateKeeper from "electron-window-state";
import Store from "electron-store";
import fs, { promises as afs } from "node:fs";
import { URL, fileURLToPath } from "node:url";
import minimist from "minimist";
@ -25,7 +24,7 @@ import "./ipc.js";
import "./seshat.js";
import "./settings.js";
import * as tray from "./tray.js";
import { migrate as migrateSafeStorage } from "./safe-storage.js";
import { Store } from "./store.js";
import { buildMenuTemplate } from "./vectormenu.js";
import webContentsHandler from "./webcontents-handler.js";
import * as updater from "./updater.js";
@ -258,54 +257,6 @@ async function moveAutoLauncher(): Promise<void> {
}
}
export interface StoreData {
warnBeforeExit: boolean;
minimizeToTray: boolean;
spellCheckerEnabled: boolean;
autoHideMenuBar: boolean;
locale?: string | string[];
disableHardwareAcceleration: boolean;
migratedToSafeStorage: boolean;
safeStorage: Record<string, string>;
}
global.store = new Store({
name: "electron-config",
schema: {
warnBeforeExit: {
type: "boolean",
default: true,
},
minimizeToTray: {
type: "boolean",
default: true,
},
spellCheckerEnabled: {
type: "boolean",
default: true,
},
autoHideMenuBar: {
type: "boolean",
default: true,
},
locale: {
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
},
disableHardwareAcceleration: {
type: "boolean",
default: false,
},
migratedToSafeStorage: {
type: "boolean",
default: false,
},
safeStorage: {
type: "object",
additionalProperties: { type: "string" },
},
},
}) as Store<StoreData>;
global.appQuitting = false;
const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
@ -315,32 +266,6 @@ const exitShortcuts: Array<(input: Input, platform: string) => boolean> = [
platform === "darwin" && input.meta && !input.control && input.key.toUpperCase() === "Q",
];
const warnBeforeExit = (event: Event, input: Input): void => {
const shouldWarnBeforeExit = global.store.get("warnBeforeExit", true);
const exitShortcutPressed =
input.type === "keyDown" && exitShortcuts.some((shortcutFn) => shortcutFn(input, process.platform));
if (shouldWarnBeforeExit && exitShortcutPressed && global.mainWindow) {
const shouldCancelCloseRequest =
dialog.showMessageBoxSync(global.mainWindow, {
type: "question",
buttons: [
_t("action|cancel"),
_t("action|close_brand", {
brand: global.vectorConfig.brand || "Element",
}),
],
message: _t("confirm_quit"),
defaultId: 1,
cancelId: 0,
}) === 0;
if (shouldCancelCloseRequest) {
event.preventDefault();
}
}
};
void configureSentry();
// handle uncaught errors otherwise it displays
@ -397,15 +322,15 @@ 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");
const store = new Store();
// Disable hardware acceleration if the setting has been set.
if (global.store.get("disableHardwareAcceleration") === true) {
if (store.get("disableHardwareAcceleration") === true) {
console.log("Disabling hardware acceleration.");
app.disableHardwareAcceleration();
}
app.on("ready", async () => {
await migrateSafeStorage();
let asarPath: string;
try {
@ -511,7 +436,7 @@ app.on("ready", async () => {
icon: global.trayConfig.icon_path,
show: false,
autoHideMenuBar: global.store.get("autoHideMenuBar"),
autoHideMenuBar: store.get("autoHideMenuBar"),
x: mainWindowState.x,
y: mainWindowState.y,
@ -533,10 +458,10 @@ app.on("ready", async () => {
// Handle spellchecker
// For some reason spellCheckerEnabled isn't persisted, so we have to use the store here
global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true));
global.mainWindow.webContents.session.setSpellCheckerEnabled(store.get("spellCheckerEnabled", true));
// Create trayIcon icon
if (global.store.get("minimizeToTray")) tray.create(global.trayConfig);
if (store.get("minimizeToTray")) tray.create(global.trayConfig);
global.mainWindow.once("ready-to-show", () => {
if (!global.mainWindow) return;
@ -550,7 +475,31 @@ app.on("ready", async () => {
}
});
global.mainWindow.webContents.on("before-input-event", warnBeforeExit);
global.mainWindow.webContents.on("before-input-event", (event: Event, input: Input): void => {
const shouldWarnBeforeExit = store.get("warnBeforeExit", true);
const exitShortcutPressed =
input.type === "keyDown" && exitShortcuts.some((shortcutFn) => shortcutFn(input, process.platform));
if (shouldWarnBeforeExit && exitShortcutPressed && global.mainWindow) {
const shouldCancelCloseRequest =
dialog.showMessageBoxSync(global.mainWindow, {
type: "question",
buttons: [
_t("action|cancel"),
_t("action|close_brand", {
brand: global.vectorConfig.brand || "Element",
}),
],
message: _t("confirm_quit"),
defaultId: 1,
cancelId: 0,
}) === 0;
if (shouldCancelCloseRequest) {
event.preventDefault();
}
}
});
global.mainWindow.on("closed", () => {
global.mainWindow = null;
@ -589,7 +538,7 @@ app.on("ready", async () => {
webContentsHandler(global.mainWindow.webContents);
global.appLocalization = new AppLocalization({
store: global.store,
store,
components: [(): void => tray.initApplicationMenu(), (): void => Menu.setApplicationMenu(buildMenuTemplate())],
});

View File

@ -13,7 +13,6 @@ import { recordSSOSession } from "./protocol.js";
import { randomArray } from "./utils.js";
import { Settings } from "./settings.js";
import { getDisplayMediaCallback, setDisplayMediaCallback } from "./displayMediaCallback.js";
import { deletePassword, getPassword, setPassword } from "./safe-storage.js";
ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void {
if (process.platform !== "win32") {
@ -141,7 +140,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
case "getPickleKey":
try {
ret = await getPassword(`${args[0]}|${args[1]}`);
ret = await global.store.getSecret(`${args[0]}|${args[1]}`);
} catch {
// if an error is thrown (e.g. keytar can't connect to the keychain),
// then return null, which means the default pickle key will be used
@ -152,7 +151,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
case "createPickleKey":
try {
const pickleKey = await randomArray(32);
await setPassword(`${args[0]}|${args[1]}`, pickleKey);
await global.store.setSecret(`${args[0]}|${args[1]}`, pickleKey);
ret = pickleKey;
} catch (e) {
console.error("Failed to create pickle key", e);
@ -162,7 +161,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
case "destroyPickleKey":
try {
await deletePassword(`${args[0]}|${args[1]}`);
await global.store.deleteSecret(`${args[0]}|${args[1]}`);
} catch (e) {
console.error("Failed to destroy pickle key", e);
}

View File

@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
import type EN from "./i18n/strings/en_EN.json";
import { loadJsonFile } from "./utils.js";
import { Store } from "./store.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
@ -58,15 +59,13 @@ export function _t(text: TranslationKey, variables: Variables = {}): string {
type Component = () => void;
type TypedStore = (typeof global)["store"];
export class AppLocalization {
private static readonly STORE_KEY = "locale";
private readonly store: TypedStore;
private readonly store: Store;
private readonly localizedComponents?: Set<Component>;
public constructor({ store, components = [] }: { store: TypedStore; components: Component[] }) {
public constructor({ store, components = [] }: { store: Store; components: Component[] }) {
counterpart.registerTranslations(FALLBACK_LOCALE, this.fetchTranslationJson("en_EN"));
counterpart.setFallbackLocale(FALLBACK_LOCALE);
counterpart.setSeparator("|");

View File

@ -1,95 +0,0 @@
/*
Copyright 2022-2025 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { safeStorage } from "electron";
import * as keytar from "keytar-forked";
const KEYTAR_SERVICE = "element.io";
const LEGACY_KEYTAR_SERVICE = "riot.im";
const getStorageKey = (key: string) => `safeStorage.${key}` as const;
/**
* Migrates keytar data to safeStorage,
* deletes data from legacy keytar but keeps it in the new keytar for downgrade compatibility.
*/
export async function migrate(): Promise<void> {
if (global.store.get("migratedToSafeStorage")) return; // already done
const credentials = [
...(await keytar.findCredentials(LEGACY_KEYTAR_SERVICE)),
...(await keytar.findCredentials(KEYTAR_SERVICE)),
];
credentials.forEach((cred) => {
deletePassword(cred.account); // delete from keytar & keytar legacy
setPassword(cred.account, cred.password); // write to safeStorage & keytar for downgrade compatibility
});
global.store.set("migratedToSafeStorage", true);
}
/**
* Get the stored password for the key.
* We read from safeStorage first, then keytar & keytar legacy.
*
* @param key The string key name.
*
* @returns A promise for the password string.
*/
export async function getPassword(key: string): Promise<string | null> {
if (safeStorage.isEncryptionAvailable()) {
const encryptedValue = global.store.get(getStorageKey(key));
if (typeof encryptedValue === "string") {
return safeStorage.decryptString(Buffer.from(encryptedValue));
}
}
return (await keytar.getPassword(KEYTAR_SERVICE, key)) ?? (await keytar.getPassword(LEGACY_KEYTAR_SERVICE, key));
}
/**
* Add the password for the key to the keychain.
* We write to both safeStorage & keytar to support downgrading the application.
*
* @param key The string key name.
* @param password The string password.
*
* @returns A promise for the set password completion.
*/
export async function setPassword(key: string, password: string): Promise<void> {
if (safeStorage.isEncryptionAvailable()) {
const encryptedValue = safeStorage.encryptString(password);
global.store.set(getStorageKey(key), encryptedValue.toString());
}
await keytar.setPassword(KEYTAR_SERVICE, key, password);
}
/**
* Delete the stored password for the key.
* Removes from safeStorage, keytar & keytar legacy.
*
* @param key The string key name.
*
* @returns A promise for the deletion status. True on success.
*/
export async function deletePassword(key: string): Promise<boolean> {
if (safeStorage.isEncryptionAvailable()) {
global.store.delete(getStorageKey(key));
await keytar.deletePassword(LEGACY_KEYTAR_SERVICE, key);
await keytar.deletePassword(KEYTAR_SERVICE, key);
return true;
}
return false;
}

View File

@ -16,7 +16,6 @@ import type {
} from "matrix-seshat"; // Hak dependency type
import IpcMainEvent = Electron.IpcMainEvent;
import { randomArray } from "./utils.js";
import { getPassword, setPassword } from "./safe-storage.js";
let seshatSupported = false;
let Seshat: typeof SeshatType;
@ -42,12 +41,12 @@ let eventIndex: SeshatType | null = null;
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
async function getOrCreatePassphrase(key: string): Promise<string> {
try {
const storedPassphrase = await getPassword(key);
const storedPassphrase = await global.store.getSecret(key);
if (storedPassphrase !== null) {
return storedPassphrase;
} else {
const newPassphrase = await randomArray(32);
await setPassword(key, newPassphrase);
await global.store.setSecret(key, newPassphrase);
return newPassphrase;
}
} catch (e) {

160
src/store.ts Normal file
View File

@ -0,0 +1,160 @@
/*
Copyright 2022-2025 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import _Store from "electron-store";
import * as keytar from "keytar-forked";
import { app, safeStorage } from "electron";
/**
* Legacy keytar service names for storing secrets.
*/
const KEYTAR_SERVICE = "element.io";
const LEGACY_KEYTAR_SERVICE = "riot.im";
/**
* JSON-backed store for settings which need to be accessible by the main process.
* Secrets are stored within the `safeStorage` object, encrypted with safeStorage.
* Any secrets operations are blocked on Electron app ready emit, and keytar migration if still needed.
*/
export class Store extends _Store<{
warnBeforeExit: boolean;
minimizeToTray: boolean;
spellCheckerEnabled: boolean;
autoHideMenuBar: boolean;
locale?: string | string[];
disableHardwareAcceleration: boolean;
safeStorage?: Record<string, string>;
}> {
public constructor() {
super({
name: "electron-config",
clearInvalidConfig: false,
schema: {
warnBeforeExit: {
type: "boolean",
default: true,
},
minimizeToTray: {
type: "boolean",
default: true,
},
spellCheckerEnabled: {
type: "boolean",
default: true,
},
autoHideMenuBar: {
type: "boolean",
default: true,
},
locale: {
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
},
disableHardwareAcceleration: {
type: "boolean",
default: false,
},
safeStorage: {
type: "object",
},
},
});
}
private whenSafeStorageReadyPromise?: Promise<void>;
private safeStorageReady(): Promise<void> {
if (!this.whenSafeStorageReadyPromise) {
this.whenSafeStorageReadyPromise = app.whenReady().then(() => this.migrate());
}
return this.whenSafeStorageReadyPromise;
}
private getSecretStorageKey = (key: string) => `safeStorage.${key}` as const;
/**
* Migrates keytar data to safeStorage,
* deletes data from legacy keytar but keeps it in the new keytar for downgrade compatibility.
*/
public async migrate(): Promise<void> {
if (this.has("safeStorage")) return;
await this.safeStorageReady();
const credentials = [
...(await keytar.findCredentials(LEGACY_KEYTAR_SERVICE)),
...(await keytar.findCredentials(KEYTAR_SERVICE)),
];
credentials.forEach((cred) => {
this.deleteSecret(cred.account); // delete from keytar & keytar legacy
this.setSecret(cred.account, cred.password); // write to safeStorage & keytar for downgrade compatibility
});
}
/**
* Get the stored secret for the key.
* We read from safeStorage first, then keytar & keytar legacy.
*
* @param key The string key name.
*
* @returns A promise for the secret string.
*/
public async getSecret(key: string): Promise<string | null> {
await this.safeStorageReady();
if (safeStorage.isEncryptionAvailable()) {
const encryptedValue = this.get(this.getSecretStorageKey(key));
if (typeof encryptedValue === "string") {
return safeStorage.decryptString(Buffer.from(encryptedValue));
}
}
return (
(await keytar.getPassword(KEYTAR_SERVICE, key)) ?? (await keytar.getPassword(LEGACY_KEYTAR_SERVICE, key))
);
}
/**
* Add the secret for the key to the keychain.
* We write to both safeStorage & keytar to support downgrading the application.
*
* @param key The string key name.
* @param secret The string password.
*
* @returns A promise for the set password completion.
*/
public async setSecret(key: string, secret: string): Promise<void> {
await this.safeStorageReady();
if (safeStorage.isEncryptionAvailable()) {
const encryptedValue = safeStorage.encryptString(secret);
this.set(this.getSecretStorageKey(key), encryptedValue.toString());
}
await keytar.setPassword(KEYTAR_SERVICE, key, secret);
}
/**
* Delete the stored password for the key.
* Removes from safeStorage, keytar & keytar legacy.
*
* @param key The string key name.
*
* @returns A promise for the deletion status. True on success.
*/
public async deleteSecret(key: string): Promise<boolean> {
await this.safeStorageReady();
if (safeStorage.isEncryptionAvailable()) {
this.delete(this.getSecretStorageKey(key));
await keytar.deletePassword(LEGACY_KEYTAR_SERVICE, key);
await keytar.deletePassword(KEYTAR_SERVICE, key);
return true;
}
return false;
}
}