Another serious issue of electron is Security of your Code. By using electron, you need to know your codes are always naked. If your application needs a key activation system, it can be cracked in less than one minute, all you need to do is get ASAR plugin for 7-Zip and edit whatever you want...
Background
Nowadays, everybody is dealing with HTML5 + CSS3 in their lives. Many of us visit hundreds of web pages and use many web apps every day. Making an app using HTML5 is very easy and fun. There are so many components and themes available online to speed up the workflow. In front-end development, HTML5 now is one of the most favorites since it's flexible and can be shipped as cross-platform.
There are many framework based projects out there which help us to start making our application in no time.
One of the famous and popular ones is Electron. Electron combines Chromium, Node.js, V8 JavaScript Virtual Machine to deliver a powerful tool to the developers. Many famous projects and services are using electron as their framework. Discord, Typora, Medal.TV, Visual Studio Code and Unity Hub are made in electron using HTML5 and CSS3, Pretty awesome stuff, right?
However, Electron has many issues which can be problematic in production level. For example, I personally hate to ship a 250+ MB app just for a small application like an installer or online service, You can fix this issue by replacing chromium in Electron with native OS web browser.
Another serious issue of electron is Security of your Code. By using electron, you need to know Your codes are always naked. If your application needs a key activation system, it can be cracked in less than one minute. All you need to do is getting ASAR plugin for 7-Zip and edit whatever you want.
Overview
So I had this tiny idea. What if I put critical parts of my code in a C++ module and do all the logic there instead of using JavaScript and use electron as my front-end only. Looks like a good idea, no? The problem is electron doesn't support custom DLLs and modules natively. To calling a native DLL from electron, you need to use node-ffi library which, well... did not get any updates after 2018 and building it for new Node.js is a pain in the ass. And the way you need to use it in electron is just dirty, I did not like it, I wanted a Better, Easier and Clean solution and I made it.
Now it's time to teach it to you real quick in this small article. Get Ready!
Note
This method doesn't make your application uncrackable, It just adds extra protection layers which make it harder to analyze and reverse engineering.
First Things First
Before we start, let's prepare everything we need for this article. Here's what we need:
Note
We need Node.js to use Electron-Forge
and build our electron with custom icon and file version, I do not cover it in this article, You can read the guide here.
After you got them all, Extract electron into a folder, open resources\default_app.asar with 7-Zip and extract the content to resources\app. This is for testing our code after you are done. Simply package it to default_app.asar again.
Internal Module
First thing we need to do is create our internal module which does all the critical things we want, like:
- Encryption/Decryption
- Communicating with Server in Special Ways
- Ciphering Messages
- Holding the Content of Pages*
Spoiler Alert
In my next article, I will show you how to make your own secured, optimized database, file archive and binary serializer, You can use it to make your own data archive of html pages and set the electron content directly from your C++ module and encrypted archive. You can also use my previous articles to pack the internal module by your own PE packer.
Internal module is the core of your app logic. You can use rust or other native languages to make it, but I prefer C++.
-
Create a new C++ project in Visual Studio, Set the build mode to Dynamic Library (.dll) and add this following code:
#include <Windows.h>
#include <string>
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID reserved)
{
if (reason == DLL_PROCESS_ATTACH)
{
MessageBoxA(0, "Hello from Electron!", 0, 0);
}
return TRUE;
}
extern "C" _declspec(dllexport) void _Proxy() {}
Build it and now you have your electron_x64.dll. Now we patch our electron EXE file and add a proxy import to its Import Address Table (IAT). This will cause electron to load our DLL after it gets launched.
-
Open electron.exe with CFF Explorer. Click on No at 40MB limit message.
-
Head to Import Adder tab and click on Add, select electron_x64.dll, select _Proxy
function, click on Import By Name, check Create New Section and finally click on Rebuild Import Table.
-
Save EXE and run electron. You will see multiply message boxes show up. This is because electron uses multi process model and each process instance is for a special task like rendering, communication, etc.
-
To fix this, we simply do a check in our DLL_PROCESS_ATTACH
by comparing command line data:
std::string cmd = GetCommandLineA();
char pathBuffer[MAX_PATH];
GetModuleFileNameA(0, pathBuffer, sizeof pathBuffer);
std::string moduleName(pathBuffer);
moduleName.insert(moduleName.begin(), '"');
moduleName += '"';
if(cmd[cmd.size()-1] == ' ') cmd = cmd.substr(0, cmd.size() - 1);
if (cmd == moduleName)
{
MessageBoxA(0, "Hello from Electron!", 0, 0);
}
Now run electron and you will see our message box only shows once from main instance. We're done. Now we are officially inside electron process!
Open World Communication
To communicate between electron and our native internal module, we will use a WebSocket/HTTP connection. It doesn't need to be secured as it only send commands to internal module and retrieves the result.
As we want to keep it clean and don't get dirty, we need a very tiny small WebSocket
server made in C, no SSL, not a single extra thing. Where can we find such a thing? Well... I did take a long search and found this gold.
WebSockets don't support response on demand. It means when you send something to server, Server doesn't return data back to you, Due to this reason, we use a Custom Request Model. We simply make a list of requests with unique ID. We include it in both Request
and Response
. Then execute the data we retrieved. Alternatively, you can use a simple and small HTTP server like this great and lightweight library. By using HTTP, you can send requests and retrieve the data on demand.
Clone wsServer
repo and add it to your internal module.
Add the following Events in your DLL code
#include "wsSrv/ws.h"
void OnClientConnect(ws_cli_conn_t* client)
{
char* cli = ws_getaddress(client);
printf("[LOG] Client Connected, Endpoint Address: %s\n", cli);
}
void OnClientDisconnect(ws_cli_conn_t* client)
{
char* cli = ws_getaddress(client);
printf("[LOG] Client Disconnected, Endpoint Address: %s\n", cli);
}
void OnClientRequest(ws_cli_conn_t* client, const unsigned char* msg,
uint64_t size, int type)
{
char* cli = ws_getaddress(client);
printf("[LOG] Client Sent Request, Endpoint Address: %s\n", cli);
if (type == 1) {
std::string msgstr((char*)msg, size);
if (msgstr == "(ON_STARTUP)")
{
ws_sendframe_txt(client, "(LOAD_SPLASH_PAGE)");
return;
}
}
}
You also need pthread
for windows. Simply grab it from this repo and build it as MT/Static library.
Link your DLL against ws2_32.lib and pthreadVSE3.lib, then add this function for server creation:
void StartWebsocketServer()
{
Sleep(200);
struct ws_events evs;
evs.onopen = &OnClientConnect;
evs.onclose = &OnClientDisconnect;
evs.onmessage = &OnClientRequest;
ws_socket(&evs, 5995, 0, 1000); }
Now head to your Dllmain
, apply the following changes:
if (cmd == moduleName)
{
::SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
::SetProcessDPIAware();
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)StartWebsocketServer,
NULL, NULL, NULL);
}
All done. Now when electron launches your internal module creates a tiny websocket server and wait for connections.
Note
You may need to add _CRT_SECURE_NO_WARNINGS
and PTW32_STATIC_LIB
to your Preprocessor Definition
The Mainframe
To be able to load your web pages dynamically, you need a mainframe. This mainframe can be used as a proxy page to overwrite the entire HTML to it or can contain a host element which can be used to write new HTML code inside it. In this article, we overwrite the entire page.
Head to app folder of electron data and open index.html in a text editor/html editor and write the following HTML code. I use Visual Studio Code.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>My Application Name</title>
<meta content="width=device-width, initial-scale=1.0,
shrink-to-fit=no" name="viewport">
<meta name="keywords" content="application, webapp, html5, css3, cpp">
<meta name="description" content="My Application Mainframe">
<meta itemprop="name" content="Mainframe">
<meta itemprop="description" content="My Application Mainframe">
<link rel="stylesheet" href="baseStyle.css">
<script src="internal.js"></script>
</head>
<body class="mainframe-body" onload="StartupEvent();">
<div id="mainframe">
<div id="mainframe-host"></div>
</div>
</body>
</html>
Now we have an empty page to load content into. If all of your pages use same style and layout, you only need to load new content from internal module inside mainframe-host
division.
Create baseStyle.css file in app folder and open it in editor. Write the following style code:
.mainframe-body
{
-webkit-user-select: none; user-select: none;
}
#mainframe
{
position: fixed;
top: 0; left: 0; bottom: 0; right: 0;
overflow: auto; background-color: transparent;
}
#mainframe-host
{
width: 100%;
height: 100%;
}
Control Center
Now, it's time to create our cross-page script which will be loaded by mainframe page and it creates a websocket connection to our internal server for control our front-end. Create internal.js and write the following code inside it:
var socket;
var connected = false;
function StartupEvent()
{
socket = new WebSocket("ws://localhost:5995");
socket.onopen = function(e)
{
socket.send("(ON_STARTUP)");
connected = true;
};
socket.onmessage = function(event)
{
if(event.data == "(LOAD_SPLASH_PAGE)")
{
fetch('./splash.html').then(response => response.text()).then(text =>
{
document.open(); document.write(text); document.close();
})
}
};
socket.onclose = function(event)
{
connected = false;
};
socket.onerror = function(error)
{
connected = false;
};
}
function DisconnectWS()
{
if (connected) socket.close();
}
Alright, create your splash.html and run the electron and Here we go... Now we have a connection to our internal module:
Note
You shouldn't use href
to go to a new page, If you do it, internal.js context gets destroyed as the new page opens as new process. You can do it but you need to connect to WebSocket again, however it's not recommended on WebSocket
method.
Tip
If you faced flicks on page switching, set your body opacity to 0
and add a onLoad
event to your page and set opacity back when page is fully loaded.
Request/Response System
We got our bidirectional connection working, Now what we need is a small request/response system to execute our functions remotely and get the data.
For request/response system, we use JSON in both C++ and JavaScript. For parsing JSON data in C++, we use this great single header and easy to use library.
Let's write our request system first, open internal.js and write the following code after values:
class RequestMetadata
{
constructor(requestId, onResponse)
{
this.requestId = requestId;
this.onResponse = onResponse;
}
}
var requests = [];
RequestMetadata
: We create a simple class to use as struct
. It holds a unique request ID and a function which process the response result. requests
: We create a simple array that holds the requests until they get response, after response, we remove them from the array.
Now let's create our request generator function which handle creating the request and processing response:
function RequestDataFromInternal()
{
function onResponseEvent(responseData)
{
document.getElementById("center_text").innerHTML = responseData;
return true;
}
var requestId = GetANewRequestID();
var requestMeta = new RequestMetadata(requestId, onResponseEvent);
requests.push(requestMeta);
socket.send(JSON.stringify({id:requestId, funcId:1001,
requestStringID:"CENTER_TITLE"}));
}
function GetANewRequestID()
{
return Math.random() * (999999 - 111111) + 111111;
}
Our request layout contains two must have parameters:
id
: An unique ID which identifies the request and will be included in response funcId
: An unique ID which identifies the function essence
The rest are the parameters of your function in C++ code, It can be anything, Numbers, Strings, etc.
And finally, we add the response executer. Go in onmessage
event and write the following code:
if(event.data == "(LOAD_SPLASH_PAGE)")
{
fetch('./splash.html').then(response => response.text()).then(text =>
{
document.open(); document.write(text); document.close();
})
return;
}
var response = JSON.parse(event.data);
requests.forEach((request) =>
{
if(Math.floor(request.requestId) === Math.floor(response.responseID))
{
var result = request.onResponse(response.responseData);
requests = RemoveRequestMeta(request);
}
});
...
function RemoveRequestMeta(value)
{
var index = requests.indexOf(value);
if (index > -1) requests.splice(index, 1);
return requests;
}
We're done! You can pack everything to default_app.asar with 7-Zip. Now it's time to make our C++ response system...
In internal module code, include json.hpp:
#include "jsonpp/json.hpp"
Now head to OnClientRequest
event and write the following code:
auto jsonData = json::parse(msgstr);
int requestID = jsonData["id"].get<int>();
int functionID = jsonData["funcId"].get<int>();
if (functionID == 1001)
{
std::string requestData = jsonData["requestStringID"].get<std::string>();
json response;
response["responseID"] = requestID;
if (requestData == "CENTER_TITLE")
response["responseData"] = "Hey!<br><span>Electron</span> Welcomes you!";
if (requestData == "CENTER_ALTERNATIVE_TITLE")
response["responseData"] = "THIS IS A DEMO<br><span>WEB PAGE</span>
For CodeProject";
ws_sendframe_txt(client, response.dump().c_str());
}
Build internal module and run electron, Congrats! You've made your request/response system!
Let's Go Old School! (Bonus)
Ok, I know you loved the article so far. So here's a bonus on creating old school HTTP request/response system!
First, remove websocket artifacts and files or you can keep it and have a WebSocket and HTTP at same time. Remember websocket is bidirectional and your internal module can receive data from server, decrypt it and call anything in electron but by using HTTP electron always needs to send data first.
C++ Side (URL Method)
-
Add HttpLib header to your source:
#include "httplib/httplib.h"
Tip: If you faced compiler error simply move the #include <httplib.h>
above #include <Windows.h>
-
Add a server value after namespaces:
httplib::Server httpSrv;
-
Create server thread function with the following code:
void StartHttpServer()
{
Sleep(200);
InitializeServerEvents();
httpSrv.listen("localhost", 5995); }
-
Add event initializer function with the following code:
void InitializeServerEvents()
{
httpSrv.Get("/requestCenterText",
[](const httplib::Request& req, httplib::Response& res)
{
if (req.has_param("requestStringID"))
{
std::string requestData = req.get_param_value("requestStringID");
if (requestData == "CENTER_TITLE")
res.set_content("Hey!<br><span>Electron</span>
Welcomes you!", "text/html");
if (requestData == "CENTER_ALTERNATIVE_TITLE")
res.set_content("THIS IS A DEMO<br><span>WEB PAGE</span>
For CodeProject", "text/html");
}
});
}
-
Update CreateThread
in DllMain
:
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)StartHttpServer, NULL, NULL, NULL);
JavaScript Side (URL Method)
Open internal.js and change it to look like the following code:
function StartupEvent()
{
fetch('./splash.html').then(response => response.text()).then(text =>
{
document.open(); document.write(text); document.close();
})
}
function RequestDataFromInternal()
{
fetch('http://localhost:5995/requestCenterText?requestStringID=CENTER_TITLE')
.then(response => response.text()).then(responseData =>
{
document.getElementById("center_text").innerHTML = responseData;
})
}
Now you can test the code and see the same result as WebSocket
method!
C++ Side (POST Method)
To make this method better and more flexible, let's improve it by implementing POST
method.
Head to your InitializeServerEvents
function and add this following code:
httpSrv.Post("/remoteNative", [](const httplib::Request& req, httplib::Response& res)
{
auto jsonData = json::parse(req.body);
int functionID = jsonData["funcId"].get<int>();
if (functionID == 1001)
{
std::string requestData = jsonData["requestStringID"].get<std::string>();
json response;
response["responseData"] = "INVALID_STRING_ID";
if (requestData == "CENTER_TITLE")
response["responseData"] = "This is a <br><span>response</span>
from POST method!";
res.set_content(response.dump(), "text/html");
}
});
JavaScript Side (POST Method)
To use POST
method, we can use different ways but in this article, I use jQuery Ajax because it's simple and nice.
- Download jquery-3.X.X.min.js and include it in your main frame before internal.js.
- Create an
ajax
request and handle the response:
function RequestDataUsingPost()
{
$.ajax({
type: 'post',
url: 'http://localhost:5995/remoteNative',
data: JSON.stringify({funcId:1001, requestStringID:"CENTER_TITLE"}),
contentType: "application/json; charset=utf-8",
traditional: true,
success: function (data)
{
var response = JSON.parse(data);
document.getElementById("center_text").innerHTML =
response.responseData;
}
});
}
- Run electron and enjoy your old school request/response system!
Conclusion
Alright, another article of mine ends here, I hope you like it and find it useful, I'm not a web developer and most of the JavaScript codes I've used in this article are simple results I found on Google. :D
You can use the following method and convert all of your critical parts of application to native code and use extreme protection on it, You can pack it, virtualize it, obfuscate it or do whatever people do to protect their native binaries, Also, you can protect your sensitive content, assets, add extra encryption to SSL, etc.
You can download the full source code on CodeProject as well.
See ya in the next article!
History
- 16th January, 2023: Initial version