mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-24 08:33:36 +00:00
89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
import crypto from "crypto";
|
||
import {clipboard} from "electron";
|
||
|
||
// This helper is exposed via electron_bridge for use in the social
|
||
// login flow.
|
||
//
|
||
// It consists of a key and a promised token. The in-app page sends
|
||
// the key to the server, and opens the user’s browser to a page where
|
||
// they can log in and get a token encrypted to that key. When the
|
||
// user copies the encrypted token from their browser to the
|
||
// clipboard, we decrypt it and resolve the promise. The in-app page
|
||
// then uses the decrypted token to log the user in within the app.
|
||
//
|
||
// The encryption is authenticated (AES-GCM) to guarantee that we
|
||
// don’t leak anything from the user’s clipboard other than the token
|
||
// intended for us.
|
||
|
||
export interface ClipboardDecrypter {
|
||
version: number;
|
||
key: Uint8Array;
|
||
pasted: Promise<string>;
|
||
}
|
||
|
||
export class ClipboardDecrypterImpl implements ClipboardDecrypter {
|
||
version: number;
|
||
key: Uint8Array;
|
||
pasted: Promise<string>;
|
||
|
||
constructor(_: number) {
|
||
// At this time, the only version is 1.
|
||
this.version = 1;
|
||
this.key = crypto.randomBytes(32);
|
||
this.pasted = new Promise((resolve) => {
|
||
let interval: NodeJS.Timeout | null = null;
|
||
const startPolling = () => {
|
||
if (interval === null) {
|
||
interval = setInterval(poll, 1000);
|
||
}
|
||
|
||
poll();
|
||
};
|
||
|
||
const stopPolling = () => {
|
||
if (interval !== null) {
|
||
clearInterval(interval);
|
||
interval = null;
|
||
}
|
||
};
|
||
|
||
const poll = () => {
|
||
let plaintext;
|
||
|
||
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("blur", stopPolling);
|
||
stopPolling();
|
||
resolve(plaintext);
|
||
};
|
||
|
||
window.addEventListener("focus", startPolling);
|
||
window.addEventListener("blur", stopPolling);
|
||
if (document.hasFocus()) {
|
||
startPolling();
|
||
}
|
||
});
|
||
}
|
||
}
|