mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-23 03:31:56 +00:00
Move clipboard decryption to main process.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
committed by
Anders Kaseorg
parent
447dd18b8b
commit
0ae998a51e
@@ -8,6 +8,7 @@ export type MainMessage = {
|
|||||||
"focus-app": () => void;
|
"focus-app": () => void;
|
||||||
"focus-this-webview": () => void;
|
"focus-this-webview": () => void;
|
||||||
"get-injected-js": () => string;
|
"get-injected-js": () => string;
|
||||||
|
"new-clipboard-key": () => {key: Uint8Array; sig: Uint8Array};
|
||||||
"permission-callback": (permissionCallbackId: number, grant: boolean) => void;
|
"permission-callback": (permissionCallbackId: number, grant: boolean) => void;
|
||||||
"quit-app": () => void;
|
"quit-app": () => void;
|
||||||
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
|
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
|
||||||
@@ -28,6 +29,7 @@ export type MainMessage = {
|
|||||||
export type MainCall = {
|
export type MainCall = {
|
||||||
"get-server-settings": (domain: string) => ServerConf;
|
"get-server-settings": (domain: string) => ServerConf;
|
||||||
"is-online": (url: string) => boolean;
|
"is-online": (url: string) => boolean;
|
||||||
|
"poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined;
|
||||||
"save-server-icon": (iconURL: string) => string;
|
"save-server-icon": (iconURL: string) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,5 +1,8 @@
|
|||||||
|
import {clipboard} from "electron/common";
|
||||||
import type {IpcMainEvent, WebContents} from "electron/main";
|
import type {IpcMainEvent, WebContents} from "electron/main";
|
||||||
import {BrowserWindow, app, dialog, powerMonitor, session} from "electron/main";
|
import {BrowserWindow, app, dialog, powerMonitor, session} from "electron/main";
|
||||||
|
import {Buffer} from "node:buffer";
|
||||||
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import process from "node:process";
|
import process from "node:process";
|
||||||
@@ -210,6 +213,42 @@ function createMainWindow(): BrowserWindow {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const clipboardSigKey = crypto.randomBytes(32);
|
||||||
|
|
||||||
|
ipcMain.on("new-clipboard-key", (event) => {
|
||||||
|
const key = crypto.randomBytes(32);
|
||||||
|
const hmac = crypto.createHmac("sha256", clipboardSigKey);
|
||||||
|
hmac.update(key);
|
||||||
|
event.returnValue = {key, sig: hmac.digest()};
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("poll-clipboard", (event, key, sig) => {
|
||||||
|
// Check that the key was generated here.
|
||||||
|
const hmac = crypto.createHmac("sha256", clipboardSigKey);
|
||||||
|
hmac.update(key);
|
||||||
|
if (!crypto.timingSafeEqual(sig, hmac.digest())) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check that the data on the clipboard was encrypted to the key.
|
||||||
|
const data = Buffer.from(clipboard.readText(), "hex");
|
||||||
|
const iv = data.slice(0, 12);
|
||||||
|
const ciphertext = data.slice(12, -16);
|
||||||
|
const authTag = data.slice(-16);
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {
|
||||||
|
authTagLength: 16,
|
||||||
|
});
|
||||||
|
decipher.setAuthTag(authTag);
|
||||||
|
return (
|
||||||
|
decipher.update(ciphertext, undefined, "utf8") + decipher.final("utf8")
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// If the parsing or decryption failed in any way,
|
||||||
|
// the correct token hasn’t been copied yet; try
|
||||||
|
// again next time.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
AppMenu.setMenu({
|
AppMenu.setMenu({
|
||||||
tabs: [],
|
tabs: [],
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
import {clipboard} from "electron/common";
|
import {ipcRenderer} from "./typed-ipc-renderer.js";
|
||||||
import {Buffer} from "node:buffer";
|
|
||||||
import crypto from "node:crypto";
|
|
||||||
|
|
||||||
// This helper is exposed via electron_bridge for use in the social
|
// This helper is exposed via electron_bridge for use in the social
|
||||||
// login flow.
|
// login flow.
|
||||||
@@ -30,7 +28,8 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
|
|||||||
constructor(_: number) {
|
constructor(_: number) {
|
||||||
// At this time, the only version is 1.
|
// At this time, the only version is 1.
|
||||||
this.version = 1;
|
this.version = 1;
|
||||||
this.key = crypto.randomBytes(32);
|
const {key, sig} = ipcRenderer.sendSync("new-clipboard-key");
|
||||||
|
this.key = key;
|
||||||
this.pasted = new Promise((resolve) => {
|
this.pasted = new Promise((resolve) => {
|
||||||
let interval: NodeJS.Timeout | null = null;
|
let interval: NodeJS.Timeout | null = null;
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
@@ -38,7 +37,7 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
|
|||||||
interval = setInterval(poll, 1000);
|
interval = setInterval(poll, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
poll();
|
void poll();
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
@@ -48,30 +47,9 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const poll = () => {
|
const poll = async () => {
|
||||||
let plaintext;
|
const plaintext = await ipcRenderer.invoke("poll-clipboard", key, sig);
|
||||||
|
if (plaintext === undefined) return;
|
||||||
try {
|
|
||||||
const data = Buffer.from(clipboard.readText(), "hex");
|
|
||||||
const iv = data.slice(0, 12);
|
|
||||||
const ciphertext = data.slice(12, -16);
|
|
||||||
const authTag = data.slice(-16);
|
|
||||||
const decipher = crypto.createDecipheriv(
|
|
||||||
"aes-256-gcm",
|
|
||||||
this.key,
|
|
||||||
iv,
|
|
||||||
{authTagLength: 16},
|
|
||||||
);
|
|
||||||
decipher.setAuthTag(authTag);
|
|
||||||
plaintext =
|
|
||||||
decipher.update(ciphertext, undefined, "utf8") +
|
|
||||||
decipher.final("utf8");
|
|
||||||
} catch {
|
|
||||||
// If the parsing or decryption failed in any way,
|
|
||||||
// the correct token hasn’t been copied yet; try
|
|
||||||
// again next time.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.removeEventListener("focus", startPolling);
|
window.removeEventListener("focus", startPolling);
|
||||||
window.removeEventListener("blur", stopPolling);
|
window.removeEventListener("blur", stopPolling);
|
||||||
|
Reference in New Issue
Block a user