欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/

欢迎在PC社区平台申请新建项目: https://atomgit.com/OpenHarmonyPCDeveloper

摘要

Electron 系应用(包括鸿蒙 PC 上的 OpenHarmony Electron)分发出去的主体就是 JS,打包工具默认只做归档、不做保护,解包后源码基本原样可见。electron-egg(ee-v5)把代码加密做成了构建链的一部分:改几行 cmd/bin.js,跑一次 npm run encrypt,主进程和前端产物就完成压缩混淆。本文在本鸿蒙 PC demo 工程上跑通了这套流程,记录配置项含义、javascript-obfuscator 里真正值得开的参数、三档强度的实测体积(基础档 +34%,增强档约 3.5 倍),以及注入 HAP 后的真机验证结果。

项目源码托管在 AtomGit PC 社区:ohos_electron-egg

一、为什么桌面应用也需要代码加密

不少人以为 .exe / .app / HAP 里的代码是「编译好的、看不到的」,实际正相反。Electron 系应用(包括鸿蒙 PC 上的 OpenHarmony Electron)分发的主体就是 JS:

  • npx asar extract app.asar ./out 一条命令就能解包,拿到原始 JS;
  • asar 只是归档格式,没有任何加密语义;
  • 就算过了 esbuild 打包,产物依然是可读 JS。前面几篇的 SQLite Studio、2048 的 AI 算法,核心逻辑都能直接读出来。

商业发布至少要加一层防护。electron-egg 把它做进了框架构建链:npm run encrypt(即 npm run build-electron && ee-bin encrypt),不用自己接混淆工具。

二、混淆加密的原理与工具链

ee-bin encryptee-bin/src/tools/encrypt.ts 实现,底层是 javascript-obfuscator(v5.x)。它把同一份 JS 改写成等价但难读的形态:

  • 变量名全部替换为 _0x49064b 式十六进制标识符;
  • 字符串常量收进一个特殊数组,取值走解码函数(stringArray / stringArrayCallsTransform);
  • 数字拆成算术表达式(numbersToExpressions);
  • 整个文件压成一行(compact)。

关键点:混淆产出的仍然是标准 JS 源码,由引擎照常解析执行——不改变模块边界、不改变加载方式,因此对桌面 Electron 和鸿蒙 ArkWeb / ohos Electron 运行时都天然兼容,这是它成为鸿蒙项目默认选择的根本原因。

工具链的行为特征:

特性说明
双 targetelectronfrontend 分别配置策略,一条命令依次处理
就地替换扫描files 匹配到的产物,逐个混淆后写回原文件
过滤语法files 支持 ! 前缀排除,如 '!electron/xxx.json'
扩展名限定只处理fileExt(默认 .js)匹配的文件

下图是 ee-bin 加密管线核心方法 Encrypt.encrypt() 的实现:扫描目标文件清单后逐个分流处理——入口文件、specificFiles 与普通文件各走各的分支,最终就地写回混淆结果。

在这里插入图片描述

三、实操:以本工程为例

3.1 配置

本 demo 的 cmd/bin.jsencrypt 段实际配置:

encrypt: {
  frontend: {
    type: 'none',                          // 前端暂不加密(见第五章)
    files: ['./public/dist/**/*.(js|json)'],
    cleanFiles: ['./public/dist'],
    confusionOptions: { compact: true, stringArray: true, /* … */ target: 'browser' },
  },
  electron: {
    type: 'confusion',                     // 主进程:压缩混淆
    files: ['./public/electron/**/*.(js|json)'],
    cleanFiles: ['./public/electron'],
    specificFiles: [
      './public/electron/main.js',          // 包启动入口,保持 .js 文件名
      './public/electron/preload/bridge.js' // BrowserWindow preload 脚本
    ],
    confusionOptions: {
      compact: true,               // 压缩成一行
      stringArray: true,           // 字符串常量收进数组
      stringArrayEncoding: ['none'], // 数组编码:none | base64 | rc4,rc4 更强
      deadCodeInjection: false,    // 死代码注入,安全↑ 体积/性能代价大
      stringArrayCallsTransform: true,
      numbersToExpressions: true,  // 数字拆成算术表达式
      target: 'node',
    },
    silent: true,                  // 屏蔽 javascript-obfuscator 的广告横幅
  },
}

