Skip to content

源代码保护

提示

源代码保护功能从 1.0.9 版本开始可用

我们都知道 Electron 使用 javascript 来构建桌面应用程序,这使得黑客很容易对我们的应用程序进行进行解包、修改逻辑破解商业化限制、重新打包,再重新分发破解版。

解决方案

要想真正解决问题,除了把所有商业化逻辑做到服务端,我们还需要对代码进行加固,避免解包、篡改、二次打包、二次分发。

主流方案:

  1. Uglify / Obfuscator: 通过对 JS 代码进行丑化和混淆,尽可能降低其可读性。
  2. Native 加密: 通过 XOR 或 AES 对构建产物进行加密,封装到 Node Addon 中,并在运行时由 JS 解密。
  3. ASAR 加密: 将 Electron ASAR 文件进行加密,并修改 Electron 源代码,在读取 ASAR 文件之前对其解密后再运行。
  4. V8 字节码: 通过 Node 标准库里的 vm 模块,可以从 script 对象中生成其缓存数据(参考)。该缓存数据可以理解为 v8 的字节码,该方案通过分发字节码的形式来达到源代码保护的目的。

方案比较:

-Obfuscator 混淆Native 加密ASAR 加密V8 字节码
解包容易
篡改容易容易中等
可读性容易容易容易
二次打包容易容易容易容易
接入成本中等
保护强度中等中等

目前,使用 v8 字节码的解决方案似乎是最好的解决方案。

阅读更多:

什么是 V8 字节码

我们可以理解,V8 字节码是 V8 引擎在解析和编译 JavaScript 后产物的序列化形式,它通常用于浏览器内的性能优化。所以如果我们通过 V8 字节码运行代码,不仅能够起到代码保护作用,还对性能有一定的提升。

electron-vite 受 bytenode 启发,具体实现:

  • 实现一个插件 bytecodePlugin 来解析构建产物,并确定是否编译为字节码。
  • 启动 Electron 进程将构建产物编译成 .jsc ​​文件,并确保生成的字节码可以在 Electron 的 Node 环境中运行。
  • 自动生成字节码加载器,使 Electorn 应用能够加载字节码模块。
  • 支持开发者自由决定编译哪些块。

此外,electron-vite 还解决了一些 bytenode 无法解决的问题:

  • 修复了异步箭头函数可能导致 Electron 应用程序崩溃的问题。
  • 保护字符串。

警告

不支持 Function.prototype.toString,原因是源代码并不跟随字节码分发,因此取不到函数的源代码。

开启字节码插件保护源代码

启用 bytecodePlugin 插件:

js
import { defineConfig, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [bytecodePlugin()]
  },
  preload: {
    plugins: [bytecodePlugin()]
  },
  renderer: {
    // ...
  }
})
import { defineConfig, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [bytecodePlugin()]
  },
  preload: {
    plugins: [bytecodePlugin()]
  },
  renderer: {
    // ...
  }
})

提示

bytecodePlugin 仅适用于生产阶段构建并且只支持主进程和预加载脚本。

需要注意的是,预加载脚本需要禁用 sandbox 才能支持字节码,因为字节码是基于 Node 的 vm 模块实现。从 Electron 20 开始,渲染器默认会被沙箱化,所以如果你想使用字节码来保护预加载脚本,你需要设置 sandbox: false

bytecodePlugin 选项

chunkAlias

  • 类型:string | string[]

设置构建产物块别名以指示字节码编译器编译关联的包。通常需要与选项 build.rollupOptions.output.manualChunks 一起使用。

transformArrowFunctions

  • 类型: boolean
  • 默认: true(自 electron-vite 1.0.10 起默认启用)

设置为 false 禁止将箭头函数转换为普通函数。

removeBundleJS

  • 类型: boolean
  • 默认: true

设置 false 以保留已编译为字节码文件的 bundle 文件。

protectedStrings

指定源代码中的哪些字符串(如 敏感字符串加密密钥密码等)需要加强保护。

V8 字节码不保护字符串,但是 electron-vite 会将这些字符串转换为字符代码(使用 String.fromCharCode),使得这些字符串可以被 V8 字节码保护。

自定义保护

例如,只要保护 src/main/foo.ts

txt
.
├──src
│  ├──main
│  │  ├──index.ts
│  │  ├──foo.ts
│  │  └──...
└──...
.
├──src
│  ├──main
│  │  ├──index.ts
│  │  ├──foo.ts
│  │  └──...
└──...

你可以像下面一样修改你的配置文件:

