Merge branch 'security' into security
This commit is contained in:
commit
81c451d31f
|
@ -8,33 +8,34 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { Settings } from 'modules';
|
||||
import { Settings, Cache } from 'modules';
|
||||
import BuiltinModule from './BuiltinModule';
|
||||
import { WebpackModules, ReactComponents, MonkeyPatch, Patcher, DiscordApi } from 'modules';
|
||||
import { WebpackModules, ReactComponents, MonkeyPatch, Patcher, DiscordApi, Security } from 'modules';
|
||||
import { VueInjector, Reflection } 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 aes256 from 'aes256';
|
||||
import crypto from 'crypto';
|
||||
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+|$)`);
|
||||
|
||||
let seed = Math.random().toString(36).replace(/[^a-z]+/g, '');
|
||||
const TEMP_KEY = 'temporarymasterkey';
|
||||
let seed;
|
||||
|
||||
export default new class E2EE extends BuiltinModule {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.master = this.encrypt(seed, 'temporarymasterkey');
|
||||
this.encryptNewMessages = true;
|
||||
}
|
||||
|
||||
setMaster(key) {
|
||||
seed = Math.random().toString(36).replace(/[^a-z]+/g, '');
|
||||
const newMaster = this.encrypt(seed, key);
|
||||
seed = Security.randomBytes();
|
||||
const newMaster = Security.encrypt(seed, key);
|
||||
// TODO re-encrypt everything with new master
|
||||
return (this.master = newMaster);
|
||||
}
|
||||
|
@ -49,11 +50,28 @@ export default new class E2EE extends BuiltinModule {
|
|||
}
|
||||
|
||||
encrypt(key, content, prefix = '') {
|
||||
return prefix + aes256.encrypt(key, content);
|
||||
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 aes256.decrypt(key, content.replace(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) {
|
||||
|
@ -73,6 +91,9 @@ export default new class E2EE extends BuiltinModule {
|
|||
}
|
||||
|
||||
async enabled(e) {
|
||||
seed = Security.randomBytes();
|
||||
// TODO Input modal for key
|
||||
this.master = Security.encrypt(seed, TEMP_KEY);
|
||||
this.patchDispatcher();
|
||||
this.patchMessageContent();
|
||||
const selector = '.' + WebpackModules.getClassName('channelTextArea', 'emojiButton');
|
||||
|
@ -119,6 +140,8 @@ export default new class E2EE extends BuiltinModule {
|
|||
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) {
|
||||
|
@ -137,7 +160,7 @@ export default new class E2EE extends BuiltinModule {
|
|||
if (!component.props.message.content.startsWith('$:')) return; // Not an encrypted string
|
||||
let decrypt;
|
||||
try {
|
||||
decrypt = this.decrypt(this.decrypt(this.decrypt(seed, this.master), key), component.props.message.content);
|
||||
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
|
||||
|
@ -166,10 +189,77 @@ export default new class E2EE extends BuiltinModule {
|
|||
|
||||
renderMessageContent(component, args, retVal) {
|
||||
if (!component.props.message.bd_encrypted) return;
|
||||
try {
|
||||
retVal.props.children[0].props.children.props.children.props.children.unshift(VueInjector.createReactElement(E2EEMessageButton));
|
||||
} catch (err) {
|
||||
Logger.err('E2EE', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
retVal.props.children[0].props.children.props.children.props.children.unshift(VueInjector.createReactElement(E2EEMessageButton, {
|
||||
message: component.props.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) {
|
||||
|
@ -215,7 +305,7 @@ export default new class E2EE extends BuiltinModule {
|
|||
handleChannelTextAreaSubmit(component, args, retVal) {
|
||||
const key = this.getKey(DiscordApi.currentChannel.id);
|
||||
if (!this.encryptNewMessages || !key) return;
|
||||
component.props.value = this.encrypt(this.decrypt(this.decrypt(seed, this.master), key), component.props.value, '$:');
|
||||
component.props.value = Security.encrypt(Security.decrypt(seed, [this.master, key]), component.props.value, '$:');
|
||||
}
|
||||
|
||||
async disabled(e) {
|
||||
|
|
|
@ -10,19 +10,26 @@
|
|||
|
||||
<template>
|
||||
<div class="bd-e2eeTaContainer" @contextmenu.prevent="channelType === '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" />
|
||||
</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" @click="E2EE.encryptNewMessages = true">
|
||||
|
||||
<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" @click="E2EE.encryptNewMessages = false">
|
||||
|
||||
<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>
|
||||
</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>
|
||||
|
@ -31,24 +38,66 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
import fs from 'fs';
|
||||
import { Utils } from 'common';
|
||||
import { remote } from 'electron';
|
||||
import { E2EE } from 'builtin';
|
||||
import { DiscordApi } from 'modules';
|
||||
import { MiLock } from '../ui/components/common/MaterialIcon';
|
||||
import { DiscordApi, Security } from 'modules';
|
||||
import { MiLock, MiPlus, MiImagePlus } from '../ui/components/common/MaterialIcon';
|
||||
import contextMenu from 'vue-context-menu';
|
||||
import { clipboard } from 'electron';
|
||||
import { Toasts } from 'ui';
|
||||
|
||||
function generatePublicKey() {
|
||||
export default {
|
||||
components: { MiLock, MiPlus, MiImagePlus },
|
||||
data() {
|
||||
return {
|
||||
E2EE,
|
||||
state: 'loading',
|
||||
error: null,
|
||||
channelType: DiscordApi.currentChannel.type
|
||||
};
|
||||
},
|
||||
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 dmChannelID = location.pathname.split("/")[3];
|
||||
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);
|
||||
}
|
||||
|
||||
function computeSharedSecret() {
|
||||
},
|
||||
computeSharedSecret() {
|
||||
try {
|
||||
const dmChannelID = location.pathname.split("/")[3];
|
||||
const chatInput = document.getElementsByClassName('da-textArea')[0];
|
||||
|
@ -64,18 +113,7 @@
|
|||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { MiLock, contextMenu },
|
||||
data() {
|
||||
return {
|
||||
E2EE,
|
||||
state: 'loading',
|
||||
error: null,
|
||||
channelType: DiscordApi.currentChannel.type
|
||||
};
|
||||
},
|
||||
methods: { generatePublicKey, computeSharedSecret },
|
||||
mounted() {
|
||||
if (!E2EE.master) {
|
||||
this.error = 'No master key set!';
|
||||
|
|
|
@ -93,6 +93,13 @@
|
|||
"hint": "When this is enabled you can use Ctrl/Cmd + B to open the BetterDiscord settings menu.",
|
||||
"value": false,
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"id": "enable-toasts",
|
||||
"type": "bool",
|
||||
"text": "Enable Toasts",
|
||||
"hint": "Allows plugins to show toasts.",
|
||||
"value": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -144,7 +151,7 @@
|
|||
{
|
||||
"id": "security",
|
||||
"text": "Security and Privacy",
|
||||
"headertext": "Security Settings",
|
||||
"headertext": "Security and Privacy Settings",
|
||||
"settings": [
|
||||
{
|
||||
"id": "default",
|
||||
|
|
|
@ -10,7 +10,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 } 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, E2EE } from 'builtin';
|
||||
import electron from 'electron';
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -25,3 +25,4 @@ export { default as DiscordApi, Modules as DiscordApiModules } from './discordap
|
|||
export { default as BdWebApi } from './bdwebapi';
|
||||
export { default as Connectivity } from './connectivity';
|
||||
export { default as Security } from './security';
|
||||
export { default as Cache } from './cache';
|
||||
|
|
|
@ -330,7 +330,8 @@ export default class PluginApi {
|
|||
success: this.showSuccessToast.bind(this),
|
||||
error: this.showErrorToast.bind(this),
|
||||
info: this.showInfoToast.bind(this),
|
||||
warning: this.showWarningToast.bind(this)
|
||||
warning: this.showWarningToast.bind(this),
|
||||
get enabled() { return Toasts.enabled }
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,21 +8,25 @@
|
|||
* 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) return this.deepEncrypt(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) return this.deepDecrypt(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);
|
||||
|
@ -31,7 +35,17 @@ export default class Security {
|
|||
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);
|
||||
|
@ -40,4 +54,30 @@ export default class Security {
|
|||
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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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: '' };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,11 +44,14 @@
|
|||
}
|
||||
|
||||
&.bd-hide-button {
|
||||
transition: opacity 0.4s ease-out;
|
||||
opacity: 0;
|
||||
animation: bd-fade-out 0.4s ease-out;
|
||||
|
||||
&.bd-active {
|
||||
transition-timing-function: ease-in;
|
||||
animation: bd-fade-in 0.4s ease-in;
|
||||
}
|
||||
|
||||
&:not(.bd-active):not(.bd-animating) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
&.bd-e2eeLock {
|
||||
&.bd-error {
|
||||
fill: #cc3e3e;
|
||||
fill: $colerr;
|
||||
}
|
||||
|
||||
&.bd-ok {
|
||||
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
|
||||
&.bd-warn {
|
||||
fill: #ccab3e;
|
||||
fill: $colwarn;
|
||||
animation: bd-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +75,7 @@
|
|||
|
||||
&.bd-e2eeLock {
|
||||
&.bd-error {
|
||||
fill: #cc3e3e;
|
||||
fill: $colerr;
|
||||
}
|
||||
|
||||
&.bd-ok {
|
||||
|
@ -83,7 +83,7 @@
|
|||
}
|
||||
|
||||
&.bd-warn {
|
||||
fill: #ccab3e;
|
||||
fill: $colwarn;
|
||||
}
|
||||
|
||||
&.bd-loading {
|
||||
|
@ -106,3 +106,87 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
.bd-formCollection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> :first-child {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.bd-collectionItem {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-top: 5px;
|
||||
|
||||
> :first-child {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.bd-removeCollectionItem {
|
||||
width: 20px;
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
&:not(.bd-disabled):hover {
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.bd-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bd-newCollectionItem {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-self: flex-end;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 2px;
|
||||
margin-top: 10px;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: #ccc;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,3 +9,4 @@
|
|||
@import './files.scss';
|
||||
@import './guilds.scss';
|
||||
@import './arrays.scss';
|
||||
@import './collections.scss';
|
||||
|
|
|
@ -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
|
@ -65,8 +65,7 @@ export default class ClassNormaliser extends Module {
|
|||
|
||||
normalizeElement(element) {
|
||||
if (!(element instanceof Element)) return;
|
||||
if (element.children && element.children.length) this.normalizeElement(element.children[0]);
|
||||
if (element.nextElementSibling) this.normalizeElement(element.nextElementSibling);
|
||||
|
||||
const classes = element.classList;
|
||||
for (let c = 0, clen = classes.length; c < clen; c++) {
|
||||
if (!randClass.test(classes[c])) continue;
|
||||
|
@ -74,6 +73,10 @@ export default class ClassNormaliser extends Module {
|
|||
const newClass = match.split('-').map((s, i) => i ? s[0].toUpperCase() + s.slice(1) : s).join('');
|
||||
element.classList.add(`${normalizedPrefix}-${newClass}`);
|
||||
}
|
||||
|
||||
for (let child of element.children) {
|
||||
this.normalizeElement(child);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
methods: {
|
||||
keyupListener(e) {
|
||||
if (Modals.stack.length || !this.active || e.which !== 27) return;
|
||||
if (this.$refs.settings.activeIndex !== -1) this.$refs.settings.closeContent();
|
||||
if (this.$refs.settings.item) this.$refs.settings.closeContent();
|
||||
else this.active = false;
|
||||
e.stopImmediatePropagation();
|
||||
},
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
<template>
|
||||
<div class="bd-formCollection">
|
||||
<div v-for="s in setting.items" class="bd-collectionItem">
|
||||
<Setting :setting="s" :key="s.id" />
|
||||
<div class="bd-removeCollectionItem" @click="removeItem(s)"><MiMinus/></div>
|
||||
<Setting :setting="s" :hide-divider="true" :key="s.id" />
|
||||
<div class="bd-removeCollectionItem" :class="{'bd-disabled': setting.disabled || setting.min && setting.items.length <= setting.min}" @click="removeItem(s)"><MiMinus/></div>
|
||||
</div>
|
||||
<div class="bd-newCollectionItem" @click="addItem"><MiPlus/></div>
|
||||
<div v-if="!setting.disabled && !setting.max || setting.items.length < setting.max" class="bd-newCollectionItem" @click="addItem"><MiPlus/></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -29,9 +29,11 @@
|
|||
},
|
||||
methods: {
|
||||
removeItem(item) {
|
||||
if (this.setting.disabled || this.setting.min && this.setting.items.length <= this.setting.min) return;
|
||||
this.setting.removeItem(item);
|
||||
},
|
||||
addItem() {
|
||||
if (this.setting.disabled || this.setting.max && this.setting.items.length >= this.setting.max) return;
|
||||
this.setting.addItem();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
<KeyValuePair v-else-if="setting.type === 'kvp'" :setting="setting" />
|
||||
<SecureKeyValuePair v-else-if="setting.type === 'securekvp'" :setting="setting" />
|
||||
<CustomSetting v-else-if="setting.type === 'custom'" :setting="setting" />
|
||||
<div class="bd-form-divider"></div>
|
||||
|
||||
<div v-if="!hideDivider" class="bd-form-divider"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -51,7 +52,8 @@
|
|||
|
||||
export default {
|
||||
props: [
|
||||
'setting'
|
||||
'setting',
|
||||
'hide-divider'
|
||||
],
|
||||
components: {
|
||||
BoolSetting,
|
||||
|
|
|
@ -19,3 +19,4 @@ 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>
|
|
@ -8,6 +8,8 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import { Settings } from 'modules';
|
||||
|
||||
let toasts = 0;
|
||||
|
||||
export default class Toasts {
|
||||
|
@ -24,6 +26,8 @@ export default class Toasts {
|
|||
* @returns {Promise} This promise resolves when the toast is removed from the DOM.
|
||||
*/
|
||||
static async push(message, options = {}) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const {type = 'basic', icon, additionalClasses, timeout = 3000} = options;
|
||||
const toast = {id: toasts++, message, type, icon, additionalClasses, closing: false};
|
||||
this.stack.push(toast);
|
||||
|
@ -72,4 +76,16 @@ export default class Toasts {
|
|||
return this._stack || (this._stack = []);
|
||||
}
|
||||
|
||||
static get setting() {
|
||||
return Settings.getSetting('ui', 'default', 'enable-toasts');
|
||||
}
|
||||
|
||||
static get enabled() {
|
||||
return this.setting.value;
|
||||
}
|
||||
|
||||
static set enabled(enabled) {
|
||||
this.setting.value = enabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ module.exports = {
|
|||
crypto: 'require("crypto")',
|
||||
request: 'require(require("path").join(require("electron").remote.app.getAppPath(), "node_modules", "request"))',
|
||||
sparkplug: 'require("../../core/dist/sparkplug")',
|
||||
crypto: 'require("crypto")'
|
||||
'node-crypto': 'require("crypto")'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
|
|
@ -176,6 +176,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();
|
||||
|
|
|
@ -81,6 +81,8 @@ gulp.task('node-sass-bindings', function () {
|
|||
]);
|
||||
});
|
||||
|
||||
gulp.task('build-release', gulp.parallel('release-package', 'client', 'core', 'sparkplug', 'core-modules', 'index', 'css-editor', gulp.series('dependencies', 'node-sass-bindings')));
|
||||
|
||||
gulp.task('release', gulp.series(function () {
|
||||
return del(['release/**/*']);
|
||||
}, gulp.parallel('release-package', 'client', 'core', 'sparkplug', 'core-modules', 'index', 'css-editor', gulp.series('dependencies', 'node-sass-bindings'))));
|
||||
}, 'build-release'));
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
"build_node-sass": "node scripts/build-node-sass.js",
|
||||
"build_release": "npm run release --prefix client && npm run build --prefix core && npm run release --prefix csseditor",
|
||||
"package_release": "node scripts/package-release.js",
|
||||
"release": "npm run lint && npm run build_release && gulp release && npm run package_release"
|
||||
"release": "npm run lint && npm run build_release && gulp release && npm run package_release",
|
||||
"update_release": "npm run build_release && gulp build-release"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue