# AES 的 PKCS#7 填充：原理、示例与安全解填充

> 逐字节理解 PKCS#7 填充，掌握完整分组填充、验证规则、常见错误，以及 AES-CBC 中的安全解填充方法。

Canonical: https://hsmkit.com/zh/guides/pkcs7-padding-aes/

Last updated: 2026-09-22

AES 每次处理 16 字节分组，因此 CBC 模式需要一套可逆规则来处理长度不是 16 倍数的明文。PKCS#7 通过追加字节完成填充，每个填充字节的值都表示填充长度。规则本身很简单，但错误验证可能产生严重的填充预言机漏洞。

## PKCS#7 填充规则

设分组大小为 `B` 字节、明文长度为 `L`，需要添加的字节数为：

```text
N = B - (L mod B)
```

追加 `N` 个字节，每个字节的数值都为 `N`。AES 的 `B` 固定为 16，所以 `N` 的取值范围是 1–16。

### 示例：5 字节明文

ASCII 字符串 `HELLO` 长 5 字节，需要追加 11 个 `0x0B`：

```text
48 45 4C 4C 4F 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B
```

解密后，最后一个字节表示追加了 11 字节。删除前必须确认最后 11 个字节全部等于 `0x0B`。

## 为什么整分组明文仍需填充

当明文长度已经是 16 的倍数时，PKCS#7 仍会添加完整填充分组：

```text
10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
```

如果不添加该分组，接收方无法判断末尾的 `0x01` 是真实数据还是填充。PKCS#7 要求每条消息都包含填充，从而避免歧义。

这也是常见长度误区：16 字节明文使用 AES-CBC + PKCS#7 后会形成两个密文分组，此外还需单独计算 IV 的长度。

## 有效与无效填充

对 AES 而言，以下结尾有效：

```text
... 01
... 02 02
... 03 03 03
... 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
```

以下结尾无效：

```text
... 00             # 0 不是有效填充长度
... 02 03          # 字节与声明长度不一致
... 11 ...         # 17 超过 AES 的 16 字节分组
```

解码器必须拒绝无效填充。只读取最后一个值便直接裁剪，会导致数据损坏并降低安全性。

## PKCS#5 与 PKCS#7 的区别

API 经常混用这两个名称，但原始定义不同：

- PKCS#5 填充最初针对 8 字节分组；
- PKCS#7/CMS 填充支持 1–255 字节分组；
- AES 分组是 16 字节，因此准确名称是 PKCS#7 填充。

部分 Java Provider 对 AES 暴露 `PKCS5Padding` 名称，内部实际执行通用 PKCS#7 规则。互操作时应查阅 Provider 文档，而不是只看算法字符串。

## 哪些 AES 模式需要填充

| 模式 | 是否需要 PKCS#7 | 原因 |
|---|---|---|
| CBC | 任意长度数据需要 | 只处理完整分组 |
| ECB | 任意长度数据需要 | 只处理完整分组 |
| CTR | 不需要 | 输出字节流 |
| GCM | 不需要 | 基于计数器的认证模式 |
| CFB / OFB | 不需要 | 以类似流密码方式运行 |

如果密码库已经自动应用 PKCS#7，不要再手工填充。双重填充会导致解填充一次后仍残留一层填充数据。

## 填充预言机风险

填充不等于认证。在 CBC 中，修改一个密文分组会以可预测方式改变下一个解密明文分组。如果服务会泄露填充是否有效，攻击者就能通过大量请求逐字节恢复明文，而无需知道密钥。

泄露信号可能是直接或间接的：

- 无效填充与无效 JSON 返回不同错误；
- 使用不同 HTTP 状态码；
- 响应时间存在可测差异；
- 解密失败后的连接行为不同。

首选修复方案是改用 AES-GCM 等认证加密。必须兼容 CBC 时，应采用 Encrypt-then-MAC，对版本、IV 与密文计算认证码，并在解密前验证 MAC。两种模式的差异见 [AES-GCM 与 AES-CBC 对比](/zh/guides/aes-gcm-vs-cbc/)。

## 安全解填充

应用代码通常应调用维护良好的密码库，而不是自己实现解填充。正确流程应当：

1. 在 CBC 解密前完成密文认证；
2. 确认解密结果长度非零且是分组大小的倍数；
3. 读取最后一个填充长度，要求在 1–16 之间；
4. 检查声明范围内每个填充字节；
5. 对所有无效加密记录返回统一错误。

不要记录解密字节或详细填充失败原因。详细诊断适合隔离测试，不应出现在攻击者可观察的生产响应中。

## 互操作排查清单

两个系统的 AES-CBC 结果不一致时，依次确认：

- 原始密钥字节与密钥长度完全相同；
- 使用相同的 16 字节 IV；
- 明文字符编码相同；
- 都使用 PKCS#7，而不是零填充或不填充；
- 密文编码都是 Hex 或都是 Base64；
- IV 是单独传输还是统一前置；
- 没有把认证标签或 HMAC 当作密文的一部分。

字符串字符数不等于字节数。包含中文等非 ASCII 字符的 UTF-8 文本会占用更多字节，因此填充长度也不同。

## 长度示例

| 明文字节数 | 填充字节 | 填充后长度 |
|---:|---:|---:|
| 0 | 16 个 `0x10` | 16 |
| 1 | 15 个 `0x0F` | 16 |
| 15 | 1 个 `0x01` | 16 |
| 16 | 16 个 `0x10` | 32 |
| 31 | 1 个 `0x01` | 32 |
| 32 | 16 个 `0x10` | 48 |

可以使用 [AES 加密工具](/aes-encryption)配合测试数据比较明文、IV 与密文编码。新系统应优先选择不需要填充的认证加密模式。
