外部化字节码数组编码密钥
使用 vmBytecodeArrayEncodingKey 提供你自己的 VM 字节码加密密钥,并通过密钥 getter 在运行时将其交还 —— 保存在包之外、从客户端存储读取,或从你的后端获取。
这些选项的作用
vmBytecodeArrayEncoding 会加密 VM 字节码数组,使其不会以明文形式出现在
输出中。默认情况下,加密密钥从环境派生并在客户端重建,因此你无需亲自处理它。这很方便,但密钥材料仍然存在于
包中。
有两个选项可以让你把密钥从包中取出并由自己掌控:
vmBytecodeArrayEncodingKey—— 你在编译时提供的密钥。设置后,它会取代默认的环境派生密钥使用,并且不会 嵌入到混淆后的输出中。vmBytecodeArrayEncodingKeyGetter—— 一个在运行时返回该相同密钥的 JavaScript 表达式。它会被原样嵌入, 并在混淆后的代码加载时于浏览器中求值。
关键在于分离:由于密钥不在代码中,对包进行纯粹的静态扫描无法恢复它。为了让代码运行,密钥仍然必须在运行时 存在,因此它并非真正保密 —— 但你可以决定它来自何处以及谁能看到它。
两个密钥如何结合
你的密钥从不单独使用 —— 在两端它都会与混淆器控制的内部密钥混合:
- 编译时。
vmBytecodeArrayEncodingKey会与混淆器派生的内部密钥结合,然后用得到的混合密钥对字节码数组进行 编码。 - 运行时。 你的
vmBytecodeArrayEncodingKeyGetter解析出的值会与相同的内部密钥结合(该内部密钥根据各种运行时 因素在客户端重建),以此解码字节码。
由于两端都会将你的密钥与内部密钥混合,getter 必须解析为你作为 vmBytecodeArrayEncodingKey 传入的完全相同的
字符串。任何一部分单独都不够:没有内部密钥,你的密钥无法解码字节码;没有你的密钥,内部密钥也毫无用处 ——
这正是为什么控制谁能收到你的密钥才是真正保护代码的关键。
在运行时提供密钥
默认情况下,getter 是同步的:当混淆后的代码加载时,表达式必须立即返回密钥。可以从任何已经存在于客户端的来源
读取 —— cookie、localStorage、全局变量,或服务器注入的 DOM 元素。
JavaScriptObfuscator.obfuscate(sourceCode, {
vmObfuscation: true,
vmBytecodeArrayEncoding: true,
vmBytecodeArrayEncodingKey: process.env.VM_KEY, // e.g. 'mySecretKey123'
vmBytecodeArrayEncodingKeyGetter: "window.__VM_KEY__" // returns the key at runtime
});
密钥必须在混淆后的代码运行之前就存在:
// Set by a different script, a server-injected inline script, etc.
window.__VM_KEY__ = 'mySecretKey123';
其他同步来源的工作方式相同 —— 选择你的应用已经填充好的那一种:
// From a cookie
vmBytecodeArrayEncodingKeyGetter: "document.cookie.match(/vmKey=([^;]+)/)?.[1]"
// From localStorage
vmBytecodeArrayEncodingKeyGetter: "localStorage.getItem('vmKey')"
// From a server-injected meta tag
vmBytecodeArrayEncodingKeyGetter: "document.querySelector('meta[name=\"vm-key\"]').content"
// From a nested object
vmBytecodeArrayEncodingKeyGetter: "window.config.encryption.key"
从你的后端获取密钥(异步)
需要 vmAsyncExecutor · v7.3.0+同步 getter 只能读取已经在客户端上的内容。若要从你的服务器获取密钥 —— 以便将其置于身份验证之后进行门控并
可撤销 —— getter 就必须是异步的,而这需要 vmAsyncExecutor。启用异步
执行器后,getter 可以返回一个 Promise,VM 会在运行前等待它。
JavaScriptObfuscator.obfuscate(sourceCode, {
vmObfuscation: true,
vmAsyncExecutor: true,
vmBytecodeArrayEncoding: true,
vmBytecodeArrayEncodingKey: process.env.VM_KEY, // kept on your server, not in the bundle
vmBytecodeArrayEncodingKeyGetter:
'fetch("/api/vm-key", { credentials: "include" }).then((res) => res.text())'
});
在服务器上,根据你的应用所信任的任何依据来决定返回哪个密钥 —— 有效会话、预期的 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
);
});
从这个端点提供的字符串,必须与你在构建时作为 vmBytecodeArrayEncodingKey 传入的完全相同。在你的环境之外运行的
包副本会拿到诱饵密钥,解密后一无所获,从而失效。
当密钥不匹配时
只有当 getter 返回与混淆期间所用完全相同的密钥时,混淆后的代码才能工作。如果密钥不同 —— 或者 getter 返回
undefined、null 或空字符串 —— 解密就会产生错误的密钥流,代码会在运行时以乱码输出或普通的运行时错误而失败。
这里有意不提供任何独特的、针对密钥的错误消息:一个失败的密钥与任何其他运行时故障无法区分。因此,当一个受 VM 保护的包只在启用此选项后才抛出异常时,请先检查密钥路径 —— 确认 getter 在页面上能够解析、返回非空字符串,并且 返回与你构建时所用相同的值。
