Migrate from keytar to safeStorage

This commit is contained in:
Michael Telatynski 2023-07-17 12:03:23 +01:00
parent fedaba9583
commit 08d844f89f
No known key found for this signature in database
GPG Key ID: A2B008A5F49F5D0D
8 changed files with 180 additions and 75 deletions

View File

@ -19,6 +19,7 @@ import Store from "electron-store";
import AutoLaunch from "auto-launch"; import AutoLaunch from "auto-launch";
import { AppLocalization } from "../language-helper"; import { AppLocalization } from "../language-helper";
import { StoreData } from "../electron-main";
// global type extensions need to use var for whatever reason // global type extensions need to use var for whatever reason
/* eslint-disable no-var */ /* eslint-disable no-var */
@ -33,13 +34,6 @@ declare global {
icon_path: string; icon_path: string;
brand: string; brand: string;
}; };
var store: Store<{ var store: Store<StoreData>;
warnBeforeExit?: boolean;
minimizeToTray?: boolean;
spellCheckerEnabled?: boolean;
autoHideMenuBar?: boolean;
locale?: string | string[];
disableHardwareAcceleration?: boolean;
}>;
} }
/* eslint-enable no-var */ /* eslint-enable no-var */

View File

@ -30,10 +30,10 @@ import { URL } from "url";
import minimist from "minimist"; import minimist from "minimist";
import "./ipc"; import "./ipc";
import "./keytar";
import "./seshat"; import "./seshat";
import "./settings"; import "./settings";
import * as tray from "./tray"; import * as tray from "./tray";
import { migrate as migrateSafeStorage } from "./safe-storage";
import { buildMenuTemplate } from "./vectormenu"; import { buildMenuTemplate } from "./vectormenu";
import webContentsHandler from "./webcontents-handler"; import webContentsHandler from "./webcontents-handler";
import * as updater from "./updater"; import * as updater from "./updater";
@ -252,7 +252,53 @@ async function moveAutoLauncher(): Promise<void> {
} }
} }
global.store = new Store({ name: "electron-config" }); 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; global.appQuitting = false;
@ -345,12 +391,14 @@ app.enableSandbox();
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService"); app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
// Disable hardware acceleration if the setting has been set. // Disable hardware acceleration if the setting has been set.
if (global.store.get("disableHardwareAcceleration", false) === true) { if (global.store.get("disableHardwareAcceleration") === true) {
console.log("Disabling hardware acceleration."); console.log("Disabling hardware acceleration.");
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
} }
app.on("ready", async () => { app.on("ready", async () => {
await migrateSafeStorage();
let asarPath: string; let asarPath: string;
try { try {
@ -456,7 +504,7 @@ app.on("ready", async () => {
icon: global.trayConfig.icon_path, icon: global.trayConfig.icon_path,
show: false, show: false,
autoHideMenuBar: global.store.get("autoHideMenuBar", true), autoHideMenuBar: global.store.get("autoHideMenuBar"),
x: mainWindowState.x, x: mainWindowState.x,
y: mainWindowState.y, y: mainWindowState.y,
@ -477,7 +525,7 @@ app.on("ready", async () => {
global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true)); global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true));
// Create trayIcon icon // Create trayIcon icon
if (global.store.get("minimizeToTray", true)) tray.create(global.trayConfig); if (global.store.get("minimizeToTray")) tray.create(global.trayConfig);
global.mainWindow.once("ready-to-show", () => { global.mainWindow.once("ready-to-show", () => {
if (!global.mainWindow) return; if (!global.mainWindow) return;

View File

@ -21,7 +21,7 @@ import IpcMainEvent = Electron.IpcMainEvent;
import { recordSSOSession } from "./protocol"; import { recordSSOSession } from "./protocol";
import { randomArray } from "./utils"; import { randomArray } from "./utils";
import { Settings } from "./settings"; import { Settings } from "./settings";
import { keytar } from "./keytar"; import { deletePassword, getPassword, setPassword } from "./safe-storage";
import { getDisplayMediaCallback, setDisplayMediaCallback } from "./displayMediaCallback"; import { getDisplayMediaCallback, setDisplayMediaCallback } from "./displayMediaCallback";
ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void { ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void {
@ -125,7 +125,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
break; break;
case "getSpellCheckEnabled": case "getSpellCheckEnabled":
ret = global.store.get("spellCheckerEnabled", true); ret = global.store.get("spellCheckerEnabled");
break; break;
case "setSpellCheckLanguages": case "setSpellCheckLanguages":
@ -149,12 +149,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
case "getPickleKey": case "getPickleKey":
try { try {
ret = await keytar?.getPassword("element.io", `${args[0]}|${args[1]}`); ret = await getPassword(`${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
if (ret === null) {
ret = await keytar?.getPassword("riot.im", `${args[0]}|${args[1]}`);
}
} catch (e) { } catch (e) {
// if an error is thrown (e.g. keytar can't connect to the keychain), // 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 // then return null, which means the default pickle key will be used
@ -165,7 +160,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
case "createPickleKey": case "createPickleKey":
try { try {
const pickleKey = await randomArray(32); const pickleKey = await randomArray(32);
await keytar?.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey); await setPassword(`${args[0]}|${args[1]}`, pickleKey);
ret = pickleKey; ret = pickleKey;
} catch (e) { } catch (e) {
ret = null; ret = null;
@ -174,10 +169,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
case "destroyPickleKey": case "destroyPickleKey":
try { try {
await keytar?.deletePassword("element.io", `${args[0]}|${args[1]}`); await deletePassword(`${args[0]}|${args[1]}`);
// migrate from riot.im (remove once we think there will no longer be
// logins from the time of riot.im)
await keytar?.deletePassword("riot.im", `${args[0]}|${args[1]}`);
} catch (e) {} } catch (e) {}
break; break;
case "getDesktopCapturerSources": case "getDesktopCapturerSources":

View File

@ -1,31 +0,0 @@
/*
Copyright 2022 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 type * as Keytar from "keytar"; // Hak dependency type
let keytar: typeof Keytar | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
keytar = require("keytar");
} catch (e) {
if ((<NodeJS.ErrnoException>e).code === "MODULE_NOT_FOUND") {
console.log("Keytar isn't installed; secure key storage is disabled.");
} else {
console.warn("Keytar unexpected error:", e);
}
}
export { keytar };

View File

@ -16,8 +16,6 @@ limitations under the License.
import counterpart from "counterpart"; import counterpart from "counterpart";
import type Store from "electron-store";
const FALLBACK_LOCALE = "en"; const FALLBACK_LOCALE = "en";
export function _td(text: string): string { export function _td(text: string): string {
@ -63,7 +61,7 @@ export function _t(text: string, variables: IVariables = {}): string {
type Component = () => void; type Component = () => void;
type TypedStore = Store<{ locale?: string | string[] }>; type TypedStore = (typeof global)["store"];
export class AppLocalization { export class AppLocalization {
private static readonly STORE_KEY = "locale"; private static readonly STORE_KEY = "locale";

106
src/safe-storage.ts Normal file
View File

@ -0,0 +1,106 @@
/*
Copyright 2022 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 type * as Keytar from "keytar";
const KEYTAR_SERVICE = "element.io";
const LEGACY_KEYTAR_SERVICE = "riot.im";
let keytar: typeof Keytar | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
keytar = require("keytar");
} catch (e) {
if ((<NodeJS.ErrnoException>e).code === "MODULE_NOT_FOUND") {
console.log("Keytar isn't installed; secure key storage is disabled.");
} else {
console.warn("Keytar unexpected error:", e);
}
}
export async function migrate(): Promise<void> {
if (global.store.get("migratedToSafeStorage")) return; // already done
if (keytar) {
const credentials = [
...(await keytar.findCredentials(LEGACY_KEYTAR_SERVICE)),
...(await keytar.findCredentials(KEYTAR_SERVICE)),
];
credentials.forEach((cred) => {
deletePassword(cred.account);
setPassword(cred.account, cred.password);
});
}
global.store.set("migratedToSafeStorage", true);
}
/**
* Get the stored password for the key.
*
* @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(`safeStorage.${key}`);
if (typeof encryptedValue === "string") {
return safeStorage.decryptString(Buffer.from(encryptedValue));
}
}
if (keytar) {
return (
(await keytar.getPassword(KEYTAR_SERVICE, key)) ?? (await keytar.getPassword(LEGACY_KEYTAR_SERVICE, key))
);
}
return null;
}
/**
* Add the password for the key to the keychain.
*
* @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(`safeStorage.${key}`, encryptedValue.toString());
}
await keytar?.setPassword(KEYTAR_SERVICE, key, password);
}
/**
* Delete the stored password for the key.
*
* @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(`safeStorage.${key}`);
await keytar?.deletePassword(LEGACY_KEYTAR_SERVICE, key);
await keytar?.deletePassword(KEYTAR_SERVICE, key);
return true;
}
return false;
}

View File

@ -25,7 +25,7 @@ import type {
} from "matrix-seshat"; // Hak dependency type } from "matrix-seshat"; // Hak dependency type
import IpcMainEvent = Electron.IpcMainEvent; import IpcMainEvent = Electron.IpcMainEvent;
import { randomArray } from "./utils"; import { randomArray } from "./utils";
import { keytar } from "./keytar"; import { getPassword, setPassword } from "./safe-storage";
let seshatSupported = false; let seshatSupported = false;
let Seshat: typeof SeshatType; let Seshat: typeof SeshatType;
@ -51,19 +51,17 @@ let eventIndex: SeshatType | null = null;
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE"; const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
async function getOrCreatePassphrase(key: string): Promise<string> { async function getOrCreatePassphrase(key: string): Promise<string> {
if (keytar) { try {
try { const storedPassphrase = await getPassword(key);
const storedPassphrase = await keytar.getPassword("element.io", key); if (storedPassphrase !== null) {
if (storedPassphrase !== null) { return storedPassphrase;
return storedPassphrase; } else {
} else { const newPassphrase = await randomArray(32);
const newPassphrase = await randomArray(32); await setPassword(key, newPassphrase);
await keytar.setPassword("element.io", key, newPassphrase); return newPassphrase;
return newPassphrase;
}
} catch (e) {
console.log("Error getting the event index passphrase out of the secret store", e);
} }
} catch (e) {
console.log("Error getting the event index passphrase out of the secret store", e);
} }
return seshatDefaultPassphrase; return seshatDefaultPassphrase;
} }

View File

@ -36,7 +36,7 @@ export const Settings: Record<string, Setting> = {
}, },
"Electron.warnBeforeExit": { "Electron.warnBeforeExit": {
async read(): Promise<any> { async read(): Promise<any> {
return global.store.get("warnBeforeExit", true); return global.store.get("warnBeforeExit");
}, },
async write(value: any): Promise<void> { async write(value: any): Promise<void> {
global.store.set("warnBeforeExit", value); global.store.set("warnBeforeExit", value);
@ -70,7 +70,7 @@ export const Settings: Record<string, Setting> = {
}, },
"Electron.enableHardwareAcceleration": { "Electron.enableHardwareAcceleration": {
async read(): Promise<any> { async read(): Promise<any> {
return !global.store.get("disableHardwareAcceleration", false); return !global.store.get("disableHardwareAcceleration");
}, },
async write(value: any): Promise<void> { async write(value: any): Promise<void> {
global.store.set("disableHardwareAcceleration", !value); global.store.set("disableHardwareAcceleration", !value);