commit
462b198895
|
@ -8,20 +8,351 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { Settings, Cache, Events } from 'modules';
|
||||
import BuiltinModule from './BuiltinModule';
|
||||
import { WebpackModules, ReactComponents, MonkeyPatch, Patcher, DiscordApi, Security } from 'modules';
|
||||
import { VueInjector, Reflection, Modals, Toasts } from 'ui';
|
||||
import { ClientLogger as Logger } from 'common';
|
||||
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");
|
||||
const everyoneMentionPattern = new RegExp(`(?:\\s+|^)@everyone(?:\\s+|$)`);
|
||||
|
||||
const START_DATE = new Date();
|
||||
const TEMP_KEY = 'temporarymasterkey';
|
||||
const ECDH_STORAGE = {};
|
||||
let seed;
|
||||
|
||||
export default new class E2EE extends BuiltinModule {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.encryptNewMessages = true;
|
||||
this.ecdhDate = START_DATE;
|
||||
this.handlePublicKey = this.handlePublicKey.bind(this);
|
||||
}
|
||||
|
||||
async enabled(e) {
|
||||
try {
|
||||
const newMaster = await Modals.input('Open Database', 'Master Password', true).promise;
|
||||
this.setMaster(newMaster);
|
||||
this.patchDispatcher();
|
||||
this.patchMessageContent();
|
||||
const selector = '.' + WebpackModules.getClassName('channelTextArea', 'emojiButton');
|
||||
const cta = await ReactComponents.getComponent('ChannelTextArea', { selector });
|
||||
this.patchChannelTextArea(cta);
|
||||
this.patchChannelTextAreaSubmit(cta);
|
||||
cta.forceUpdateAll();
|
||||
} catch (err) {
|
||||
Settings.getSetting(...this.settingPath).value = false;
|
||||
Toasts.error('Invalid master password! E2EE Disabled');
|
||||
}
|
||||
}
|
||||
|
||||
async disabled(e) {
|
||||
Events.off('discord:MESSAGE_CREATE', this.handlePublicKey);
|
||||
for (const patch of Patcher.getPatchesByCaller('BD:E2EE')) patch.unpatch();
|
||||
const ctaComponent = await ReactComponents.getComponent('ChannelTextArea');
|
||||
ctaComponent.forceUpdateAll();
|
||||
}
|
||||
|
||||
setMaster(key) {
|
||||
seed = Security.randomBytes();
|
||||
const newMaster = Security.encrypt(seed, key);
|
||||
// TODO re-encrypt everything with new master
|
||||
return (this.master = newMaster);
|
||||
}
|
||||
|
||||
get settingPath() {
|
||||
return ['security', 'default', 'e2ee'];
|
||||
}
|
||||
|
||||
enabled(e) {
|
||||
|
||||
|
||||
get database() {
|
||||
return Settings.getSetting('security', 'e2eedb', 'e2ekvps').value;
|
||||
}
|
||||
|
||||
disabled(e) {
|
||||
encrypt(key, content, prefix = '') {
|
||||
if (!key) {
|
||||
// Encrypt something with master
|
||||
return Security.encrypt(Security.decrypt(seed, this.master), content);
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
// Get key for current channel and encrypt
|
||||
const haveKey = this.getKey(DiscordApi.currentChannel.id);
|
||||
if (!haveKey) return 'nokey';
|
||||
return Security.encrypt(Security.decrypt(seed, [this.master, haveKey]), key);
|
||||
}
|
||||
return prefix + Security.encrypt(key, content);
|
||||
}
|
||||
|
||||
decrypt(key, content, prefix = '') {
|
||||
return Security.decrypt(key, content, prefix);
|
||||
}
|
||||
|
||||
async createHmac(data) {
|
||||
const haveKey = this.getKey(DiscordApi.currentChannel.id);
|
||||
if (!haveKey) return null;
|
||||
return Security.createHmac(Security.decrypt(seed, [this.master, haveKey]), data);
|
||||
}
|
||||
|
||||
getKey(channelId) {
|
||||
const haveKey = this.database.find(kvp => kvp.value.key === channelId);
|
||||
if (!haveKey) return null;
|
||||
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 } });
|
||||
}
|
||||
|
||||
createKeyExchange(dmChannelID) {
|
||||
if (ECDH_STORAGE.hasOwnProperty(dmChannelID)) return null;
|
||||
ECDH_STORAGE[dmChannelID] = Security.createECDH();
|
||||
setTimeout(() => {
|
||||
if (ECDH_STORAGE.hasOwnProperty(dmChannelID)) {
|
||||
delete ECDH_STORAGE[dmChannelID];
|
||||
Toasts.error('Key exchange expired!');
|
||||
if (this.preExchangeState) this.encryptNewMessages = this.preExchangeState;
|
||||
this.preExchangeState = null;
|
||||
}
|
||||
}, 30000);
|
||||
return Security.generateECDHKeys(ECDH_STORAGE[dmChannelID]);
|
||||
}
|
||||
|
||||
publicKeyFor(dmChannelID) {
|
||||
return Security.getECDHPublicKey(ECDH_STORAGE[dmChannelID]);
|
||||
}
|
||||
|
||||
computeSecret(dmChannelID, otherKey) {
|
||||
try {
|
||||
const secret = Security.computeECDHSecret(ECDH_STORAGE[dmChannelID], otherKey);
|
||||
delete ECDH_STORAGE[dmChannelID];
|
||||
return Security.hash('sha384', secret, 'hex');
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Received exchange should also expire if not accepted in time
|
||||
async handlePublicKey(e) {
|
||||
if (!DiscordApi.currentChannel) return;
|
||||
if (DiscordApi.currentChannel.type !== 'DM') return;
|
||||
const { id, content, author, channelId } = e.args;
|
||||
if (author.id === DiscordApi.currentUser.id || channelId !== DiscordApi.currentChannel.id) return;
|
||||
|
||||
const [tagstart, begin, key, end, tagend] = content.split('\n');
|
||||
if (begin !== '-----BEGIN PUBLIC KEY-----' || end !== '-----END PUBLIC KEY-----') return;
|
||||
|
||||
try {
|
||||
await Modals.confirm('Key Exchange', `Key exchange request from: ${author.username}#${author.discriminator}`, 'Accept', 'Reject').promise;
|
||||
// We already sent our key
|
||||
if (!ECDH_STORAGE.hasOwnProperty(channelId)) {
|
||||
const publicKeyMessage = `\`\`\`\n-----BEGIN PUBLIC KEY-----\n${this.createKeyExchange(channelId)}\n-----END PUBLIC KEY-----\n\`\`\``;
|
||||
if (this.encryptNewMessages) this.encryptNewMessages = false;
|
||||
WebpackModules.getModuleByName('DraftActions').saveDraft(channelId, publicKeyMessage);
|
||||
}
|
||||
const secret = this.computeSecret(channelId, key);
|
||||
this.setKey(channelId, secret);
|
||||
Toasts.success('Key exchange complete!');
|
||||
if (this.preExchangeState) this.encryptNewMessages = this.preExchangeState;
|
||||
this.preExchangeState = null;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
|
||||
// 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))
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
const ImageWrapper = await ReactComponents.getComponent('ImageWrapper', { selector: '.' + WebpackModules.getClassName('imageWrapper') });
|
||||
MonkeyPatch('BD:E2EE', ImageWrapper.component.prototype).before('render', this.beforeRenderImageWrapper.bind(this));
|
||||
}
|
||||
|
||||
beforeRenderMessageContent(component) {
|
||||
if (!component.props || !component.props.message) return;
|
||||
|
||||
const key = this.getKey(component.props.message.channel_id);
|
||||
if (!key) return; // We don't have a key for this channel
|
||||
|
||||
const Message = WebpackModules.getModuleByPrototypes(['isMentioned']);
|
||||
const MessageParser = WebpackModules.getModuleByName('MessageParser');
|
||||
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;
|
||||
try {
|
||||
decrypt = Security.decrypt(seed, [this.master, key, component.props.message.content]);
|
||||
} catch (err) { return } // Ignore errors such as non empty
|
||||
|
||||
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);
|
||||
|
||||
// Create a new message to parse it properly
|
||||
const create = Message.create(message);
|
||||
if (!create.content || !create.contentParsed) return;
|
||||
|
||||
component.props.message.mentions = create.mentions;
|
||||
component.props.message.mentionRoles = create.mentionRoles;
|
||||
component.props.message.mentionEveryone = create.mentionEveryone;
|
||||
component.props.message.mentioned = create.mentioned;
|
||||
component.props.message.content = create.content;
|
||||
component.props.message.contentParsed = create.contentParsed;
|
||||
}
|
||||
|
||||
renderMessageContent(component, args, retVal) {
|
||||
if (!component.props.message.bd_encrypted) return;
|
||||
const buttons = Utils.findInReactTree(retVal, m => Array.isArray(m) && m[1].props && m[1].props.currentUserId);
|
||||
if (!buttons) return;
|
||||
try {
|
||||
buttons.unshift(VueInjector.createReactElement(E2EEMessageButton));
|
||||
} catch (err) {
|
||||
Logger.err('E2EE', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
beforeRenderImageWrapper(component, args, retVal) {
|
||||
if (!component.props || !component.props.src) return;
|
||||
if (component.props.decrypting) return;
|
||||
component.props.decrypting = true;
|
||||
|
||||
const src = component.props.original || component.props.src.split('?')[0];
|
||||
if (!src.includes('bde2ee')) return;
|
||||
component.props.className = 'bd-encryptedImage';
|
||||
|
||||
const haveKey = this.getKey(DiscordApi.currentChannel.id);
|
||||
if (!haveKey) return;
|
||||
|
||||
const cached = Cache.find('e2ee:images', item => item.src === src);
|
||||
if (cached) {
|
||||
if (cached.invalidKey) { // TODO If key has changed we should recheck all with invalid key
|
||||
component.props.className = 'bd-encryptedImage bd-encryptedImageBadKey';
|
||||
component.props.readyState = 'READY';
|
||||
return;
|
||||
}
|
||||
Logger.info('E2EE', 'Returning encrypted image from cache');
|
||||
try {
|
||||
const decrypt = Security.decrypt(seed, [this.master, haveKey, cached.image]);
|
||||
component.props.className = 'bd-decryptedImage';
|
||||
component.props.src = component.props.original = 'data:;base64,' + decrypt;
|
||||
} catch (err) { return } finally { component.props.readyState = 'READY' }
|
||||
return;
|
||||
}
|
||||
|
||||
component.props.readyState = 'LOADING';
|
||||
Logger.info('E2EE', 'Decrypting image: ' + src);
|
||||
request.get(src, { encoding: 'binary' }).then(res => {
|
||||
(async () => {
|
||||
const arr = new Uint8Array(new ArrayBuffer(res.length));
|
||||
for (let i = 0; i < res.length; i++) arr[i] = res.charCodeAt(i);
|
||||
|
||||
const aobindex = Utils.aobscan(arr, [73, 69, 78, 68]) + 8;
|
||||
const sliced = arr.slice(aobindex);
|
||||
const image = new TextDecoder().decode(sliced);
|
||||
|
||||
const hmac = image.slice(-64);
|
||||
const data = image.slice(0, -64);
|
||||
const validateHmac = await this.createHmac(data);
|
||||
if (hmac !== validateHmac) {
|
||||
Cache.push('e2ee:images', { src, invalidKey: true });
|
||||
if (component && component.props) {
|
||||
component.props.decrypting = false;
|
||||
component.forceUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Cache.push('e2ee:images', { src, image: data });
|
||||
|
||||
if (!component || !component.props) {
|
||||
Logger.warn('E2EE', 'Component seems to be gone');
|
||||
return;
|
||||
}
|
||||
|
||||
component.props.decrypting = false;
|
||||
component.forceUpdate();
|
||||
})();
|
||||
}).catch(err => {
|
||||
console.log('request error', err);
|
||||
});
|
||||
}
|
||||
|
||||
patchChannelTextArea(cta) {
|
||||
MonkeyPatch('BD:E2EE', cta.component.prototype).after('render', this.renderChannelTextArea);
|
||||
}
|
||||
|
||||
renderChannelTextArea(component, args, retVal) {
|
||||
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));
|
||||
}
|
||||
|
||||
patchChannelTextAreaSubmit(cta) {
|
||||
MonkeyPatch('BD:E2EE', cta.component.prototype).before('handleSubmit', this.handleChannelTextAreaSubmit.bind(this));
|
||||
}
|
||||
|
||||
handleChannelTextAreaSubmit(component, args, retVal) {
|
||||
const key = this.getKey(DiscordApi.currentChannel.id);
|
||||
if (!this.encryptNewMessages || !key) return;
|
||||
component.props.value = Security.encrypt(Security.decrypt(seed, [this.master, key]), component.props.value, '$:');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* BetterDiscord E2EE Component
|
||||
* 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.
|
||||
*/
|
||||
|
||||
<template>
|
||||
<div class="bd-e2eeTaContainer">
|
||||
<v-popover popoverClass="bd-popover bd-e2eePopover" placement="top">
|
||||
<div v-if="error" class="bd-e2eeTaBtn bd-e2eeLock bd-error">
|
||||
<MiLock v-tooltip="error" />
|
||||
</div>
|
||||
<div v-else-if="state === 'loading'" class="bd-e2eeTaBtn bd-e2eeLock bd-loading bd-warn">
|
||||
<MiLock v-tooltip="'Loading'" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="!E2EE.encryptNewMessages" class="bd-e2eeTaBtn bd-e2eeLock bd-warn">
|
||||
<MiLock v-tooltip="'New messages will not be encrypted.'" />
|
||||
</div>
|
||||
|
||||
<div v-else class="bd-e2eeTaBtn bd-e2eeLock bd-ok">
|
||||
<MiLock v-tooltip="'Ready!'" />
|
||||
</div>
|
||||
<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="DiscordApi.currentChannel.type === 'DM'"><MiPencil size="16" v-tooltip="'Begin Key Exchange'" /></div>
|
||||
</template>
|
||||
</v-popover>
|
||||
<div class="bd-taDivider"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
import fs from 'fs';
|
||||
import { Utils } from 'common';
|
||||
import { remote } from 'electron';
|
||||
import { E2EE } from 'builtin';
|
||||
import { DiscordApi, Security, WebpackModules } from 'modules';
|
||||
import { MiLock, MiPlus, MiImagePlus, MiPencil, MiRefresh } from '../ui/components/common/MaterialIcon';
|
||||
import { Toasts } from 'ui';
|
||||
|
||||
export default {
|
||||
components: { MiLock, MiPlus, MiImagePlus, MiPencil, MiRefresh },
|
||||
data() {
|
||||
return {
|
||||
E2EE,
|
||||
state: 'loading',
|
||||
error: null,
|
||||
DiscordApi
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async showUploadDialog() {
|
||||
const dialogResult = remote.dialog.showOpenDialog({ properties: ['openFile'] });
|
||||
if (!dialogResult) return;
|
||||
|
||||
const readFile = fs.readFileSync(dialogResult[0]);
|
||||
const FileActions = _bd.WebpackModules.getModuleByProps(["makeFile"]);
|
||||
const Uploader = _bd.WebpackModules.getModuleByProps(["instantBatchUpload"]);
|
||||
|
||||
const img = await Utils.getImageFromBuffer(readFile);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = img.height;
|
||||
canvas.width = img.width;
|
||||
const arrBuffer = await Utils.canvasToArrayBuffer(canvas);
|
||||
const encrypted = E2EE.encrypt(img.src.replace('data:;base64,', ''));
|
||||
const hmac = await E2EE.createHmac(encrypted);
|
||||
const encodedBytes = new TextEncoder().encode(encrypted + hmac);
|
||||
Uploader.upload(DiscordApi.currentChannel.id, FileActions.makeFile(new Uint8Array([...new Uint8Array(arrBuffer), ...encodedBytes]), 'bde2ee.png'));
|
||||
},
|
||||
toggleEncrypt() {
|
||||
const newState = !E2EE.encryptNewMessages;
|
||||
E2EE.encryptNewMessages = newState;
|
||||
if (!newState) {
|
||||
Toasts.warning('New messages will not be encrypted');
|
||||
return;
|
||||
}
|
||||
Toasts.success('New messages will be encrypted');
|
||||
},
|
||||
generatePublicKey() {
|
||||
const keyExchange = E2EE.createKeyExchange(DiscordApi.currentChannel.id);
|
||||
if (keyExchange === null) {
|
||||
Toasts.warning('Key exchange for channel already in progress!');
|
||||
return;
|
||||
}
|
||||
E2EE.preExchangeState = E2EE.encryptNewMessages;
|
||||
E2EE.encryptNewMessages = false; // Disable encrypting new messages so we won't encrypt public keys
|
||||
const publicKeyMessage = `\`\`\`\n-----BEGIN PUBLIC KEY-----\n${keyExchange}\n-----END PUBLIC KEY-----\n\`\`\``;
|
||||
WebpackModules.getModuleByName('DraftActions').saveDraft(DiscordApi.currentChannel.id, publicKeyMessage);
|
||||
Toasts.info('Key exchange started. Expires in 30 seconds');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!E2EE.master) {
|
||||
this.error = 'No master key set!';
|
||||
return;
|
||||
}
|
||||
const haveKey = E2EE.getKey(DiscordApi.currentChannel.id);
|
||||
if (!haveKey) {
|
||||
this.error = 'No key for channel!';
|
||||
return;
|
||||
}
|
||||
this.state = 'OK';
|
||||
this.error = null;
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* BetterDiscord E2EE Component
|
||||
* 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.
|
||||
*/
|
||||
|
||||
<template>
|
||||
<div class="bd-e2eeMessageButtonWrap">
|
||||
<div class="bd-e2eeMessageButton">
|
||||
<MiLock v-tooltip="'Encrypted'" :size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { MiLock } from '../ui/components/common/MaterialIcon';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MiLock
|
||||
},
|
||||
props: ['message']
|
||||
}
|
||||
</script>
|
|
@ -2,6 +2,7 @@ import { default as EmoteModule } from './EmoteModule';
|
|||
import { default as ReactDevtoolsModule } from './ReactDevtoolsModule';
|
||||
import { default as VueDevtoolsModule } from './VueDevToolsModule';
|
||||
import { default as TrackingProtection } from './TrackingProtection';
|
||||
import { default as E2EE } from './E2EE';
|
||||
|
||||
export default class {
|
||||
static initAll() {
|
||||
|
@ -9,5 +10,6 @@ export default class {
|
|||
ReactDevtoolsModule.init();
|
||||
VueDevtoolsModule.init();
|
||||
TrackingProtection.init();
|
||||
E2EE.init();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ export default new class VueDevtoolsModule extends BuiltinModule {
|
|||
}
|
||||
|
||||
devToolsOpened() {
|
||||
electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
|
||||
electron.remote.BrowserWindow.removeDevToolsExtension('Vue.js devtools');
|
||||
electron.webFrame.registerURLSchemeAsPrivileged('chrome-extension');
|
||||
try {
|
||||
const res = electron.remote.BrowserWindow.addDevToolsExtension(path.join(Globals.getPath('ext'), 'extensions', 'vdt'));
|
||||
|
|
|
@ -3,3 +3,4 @@ export { default as ReactDevtoolsModule } from './ReactDevtoolsModule';
|
|||
export { default as VueDevtoolsModule } from './VueDevToolsModule';
|
||||
export { default as TrackingProtection } from './TrackingProtection';
|
||||
export { default as BuiltinManager } from './Manager';
|
||||
export { default as E2EE } from './E2EE';
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
|
||||
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 } from 'modules';
|
||||
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';
|
||||
|
||||
|
@ -37,6 +37,7 @@ class BetterDiscord {
|
|||
EmoteModule,
|
||||
BdWebApi,
|
||||
Connectivity,
|
||||
Cache,
|
||||
Logger, ClientIPC, Utils,
|
||||
|
||||
plugins: PluginManager.localContent,
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* BetterDiscord Cache 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.
|
||||
*/
|
||||
const CACHE = [];
|
||||
|
||||
export default class Cache {
|
||||
|
||||
static get cache() {
|
||||
return CACHE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push something to cache
|
||||
* @param {String} where Cache identifier
|
||||
* @param {any} data Data to push
|
||||
*/
|
||||
static push(where, data) {
|
||||
if (!this.cache[where]) this.cache[where] = [];
|
||||
this.cache[where].push(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find something in cache
|
||||
* @param {String} where Cache identifier
|
||||
* @param {Function} what Find callback
|
||||
*/
|
||||
static find(where, what) {
|
||||
if (!this.cache[where]) this.cache[where] = [];
|
||||
return this.cache[where].find(what);
|
||||
}
|
||||
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
*/
|
||||
|
||||
import { WebpackModules } from './webpackmodules';
|
||||
import { MonkeyPatch } from './patcher';
|
||||
import Events from './events';
|
||||
import EventListener from './eventlistener';
|
||||
|
||||
|
@ -21,6 +22,7 @@ import * as SocketStructs from '../structs/socketstructs';
|
|||
export default class extends EventListener {
|
||||
|
||||
init() {
|
||||
this.ignoreMultiple = -1;
|
||||
this.hook();
|
||||
}
|
||||
|
||||
|
@ -35,15 +37,21 @@ export default class extends EventListener {
|
|||
}
|
||||
|
||||
hook() {
|
||||
const self = this;
|
||||
const Events = WebpackModules.getModuleByName('Events');
|
||||
|
||||
MonkeyPatch('BD:EVENTS', Events.prototype).after('emit', (obj, args, retVal) => {
|
||||
const eventId = args.length >= 3 ? args[2].id || -1 : -1;
|
||||
if (eventId === this.ignoreMultiple) return;
|
||||
this.ignoreMultiple = eventId;
|
||||
if (obj.webSocket) this.wsc = obj.webSocket;
|
||||
this.emit(...args);
|
||||
});
|
||||
/*
|
||||
const orig = Events.prototype.emit;
|
||||
Events.prototype.emit = function (...args) {
|
||||
orig.call(this, ...args);
|
||||
self.wsc = this;
|
||||
self.emit(...args);
|
||||
};
|
||||
};*/
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,3 +24,5 @@ export { default as EventHook } from './eventhook';
|
|||
export { default as DiscordApi, Modules as DiscordApiModules } from './discordapi';
|
||||
export { default as BdWebApi } from './bdwebapi';
|
||||
export { default as Connectivity } from './connectivity';
|
||||
export { default as Security } from './security';
|
||||
export { default as Cache } from './cache';
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* BetterDiscord Security 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.
|
||||
*/
|
||||
|
||||
import nodecrypto from 'node-crypto';
|
||||
import aes256 from 'aes256';
|
||||
|
||||
export default class Security {
|
||||
|
||||
static encrypt(key, content, prefix = '') {
|
||||
if (key instanceof Array || content instanceof Array) return this.deepEncrypt(key, content, prefix);
|
||||
return `${prefix}${aes256.encrypt(key, content)}`;
|
||||
}
|
||||
|
||||
static decrypt(key, content, prefix = '') {
|
||||
if (key instanceof Array || content instanceof Array) {
|
||||
return this.deepDecrypt(key, content, prefix);
|
||||
}
|
||||
return aes256.decrypt(key, content.replace(prefix, ''));
|
||||
}
|
||||
|
||||
static deepEncrypt(keys, content, prefix = '') {
|
||||
if (content && content instanceof Array) return this.deepEncryptContent(keys, content, prefix);
|
||||
let encrypt = null;
|
||||
for (const key of keys) {
|
||||
if (encrypt === null) encrypt = this.encrypt(key, content, prefix);
|
||||
else encrypt = this.encrypt(key, encrypt, prefix);
|
||||
}
|
||||
return encrypt;
|
||||
}
|
||||
|
||||
static deepEncryptContent(key, contents, prefix = '') {
|
||||
let encrypt = null;
|
||||
for (const content of contents) {
|
||||
if (encrypt === null) encrypt = this.encrypt(key, content, prefix);
|
||||
else encrypt = this.encrypt(encrypt, content, prefix);
|
||||
}
|
||||
return encrypt;
|
||||
}
|
||||
|
||||
static deepDecrypt(keys, content, prefix = '') {
|
||||
if (content && content instanceof Array) return this.deepDecryptContent(keys, content, prefix);
|
||||
let decrypt = null;
|
||||
for (const key of keys.reverse()) {
|
||||
if (decrypt === null) decrypt = this.decrypt(key, content, prefix);
|
||||
else decrypt = this.decrypt(key, decrypt, prefix);
|
||||
}
|
||||
return decrypt;
|
||||
}
|
||||
|
||||
static deepDecryptContent(key, contents, prefix = '') {
|
||||
let decrypt = null;
|
||||
for (const content of contents) {
|
||||
if (decrypt === null) decrypt = this.decrypt(key, content, prefix);
|
||||
else decrypt = this.decrypt(decrypt, content, prefix);
|
||||
}
|
||||
return decrypt;
|
||||
}
|
||||
|
||||
static randomBytes(length = 64, to = 'hex') {
|
||||
return nodecrypto.randomBytes(length).toString(to);
|
||||
}
|
||||
|
||||
static async createHmac(key, data, algorithm = 'sha256') {
|
||||
const hmac = nodecrypto.createHmac(algorithm, key);
|
||||
return new Promise((resolve, reject) => {
|
||||
hmac.on('readable', () => {
|
||||
const data = hmac.read();
|
||||
if (data) return resolve(data.toString('hex'));
|
||||
reject(null);
|
||||
});
|
||||
hmac.write(data);
|
||||
hmac.end();
|
||||
});
|
||||
}
|
||||
|
||||
static createECDH(curve = 'secp384r1') {
|
||||
return nodecrypto.createECDH(curve);
|
||||
}
|
||||
|
||||
static hash(algorithm, data, encoding) {
|
||||
const hash = nodecrypto.createHash(algorithm);
|
||||
hash.update(data);
|
||||
return hash.digest(encoding);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
}
|
|
@ -56,6 +56,7 @@ const KnownModules = {
|
|||
UserNameResolver: Filters.byProperties(['getName']),
|
||||
UserNoteStore: Filters.byProperties(['getNote']),
|
||||
UserNoteActions: Filters.byProperties(['updateNote']),
|
||||
DraftActions: Filters.byProperties(['changeDraft']),
|
||||
|
||||
/* Emoji Store and Utils */
|
||||
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
|
||||
|
|
|
@ -11,10 +11,11 @@
|
|||
import Kvp from './kvp';
|
||||
|
||||
export default class SecureKvpSetting extends Kvp {
|
||||
|
||||
/**
|
||||
* The value to use when the setting doesn't have a value.
|
||||
*/
|
||||
get defaultValue() {
|
||||
return { key: 'Key', value: '**********' };
|
||||
return { key: 'PlaceholderKey', value: '' };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,3 +129,17 @@
|
|||
transform: translate3d(4px, -4px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bd-pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
.bd-formCollection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
div:first-child {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.bd-collectionItem {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-top: 5px;
|
||||
|
||||
.bd-removeCollectionItem {
|
||||
width: 20px;
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-newCollectionItem {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-self: flex-end;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 2px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
.bd-e2eeTaContainer {
|
||||
display: flex;
|
||||
|
||||
.bd-e2eeTaBtn {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
opacity: .8;
|
||||
transition: opacity .2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.bd-e2eeLock {
|
||||
&.bd-error {
|
||||
fill: $colerr;
|
||||
}
|
||||
|
||||
&.bd-ok {
|
||||
fill: $colbdgreen;
|
||||
}
|
||||
|
||||
&.bd-warn {
|
||||
fill: $colwarn;
|
||||
animation: bd-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-taDivider {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
top: 10%;
|
||||
width: 1px;
|
||||
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 {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
top: 4px;
|
||||
|
||||
.bd-e2eeMdBtn {
|
||||
cursor: pointer;
|
||||
|
||||
&.bd-e2eeLock {
|
||||
&.bd-error {
|
||||
fill: $colerr;
|
||||
}
|
||||
|
||||
&.bd-ok {
|
||||
fill: $colbdgreen;
|
||||
}
|
||||
|
||||
&.bd-warn {
|
||||
fill: $colwarn;
|
||||
}
|
||||
|
||||
&.bd-loading {
|
||||
animation: bd-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-e2eeMessageButtonWrap {
|
||||
.bd-e2eeMessageButton {
|
||||
fill: #99aab5;
|
||||
margin-left: 6px;
|
||||
margin-right: -2px;
|
||||
opacity: .4;
|
||||
transition: opacity .2s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-e2eePopover {
|
||||
background: #484b51;
|
||||
margin: 0;
|
||||
margin-top: 15px;
|
||||
|
||||
.bd-ok svg {
|
||||
fill: $colbdgreen;
|
||||
}
|
||||
|
||||
.bd-warn svg {
|
||||
fill: $colwarn;
|
||||
}
|
||||
|
||||
.bd-popover-wrapper,
|
||||
.bd-popoverWrapper {
|
||||
.bd-popover-inner,
|
||||
.bd-popoverInner {
|
||||
display: flex;
|
||||
|
||||
> div:first-child {
|
||||
display: flex;
|
||||
|
||||
> div:not(:first-child) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-material-design-icon,
|
||||
.bd-materialDesignIcon {
|
||||
display: flex;
|
||||
fill: #7e8084;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
fill: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-popover-arrow,
|
||||
.bd-popoverArrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bd-encryptedImage::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
background: $colbdgreen;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: flex-start;
|
||||
// font-size: 1.2em;
|
||||
// font-weight: 700;
|
||||
// color: #2c2c2c;
|
||||
// line-height: 30px;
|
||||
background-image: $lockIcon;
|
||||
background-size: calc(100% / 2);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.bd-decryptedImage::before {
|
||||
content: "";
|
||||
background-image: $lockIcon;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
background-size: cover;
|
||||
background-color: $colbdgreen;
|
||||
background-repeat: no-repeat;
|
||||
border-radius: 100%;
|
||||
border: 2px solid $colbdgreen;
|
||||
top: 5px;
|
||||
left: 5px;
|
||||
opacity: .5;
|
||||
}
|
|
@ -8,3 +8,5 @@
|
|||
@import './updater.scss';
|
||||
@import './window-preferences';
|
||||
@import './kvp';
|
||||
@import './collection';
|
||||
@import './e2ee';
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
input[type="number"],
|
||||
input[type="password"] {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #b9bbbe;
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
@import './error-modal.scss';
|
||||
@import './settings-modal.scss';
|
||||
@import './permission-modal.scss';
|
||||
@import './input-modal.scss';
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.bd-inputModalBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
input {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
|
@ -3,5 +3,5 @@ $colbdgreen: #3ecc9c;
|
|||
$colbdblue: $colbdgreen;
|
||||
$colerr: #d84040;
|
||||
$colwarn: #faa61a;
|
||||
$colok: #43b581;
|
||||
$colok: $colbdgreen;
|
||||
$coldimwhite: #b9bbbe;
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -13,8 +13,8 @@
|
|||
<div slot="body" class="bd-modal-basic-body">{{ modal.text }}</div>
|
||||
<div slot="footer" class="bd-modal-controls">
|
||||
<div class="bd-flex-grow"></div>
|
||||
<div class="bd-button" @click="modal.close">Cancel</div>
|
||||
<div class="bd-button bd-ok" @click="() => { modal.confirm(); modal.close(); }">OK</div>
|
||||
<div class="bd-button" @click="modal.close">{{ modal.cancelText || 'Cancel' }}</div>
|
||||
<div class="bd-button bd-ok" @click="() => { modal.confirm(); modal.close(); }">{{ modal.confirmText || 'OK' }}</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* BetterDiscord Input Modal Component
|
||||
* 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.
|
||||
*/
|
||||
|
||||
<template>
|
||||
<Modal :class="['bd-modal-basic', {'bd-modal-out': modal.closing}]" :headerText="modal.title" @close="modal.close">
|
||||
<div slot="body" class="bd-modal-basic-body bd-inputModalBody bd-form-textinput">
|
||||
{{ modal.text }}
|
||||
<input v-if="modal.password" ref="input" type="password" @keyup.stop="keyup" />
|
||||
<input v-else ref="input" type="text" @keyup.stop="keyup"/>
|
||||
</div>
|
||||
<div slot="footer" class="bd-modal-controls">
|
||||
<div class="bd-flex-grow"></div>
|
||||
<div class="bd-button bd-ok" @click="() => { modal.confirm(value); modal.close(); }">OK</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// Imports
|
||||
import { Modal } from '../../common';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
props: ['modal'],
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
methods: {
|
||||
keyup(e) {
|
||||
if (e.key === 'Enter') {
|
||||
this.modal.confirm(this.value);
|
||||
this.modal.close();
|
||||
return;
|
||||
}
|
||||
this.value = e.target.value;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -22,11 +22,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import aes256 from 'aes256';
|
||||
import { DiscordApi } from 'modules';
|
||||
import { E2EE } from 'builtin';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
masterKey: 'temporarymasterkey',
|
||||
valueChanged: false
|
||||
}
|
||||
},
|
||||
|
@ -40,7 +41,7 @@
|
|||
},
|
||||
valueBlur(e) {
|
||||
if (!this.valueChanged) return;
|
||||
const value = aes256.encrypt(this.masterKey, e.target.value);
|
||||
const value = E2EE.encrypt(null, e.target.value);
|
||||
this.setting.value = { key: this.setting.value.key, value }
|
||||
this.valueChanged = false;
|
||||
},
|
||||
|
@ -52,6 +53,10 @@
|
|||
if (e.key !== 'Enter') return;
|
||||
e.target.blur();
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.setting.value.key !== 'PlaceholderKey') return;
|
||||
this.setting.value.key = DiscordApi.currentChannel.id || 'Key';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -18,3 +18,5 @@ export { default as MiInfo } from './materialicons/Info.vue';
|
|||
export { default as MiWarning } from './materialicons/Warning.vue';
|
||||
export { default as MiSuccess } from './materialicons/Success.vue';
|
||||
export { default as AccountCircle } from './materialicons/AccountCircle.vue';
|
||||
export { default as MiLock } from './materialicons/Lock.vue';
|
||||
export { default as MiImagePlus } from './materialicons/ImagePlus.vue';
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* BetterDiscord Image Plus Icon
|
||||
* 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.
|
||||
*
|
||||
* Material Design Icons
|
||||
* Copyright (c) 2014 Google
|
||||
* Apache 2.0 LICENSE
|
||||
* https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
*/
|
||||
|
||||
<template>
|
||||
<span class="bd-material-design-icon">
|
||||
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
|
||||
<path d="M 5,3C 3.89543,3 3,3.8954 3,5L 3,19C 3,20.1046 3.89543,21 5,21L 14.0859,21C 14.0294,20.6696 14.0007,20.3351 14,20C 14.0027,19.3182 14.1216,18.6418 14.3516,18L 5,18L 8.5,13.5L 11,16.5L 14.5,12L 16.7285,14.9727C 17.7019,14.3386 18.8383,14.0007 20,14C 20.3353,14.002 20.6698,14.032 21,14.0898L 21,5C 21,3.89 20.1,3 19,3L 5,3 Z M 19,16L 19,19L 16,19L 16,21L 19,21L 19,24L 21,24L 21,21L 24,21L 24,19L 21,19L 21,16L 19,16 Z " />
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['size']
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* BetterDiscord Material Design Icon
|
||||
* 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.
|
||||
*
|
||||
* Material Design Icons
|
||||
* Copyright (c) 2014 Google
|
||||
* Apache 2.0 LICENSE
|
||||
* https://www.apache.org/licenses/LICENSE-2.0.txt
|
||||
*/
|
||||
|
||||
<template>
|
||||
<span class="bd-material-design-icon">
|
||||
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
|
||||
<path fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 11.9994,16.998C 13.1044,16.998 13.9994,16.1021 13.9994,14.998C 13.9994,13.894 13.1044,12.998 11.9994,12.998C 10.8954,12.998 9.9994,13.894 9.9994,14.998C 9.9994,16.1021 10.8954,16.998 11.9994,16.998 Z M 17.9994,7.99813C 19.1034,7.99813 19.9994,8.89413 19.9994,9.99813L 19.9994,19.9981C 19.9994,21.1021 19.1034,21.9981 17.9994,21.9981L 5.99938,21.9981C 4.89539,21.9981 3.99938,21.1021 3.99938,19.9981L 3.99938,9.99813C 3.99938,8.89413 4.89539,7.99813 5.99938,7.99813L 6.99938,7.99813L 6.99938,5.99813C 6.99938,3.23714 9.23838,0.998133 11.9994,0.998133C 14.7604,0.998133 16.9994,3.23714 16.9994,5.99813L 16.9994,7.99813L 17.9994,7.99813 Z M 12,3C 10.3431,3 9,4.34315 9,6L 9,8L 15,8L 15,6C 15,4.34315 13.6569,3 12,3 Z " />
|
||||
</svg>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ['size']
|
||||
}
|
||||
</script>
|
|
@ -16,6 +16,7 @@ import ConfirmModal from './components/bd/modals/ConfirmModal.vue';
|
|||
import ErrorModal from './components/bd/modals/ErrorModal.vue';
|
||||
import SettingsModal from './components/bd/modals/SettingsModal.vue';
|
||||
import PermissionModal from './components/bd/modals/PermissionModal.vue';
|
||||
import InputModal from './components/bd/modals/InputModal.vue';
|
||||
|
||||
let modals = 0;
|
||||
|
||||
|
@ -163,12 +164,12 @@ export default class Modals {
|
|||
* @param {String} text A string that will be displayed in the modal body
|
||||
* @return {Modal}
|
||||
*/
|
||||
static confirm(title, text) {
|
||||
return this.add(this.createConfirmModal(title, text));
|
||||
static confirm(title, text, confirmText, cancelText) {
|
||||
return this.add(this.createConfirmModal(title, text, confirmText, cancelText));
|
||||
}
|
||||
|
||||
static createConfirmModal(title, text) {
|
||||
const modal = { title, text };
|
||||
static createConfirmModal(title, text, confirmText, cancelText) {
|
||||
const modal = { title, text, confirmText, cancelText };
|
||||
modal.promise = new Promise((resolve, reject) => {
|
||||
modal.confirm = () => resolve(true);
|
||||
modal.beforeClose = () => reject();
|
||||
|
@ -176,6 +177,19 @@ export default class Modals {
|
|||
return new Modal(modal, ConfirmModal);
|
||||
}
|
||||
|
||||
static input(title, text, password = false) {
|
||||
return this.add(this.createInputModal(title, text, password));
|
||||
}
|
||||
|
||||
static createInputModal(title, text, password = false) {
|
||||
const modal = { title, text, password };
|
||||
modal.promise = new Promise((resolve, reject) => {
|
||||
modal.confirm = value => resolve(value);
|
||||
modal.beforeClose = () => reject();
|
||||
});
|
||||
return new Modal(modal, InputModal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new permissions modal and adds it to the open stack.
|
||||
* The modal will have a promise property that will be set to a Promise object that is resolved or rejected if the user accepts the permissions or closes the modal.
|
||||
|
|
|
@ -38,7 +38,8 @@ module.exports = {
|
|||
process: 'require("process")',
|
||||
net: 'require("net")',
|
||||
request: 'require(require("path").join(require("electron").remote.app.getAppPath(), "node_modules", "request"))',
|
||||
sparkplug: 'require("../../core/dist/sparkplug")'
|
||||
sparkplug: 'require("../../core/dist/sparkplug")',
|
||||
'node-crypto': 'require("crypto")'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
|
@ -39,7 +39,8 @@ module.exports = {
|
|||
process: 'require("process")',
|
||||
net: 'require("net")',
|
||||
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: {
|
||||
|
|
|
@ -53,6 +53,45 @@ export class Utils {
|
|||
return camelCased;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a value, subobject, or array from a tree that matches a specific filter. Great for patching render functions.
|
||||
* @param {object} tree React tree to look through. Can be a rendered object or an internal instance.
|
||||
* @param {callable} searchFilter Filter function to check subobjects against.
|
||||
*/
|
||||
static findInReactTree(tree, searchFilter) {
|
||||
return this.findInTree(tree, searchFilter, {walkable: ['props', 'children', 'child', 'sibling']});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a value, subobject, or array from a tree that matches a specific filter.
|
||||
* @param {object} tree Tree that should be walked
|
||||
* @param {callable} searchFilter Filter to check against each object and subobject
|
||||
* @param {object} options Additional options to customize the search
|
||||
* @param {Array<string>|null} [options.walkable=null] Array of strings to use as keys that are allowed to be walked on. Null value indicates all keys are walkable
|
||||
* @param {Array<string>} [options.ignore=[]] Array of strings to use as keys to exclude from the search, most helpful when `walkable = null`.
|
||||
*/
|
||||
static findInTree(tree, searchFilter, {walkable = null, ignore = []}) {
|
||||
if (searchFilter(tree)) return tree;
|
||||
if (typeof tree !== "object" || tree == null) return undefined;
|
||||
|
||||
let tempReturn = undefined;
|
||||
if (tree instanceof Array) {
|
||||
for (let value of tree) {
|
||||
tempReturn = this.findInTree(value, searchFilter, {walkable, ignore});
|
||||
if (typeof tempReturn != "undefined") return tempReturn;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const toWalk = walkable == null ? Object.keys(tree) : walkable;
|
||||
for (let key of toWalk) {
|
||||
if (!tree.hasOwnProperty(key) || ignore.includes(key)) continue;
|
||||
tempReturn = this.findInTree(tree[key], searchFilter, {walkable, ignore});
|
||||
if (typeof tempReturn != "undefined") return tempReturn;
|
||||
}
|
||||
}
|
||||
return tempReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two or more values contain the same data.
|
||||
* @param {Any} ...value The value to compare
|
||||
|
@ -176,6 +215,69 @@ export class Utils {
|
|||
} while (!value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of array of bytes in another array
|
||||
* @param {Array} haystack The array to find aob in
|
||||
* @param {Array} needle The aob to find
|
||||
* @return {Number} aob index, -1 if not found
|
||||
*/
|
||||
static aobscan(haystack, needle) {
|
||||
for (let h = 0; h < haystack.length - needle.length + 1; ++h) {
|
||||
let found = true;
|
||||
for (let n = 0; n < needle.length; ++n) {
|
||||
if (needle[n] === null ||
|
||||
needle[n] === '??' ||
|
||||
haystack[h + n] === needle[n]) continue;
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
if (found) return h;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert buffer to base64 encoded string
|
||||
* @param {any} buffer buffer to convert
|
||||
* @returns {String} base64 encoded string from buffer
|
||||
*/
|
||||
static arrayBufferToBase64(buffer) {
|
||||
let binary = '';
|
||||
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
static async getImageFromBuffer(buffer) {
|
||||
if (!(buffer instanceof Blob)) buffer = new Blob([buffer]);
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(buffer);
|
||||
await new Promise(r => {
|
||||
reader.onload = r
|
||||
});
|
||||
const img = new Image();
|
||||
img.src = reader.result;
|
||||
return await new Promise(resolve => {
|
||||
img.onload = () => {
|
||||
resolve(img);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async canvasToArrayBuffer(canvas, mime = 'image/png') {
|
||||
const reader = new FileReader();
|
||||
return new Promise(resolve => {
|
||||
canvas.toBlob(blob => {
|
||||
reader.addEventListener('loadend', () => {
|
||||
resolve(reader.result);
|
||||
});
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, mime);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class FileUtils {
|
||||
|
|
|
@ -257,9 +257,11 @@ export class BetterDiscord {
|
|||
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||
for (let [header, values] of Object.entries(details.responseHeaders)) {
|
||||
if (!header.match(/^Content-Security-Policy(-Report-Only)?$/i)) continue;
|
||||
|
||||
details.responseHeaders[header] = values.map(value => {
|
||||
const policy = new ContentSecurityPolicy(value);
|
||||
for (const [key, value] of Object.entries(CSP)) {
|
||||
if (!policy.get(key)) continue;
|
||||
policy.add(key, value.join(' '));
|
||||
}
|
||||
return policy.toString();
|
||||
|
|
|
@ -4796,9 +4796,9 @@
|
|||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"asynckit": "0.4.0",
|
||||
"combined-stream": "1.0.5",
|
||||
"mime-types": "2.1.15"
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.5",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
|
@ -4901,8 +4901,8 @@
|
|||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ajv": "4.11.8",
|
||||
"har-schema": "1.0.5"
|
||||
"ajv": "^4.9.1",
|
||||
"har-schema": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"has-unicode": {
|
||||
|
@ -4971,7 +4971,7 @@
|
|||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"number-is-nan": "1.0.1"
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-typedarray": {
|
||||
|
@ -5076,7 +5076,7 @@
|
|||
"integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "1.27.0"
|
||||
"mime-db": "~1.27.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
|
@ -5085,7 +5085,7 @@
|
|||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "1.1.7"
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
|
@ -5137,8 +5137,8 @@
|
|||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"abbrev": "1.1.0",
|
||||
"osenv": "0.1.4"
|
||||
"abbrev": "1",
|
||||
"osenv": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"npmlog": {
|
||||
|
@ -5204,8 +5204,8 @@
|
|||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"os-homedir": "1.0.2",
|
||||
"os-tmpdir": "1.0.2"
|
||||
"os-homedir": "^1.0.0",
|
||||
"os-tmpdir": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
|
@ -13459,6 +13459,12 @@
|
|||
"tinycolor2": "1.4.1"
|
||||
}
|
||||
},
|
||||
"vue-context-menu": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-context-menu/-/vue-context-menu-2.0.6.tgz",
|
||||
"integrity": "sha512-wTyyjWbrGq/Rc1iivHl9ryI0IcsodRgg46CAGCm6knADDsTj5qJzo2pPDfi6y2nQreg8llJC2lGGts88tMX1sQ==",
|
||||
"dev": true
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz",
|
||||
|
|
|
@ -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