Merge pull request #216 from Mega-Mewthree/security
E2EE Secure ECDH key exchange for DMs
This commit is contained in:
commit
0315d610e0
|
@ -17,6 +17,7 @@ import { request } from 'vendor';
|
|||
import { Utils } from 'common';
|
||||
import E2EEComponent from './E2EEComponent.vue';
|
||||
import E2EEMessageButton from './E2EEMessageButton.vue';
|
||||
import nodecrypto from 'node-crypto';
|
||||
|
||||
const userMentionPattern = new RegExp(`<@!?([0-9]{10,})>`, "g");
|
||||
const roleMentionPattern = new RegExp(`<@&([0-9]{10,})>`, "g");
|
||||
|
@ -43,6 +44,7 @@ export default new class E2EE extends BuiltinModule {
|
|||
return ['security', 'default', 'e2ee'];
|
||||
}
|
||||
|
||||
|
||||
get database() {
|
||||
return Settings.getSetting('security', 'e2eedb', 'e2ekvps').value;
|
||||
}
|
||||
|
@ -78,6 +80,39 @@ export default new class E2EE extends BuiltinModule {
|
|||
return haveKey.value.value;
|
||||
}
|
||||
|
||||
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 } });
|
||||
}
|
||||
|
||||
get ecdhStorage() {
|
||||
return this._ecdhStorage || (this._ecdhStorage = {});
|
||||
}
|
||||
|
||||
createKeyExchange(dmChannelID) {
|
||||
this.ecdhStorage[dmChannelID] = Security.createECDH();
|
||||
return Security.generateECDHKeys(this.ecdhStorage[dmChannelID]);
|
||||
}
|
||||
|
||||
publicKeyFor(dmChannelID) {
|
||||
return Security.getECDHPublicKey(this.ecdhStorage[dmChannelID]);
|
||||
}
|
||||
|
||||
computeSecret(dmChannelID, otherKey) {
|
||||
try {
|
||||
const secret = Security.computeECDHSecret(this.ecdhStorage[dmChannelID], otherKey);
|
||||
delete this.ecdhStorage[dmChannelID];
|
||||
return Security.sha256(secret);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async enabled(e) {
|
||||
seed = Security.randomBytes();
|
||||
// TODO Input modal for key
|
||||
|
@ -98,7 +133,7 @@ export default new class E2EE extends BuiltinModule {
|
|||
|
||||
const key = this.getKey(event.message.channel_id);
|
||||
if (!key) return; // We don't have a key for this channel
|
||||
|
||||
|
||||
if (typeof event.message.content !== 'string') return; // Ignore any non string content
|
||||
if (!event.message.content.startsWith('$:')) return; // Not an encrypted string
|
||||
let decrypt;
|
||||
|
@ -110,10 +145,10 @@ export default new class E2EE extends BuiltinModule {
|
|||
const Permissions = WebpackModules.getModuleByName('GuildPermissions');
|
||||
const DiscordConstants = WebpackModules.getModuleByName('DiscordConstants');
|
||||
const currentChannel = DiscordApi.Channel.fromId(event.message.channel_id).discordObject;
|
||||
|
||||
|
||||
// Create a generic message object to parse mentions with
|
||||
const parsed = MessageParser.parse(currentChannel, decrypt).content;
|
||||
|
||||
|
||||
if (userMentionPattern.test(parsed))
|
||||
event.message.mentions = parsed.match(userMentionPattern).map(m => {return {id: m.replace(/[^0-9]/g, '')}});
|
||||
if (roleMentionPattern.test(parsed))
|
||||
|
@ -143,7 +178,7 @@ export default new class E2EE extends BuiltinModule {
|
|||
const Permissions = WebpackModules.getModuleByName('GuildPermissions');
|
||||
const DiscordConstants = WebpackModules.getModuleByName('DiscordConstants');
|
||||
const currentChannel = DiscordApi.Channel.fromId(component.props.message.channel_id).discordObject;
|
||||
|
||||
|
||||
if (typeof component.props.message.content !== 'string') return; // Ignore any non string content
|
||||
if (!component.props.message.content.startsWith('$:')) return; // Not an encrypted string
|
||||
let decrypt;
|
||||
|
@ -264,6 +299,32 @@ export default new class E2EE extends BuiltinModule {
|
|||
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) {
|
||||
try {
|
||||
const secret = this.ecdh[dmChannelID].computeSecret(otherKey, 'base64', 'base64');
|
||||
delete this.ecdh[dmChannelID];
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(secret);
|
||||
return hash.digest('base64');
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
handleChannelTextAreaSubmit(component, args, retVal) {
|
||||
const key = this.getKey(DiscordApi.currentChannel.id);
|
||||
if (!this.encryptNewMessages || !key) return;
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
*/
|
||||
|
||||
<template>
|
||||
<div class="bd-e2eeTaContainer">
|
||||
<div class="bd-e2eeTaContainer" @contextmenu.prevent="currentChannel.type === 'DM' && $refs.ee2eLockContextMenu.open()">
|
||||
<v-popover popoverClass="bd-popover bd-e2eePopover" placement="top">
|
||||
<div v-if="error" class="bd-e2eeTaBtn bd-e2eeLock bd-error">
|
||||
<MiLock v-tooltip="error" />
|
||||
|
@ -28,9 +28,16 @@
|
|||
<template slot="popover">
|
||||
<div @click="toggleEncrypt" :class="{'bd-warn': !E2EE.encryptNewMessages, 'bd-ok': E2EE.encryptNewMessages}"><MiLock size="16" v-tooltip="'Toggle Encryption'" /></div>
|
||||
<div v-close-popover @click="showUploadDialog" v-if="!error"><MiImagePlus size="16" v-tooltip="'Upload Encrypted Image'" /></div>
|
||||
<!-- Using these icons for now -->
|
||||
<div v-close-popover @click="generatePublicKey" v-if="currentChannel.type === 'DM'"><MiPencil size="16" v-tooltip="'Generate Public Key'" /></div>
|
||||
<div v-close-popover @click="receivePublicKey" v-if="currentChannel.type === 'DM' && E2EE.ecdhStorage[currentChannel.id]"><MiRefresh size="16" v-tooltip="'Receive Public Key'" /></div>
|
||||
</template>
|
||||
</v-popover>
|
||||
<div class="bd-taDivider"></div>
|
||||
<context-menu id="bd-e2eeLockContextMenu" class="bd-e2eeLockContextMenu" ref="ee2eLockContextMenu" v-if="channelType === 'DM'">
|
||||
<li class="bd-e2eeLockContextMenuOption" @click="generatePublicKey()">Generate Public Key</li>
|
||||
<li class="bd-e2eeLockContextMenuOption" @click="computeSharedSecret()">Receive Public Key</li>
|
||||
</context-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -42,16 +49,17 @@
|
|||
import { remote } from 'electron';
|
||||
import { E2EE } from 'builtin';
|
||||
import { DiscordApi, Security } from 'modules';
|
||||
import { MiLock, MiPlus, MiImagePlus } from '../ui/components/common/MaterialIcon';
|
||||
import { MiLock, MiPlus, MiImagePlus, MiPencil, MiRefresh } from '../ui/components/common/MaterialIcon';
|
||||
import { Toasts } from 'ui';
|
||||
|
||||
export default {
|
||||
components: { MiLock, MiPlus, MiImagePlus },
|
||||
components: { MiLock, MiPlus, MiImagePlus, MiPencil, MiRefresh },
|
||||
data() {
|
||||
return {
|
||||
E2EE,
|
||||
state: 'loading',
|
||||
error: null
|
||||
error: null,
|
||||
currentChannel: DiscordApi.currentChannel
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -82,6 +90,32 @@
|
|||
return;
|
||||
}
|
||||
Toasts.success('New messages will be encrypted');
|
||||
},
|
||||
generatePublicKey() {
|
||||
const dmChannelID = DiscordApi.currentChannel.id;
|
||||
const publicKeyMessage = `My public key is: \`${E2EE.createKeyExchange(dmChannelID)}\`. Please give me your public key if you haven't done so and add my public key by pasting it in the chat textbox, right clicking the lock icon, and selecting \`Receive Public Key\`.`;
|
||||
const chatInput = document.getElementsByClassName('da-textArea')[0];
|
||||
chatInput.value = publicKeyMessage;
|
||||
const evt = { currentTarget: chatInput };
|
||||
chatInput[Object.keys(chatInput).find(k => k.startsWith('__reactEventHandlers'))].onChange.call(chatInput, evt);
|
||||
this.$forceUpdate();
|
||||
},
|
||||
receivePublicKey() {
|
||||
try {
|
||||
const dmChannelID = DiscordApi.currentChannel.id;
|
||||
const chatInput = document.getElementsByClassName('da-textArea')[0];
|
||||
const otherPublicKey = chatInput.value;
|
||||
const secret = E2EE.computeSecret(dmChannelID, otherPublicKey);
|
||||
E2EE.setKey(dmChannelID, secret);
|
||||
chatInput.value = "";
|
||||
const evt = { currentTarget: chatInput };
|
||||
chatInput[Object.keys(chatInput).find(k => k.startsWith('__reactEventHandlers'))].onChange.call(chatInput, evt);
|
||||
Toasts.success("Encryption key has been set for this DM channel.");
|
||||
this.$forceUpdate();
|
||||
} catch (e) {
|
||||
Toasts.error("Invalid public key. Please set up a new key exchange.");
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -12,7 +12,7 @@ import { DOM, BdUI, BdMenu, Modals, Reflection, Toasts } from 'ui';
|
|||
import BdCss from './styles/index.scss';
|
||||
import { Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, WebpackModules, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi, BdWebApi, Connectivity, Cache } from 'modules';
|
||||
import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
|
||||
import { BuiltinManager, EmoteModule, ReactDevtoolsModule, VueDevtoolsModule, TrackingProtection } from 'builtin';
|
||||
import { BuiltinManager, EmoteModule, ReactDevtoolsModule, VueDevtoolsModule, TrackingProtection, E2EE } from 'builtin';
|
||||
import electron from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
|
|
|
@ -80,4 +80,26 @@ export default class Security {
|
|||
});
|
||||
}
|
||||
|
||||
static createECDH() {
|
||||
return nodecrypto.createECDH('secp521r1');
|
||||
}
|
||||
|
||||
static sha256(text) {
|
||||
const hash = nodecrypto.createHash('sha256');
|
||||
hash.update(text);
|
||||
return hash.digest('base64');
|
||||
}
|
||||
|
||||
static generateECDHKeys(ecdh) {
|
||||
return ecdh.generateKeys('base64');
|
||||
}
|
||||
|
||||
static getECDHPublicKey(ecdh) {
|
||||
return ecdh.getPublicKey('base64');
|
||||
}
|
||||
|
||||
static computeECDHSecret(ecdh, otherPublicKey) {
|
||||
return ecdh.computeSecret(otherPublicKey, 'base64', 'base64');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,6 +39,30 @@
|
|||
height: 37px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.bd-e2eeLockContextMenu {
|
||||
|
||||
border: none;
|
||||
|
||||
.ctx-menu {
|
||||
background: #23272A;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
|
||||
.bd-e2eeLockContextMenuOption {
|
||||
background: #23272A;
|
||||
color: #99AAB5;
|
||||
padding: 8px 8px;
|
||||
font-weight: 500;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
background: #000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-e2eeMdContainer {
|
||||
|
|
|
@ -37,6 +37,7 @@ module.exports = {
|
|||
util: 'require("util")',
|
||||
process: 'require("process")',
|
||||
net: 'require("net")',
|
||||
crypto: 'require("crypto")',
|
||||
request: 'require(require("path").join(require("electron").remote.app.getAppPath(), "node_modules", "request"))',
|
||||
sparkplug: 'require("../../core/dist/sparkplug")',
|
||||
'node-crypto': 'require("crypto")'
|
||||
|
|
|
@ -38,8 +38,10 @@ module.exports = {
|
|||
util: 'require("util")',
|
||||
process: 'require("process")',
|
||||
net: 'require("net")',
|
||||
crypto: 'require("crypto")',
|
||||
request: 'require(require("path").join(require("electron").remote.app.getAppPath(), "node_modules", "request"))',
|
||||
sparkplug: 'require("./sparkplug")'
|
||||
sparkplug: 'require("./sparkplug")',
|
||||
'node-crypto': 'require("crypto")'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -61,6 +61,7 @@
|
|||
"vue": "^2.5.17",
|
||||
"vue-codemirror": "^4.0.5",
|
||||
"vue-color": "^2.4.6",
|
||||
"vue-context-menu": "^2.0.6",
|
||||
"vue-loader": "^13.7.2",
|
||||
"vue-material-design-icons": "^1.6.0",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
|
|
Loading…
Reference in New Issue