mirror of
https://github.com/element-hq/element-desktop
synced 2025-04-01 20:03:43 +02:00
Migrate from keytar to safeStorage
This commit is contained in:
parent
fedaba9583
commit
08d844f89f
10
src/@types/global.d.ts
vendored
10
src/@types/global.d.ts
vendored
@ -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 */
|
||||||
|
@ -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;
|
||||||
|
18
src/ipc.ts
18
src/ipc.ts
@ -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":
|
||||||
|
@ -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 };
|
|
@ -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
106
src/safe-storage.ts
Normal 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;
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user