错误信息
Malformed UTF-8 data
CryptoJS — thrown by .toString(CryptoJS.enc.Utf8) after decrypt
含义
CryptoJS 把你的密文解密成了不是合法 UTF-8 的字节,把它们转换为字符串时失败了。由于 CryptoJS 的 AES(CBC 模式)没有完整性校验,密钥错误并不会让解密失败——只会产生随机字节,而 UTF-8 解码器是第一个察觉问题的环节。同样是密钥错误,也可能不出现这个错误,而是得到空字符串(填充检查恰好明确失败时),因此这两种症状的诊断是一样的。
原因
密钥或密码短语错误
常见用与加密时不同的密钥解密,会得到伪随机的输出。出现哪种症状取决于消息长度:在我们的测试中,对于 29 字节的消息,约八分之一的错误密钥解密抛出了这个错误;100 字节的消息约有一半;300 字节的消息则每次都会抛出;其余几乎都返回 ""——无论哪种情况,嫌疑都在密钥上。
两端的密钥派生方式不一致
常见CryptoJS.AES.encrypt(msg, "passphrase") 会通过与 OpenSSL 兼容的 KDF,用随机盐派生出密钥和 IV。如果另一端把密码短语当作原始密钥使用(或者其他平台解密时没有经过这个 KDF),派生出的密钥就会不同,输出也就成了乱码。
密文在传输中损坏
偶见被 URL 编码破坏的 Base64(+ 变成了空格)、存储时的截断或重复编码,都会改变密文字节;对被改动的字节做 CBC 解密只会得到乱码,加密层面不会报任何错误。
修复方法
- 1.
显式处理密钥,两端保持一致
避开密码短语 KDF 带来的歧义:以 WordArray 形式传入真正的密钥,并显式提供 IV,两端的做法完全相同。
js const key = CryptoJS.enc.Hex.parse(hexKey); // 32-byte key, both sides const iv = CryptoJS.enc.Hex.parse(hexIv); // 16-byte IV, both sides const ct = CryptoJS.AES.encrypt(plaintext, key, { iv }); const pt = CryptoJS.AES.decrypt(ct.toString(), key, { iv }) .toString(CryptoJS.enc.Utf8); - 2.
把空输出视为同一种失败
检查结果,并明确报出错误——静默返回的空字符串会把密钥错误的 bug 掩盖好几个月。
js const pt = CryptoJS.AES.decrypt(ct, key, { iv }).toString(CryptoJS.enc.Utf8); if (!pt) throw new Error("decryption failed: wrong key/IV or corrupted ciphertext"); - 3.
优先使用认证加密方案
WebCrypto 的 AES-GCM 会对密文做认证:密钥错误时,解密每次都会明确失败,而不是产生乱码——从而彻底消除这一类错误。
js const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct); // wrong key → OperationError, never silent garbage