用 AES 加密一段 100 字节的文本时,会遇到一个小麻烦:AES 的分组长度是 16 字节,而 100 不是 16 的整数倍(100 ÷ 16 = 6 余 4)。最后那 4 个字节凑不满一块,怎么办?答案是把不足的部分补上,这个动作叫填充。填充看起来是个小细节,但填充方式选错或者解密时去掉填充的逻辑写错,都会导致「解密失败」或「解密后多出几个字节」。
为什么必须填充
分组密码的工作单位是固定大小的块。AES 的块是 16 字节,加密函数只接受恰好 16 字节的输入,输出也是 16 字节。
所以明文长度必须是 16 的整数倍,否则最后一块不完整,无法送入加密函数。填充的作用就是把最后一块补齐到 16 字节。
这里有一个容易忽略的点:填充是分组模式(ECB、CBC)才需要的。流式模式(CFB、OFB、CTR)把分组密码当密钥流使用,密文长度与明文完全相同,不存在补齐的问题,因此不使用填充。
四种填充方式
PKCS#7 是最常用的一种。规则是:缺几个字节,就补几个值为几的字节。缺 3 个字节就补 03 03 03,缺 5 个字节就补 05 05 05 05 05。
它有一个特别的设计:即使明文已经对齐,也要补满一整块。也就是说,如果明文恰好是 16 字节的整数倍,会额外添加 16 个值为 0x10(十六进制 10,即十进制 16)的字节。
为什么要这样?因为如果对齐时不填充,解密方就无法区分「最后一块是真实数据」还是「最后一块是填充」。补满一整块之后,解密方总能找到至少一个填充字节,读出它的值就知道该去掉几个字节,逻辑统一且无歧义。
ZeroPadding 的规则是:只在未对齐时补 0x00,已经对齐就不补。
它的问题是显而易见的:如果原文本身就以 0x00 结尾,解密方无法判断末尾的 0x00 是原文还是填充。这会导致「解密后多出 NUL 字节」的现象。另外由于对齐时不填充,解密方也无法校验填充是否正确。所以它只适合明文本身不含 0x00 的场景,例如纯文本。
ANSI X9.23 的规则是:前面补 0x00,最后一个字节写填充长度,同样即使对齐也补一整块。
它和 PKCS#7 的思路类似,区别在于填充内容不是重复的长度值,而是零加上一个长度字节。这种方式的可自校验性不如 PKCS#7(因为填充内容大部分是零,校验能力有限),但它是某些金融和标准化协议里指定的格式。
NoPadding 表示不做任何填充。选择它意味着调用方必须自己保证明文长度是 16 的整数倍,否则加密会直接失败。
一个常见的误区是「NoPadding 时加密不会报错,只是结果不对」。实际上成熟的实现会直接抛出错误,因为分组长度不匹配是一个明确的输入错误。遇到这种报错,正确做法是检查数据长度或者改用带填充的方式,而不是换一种填充方式碰运气。
加解密双方必须用同一种
填充方式属于加解密约定的一部分,双方必须完全一致。
如果加密端用 PKCS#7、解密端按 ZeroPadding 去掉尾部,结果就是解密后多出几个字节;反过来,解密端可能因为找不到合法的填充而报错。这类问题的表现是「解密能跑完但结果是错的」或者「解密报填充错误」,排查时容易被误认为是密钥或 IV 的问题。
实际对接中最容易出错的地方是跨语言场景。不同语言、不同库对填充的默认值可能不同,有些库默认 PKCS#7,有些默认 NoPadding。所以对接时应该显式指定填充方式,不要依赖默认值。
另外要注意命名差异。PKCS#7 在不同的库里有不同的叫法,有的写 PKCS5Padding,有的写 PKCS7。对 AES 来说,PKCS#5 实际上就是 PKCS#7(PKCS#5 原本是为 8 字节分组设计的,但实践中被广泛用于 16 字节分组,名称也就沿用下来)。看到这两种写法,可以认为是同一件事。
检查清单
- 明文长度不是 16 字节整数倍时必须填充,否则无法加密
- 流式模式(CFB、OFB、CTR)不使用填充,密文长度与明文一致
- PKCS#7 是最常用的方式,缺 n 字节补 n 个值为 n 的字节,对齐时补满一整块
- ZeroPadding 无法区分末尾的
0x00是原文还是填充,只适合不含 NUL 的文本 - ANSI X9.23 用零加长度字节填充,常见于部分标准化协议
- NoPadding 要求明文自行对齐,否则实现会直接报错
- 加解密双方必须显式约定同一种填充方式,不要依赖库的默认值