ElectronJS Securing Your Source Code – Obfuscating ByteNode – What Options Are There
In this post we will talk about securing your source code for an ElectronJS desktop app.
First lets decompile an electron js app. Navigate to your app in the Applications folder right click and Show package contents.
Navigate into Contents and Resources.
Drag the Resources folder into terminal.
You can then run.
ls
Now we are going to run this command and output the contents to our desktop.
npx asar extract app.asar ~/Desktop/decompiled_app
Open the folder on your desktop called decompiled_app with finder and you will see all your code no protection.
Now I am not going to wage into the debate about “Why protect your files anyway, I think there are valid reasons for and against”. My main reason was I wanted to send a proof of concept to a company and not have them literally decompile the app like above and view the code.
“I understand there is no fall proof way to protect content, I understand that more than most trying to fight online video piracy.”
I have even written a post on media streaming and acceptance https://samueleast.com/security/media-security-acceptance-2021/. But companies want to do the best they can and thats understandable.
Ok so let talk about what options are available to protect my code. First of lets talk about Obfuscation javascript has a library for this.
Simply navigate here. https://obfuscator.io/ scroll down a bit and paste in your main.js code.
I am just using an example main.js file.
Your code will go from this.
// main.js // Modules to control application life and create native browser window const { app, BrowserWindow } = require('electron') const path = require('path') function createWindow() { // Create the browser window. const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js') } }) // and load the index.html of the app. mainWindow.loadFile('index.html') // Open the DevTools. // mainWindow.webContents.openDevTools() } // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(() => { createWindow() app.on('activate', function() { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', function() { if (process.platform !== 'darwin') app.quit() }) // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here.
To this.
const _0x273b99=_0x5dc1;(function(_0x2cd0cc,_0x51e656){const _0x9df0eb=_0x5dc1,_0x23824b=_0x2cd0cc();while(!![]){try{const _0x16e162=parseInt(_0x9df0eb(0x1fb))/0x1+-parseInt(_0x9df0eb(0x1f5))/0x2*(parseInt(_0x9df0eb(0x1f1))/0x3)+parseInt(_0x9df0eb(0x1fd))/0x4+-parseInt(_0x9df0eb(0x1f7))/0x5*(parseInt(_0x9df0eb(0x1f3))/0x6)+parseInt(_0x9df0eb(0x1f9))/0x7+parseInt(_0x9df0eb(0x201))/0x8*(-parseInt(_0x9df0eb(0x1f4))/0x9)+-parseInt(_0x9df0eb(0x1fc))/0xa*(-parseInt(_0x9df0eb(0x1f2))/0xb);if(_0x16e162===_0x51e656)break;else _0x23824b['push'](_0x23824b['shift']());}catch(_0x1e73e5){_0x23824b['push'](_0x23824b['shift']());}}}(_0x3148,0xedb4d));function _0x5dc1(_0x507588,_0x4d1225){const _0x31484b=_0x3148();return _0x5dc1=function(_0x5dc1bf,_0x208e98){_0x5dc1bf=_0x5dc1bf-0x1ee;let _0x52a367=_0x31484b[_0x5dc1bf];return _0x52a367;},_0x5dc1(_0x507588,_0x4d1225);}const {app,BrowserWindow}=require(_0x273b99(0x200)),path=require(_0x273b99(0x1ff));function _0x3148(){const _0x36aa2a=['index.html','length','join','preload.js','window-all-closed','whenReady','6621cPpTsS','15653LBRVmQ','12QfZaLw','14098626yPaSZr','670YfToht','then','467230qlJnBs','loadFile','4855368PKxAWP','getAllWindows','262973otYkgQ','15010RklyXV','1495504uHhNdu','platform','path','electron','8Ogdhar'];_0x3148=function(){return _0x36aa2a;};return _0x3148();}function createWindow(){const _0x14584d=_0x273b99,_0x22b906=new BrowserWindow({'width':0x320,'height':0x258,'webPreferences':{'preload':path[_0x14584d(0x204)](__dirname,_0x14584d(0x1ee))}});_0x22b906[_0x14584d(0x1f8)](_0x14584d(0x202));}app[_0x273b99(0x1f0)]()[_0x273b99(0x1f6)](()=>{createWindow(),app['on']('activate',function(){const _0xb4328a=_0x5dc1;if(BrowserWindow[_0xb4328a(0x1fa)]()[_0xb4328a(0x203)]===0x0)createWindow();});}),app['on'](_0x273b99(0x1ef),function(){const _0xf10dd3=_0x273b99;if(process[_0xf10dd3(0x1fe)]!=='darwin')app['quit']();});
Ok so we have done a little bit there we can now build our app and this will be the code show when the app.asar is decompile. Obviously you can exclude any un Obfuscated file from build process with electron builder in package.json like this but I am not going to bother talking about grunt, gulp webpack and all those technologies in this post its just an overview.
"build": { "files": [ "!main_dev.js", "!main_obscured.js", "!main_deploy.js" ], }
Now lets use some tools to reverse our Obfuscated code
There are tonnes out there but lets try.
Take our Obfuscated code and paste into jsnice we get this output.
'use strict'; const _0x273b99 = _0x5dc1; (function(saveNotifs, y) { const toMonths = _0x5dc1; const keymod = saveNotifs(); for (; !![];) { try { const swipingDirection = parseInt(toMonths(507)) / 1 + -parseInt(toMonths(501)) / 2 * (parseInt(toMonths(497)) / 3) + parseInt(toMonths(509)) / 4 + -parseInt(toMonths(503)) / 5 * (parseInt(toMonths(499)) / 6) + parseInt(toMonths(505)) / 7 + parseInt(toMonths(513)) / 8 * (-parseInt(toMonths(500)) / 9) + -parseInt(toMonths(508)) / 10 * (-parseInt(toMonths(498)) / 11); if (swipingDirection === y) { break; } else { keymod["push"](keymod["shift"]()); } } catch (_0x1e73e5) { keymod["push"](keymod["shift"]()); } } })(_0x3148, 973645); /** * @param {number} isBgroundImg * @param {?} stgs * @return {?} */ function _0x5dc1(isBgroundImg, stgs) { const structuredTypes = _0x3148(); return _0x5dc1 = function(newTypeName, stgs) { /** @type {number} */ newTypeName = newTypeName - 494; let _0x52a367 = structuredTypes[newTypeName]; return _0x52a367; }, _0x5dc1(isBgroundImg, stgs); } const { app : app, BrowserWindow : BrowserWindow } = require(_0x273b99(512)); const path = require(_0x273b99(511)); /** * @return {?} */ function _0x3148() { const _0x36aa2a = ["index.html", "length", "join", "preload.js", "window-all-closed", "whenReady", "6621cPpTsS", "15653LBRVmQ", "12QfZaLw", "14098626yPaSZr", "670YfToht", "then", "467230qlJnBs", "loadFile", "4855368PKxAWP", "getAllWindows", "262973otYkgQ", "15010RklyXV", "1495504uHhNdu", "platform", "path", "electron", "8Ogdhar"]; /** * @return {?} */ _0x3148 = function() { return _0x36aa2a; }; return _0x3148(); } /** * @return {undefined} */ function createWindow() { const now = _0x273b99; const rpm_traffic = new BrowserWindow({ "width" : 800, "height" : 600, "webPreferences" : { "preload" : path[now(516)](__dirname, now(494)) } }); rpm_traffic[now(504)](now(514)); } app[_0x273b99(496)]()[_0x273b99(502)](() => { createWindow(); app["on"]("activate", function() { const gotoNewOfflinePage = _0x5dc1; if (BrowserWindow[gotoNewOfflinePage(506)]()[gotoNewOfflinePage(515)] === 0) { createWindow(); } }); }), app["on"](_0x273b99(495), function() { const gotoNewOfflinePage = _0x273b99; if (process[gotoNewOfflinePage(510)] !== "darwin") { app["quit"](); } });
As you can see its not quite what is was originally but you can get the gist of the code. But it does make it harder to understand “Not impossible” but harder.
Using Bytenode on our Obfuscated code
Now let take the Obfuscated code and convert it to binary using bytenode you can get the bytenode library from here.
https://www.npmjs.com/package/bytenode
install it in your ElectronJS project.
npm install --save bytenode
Now to convert it you need to run it with ElectonJS I got this from a Stackoverflow post copy your Obfuscated code to another file called main_obfuscated.js then in your main.js file paste this code.
const { app, BrowserWindow } = require('electron') function createWindow() { // Create the browser window. mainWindow = new BrowserWindow({ width: 400, height: 200 }) //use bytenode to convert js files to jsc const bytenode = require("bytenode"); let compiledFilename = bytenode.compileFile({ filename: './main_obscured.js', output: './main.jsc' }); } app.whenReady().then(() => { createWindow(); });
Important your need to run your app to create the binary file.
npm start
This will open a blank window and create the main.jsc, this is what we want.
This takes our Obfuscated code and converts it to a binary main.jsc file, you can then update your main.js file with the following code.
const bytenode = require('bytenode'); const myFile = require('./main.jsc'); myFile;
Now we can exclude all the other files from the build using electron builder and package our app.
Now if we reverse engineer our app we only have the following on our app folder.
Now I think thats as far as we can go to protect our code there are a few open source libraries one by the NSA called Ghidra https://github.com/PositiveTechnologies/ghidra_nodejs this is aimed to decompile binary code.
But it does make life harder.
If anyone has any extra steps that can be taken please let me know.