mirror of
https://github.com/element-hq/element-desktop
synced 2025-04-21 09:03:56 +02:00
Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
1953ddb436
commit
68c42fccdd
@ -67,7 +67,7 @@ export const test = base.extend<Fixtures>({
|
|||||||
await fs.rm(tmpDir, { recursive: true });
|
await fs.rm(tmpDir, { recursive: true });
|
||||||
},
|
},
|
||||||
app: async ({ tmpDir, extraEnv, extraArgs, stdout, stderr }, use) => {
|
app: async ({ tmpDir, extraEnv, extraArgs, stdout, stderr }, use) => {
|
||||||
const args = ["--profile-dir", tmpDir];
|
const args = ["--profile-dir", tmpDir, "--allow-plaintext-storage"];
|
||||||
|
|
||||||
const executablePath = process.env["ELEMENT_DESKTOP_EXECUTABLE"];
|
const executablePath = process.env["ELEMENT_DESKTOP_EXECUTABLE"];
|
||||||
if (!executablePath) {
|
if (!executablePath) {
|
||||||
|
@ -364,7 +364,7 @@ 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
|
// 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");
|
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");
|
||||||
|
|
||||||
store.prepare(); // must be called before any async actions
|
store.prepare(argv["allow-plaintext-storage"] === true); // must be called before any async actions
|
||||||
|
|
||||||
// Disable hardware acceleration if the setting has been set.
|
// Disable hardware acceleration if the setting has been set.
|
||||||
if (store.get("disableHardwareAcceleration") === true) {
|
if (store.get("disableHardwareAcceleration") === true) {
|
||||||
|
134
src/store.ts
134
src/store.ts
@ -55,12 +55,7 @@ async function clearDataAndRelaunch(): Promise<void> {
|
|||||||
relaunchApp();
|
relaunchApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
interface StoreData {
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
class Store extends ElectronStore<{
|
|
||||||
warnBeforeExit: boolean;
|
warnBeforeExit: boolean;
|
||||||
minimizeToTray: boolean;
|
minimizeToTray: boolean;
|
||||||
spellCheckerEnabled: boolean;
|
spellCheckerEnabled: boolean;
|
||||||
@ -74,7 +69,50 @@ class Store extends ElectronStore<{
|
|||||||
safeStorageBackendOverride?: boolean;
|
safeStorageBackendOverride?: boolean;
|
||||||
// whether to perform a migration of the safeStorage data
|
// whether to perform a migration of the safeStorage data
|
||||||
safeStorageBackendMigrate?: boolean;
|
safeStorageBackendMigrate?: boolean;
|
||||||
}> {
|
}
|
||||||
|
|
||||||
|
class PlaintextStorageWriter {
|
||||||
|
public constructor(private readonly store: ElectronStore<StoreData>) {}
|
||||||
|
|
||||||
|
protected getSecretStorageKey = (key: string) => `safeStorage.${key.replaceAll(".", "-")}` as const;
|
||||||
|
|
||||||
|
public set(key: string, secret: string): void {
|
||||||
|
this.store.set(this.getSecretStorageKey(key), secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: string): string | null {
|
||||||
|
return this.store.get(this.getSecretStorageKey(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public delete(key: string): void {
|
||||||
|
this.store.delete(this.getSecretStorageKey(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SafeStorageWriter extends PlaintextStorageWriter {
|
||||||
|
public set(key: string, secret: string): void {
|
||||||
|
this.set(this.getSecretStorageKey(key), safeStorage.encryptString(secret).toString("base64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(key: string): string | null {
|
||||||
|
const ciphertext = this.get(this.getSecretStorageKey(key));
|
||||||
|
if (typeof ciphertext === "string") {
|
||||||
|
return safeStorage.decryptString(Buffer.from(ciphertext, "base64"));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
class Store extends ElectronStore<StoreData> {
|
||||||
|
// Provides "raw" access to the underlying secrets storage,
|
||||||
|
// should be avoided in favour of the getSecret/setSecret/deleteSecret methods.
|
||||||
|
private secrets: PlaintextStorageWriter | SafeStorageWriter;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super({
|
super({
|
||||||
name: "electron-config",
|
name: "electron-config",
|
||||||
@ -117,13 +155,20 @@ class Store extends ElectronStore<{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// May be upgraded to a SafeStorageWriter later in prepareSafeStorage
|
||||||
|
this.secrets = new PlaintextStorageWriter(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private allowPlaintextStorage = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare the store, does not prepare safeStorage, which needs to be done after the app is ready.
|
* Prepare the store, does not prepare safeStorage, which needs to be done after the app is ready.
|
||||||
* Must be executed in the first tick of the event loop so that it can call Electron APIs before ready state.
|
* Must be executed in the first tick of the event loop so that it can call Electron APIs before ready state.
|
||||||
*/
|
*/
|
||||||
public prepare(): void {
|
public prepare(allowPlaintextStorage = false): void {
|
||||||
|
this.allowPlaintextStorage = allowPlaintextStorage;
|
||||||
|
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
const backend = this.get("safeStorageBackend")!;
|
const backend = this.get("safeStorageBackend")!;
|
||||||
if (backend in safeStorageBackendMap) {
|
if (backend in safeStorageBackendMap) {
|
||||||
@ -143,8 +188,6 @@ class Store extends ElectronStore<{
|
|||||||
await this.safeStorageReadyPromise;
|
await this.safeStorageReadyPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSecretStorageKey = (key: string) => `safeStorage.${key.replaceAll(".", "-")}` as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare the safeStorage backend for use.
|
* Prepare the safeStorage backend for use.
|
||||||
*/
|
*/
|
||||||
@ -169,11 +212,11 @@ class Store extends ElectronStore<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.get("safeStorageBackendMigrate")) {
|
if (this.get("safeStorageBackendMigrate")) {
|
||||||
return this.migratePhase2();
|
return this.upgradeLinuxBackend2();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!safeStorageBackend) {
|
if (!safeStorageBackend) {
|
||||||
if (selectedSafeStorageBackend === "basic_text") {
|
if (selectedSafeStorageBackend === "basic_text" && !this.allowPlaintextStorage) {
|
||||||
// Ask the user if they want to use plain text encryption
|
// Ask the user if they want to use plain text encryption
|
||||||
// TODO should we only do this if they have existing data
|
// TODO should we only do this if they have existing data
|
||||||
const { response } = await dialog.showMessageBox({
|
const { response } = await dialog.showMessageBox({
|
||||||
@ -189,6 +232,7 @@ class Store extends ElectronStore<{
|
|||||||
if (response === 0) {
|
if (response === 0) {
|
||||||
throw new Error("safeStorage backend basic_text and user rejected it");
|
throw new Error("safeStorage backend basic_text and user rejected it");
|
||||||
}
|
}
|
||||||
|
this.allowPlaintextStorage = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the backend used for the safeStorage data so we can detect if it changes
|
// Store the backend used for the safeStorage data so we can detect if it changes
|
||||||
@ -198,18 +242,30 @@ class Store extends ElectronStore<{
|
|||||||
console.warn(`safeStorage backend changed from ${safeStorageBackend} to ${selectedSafeStorageBackend}`);
|
console.warn(`safeStorage backend changed from ${safeStorageBackend} to ${selectedSafeStorageBackend}`);
|
||||||
|
|
||||||
if (safeStorageBackend === "plaintext") {
|
if (safeStorageBackend === "plaintext") {
|
||||||
this.migratePhase3();
|
this.upgradeLinuxBackend3();
|
||||||
} else if (safeStorageBackend === "basic_text") {
|
} else if (safeStorageBackend === "basic_text") {
|
||||||
return this.migratePhase1();
|
return this.upgradeLinuxBackend1();
|
||||||
} else if (safeStorageBackend in safeStorageBackendMap) {
|
} else if (safeStorageBackend in safeStorageBackendMap) {
|
||||||
this.set("safeStorageBackendOverride", true);
|
this.set("safeStorageBackendOverride", true);
|
||||||
relaunchApp();
|
relaunchApp();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Warn the user that the backend has changed and tell them that we cannot migrate
|
// Warn the user that the backend has changed and tell them that we cannot migrate
|
||||||
// dialog.showErrorBox(_t(""), _t("")); TODO
|
const { response } = await dialog.showMessageBox({
|
||||||
|
// TODO
|
||||||
|
title: "Error 2",
|
||||||
|
message: "Message",
|
||||||
|
// detail: _t(""),
|
||||||
|
type: "question",
|
||||||
|
buttons: [_t("common|no"), _t("common|yes")],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 0,
|
||||||
|
});
|
||||||
|
if (response === 0) {
|
||||||
throw new Error("safeStorage backend changed and cannot migrate");
|
throw new Error("safeStorage backend changed and cannot migrate");
|
||||||
}
|
}
|
||||||
|
await clearDataAndRelaunch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (safeStorageBackend === "basic_text" && selectedSafeStorageBackend === safeStorageBackend) {
|
if (safeStorageBackend === "basic_text" && selectedSafeStorageBackend === safeStorageBackend) {
|
||||||
@ -218,35 +274,29 @@ class Store extends ElectronStore<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!safeStorage.isEncryptionAvailable()) {
|
if (safeStorage.isEncryptionAvailable()) {
|
||||||
|
this.secrets = new SafeStorageWriter(this);
|
||||||
|
} else if (!this.allowPlaintextStorage) {
|
||||||
console.error("Store migration: safeStorage is not available");
|
console.error("Store migration: safeStorage is not available");
|
||||||
throw new Error(`safeStorage is not available`);
|
throw new Error(`safeStorage is not available`);
|
||||||
// TODO fatal error?
|
// TODO fatal error?
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.migrateSecrets();
|
await this.importKeytarSecrets();
|
||||||
}
|
}
|
||||||
|
|
||||||
private recordSafeStorageBackend(backend: SafeStorageBackend): void {
|
private recordSafeStorageBackend(backend: SafeStorageBackend): void {
|
||||||
this.set("safeStorageBackend", backend);
|
this.set("safeStorageBackend", backend);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get isPlaintext(): boolean {
|
|
||||||
return this.get("safeStorageBackend") === "basic_text";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrates keytar data to safeStorage,
|
* Migrates keytar data to safeStorage,
|
||||||
* deletes data from legacy keytar but keeps it in the new keytar for downgrade compatibility.
|
* deletes data from legacy keytar but keeps it in the new keytar for downgrade compatibility.
|
||||||
*/
|
*/
|
||||||
private async migrateSecrets(): Promise<void> {
|
private async importKeytarSecrets(): Promise<void> {
|
||||||
if (this.has("safeStorage")) return; // already migrated
|
if (this.has("safeStorage")) return; // already migrated
|
||||||
console.info("Store migration: started");
|
console.info("Store migration: started");
|
||||||
|
|
||||||
if (process.platform === "linux" && this.isPlaintext) {
|
|
||||||
console.warn("Store migration: safeStorage is using basic text encryption");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const credentials = [
|
const credentials = [
|
||||||
...(await keytar.findCredentials(LEGACY_KEYTAR_SERVICE)),
|
...(await keytar.findCredentials(LEGACY_KEYTAR_SERVICE)),
|
||||||
@ -254,7 +304,7 @@ class Store extends ElectronStore<{
|
|||||||
];
|
];
|
||||||
for (const cred of credentials) {
|
for (const cred of credentials) {
|
||||||
console.info("Store migration: writing", cred);
|
console.info("Store migration: writing", cred);
|
||||||
await this.setSecretSafeStorage(cred.account, cred.password);
|
this.secrets?.set(cred.account, cred.password);
|
||||||
console.info("Store migration: deleting", cred);
|
console.info("Store migration: deleting", cred);
|
||||||
await this.deleteSecretKeytar(LEGACY_KEYTAR_SERVICE, cred.account);
|
await this.deleteSecretKeytar(LEGACY_KEYTAR_SERVICE, cred.account);
|
||||||
}
|
}
|
||||||
@ -270,29 +320,29 @@ class Store extends ElectronStore<{
|
|||||||
* this is quite a tricky process as the backend is not known until the app is ready & cannot be changed once it is.
|
* this is quite a tricky process as the backend is not known until the app is ready & cannot be changed once it is.
|
||||||
* First we restart the app in basic_text backend mode, and decrypt the data, then restart back in default backend mode and re-encrypt the data.
|
* First we restart the app in basic_text backend mode, and decrypt the data, then restart back in default backend mode and re-encrypt the data.
|
||||||
*/
|
*/
|
||||||
private migratePhase1(): void {
|
private upgradeLinuxBackend1(): void {
|
||||||
console.info(`Starting safeStorage migration to ${safeStorage.getSelectedStorageBackend()}`);
|
console.info(`Starting safeStorage migration to ${safeStorage.getSelectedStorageBackend()}`);
|
||||||
this.set("safeStorageBackendMigrate", true);
|
this.set("safeStorageBackendMigrate", true);
|
||||||
relaunchApp();
|
relaunchApp();
|
||||||
}
|
}
|
||||||
private migratePhase2(): void {
|
private upgradeLinuxBackend2(): void {
|
||||||
console.info("Performing safeStorage migration");
|
console.info("Performing safeStorage migration");
|
||||||
const data = this.get("safeStorage");
|
const data = this.get("safeStorage");
|
||||||
if (data) {
|
if (data) {
|
||||||
for (const key in data) {
|
for (const key in data) {
|
||||||
this.set(this.getSecretStorageKey(key), this.getSecret(key));
|
this.set(key, this.secrets!.get(key));
|
||||||
}
|
}
|
||||||
this.set("safeStorageBackend", "plaintext");
|
this.set("safeStorageBackend", "plaintext");
|
||||||
}
|
}
|
||||||
this.set("safeStorageBackendMigrate", false);
|
this.set("safeStorageBackendMigrate", false);
|
||||||
relaunchApp();
|
relaunchApp();
|
||||||
}
|
}
|
||||||
private migratePhase3(): void {
|
private upgradeLinuxBackend3(): void {
|
||||||
console.info(`Finishing safeStorage migration to ${safeStorage.getSelectedStorageBackend()}`);
|
console.info(`Finishing safeStorage migration to ${safeStorage.getSelectedStorageBackend()}`);
|
||||||
const data = this.get("safeStorage");
|
const data = this.get("safeStorage");
|
||||||
if (data) {
|
if (data) {
|
||||||
for (const key in data) {
|
for (const key in data) {
|
||||||
this.setSecretSafeStorage(key, data[key]);
|
this.secrets.set(key, data[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,18 +357,15 @@ class Store extends ElectronStore<{
|
|||||||
*/
|
*/
|
||||||
public async getSecret(key: string): Promise<string | null> {
|
public async getSecret(key: string): Promise<string | null> {
|
||||||
await this.safeStorageReady();
|
await this.safeStorageReady();
|
||||||
if (!safeStorage.isEncryptionAvailable()) {
|
|
||||||
|
if (!safeStorage.isEncryptionAvailable() && !this.allowPlaintextStorage) {
|
||||||
return (
|
return (
|
||||||
(await keytar.getPassword(KEYTAR_SERVICE, key)) ??
|
(await keytar.getPassword(KEYTAR_SERVICE, key)) ??
|
||||||
(await keytar.getPassword(LEGACY_KEYTAR_SERVICE, key))
|
(await keytar.getPassword(LEGACY_KEYTAR_SERVICE, key))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedValue = this.get(this.getSecretStorageKey(key));
|
return this.secrets.get(key);
|
||||||
if (typeof encryptedValue === "string") {
|
|
||||||
return safeStorage.decryptString(Buffer.from(encryptedValue, "base64"));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -333,19 +380,14 @@ class Store extends ElectronStore<{
|
|||||||
*/
|
*/
|
||||||
public async setSecret(key: string, secret: string): Promise<void> {
|
public async setSecret(key: string, secret: string): Promise<void> {
|
||||||
await this.safeStorageReady();
|
await this.safeStorageReady();
|
||||||
if (!safeStorage.isEncryptionAvailable()) {
|
if (!safeStorage.isEncryptionAvailable() && !this.allowPlaintextStorage) {
|
||||||
throw new Error("safeStorage is not available");
|
throw new Error("safeStorage is not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setSecretSafeStorage(key, secret);
|
this.secrets.set(key, secret);
|
||||||
await keytar.setPassword(KEYTAR_SERVICE, key, secret);
|
await keytar.setPassword(KEYTAR_SERVICE, key, secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
private setSecretSafeStorage(key: string, secret: string): void {
|
|
||||||
const encryptedValue = safeStorage.encryptString(secret);
|
|
||||||
this.set(this.getSecretStorageKey(key), encryptedValue.toString("base64"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete the stored password for the key.
|
* Delete the stored password for the key.
|
||||||
* Removes from safeStorage, keytar & keytar legacy.
|
* Removes from safeStorage, keytar & keytar legacy.
|
||||||
@ -357,7 +399,7 @@ class Store extends ElectronStore<{
|
|||||||
|
|
||||||
await this.deleteSecretKeytar(LEGACY_KEYTAR_SERVICE, key);
|
await this.deleteSecretKeytar(LEGACY_KEYTAR_SERVICE, key);
|
||||||
await this.deleteSecretKeytar(KEYTAR_SERVICE, key);
|
await this.deleteSecretKeytar(KEYTAR_SERVICE, key);
|
||||||
this.delete(this.getSecretStorageKey(key));
|
this.secrets.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteSecretKeytar(namespace: string, key: string): Promise<void> {
|
private async deleteSecretKeytar(namespace: string, key: string): Promise<void> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user