别再搞混了!PHP中AES加密的PKCS5与PKCS7填充终极辨析
如果你在PHP项目中对接过Java、C#或其他语言的AES加密接口,大概率遇到过这样的场景:对方文档明确写着“AES/CBC/PKCS5Padding”,你信心满满地用PHP的openssl_encrypt配上OPENSSL_RAW_DATA选项,结果加密出来的数据对方死活解不开,或者对方发来的密文你这边解密出来全是乱码。折腾半天,你开始怀疑人生——明明算法、密钥、IV都对,为什么就是不行?
问题的根源,往往就藏在那个看似不起眼的“PKCS5Padding”里。更准确地说,是藏在PHP生态中mcrypt与openssl两个扩展的历史纠葛,以及PKCS5与PKCS7这两个填充标准长达十余年的概念混淆之中。今天,我们就彻底掰开揉碎,从标准定义、历史背景到代码实战,把这个问题讲清楚。这不仅是技术细节的澄清,更是避免跨系统对接时踩坑的必备知识。
1. 历史迷雾:mcrypt的遗产与openssl的“误会”
要理解今天的混乱,必须回到PHP加密扩展的“上古时期”。在PHP 7.1之前,mcrypt扩展是许多开发者进行对称加密的首选。它功能强大,但设计上有些“特立独行”。
1.1 mcrypt时代的“块大小”与“密钥大小”之谜
在mcrypt中,当你指定MCRYPT_RIJNDAEL_128时,这里的128指的是块大小(Block Size),单位是比特。而AES算法的块大小固定就是128比特(16字节),所以无论你使用128位、192位还是256位的密钥,在mcrypt里你都得用MCRYPT_RIJNDAEL_128。
密钥的长度,则是通过你传入的$key参数的实际字节数来决定的。看下面这个经典的mcrypt加密示例:
// PHP 5.6 使用 mcrypt 的例子
function encryptWithMcrypt($data, $key, $iv) {
$cipher = MCRYPT_RIJNDAEL_128; // 这里128指块大小,固定值
$mode = MCRYPT_MODE_CBC;
// 密钥长度由 $key 的字节数决定
// 如果 $key 是32字节,实际就是AES-256
$encrypted = mcrypt_encrypt($cipher, $key, $data, $mode, $iv);
return base64_encode($encrypted);
}
这里有个关键点:mcrypt不会根据密钥长度自动选择AES变体(AES-128、AES-192、AES-256)。它只是用你给的密钥去执行Rijndael算法。如果密钥是16字节,就是AES-128;24字节就是AES-192;32字节就是AES-256。
注意:很多人在从
mcrypt迁移到openssl时,发现同样的16字节密钥,在mcrypt里工作正常,在openssl里却需要把算法名从AES-128-CBC改成AES-256-CBC才能解密成功。这通常是因为mcrypt实际使用的是256位密钥(密钥被填充或处理成32字节),而openssl严格遵循算法名指定的密钥长度。
1.2 openssl的清晰世界与历史包袱
openssl扩展的设计更加贴近OpenSSL库本身,也更符合现代密码学的惯例。在openssl_encrypt函数中,算法名称直接决定了密钥长度:
// PHP 7.1+ 使用 openssl 的例子
function encryptWithOpenssl($data, $key, $iv) {
// 这里的“128”明确指密钥长度,必须是16字节
$method = 'AES-128-CBC';
// 如果 $key 是32字节,但这里指定了AES-128,openssl只会取前16字节
$encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($encrypted);
}
这种设计上的差异,导致了很多历史代码在迁移时出现兼容性问题。但更大的混乱,来自于填充标准。
2. 填充标准的真相:PKCS5与PKCS7本是同根生
这是本文的核心,也是大多数混淆的源头。让我们先看一个简单的对比:
| 特性 | PKCS5 | PKCS7 |
|---|---|---|
| 标准出处 | RSA实验室的PKCS#5标准 | RSA实验室的PKCS#7标准 |
| 设计目标 | 为8字节块大小的算法设计(如DES) | 为1-255字节块大小的算法设计 |
| 填充方式 | 缺少N字节就填充N个值为N的字节 | 缺少N字节就填充N个值为N的字节 |
| 数学公式 | 完全一致 | 完全一致 |
| 适用算法 | 理论上只适用于8字节块 | 适用于任意块大小(包括AES的16字节) |
看到问题了吗?PKCS5和PKCS7的填充算法在数学上是完全相同的。它们唯一的“区别”只存在于标准文档的适用范围描述中。
2.1 填充算法详解
无论叫PKCS5还是PKCS7,填充的逻辑都是一样的:
- 计算需要填充的字节数:
pad_len = block_size - (data_len % block_size) - 如果数据长度正好是块大小的整数倍,则填充一个完整的块(例如AES-128就填充16个值为16的字节)
- 填充的每个字节的值都等于
pad_len
举个例子,假设使用AES-128(块大小16字节),要加密字符串"Hello"(5字节):
原始数据: 48 65 6C 6C 6F (5字节)
需要填充: 16 - (5 % 16) = 11字节
填充后: 48 65 6C 6C 6F 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B 0B
解密时,查看最后一个字节的值(0x0B = 11),就知道需要去掉最后11个字节。
2.2 Java世界的“错误”约定
在Java的Cipher类中,你经常会看到这样的写法:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
但AES的块大小是16字节,不是8字节。按照标准定义,这里应该用PKCS7Padding才对。实际上,Java的PKCS5Padding实现就是PKCS7填充。Sun/Oracle的工程师们早期可能觉得“反正算法一样,就叫PKCS5吧”,这个命名就这样流传下来了。
这导致了一个行业事实:当人们说“PKCS5Padding”时,90%的情况下他们实际指的是PKCS7填充算法。这个约定在Java、.NET、Python等多个生态中都有体现。
3. PHP中的实现差异与陷阱
理解了历史背景和标准真相后,我们来看看PHP中具体的实现差异,以及那些容易踩坑的地方。
3.1 mcrypt的“零填充”与手动PKCS7
mcrypt扩展默认使用零填充(Zero Padding),也就是在数据末尾补\0直到块大小对齐。这带来了两个问题:
- 如果原始数据本身就可能以
\0结尾,解密时无法区分哪些是填充哪些是真实数据 - 与使用PKCS7填充的其他系统不兼容
因此,使用mcrypt时,如果需要PKCS7填充,必须手动实现:
// 经典的 mcrypt + 手动PKCS7填充
function encryptWithMcryptAndPad($data, $key, $iv) {
// 计算块大小(AES是16字节)
$blockSize = 16;
// 手动PKCS7填充
$pad = $blockSize - (strlen($data) % $blockSize);
$data .= str_repeat(chr($pad), $pad);
// 加密
$encrypted = mcrypt_encrypt(
MCRYPT_RIJNDAEL_128,
$key,
$data,
MCRYPT_MODE_CBC,
$iv
);
return base64_encode($encrypted);
}
// 解密时需要


525

被折叠的 条评论
为什么被折叠?



