在使用 DES 算法(CBC 模式,PKCS7/PKCS5 填充)进行解密时,我发现了一个奇怪的现象:Python 和 TypeScript 实现的解密结果在十六进制表示的前 16 个字符(即 8 字节)不同,但后续部分完全一致。输入的密文、密钥和初始化向量(IV)都确认一致,那么问题出在哪里呢?经过详细排查,我找到了原因并成功解决。以下是我的分析和解决过程,希望对你有所帮助!
问题描述
我们先来看看两段代码的实现:
Python 版本:
from pyDes import des, CBC, PAD_PKCS5
import base64
# 假设的密钥和 IV
_DES_KEY = b'12345678'
_DES_IV = [0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21]
# 假设的 base64 编码密文
data = "SGVsbG8gV29ybGQ="
# 创建 DES 实例并解密
k = des(_DES_KEY, CBC, bytes(_DES_IV), padmode=PAD_PKCS5)
des_k = k.decrypt(base64.b64decode(data))
print(des_k.hex()) # 输出解密结果的十六进制表示
TypeScript 版本(使用 CryptoJS):
import CryptoJS from 'crypto-js';
// 假设的密钥和 IV
const DES_KEY = '12345678';
const DES_IV = [0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21];
// 与 Python 相同的密文
const data = "SGVsbG8gV29ybGQ=";
const key = CryptoJS.enc.Utf8.parse(DES_KEY);
const iv = CryptoJS.lib.WordArray.create(DES_IV);
// 解密
const decrypted = CryptoJS.DES.decrypt(data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
const decryptedHex = decrypted.toString(CryptoJS.enc.Hex);
console.log(decryptedHex); // 输出解密结果的十六进制表示
现象:两段代码的解密结果在十六进制字符串的前 16 个字符(8 字节)不同,但后续部分一致。
分析过程
为了找到问题根源,我逐步分析了两者的差异:
DES 和 CBC 模式的工作原理:
DES 是一种块加密算法,每个块大小为 8 字节。
在 CBC(Cipher Block Chaining)模式下,解密过程如下:
第一个明文块 = Decrypt(第一个密文块, 密钥) XOR IV
后续明文块 = Decrypt(当前密文块, 密钥) XOR 前一个密文块
这意味着 IV 只影响第一个块的解密,后续块的结果由密文和密钥决定,与 IV 无关。
问题定位:
既然只有前 8 字节(第一个块)不同,而后续一致,问题很可能与 IV 的处理有关。
检查 IV 的实现:
Python:_DES_IV = [0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21]
是一个字节列表,通过 bytes(_DES_IV) 转换为 8 字节的 IV,直接被 des 对象使用。
TypeScript:DES_IV = [0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21]
通过 CryptoJS.lib.WordArray.create(DES_IV)
创建 IV。
关键差异:CryptoJS 的 WordArray.create 默认将输入数组视为 32 位字(word)数组,而不是字节(byte)数组。
实际效果:[0x12, 0x34, ...] 被解释为 [0x00000012, 0x00000034, ...],取前 8 字节后变为 00 00 00 12 00 00 00 34,与 Python 的 12 34 56 78 87 65 43 21 不一致。
影响:
IV 的不一致导致第一个块的解密结果不同。
后续块依赖密文和密钥,与 IV 无关,因此结果一致。
解决方案
找到问题后,解决方法很简单:需要确保 TypeScript 中的 IV 与 Python 保持一致,即正确创建 8 字节的 IV。以下是几种可行的方案:
使用 Uint8Array(推荐):
const DES_IV = [0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21];
const iv = CryptoJS.lib.WordArray.create(new Uint8Array(DES_IV));
Uint8Array 明确指定每个元素为 8 位字节,生成的 IV 与 Python 一致。
使用十六进制字符串:
const DES_IV_HEX = '1234567887654321'; // 对应 [0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21]
const iv = CryptoJS.enc.Hex.parse(DES_IV_HEX);
将 IV 表示为十六进制字符串,然后解析为正确的字节序列。
手动指定字节长度:
const DES_IV = [0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21];
const iv = CryptoJS.lib.WordArray.create(DES_IV.map(byte => byte & 0xFF), 8);
通过掩码确保每个元素是 8 位,并指定总长度为 8 字节。
验证
使用上述任一方法修正 IV 后,TypeScript 的解密结果与 Python 完全一致,前 8 字节的差异消失。
总结
问题根源:TypeScript 中 CryptoJS.lib.WordArray.create 对 IV 数组的默认解释(32 位字而非 8 位字节)导致 IV 与 Python 不一致。
解决方法:通过 Uint8Array、十六进制字符串或手动指定字节长度,确保 IV 正确创建为 8 字节。
经验教训:在跨语言使用加密库时,务必注意数据类型的转换和库的默认行为,确保输入参数(尤其是 IV 和密钥)在底层字节级别一致。
希望这篇博客能帮到你!如果你在加密解密中遇到类似问题,不妨仔细检查一下输入参数的处理方式,避免掉进类似的“坑”里。有什么疑问,欢迎留言讨论!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。