用 3DES 与别的系统对接时,最常见的失败原因不是密钥不对,而是几个参数填错了。3DES 的分组长度、密钥长度和 IV 长度之间的关系不像 AES 那样直观,尤其是从 AES 迁移过来的开发者,很容易按 AES 的习惯去填,结果就是解密报错或者得到乱码。
第一处:IV 是 8 字节,不是 16 字节
这是最高频的错误。
AES 的分组是 128 位,所以它的 IV 是 16 字节。开发者习惯了「IV 等于 16 字节」这个印象,切换到 3DES 时就会顺手填 16 字节。
但 3DES 的分组仍然是 64 位——它是基于 DES 的,而 DES 的分组从 1977 年定下来就是 64 位,三倍运算只改变了密钥处理,没有改变分组大小。所以 3DES 的 IV 必须是 8 字节,也就是 16 个十六进制字符。
填错的典型表现是:工具或库直接报错,提示「IV 长度必须是 8 字节」;如果某个实现不校验而是截断或补零,问题会更隐蔽,表现为解密结果不对但没有任何错误提示。
判断方法很简单:看报错信息里说的期望长度。如果提示期望 8 字节,那这个算法就是 64 位分组。
第二处:密钥是 24 字节,不是 16 字节
3DES 的标准形式需要 24 字节密钥,也就是 48 个十六进制字符。这个长度来自三把 8 字节子密钥。
但有些老系统用的是 16 字节密钥,也就是 2-key 形式的 3DES。这两者不能混用——用 16 字节密钥去配置一个要求 24 字节的实现,会直接报长度错误。
处理方式取决于手上的密钥长度:
如果密钥是 24 字节,直接使用标准形式。
如果密钥是 16 字节,说明对端用的是 2-key 形式(K1、K2、K1)。需要把它拼成 24 字节——把前 8 字节复制到末尾,形成 K1 + K2 + K1。拼接后交给标准实现,结果与对端一致。
如果密钥是 8 字节,那说明对端用的是单 DES 而不是 3DES。这时可以把同一把密钥重复三次拼成 24 字节,3DES 会自动退化成单 DES,能正确互通。
关键在于先确认对端的密钥长度和算法形式,再决定怎么填。不要凭猜测填一个长度然后试。
第三处:填充方式的命名差异
填充方式的实际行为在不同实现里是一致的,但名字不统一。
Java 的 JCE 用 PKCS5Padding 这个名字,PHP 的 OpenSSL 和多数库用 PKCS7。看起来是两个不同的东西,但对 8 字节分组的算法来说,它们的填充规则完全相同——缺 n 字节就补 n 个值为 n 的字节,即使对齐也补满一整块。
这个命名差异来自历史:PKCS#5 原本是为 8 字节分组的算法设计的,PKCS#7 是它的推广版本,适用于任意分组长度。实践中 Java 沿用了 PKCS#5 的名字,但实际实现的是 PKCS#7 的行为。所以看到 DESede/CBC/PKCS5Padding 时,可以理解为「3DES + CBC + PKCS#7 填充」。
真正需要注意的是 ZeroPadding。它的行为与其他方式不同:只在未对齐时补 0x00,对齐时不补。如果对端用的是 ZeroPadding,而本地选了 PKCS#7,解密结果末尾就会残留填充字节,或者因为填充校验失败而报错。
所以对接时应该显式指定填充方式,不要依赖默认值。同时要问清楚对端用的是哪一种,而不是假设。
一个可复用的验证方法
与其在参数上反复试错,不如用一组固定的测试数据做验证。
准备一段简短的明文、一把确定的密钥、一个确定的 IV,在对端算一遍密文,在本地用相同的参数算一遍,比对结果。如果一致,说明所有参数都对上了;如果不一致,再逐项检查密钥、IV、模式、填充。
这个方法的价值在于把「参数是否正确」变成一个可验证的问题,而不是靠现象去猜。特别是跨语言对接时,用测试向量验证比读文档更可靠——文档可能过期,而计算结果不会骗人。
检查清单
- 3DES 的分组是 64 位,IV 必须是 8 字节而不是 16 字节
- 标准 3DES 密钥是 24 字节,16 字节密钥属于 2-key 形式,需拼成 K1+K2+K1
- 8 字节密钥意味着单 DES,重复三次可让 3DES 退化为单 DES 以互通
- Java 的 PKCS5Padding 在 8 字节分组下等价于 PKCS#7
- ZeroPadding 行为与其他方式不同,必须与对端显式确认
- 用一组固定测试向量验证参数,比反复试错或只读文档更可靠