文档
/
实用方案
/

Bytecode Array Encoding 密钥

外置 Bytecode Array Encoding 密钥

Pro

使用 vmBytecodeArrayEncodingKey 提供您自己的 VM 字节码加密密钥,并在运行时通过 key getter 交还它:密钥不放在包中,而是从客户端存储读取,或从您的后端获取。

观看

Obfuscator.io Async Executor: Async Bytecode Key Getter and Async-Only VM Virtualization

在 YouTube 观看

Obfuscator.io Custom Bytecode Key: Compile-Time Key and Runtime Key Getter (Cookie, Fetch, Decoy Key)

在 YouTube 观看

这些选项的作用

vmBytecodeArrayEncoding 会加密 VM 字节码数组,使其不以明文形式出现在输出中。默认情况下,加密密钥由环境派生,并在客户端重建,因此您无需亲自处理它。这很方便,但密钥材料仍然存在于包中。

有两个选项可以让您把密钥从包中取出,由自己掌控:

  • vmBytecodeArrayEncodingKey - 您在编译时提供的密钥。设置后,它会取代默认的环境派生密钥,并且不会嵌入混淆后的输出。
  • vmBytecodeArrayEncodingKeyGetter - 一个在运行时返回同一密钥的 JavaScript 表达式。它会被原样嵌入,并在混淆代码加载时于浏览器中求值。

关键在于分离:由于密钥不在代码中,对包进行纯静态扫描无法恢复它。代码要运行,密钥在运行时仍必须存在,因此它并非真正的秘密,但它从哪里来、谁能看到它,由您决定。

这两个选项是一对,并且混淆器会强制执行这一点:只设置其中一个会导致构建时校验错误(只设置 vmBytecodeArrayEncodingKey 时,运行时无法获取密钥;只设置 getter 时,没有可匹配的编译时密钥)。此外,只有在字节码数组确实被加密时它们才会起作用,因此还必须开启 vmBytecodeArrayEncoding: true,或者开启 vmSelfDefending: true,它会在内部强制开启 vmBytecodeArrayEncoding。请同时设置这两个密钥选项,并配合上述其中一项。

两个密钥如何组合

您的密钥从不单独使用,在两端它都会与一个由混淆器控制的内部密钥混合:

  • 编译时。 vmBytecodeArrayEncodingKey 会与混淆器派生的内部密钥组合,字节码数组使用组合后的混合密钥进行编码。
  • 运行时。 您的 vmBytecodeArrayEncodingKeyGetter 解析出的值,会与同一个内部密钥(在客户端根据多种运行时因素重建)组合,用于解码字节码。

由于两端都把您的密钥与内部密钥混合,getter 必须解析为与您传给 vmBytecodeArrayEncodingKey 的完全相同的字符串。任何一部分单独都不足以解码:没有内部密钥,您的密钥无法解码字节码;没有您的密钥,内部密钥也毫无用处。正因如此,控制谁能拿到您的密钥,才是真正保护代码的关键。

在运行时提供密钥

默认情况下 getter 是同步的:表达式必须在混淆代码加载时立即返回密钥。可以从客户端上已有的任何来源读取它:cookie、localStorage、全局变量,或由服务端注入的 DOM 元素。

JavaScript

密钥必须在混淆代码运行之前就已存在:

JavaScript

其他同步来源的用法相同,选择您的应用已经填充的那个即可:

JavaScript

不要把密钥放在与混淆代码相同的文件或脚本中。把它内联在那里会让这一切失去意义:对包的静态扫描将同时恢复代码及其密钥。请把它存放在单独的来源中,并通过环境变量或密钥管理注入编译时的 vmBytecodeArrayEncodingKey,而不要把它提交到代码库。

从后端获取密钥(异步)

需要 vmAsyncExecutor · v7.3.0+

同步 getter 只能读取客户端上已有的内容。要从您的服务器获取密钥,以便用身份验证加以控制并随时撤销,getter 必须是异步的,而这需要 vmAsyncExecutor。启用 Async Executor 后,getter 可以返回一个 Promise,VM 会在运行前等待它完成。

启用 vmAsyncExecutor 还会缩小虚拟化的范围:在这种模式下,只有 最外层的 async 函数 会被编译进 VM,同步代码不会被虚拟化(其余混淆仍然适用)。如果您的程序大部分是同步的,请把需要保护的代码包装在一个 async 函数中,使其仍能得到覆盖;完整规则请参阅 vmAsyncExecutor。

JavaScript

返回 Promise 的 getter 必须配合 vmAsyncExecutor。这一点无法在构建时检查,因此在关闭 vmAsyncExecutor 的情况下使用 Promise getter 会在运行时失败。

在服务端,根据您的应用所信任的依据来决定返回哪个密钥,例如经过验证的会话、许可证检查等。仅凭 Origin 或 Referer 并不能认证调用方,而且同源的 GET 请求可能不带 Origin。巧妙之处在于:不要拒绝不受信任的调用方,而是返回一个错误的密钥(下文的 VM_DECOY_KEY)。这样受保护的代码会自行失败(报错、结果错误,或页面停止响应),这比一个明显的 401 更隐蔽,后者会准确地告诉攻击者该绕过什么。

请禁用响应缓存,确保一个调用方的密钥永远不会返回给另一个调用方。把密钥与正确的构建版本绑定,并让密钥和包一起部署。收到真实密钥的客户端仍然可以在运行时查看它。

JavaScript

此端点返回的字符串必须与您在构建时传给 vmBytecodeArrayEncodingKey 的完全相同。上面的 getter 请求的是一个 相对 URL(/api/vm-key),因此托管在其他源上的包副本会向那个源请求 /api/vm-key,永远拿不到您的密钥;而诱饵密钥是调用方 确实 访问到您的端点、但没有受信任会话时得到的结果(未经身份验证的请求,或通过您的源代理的被盗包)。无论哪种情况,真实密钥都不会送达,受保护的代码也不会运行。

什么算"有效"完全取决于具体应用:已认证的会话、签名的许可证,或它们的任意组合。无论密钥由什么产生,只要把它包装在 Promise 中,异步执行器就会在运行 VM 之前等待它。

密钥不匹配时

只有当 getter 返回的密钥与混淆时使用的完全一致时,混淆代码才能正常工作。如果密钥不同,或 getter 返回 undefined、null 或空字符串,代码会在运行时失败:产生错误的结果、抛出普通的运行时错误,或停止响应。

这里特意没有专门针对密钥的错误信息:密钥错误与其他任何运行时故障无法区分。因此,如果某个受 VM 保护的包只在启用此选项后才抛出错误、返回错误结果或卡死,请先检查密钥链路:getter 在页面上能否解析、是否返回非空字符串,以及返回值是否与您构建时使用的相同。