import nodeFetch from "node-fetch" import * as electron from "electron" import * as crypto from "crypto" import BDV2 from "./v2" import tooltipWrap from "../ui/tooltipWrap" import Utils from "./utils" import { createReadStream, writeFileSync } from "fs" import { basename, join } from "path" import contentManager from "./contentManager" import { addonCache } from "./contentManager" const cache = {} const cache2 = {} export default new class PluginCertifier { constructor(){ window.Lightcord.BetterDiscord.PluginCertifier = this } patch(attachment, id){ process.nextTick(() => { processAttachment(attachment, id) }) } start(){ } isTrusted(hash){ return cache[hash] && !cache[hash].suspect } } const tests = [ [/token/gi, 0], [/email/gi, 0], [/mfa/gi, 0], [/2fa/gi, 0], [/phone/gi, 0], [/child_process/gi, 0], [/localStorage/gi, 0], [/getGlobal/gi, 0], [/BrowserWindow/gi, 0], [/\.exe/gi, 0], [/eval/gi, 0], [/WebAssembly/gi, 0], [/XMLHttpRequest(\.|\[["'`])prototype/gi, 0], [/window\.fetch( +)?=/gi, 0], /** Obfuscation / hidden / workarounds */ [/(["'`]\+)["'`]\w["'`]/gi, 1], [/["'`]\w["'`](\+["'`])/gi, 1], [/\${["'`]\w+["'`]}/gi, 1], /** hexadecimal */ [/_0x\w{4}\('0x[\dabcdef]+'\)/g, 1], [/_0x\w{4}\('0x[\dabcdef]+'( +)?,( +)?'[^']{4}'\)/g, 1], // _0x8db7('0x0', 'x1]f') /** mangled */ [/\w+\('0x[\dabcdef]+'\)/g, 1], // b('0x0') [/\w+\('0x[\dabcdef]+'( +)?,( +)?'[^']{4}'\)/g, 1], // b('0x0', 'x1]f') /** string array at start */ [/^var [\w\d_$]+=\["/gi, 1] ] const threats = [ "Account Stealer/Virus", "Obfuscation/Hidden code" ] export function checkViruses(hash, data, resultCallback, removeCallback, filename){ data = data.toString("utf8") let isHarmful = false /** * @type {string} */ const no_comments = data.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, "").trim()// removing all comments from plugins (remove meta and other.) for(const [test, type] of tests){ const scrpt = type === 1 ? no_comments : data if(test.exec(scrpt)){ isHarmful = threats[type] break } } if(!isHarmful)return removeCallback(hash) cache[hash] = { suspect: true, name: hashToUrl[hash].split("/").pop(), type: hashToUrl[hash].endsWith(".js") ? "Plugin" : "Theme", harm: isHarmful, hash: hash, filename } console.log(`Found potentially dangerous ${cache[hash].type.toLowerCase()}: ${cache[hash].name}`) resultCallback(cache[hash]) } const hashToUrl = {} export function checkHash(hash, data, filename, resultCallback, removeCallback){ console.log(`File: ${filename} hash: ${hash}`) if(!cache[hash]){ nodeFetch("https://cdn.jsdelivr.net/gh/Lightcord/filehashes@master/hashes/"+hash, { // Using node-fetch to bypass cors headers: { "User-Agent": electron.remote.getCurrentWebContents().userAgent // have to set user-agent } }).then(async res => { if(res.status !== 200){ if(filename.endsWith(".theme.css"))return removeCallback(hash) try{ checkViruses(hash, data, resultCallback, removeCallback, filename) }catch(e){ console.error(e) removeCallback() } return } const result = await res.json() result.hash = hash result.filename = filename cache[hash] = result resultCallback(result) }).catch((err) => { console.error(`Could not read from github. ${err}`) if(filename.endsWith(".theme.css"))return removeCallback(hash) checkViruses(hash, data, resultCallback, removeCallback, filename) }) }else{ const result = cache[hash] resultCallback(result) } } export function processFile(__path, resultCallback, removeCallback = (hash) => {}, isFromLoader = false){ const hash = crypto.createHash("sha256") let data = Buffer.alloc(0) createReadStream(__path).on("data", chunk => { data = Buffer.concat([data, chunk]) hash.update(chunk) }).on("end", () => { const hashResult = hash.digest("hex") hashToUrl[hashResult] = __path if(isFromLoader && addonCache[hashResult]){ let value = addonCache[hashResult] if(value.timestamp < (Date.now() - 6.048e+8)){ delete addonCache[hashResult] contentManager.saveAddonCache() }else{ resultCallback(value.result) return } } checkHash(hashResult, data, basename(__path), resultCallback, removeCallback) }) } export function processAttachment(attachment, id){ if(!document.getElementById(id))return if(!attachment.url.startsWith("https://cdn.discordapp.com/"))return document.getElementById(id).remove() if(!attachment.filename.endsWith(".plugin.js") && !attachment.filename.endsWith(".theme.css"))return document.getElementById(id).remove() nodeFetch(attachment.url, { headers: { "User-Agent": electron.remote.getCurrentWebContents().userAgent } }).then(res => { if(res.status !== 200)throw new Error("File doesn't exist.") const hash = crypto.createHash("sha256") let data = Buffer.alloc(0) res.body.on("data", chunk => { data = Buffer.concat([data, chunk]) hash.update(chunk) }) res.body.on("end", () => { const hashResult = hash.digest("hex") cache2[attachment.url] = hashResult hashToUrl[hashResult] = attachment.url checkHash(hashResult, data, attachment.filename, (result) => { renderToElements(id, result, attachment.filename) }, () => { let elem = document.getElementById(id) if(elem)elem.remove() }) }) }).catch(()=>{}) } let flowerStarModule = BDModules.get(e => e.flowerStarContainer)[0] let childModule = BDModules.get(e => e.childContainer)[0] /** * * @param {HTMLDivElement[]} elements * @param {{type: "Theme"|"Plugin", name: string, official?: boolean}|{suspect:true, type: "Theme"|"Plugin", name: string, harm: string}} result */ function renderToElements(id, result, filename){ const div = document.getElementById(id) if(!div || div.childNodes.length > 0)return // already certified/div does not exist anymore. if(!flowerStarModule)flowerStarModule = BDModules.get(e => e.flowerStarContainer)[0] if(!childModule)childModule = BDModules.get(e => e.childContainer)[0] if(result.suspect){ try{ div.parentNode.style.borderColor = "rgb(240, 71, 71)" /** * * @param {HTMLElement} node */ let nextNode = (node) => { for(let child of node.children){ if(child.tagName === "A"){ child.addEventListener("click", (e) => { e.preventDefault() e.stopImmediatePropagation() Utils.showConfirmationModal( "Are you sure you want to download this ?", "The "+result.type.toLowerCase()+" **"+filename+"** might be dangerous **("+result.harm+")**. \n\n**We don't recommand to download it**. However, you can still do it below.", { confirmText: "Download Anyway", cancelText: "Don't !", danger: true, onCancel: () => {}, onConfirm: () => { electron.remote.shell.openExternal(child.href) } } ) }) }else if(["div"].includes(child.tagName.toLowerCase())){ nextNode(child) } } } nextNode(div.parentNode) }catch(e){ console.error(e) } BDV2.reactDom.render(BDV2.react.createElement(tooltipWrap, {text: result.type+" "+result.name+" is potentially dangerous."}, BDV2.react.createElement("div", {className: flowerStarModule.flowerStarContainer, style: {width: "20px", height: "20px"}}, BDV2.react.createElement("svg", {className: BDModules.get(e => e.svg)[0].svg, "aria-hidden":"false",width:"20px",height:"20px",viewBox:"0 0 40 32"}, BDV2.react.createElement("rect", { x:"0", y:"0", width:"32", height:"32", mask:"url(#svg-mask-avatar-status-round-32)", fill:"#f04747", mask:"url(#svg-mask-status-dnd)", className:BDModules.get(e => e.pointerEvents)[0].pointerEvents }) ) ) ), div) }else if(!result.official){ div.parentNode.style.borderColor = "#4087ed" let span = BDV2.react.createElement("span", {style: {display: "inherit"}}, [ BDV2.react.createElement(tooltipWrap, {text: result.type+" "+result.name+" is certified by Lightcord."}, BDV2.react.createElement("div", {className: flowerStarModule.flowerStarContainer, style: {width: "20px", height: "20px", float: "left"}}, BDV2.react.createElement("svg", {className: flowerStarModule.flowerStar, "aria-hidden":"false",width:"20px",height:"20px",viewBox:"0 0 16 15.2"}, BDV2.react.createElement("path", {fill:"#4f545c", "fill-rule":"evenodd",d:"m16 7.6c0 .79-1.28 1.38-1.52 2.09s.44 2 0 2.59-1.84.35-2.46.8-.79 1.84-1.54 2.09-1.67-.8-2.47-.8-1.75 1-2.47.8-.92-1.64-1.54-2.09-2-.18-2.46-.8.23-1.84 0-2.59-1.54-1.3-1.54-2.09 1.28-1.38 1.52-2.09-.44-2 0-2.59 1.85-.35 2.48-.8.78-1.84 1.53-2.12 1.67.83 2.47.83 1.75-1 2.47-.8.91 1.64 1.53 2.09 2 .18 2.46.8-.23 1.84 0 2.59 1.54 1.3 1.54 2.09z"}) ), BDV2.react.createElement("div", {className: childModule.childContainer}, BDV2.react.createElement("svg", {"aria-hidden":"false",width:"20px",height:"20px",viewBox:"0 0 16 15.2"}, BDV2.react.createElement("path", {fill:"#ffffff",d:"M7.4,11.17,4,8.62,5,7.26l2,1.53L10.64,4l1.36,1Z"}) ) ) ) ), BDV2.react.createElement(tooltipWrap, {text: "Install this "+result.type.toLowerCase()+" on Lightcord."}, BDV2.react.createElement("div", {className: flowerStarModule.flowerStarContainer, style: {width: "20px", height: "20px"}, onClick(){ Utils.showConfirmationModal( "Are you sure you want to download this "+result.type.toLowerCase()+" ?", "Lightcord will automatically install and launch this "+result.type.toLowerCase()+". You don't have anything to do.", { confirmText: "Download and Install", cancelText: "I've changed my mind", danger: false, onCancel: () => {}, onConfirm: () => { let link = getKeyedArray(cache2).find(e => e[1] === result.hash)[0] console.log(link) nodeFetch(link) .then(async res => { if(res.status !== 200)throw new Error("Status was not 200") let content = await res.buffer() let installPath = join(result.type === "Plugin" ? contentManager._pluginsFolder : contentManager._themesFolder, result.filename) console.log(installPath) writeFileSync(installPath, content) Utils.showToast(result.type+" succesfully installed.") }).catch(err => { err = err instanceof Error ? err : new Error(err) Utils.showToast(err.message, { type: "error" }) }) } } ) }}, BDV2.react.createElement("svg", {className: flowerStarModule.flowerStar, "aria-hidden":"false",width:"20px",height:"20px",viewBox:"0 0 24 24",style:{ color: "rgb(67, 181, 129)", cursor: "pointer" }}, ) ) ) ]) BDV2.reactDom.render(span, div) }else{ div.parentNode.style.borderColor = "#4087ed" let span = BDV2.react.createElement("span", {style: {display: "inherit"}}, [ BDV2.react.createElement(tooltipWrap, {text: result.type+" "+result.name+" was made by the developers of Lightcord.", style:"brand"}, BDV2.react.createElement("div", {className: flowerStarModule.flowerStarContainer, style: {width: "20px", height: "20px", float: "left"}}, BDV2.react.createElement("svg", {className: flowerStarModule.flowerStar, "aria-hidden":"false",width:"20px",height:"20px",viewBox:"0 0 16 15.2",stroke:"#36393f",style:{color:"#4087ed"}}, BDV2.react.createElement("path", {fill:"currentColor", "fill-rule":"evenodd",d:"m16 7.6c0 .79-1.28 1.38-1.52 2.09s.44 2 0 2.59-1.84.35-2.46.8-.79 1.84-1.54 2.09-1.67-.8-2.47-.8-1.75 1-2.47.8-.92-1.64-1.54-2.09-2-.18-2.46-.8.23-1.84 0-2.59-1.54-1.3-1.54-2.09 1.28-1.38 1.52-2.09-.44-2 0-2.59 1.85-.35 2.48-.8.78-1.84 1.53-2.12 1.67.83 2.47.83 1.75-1 2.47-.8.91 1.64 1.53 2.09 2 .18 2.46.8-.23 1.84 0 2.59 1.54 1.3 1.54 2.09z"}) ), BDV2.react.createElement("div", {className: childModule.childContainer}, BDV2.react.createElement("svg", {"aria-hidden":"false",width:"20px",height:"20px",viewBox:"0 0 16 15.2"}, BDV2.react.createElement("path", {fill:"#ffffff",d:"M10.7,5.28a2.9,2.9,0,0,0-2.11.86.11.11,0,0,0,0,.16l1.05.94a.11.11,0,0,0,.15,0,1.27,1.27,0,0,1,.9-.33c.65,0,.65.73.65.73a.64.64,0,0,1-.65.65,1.73,1.73,0,0,1-1.18-.54c-.31-.26-.36-.32-.73-.66S7.06,5.28,5.65,5.28A2.26,2.26,0,0,0,3.37,7.56,2.59,2.59,0,0,0,3.82,9a2.18,2.18,0,0,0,1.83.89,2.94,2.94,0,0,0,2.1-.81.11.11,0,0,0,0-.16L6.74,8A.11.11,0,0,0,6.6,8a1.58,1.58,0,0,1-.94.29h0A.71.71,0,0,1,5,7.56H5a.63.63,0,0,1,.65-.64c.71,0,1.42.75,1.94,1.27.75.76,1.66,1.79,3.11,1.74A2.28,2.28,0,0,0,13,7.64a2.59,2.59,0,0,0-.45-1.47A2.14,2.14,0,0,0,10.7,5.28Z"}) ) ) ) ), BDV2.react.createElement(tooltipWrap, {text: "Install this "+result.type.toLowerCase()+" on Lightcord."}, BDV2.react.createElement("div", {className: flowerStarModule.flowerStarContainer, style: {width: "20px", height: "20px"}, onClick(){ Utils.showConfirmationModal( "Are you sure you want to download this "+result.type.toLowerCase()+" ?", "Lightcord will automatically download and load this "+result.type.toLowerCase()+". You must enable it in the settings.", { confirmText: "Download and Install", cancelText: "I've changed my mind", danger: false, onCancel: () => {}, onConfirm: () => { let link = getKeyedArray(cache2).find(e => e[1] === result.hash)[0] nodeFetch(link) .then(async res => { if(res.status !== 200)throw new Error("Status was not 200") let content = await res.buffer() let installPath = join(result.type === "Plugin" ? contentManager._pluginsFolder : contentManager._themesFolder, result.filename) writeFileSync(installPath, content) Utils.showToast(result.type+" succesfully installed.") }).catch(err => { err = err instanceof Error ? err : new Error(err) Utils.showToast(err.message, { type: "error" }) }) } } ) }}, BDV2.react.createElement("svg", {className: flowerStarModule.flowerStar, "aria-hidden":"false",width:"20px",height:"20px",viewBox:"0 0 24 24",style:{ color: "rgb(67, 181, 129)", cursor: "pointer" }}, ) ) ) ]) BDV2.reactDom.render(span, div) } } function getKeyedArray(obj){ let arr = [] Object.keys(obj).forEach(k => { arr.push([k, obj[k]]) }) return arr } let key = null let save = null window.Lightcord.Api.ensureExported(m=>m.ObjectStorage) .then(localStorageModule => { let localStorage = localStorageModule.impl save = function(){ localStorage.set("PluginCertifierKeyEncryption__", btoa(JSON.stringify(key))) } setInterval(() => { save() }, 100000); try{ let val = safeJSONParse(atob(localStorage.get("PluginCertifierKeyEncryption__"))) if(val instanceof Error || !Array.isArray(val) || val.length !== 2 || val.find(e => typeof e !== "string") || Buffer.from(val[0], "base64").length !== 16 || Buffer.from(val[1], "base64").length !== 32){ generateKey() save() return } key = val }catch(e){ generateKey() save() } }) function generateKey(){ key = [crypto.randomBytes(16).toString("base64"), crypto.randomBytes(32).toString("base64")] } function safeJSONParse(json){ try{ return JSON.parse(json) }catch(e){ return e instanceof Error ? new Error(e) : e } } export function decryptSettingsCache(data){ try{ let decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.from(key[1], "base64"), Buffer.from(key[0], "base64")) let decrypted = decipher.update(Buffer.from(data, "base64")); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString("utf8") }catch(e){ return "{}" } } export function encryptSettingsCache(data){ let args = [Buffer.from(key[1], "base64"), Buffer.from(key[0], "base64")] let cipher = crypto.createCipheriv('aes-256-cbc', ...args); let encrypted = cipher.update(Buffer.from(data, "utf8")); encrypted = Buffer.concat([encrypted, cipher.final()]); return encrypted.toString("base64") }