文档
/
实用方案
/

字节码数组编码密钥

外部化字节码数组编码密钥

Pro

使用 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())'
});

在服务器上,根据你的应用所信任的任何依据来决定返回哪个密钥 —— 有效会话、预期的 OriginReferer、许可证 检查等等。诀窍在于:与其拒绝不受信任的调用者,不如返回一个错误的密钥。这样字节码就会解码成乱码,受保护的 代码会自行失败,这比返回一个明显的 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 返回 undefinednull 或空字符串 —— 解密就会产生错误的密钥流,代码会在运行时以乱码输出或普通的运行时错误而失败。

这里有意不提供任何独特的、针对密钥的错误消息:一个失败的密钥与任何其他运行时故障无法区分。因此,当一个受 VM 保护的包只在启用此选项后才抛出异常时,请先检查密钥路径 —— 确认 getter 在页面上能够解析、返回非空字符串,并且 返回与你构建时所用相同的值。