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

View file

@ -30,10 +30,10 @@ import { URL } from "url";
import minimist from "minimist";
import "./ipc";
import "./keytar";
import "./seshat";
import "./settings";
import * as tray from "./tray";
import { migrate as migrateSafeStorage } from "./safe-storage";
import { buildMenuTemplate } from "./vectormenu";
import webContentsHandler from "./webcontents-handler";
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;
@ -345,12 +391,14 @@ app.enableSandbox();
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
// 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.");
app.disableHardwareAcceleration();
}
app.on("ready", async () => {
await migrateSafeStorage();
let asarPath: string;
try {
@ -456,7 +504,7 @@ app.on("ready", async () => {
icon: global.trayConfig.icon_path,
show: false,
autoHideMenuBar: global.store.get("autoHideMenuBar", true),
autoHideMenuBar: global.store.get("autoHideMenuBar"),
x: mainWindowState.x,
y: mainWindowState.y,
@ -477,7 +525,7 @@ app.on("ready", async () => {
global.mainWindow.webContents.session.setSpellCheckerEnabled(global.store.get("spellCheckerEnabled", true));
// 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", () => {
if (!global.mainWindow) return;

View file

@ -21,7 +21,7 @@ import IpcMainEvent = Electron.IpcMainEvent;
import { recordSSOSession } from "./protocol";
import { randomArray } from "./utils";
import { Settings } from "./settings";
import { keytar } from "./keytar";
import { deletePassword, getPassword, setPassword } from "./safe-storage";
import { getDisplayMediaCallback, setDisplayMediaCallback } from "./displayMediaCallback";
ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void {
@ -125,7 +125,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
break;
case "getSpellCheckEnabled":
ret = global.store.get("spellCheckerEnabled", true);
ret = global.store.get("spellCheckerEnabled");
break;
case "setSpellCheckLanguages":
@ -149,12 +149,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
case "getPickleKey":
try {
ret = await keytar?.getPassword("element.io", `${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]}`);
}
ret = await getPassword(`${args[0]}|${args[1]}`);
} catch (e) {
// 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
@ -165,7 +160,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
case "createPickleKey":
try {
const pickleKey = await randomArray(32);
await keytar?.setPassword("element.io", `${args[0]}|${args[1]}`, pickleKey);
await setPassword(`${args[0]}|${args[1]}`, pickleKey);
ret = pickleKey;
} catch (e) {
ret = null;
@ -174,10 +169,7 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
case "destroyPickleKey":
try {
await keytar?.deletePassword("element.io", `${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]}`);
await deletePassword(`${args[0]}|${args[1]}`);
} catch (e) {}
break;
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 type Store from "electron-store";
const FALLBACK_LOCALE = "en";
export function _td(text: string): string {
@ -63,7 +61,7 @@ export function _t(text: string, variables: IVariables = {}): string {
type Component = () => void;
type TypedStore = Store<{ locale?: string | string[] }>;
type TypedStore = (typeof global)["store"];
export class AppLocalization {
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
import IpcMainEvent = Electron.IpcMainEvent;
import { randomArray } from "./utils";
import { keytar } from "./keytar";
import { getPassword, setPassword } from "./safe-storage";
let seshatSupported = false;
let Seshat: typeof SeshatType;
@ -51,19 +51,17 @@ let eventIndex: SeshatType | null = null;
const seshatDefaultPassphrase = "DEFAULT_PASSPHRASE";
async function getOrCreatePassphrase(key: string): Promise<string> {
if (keytar) {
try {
const storedPassphrase = await keytar.getPassword("element.io", key);
if (storedPassphrase !== null) {
return storedPassphrase;
} else {
const newPassphrase = await randomArray(32);
await keytar.setPassword("element.io", key, newPassphrase);
return newPassphrase;
}
} catch (e) {
console.log("Error getting the event index passphrase out of the secret store", e);
try {
const storedPassphrase = await getPassword(key);
if (storedPassphrase !== null) {
return storedPassphrase;
} else {
const newPassphrase = await randomArray(32);
await setPassword(key, newPassphrase);
return newPassphrase;
}
} catch (e) {
console.log("Error getting the event index passphrase out of the secret store", e);
}
return seshatDefaultPassphrase;
}

View file

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