从后端获取字节码数组密钥
让 VM 字节码解密密钥不出现在客户端:在加载时用异步密钥获取器从您的后端拉取它,并置于鉴权之后,从而让被窃取的包无法运行。
问题
vmBytecodeArrayEncoding 用一个密钥对 VM 字节码加密。如果该密钥随包
一起分发,任何拿到该文件的人就掌握了解密它所需的一切。vmBytecodeArrayEncodingKeyGetter 让
密钥可以存放在别处,并在运行时产生 —— 但同步获取器只能读取客户端上已经存在的
内容(一个全局变量、一个 cookie、localStorage)。要从您的服务器获取密钥 —— 以便您能将其置于
鉴权之后并可撤销 —— 获取器就必须是 异步的。
解决方案
启用 vmAsyncExecutor(v7.3.0+)。有了异步执行器,密钥获取器就可以
返回一个 Promise,因此它能在 VM 运行之前从您的后端 fetch 密钥。三个选项协同工作:
vmBytecodeArrayEncoding: true—— 对字节码数组加密。vmBytecodeArrayEncodingKey—— 编译期使用的密钥(保存在您的服务器上,而非包内)。vmBytecodeArrayEncodingKeyGetter—— 一个在运行时返回同一密钥的 JS 表达式。在启用vmAsyncExecutor时它可以返回Promise;不启用时获取器必须同步返回密钥。
客户端混淆配置
JavaScriptObfuscator.obfuscate(sourceCode, {
vmObfuscation: true,
vmAsyncExecutor: true,
vmBytecodeArrayEncoding: true,
vmBytecodeArrayEncodingKey: process.env.VM_KEY, // e.g. 'mySecretKey123'
vmBytecodeArrayEncodingKeyGetter:
'fetch("/api/vm-key", { credentials: "include" }).then((res) => res.text())'
});
获取器表达式会被原样嵌入并在浏览器中求值。请让编译期的
vmBytecodeArrayEncodingKey 不出现在您的客户端代码仓库中 —— 在构建时从环境变量或密钥注入它,
并从下面的端点提供完全相同的字符串。
两个密钥如何协作
您的密钥从不单独使用 —— 在两侧它都会与混淆器控制的一个内部密钥混合:
- 编译期。
vmBytecodeArrayEncodingKey会与混淆器推导出的一个内部密钥结合,然后用 由此得到的混合密钥对字节码数组进行编码。 - 运行时。 您的
vmBytecodeArrayEncodingKeyGetter解析出的值 —— 由您的服务器返回 —— 会与 同一个内部密钥(在客户端从各种运行时因素中重建)结合,以解码字节码。
由于两侧都会将您的密钥与内部密钥混合,获取器必须解析为与您作为
vmBytecodeArrayEncodingKey 传入的 完全相同的字符串。任一部分单独都不够:没有内部密钥,您的密钥无法解码
字节码;而没有您的密钥,内部密钥也毫无用处 —— 这正是为什么只向已鉴权的调用方提供您的密钥
能让被窃取的包无法运行。
服务端
该端点根据您的应用所信任的任何依据来决定返回哪个密钥 —— 有效会话、预期的
Origin 或 Referer、授权校验,等等。妙处在于:与其拒绝不受信任的调用方,不如返回一个
错误的 密钥。字节码随后会解码成乱码,受保护的代码便自行失败,这比一个
明显的 401(它会直接告诉攻击者该绕过什么)更为隐蔽。
// Express example — the exact checks depend on your app
app.get('/api/vm-key', (req, res) => {
const origin = req.get('origin');
const trusted =
req.session?.user && // a valid session, and
origin === 'https://app.example.com'; // the expected production origin
res.type('text/plain').send(
// Real key for valid users; a decoy for everyone else
// (no session, or a localhost / unexpected origin).
trusted ? process.env.VM_KEY : process.env.VM_DECOY_KEY
);
});
