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

Laravel如何分块加密解密大文件?解决解密无效负载报错

大文件分块加密解密(Laravel Encrypter)问题解决

问题背景

直接加密大文件时触发内存溢出错误:

Allowed memory size of 134217728 bytes exhausted

原实现方式会把整个文件和加密内容加载到内存,仅适用于小文件:

Storage::put($filePath, $encrypted->encrypt(file_get_contents($file)));

尝试分块加密解密时,解密阶段出现:

Illuminate\Contracts\Encryption\DecryptException: The payload is invalid.

错误原因

Laravel的Encrypter::encrypt()方法会给每个加密块生成独立的**IV(初始化向量)**和认证信息,加密后的每个块是完整的加密 payload(包含IV、加密内容、哈希校验)。但解密时按固定1MB读取,会把一个完整的加密 payload 截断,或者把多个payload混读,导致解密时无法识别有效payload,触发校验错误。

正确实现方案

要实现分块加密解密,需要:

  • 加密时,给每个加密块添加长度标记,方便解密时准确读取完整的加密payload
  • 解密时,先读取块的长度,再读取对应长度的加密内容,确保每次解密的是完整的加密块

修改后的加密代码

use Illuminate\Encryption\Encrypter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;

Route::post('/upload', function (Request $request) {
    $file = $request->file('file');

    if (!$file) {
        return response()->json(['success' => false, 'message' => 'File not uploaded']);
    }

    $key = Config::get('app.file_key');
    $key = str_replace('base64:', '', $key);
    $key = base64_decode($key);
    $encrypted = new Encrypter($key, Config::get('app.cipher'));

    $encryptedFilePath = storage_path('app/public/my-file.enc');
    $chunkSize = 1024 * 1024; // 1MB 明文块大小

    $fpSource = fopen($file->path(), 'rb');
    $fpDest = fopen($encryptedFilePath, 'wb');

    while (!feof($fpSource)) {
        $plaintext = fread($fpSource, $chunkSize);
        // 加密当前明文块
        $encryptedChunk = $encrypted->encrypt($plaintext);
        // 先写入加密块的长度(用4字节无符号大端存储,最大支持4GB单个块)
        fwrite($fpDest, pack('N', strlen($encryptedChunk)));
        // 写入加密块内容
        fwrite($fpDest, $encryptedChunk);
    }

    fclose($fpSource);
    fclose($fpDest);

    return response()->json(['success' => true, 'message' => 'File uploaded and encrypted successfully']);
})->name('upload');

修改后的解密代码

Route::get('/decrypt/{file_name}', function ($file_name) {
    $key = Config::get('app.file_key');
    $key = str_replace('base64:', '', $key);
    $key = base64_decode($key);
    $encrypted = new Encrypter($key, Config::get('app.cipher'));

    $sourceFilePath = storage_path("app/public/$file_name");
    $destinationFilePath = storage_path("app/public/decrypted-$file_name");

    if (!file_exists($sourceFilePath)) {
        return response()->json(['success' => false, 'message' => 'Encrypted file not found']);
    }

    $fpEncrypted = fopen($sourceFilePath, 'rb');
    $fpDest = fopen($destinationFilePath, 'wb');

    while (!feof($fpEncrypted)) {
        // 先读取4字节的块长度
        $lengthBytes = fread($fpEncrypted, 4);
        if (strlen($lengthBytes) < 4) {
            break; // 文件结束或损坏
        }
        // 解析出加密块的长度
        $chunkLength = unpack('N', $lengthBytes)[1];
        // 读取对应长度的加密块内容
        $encryptedChunk = fread($fpEncrypted, $chunkLength);
        // 解密并写入明文
        $decryptedChunk = $encrypted->decrypt($encryptedChunk);
        fwrite($fpDest, $decryptedChunk);
    }

    fclose($fpEncrypted);
    fclose($fpDest);

    return response()->download($destinationFilePath, 'decrypted-' . $file_name);
})->name('decrypt');

关键说明

  • 块长度标记:用pack('N', $length)将加密块长度转为4字节无符号大端整数,确保跨平台兼容性,解密时用unpack解析出长度,保证每次读取完整的加密payload。
  • 内存控制:加密和解密时每次仅处理1MB明文块(加密后块会更大,但读取时按实际加密块长度读取,不会加载整个文件到内存)。
  • 错误处理:解密时增加了文件存在检查和长度读取完整性判断,避免文件损坏导致的异常。

内容的提问来源于stack exchange,提问作者9uifranco

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 22:02:11