Plugin
Encryptcord
End-to-end encryption in Discord!
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { addChatBarButton, ChatBarButton } from "@api/ChatButtons";8
import {9
ApplicationCommandInputType,10
ApplicationCommandOptionType,11
sendBotMessage,12
} from "@api/Commands";13
import * as DataStore from "@api/DataStore";14
import { addPreSendListener, removePreSendListener, SendListener } from "@api/MessageEvents";15
import { removeButton } from "@api/MessagePopover";16
import { Devs } from "@utils/constants";17
import { sleep } from "@utils/misc";18
import definePlugin from "@utils/types";19
import { findByPropsLazy } from "@webpack";20
import {21
FluxDispatcher, MessageActions,22
PrivateChannelsStore, RestAPI,23
SnowflakeUtils,24
useEffect, UserStore,25
UserUtils, useState,26
} from "@webpack/common";27
import { Message } from "discord-types/general";28
29
import { decryptData, encryptData, formatPemKey, generateKeys } from "./rsa-utils";30
const MessageCreator = findByPropsLazy("createBotMessage");31
const CloudUtils = findByPropsLazy("CloudUpload");32
import { getCurrentChannel } from "@utils/discord";33
34
let enabled;35
let setEnabled;36
37
// Interface for Message Create38
interface IMessageCreate {39
type: "MESSAGE_CREATE";40
optimistic: boolean;41
isPushNotification: boolean;42
channelId: string;43
message: Message;44
}45
46
// Chat Bar Icon Component47
const ChatBarIcon: ChatBarButton = ({ isMainChat }) => {48
[enabled, setEnabled] = useState(false);49
const [buttonDisabled, setButtonDisabled] = useState(false);50
51
useEffect(() => {52
const listener: SendListener = async (_, message) => {53
if (enabled) {54
const groupChannel = await DataStore.get("encryptcordChannelId");55
if (getCurrentChannel().id !== groupChannel) {56
sendBotMessage(getCurrentChannel().id, { content: `You must be in <#${groupChannel}> to send an encrypted message!\n> If you wish to send an unencrypted message, please click the button in the chatbar.` });57
message.content = "";58
return;59
}60
const trimmedMessage = message.content.trim();61
await MessageActions.receiveMessage(groupChannel, await createMessage(trimmedMessage, UserStore.getCurrentUser().id, groupChannel, 0));62
const encryptcordGroupMembers = await DataStore.get("encryptcordGroupMembers");63
const dmPromises = Object.keys(encryptcordGroupMembers).map(async memberId => {64
const groupMember = await UserUtils.getUser(memberId).catch(() => null);65
if (!groupMember) return;66
const encryptedMessage = await encryptData(encryptcordGroupMembers[memberId].key, trimmedMessage);67
const encryptedMessageString = JSON.stringify(encryptedMessage);68
await sendTempMessage(groupMember.id, encryptedMessageString, "message");69
});70
71
await Promise.all(dmPromises);72
message.content = "";73
}74
};75
76
addPreSendListener(listener);77
return () => void removePreSendListener(listener);78
}, [enabled]);79
80
if (!isMainChat) return null;81
82
return (83
<ChatBarButton84
tooltip={enabled ? "Send Unencrypted Messages" : "Send Encrypted Messages"}85
onClick={async () => {86
if (await DataStore.get("encryptcordGroup") === false || (await DataStore.get("encryptcordChannelId") !== getCurrentChannel().id)) {87
setButtonDisabled(true);88
await sendTempMessage(getCurrentChannel().id, "", `join\`\`\`\n${await DataStore.get("encryptcordPublicKey")}\`\`\``, false);89
sendBotMessage(getCurrentChannel().id, { content: `*Checking for any groups in this channel...*\n> If none is found, a new one will be created <t:${Math.floor(Date.now() / 1000) + 5}:R>\n> [Tip] You can do \`/encryptcord leave\` to leave a group` });90
await sleep(5000);91
if (await DataStore.get("encryptcordGroup") === true && (await DataStore.get("encryptcordChannelId") !== getCurrentChannel().id)) {92
sendBotMessage(getCurrentChannel().id, { content: "*Leaving current group...*" });93
await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } });94
} else if (await DataStore.get("encryptcordGroup") === true) {95
setButtonDisabled(false);96
return;97
}98
await startGroup("", { channel: { id: getCurrentChannel().id } });99
}100
setEnabled(!enabled);101
setButtonDisabled(false);102
}}103
buttonProps={{104
style: {105
transition: "transform 0.3s ease-in-out",106
transform: `rotate(${enabled ? 0 : 15}deg)`,107
},108
disabled: buttonDisabled109
}}110
>111
<svg112
width="24"113
height="24"114
viewBox="0 0 129 171"115
>116
{!enabled && <>117
<mask id="encordBarIcon">118
</mask>119
<path120
fill="currentColor"121
d="M128.93 149.231V74.907a21.142 21.142 0 00-6.195-15.1 21.165 21.165 0 00-15.101-6.195h-1.085V40.918A40.604 40.604 0 0042.214 8.065 40.602 40.602 0 0026.28 32.318h15.972a25.164 25.164 0 0128.043-15.94 25.166 25.166 0 0120.691 24.745v12.694H22.184A21.276 21.276 0 00.89 75.111v74.325a21.27 21.27 0 0013.143 19.679 21.273 21.273 0 008.152 1.615h85.388a21.455 21.455 0 0015.083-6.357 21.453 21.453 0 006.213-15.142h.062zm-63.888-15.765a21.296 21.296 0 01-15.058-36.352 21.296 21.296 0 0136.354 15.057 21.151 21.151 0 01-21.296 21.295z"122
/>123
</>124
}125
<path126
mask="url(#encordBarIcon)"127
fill="currentColor"128
d="M129.497 149.264V75.001a21.27 21.27 0 00-21.295-21.294h-3.072V41.012a41.079 41.079 0 00-1.024-8.6A40.62 40.62 0 0070.729 1.087 5.673 5.673 0 0168.886.88h-.204c-.615 0-1.23-.205-1.844-.205h-4.095A5.672 5.672 0 0060.9.881h-.204a5.672 5.672 0 00-1.843.205A40.627 40.627 0 0025.27 32.413h.205a41.092 41.092 0 00-1.024 8.6v12.694h-3.133A21.153 21.153 0 00.023 75v74.325a21.415 21.415 0 0021.296 21.294h87.231a21.336 21.336 0 0020.886-21.294l.061-.062zm-64.91-15.97a21.317 21.317 0 01-22.069-24.804 21.316 21.316 0 0142.34 3.509 21.355 21.355 0 01-20.272 21.295zm25.185-79.649H39.604V40.951a24.283 24.283 0 016.963-17.2 25.351 25.351 0 0116.79-7.78h2.663a25.31 25.31 0 0123.752 25.184v12.49z"129
/>130
</svg>131
</ChatBarButton>132
);133
};134
135
// Export Plugin136
export default definePlugin({137
name: "Encryptcord",138
description: "End-to-end encryption in Discord!",139
authors: [Devs.FiveCord],140
dependencies: ["CommandsAPI"],141
patches: [142
{143
find: "executeMessageComponentInteraction:",144
replacement: {145
match: /await \i\.HTTP\.post\({url:\i\.Endpoints\.INTERACTIONS,body:\i,timeout:/,146
replace: "await $self.joinGroup(C);$&"147
}148
}149
],150
async joinGroup(interaction) {151
const sender = await UserUtils.getUser(interaction.application_id).catch(() => null);152
if (!sender || (sender.bot === true && sender.id !== "1")) return;153
if (interaction.data.component_type !== 2) return;154
switch (interaction.data.custom_id) {155
case "removeFromSelf":156
await handleLeaving(sender.id, await DataStore.get("encryptcordGroupMembers") ?? {}, interaction.channel_id);157
await sendTempMessage(sender.id, "", "leaving");158
FluxDispatcher.dispatch({159
type: "MESSAGE_DELETE",160
channelId: interaction.channel_id,161
id: interaction.message_id,162
mlDeleted: true163
});164
break;165
case "createGroup":166
await leave("", { channel: { id: interaction.channel_id } });167
await startGroup("", { channel: { id: interaction.channel_id } });168
break;169
default:170
return;171
}172
},173
flux: {174
async MESSAGE_CREATE({ optimistic, type, message, channelId }: IMessageCreate) {175
if (optimistic || type !== "MESSAGE_CREATE") return;176
if (message.state === "SENDING") return;177
if (message.author.id === UserStore.getCurrentUser().id) return;178
if (!message.content) return;179
const encryptcordGroupMembers = await DataStore.get("encryptcordGroupMembers");180
if (!Object.keys(encryptcordGroupMembers).some(key => key === message.author.id)) {181
switch (message.content.toLowerCase().split("```")[0]) {182
case "groupdata":183
const response = await fetch(message.attachments[0].url);184
const groupdata = await response.json();185
await handleGroupData(groupdata);186
break;187
case "join":188
if (encryptcordGroupMembers[UserStore.getCurrentUser().id].child) return;189
if (!await DataStore.get("encryptcordGroup")) return;190
const sender = await UserUtils.getUser(message.author.id).catch(() => null);191
if (!sender) return;192
const userKey = message.content.split("```")[1];193
await handleJoin(sender.id, userKey, encryptcordGroupMembers);194
break;195
default:196
break;197
}198
return;199
}200
const dmChannelId = await PrivateChannelsStore.getOrEnsurePrivateChannel(message.author.id);201
if (channelId !== dmChannelId) return;202
const sender = await UserUtils.getUser(message.author.id).catch(() => null);203
if (!sender) return;204
const groupChannel = await DataStore.get("encryptcordChannelId");205
switch (message.content.toLowerCase()) {206
case "leaving":207
handleLeaving(sender.id, encryptcordGroupMembers, groupChannel);208
break;209
case "message":210
const msgResponse = await fetch(message.attachments[0].url);211
const messagedata = await msgResponse.json();212
await handleMessage(messagedata, sender.id, groupChannel);213
break;214
case "groupdata":215
const response = await fetch(message.attachments[0].url);216
const groupdata = await response.json();217
await handleGroupData(groupdata);218
break;219
default:220
break;221
}222
},223
},224
commands: [225
{226
name: "encryptcord",227
description: "End-to-end encryption in Discord!",228
options: [229
{230
name: "leave",231
description: "Leave current group",232
options: [],233
type: ApplicationCommandOptionType.SUB_COMMAND,234
},235
{236
name: "data",237
description: "View your keys and current group members",238
options: [],239
type: ApplicationCommandOptionType.SUB_COMMAND,240
},241
],242
inputType: ApplicationCommandInputType.BOT,243
execute: (opts, ctx) => {244
switch (opts[0].name) {245
case "start":246
startGroup(opts[0].options, ctx);247
break;248
case "leave":249
leave(opts[0].options, ctx);250
break;251
case "data":252
data(opts[0].options, ctx);253
break;254
}255
},256
},257
],258
async start() {259
addChatBarButton("Encryptcord", ChatBarIcon);260
const pair = await generateKeys();261
await DataStore.set("encryptcordPublicKey", pair.publicKey);262
await DataStore.set("encryptcordPrivateKey", pair.privateKey);263
if (await DataStore.get("encryptcordGroup") === true) {264
await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } });265
}266
await DataStore.set("encryptcordGroup", false);267
await DataStore.set("encryptcordChannelId", "");268
await DataStore.set("encryptcordGroupMembers", {});269
},270
async stop() {271
removeButton("Encryptcord");272
if (await DataStore.get("encryptcordGroup") === true) {273
await leave("", { channel: { id: await DataStore.get("encryptcordChannelId") } });274
}275
},276
});277
278
// Send Temporary Message279
async function sendTempMessage(recipientId: string, attachment: string, content: string, dm: boolean = true) {280
if (recipientId === UserStore.getCurrentUser().id) return;281
const channelId = dm ? await PrivateChannelsStore.getOrEnsurePrivateChannel(recipientId) : recipientId;282
if (attachment && attachment !== "") {283
const upload = await new CloudUtils.CloudUpload({284
file: new File([new Blob([attachment])], "file.text", { type: "text/plain; charset=utf-8" }),285
isClip: false,286
isThumbnail: false,287
platform: 1,288
}, channelId, false, 0);289
upload.on("complete", async () => {290
const messageId = await RestAPI.post({291
url: `/channels/${channelId}/messages`,292
body: {293
content,294
attachments: [{295
id: "0",296
filename: upload.filename,297
uploaded_filename: upload.uploadedFilename,298
}],299
nonce: SnowflakeUtils.fromTimestamp(Date.now()),300
},301
}).then(response => response.body.id);302
303
await sleep(500);304
MessageActions.deleteMessage(channelId, messageId);305
});306
await upload.upload();307
return;308
}309
310
const messageId = await RestAPI.post({311
url: `/channels/${channelId}/messages`,312
body: {313
content,314
nonce: SnowflakeUtils.fromTimestamp(Date.now()),315
},316
}).then(response => response.body.id);317
318
await sleep(500);319
MessageActions.deleteMessage(channelId, messageId);320
}321
322
// Handle leaving group323
async function handleLeaving(senderId: string, encryptcordGroupMembers: object, groupChannel: string) {324
const updatedMembers = Object.keys(encryptcordGroupMembers).reduce((result, memberId) => {325
if (memberId !== senderId) {326
result[memberId] = encryptcordGroupMembers[memberId];327
if (result[memberId].child === senderId) {328
result[memberId].child = encryptcordGroupMembers[senderId].child;329
}330
if (result[memberId].parent === senderId) {331
result[memberId].parent = encryptcordGroupMembers[senderId].parent;332
}333
}334
return result;335
}, {});336
337
await DataStore.set("encryptcordGroupMembers", updatedMembers);338
339
await MessageActions.receiveMessage(groupChannel, await createMessage("", senderId, groupChannel, 2));340
}341
342
// Handle receiving message343
async function handleMessage(message, senderId: string, groupChannel: string) {344
const decryptedMessage = await decryptData(await DataStore.get("encryptcordPrivateKey"), message);345
await MessageActions.receiveMessage(groupChannel, await createMessage(decryptedMessage, senderId, groupChannel, 0));346
}347
348
// Handle receiving group data349
async function handleGroupData(groupData) {350
await DataStore.set("encryptcordChannelId", groupData.channel);351
await DataStore.set("encryptcordGroupMembers", groupData.members);352
await DataStore.set("encryptcordGroup", true);353
await MessageActions.receiveMessage(groupData.channel, await createMessage("", UserStore.getCurrentUser().id, groupData.channel, 7));354
setEnabled(true);355
}356
357
// Handle joining group358
async function handleJoin(senderId: string, senderKey: string, encryptcordGroupMembers: object) {359
encryptcordGroupMembers[senderId] = { key: senderKey, parent: UserStore.getCurrentUser().id, child: null };360
encryptcordGroupMembers[UserStore.getCurrentUser().id].child = senderId;361
await DataStore.set("encryptcordGroupMembers", encryptcordGroupMembers);362
const groupChannel = await DataStore.get("encryptcordChannelId");363
const newMember = await UserUtils.getUser(senderId).catch(() => null);364
if (!newMember) return;365
366
const membersData = {};367
Object.entries(encryptcordGroupMembers)368
.forEach(([memberId, value]) => {369
membersData[memberId] = value;370
});371
372
const membersDataString = JSON.stringify({ members: membersData, channel: groupChannel });373
374
const dmPromises = Object.keys(encryptcordGroupMembers).map(async memberId => {375
const groupMember = await UserUtils.getUser(memberId).catch(() => null);376
if (!groupMember) return;377
await sendTempMessage(groupMember.id, membersDataString, "groupdata");378
});379
380
await Promise.all(dmPromises);381
await MessageActions.receiveMessage(groupChannel, {382
...await createMessage("", senderId, groupChannel, 7), components: [{383
type: 1,384
components: [{385
type: 2,386
style: 4,387
label: "I don039;t want to talk to you!",388
custom_id: "removeFromSelf"389
},390
{391
type: 2,392
style: 2,393
label: "(Other users can still send/receive messages to/from them)",394
disabled: true,395
custom_id: "encryptcord"396
}]397
}]398
});399
}400
401
// Create message for group402
async function createMessage(message: string, senderId: string, channelId: string, type: number) {403
const messageStart = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] });404
const sender = await UserUtils.getUser(senderId).catch(() => null);405
if (!sender) return;406
return { ...messageStart, content: message, author: sender, type, flags: 0 };407
}408
409
// Start E2EE Group410
async function startGroup(opts, ctx) {411
const channelId = ctx.channel.id;412
await DataStore.set("encryptcordChannelId", channelId);413
await DataStore.set("encryptcordGroupMembers", {414
[UserStore.getCurrentUser().id]: { key: await DataStore.get("encryptcordPublicKey"), parent: null, child: null }415
});416
await DataStore.set("encryptcordGroup", true);417
sendBotMessage(channelId, { content: "Group created!\n> Other users can click the lock icon to join." });418
await MessageActions.receiveMessage(channelId, await createMessage("", UserStore.getCurrentUser().id, channelId, 7));419
setEnabled(true);420
}421
422
// Leave the Group;423
async function leave(opts, ctx) {424
const channelId = ctx.channel.id;425
if (!(await DataStore.get("encryptcordGroup"))) {426
sendBotMessage(channelId, { content: "You039;re not in a group!" });427
return;428
}429
const user = UserStore.getCurrentUser();430
const encryptcordGroupMembers = await DataStore.get("encryptcordGroupMembers");431
432
const dmPromises = Object.keys(encryptcordGroupMembers).map(async memberId => {433
const groupMember = await UserUtils.getUser(memberId).catch(() => null);434
if (!groupMember) return;435
await sendTempMessage(groupMember.id, "", "leaving");436
});437
438
await Promise.all(dmPromises);439
await DataStore.set("encryptcordGroup", false);440
await DataStore.set("encryptcordChannelId", "");441
await DataStore.set("encryptcordGroupMembers", {});442
await MessageActions.receiveMessage(channelId, await createMessage("", user.id, channelId, 2));443
setEnabled(false);444
}445
446
// View user data447
async function data(opts, ctx) {448
const channelId = ctx.channel.id;449
const encryptcordGroupMembers = await DataStore.get("encryptcordGroupMembers");450
const encryptcordPublicKey = await DataStore.get("encryptcordPublicKey");451
const encryptcordPrivateKey = await DataStore.get("encryptcordPrivateKey");452
const exportedPrivateKey = await crypto.subtle.exportKey("pkcs8", encryptcordPrivateKey);453
const groupMembers = Object.keys(encryptcordGroupMembers);454
sendBotMessage(channelId, {455
content: `## Public key:\n\`\`\`${encryptcordPublicKey}\`\`\`\n## Private key:\n||\`\`\`${formatPemKey(exportedPrivateKey, "private")}\`\`\`||*(DO **NOT** SHARE THIS)*\n## Group members:\n\`\`\`json\n${JSON.stringify(groupMembers)}\`\`\``456
});457
}458