Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate from keytar to safeStorage #1087

Draft
wants to merge 7 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@
"keytar": "^7.9.0"
},
"resolutions": {
"@types/node": "16.18.38"
"@types/node": "16.18.38",
"conf": "11.0.2"
},
"build": {
"appId": "im.riot.app",
Expand Down
10 changes: 2 additions & 8 deletions src/@types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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 */
58 changes: 53 additions & 5 deletions src/electron-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
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";
Expand Down Expand Up @@ -252,7 +252,53 @@
}
}

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;

Expand All @@ -263,7 +309,7 @@
];

const warnBeforeExit = (event: Event, input: Input): void => {
const shouldWarnBeforeExit = global.store.get("warnBeforeExit", true);

Check failure on line 312 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 312 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / macOS / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 312 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x64) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 312 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x86) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.
const exitShortcutPressed =
input.type === "keyDown" && exitShortcuts.some((shortcutFn) => shortcutFn(input, process.platform));

Expand Down Expand Up @@ -345,12 +391,14 @@
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) {

Check failure on line 394 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 394 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / macOS / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 394 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x64) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 394 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x86) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.
console.log("Disabling hardware acceleration.");
app.disableHardwareAcceleration();
}

app.on("ready", async () => {
await migrateSafeStorage();

let asarPath: string;

try {
Expand Down Expand Up @@ -456,7 +504,7 @@

icon: global.trayConfig.icon_path,
show: false,
autoHideMenuBar: global.store.get("autoHideMenuBar", true),
autoHideMenuBar: global.store.get("autoHideMenuBar"),

Check failure on line 507 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 507 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / macOS / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 507 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x64) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 507 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x86) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

x: mainWindowState.x,
y: mainWindowState.y,
Expand All @@ -474,10 +522,10 @@

// 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));

Check failure on line 525 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 525 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / macOS / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 525 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x64) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 525 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x86) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

// Create trayIcon icon
if (global.store.get("minimizeToTray", true)) tray.create(global.trayConfig);
if (global.store.get("minimizeToTray")) tray.create(global.trayConfig);

Check failure on line 528 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 528 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / macOS / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 528 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x64) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 528 in src/electron-main.ts

View workflow job for this annotation

GitHub Actions / Windows (x86) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

global.mainWindow.once("ready-to-show", () => {
if (!global.mainWindow) return;
Expand Down
18 changes: 5 additions & 13 deletions src/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
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 {
Expand Down Expand Up @@ -121,11 +121,11 @@
if (typeof args[0] !== "boolean") return;

global.mainWindow.webContents.session.setSpellCheckerEnabled(args[0]);
global.store.set("spellCheckerEnabled", args[0]);

Check failure on line 124 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'set' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 124 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / macOS / build

Property 'set' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 124 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / Windows (x64) / build

Property 'set' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 124 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / Windows (x86) / build

Property 'set' does not exist on type 'ElectronStore<StoreData>'.
break;

case "getSpellCheckEnabled":
ret = global.store.get("spellCheckerEnabled", true);
ret = global.store.get("spellCheckerEnabled");

Check failure on line 128 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 128 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / macOS / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 128 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / Windows (x64) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 128 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / Windows (x86) / build

Property 'get' does not exist on type 'ElectronStore<StoreData>'.
break;

case "setSpellCheckLanguages":
Expand All @@ -149,12 +149,7 @@

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
Expand All @@ -165,7 +160,7 @@
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;
Expand All @@ -174,10 +169,7 @@

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":
Expand All @@ -194,7 +186,7 @@
break;

case "clearStorage":
global.store.clear();

Check failure on line 189 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'clear' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 189 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / macOS / build

Property 'clear' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 189 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / Windows (x64) / build

Property 'clear' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 189 in src/ipc.ts

View workflow job for this annotation

GitHub Actions / Windows (x86) / build

Property 'clear' does not exist on type 'ElectronStore<StoreData>'.
global.mainWindow.webContents.session.flushStorageData();
await global.mainWindow.webContents.session.clearStorageData();
relaunchApp();
Expand Down
31 changes: 0 additions & 31 deletions src/keytar.ts

This file was deleted.

4 changes: 1 addition & 3 deletions src/language-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

import counterpart from "counterpart";

import type Store from "electron-store";

const FALLBACK_LOCALE = "en";

export function _td(text: string): string {
Expand Down Expand Up @@ -63,7 +61,7 @@

type Component = () => void;

type TypedStore = Store<{ locale?: string | string[] }>;
type TypedStore = (typeof global)["store"];

export class AppLocalization {
private static readonly STORE_KEY = "locale";
Expand All @@ -81,7 +79,7 @@
}

this.store = store;
if (this.store.has(AppLocalization.STORE_KEY)) {

Check failure on line 82 in src/language-helper.ts

View workflow job for this annotation

GitHub Actions / Typescript Syntax Check

Property 'has' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 82 in src/language-helper.ts

View workflow job for this annotation

GitHub Actions / macOS / build

Property 'has' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 82 in src/language-helper.ts

View workflow job for this annotation

GitHub Actions / Windows (x64) / build

Property 'has' does not exist on type 'ElectronStore<StoreData>'.

Check failure on line 82 in src/language-helper.ts

View workflow job for this annotation

GitHub Actions / Windows (x86) / build

Property 'has' does not exist on type 'ElectronStore<StoreData>'.
const locales = this.store.get(AppLocalization.STORE_KEY);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.setAppLocale(locales!);
Expand Down
106 changes: 106 additions & 0 deletions src/safe-storage.ts
Original file line number Diff line number Diff line change
@@ -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;
}
24 changes: 11 additions & 13 deletions src/seshat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
Loading
Loading