├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .travis.txt
├── .vscode
├── c_cpp_properties.json
└── launch.json
├── LICENSE.md
├── README.md
├── app
├── asar.js
├── index.html
├── main.js
├── package.json
├── renderer
│ ├── button.js
│ ├── element.js
│ ├── input.js
│ ├── message.js
│ ├── renderer.js
│ └── title.js
└── win.js
├── binding.gyp
├── common.gypi
├── node_modules_asar
└── outerpkg
│ ├── index.js
│ ├── mod.js
│ └── package.json
├── package.json
├── script
├── dist.js
├── js2c.js
├── keygen.js
├── pack.js
├── path.js
├── postinstall.js
├── start.js
└── test.js
└── src
├── aes
├── aes.c
├── aes.h
├── aes.hpp
└── unlicense.txt
├── base64.c
├── base64.h
├── find.js
├── main.cpp
├── require.js
└── script.h
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on: push
4 |
5 | jobs:
6 | build:
7 | name: Build
8 | runs-on: ${{ matrix.os }}
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | os: [windows-latest, ubuntu-latest, macos-latest]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: actions/setup-node@v1
17 |
18 | - run: npm install
19 | - run: npm test
20 | - run: npm run dist
21 | if: ${{ startsWith(github.event.ref, 'refs/tags') }}
22 |
23 | - name: Create release
24 | if: ${{ startsWith(github.event.ref, 'refs/tags') }}
25 | uses: toyobayashi/upload-release-assets@v3.0.0
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 | with:
29 | tag_name: ${{ github.event.after }}
30 | release_name: ${{ github.event.after }}
31 | draft: true
32 | prerelease: false
33 | assets: ./dist/*.zip
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /test
3 | package-lock.json
4 | /build
5 | *.node
6 | /src/key*
7 | .DS_Store
8 | /dist
9 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | build_from_source=true
2 | runtime=electron
3 | target=16.0.1
4 | toolset=v142
5 | disturl=https://electronjs.org/headers
6 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.14.0
--------------------------------------------------------------------------------
/.travis.txt:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - lts/*
5 | os:
6 | - windows
7 | - linux
8 | - osx
9 |
10 | before_install:
11 | - python -V
12 |
13 | install:
14 | - npm install
15 | script:
16 | - npm test
17 |
18 | before_deploy:
19 | - npm run dist
20 |
21 | deploy:
22 | provider: releases
23 | api_key:
24 | secure: jwzy+9oGkJNI64R4g4ttqKhX4IPQUnSXqFGGzGvUkMtjeOTvmONhRQS2xbrQf283Xko0+LFWhQoNGfHLVK/dzpExsxI4OMyafTIgowNDOGfPUeWS2fAenQkMw0bpAIqP4ztj06xHfDONSrWRUGpn88pgDTrlUwDIGQlEhGtsnbsC/PlwmUARp+W3ra0ml6FndCs3KOd+plOYB+IAKdSDPLUzhJYUObGzEUFZX69/WUD2WJwAjWfo3goplvNxsh0g8b1ufkWVk13ajTApRl2NkGDQqIYLP8QyiNAsmAt1dFX4lsiHLl8XtjX6P0Zb7WZfXJauMNHE5VJp8s1DF/3rTX/noawCXSGhU9dUUO78D8tMvxmpSnbPkJQV3CRLlxWKQg2goDrmf4Lb27yZjUUtGbz1yDscgTR6bqfBdDK1GPRlNHL2iKcQ7OixlIrDHuftiC0vB5PxZ0+eZsQ5Ey4a8hF5AUzQ8nH0/QqR6ooxn8Pgx1rczk1U9rA8VafoaxBr67HwvRFaG1QMy0JYg5dCcmBx7oZgRFKv0Sf0F8lNm/xc8Z/S2FAJnEhcE1co40sEFRq/ESHkfTSiphmhZdqhkRLVnTCX2dzn5ZlxrPMEp4Qp3nJR5d5WaceGpoy2HUnEPDZscHyJ+aMbeC7iAHOdjw7TX992w5XVsn4USzyT5vs=
25 | file:
26 | - ./dist/*.zip
27 | file_glob: true
28 | skip_cleanup: true
29 | draft: true
30 | on:
31 | repo: toyobayashi/electron-asar-encrypt-demo
32 | tags: true
33 |
--------------------------------------------------------------------------------
/.vscode/c_cpp_properties.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "includePath": [
4 | "${default}",
5 | "${workspaceFolder}/node_modules/node-addon-api",
6 | "${env:HOME}/AppData/Local/node-gyp/Cache/16.0.1/include/node"
7 | ],
8 | "defines": []
9 | },
10 | "configurations": [
11 | {
12 | "name": "Win32",
13 | "defines": ["${defines}", "_DEBUG", "UNICODE", "_UNICODE", "_CRT_SECURE_NO_WARNINGS"],
14 | "compilerPath": "${env:VCToolsInstallDir}\\bin\\Host${env:VSCMD_ARG_HOST_ARCH}\\${env:VSCMD_ARG_TGT_ARCH}\\cl.exe",
15 | "windowsSdkVersion": "10.0.19041.0",
16 | "intelliSenseMode": "windows-msvc-x64",
17 | "cStandard": "c11",
18 | "cppStandard": "c++17",
19 | "includePath": ["${includePath}"]
20 | },
21 | {
22 | "name": "Linux",
23 | "defines": ["${defines}"],
24 | "compilerPath": "/usr/bin/gcc",
25 | "cStandard": "c11",
26 | "cppStandard": "c++17",
27 | "intelliSenseMode": "linux-gcc-x64",
28 | "browse": {
29 | "path": [
30 | "${workspaceFolder}"
31 | ],
32 | "limitSymbolsToIncludedHeaders": true,
33 | "databaseFilename": ""
34 | },
35 | "includePath": ["${includePath}"]
36 | },
37 | {
38 | "name": "macOS",
39 | "includePath": ["${includePath}"],
40 | "defines": ["${defines}"],
41 | "macFrameworkPath": ["/System/Library/Frameworks", "/Library/Frameworks"],
42 | "compilerPath": "/usr/bin/clang",
43 | "cStandard": "c11",
44 | "cppStandard": "c++17",
45 | "intelliSenseMode": "macos-clang-x64"
46 | },
47 | {
48 | "name": "Emscripten",
49 | "defines": ["${defines}"],
50 | "compilerPath": "${env:EMSDK}/upstream/emscripten/emcc",
51 | "intelliSenseMode": "clang-x86",
52 | "cStandard": "c11",
53 | "cppStandard": "c++17",
54 | "includePath": ["${includePath}"]
55 | },
56 | {
57 | "name": "Emscripten (Win32)",
58 | "defines": ["${defines}"],
59 | "compilerPath": "${env:EMSDK}/upstream/emscripten/emcc.bat",
60 | "intelliSenseMode": "clang-x86",
61 | "cStandard": "c11",
62 | "cppStandard": "c++17",
63 | "includePath": ["${includePath}"]
64 | }
65 | ],
66 | "version": 4
67 | }
68 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "node",
6 | "request": "launch",
7 | "name": "Electron Debug",
8 | "console": "integratedTerminal",
9 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
10 | "windows": {
11 | "runtimeExecutable": "${workspaceFolder}\\node_modules\\.bin\\electron.cmd"
12 | },
13 | "program": "${workspaceFolder}/app/index.js"
14 | },
15 | {
16 | "name": "Windows Attach",
17 | "type": "cppvsdbg",
18 | "request": "attach",
19 | "processId": "${command:pickProcess}"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-present Toyobayashi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This text was translated with help from https://www.deepl.com/translator and some manual adjustments. It may require some work here and there, improvements are welcome!
2 |
3 | ---
4 |
5 | # Encrypting source code for Electron applications
6 |
7 | ## Translations
8 |
9 | Great thanks to those who translated this README to other languages.
10 |
11 | * English - [sleeyax/electron-asar-encrypt-demo](https://github.com/sleeyax/electron-asar-encrypt-demo)
12 | * Portuguese - [maxwellcc/electron-asar-encrypt-demo](https://github.com/maxwellcc/electron-asar-encrypt-demo)
13 |
14 | ## Why does this repository exist?
15 |
16 | As we all know, [Electron](https://electronjs.org) does not officially provide a way to protect the source code. To package an Electron application, you simply [copy the source code to a fixed location](http://electronjs.org/docs/tutorial/application-distribution), such as the `resources/app` directory on Windows/Linux. When running an Electron application, Electron treats this directory as a Node.js project to run the JS code in. However, the files in the ASAR package are not encrypted, they are just stitched together into one file with header information, and it is easy to extract all the source code from the ASAR package using the official `asar` library, so the effect of encryption is not achieved.
17 |
18 | So I was thinking about how to encrypt the ASAR package to prevent the commercial source code from being easily tampered or injected with some malicious code by some people who want to distribute it again. Here is a way to do it without recompiling Electron.
19 |
20 | ## Start running
21 |
22 | ``` bash
23 | git clone https://github.com/toyobayashi/electron-asar-encrypt-demo.git
24 | cd ./electron-asar-encrypt-demo
25 | npm install # Copy electron release to the test directory
26 | npm start # Compile and start the application
27 | npm test # Compile and run the test
28 | ```
29 |
30 | ## Encryption
31 |
32 | As an example, a key is encrypted with AES-256-CBC and stored in a local file for easy import into JS package scripts and inline with C++ include.
33 |
34 | ``` js
35 | // This script is not packaged into the client and is used for local development
36 | const fs = require('fs')
37 | const path = require('path')
38 | const crypto = require('crypto')
39 |
40 | fs.writeFileSync(path.join(__dirname, 'src/key.txt'), Array.prototype.map.call(crypto.randomBytes(32), (v => ('0x' + ('0' + v.toString(16)).slice(-2)))))
41 | ```
42 |
43 | This generates a `key.txt` file in `src`, which looks like this:
44 |
45 | ```
46 | 0x87,0xdb,0x34,0xc6,0x73,0xab,0xae,0xad,0x4b,0xbe,0x38,0x4b,0xf5,0xd4,0xb5,0x43,0xfe,0x65,0x1c,0xf5,0x35,0xbb,0x4a,0x78,0x0a,0x78,0x61,0x65,0x99,0x2a,0xf1,0xbb
47 | ```
48 |
49 | Encryption is done when packaging, using the `asar.createPackageWithOptions()` API of the `asar` library:
50 |
51 | ``` ts
52 | ///
53 |
54 | declare namespace asar {
55 | // ...
56 | export function createPackageWithOptions(
57 | src: string,
58 | dest: string,
59 | options: {
60 | // ...
61 | transform?: (filePath: string) => NodeJS.ReadWriteStream | void;
62 | }
63 | ): Promise
64 | }
65 |
66 | export = asar;
67 | ```
68 |
69 | Pass the transform option in the third argument, which is a function that returns a `ReadWriteStream` to process the file, or undefined if it does not process the file. This step encrypts all JS files and inserts them into the ASAR package.
70 |
71 | ``` js
72 | // This script is not packaged into the client and is used for local development
73 |
74 | const crypto = require('crypto')
75 | const path = require('path')
76 | const fs = require('fs')
77 | const asar = require('asar')
78 |
79 | // Read the key and make a Buffer
80 | const key = Buffer.from(fs.readFileSync(path.join(__dirname, 'src/key.txt'), 'utf8').trim().split(',').map(v => Number(v.trim())))
81 |
82 | asar.createPackageWithOptions(
83 | path.join(__dirname, './app'),
84 | path.join(__dirname, './test/resources/app.asar'),
85 | {
86 | unpack: '*.node', // do not pack C++ modules
87 | transform (filename) {
88 | if (path.extname(filename) === '.js') {
89 | // generate a random 16-byte initialization vector IV
90 | const iv = crypto.randomBytes(16)
91 |
92 | // whether we have already put the IV at the start of the encrypted data (see below)
93 | let append = false
94 |
95 | const cipher = crypto.createCipheriv(
96 | 'aes-256-cbc',
97 | key,
98 | iv
99 | )
100 | cipher.setAutoPadding(true)
101 | cipher.setEncoding('base64')
102 |
103 | // rewrite `Readable.prototype.push` to put the IV at the start of the encrypted data
104 | const _p = cipher.push
105 | cipher.push = function (chunk, enc) {
106 | if (!append && chunk != null) {
107 | append = true
108 | return _p.call(this, Buffer.concat([iv, chunk]), enc)
109 | } else {
110 | return _p.call(this, chunk, enc)
111 | }
112 | }
113 | return cipher
114 | }
115 | }
116 | }
117 | )
118 | ```
119 |
120 | ## Main process decryption
121 | Decryption is done client-side because the V8 engine can't run the encrypted JS, so it must be decrypted before being thrown to V8 to run. The client-side code can be accessed by anyone, so the key cannot be written explicitly or placed in a configuration file, so it has to be put into C++. Write a native module in C++ to implement decryption, and this module can not export decryption methods, otherwise it is meaningless. Also the key cannot be written hard coded in C++ source code as a string, because a string can be easily found in a compiled binary file.
122 |
123 | What? Can't we use it without exporting it? It's easy to Hack the Node.js API to make sure it's not available externally, then use the native module as the entry module and require the real entry JS in the native module.
124 | ``` js
125 | // Write the following logic in C++, so that the key can be compiled into the dynamic library
126 | // Only by decompiling the dynamic library can it be analyzed
127 |
128 | // disable debugging
129 | for (let i = 0; i < process.argv.length; i++) {
130 | if (process.argv[i].startsWith('--inspect') ||
131 | process.argv[i].startsWith('--remote-debugging-port')) {
132 | throw new Error('Not allow debugging this program.')
133 | }
134 | }
135 |
136 | const { app, dialog } = require('electron')
137 |
138 | const moduleParent = module.parent;
139 | if (module !== process.mainModule || (moduleParent !== Module && moduleParent !== undefined && moduleParent !== null)) {
140 | // If the native module is not an entry, an error will be reported and exit
141 | dialog.showErrorBox('Error', 'This program has been changed by others.')
142 | app.quit()
143 | }
144 |
145 | const Module = require('module')
146 |
147 | function getKey () {
148 | // inline the key generated by the JS script here
149 | // const unsigned char key[32] = {
150 | // #include "key.txt"
151 | // };
152 | return KEY
153 | }
154 |
155 | function decrypt (body) { // body is Buffer
156 | const iv = body.slice(0, 16) // first 16 bytes are IV
157 | const data = body.slice(16) // after 16 bytes is encrypted code
158 |
159 | // It is better to use the native library for decryption, the Node API is at risk of being intercepted
160 |
161 | // const clearEncoding = 'utf8' // output is string
162 | // const cipherEncoding = 'binary' // input is binary
163 | // const chunks = [] // string to save chunks
164 | // const decipher = require(' crypto').createDecipheriv(
165 | // 'aes-256-cbc',
166 | // getKey(),
167 | // iv
168 | // )
169 | // decipher.setAutoPadding(true)
170 | // chunks.push(decipher.update(data, cipherEncoding, clearEncoding))
171 | // chunks.push(decipher.final(clearEncoding))
172 | // const code = chunks.join('')
173 | // return code
174 |
175 | // [native code]
176 | }
177 |
178 | const oldCompile = Module.prototype._compile
179 | // Rewrite Module.prototype . _compile
180 | // I won't write more about the reason, just look at the source code of Node and you will know
181 | Object.defineProperty(Module.prototype, '_compile', {
182 | enumerable: true,
183 | value: function (content, filename) {
184 | if (filename.indexOf('app.asar') !== -1) {
185 | // If this JS is in app.asar, decrypt it first
186 | return oldCompile.call(this, decrypt(Buffer.from(content, 'base64')), filename)
187 | }
188 | return oldCompile.call(this, content, filename)
189 | }
190 | })
191 |
192 | try {
193 | // The main process creates the window here, if necessary, pass the key to JS, it is best not to pass
194 | require('./main.js')(getKey())
195 | } catch (err) {
196 | // prevent Electron does not exit
197 | dialog.showErrorBox('Error', err.stack)
198 | app.quit()
199 | }
200 | ```
201 | To write the above code in C++, there is a problem: How do I get the JS `require` function in C++?
202 |
203 | If you look at the Node source code, you can see that calling require is equivalent to calling `Module.prototype.require`, so if you can get the module object, you can also get the `require` function. Unfortunately, NAPI does not expose the module object in the module initialization callback, someone mentioned PR but it seems that for some reason (aligning with the ES Module standard) the Node.js developers do not want to expose the module, only the exports object, unlike the Node CommonJS module where the JS code is wrapped in a layer of functions.
204 |
205 | ``` js
206 | function (exports, require, module, __filename, __dirname) {
207 | // write your own code here
208 | }
209 | ```
210 |
211 | If you look through the Node.js documentation, you can see in the process section that there is such a thing as `global.process.mainModule`, which means that the entry module can be obtained from the global, and you can find the module object of the current native module by traversing the children array of the module and comparing `module.exports`, which is not equal to `exports`. you can find the module object of the current native module.
212 |
213 | First, let's encapsulate the method of running the script.
214 | ``` cpp
215 | #include
216 | #include "napi.h"
217 |
218 | // First encapsulate the script running method
219 | Napi::Value RunScript(Napi::Env& env, const Napi::String& script) {
220 | napi_value res;
221 | NAPI_THROW_IF_FAILED(env, napi_run_script(env, script, &res), env.Undefined());
222 | return Napi::Value(env, res); // env.RunScript(script);
223 | }
224 |
225 | Napi::Value RunScript(Napi::Env& env, const std::string& script) {
226 | return RunScript(env, Napi::String::New(env, script)); // env.RunScript(script);
227 | }
228 |
229 | Napi::Value RunScript(Napi::Env& env, const char* script) {
230 | return RunScript(env, Napi::String::New(env, script)); // env.RunScript(script);
231 | }
232 | ```
233 |
234 | `node-addon-api` v3 and above can be used directly:
235 |
236 | ``` cpp
237 | Napi::Value Napi::Env::RunScript(const char* utf8script);
238 | Napi::Value Napi::Env::RunScript(const std::string& utf8script);
239 | Napi::Value Napi::Env::RunScript(Napi::String script);
240 | ```
241 |
242 | Then you can happily execute JS code in C++.
243 |
244 | ``` cpp
245 | Napi::Value GetModuleObject(Napi::Env& env, const Napi::Object& main_module, const Napi::Object& exports) {
246 | std::string script = "(function (mainModule, exports) {\n"
247 | "function findModule(start, target) {\n"
248 | " if (start.exports === target) {\n"
249 | " return start;\n"
250 | " }\n"
251 | " for (var i = 0; i < start.children.length; i++) {\n"
252 | " var res = findModule(start.children[i], target);\n"
253 | " if (res) {\n"
254 | " return res;\n"
255 | " }\n"
256 | " }\n"
257 | " return null;\n"
258 | "}\n"
259 | "return findModule(mainModule, exports);\n"
260 | "});";
261 | Napi::Function find_function = RunScript(env, script).As();
262 | Napi::Value res = find_function({ main_module, exports });
263 | if (res.IsNull()) {
264 | Napi::Error::New(env, "Cannot find module object.").ThrowAsJavaScriptException();
265 | }
266 | return res;
267 | }
268 | Napi::Function MakeRequireFunction(Napi::Env& env, const Napi::Object& mod) {
269 | std::string script = "(function makeRequireFunction(mod) {\n"
270 | "const Module = mod.constructor;\n"
271 |
272 | "function validateString (value, name) { if (typeof value !== 'string') throw new TypeError('The \"' + name + '\" argument must be of type string. Received type ' + typeof value); }\n"
273 |
274 | "const require = function require(path) {\n"
275 | " return mod.require(path);\n"
276 | "};\n"
277 |
278 | "function resolve(request, options) {\n"
279 | "validateString(request, 'request');\n"
280 | "return Module._resolveFilename(request, mod, false, options);\n"
281 | "}\n"
282 |
283 | "require.resolve = resolve;\n"
284 |
285 | "function paths(request) {\n"
286 | "validateString(request, 'request');\n"
287 | "return Module._resolveLookupPaths(request, mod);\n"
288 | "}\n"
289 |
290 | "resolve.paths = paths;\n"
291 |
292 | "require.main = process.mainModule;\n"
293 |
294 | "require.extensions = Module._extensions;\n"
295 |
296 | "require.cache = Module._cache;\n"
297 |
298 | "return require;\n"
299 | "});";
300 |
301 | Napi::Function make_require = RunScript(env, script).As();
302 | return make_require({ mod }).As();
303 | }
304 | ```
305 |
306 | ``` cpp
307 | #include
308 |
309 | struct AddonData {
310 | // Save Node module reference
311 | // std::unordered_map modules;
312 | // Save function reference
313 | std::unordered_map functions;
314 | };
315 |
316 | Napi::Value ModulePrototypeCompile(const Napi::CallbackInfo& info) {
317 | AddonData* addon_data = static_cast(info.Data());
318 | Napi::Function old_compile = addon_data->functions["Module.prototype._compile"].Value();
319 | // It is recommended to use a C/C++ library for decryption // ...
320 | }
321 |
322 | Napi::Object Init(Napi::Env env, Napi::Object exports) {
323 | #ifdef _TARGET_ELECTRON_RENDERER_
324 | // const mainModule = window.module
325 | Napi::Object main_module = env.Global().Get("module").As();
326 | #else
327 |
328 | Napi::Object process = env.Global().Get("process").As();
329 | Napi::Array argv = process.Get("argv").As();
330 | for (uint32_t i = 0; i < argv.Length(); ++i) {
331 | std::string arg = argv.Get(i).As().Utf8Value();
332 | if (arg.find("--inspect") == 0 ||
333 | arg.find("--remote-debugging-port") == 0) {
334 | Napi::Error::New(env, "Not allow debugging this program.")
335 | .ThrowAsJavaScriptException();
336 | return exports;
337 | }
338 | }
339 | // const mainModule = process.mainModule
340 | Napi::Object main_module = process.Get("mainModule").As();
341 | #endif
342 |
343 | Napi::Object this_module = GetModuleObject(&env, main_module, exports).As();
344 | Napi::Function require = MakeRequireFunction(env, this_module);
345 | // const mainModule = process.mainModule
346 | Napi::Object main_module = env.Global().As().Get("process").As().Get("mainModule").As();
347 | // const electron = require('electron')
348 | Napi::Object electron = require({ Napi::String::New(env, "electron") }).As();
349 | // require('module')
350 | Napi::Object module_constructor = require({ Napi::String::New(env, "module") }).As();
351 | // module.parent
352 | Napi::Value module_parent = this_module.Get("parent");
353 |
354 | if (this_module != main_module ||
355 | (module_parent != module_constructor && module_parent != env.Undefined() && module_parent != env.Null())) {
356 | // The entry module is not the current native module and may be intercepted by the API to leak the key
357 | // pop-up warning after exit
358 | }
359 |
360 | AddonData* addon_data = env.GetInstanceData();
361 |
362 | if (addon_data == nullptr) {
363 | addon_data = new AddonData();
364 | env.SetInstanceData(addon_data);
365 | }
366 |
367 | // require('crypto')
368 | // addon_data->modules["crypto"] = Napi::Persistent(require({ Napi::String::New(env, "crypto") }).As());
369 |
370 | Napi::Object module_prototype = module_constructor.Get("prototype").As();
371 | addon_data->functions["Module.prototype._compile"] = Napi::Persistent(module_prototype.Get("_compile").As());
372 | module_prototype["_compile"] = Napi::Function::New(env, ModulePrototypeCompile, "_compile", addon_data);
373 |
374 | try {
375 | require({ Napi::String::New(env, "./main.js") }).Call({ getKey() });
376 | } catch (const Napi::Error& e) {
377 | // Exit after the popup window
378 | // ...
379 | }
380 | return exports;
381 | }
382 |
383 | // Don't use semicolon, NODE_API_MODULE is a macro
384 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
385 | ```
386 |
387 | You may ask why you need to use C++ to write JS after all this time, isn't it obvious that you can `RunScript()`. As mentioned earlier, `RunScript()` directly requires JS to be written as a string, which exists in the compiled binary as is, and the key will be leaked, so using C++ to write the logic can increase the difficulty to reverse engineer.
388 |
389 | To summarize, it looks like this:
390 | 1. `main.node` (compiled) inside requires `main.js` (encrypted)
391 | 2. `main.js` (encrypted) requires other encrypted JS, creates windows, etc.
392 |
393 | Note that the entry must be main.node. If it is not, it is very likely that the attacker will hack the Node API in the JS before main.node is loaded, resulting in key leakage. For example, an entry file like this:
394 |
395 | ``` js
396 | const crypto = require('crypto')
397 |
398 | const old = crypto.createDecipheriv
399 | crypto.createDecipheriv = function (...args) {
400 | console.log(...args) // key is output
401 | return old.call(crypto, ...args)
402 | }
403 |
404 | const Module = require('module')
405 |
406 | const oldCompile = Module.prototype._compile
407 |
408 | Module.prototype._compile = function (content, filename) {
409 | console.log(content) // JS source code is output
410 | return oldCompile.call(this, content, filename)
411 | }
412 |
413 | process.argv.length = 1
414 |
415 | require('./main.node')
416 | // or Module._load('./main.node', module, true)
417 | ```
418 |
419 | ## Render process decryption
420 | Similar to the logic of the main process, you can use predefined macros in C++ to distinguish between the main process and the rendering process. The native module loaded by the rendering process must be context-aware, and the module written in NAPI is already context-aware, so there is no problem, but not if written in the V8 API.
421 |
422 | There is a restriction that you can't load JS in HTML by directly referencing the `