Electron源码保护:用bytenode将JS编译为V8字节码 📅 发布时间:2026/9/14 3:32:40 👁 浏览次数: 简介面向Electron与electron-vue开发者的源码保护工具包定位是解决桌面应用打包后JS源码易被查看、篡改和盗用的问题。压缩包内含2个文件——一个基于ByteNode的JavaScript编译脚本和一份txt使用说明整体仅2KB轻量且上手成本低。脚本可将源码编译为Node.js可读的二进制文件达到混淆与加密效果在保持原有业务逻辑不变的前提下显著增加逆向工程难度。配套说明文档梳理了环境依赖、转换流程与集成到Electron项目中的步骤便于开发者快速落地。已有803人学习下载适合关注Electron应用安全、希望保护商业逻辑与知识产权的初中级前端开发者借此低成本为应用添加防护降低代码泄露与恶意注入风险。1. 打包成 app.asar 不等于源码安全很多团队把 Electron 构建出来的app.asar直接当成“编译产物”交付认为文件里面已经是机器码别人拿到也看不懂。可实际上 asar 只是一种归档格式和 tar 相似里面的main.js、renderer.js、package.json全是明文文本。用官方解包命令跑一遍接口地址、加密密钥、前端路由、业务算法都能完整还原。更难受的是解包后的项目换一个图标就能重新打包变成带后门的分发版本。bytenode.js解决的是其中一条可行路径把 JavaScript 编译成 V8 能直接执行的.jsc字节码让 asar 里不再出现可读的业务源码。本文从原理、编译、打包到验证完整走一遍适用于已经有现成 Electron 项目、准备在发布阶段加一层防护的工程团队。2. 反直觉的源码暴露路径asar 解包、V8 缓存与字节码的取舍2.1 被当成二进制的 app.asar 其实是文本归档Electron 主进程依赖 Node.js 执行 JavaScript渲染进程依赖 Chromium 执行 JavaScript因此业务代码本质上是文本文件。打包工具负责把这些文件塞进一个叫 asar 的归档里通过自定义 Nodefs模块让应用在运行时无感知地读取它们。整个过程没有加密只有“拼包”。用下面命令可以直接把发布版本的 asar 拆开npx electron/asar extract app.asar app-exposed find app-exposed/bundled -type f | head -20 grep -R vue-router\|apiBase app-exposed --include*.js -l第一行把 asar 解包到app-exposed第二行看 bundle 目录下有哪些文件第三行直接搜关键字只要命中说明你的源码没有经过额外处理。命令本身没有破坏任何东西是因为 asar 的 header 里就保存着文件路径、长度、偏移量Electron 运行时也是靠这份 header 去读文件解包只是另存一份。2.2 V8 的 code cache 解决不了发布场景有人会问Node 的vm.Script可以生成cachedDataV8 也有代码缓存为什么不用它代码缓存确实是 V8 编译过程的中间产物但它有两个不适合发布的问题第一缓存数据高度依赖 V8 版本和源码内容Electron 升级后很容易失效第二cachedData的生成过程仍然需要源码输入没有办法直接拿一份源码生成一个“独立可执行单元”。bytenode 的做法不是缓存而是调 V8 的字节码编译器把 JavaScript 源码编译成字节码序列编译结果可以脱离源码单独存在。Electron 运行时加载.jsc相当于让 V8 直接消费不可读的中间表示而不是先解析文本再编译。2.3 为什么字节码比字符串混淆适合发布场景源码保护工具很多常用的还有javascript-obfuscator。字符串混淆把变量名、控制流改得面目全非但整个应用交付后AST 和原始文本结构仍然完整存在。分析者把混淆代码扔进智能解混淆工具再配合动态执行很快能找到真正的业务入口。bytenode 走的是另一条路从入口上让文本文件不出现V8 拿到的是字节码流反编译不能还原成一行行美观的源码。对比见下表维度字符串混淆运行时解密bytenode.jsc文本分析难度低AST 可还原中内存中有明文高无完整 AST构建接入成本低中低性能损耗低高基本无版本依赖无无与 V8 版本强绑定核心风险复用性差内存抓取字符串常量仍可见2.4 这套方案的边界字节码不是银弹。HTML、CSS、资源文件必须保持明文纯前端模板如果被打进独立的 chunk又不能被 Node 直接 require就需要单独设计。另外.jsc中仍然可能出现字符串常量比如接口 URL、错误码。不要把高价值密钥直接写在源码里密钥保护应该交给环境变量或系统 keychain。3. 用 bytenode.js 把主进程代码编译成 JSC 字节码3.1 用 Electron 自带的 Node 版本编译避免版本错位开始时最容易踩的坑是先用系统里的 Node 装 bytenode然后执行编译生成的.jsc文件在系统 Node 里能跑但放进 Electron 项目就回报错。原因是 Electron 内置的 V8 和系统 Node 内置的 V8 通常不是同一个版本字节码是版本敏感的序列化数据。正确做法是让 Electron 自己以纯 Node 模式运行编译脚本npm install bytenode --save ELECTRON_RUN_AS_NODE1 npx electron scripts/protect.jsELECTRON_RUN_AS_NODE1让 Electron 进程退化成普通 Node 运行时不启动 Chromium、不创建窗口但 V8 版本和最终运行 Electron 应用的版本完全一致。scripts/protect.js负责调用 bytenode 的compileFile。这样得到的.jsc才能直接被同一个 Electron 版本加载。包里的1.txt如果只写“直接跑 bytenode app.js”通常是拿系统 Node 编译这一步建议换成上述命令能省下大量环境兼容问题。3.2 编译脚本的核心参数下面是一个最小可用的编译脚本const { compileFile } require(bytenode); const path require(path); compileFile({ filename: path.join(__dirname, src/main.js), output: path.join(__dirname, dist/main.jsc) }).then(() { console.log(compiled ok); process.exit(0); }).catch((err) { console.error(compile failed:, err); process.exit(1); });filename必须传绝对路径output指定最终生成的.jsc文件路径。compileFile返回 Promise编译完成后显式调用process.exit(0)避免 Electron 纯 Node 模式的进程挂住。编译失败时输出完整错误并退出码 1方便 CI 流程接管。compileFile的几个主要参数参数作用默认值filename要编译的 JS 源码路径必填output输出的.jsc路径同目录同名.jsccompileAsModule是否按 CommonJS 模块编译truecreateCache是否额外生成 V8 内部缓存保持默认即可compileAsModule: true保证编译出来的字节码能被require正常加载并正确处理exports。如果把它设成 false得到的字节码更像一个独立脚本无法导出模块内容。3.3 require(bytenode) 做的事注册 .jsc 扩展bytenode 不是魔法它底层还是 Node 的Module._extensions机制。Node 加载文件时会根据扩展名查找对应的处理函数.js走Module._extensions[.js].json走.json。bytenode 在启动时向这个表里注册一个.jsc处理器const Module require(module); require(bytenode); const businessModule require(./dist/main.jsc);require(bytenode)会读取.jsc文件字节流把字节流交给 V8 的字节码编译器执行最终返回一个模块对象。这个模块内部可能有多个exports和原来的dist/main.js完全一致。这也是为什么入口文件可以只留两行脚本require(bytenode); require(./dist/main.jsc);第一行注册扩展第二行加载真正的业务模块。主进程代码里原来的app.on(ready)、BrowserWindow、ipcMain全部封装在dist/main.jsc内部入口文件本身不会暴露任何业务实现。4. electron-vue 项目里落地字节码保护的四个动作4.1 第一步关闭 webpack 的 chunk 拆分使用electron-vue或vue-cli-plugin-electron-builder时webpack 默认可能生成多个 chunk 文件。如果每个 chunk 都要单独编译成.jsc动态加载的import()路径会被 webpack runtime 改为内部 mapping手工替换很容易漏。更稳妥的配置是把主进程和渲染进程各收敛成一个入口 bundle// vue.config.js module.exports { pluginOptions: { electronBuilder: { mainProcessFile: src/main/index.js, rendererProcessFile: src/renderer/index.js } }, configureWebpack: { optimization: { splitChunks: false, runtimeChunk: false } } };splitChunks: false关闭公共模块提取runtimeChunk: false关闭运行时独立打包。这样构建产物里主进程通常只有一个main.js渲染进程只有一个页面脚本后续保护脚本只需要处理这一两个文件映射关系清晰。4.2 第二步批量编译 bundle并重写入口 loader针对构建出来的两个 bundle我习惯写一个独立的保护脚本放在scripts/protect.jsconst { compileFile } require(bytenode); const fs require(fs); const path require(path); const root path.resolve(__dirname, ..); const jobs [ { src: path.join(root, dist_electron/bundled/main.js), out: path.join(root, dist_electron/bundled/main.jsc) }, { src: path.join(root, dist_electron/renderer/static/js/app.js), out: path.join(root, dist_electron/renderer/static/js/app.jsc) } ]; (async () { for (const job of jobs) { await compileFile({ filename: job.src, output: job.out }); } fs.writeFileSync(jobs[0].src, [ require(\bytenode\);, require(\./main.jsc\); ].join(\n)); console.log(protected bundles written); })();脚本先遍历jobs数组分别对 main 和 renderer 执行编译。编译完成后把原来的main.js覆盖成 loader 文件下一步 Electron 启动时首先执行这一段再加载字节码。renderer 的app.js被编译成app.jsc后原来 HTML 里的script srcapp.js不能再直接访问需要额外处理页面引用或者只在主进程使用.jsc。4.3 第三步electron-builder 的 files 白名单保护完成后asar 里不应该出现源代码文件、source map 和原始main.js的正文。在package.json的build字段里过滤内容{ build: { asar: true, files: [ dist_electron/bundled/main.js, dist_electron/bundled/main.jsc, dist_electron/renderer/**/*, !**/src/**, !**/*.map, !**/*.log, node_modules/bytenode/**/* ] } }files数组里以!开头的是排除规则src目录是保护前的前端工程源码绝对不能跟着发出去。node_modules/bytenode需要保留否则运行时require(bytenode)会直接报模块不存在。builder 打包后asar 内主进程只有main.js和main.jsc原有源码不再占体积。4.4 渲染进程的字节码保护要单独决策渲染进程和主进程不同。HTML 的script标签加载一个.jsc文件时Chromium 不会把它当作 Node 模块处理浏览器引擎直接按文本脚本解析.jsc字节数据会抛语法错误。稳定的做法是把渲染进程里需要保护的纯逻辑代码抽到 preload 或独立模块中然后通过 bytenode 加载// preload.js require(bytenode); require(./renderer-core.jsc); const { expose } require(./renderer-core.jsc); window.addEventListener(DOMContentLoaded, () { // 把业务能力暴露给页面 });这样做的代价是如果原来的渲染进程代码直接操作 DOMpreload 里没有完整页面上下文。我通常的策略是UI 模板和组件结构不做字节码保护把底层请求、鉴权、算法抽成renderer-core.jsc页面只通过 postMessage 或 contextBridge 调用。如果项目里每一个组件都重度依赖window对象强行保护渲染进程只会增加维护成本。5. 发布前做一次“偷源码”验证以及三条常见报错5.1 验证 asar 里是否只剩 loader构建出安装包后先解包确认没有明文业务代码npx electron/asar extract out/win-unpacked/resources/app.asar app-exposed find app-exposed/bundled -type f cat app-exposed/bundled/main.jsfind看到main.jsc而不是main.js的完整源码说明主进程已经完成字节码替换。cat main.js只能看到两行 loader。如果这时还能看到app.js的完整 bundle说明保护脚本没有正确覆盖源文件需要回查scripts/protect.js中的路径。5.2 三个高频错误第一Invalid or incompatible cached data。这是.jsc的 V8 版本和应用运行时的 V8 版本不一致重新用ELECTRON_RUN_AS_NODE1 npx electron scripts/protect.js编译即可不要用系统 Node 编译。第二Cannot find module bytenode。bytenode 被打进了devDependencieselectron-builder 默认不会打包 devDependencies。安装时改成npm install bytenode --save并把node_modules/bytenode加进files。第三渲染进程页面报Unexpected token .。说明 HTML 引用了.jsc渲染进程不识别字节码。需要用一个 JS loader 去require或者把这一段 JS 放到 preload 中执行。5.3 最后压一句字符串常量不是安全边界字节码隐藏的是“代码结构”不隐藏常量。用strings main.jsc搜索仍能看到部分 URL、SQL 片段和错误文案。真正敏感的密钥和凭证不要放在任何 JS 层建议通过环境变量注入或者存到系统钥匙串里。源码保护的目标是提高逆向成本而不是替代加密体系。本文还有配套的精品资源点击获取