Electron代码保护工程实践

发表于:2025-12-10 21:40:06

Electron使用nodejs和前端代码来开发应用程序,我们先不说开发出来的应用的包大小问题,因为在今天硬盘和内存已经完全超量的情况下,一个Electron的应用跑起来简直太轻松了。

但是,Electron应用在发布的时候,只是简单的将代码打包,其源代码未受到任何保护。本文将介绍我在Electron应用源代码的保护过程中的实践。

Electron代码运行环境为两种:

  • 主进程中的代码
  • 渲染进程中的代码

在不同的进程中,对代码使用不同的保护方式。

主进程中的代码,可以直接使用bytenode编译为字节码。对于渲染进程中的代码,虽然bytenode也支持,但这是有条件的支持,需要设置nodeIntegrationtrue,开启node的集成才可以。

所以,这里的总体思路是,在主进程中的代码使用bytenode进行编译,渲染进程的预加载(preload)代码,使用javascript-obfuscator进行混淆。

对于bytenode大家应该知道,需要编译所使用的版本需要与运行时的版本保持一致,所以这里就直接使用electron来执行编译脚本。

为了方便管理,在根目录建立了三个子目录:

  • builder - bytenode编译脚本管理
  • dist - 最终生成的打包发布的文件
  • src - 开发过程中的源代码

在打包发布的时候,只需要打包dist目录即可。

入口加载器

Electron在package.jsonmain字段中指定入口的js文件,这里的js文件,只能是源代码。所以这个指定的入口文件,不能使用bytenode来编译。

在我的项目中,入口文件指定为loader.js,该文件主要完成两个工作:

  • 加载bytenode环境
  • 根据当前模式(是否是开发模式)来加载不同的脚本。开发模式加载src中的代码,发布模式加载dist中的文件。

以下是我在工程中使用的加载器代码:

require('bytenode');
const { app } = require('electron');
function Main() {
    if (app.isPackaged) {
        require("./dist/backend/main")
    }
    else {
        require("./src/backend/main")
    }
}
Main();

编译脚本

工程中,将编译用的脚本放置于builder目录中,通过命令electron ./builder/index.js来启动编译。

index.js是编译的入口文件,该文件中,通过遍历源代码目录,再根据configure.js中的配置,针对不同的源代码文件,调用不同的处理器插件。

没错,bytenode在这里,仅仅是一个众多的处理器之一,你还可以根据你的需求编写自己的处理器。

在此处,实现了以下处理器:

  • Copy - 直接将源代码目录的文件复制到发布目录,不做任何修改。
  • Compile - 通过bytenode将源代码的文件编译为字节码,并存储到发布目录下。
  • Obfuscator - js代码的混淆器。
  • CssMinimizer - CSS压缩器。
  • Ignore - 忽略器。源代码中的文件不会发布到发布目录中。一般用于保护一些不想公开的数据,需配合自定义处理器。

配置文件

使用名为configure.js的文件做为配置文件,会在index.js中通过require加载使用。

以下是工程中的示例:

const Copy = require("./plugins/copy")
    , Compile = require("./plugins/compile")
    , Obfuscator = require("./plugins/obfuscator")
    , CssMinimizer = require("./plugins/css")
    , Ignore = require("./plugins/ignore")
    , StringLoader = require("./secret/string")
    , FileProtect = require("./secret/file")
    ;
module.exports = {
    source: "src",
    target: "dist",
    clean: true,
    protectMain:true,
    rules: [
        ["**/*.css", CssMinimizer]
        , ["assets/languages/index.js", Compile]
        , ["assets/languages/**", Copy]
        , ["assets/strings/texts/**", Ignore]
        , ["assets/strings/loader.js", StringLoader]
        , ["assets/files.js", FileProtect]
        , ["assets/**/*.png", Copy]
        , ["assets/**/*.ico", Copy]
        , ["backend/**", Compile]
        , ["preloads/*.css", CssMinimizer]
        , ["preloads/**", Obfuscator]
        , ["protocols/**/*.js", Obfuscator]
        , ["protocols/**", Copy]
        , ["windows/**/*.js", Obfuscator]
        , ["windows/**", Copy]
        , ["**", Copy]
    ]
}
  • source - 指定源代码目录。相对于工程根目录。
  • target - 发布目录。相对于工程根目录。
  • clean - 发布前是否清空发布目录。
  • protectMain - 是否保护加载器的中间目录。要求修改loader.js加载器。
  • rules - 代码保护规则。在执行时,遍历源代码目录,若匹配,则使用配置的处理器处理代码。若处理器返回的是一个函数,则会在遍历完目录后执行。

字符串保护

bytenode中,字符串通常是不被保护的,直接打开编译后的jsc文件很容易就能找到一些关键字符串,比如密钥什么的。

builder/secret目录中,实现了一个StringLoader的处理器,这个处理器将关键的字符串进行加密后,再动态实现一个解密算法,最后将这个解密的过程动态编译为jsc,编译后的模块与源代码中的声明相同即可。

文件保护

在发布的Electron应用中,最怕的就是别人修改源代码文件,所以,需要在正式启动前校验一下文件是否被修改。

在此处,通过FileProtect返回一个函数,这个函数会在所有目录遍历完成后执行。返回的函执行时,为dist中的所有文件生成一个hash表,软件正式启动前,可以通过这个表来检查文件是否有被修改过,若被修改了,则可以拒绝运行。

与字符串保护一样,最终这个函数会在dist中生成一个模块,这个模块被编译后在已经经过编译的应用程序主进程代码中调用。

软件授权

这里主要是针对使用机器码对软件进行授权的情况进行保护。

在我的项目中,使用node-hardware来读取机器的硬件信息,目前主要读取了以下信息:

  • CPU信息
  • 硬盘信息
  • 网卡信息

在项目中,通过这三个数据生成机器码。但是node-hardwarewindows中是通过.node文件来读取的硬件信息(其实就是一个DLL文件)。

如果.node被修改了,那这里的授权保护就形同虚设。根据上面的思路,在项目中同样校验一下.nodehash值即可,若hash不对,软件可以拒绝运行。


本文所说的builder中的所有文件,已经开源到Github。

https://github.com/zsea/electron-protect