两个配置项值得展开:

  • files 用 globby 扫描出文件清单后逐个就地加密写回,支持 ! 过滤语法排除个别文件;
  • specificFiles 显式列出的文件走单独处理分支——main.js 是包入口、bridge.js 是 preload 脚本,两者的文件名与格式约束最严格,单列出来便于后续调整策略时不误伤。

3.2 执行

npm run encrypt
# = npm run build-electron && ee-bin encrypt

ee-bin encrypt 会依次处理 electronfrontend 两个 target,按各自 type 决定是否加密、怎么加密。加密是就地替换 public/electron/ 下的产物,完成后目录内容:

public/electron/
├── main.js                    # 混淆后的主进程 bundle
├── preload/bridge.js          # 混淆后的 preload
└── jobs/example/
    ├── hello.js               # fork 用的后台任务,同样逐个混淆
    └── timer.js

混淆后的 main.js 开头(真实产物):

const _0x596237=_0x2e21;(function(_0x43f9b4,_0x3e2dd7){const _0x143c18=
{_0x47255f:0x19f,_0x113568:0x1ee,...} ...

已完全不具备人工阅读价值。

在这里插入图片描述

3.3 体积代价(实测)

对同一份代码执行混淆前后的字节数对比(wc -c 实测):

文件混淆前混淆后变化
main.js(主进程 bundle)49,89067,067+34%
preload/bridge.js1521,705×11(小文件被包装开销占主导)
jobs/example/hello.js1,1523,309×2.9
jobs/example/timer.js2,6195,865×2.2
合计53,81377,946约 +45%

结论:当前配置(不开 deadCodeInjection)下体积膨胀约 45%,对安装包大小和启动速度都无感;如果把 stringArrayEncoding 提到 rc4、打开死代码注入,安全提升但体积和运行开销会继续上升,按项目风险等级取舍。

四、javascript-obfuscator 高价值参数详解

4.1 参数是直通官方配置的

ee-bin 的混淆实现只固定了三个默认值(compact: truestringArray: truestringArrayThreshold: 1),其余 confusionOptions 原样透传给 javascript-obfuscator v5.x。也就是说,官方文档里的每一个选项都可以直接写进 cmd/bin.js,无需改框架。v5 还提供了 optionsPreset: 'low-obfuscation' | 'medium-obfuscation' | 'high-obfuscation' 一键预设,作为调强起点。

4.2 值得关注的参数

参数作用代价建议
controlFlowFlattening + controlFlowFlatteningThreshold(0.5~0.75)控制流扁平化:把顺序执行改写为 while + switch 状态机跳转,对人工逆向单点效果最强体积明显↑,执行有小幅开销核心算法文件值得开,阈值不必拉满
stringArrayEncoding: ['rc4']字符串数组再加密一层(可配 base64解码依赖 eval 系能力:与 target: 'browser-no-eval' / 严格 CSP 页面不兼容主进程(target: 'node')放心用;前端在 ArkWeb 下先验证 CSP
stringArrayWrappersType: 'function'字符串取值函数再多套几层包装(配合 stringArrayCallsTransform轻微开着
splitStrings + splitStringsChunkLength: 10长字符串拆成碎片拼接,接口名、提示文案等不再能整串搜索轻微
renameGlobals顶层全局名一并混淆(bundle 作用域内)开——防函数名泄露业务语义
selfDefending产物防美化格式化,一经 beautify 即失效compact: false 冲突(会毁掉代码)只在 compact: true 的发布产物上用
identifierNamesGenerator: 'dictionary' / 'mangled'标识符命名风格:默认 hexadecimal 满屏 _0x 很显眼,dictionary 用随机单词命名,更像「正常但难看」的代码想降低「被针对」程度可换
seed: <数字>固定随机种子,混淆输出可复现(同输入同产物,利于构建缓存与审查)CI 里固定;「重跑 encrypt 修复偶发不可运行」的本质就是换 seed
debugProtection + debugProtectionInterval打开 DevTools 即陷入 debugger 循环卡死页面自己也别想调试前端 target 的攻击面防御,主进程不开
reservedNames / reservedStrings白名单:指定正则命中的标识符/字符串保持原样不混淆个别必须保持字面值的场景兜底用

两个看似诱人、实际要慎用的参数:

  • transformObjectKeys / renameProperties:改对象属性名。Electron 的控制器返回值是跨 IPC 的普通对象,渲染进程按属性名取值——若前端(public/dist)没有同步混淆,键名一错两端直接断联。默认关闭是正确姿势。
  • sourceMap 系列:obfuscator 本身能产出还原错误栈的 map,但 ee-bin 只取 getObfuscatedCode() 写回文件、不落盘 sourcemap,这些参数在 ee-bin 链路下不生效;确有需要就得自己接混淆步骤。

4.3 增强配置与实测代价

把上表的「开」项组装成增强配置:

confusionOptions: {
  compact: true,
  stringArray: true,
  stringArrayEncoding: ['rc4'],
  stringArrayCallsTransform: true,
  stringArrayWrappersType: 'function',
  numbersToExpressions: true,
  splitStrings: true,
  splitStringsChunkLength: 12,
  renameGlobals: true,
  controlFlowFlattening: true,
  controlFlowFlatteningThreshold: 0.6,
  deadCodeInjection: true,
  deadCodeInjectionThreshold: 0.1,
  selfDefending: true,
  seed: 114514,
  target: 'node',
}

拿同一份 49,890 字节的明文 main.js 做了三档对照(固定 seed,数字可复现。这次和 3.3 那次整目录实测不是同一次运行,几十字节的出入来自混淆的随机因子):

档位产物大小相对明文
明文49,890 B
基础混淆(本文第三章配置)66,927 B+34%
增强混淆(上面这套)173,508 B+248%(约 3.5 倍)

增强档体积约为基础档的 2.6 倍,大头来自控制流扁平化与死代码注入。务实结论:日常发布用基础档,核心商业版本上增强档,并在真机回归一次启动耗时与关键流程。

五、前端代码怎么办

前端(public/dist)的 target 同样支持 confusiontarget: 'browser' 的混淆配置),但对本工程的鸿蒙形态有一层现实考量:前端资源最终由 ArkWeb 加载执行,Vite 构建产物本身已经过 minify,进一步混淆对「防抄」收益有限(前端代码反正要下发到客户端执行)。因此本工程 frontend.type 保持 'none';确有要求时改成 'confusion' 即可,命令不变。

真正需要保护的算法放在主进程 service 里,恰好是 electron.type = 'confusion' 覆盖的范围——这也是「能力放主进程」除了性能之外的又一条理由。

六、部署到鸿蒙 PC 与真机验证

加密只动 public/electron/,鸿蒙链路不需要任何额外步骤——加密产物照常随资源注入:

npm run encrypt              # 构建主进程 + 混淆加密
npm run ohos-test            # 把 public/ 注入 ohos_hap 资源目录
# DevEco Studio: build_project --module electron@default → start_app

混淆产物在鸿蒙 PC 真机上按 T0/T1/T2 走了一遍:

阶段目标验收内容
T0:可启动混淆产物注入 HAP 后能安装并启动应用窗口正常出现,无白屏
T1:核心业务可用混淆不改变行为IPC 通道调用、jobs 子进程、控制器/service 全部回归
T2:可发布发布形态完整性解包检查产物确已混淆;多环境(dev/打包后)路径与功能一致

在这里插入图片描述

七、踩坑与经验

问题原因解决办法
混淆后偶发代码无法运行混淆输出极端情况下触发关键字/编码冲突官方文档建议:重新执行 npm run encrypt 即可(混淆带随机因子,重跑即换输出);重要版本加密后先冒烟
体积涨得比预期多要么打开了deadCodeInjection,要么文件本身太小死代码注入保持 false(本文配置实测全量 +45%);bridge.js 这类薄文件包装开销占比极高,膨胀十倍是正常现象,收益低可直接排除
误以为加密=安全,解包还能看到逻辑asar 只是归档混淆是底线防护;核心算法另加服务端校验
加密后调试困难产物不可读调试期 type 回none;发布流水线里再开加密

还有一条顺序上的坑:加密发生在构建之后、资源注入之前,固定顺序是 build-electron → encrypt → ohos 注入 → HAP 构建。如果先跑 ohos-test 再加密,注入进 HAP 的仍是明文。

八、总结

  • 加密是构建链自带的能力,cmd/bin.js 里配几行、跑一次 npm run encrypt 就行,主进程和前端两个 target 各配各的策略;
  • 压缩混淆产出仍是标准 JS,对运行时没有额外要求,体积涨 45% 量级,真机行为和明文版一致——对鸿蒙 PC 来说这是性价比最高的防护手段;
  • 想再强一点,先把值得保护的算法挪进主进程 service。它天然落在混淆覆盖范围内,比前端加密划算得多。

参考与延伸

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