BetterDiscordApp-v2/client/src/builtin/E2EE.js

228 lines
9.8 KiB
JavaScript
Raw Normal View History

2018-08-09 09:56:44 +02:00
/**
* BetterDiscord E2EE Module
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
2018-08-10 15:22:30 +02:00
import { Settings } from 'modules';
2018-08-09 09:56:44 +02:00
import BuiltinModule from './BuiltinModule';
2018-08-10 15:22:30 +02:00
import { WebpackModules, ReactComponents, MonkeyPatch, Patcher, DiscordApi } from 'modules';
2018-08-10 14:08:21 +02:00
import { VueInjector, Reflection } from 'ui';
import { ClientLogger as Logger } from 'common';
2018-08-10 14:30:24 +02:00
import E2EEComponent from './E2EEComponent.vue';
import E2EEMessageButton from './E2EEMessageButton.vue';
2018-08-10 14:08:21 +02:00
import aes256 from 'aes256';
import crypto from 'crypto';
2018-08-09 09:56:44 +02:00
2018-08-11 07:15:49 +02:00
const userMentionPattern = new RegExp(`<@!?([0-9]{10,})>`, "g");
const roleMentionPattern = new RegExp(`<@&([0-9]{10,})>`, "g");
const everyoneMentionPattern = new RegExp(`(?:\\s+|^)@everyone(?:\\s+|$)`);
2018-08-10 16:02:41 +02:00
let seed = Math.random().toString(36).replace(/[^a-z]+/g, '');
2018-08-10 16:01:07 +02:00
2018-08-09 09:56:44 +02:00
export default new class E2EE extends BuiltinModule {
2018-08-10 16:01:07 +02:00
constructor() {
super();
this.master = this.encrypt(seed, 'temporarymasterkey');
this.encryptNewMessages = true;
2018-08-09 09:56:44 +02:00
}
2018-08-10 16:02:41 +02:00
setMaster(key) {
seed = Math.random().toString(36).replace(/[^a-z]+/g, '');
2018-08-10 16:04:09 +02:00
const newMaster = this.encrypt(seed, key);
// TODO re-encrypt everything with new master
return (this.master = newMaster);
2018-08-10 16:02:41 +02:00
}
2018-08-10 16:01:07 +02:00
get settingPath() {
return ['security', 'default', 'e2ee'];
2018-08-10 15:22:30 +02:00
}
2018-08-10 15:22:30 +02:00
get database() {
return Settings.getSetting('security', 'e2eedb', 'e2ekvps').value;
2018-08-10 15:22:30 +02:00
}
2018-08-10 20:17:52 +02:00
encrypt(key, content, prefix = '') {
2018-08-10 15:22:30 +02:00
return prefix + aes256.encrypt(key, content);
}
2018-08-10 20:17:52 +02:00
decrypt(key, content, prefix = '') {
return aes256.decrypt(key, content.replace(prefix, ''));
2018-08-10 15:22:30 +02:00
}
getKey(channelId) {
const haveKey = this.database.find(kvp => kvp.value.key === channelId);
if (!haveKey) return null;
return haveKey.value.value;
}
2018-08-12 01:21:08 +02:00
setKey(channelId, key) {
const items = Settings.getSetting('security', 'e2eedb', 'e2ekvps').items;
const index = items.findIndex(kvp => kvp.value.key === channelId);
if (index > -1) {
items[index].value = {key: channelId, value: key};
return;
}
Settings.getSetting('security', 'e2eedb', 'e2ekvps').addItem({ value: { key: channelId, value: key } });
}
2018-08-10 14:08:21 +02:00
async enabled(e) {
2018-08-11 07:15:49 +02:00
this.patchDispatcher();
2018-08-10 19:38:02 +02:00
this.patchMessageContent();
const selector = '.' + WebpackModules.getClassName('channelTextArea', 'emojiButton');
2018-08-11 02:39:13 +02:00
const cta = await ReactComponents.getComponent('ChannelTextArea', { selector });
this.patchChannelTextArea(cta);
this.patchChannelTextAreaSubmit(cta);
cta.forceUpdateAll();
}
2018-08-11 07:15:49 +02:00
patchDispatcher() {
const Dispatcher = WebpackModules.getModuleByName('Dispatcher');
MonkeyPatch('BD:E2EE', Dispatcher).before('dispatch', (_, [event]) => {
if (event.type !== "MESSAGE_CREATE") return;
const key = this.getKey(event.message.channel_id);
if (!key) return; // We don't have a key for this channel
2018-08-12 01:21:08 +02:00
2018-08-11 07:15:49 +02:00
if (typeof event.message.content !== 'string') return; // Ignore any non string content
if (!event.message.content.startsWith('$:')) return; // Not an encrypted string
let decrypt;
try {
decrypt = this.decrypt(this.decrypt(this.decrypt(seed, this.master), key), event.message.content);
} catch (err) { return } // Ignore errors such as non empty
const MessageParser = WebpackModules.getModuleByName('MessageParser');
const Permissions = WebpackModules.getModuleByName('GuildPermissions');
const DiscordConstants = WebpackModules.getModuleByName('DiscordConstants');
const currentChannel = DiscordApi.Channel.fromId(event.message.channel_id).discordObject;
2018-08-12 01:21:08 +02:00
2018-08-11 07:15:49 +02:00
// Create a generic message object to parse mentions with
const parsed = MessageParser.parse(currentChannel, decrypt).content;
2018-08-12 01:21:08 +02:00
2018-08-11 07:15:49 +02:00
if (userMentionPattern.test(parsed))
event.message.mentions = parsed.match(userMentionPattern).map(m => {return {id: m.replace(/[^0-9]/g, '')}});
if (roleMentionPattern.test(parsed))
event.message.mention_roles = parsed.match(roleMentionPattern).map(m => m.replace(/[^0-9]/g, ''));
if (everyoneMentionPattern.test(parsed))
event.message.mention_everyone = Permissions.can(DiscordConstants.Permissions.MENTION_EVERYONE, currentChannel);
});
}
2018-08-11 02:39:13 +02:00
async patchMessageContent() {
const selector = '.' + WebpackModules.getClassName('container', 'containerCozy', 'containerCompact', 'edited');
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector });
MonkeyPatch('BD:E2EE', MessageContent.component.prototype).before('render', this.beforeRenderMessageContent.bind(this));
MonkeyPatch('BD:E2EE', MessageContent.component.prototype).after('render', this.renderMessageContent.bind(this));
2018-08-09 09:56:44 +02:00
}
2018-08-11 07:15:49 +02:00
beforeRenderMessageContent(component) {
if (!component.props || !component.props.message) return;
const key = this.getKey(component.props.message.channel_id);
2018-08-11 02:39:13 +02:00
if (!key) return; // We don't have a key for this channel
const Message = WebpackModules.getModuleByPrototypes(['isMentioned']);
const MessageParser = WebpackModules.getModuleByName('MessageParser');
2018-08-11 07:15:49 +02:00
const Permissions = WebpackModules.getModuleByName('GuildPermissions');
const DiscordConstants = WebpackModules.getModuleByName('DiscordConstants');
const currentChannel = DiscordApi.Channel.fromId(component.props.message.channel_id).discordObject;
2018-08-12 01:21:08 +02:00
2018-08-11 07:15:49 +02:00
if (typeof component.props.message.content !== 'string') return; // Ignore any non string content
if (!component.props.message.content.startsWith('$:')) return; // Not an encrypted string
2018-08-11 02:39:13 +02:00
let decrypt;
try {
decrypt = this.decrypt(this.decrypt(this.decrypt(seed, this.master), key), component.props.message.content);
} catch (err) { return } // Ignore errors such as non empty
2018-08-11 07:15:49 +02:00
component.props.message.bd_encrypted = true; // signal as encrypted
// Create a generic message object to parse mentions with
const message = MessageParser.createMessage(currentChannel.id, MessageParser.parse(currentChannel, decrypt).content);
if (userMentionPattern.test(message.content))
message.mentions = message.content.match(userMentionPattern).map(m => {return {id: m.replace(/[^0-9]/g, '')}});
if (roleMentionPattern.test(message.content))
message.mention_roles = message.content.match(roleMentionPattern).map(m => m.replace(/[^0-9]/g, ''));
if (everyoneMentionPattern.test(message.content))
message.mention_everyone = Permissions.can(DiscordConstants.Permissions.MENTION_EVERYONE, currentChannel);
2018-08-11 02:39:13 +02:00
// Create a new message to parse it properly
2018-08-11 07:15:49 +02:00
const create = Message.create(message);
2018-08-11 02:39:13 +02:00
if (!create.content || !create.contentParsed) return;
2018-08-11 07:15:49 +02:00
component.props.message.mentions = create.mentions;
component.props.message.mentionRoles = create.mentionRoles;
component.props.message.mentionEveryone = create.mentionEveryone;
component.props.message.mentioned = create.mentioned;
2018-08-11 02:39:13 +02:00
component.props.message.content = create.content;
component.props.message.contentParsed = create.contentParsed;
2018-08-10 19:38:02 +02:00
}
renderMessageContent(component, args, retVal) {
if (!component.props.message.bd_encrypted) return;
retVal.props.children[0].props.children.props.children.props.children.unshift(VueInjector.createReactElement(E2EEMessageButton, {
message: component.props.message
}));
}
2018-08-11 02:39:13 +02:00
patchChannelTextArea(cta) {
MonkeyPatch('BD:E2EE', cta.component.prototype).after('render', this.renderChannelTextArea);
}
renderChannelTextArea(component, args, retVal) {
2018-08-10 14:08:21 +02:00
if (!(retVal.props.children instanceof Array)) retVal.props.children = [retVal.props.children];
const inner = retVal.props.children.find(child => child.props.className && child.props.className.includes('inner'));
inner.props.children.splice(0, 0, VueInjector.createReactElement(E2EEComponent));
2018-08-10 14:08:21 +02:00
}
2018-08-09 09:56:44 +02:00
2018-08-11 02:39:13 +02:00
patchChannelTextAreaSubmit(cta) {
MonkeyPatch('BD:E2EE', cta.component.prototype).before('handleSubmit', this.handleChannelTextAreaSubmit.bind(this));
}
get ecdh() {
if (!this._ecdh) this._ecdh = {};
return this._ecdh;
}
createKeyExchange(dmChannelID) {
this.ecdh[dmChannelID] = crypto.createECDH('secp521r1');
return this.ecdh[dmChannelID].generateKeys('base64');
}
publicKeyFor(dmChannelID) {
return this.ecdh[dmChannelID].getPublicKey('base64');
}
computeSecret(dmChannelID, otherKey) {
2018-08-12 01:28:34 +02:00
try {
const secret = this.ecdh[dmChannelID].computeSecret(otherKey, 'base64', 'base64');
delete this.ecdh[dmChannelID];
2018-08-12 01:28:34 +02:00
const hash = crypto.createHash('sha256');
hash.update(secret);
return hash.digest('base64');
} catch (e) {
throw e;
}
}
2018-08-11 02:39:13 +02:00
handleChannelTextAreaSubmit(component, args, retVal) {
2018-08-10 15:22:30 +02:00
const key = this.getKey(DiscordApi.currentChannel.id);
if (!this.encryptNewMessages || !key) return;
2018-08-10 20:17:52 +02:00
component.props.value = this.encrypt(this.decrypt(this.decrypt(seed, this.master), key), component.props.value, '$:');
2018-08-10 14:08:21 +02:00
}
2018-08-10 19:38:02 +02:00
async disabled(e) {
2018-08-10 14:08:21 +02:00
for (const patch of Patcher.getPatchesByCaller('BD:E2EE')) patch.unpatch();
2018-08-10 19:38:02 +02:00
const ctaComponent = await ReactComponents.getComponent('ChannelTextArea');
ctaComponent.forceUpdateAll();
2018-08-09 09:56:44 +02:00
}
}