WEBKT

Vite + Electron 结合 Bytenode 实现源码字节码加密:全流程自动化与避坑指南

475 0 0 0

在 Electron 桌面端开发中,源码安全一直是个绕不开的话题。虽然我们可以使用 Webpack 或 Vite 混淆代码,但对于稍微懂点技术的人来说,asar 包解压后配合混淆还原工具,逻辑几乎是裸奔的。

Bytenode 通过将 JavaScript 编译成 V8 字节码(.jsc 文件),从底层提高了逆向工程的难度。然而,在 Vite + Electron 这种现代构建环境下,由于 Vite 默认的 ESM 模块机制与 Bytenode 的 CommonJS 编译方式存在天然代差,配置过程往往充满了各种“莫名其妙”的报错。

本文将分享一套成熟的自动化构建方案,并复盘我在配置过程中踩过的坑。

为什么选择 Bytenode?

  1. 性能损耗极小:V8 加载字节码的速度甚至比解析 JS 源码更快。
  2. 安全性高:不像混淆,它是二进制级别的编译,无法简单还原源码。
  3. 成熟稳定:基于 V8 自带的 v8::ScriptCompiler。

一、 核心逻辑与构建链路

要实现自动化,我们需要在 Vite 完成 Bundle 之后,立即触发一个 Hook,将生成的 main.js(主进程代码)转换成 main.jsc,并生成一个引导文件。

构建链路如下:
TypeScript 源码 -> Vite (Rollup) 打包 -> 生成 JS 产物 -> Bytenode 编译 -> 生成 .jsc -> 修改入口文件 -> Electron-Builder 封装。


二、 关键配置步骤

1. Vite 配置:锁定 CommonJS

Bytenode 目前对 ESM 的支持并不完美。因此,Electron 的主进程(Main Process)务必配置为输出 CommonJS 格式。

// vite.config.ts (主进程配置)
export default defineConfig({
  build: {
    ssr: true,
    lib: {
      entry: 'src/main/index.ts',
      formats: ['cjs'], // 必须是 cjs
    },
    rollupOptions: {
      external: ['electron', 'bytenode'], // 外部化,不要打包进去
    },
  },
});

2. 自动化编译脚本

我们需要编写一个 build-jsc.js 脚本,在 Vite 构建结束后运行。

const bytenode = require('bytenode');
const fs = require('fs');
const path = require('path');

async function compileToBytecode() {
  const mainPath = path.resolve(__dirname, '../dist/main/index.js');
  const jscPath = path.resolve(__dirname, '../dist/main/index.jsc');

  // 1. 编译生成 jsc
  await bytenode.compileFile({
    filename: mainPath,
    output: jscPath,
  });

  // 2. 删除原始 JS 文件或清空内容,仅保留一个加载器
  // 坑点:不能直接删掉 index.js,因为 Electron 入口需要指向它
  const loaderContent = `"use strict";
require('bytenode');
require('./index.jsc');`;
  
  fs.writeFileSync(mainPath, loaderContent);
  console.log('✅ Bytecode compilation successful!');
}

compileToBytecode();

三、 避坑指南(血泪经验)

坑 1:V8 版本不匹配(致命伤)

Bytenode 生成的字节码是 版本强绑定 的。

  • 如果你用 Node.js v18 编译了 index.jsc。
  • 但 Electron 内置的 Node.js 版本是 v16。
  • 结果: 程序启动时会报 Invalid or incompatible cached data 错误。

对策: 编译时必须使用与 Electron 内部版本完全一致的 Node 运行环境。建议在 package.json 中使用 electron-node 来执行编译脚本,或者确保本地 Node 版本与 Electron 版本对应。

坑 2:__dirname 与 __filename 的丢失

在字节码环境下,由于代码不再以文本形式解析,传统的 __dirname 可能失效。
对策:
在 Vite 打包主进程时,确保启用 node.__dirname = true。此外,建议在代码中使用 app.getAppPath() 来获取路径,而不是依赖 Node 的全局变量。

3. 依赖项必须 External

不要试图把 bytenode 本身或者 electron 模块打包进那个被编译的 index.js 中。
在 vite.config.ts 的 rollupOptions.external 中一定要把它们排除。否则,编译后的字节码在尝试 require 这些内置模块时会由于上下文丢失而崩溃。

4. 只有主进程需要加密

不要尝试对渲染进程(Renderer Process)的 index.html 或生成的 js 字节码化。渲染进程运行在 Chromium 环境,处理字节码非常复杂且容易导致白屏。源码保护的重点应该放在涉及商业逻辑、API 密钥的主进程上。


四、 最后的自动化集成

在 package.json 中串联指令:

{
  "scripts": {
    "build": "vite build && node scripts/build-jsc.js",
    "dist": "npm run build && electron-builder"
  }
}

总结

Vite + Electron + Bytenode 的组合虽然在初次配置时稍显繁琐,但它是目前低成本、高效率保护 Electron 应用逻辑的最佳方案。

核心记住三点:

  1. 版本对齐:Node 版本必须和 Electron 版本一致。
  2. 格式对齐:必须强行指定 Vite 输出为 CJS。
  3. 引导加载:用一个极简的 JS 文件作为外壳去加载 JSC。

通过这种方式,即使别人拿到了你的 asar 包,看到的也只是二进制的字节码,安全感直接拉满。

DevMaster ElectronViteBytenode

评论点评