PHP与Node.js crypto的AES-256-CTR加解密不一致问题求助
解决PHP与Node.js AES-256-CTR加解密不兼容的问题
你遇到的核心问题是Node.js旧版加密API与PHP默认加密逻辑的密钥、IV生成规则不匹配,导致两边的加密参数不一致,最终密文无法兼容。
问题根源拆解
- Node.js端:
createCipher/createDecipher使用的是EVP_BytesToKey算法从密码派生密钥和IV,默认用MD5哈希计算,生成的IV并非全零值。 - PHP端:你调用
openssl_encrypt时未指定IV参数,CTR模式下会默认使用全零IV;同时PHP不会自动用EVP_BytesToKey处理密码,直接将短密码补零到32字节当密钥(AES-256要求32字节密钥),这和Node.js的密钥派生结果完全不同。
兼容方案:统一加密参数逻辑
下面提供两种解决方案,推荐第二种(符合现代加密安全规范):
方式1:模拟Node.js旧API的EVP_BytesToKey逻辑(兼容现有Node.js输出)
先实现PHP版的EVP_BytesToKey,生成和Node.js完全一致的密钥和IV:
function evpBytesToKey($password, $salt = null, $keyLen = 32, $ivLen = 16) { $key = ''; $iv = ''; $data = ''; while (strlen($key) < $keyLen || strlen($iv) < $ivLen) { $data .= $password . $salt; $md5Hash = md5($data, true); $data = $md5Hash; $key .= substr($md5Hash, 0, min(strlen($md5Hash), $keyLen - strlen($key))); if (strlen($iv) < $ivLen) { $iv .= substr($md5Hash, strlen($key), min(strlen($md5Hash) - strlen($key), $ivLen - strlen($iv))); } } return ['key' => $key, 'iv' => $iv]; } // 测试代码 $password = 'd6F3Efeq'; $text = 'pereira'; // Node.js的createCipher默认无盐,所以salt传null $params = evpBytesToKey($password, null, 32, 16); $encrypted = bin2hex(openssl_encrypt($text, 'aes-256-ctr', $params['key'], OPENSSL_RAW_DATA, $params['iv'])); echo $encrypted; // 现在输出会和Node.js的148bc695286379一致
对应Node.js代码(如果需要继续使用旧逻辑):
const crypto = require('crypto'); const algorithm = 'aes-256-ctr'; const password = 'd6F3Efeq'; function encrypt(text){ const cipher = crypto.createCipher(algorithm,password); let crypted = cipher.update(text,'utf8','hex'); crypted += cipher.final('hex'); return crypted; } console.log(encrypt('pereira')); // 输出148bc695286379
方式2:使用现代加密规范(推荐)
废弃不安全的createCipher,改用createCipheriv/createDecipheriv,手动生成随机IV并随密文传输,用PBKDF2派生密钥:
Node.js代码
const crypto = require('crypto'); const algorithm = 'aes-256-ctr'; const password = 'd6F3Efeq'; function encrypt(text) { const salt = crypto.randomBytes(16); // 随机盐 const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256'); // 派生32字节密钥 const iv = crypto.randomBytes(16); // CTR模式IV推荐16字节(和块大小一致) const cipher = crypto.createCipheriv(algorithm, key, iv); const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); // 返回盐+IV+密文的hex(也可用base64) return salt.toString('hex') + iv.toString('hex') + encrypted.toString('hex'); } function decrypt(encryptedHex) { const salt = Buffer.from(encryptedHex.slice(0, 32), 'hex'); // 前32位是盐(16字节) const iv = Buffer.from(encryptedHex.slice(32, 64), 'hex'); // 中间32位是IV(16字节) const encrypted = Buffer.from(encryptedHex.slice(64), 'hex'); const key = crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256'); const decipher = crypto.createDecipheriv(algorithm, key, iv); const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); return decrypted.toString('utf8'); } // 测试 const encrypted = encrypt('pereira'); console.log('加密结果:', encrypted); console.log('解密结果:', decrypt(encrypted));
PHP代码
function encrypt($text, $password) { $salt = random_bytes(16); $key = hash_pbkdf2('sha256', $password, $salt, 100000, 32, true); $iv = random_bytes(16); $encrypted = openssl_encrypt($text, 'aes-256-ctr', $key, OPENSSL_RAW_DATA, $iv); // 拼接盐、IV、密文并转hex return bin2hex($salt) . bin2hex($iv) . bin2hex($encrypted); } function decrypt($encryptedHex, $password) { $salt = hex2bin(substr($encryptedHex, 0, 32)); $iv = hex2bin(substr($encryptedHex, 32, 32)); $encrypted = hex2bin(substr($encryptedHex, 64)); $key = hash_pbkdf2('sha256', $password, $salt, 100000, 32, true); return openssl_decrypt($encrypted, 'aes-256-ctr', $key, OPENSSL_RAW_DATA, $iv); } // 测试 $password = 'd6F3Efeq'; $text = 'pereira'; $encrypted = encrypt($text, $password); echo '加密结果: ' . $encrypted . PHP_EOL; echo '解密结果: ' . decrypt($encrypted, $password) . PHP_EOL;
关键注意事项
- AES-256必须使用32字节(256位)密钥,短密码需通过统一的密钥派生算法生成,避免两边补位逻辑不一致。
- CTR模式要求IV唯一且不可预测,重复使用IV会导致加密安全漏洞,推荐随机生成并随密文传输。
- Node.js的
createCipher/createDecipher已被官方标记为废弃,建议尽快升级到createCipheriv/createDecipheriv。
内容的提问来源于stack exchange,提问作者Esteban M.
相关产品推荐
相关产品推荐

