分块读取大文件时Node.js与PHP加密输出不一致问题
分块读取大文件时Node.js与PHP加密输出不一致
问题描述
分块读取大文件并执行AES-256-CBC加密时,Node.js与PHP的输出结果始终不一致,尽管两者使用的密钥、IV、算法、分块大小完全相同。
PHP代码
<?php require 'vendor/autoload.php'; define('PLAINTEXT_DATA_KEY', 'poSENHhkGVG/4fEHvhRO6j9W3goETWZAg+ZgTWxhw34='); define('IV', "X1bIRjgIoDn/BDFhHIbg7g=="); define('ALGORITHM', 'aes-256-cbc'); define('CHUNK_SIZE', 16 * 1024); class Cipher { private function pkcs7_pad(string $data, int $blockSize) { $padLength = $blockSize - (strlen($data) % $blockSize); return $data . str_repeat(chr($padLength), $padLength); } public function encrypt($source, $destination) { $inputFile = fopen($source, 'rb'); $outputFile = fopen($destination, 'wb'); try { fwrite($outputFile, base64_decode(IV)); while (!feof($inputFile)) { $buffer = fread($inputFile, CHUNK_SIZE); // Pad the last chunk if it is not the block size if (feof($inputFile)) { $buffer = $this->pkcs7_pad($buffer, 16); } $cipherText = openssl_encrypt($buffer, ALGORITHM, PLAINTEXT_DATA_KEY, OPENSSL_NO_PADDING, base64_decode(IV)); fwrite($outputFile, $cipherText); } } catch (Exception $e) { throw $e; } finally { fclose($inputFile); fclose($outputFile); } } } ?>
Node.js代码
const { createCipheriv, createReadStream, createWriteStream } = require('crypto'); const DATA_EVENT = 'data'; const PADDING_BLOCK_SIZE = 16; const ALGORITHM = "aes-256-cbc"; const PLAINTEXT_DATA_KEY = "poSENHhkGVG/4fEHvhRO6j9W3goETWZAg+ZgTWxhw34="; const IV = "X1bIRjgIoDn/BDFhHIbg7g=="; // randombytes(16) converted to base64 const CHUNK_SIZE = 16 * 1024; // 补充缺失的工具函数 function base64ToBuffer(base64Str: string): Buffer { return Buffer.from(base64Str, 'base64'); } function base64ToUint8Array(base64Str: string): Uint8Array { return new Uint8Array(base64ToBuffer(base64Str)); } class Cipher { private pkcs7Pad(buffer: Buffer, blockSize: number = PADDING_BLOCK_SIZE): Buffer { const padding = blockSize - (buffer.length % blockSize); const padBuffer = Buffer.alloc(padding, padding); return Buffer.concat([buffer, padBuffer]); } async encrypt(source: string, dest: string) { return new Promise(async (res, rej) => { const iv = base64ToBuffer(IV); const cipher = createCipheriv(ALGORITHM, base64ToUint8Array(PLAINTEXT_DATA_KEY), iv); cipher.setAutoPadding(false); const readStream = createReadStream(source, { highWaterMark: CHUNK_SIZE }); const writeStream = createWriteStream(dest, { highWaterMark: CHUNK_SIZE }); writeStream.write(iv); let tempChunkStorage = Buffer.alloc(0); // Buffer to store remaining data readStream.on(DATA_EVENT, (chunk) => { if (typeof chunk === "string") { chunk = Buffer.from(chunk); } // Append the new chunk to the temp storage tempChunkStorage = Buffer.concat([tempChunkStorage, chunk]); while (tempChunkStorage.length >= CHUNK_SIZE) { const block = tempChunkStorage.subarray(0, CHUNK_SIZE); const encryptedBuffer = cipher.update(block); writeStream.write(encryptedBuffer); tempChunkStorage = tempChunkStorage.subarray(CHUNK_SIZE); } }); readStream.on("end", () => { if (tempChunkStorage.length > 0) { const encryptedBuffer = cipher.update(this.pkcs7Pad(tempChunkStorage)); // Add padding writeStream.write(encryptedBuffer); cipher.final(); } writeStream.end(); res(true); }); readStream.on("error", (err) => { writeStream.close(); rej(err); }); }); } }
输出对比
PHP加密结果(Base64)前50字符:0tCb9xtx5KpG+56ukYvcQDoNKCdoPtAFUrFDRc4TiqQrQocQRK Node.js加密结果(Base64)前50字符:sUUI4nXHwhKNdRs+Brqc5neKuKb3fx4qqBohlDSn/7FVrYo46/

问题根源分析
CBC链式加密逻辑错误
Node.js的createCipheriv会自动维护CBC模式的链式状态:每个块加密后,会用当前块的密文作为下一个块的IV,符合CBC标准流程。而PHP代码中每次调用openssl_encrypt都传入初始IV,导致每个文件块独立加密,完全破坏了CBC的链式特性,这是核心差异。密文编码格式不一致
PHP的openssl_encrypt默认返回Base64编码的密文字符串并直接写入文件;Node.js的cipher.update返回二进制Buffer,写入的是原始密文数据,两者输出格式完全不同。
修复方案
修正PHP代码
改用OpenSSL流式加密接口维护链式状态,同时输出二进制密文:
<?php require 'vendor/autoload.php'; define('PLAINTEXT_DATA_KEY', 'poSENHhkGVG/4fEHvhRO6j9W3goETWZAg+ZgTWxhw34='); define('IV', "X1bIRjgIoDn/BDFhHIbg7g=="); define('ALGORITHM', 'aes-256-cbc'); define('CHUNK_SIZE', 16 * 1024); class Cipher { private function pkcs7_pad(string $data, int $blockSize) { $padLength = $blockSize - (strlen($data) % $blockSize); return $data . str_repeat(chr($padLength), $padLength); } public function encrypt($source, $destination) { $inputFile = fopen($source, 'rb'); $outputFile = fopen($destination, 'wb'); try { $iv = base64_decode(IV); $key = base64_decode(PLAINTEXT_DATA_KEY); // 初始化加密上下文,维护链式状态 $cipherContext = openssl_encrypt_init(ALGORITHM, $key, $iv, OPENSSL_NO_PADDING); fwrite($outputFile, $iv); while (!feof($inputFile)) { $buffer = fread($inputFile, CHUNK_SIZE); $isFinalChunk = feof($inputFile); // 仅对最后非空块做填充 if ($isFinalChunk && !empty($buffer)) { $buffer = $this->pkcs7_pad($buffer, 16); } if (!empty($buffer)) { $cipherText = openssl_encrypt_update($cipherContext, $buffer); fwrite($outputFile, $cipherText); } } // 完成加密,处理最终块 $finalCipherText = openssl_encrypt_final($cipherContext); fwrite($outputFile, $finalCipherText); } catch (Exception $e) { throw $e; } finally { fclose($inputFile); fclose($outputFile); } } } ?>
验证效果
修正后,两者将遵循相同的加密流程:
- 采用CBC标准链式流式加密,维护块间IV传递
- 输出二进制密文数据
此时加密后的文件内容将完全一致。
内容的提问来源于stack exchange,提问作者Bikash
相关产品推荐
相关产品推荐