js
import { defineConfig, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [bytecodePlugin({ chunkAlias: 'foo' })],
    build: {
      rollupOptions: {
        output: {
          manualChunks(id): string | void {
            if (id.includes('foo')) {
              return 'foo'
            }
          }
        }
      }
    }
  },
  preload: {
    // ...
  },
  renderer: {
    // ...
  }
})
import { defineConfig, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [bytecodePlugin({ chunkAlias: 'foo' })],
    build: {
      rollupOptions: {
        output: {
          manualChunks(id): string | void {
            if (id.includes('foo')) {
              return 'foo'
            }
          }
        }
      }
    }
  },
  preload: {
    // ...
  },
  renderer: {
    // ...
  }
})

示例

你可以通过演练 示例 来了解更多信息。

V8 字节码的局限性

V8 字节码不保护字符串,如果我们在 JS 代码中写死了一些加密密钥或其他敏感字符串,只要将 V8 字节码作为字符串阅读,还是能直接看到这些字符串内容的。

但是 electron-vite 可以将这些字符串转为字符代码,这样这些字符串就可以受到 V8 字节码的保护。例如:

js
// string in source code
const encryptKey = 'ABC'

// electron-vite will transform string to character codes
const encryptKey = String.fromCharCode(65, 66, 67)
// string in source code
const encryptKey = 'ABC'

// electron-vite will transform string to character codes
const encryptKey = String.fromCharCode(65, 66, 67)

源代码中要保护的字符串可以通过插件 protectedStrings 选项指定。

js
import { defineConfig, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [bytecodePlugin({ protectedStrings: ['ABC'] })]
  },
  // ...
})
import { defineConfig, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [bytecodePlugin({ protectedStrings: ['ABC'] })]
  },
  // ...
})

警告

不应该为了保护而枚举源代码中的所有字符串,通常我们只需要保护敏感字符串即可。

多平台构建

提示

不要期望可以在一个平台上为所有平台构建应用程序。

默认情况下根据当前 Electorn Node.js 版本和当前架构(例如 x86、x64、ARM 等)编译字节码。除了确保发布的 Electorn 应用程序的 Node.js 版本与编译时的版本相同外,架构是多平台构建的约束。

单一架构目标构建多平台

可能支持多平台构建:

  • 在 64 位的 MacOS 中构建 MacOS、Windows 或 Linux 的 64 位 Electorn 应用程序

单一平台构建多架构目标

例如,在 arm64 MacOS 中构建 MacOS 的 64 位应用程序,它会运行出错。因为默认构建的基于 arm64 的字节码无法在 64 位应用程序中运行。

但是,我们可以指定另一个配置文件并将环境变量 ELECTRON_EXEC_PATH 设置为(64 位)Electron 应用程序的路径。字节码编译器将使用指定的 Electron 应用程序进行编译。

js
// specify `electron.x64.vite.config.ts` for building x64 Electron app
import { defineConfig } from 'electron-vite'

export default defineConfig(() => {
  process.env.ELECTRON_EXEC_PATH = '/path/to/electron-x64/electron.app'

  return {
    // electron-vite config
  }
})
// specify `electron.x64.vite.config.ts` for building x64 Electron app
import { defineConfig } from 'electron-vite'

export default defineConfig(() => {
  process.env.ELECTRON_EXEC_PATH = '/path/to/electron-x64/electron.app'

  return {
    // electron-vite config
  }
})

提示

可以将 --arch 标志与 npm install 一起使用来安装其他架构的 Electron。

sh
npm install --arch=ia32 electron
npm install --arch=ia32 electron

当然这也是有限制的,因为需要运行 Electron 进程编译字节码,保证字节码是按照 Electron Node.js 版本生成的。但不同的架构不一定相互兼容。例如,64 位应用程序可以在 arm64 MacOS 中运行,但 arm64 应用程序无法在 64 位 MacOS 中运行。

可能支持多架构构建:

  • 在 arm64 MacOS 中构建 MacOS 的 64 位 Electorn 应用程序
  • 在 arm64 Windows 中构建 Windows 的 64 位 Electorn 应用程序
  • 在 64 位 Windows 中构建 Windows 的 32 位 Electorn 应用程序

警告

字节码与 CPU 无关。但是,你应该在部署前后运行测试,因为 V8 健全性检查包括一些与 CPU 支持的功能相关的检查,因此在极少数情况下这可能会导致错误。

常见疑问

对代码组织和编写的影响?

目前发现字节码方案对代码的唯一影响,是 Function.prototype.toString() 方法无法正常使用,原因是源代码并不跟随字节码分发,因此取不到函数的源代码。

对程序性能是否有影响?

对代码执行性能没有影响,略有提升。

对程序体积的影响?

对于只有几百 KB 的 Bundle 来说,字节码体积会有比较明显的膨胀,但是对于 2M+ 的 Bundle 来说,字节码体积没有太大的区别。

代码保护强度如何?

目前来说,还没有现成的工具能够对 v8 字节码进行反编译,因此该方案还是还是比较可靠且安全的。

Released under the MIT License