2020-09-05 22:50:45 +02:00
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 = [
2020-09-14 11:38:34 +02:00
[ /token/gi , 0 , 0 ] ,
[ /email/gi , 0 , 0 ] ,
[ /mfa/gi , 0 , 0 ] ,
[ /2fa/gi , 0 , 0 ] ,
[ /phone/gi , 0 , 0 ] ,
[ /child_process/gi , 0 , 0 ] ,
[ /localStorage/gi , 0 , 0 ] ,
[ /getGlobal/gi , 0 , 0 ] ,
[ /BrowserWindow/gi , 0 , 0 ] ,
[ /\.exe/gi , 0 , 1 ] ,
[ /system32/gi , 0 , 0 ] ,
[ /password/gi , 0 , 0 ] ,
[ /eval/gi , 0 , 0 ] ,
[ /WebAssembly/gi , 0 , 0 ] ,
[ /XMLHttpRequest(\.|\[["'`])prototype/gi , 0 , 0 ] ,
[ /window\.fetch( +)?=/gi , 0 , 0 ] ,
2020-09-05 22:50:45 +02:00
/** Obfuscation / hidden / workarounds */
2020-09-14 11:38:34 +02:00
[ /(["'`]\+)["'`]\w["'`]/gi , 1 , 1 ] ,
[ /["'`]\w["'`](\+["'`])/gi , 1 , 1 ] ,
[ /\${["'`]\w+["'`]}/gi , 1 , 1 ] ,
2020-09-05 22:50:45 +02:00
/** hexadecimal */
2020-09-14 11:38:34 +02:00
[ /_0x\w{4}\('0x[\dabcdef]+'\)/g , 1 , 1 ] ,
[ /_0x\w{4}\('0x[\dabcdef]+'( +)?,( +)?'[^']{4}'\)/g , 1 , 1 ] , // _0x8db7('0x0', 'x1]f')
2020-09-05 22:50:45 +02:00
/** mangled */
2020-09-14 11:38:34 +02:00
[ /\w+\('0x[\dabcdef]+'\)/g , 1 , 1 ] , // b('0x0')
[ /\w+\('0x[\dabcdef]+'( +)?,( +)?'[^']{4}'\)/g , 1 , 1 ] , // b('0x0', 'x1]f')
2020-09-05 22:50:45 +02:00
/** string array at start */
2020-09-14 11:38:34 +02:00
[ /^var [\w\d_$]+=\["/gi , 1 , 1 ]
2020-09-05 22:50:45 +02:00
]
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.)
2020-09-14 11:38:34 +02:00
for ( const [ test , type , removeComment ] of tests ) {
const scrpt = removeComment === 1 ? no _comments : data
2020-09-05 22:50:45 +02:00
if ( test . exec ( scrpt ) ) {
isHarmful = threats [ type ]
2020-09-14 11:38:34 +02:00
console . log ( ` ${ hashToUrl [ hash ] . split ( "/" ) . pop ( ) } failed at test ` , test , ". Marked as" , threats [ type ] )
2020-09-05 22:50:45 +02:00
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 : {
2020-09-12 14:52:50 +02:00
"User-Agent" : electron . ipcRenderer . sendSync ( "LIGHTCORD_GET_USER_AGENT" ) // have to set user-agent
2020-09-05 22:50:45 +02:00
}
} ) . 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.048 e + 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 : {
2020-09-12 14:52:50 +02:00
"User-Agent" : electron . ipcRenderer . sendSync ( "LIGHTCORD_GET_USER_AGENT" )
2020-09-05 22:50:45 +02:00
}
} ) . 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 : ( ) => {
2020-09-12 14:52:50 +02:00
electron . ipcRenderer . sendSync ( "LIGHTCORD_OPEN_EXTERNAL" , child . href )
2020-09-05 22:50:45 +02:00
}
}
)
} )
} 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"
} } ,
< g fill = "none" fill - rule = "evenodd" >
< path d = "M0 0h24v24H0z" > < / p a t h >
< path class = "fill" fill = "currentColor" d = "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" > < / p a t h >
< / g >
)
)
)
] )
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"
} } ,
< g fill = "none" fill - rule = "evenodd" >
< path d = "M0 0h24v24H0z" > < / p a t h >
< path class = "fill" fill = "currentColor" d = "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" > < / p a t h >
< / g >
)
)
)
] )
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" )
2020-05-21 18:37:38 +02:00
}