Skip to content

Commit 0ae998a

Browse files
committed
Move clipboard decryption to main process.
Signed-off-by: Anders Kaseorg <[email protected]>
1 parent 447dd18 commit 0ae998a

File tree

3 files changed

+48
-29
lines changed

3 files changed

+48
-29
lines changed

app/common/typed-ipc.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type MainMessage = {
88
"focus-app": () => void;
99
"focus-this-webview": () => void;
1010
"get-injected-js": () => string;
11+
"new-clipboard-key": () => {key: Uint8Array; sig: Uint8Array};
1112
"permission-callback": (permissionCallbackId: number, grant: boolean) => void;
1213
"quit-app": () => void;
1314
"realm-icon-changed": (serverURL: string, iconURL: string) => void;
@@ -28,6 +29,7 @@ export type MainMessage = {
2829
export type MainCall = {
2930
"get-server-settings": (domain: string) => ServerConf;
3031
"is-online": (url: string) => boolean;
32+
"poll-clipboard": (key: Uint8Array, sig: Uint8Array) => string | undefined;
3133
"save-server-icon": (iconURL: string) => string;
3234
};
3335

app/main/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import {clipboard} from "electron/common";
12
import type {IpcMainEvent, WebContents} from "electron/main";
23
import {BrowserWindow, app, dialog, powerMonitor, session} from "electron/main";
4+
import {Buffer} from "node:buffer";
5+
import crypto from "node:crypto";
36
import fs from "node:fs";
47
import path from "node:path";
58
import process from "node:process";
@@ -210,6 +213,42 @@ function createMainWindow(): BrowserWindow {
210213
);
211214
});
212215

216+
const clipboardSigKey = crypto.randomBytes(32);
217+
218+
ipcMain.on("new-clipboard-key", (event) => {
219+
const key = crypto.randomBytes(32);
220+
const hmac = crypto.createHmac("sha256", clipboardSigKey);
221+
hmac.update(key);
222+
event.returnValue = {key, sig: hmac.digest()};
223+
});
224+
225+
ipcMain.handle("poll-clipboard", (event, key, sig) => {
226+
// Check that the key was generated here.
227+
const hmac = crypto.createHmac("sha256", clipboardSigKey);
228+
hmac.update(key);
229+
if (!crypto.timingSafeEqual(sig, hmac.digest())) return;
230+
231+
try {
232+
// Check that the data on the clipboard was encrypted to the key.
233+
const data = Buffer.from(clipboard.readText(), "hex");
234+
const iv = data.slice(0, 12);
235+
const ciphertext = data.slice(12, -16);
236+
const authTag = data.slice(-16);
237+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv, {
238+
authTagLength: 16,
239+
});
240+
decipher.setAuthTag(authTag);
241+
return (
242+
decipher.update(ciphertext, undefined, "utf8") + decipher.final("utf8")
243+
);
244+
} catch {
245+
// If the parsing or decryption failed in any way,
246+
// the correct token hasn’t been copied yet; try
247+
// again next time.
248+
return undefined;
249+
}
250+
});
251+
213252
AppMenu.setMenu({
214253
tabs: [],
215254
});

app/renderer/js/clipboard-decrypter.ts

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import {clipboard} from "electron/common";
2-
import {Buffer} from "node:buffer";
3-
import crypto from "node:crypto";
1+
import {ipcRenderer} from "./typed-ipc-renderer.js";
42

53
// This helper is exposed via electron_bridge for use in the social
64
// login flow.
@@ -30,15 +28,16 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
3028
constructor(_: number) {
3129
// At this time, the only version is 1.
3230
this.version = 1;
33-
this.key = crypto.randomBytes(32);
31+
const {key, sig} = ipcRenderer.sendSync("new-clipboard-key");
32+
this.key = key;
3433
this.pasted = new Promise((resolve) => {
3534
let interval: NodeJS.Timeout | null = null;
3635
const startPolling = () => {
3736
if (interval === null) {
3837
interval = setInterval(poll, 1000);
3938
}
4039

41-
poll();
40+
void poll();
4241
};
4342

4443
const stopPolling = () => {
@@ -48,30 +47,9 @@ export class ClipboardDecrypterImpl implements ClipboardDecrypter {
4847
}
4948
};
5049

51-
const poll = () => {
52-
let plaintext;
53-
54-
try {
55-
const data = Buffer.from(clipboard.readText(), "hex");
56-
const iv = data.slice(0, 12);
57-
const ciphertext = data.slice(12, -16);
58-
const authTag = data.slice(-16);
59-
const decipher = crypto.createDecipheriv(
60-
"aes-256-gcm",
61-
this.key,
62-
iv,
63-
{authTagLength: 16},
64-
);
65-
decipher.setAuthTag(authTag);
66-
plaintext =
67-
decipher.update(ciphertext, undefined, "utf8") +
68-
decipher.final("utf8");
69-
} catch {
70-
// If the parsing or decryption failed in any way,
71-
// the correct token hasn’t been copied yet; try
72-
// again next time.
73-
return;
74-
}
50+
const poll = async () => {
51+
const plaintext = await ipcRenderer.invoke("poll-clipboard", key, sig);
52+
if (plaintext === undefined) return;
7553

7654
window.removeEventListener("focus", startPolling);
7755
window.removeEventListener("blur", stopPolling);

0 commit comments

Comments
 (0)