Removed node-forge, encryptcord is now fully self contained

This commit is contained in:
Inbestigator 2024-03-02 02:41:04 -08:00
parent 3275c01a8b
commit 92287026da
4 changed files with 163 additions and 73 deletions

View file

@ -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": {

8
pnpm-lock.yaml generated
View file

@ -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:

View file

@ -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

View file

@ -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;
}
};