对Electron源码保护方案讨论由来已久,但是官方并没有打算提供解决方案。
作者们认为,无论用什么形式去加密打包文件,密钥总归是需要放置在包里的。不过,官方推荐使用V8字节码和node addon插件的形式进行保护。
V8 字节码主要是通过 Node 标准库里的 vm
模块,可以从 script 对象中生成其缓存数据。该缓存数据可以理解为 v8 的字节码,该方案通过分发字节码的形式来达到源代码保护的目的。
V8 字节码是 V8 JavaScript 引擎的一种形式,它是用于优化和执行 JavaScript 代码的一种中间表示形式。当 JavaScript 代码在 V8 引擎中执行时,它首先经历了解析和编译阶段,然后被转换成字节码以供执行。
字节码是一种介于高级源代码和底层机器代码之间的中间表示形式。它比源代码更接近底层机器代码,但仍具有某种程度的可移植性和可读性。V8 引擎将 JavaScript 代码编译成字节码,以便更高效地执行代码。字节码可以被即时编译器(JIT)进一步优化,转换为本地机器代码以加速执行过程。
所以,如果我们通过 V8 字节码运行代码,不仅能够起到代码保护作用,还对性能有一定的提升。
如果你用的是electron-vite
作为脚手架来构建你的应用程序,那么你可以通过 bytecodePlugin
插件来开启字节码保护:
// electron.vite.config.js
import { defineConfig, bytecodePlugin } from 'electron-vite'
export default defineConfig({
main: {
plugins: [bytecodePlugin()]
},
preload: {
plugins: [bytecodePlugin()]
},
renderer: {
// ...
}
})
但是开启 V8 字节码后也会有一些缺陷产生,比如:Function.prototype.toString
代码转字节码后会导致程序异常,所以尽量不要在项目中使用这个方法或依赖了使用这个方法的库,这通常有点难。
另外,v8 字节码不保护字符串,如果我们在 JS 代码中写死了一些数据库的密钥等信息,只要将 v8 字节码作为字符串阅读,还是能直接看到这些字符串内容的。当然,简单一点的方法就是使用 Binary 形式的非字符串密钥。
本文的重点主要是介绍第二种方案,即node addon(原生模块)。
我们知道,Electron 可以调用和执行一些 Node
原生模块和 Node
扩展程序,因此,我们可以将一些业务核心代码(证书、秘钥、加解密)通过 C++
编写,然后通过 node-gyp
构建成 .node
的二进制文件再提供给应用程序使用。
这个方法确实能解决对核心业务的安全防护能力,但是需要一定的 C++
开发经验。
所以,这里介绍如何使用rust
来构建原生模块,实现重要逻辑的保护。
Rust
和 C++
虽然都可以用于开发 Node 的原生扩展,但它们有一些不同之处,其中 Rust
相对于 C++
有一些优势:
- 内存安全性:
Rust
在语言级别提供了内存安全性,通过借用检查器(Borrow Checker)可以避免常见的内存安全问题,如空指针引用、内存泄漏等。这意味着在编写Rust
扩展时,更容易避免许多常见的安全漏洞。 - 性能:
Rust
以及其所提供的内存安全性和零成本抽象,可以带来出色的性能。它可以通过其强大的编译器优化产生高效的机器码,这在某些情况下可能比C++
更高效。 - 生态系统:虽然
C++
有着长期的历史和庞大的生态系统,但Rust
作为一门新兴语言,拥有逐渐壮大的社区和生态系统。它的包管理器Cargo
提供了便捷的依赖管理和构建工具。
为了使用 Rust
开发 Nodejs Addon
,你需要使用到 NAPI-RS 这个库。
NAPI-RS 是一个用于 Node.js Addon API(N-API)的 Rust 绑定库。通过使用 NAPI-RS,开发者可以充分利用 Rust 强大的性能和安全性,并与 Node.js 生态系统无缝集成,为 Node.js 应用程序编写高性能、高质量的原生扩展。
N-API 是 Node.js 提供的一个稳定的原生 API,允许开发者用 C、C++ 或 Rust 等编程语言编写 Node.js 的原生插件,而不会受到 Node.js 版本变化的影响。NAPI-RS 充分利用了 Rust 的特性,并提供了一种在 Rust 中编写 Node.js 原生插件的简洁而强大的方式。
以上看到其平台支持是很完备的,你编写的功能甚至能移植到 Android 设备上,此外还支持编译到 WASM
,在浏览器等环境中使用。
接下来,我们来一起了解一下如何通过 Rust
来开发一个可以在 Electron
中使用的 Nodejs
原生扩展程序。
默认你安装了
Node.js
和Electron
。
- 首先我们要安装 Rustup,使用 Rustup 官方的安装器即可。
rustup 是 rust 的版本管理器,类似于 nodejs 下的 nvm。安装 Rust - 点击跳转官网
- 安装 Rust。命令行输入以下命令即可。
rustup install nightly
自动下载安装后,命令行输入 cargo ,可以看到 Rust 开发工具链已经安装完毕。
- 安装 NAPI-RS
npm install -g @napi-rs/cli
- 命令行输入:
napi new
输入你的拓展名称,并选择需要支持的平台。
完成导航后,NAPI-RS 会自动写入项目文件:
我们查看目录后发现,实际上这就是一个 NPM 包:
唯一不同的是,src
目录下是 .rs
后辍的文件。
这里 .rs
后辍的文件,就是 rust 的源代码文件。
默认生成的代码这里简化了,这里所实现的是一个简单的加和功函数,用 #[napi]
进行标记,以暴露给 JS 调用:
// 导入依赖
use napi_derive::napi;
#[napi] // 标记该函数需要暴露给 JS 调用
pub fn sum(a: i32, b: i32) -> i32 {
return a + b;
}
以上我们已经在 Rust 世界里创建了一个加和函数,那么如何在 JavaScript 世界中使用呢?
很简单,我们尝试运行编译命令:
npm run build
发现项目目录下多了三个文件:
.node
结尾的文件名会因平台架构和操作系统而异,这是原生拓展的本体,一个二进制文件。
在项目目录下运行 Nodejs ,并尝试直接引入该原生拓展:
刚刚在 Rust 世界里创建的加和函数,现在能在 JS 的世界里为我们所用了。
该二进制文件无法逆向出代码,也就是说你可以将需要保护的代码例如用户认证、数据加密等逻辑“藏”在原生模块中。
那另外两个文件呢?
index.js
:提供跨平台兼容性,识别系统架构并加载正确的二进制原生拓展。index.d.ts
:这个文件是 NAPI-RS 基于 Rust 代码生成的 TS 类型定义文件。
接下来,你就可以在任意 Node.js 项目或是 Electron 项目中以以上方式导入并使用该原生模块。