From 92287026daf988a8fb2af58d91ba68c492ca6e6f Mon Sep 17 00:00:00 2001 From: Inbestigator Date: Sat, 2 Mar 2024 02:41:04 -0800 Subject: [PATCH] Removed node-forge, encryptcord is now fully self contained --- package.json | 1 - pnpm-lock.yaml | 8 -- src/plugins/encryptcord/index.tsx | 111 +++++++++++------------- src/plugins/encryptcord/rsa-utils.tsx | 116 ++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 73 deletions(-) create mode 100644 src/plugins/encryptcord/rsa-utils.tsx diff --git a/package.json b/package.json index 908a87d62..91e596358 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", "monaco-editor": "^0.43.0", "nanoid": "^4.0.2", - "node-forge": "^1.3.1", "virtual-merge": "^1.0.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e407334c..55799a516 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,9 +37,6 @@ dependencies: nanoid: specifier: ^4.0.2 version: 4.0.2 - node-forge: - specifier: ^1.3.1 - version: 1.3.1 virtual-merge: specifier: ^1.0.1 version: 1.0.1 @@ -2483,11 +2480,6 @@ packages: whatwg-url: 5.0.0 dev: true - /node-forge@1.3.1: - resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} - engines: {node: '>= 6.13.0'} - dev: false - /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: diff --git a/src/plugins/encryptcord/index.tsx b/src/plugins/encryptcord/index.tsx index 8d71130b9..d60f7d0ca 100644 --- a/src/plugins/encryptcord/index.tsx +++ b/src/plugins/encryptcord/index.tsx @@ -1,11 +1,13 @@ import { addChatBarButton, ChatBarButton } from "@api/ChatButtons"; import { removeButton } from "@api/MessagePopover"; -import definePlugin, { StartAt } from "@utils/types"; +import { addDecoration } from "@api/MessageDecorations"; +import definePlugin from "@utils/types"; import * as DataStore from "@api/DataStore"; import { sleep } from "@utils/misc"; import { findByPropsLazy } from "@webpack"; import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents"; import { useEffect, useState, FluxDispatcher } from "@webpack/common"; +import { generateKeys, encryptData, decryptData } from "./rsa-utils"; import { Devs } from "@utils/constants"; import { RestAPI, @@ -38,56 +40,6 @@ interface IMessageCreate { message: Message; } -// Generate RSA key pair -function generateKeyPair(): { privateKey: string; publicKey: string; } { - const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 }); - const privateKey = forge.pki.privateKeyToPem(keys.privateKey); - const publicKey = forge.pki.publicKeyToPem(keys.publicKey); - - return { privateKey, publicKey }; -} - -// Encrypt message with public key -function encrypt(message: string, publicKey): string[] { - try { - const publicKeyObj = forge.pki.publicKeyFromPem(publicKey); - const chunkSize = 190; - - const emojiRegex = /(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])/g; - message = message.replace(emojiRegex, ''); - - const encryptedChunks: string[] = []; - - for (let i = 0; i < message.length; i += chunkSize) { - const chunk = message.substring(i, i + chunkSize); - const encryptedChunk = publicKeyObj.encrypt(chunk, 'RSA-OAEP', { - md: forge.md.sha256.create(), - }); - encryptedChunks.push(forge.util.encode64(encryptedChunk)); - } - - return encryptedChunks; - } catch (error) { - return []; - } -} - -// Decrypt message with private key -function decrypt(encryptedMessages: string[], privateKey): string { - const privateKeyObj = forge.pki.privateKeyFromPem(privateKey); - let decryptedMessages: string[] = []; - - encryptedMessages.forEach((encryptedMessage) => { - const encrypted = forge.util.decode64(encryptedMessage); - const decrypted = privateKeyObj.decrypt(encrypted, 'RSA-OAEP', { - md: forge.md.sha256.create(), - }); - decryptedMessages.push(decrypted); - }); - - return decryptedMessages.join(''); -} - // Chat Bar Icon Component const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { [enabled, setEnabled] = useState(false); @@ -107,7 +59,7 @@ const ChatBarIcon: ChatBarButton = ({ isMainChat }) => { const dmPromises = Object.keys(encryptcordGroupMembers).map(async (memberId) => { const groupMember = await UserUtils.getUser(memberId).catch(() => null); if (!groupMember) return; - const encryptedMessage = encrypt(trimmedMessage, encryptcordGroupMembers[memberId]); + const encryptedMessage = await encryptData(encryptcordGroupMembers[memberId], trimmedMessage); const encryptedMessageString = JSON.stringify(encryptedMessage); await sendTempMessage(groupMember.id, encryptedMessageString, `message`); }); @@ -185,14 +137,29 @@ export default definePlugin({ async joinGroup(interaction) { const sender = await UserUtils.getUser(interaction.application_id).catch(() => null); if (!sender || sender.bot == true) return; - if (interaction.data.component_type != 2 || interaction.data.custom_id != "acceptGroup") return; - await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); - FluxDispatcher.dispatch({ - type: "MESSAGE_DELETE", - channelId: interaction.channel_id, - id: interaction.message_id, - mlDeleted: true - }); + if (interaction.data.component_type != 2) return; + switch (interaction.data.custom_id) { + case "acceptGroup": + await sendTempMessage(interaction.application_id, `${await DataStore.get("encryptcordPublicKey")}`, "join"); + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: interaction.channel_id, + id: interaction.message_id, + mlDeleted: true + }); + break; + case "removeFromGroup": + await handleLeaving(sender.id, await DataStore.get("encryptcordGroupMembers") ?? {}, interaction.channel_id); + FluxDispatcher.dispatch({ + type: "MESSAGE_DELETE", + channelId: interaction.channel_id, + id: interaction.message_id, + mlDeleted: true + }); + break; + default: + return; + } }, flux: { async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) { @@ -232,7 +199,6 @@ export default definePlugin({ const sender = await UserUtils.getUser(message.author.id).catch(() => null); if (!sender) return; const response = await fetch(message.attachments[0].url); - console.log(response); const userKey = await response.text(); await handleJoin(sender.id, userKey, encryptcordGroupMembers); return; @@ -315,7 +281,7 @@ export default definePlugin({ ], async start() { addChatBarButton("Encryptcord", ChatBarIcon); - const pair = generateKeyPair(); + const pair = await generateKeys(); await DataStore.set('encryptcordPublicKey', pair.publicKey); await DataStore.set('encryptcordPrivateKey', pair.privateKey); if (await DataStore.get("encryptcordGroup") == true) { @@ -402,7 +368,7 @@ async function handleLeaving(senderId: string, encryptcordGroupMembers: object, // Handle receiving message async function handleMessage(message, senderId: string, groupChannel: string) { - const decryptedMessage = decrypt(message, await DataStore.get("encryptcordPrivateKey")); + const decryptedMessage = await decryptData(await DataStore.get("encryptcordPrivateKey"), message); await MessageActions.receiveMessage(groupChannel, await createMessage(decryptedMessage, senderId, groupChannel, 0)); } @@ -442,7 +408,24 @@ async function handleJoin(senderId: string, senderKey: string, encryptcordGroupM }); await Promise.all(dmPromises); - await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 7)); + await MessageActions.receiveMessage(groupChannel, { + ...await createMessage("", senderId, groupChannel, 7), components: [{ + type: 1, + components: [{ + type: 2, + style: 4, + label: 'I don\'t want to talk to you!', + custom_id: 'removeFromGroup' + }, + { + type: 2, + style: 2, + label: '(Other users can still send/receive messages to/from them)', + disabled: true, + custom_id: 'encryptcord' + }] + }] + }); } // Create message for group diff --git a/src/plugins/encryptcord/rsa-utils.tsx b/src/plugins/encryptcord/rsa-utils.tsx new file mode 100644 index 000000000..9d06256ba --- /dev/null +++ b/src/plugins/encryptcord/rsa-utils.tsx @@ -0,0 +1,116 @@ +export const generateKeys = async () => { + const keyPair = await crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + }, + true, + ["encrypt", "decrypt"] + ); + + const exportedPublicKey = await crypto.subtle.exportKey("spki", keyPair.publicKey); + const publicKey = formatPemKey(exportedPublicKey); + + return { privateKey: keyPair.privateKey, publicKey }; +}; + +export const encryptData = async (pemPublicKey, data) => { + const publicKey = await importPemPublicKey(pemPublicKey); + + const chunkSize = 190; + + const encryptedChunks: any[] = []; + const encoder = new TextEncoder(); + + for (let i = 0; i < data.length; i += chunkSize) { + const chunk = await data.substring(i, i + chunkSize); + const encryptedChunk = await crypto.subtle.encrypt( + { + name: "RSA-OAEP", + }, + publicKey, + encoder.encode(chunk) + ); + encryptedChunks.push(arrayBufferToBase64(encryptedChunk)); + } + + return encryptedChunks; +}; + +export const decryptData = async (privateKey, encArray) => { + const decryptionPromises = encArray.map(async (encStr) => { + const encBuffer = base64ToArrayBuffer(encStr); + + const dec = await crypto.subtle.decrypt( + { + name: "RSA-OAEP", + }, + privateKey, + encBuffer + ); + + return new TextDecoder().decode(dec); + }); + + const decryptedMessages = await Promise.all(decryptionPromises); + + return decryptedMessages.join(''); +}; + +// Helper functions +const arrayBufferToBase64 = (buffer) => { + const binary = String.fromCharCode(...new Uint8Array(buffer)); + return btoa(binary); +}; + +const base64ToArrayBuffer = (base64String) => { + const binaryString = atob(base64String); + const length = binaryString.length; + const buffer = new ArrayBuffer(length); + const view = new Uint8Array(buffer); + + for (let i = 0; i < length; i++) { + view[i] = binaryString.charCodeAt(i); + } + + return buffer; +}; + +const formatPemKey = (keyData) => { + const base64Key = arrayBufferToBase64(keyData); + return `-----BEGIN PUBLIC KEY-----\n` + base64Key + `\n----- END PUBLIC KEY----- `; +}; + +const importPemPublicKey = async (pemKey) => { + try { + const trimmedPemKey = pemKey.trim(); + + const keyBody = trimmedPemKey + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("----- END PUBLIC KEY-----", ""); + + const binaryDer = atob(keyBody); + + const arrayBuffer = new Uint8Array(binaryDer.length); + for (let i = 0; i < binaryDer.length; i++) { + arrayBuffer[i] = binaryDer.charCodeAt(i); + } + + return await crypto.subtle.importKey( + "spki", + arrayBuffer, + { + name: "RSA-OAEP", + hash: { name: "SHA-256" }, + }, + true, + ["encrypt"] + ); + } catch (error) { + console.error("Error importing PEM public key:", error); + throw error; + } +}; +