# AfterPack JavaScript 混淆器的工作原理

标准（Free）输出会对你的代码做什么、它如何抵抗还原，以及攻击者实际上能恢复出什么。

Source: https://www.afterpack.dev/zh/docs/concepts

AfterPack 是一个构建后的 JavaScript 保护工具。它接收你的工具链产出的 bundle，把它重写成一种人类或 AI 智能体不付出不成比例的代价就无法阅读、搜索或修改的形式。它用一行配置接入[你现有的构建](https://www.afterpack.dev/zh/docs/frameworks)，也可以完全不用插件，直接[在输出目录上](https://www.afterpack.dev/zh/docs/cli)运行。

## 为什么固定的混淆层会失效

大多数混淆器都会交付两样攻击者可以利用的东西：一层接一层叠加上去的若干层变换，以及工具一眼就能认出的预制结构（字符串数组，哪怕是加密过的；自定义的 VM 解释器；一眼可辨的解码器引导代码）。还原的方法是机械的：根据特征识别出某一层或某个结构，剥掉它，重复这一过程，直到原始代码露出来。`webcrack` 和 `restringer` 做的正是这件事，只需几微秒。

AfterPack 不交付层层叠加的变换，也不交付现成的解码器。每次构建的形态都不同，所以针对某次构建的模式编写的工具，到下一次构建就会失效。运行你代码的人仍然可以读到它使用的值。它与那些工具的[对比](https://www.afterpack.dev/zh/docs/comparison)单独成页。

*(animated fixed-pipeline vs per-build-shape comparison — see the web page)*

## AI 智能体改变了威胁

前沿模型不只是总结压缩过的代码：像 Claude Code 这样的智能体现在已经能对传统混淆器的输出进行去混淆。在一次公开实验中，一个智能体在大约十分钟内就把 `javascript-obfuscator` 和一款商业工具（JScrambler）的输出都还原成了可读的逻辑（[这次运行的完整记录在我们的博客上](https://www.afterpack.dev/zh/blog/ai-deobfuscates-javascript)）。

改名、字符串数组和几层变换已经换不来保护：它们正是这些工具和模型最擅长处理的模式。[威胁模型](https://www.afterpack.dev/zh/docs/threat-model)对哪种对手能击败哪种技术做了排序。对模式类工具仍然有效的是每次构建都换一种形态：为一次构建写的识别器，到下一次就得重写。

## 标准混淆已经做了什么

AfterPack 通过[框架插件](https://www.afterpack.dev/zh/docs/frameworks)或[构建后 CLI](https://www.afterpack.dev/zh/docs/cli#what-a-run-does)，在打包工具之后、针对已构建的输出运行，从不触碰你的源代码。[快速上手](https://www.afterpack.dev/zh/docs/quickstart)是它的五分钟版本。

*(build flow diagram — see the web page)*

Free 会应用以下全部内容：

- **它会编码你的字符串字面量。**在任何正数的[复杂度目标](https://www.afterpack.dev/zh/docs/config#complexity)下，[字符串下限](https://www.afterpack.dev/zh/docs/config#strings-encode)都是开启的，因此字符串字面量会被重写为一个由[种子](https://www.afterpack.dev/zh/docs/config#seed)选定的运行时解码器，而不是以文本形式留在产物中。这是默认开启的，也是它与压缩代码之间最大的区别。指令序言、`typeof` 的比较对象、运行时需要的属性名以及导出名会保持可读，少量源字符串值可能会残留在高度压缩的第三方 bundle 中；构建会报告有多少个留了下来，[Protection Map](https://www.afterpack.dev/zh/docs/protection-map) 会把它们列出来。
- **它把你的逻辑重写为现成工具无法还原回去的[等价](https://www.afterpack.dev/zh/docs/semantic-contract)形式**：混合布尔算术、不透明谓词、整数表达式字节码 VM。静态阅读者花费真正的功夫仍能还原出你的原始程序，而针对某一次构建的形态编写的工具，到下一次构建就会失效。[`transforms.<kind>.enabled`](https://www.afterpack.dev/zh/docs/config#transforms-kind-enabled) 用于开关各个变换族。
- **它重塑控制流和结构**：控制流扁平化、死分支、作用域加深、交叉依赖纠缠。
- **它每次构建都会改变一切**，靠的是逐构建的[多态性](#a-new-program-every-build)（见下文）。

零配置默认值是 [`light`](https://www.afterpack.dev/zh/docs/config#preset)：[字符串下限](https://www.afterpack.dev/zh/docs/config#strings-encode)加上重命名的标识符，不做结构[膨胀](https://www.afterpack.dev/zh/docs/config#inflation-max)。把[预设](https://www.afterpack.dev/zh/docs/presets)提高到 `medium` 或以上，会加入上面列出的结构性工作；无论哪个预设，字符串的可读性都已经处于下限。以上所有内容在每个层级上都会运行，包括 Free；Free 与 Pro 的边界见[套餐](https://www.afterpack.dev/zh/docs/tiers)。

## 每次构建都是一个新程序

AfterPack 的输出由一个逐构建的[种子](https://www.afterpack.dev/zh/docs/config#seed)推导而来，默认是随机的。同一份源码的两次构建在结构上各不相同：名称不同，编码不同，处处形态都不同。

从 `medium` 起，这远不止是表面上的重新洗牌。AfterPack 改变的是程序本身的结构，而不只是它的文本：控制流被扁平化并重新排序，值在每次使用时重新计算而不是被存储，bundle 中原本相互独立的部分在输出中彼此纠缠。交付出去的是你所写代码的一个功能等价物，一个[行为相同](https://www.afterpack.dev/zh/docs/semantic-contract)的不同程序，而种子决定了这一次你得到的是哪一个等价物。

所以针对某次构建的模式编写的工具到下一次就会失效，补丁每次发布都得重新定位。运行代码的人仍然可以读到它使用的值。频繁发布会让这份代价每次都重来一遍。[构建与发布](https://www.afterpack.dev/zh/docs/builds)介绍了如何把它变成习惯。

如果需要可复现的输出，比如为了缓存命中或审计，可以在 [CLI](https://www.afterpack.dev/zh/docs/cli#flags) 上用 [`--seed=git`](https://www.afterpack.dev/zh/docs/config#seed)，或在插件中用 [`seed: "git"`](https://www.afterpack.dev/zh/docs/config#seed) 固定种子。两者都从你的提交推导出种子。参见[构建与发布](https://www.afterpack.dev/zh/docs/builds)。

## 攻击者实际上能恢复什么

并不是每个变换都以同样的方式提高还原代价。AfterPack 把它的保护分成若干[还原类](https://www.afterpack.dev/zh/docs/reversal-classes)；知道某个区域由哪一类覆盖，就知道攻击者最终能拿走什么。

这些是面向静态攻击者的分类：它们描述的是阅读字节的人所面对的情况，比如 grep、AST 工具，或者不运行代码、只解释代码的 LLM。在被插桩的运行时下运行产物的攻击者是一个独立的、更强的情形，在下文的[范围](#scope)以及[威胁模型](https://www.afterpack.dev/zh/docs/threat-model)页面中讨论。

无论使用哪个预设，构建在整个 bundle 上都停留在重命名与编码类别；更重的预设会在这一类别中做更多工作，而不是进入另一个类别。[Protection Map](https://www.afterpack.dev/zh/docs/protection-map) 按复杂度为每个词元着色，并标出你所选代码所属的类。

[区域覆盖](https://www.afterpack.dev/zh/docs/directives#what-you-can-set) [`preset=hard`](https://www.afterpack.dev/zh/docs/config#preset) 和 [`preset=extreme`](https://www.afterpack.dev/zh/docs/config#preset) 会提高一个区域的[复杂度目标](https://www.afterpack.dev/zh/docs/config#complexity)，并强制开启其[字符串下限](https://www.afterpack.dev/zh/docs/config#strings-encode)，把同样的工作集中到你指定的地方。

## 密钥如何得以保留

保护一个密钥所用的机制，与重塑你的代码不同。一个密钥（API 密钥、许可证种子、一个阈值）会出现在任何行为相同的程序中，所以重塑它周围的代码并不能把它藏起来。

让密钥对静态阅读者或盲眼 LLM 阅读者不可见的是**非物化**：这个值从不出现在输出中。[`transforms.comparisonHardening.enabled`](https://www.afterpack.dev/zh/docs/config#transforms-kind-enabled) 为你的代码用于比较的字面量提供这一能力，它把比较重写为一次单向摘要校验，只有摘要会被发布。对密钥字面量使用 [`preset=extreme`](https://www.afterpack.dev/zh/docs/config#preset) 会让恢复它的代价变得高昂，而该字面量仍然存在于输出中。

在被插桩的运行时下运行你代码的攻击者，能拿到代码本身用到的任何密钥。没有任何构建期混淆工具能改变这一点：如果运行中的程序需要这个值，一个坚定的动态攻击者就能观察到它。非物化能击败静态阅读者和盲眼 LLM 阅读者，并在每次轮换时提高重新提取的代价。它不会让客户端密钥变得无法恢复。高价值的密钥应该放在服务端，客户端永远不持有它们。

## 范围

一个放在 [`/* @afterpack preset=hard */`](https://www.afterpack.dev/zh/docs/config#preset) 区域中的许可证校验，配合频繁发布和不断变化的[种子](https://www.afterpack.dev/zh/docs/config#seed)，不值得花工程时间在下一次发布到来之前把它破解掉。对大多数产品来说，这就是合适的标准。

这种防御是经济上的。只要时间不受限制，任何客户端代码都能在某种程度上被还原。目标是把代价推高到超过价值，并让这份代价在每次发布时重复出现。当某项资产的价值超过任何还原所需的投入时，把它留在服务端，客户端永远不持有它。AfterPack 强化这种架构；[它不能替代这种架构](https://www.afterpack.dev/zh/docs/threat-model#what-afterpack-does-not-replace)。[最佳实践指南](https://www.afterpack.dev/zh/docs/best-practices)介绍了如何把保护用在关键之处。

## 下一步

- [还原类](https://www.afterpack.dev/zh/docs/reversal-classes)：深入讲解三类表格，逐个变换说明。
- [威胁模型](https://www.afterpack.dev/zh/docs/threat-model)：A0 到 A5 的对手阶梯，以及静态与动态这条轴。
- [Protection Map](https://www.afterpack.dev/zh/docs/protection-map)：按令牌展示的视图，证明哪一类作用于哪一行。
- [最佳实践](https://www.afterpack.dev/zh/docs/best-practices)：标记什么、放过什么，以及如何轮换构建。
- [套餐](https://www.afterpack.dev/zh/docs/tiers)：完整的 Free / Pro / Team / Enterprise 边界。
- [快速上手](https://www.afterpack.dev/zh/docs/quickstart)：安装插件，几分钟内发布受保护的输出。
- [框架](https://www.afterpack.dev/zh/docs/frameworks)：适用于你技术栈的插件，或者没有插件时的 CLI 路径。
- [配置](https://www.afterpack.dev/zh/docs/config)：上文提到的每一个键，以及哪些形式可以设置它。
- [预设](https://www.afterpack.dev/zh/docs/presets)：`minify`/`light`/`medium`/`hard`/`extreme` 阶梯。
- [指令](https://www.afterpack.dev/zh/docs/directives)：`preset=hard` 和 `skip` 背后的标记语法。
