ReactJS移植PHP端OpenSSL+zlib双层压缩AES加解密报错求助
移植PHP OpenSSL加解密逻辑到ReactJS的问题定位与修复方案
原PHP端可正常运行的实现
$secret_key = 'thisIsK3y'; $secret_iv = 'tHis1s1v'; $output = false; $encrypt_method = "AES-256-CBC"; $key = hash( 'sha256', $secret_key ); $iv = substr( hash( 'sha256', $secret_iv ), 0, 16 ); $action = 'e'; // 已知测试密文:33f6f1d3ebe5b9da5fd0e3a1c7dd71c6e484be914731cf96adac0c00 对应明文 "test" $string = "33f6f1d3ebe5b9da5fd0e3a1c7dd71c6e484be914731cf96adac0c00"; if( $action == 'e' ) { $output = bin2hex(gzdeflate( gzdeflate(openssl_encrypt( $string, $encrypt_method, $key, 0, $iv ), 9), 9) ); } else if( $action == 'd' ) { if (!empty($string)) $output = openssl_decrypt( gzinflate(gzinflate(pack("H*", $string ) ) ), $encrypt_method, $key, 0, $iv ); } echo $output; // 输出test,PHP端运行无异常
移植阶段已准备的基础实现
- 已引入依赖:CryptoJS、PakoJS
- 已拆分的独立工具方法(存储于
string.js)
export function gzdeflate(str) { return pako.deflateRaw(str); } export function gzinflate(str) { return pako.inflateRaw(str); } export function bin2hex(s) { let i let l let o = '' let n s += '' for (i = 0, l = s.length; i < l; i++) { n = s.charCodeAt(i) .toString(16) o += n.length < 2 ? '0' + n : n } return o } export function hex2bin(hex) { let bytes = [], str; for(var i=0; i< hex.length-1; i+=2){ bytes.push(parseInt(hex.substr(i, 2), 16)); } str = String.fromCharCode.apply(String, bytes); return str; }
调试阶段实际出现的异常现象
hex2bin方法可正常执行无语法报错- 对十六进制密文执行两次
gzinflate操作时,抛出Cannot read property 'ciphertext' of undefined错误 - 调整
secret_key、secret_iv参数重试后,抛出Error: invalid stored block lengths错误 - 全流程跑通后输出结果始终无法和PHP端的明文对齐
核心问题定位与对应修复点
- 数据类型全链路不匹配
- 原
hex2bin方法返回JS字符串而非原始二进制字节流,Pako的inflateRaw方法要求输入为Uint8Array类型的压缩流字节,传入字符串时会因编码转换损坏原始字节结构,直接抛出非法压缩块长度的解压错误 - Pako解压后返回的也是
Uint8Array类型的二进制数据,不能直接转UTF8字符串传给CryptoJS,二进制字节转字符串的过程会因不可见字符截断、编码转换导致密文损坏
- 原
- 密钥长度与编码逻辑错位
- PHP端的
hash('sha256', $secret_key)输出为完整64位hex字符串,直接作为参数传入OpenSSL加密函数;原JS代码错误对SHA256结果做substr(0,32)截断,仅取前32位hex作为密钥,和PHP端实际使用的密钥字节完全不一致 - PHP端的IV逻辑为截取SHA256结果的前16位hex字符,直接传入OpenSSL,JS端需要严格保持同样的截取规则,按字符串字节作为IV,长度刚好匹配AES-CBC要求的16字节块大小
- PHP端的
- CryptoJS解密入参格式错误
- 原代码给
AES.decrypt第一个参数传入普通字符串,CryptoJS会默认按OpenSSL加盐格式(前缀为Salted__)解析密文,非标准格式的输入会被解析为ciphertext为undefined的对象,直接触发取值报错
- 原代码给
修复后可直接运行的ReactJS端解密代码
// 引入依赖 import sha256 from 'crypto-js/sha256'; import CryptoJS from 'crypto-js'; import pako from 'pako'; // 替换原错误工具方法 function hexToUint8Array(hexStr) { const byteLen = hexStr.length / 2; const bytes = new Uint8Array(byteLen); for (let i = 0; i < byteLen; i++) { bytes[i] = parseInt(hexStr.substr(i * 2, 2), 16); } return bytes; } function uint8ToWordArray(u8arr) { return CryptoJS.lib.WordArray.create(u8arr); } let secret_key = "thisIsK3y"; let secret_iv = "tHis1s1v"; let action = 'd'; // 已知测试密文 let string = "33f6f1d3ebe5b9da5fd0e3a1c7dd71c6e484be914731cf96adac0c00"; let output = false; // 对齐PHP端密钥逻辑:使用完整SHA256 hex字符串作为密钥,不做截断 const keyHex = sha256(secret_key).toString(CryptoJS.enc.Hex); const key = CryptoJS.enc.Utf8.parse(keyHex); // 对齐PHP端IV逻辑:截取SHA256结果前16位hex字符作为IV const ivHex = sha256(secret_iv).toString(CryptoJS.enc.Hex).substr(0, 16); const iv = CryptoJS.enc.Utf8.parse(ivHex); if (action == 'd') { // 1. 十六进制密文转原始字节流 const compressedLv1 = hexToUint8Array(string); // 2. 第一次解压 const compressedLv2 = pako.inflateRaw(compressedLv1); // 3. 第二次解压拿到OpenSSL输出的原始密文 const encryptedBuf = pako.inflateRaw(compressedLv2); // 4. 密文转CryptoJS可识别的WordArray格式 const encryptedWord = uint8ToWordArray(encryptedBuf); // 5. 执行AES-256-CBC解密,显式传入ciphertext结构避免自动格式解析 output = CryptoJS.AES.decrypt( { ciphertext: encryptedWord }, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ).toString(CryptoJS.enc.Utf8); } console.log(output); // 输出和PHP端一致的"test"
注意:PHP端两次连续
gzdeflate是为了进一步压缩加密输出的二进制密文长度,移植时必须严格保持两次解压的顺序,不能省略步骤或颠倒解压顺序。
内容的提问来源于stack exchange,提问作者0x00b0
相关产品推荐
相关产品推荐

