Merge pull request #216 from Mega-Mewthree/security

E2EE Secure ECDH key exchange for DMs
This commit is contained in:
Alexei Stukov 2018-08-14 04:03:11 +03:00 committed by GitHub
commit 0315d610e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 2871 additions and 2719 deletions

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

@ -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")'

View File

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

5425
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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