├── .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 `