使用AES(Rijndael/ECB/NoPadding)在JavaScript中加解密报错排查
对接第三方REST API的AES加密解密问题
加密核心要求
- 使用Rijndael cipher
- 采用Electronic Code Book mode (ECB)
- 不使用内置填充(NoPadding)
集成补充规则
所有URL参数需用共享密钥通过AES加密生成安全令牌,额外要求:
- 加密后的缓冲区需手动补空格,使总长度为32的倍数(
length(encrypted padded string) mod 32 = 0) - 示例Java代码:
Cipher cipher = Cipher.getInstance("Rijndael/ECB/NoPadding", "SunJCE");
我的JavaScript实现及问题
代码实现
var clearMessage = "The brown fox jumps over the laxy dog"; console.log("Clear message: ", clearMessage); var encodingKey = CryptoJS.enc.Hex.parse("0123456789ABCDEF0123456789ABCDEF"); var encryptedMessage = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(clearMessage), encodingKey, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding}); console.log("Encrypted message: ", encryptedMessage.ciphertext.toString()); const decryptedMessage = CryptoJS.AES.decrypt(encryptedMessage.ciphertext.toString(), encodingKey, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding}); clearMessage = decryptedMessage.toString(CryptoJS.enc.Utf8); console.log("Decrypted message: ", clearMessage);
控制台输出
Clear message: The brown fox jumps over the laxy dog Encrypted message: 56aaf639f44c106889aa4a765d2bf7c83a7860a379d776982991c7575eb63fd31f7c9ce3db crypto-js.js:523 Uncaught Error: Malformed UTF-8 data at Object.stringify (crypto-js.js:523:24) at WordArray.init.toString (crypto-js.js:278:38) at encryptDecrypt (index.html:157:39) at HTMLButtonElement.onclick (index.html:46:60)
错误触发于clearMessage = decryptedMessage.toString(CryptoJS.enc.Utf8);这一行。
ChatGPT生成的代码同样报错
// Import the necessary libraries var CryptoJS = require("crypto-js"); // Set the plaintext message and secret key var message = "This is a secret message!"; var secretKey = "ThisIsASecretKey"; // Convert the key and message to WordArrays (required by CryptoJS) var key = CryptoJS.enc.Utf8.parse(secretKey); var plaintext = CryptoJS.enc.Utf8.parse(message); // Encrypt the plaintext message using AES with Rijndael cipher, ECB mode, and no padding var ciphertext = CryptoJS.AES.encrypt(plaintext, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding, cipher: CryptoJS.algo.Rijndael }); // Print the encrypted ciphertext in Base64 format console.log(ciphertext.toString()); // Decrypt the ciphertext message using AES with Rijndael cipher, ECB mode, and no padding var decrypted = CryptoJS.AES.decrypt(ciphertext, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding, cipher: CryptoJS.algo.Rijndael }); // Convert the decrypted message back to plaintext and print it console.log(decrypted.toString(CryptoJS.enc.Utf8));
问题原因及解决思路
核心原因
- 加密前未手动补位:使用
NoPadding模式时,明文长度必须是Rijndael块大小的整数倍(此处要求32字节)。若明文长度不满足,CryptoJS会直接截断明文到块大小倍数,导致解密后的字节序列不完整或包含无效UTF-8字节,触发编码错误。 - 解密参数传递错误:
CryptoJS.AES.decrypt直接接收十六进制字符串作为密文时无法正确解析,必须先将其转换为WordArray类型,否则会解密出无效数据。
解决步骤
1. 加密前手动补空格到32字节倍数
按照集成要求对明文补空格:
function padTo32Multiple(str) { const padLength = 32 - (str.length % 32); return str.padEnd(str.length + padLength, ' '); } const paddedMessage = padTo32Multiple(clearMessage);
2. 修正解密的密文解析方式
解密时需将十六进制密文先解析为WordArray:
// 加密后获取十六进制密文 const ciphertextHex = encryptedMessage.ciphertext.toString(); // 解密时传入解析后的WordArray const decryptedMessage = CryptoJS.AES.decrypt( { ciphertext: CryptoJS.enc.Hex.parse(ciphertextHex) }, encodingKey, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding } );
3. 解密后去除手动补的空格
恢复原始明文内容:
const decryptedStr = decryptedMessage.toString(CryptoJS.enc.Utf8).trimEnd();
完整修正代码
var clearMessage = "The brown fox jumps over the laxy dog"; console.log("Clear message: ", clearMessage); // 手动补空格到32字节倍数 function padTo32Multiple(str) { const padLength = 32 - (str.length % 32); return str.padEnd(str.length + padLength, ' '); } const paddedMessage = padTo32Multiple(clearMessage); var encodingKey = CryptoJS.enc.Hex.parse("0123456789ABCDEF0123456789ABCDEF"); // 加密(显式指定Rijndael块大小为32字节) var encryptedMessage = CryptoJS.AES.encrypt(CryptoJS.enc.Utf8.parse(paddedMessage), encodingKey, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding, cipher: CryptoJS.algo.Rijndael.create({ blockSize: 32 }) }); const ciphertextHex = encryptedMessage.ciphertext.toString(); console.log("Encrypted message: ", ciphertextHex); // 解密 const decryptedMessage = CryptoJS.AES.decrypt( { ciphertext: CryptoJS.enc.Hex.parse(ciphertextHex) }, encodingKey, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.NoPadding, cipher: CryptoJS.algo.Rijndael.create({ blockSize: 32 }) } ); // 去除补位空格并输出 const decryptedStr = decryptedMessage.toString(CryptoJS.enc.Utf8).trimEnd(); console.log("Decrypted message: ", decryptedStr);
额外注意事项
- CryptoJS默认Rijndael块大小为128位(16字节),需显式指定
blockSize:32(256位)以匹配集成要求,加密和解密时都要设置,否则会导致解密失败。
内容的提问来源于stack exchange,提问作者Eddie
相关产品推荐
相关产品推荐

