You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

分块读取大文件时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/

输出对比


问题根源分析

  1. CBC链式加密逻辑错误
    Node.js的createCipheriv会自动维护CBC模式的链式状态:每个块加密后,会用当前块的密文作为下一个块的IV,符合CBC标准流程。而PHP代码中每次调用openssl_encrypt都传入初始IV,导致每个文件块独立加密,完全破坏了CBC的链式特性,这是核心差异。

  2. 密文编码格式不一致
    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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 04:24:56