Laravel — Как шифровать и расшифровывать большие файлы частями?

Изначально я шифровал файлы напрямую, без использования фрагментов, и начал сталкиваться с ошибкой при попытке зашифровать большие файлы:

Разрешенный объем памяти 134217728 байт исчерпан.

После некоторых исследований я обнаружил, что совершил большую ошибку, пытаясь зашифровать напрямую, выполнив что-то вроде:

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

Это плохая идея, поскольку в память будет загружен весь файл, а также его зашифрованная версия. Для небольших файлов это работает нормально, но не для больших файлов, поскольку это будет превышать PHP memory_limit.

Чтобы решить эту проблему, я решил реализовать шифрование и дешифрование по частям. Я хочу использовать класс Encrypter, предоставленный Laravel, но у меня возникли некоторые проблемы. Это то, что я пробовал:

Примечание. Следующие маршруты предназначены для тестирования.

Шифрование

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) {
        $key = Config::get('app.file_key'); // Base64 encoded key stored in .env
        $key = str_replace('base64:', '', $key);
        $key = base64_decode($key);
        $encrypted = new Encrypter($key, Config::get('app.cipher'));

        $path = 'my-file.enc';
        $tempFilePath = storage_path('app/public/temp');
        $chunkSize = 1024 * 1024; // 1 MB

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

        $firstChunkEnc = null;
        while (!feof($fpSource)) {
            $plaintext = fread($fpSource, $chunkSize);
            $encryptedChunk = $encrypted->encrypt($plaintext);
            $firstChunkEnc = $firstChunkEnc ?? $encryptedChunk;
            fwrite($fpDest, $encryptedChunk);
        }

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

        if ($hasUploaded) {
            return response()->json(['success' => true, 'message' => 'File uploaded and encrypted successfully']);
        }
    }

    return response()->json(['success' => false, 'message' => 'File not uploaded']);
})->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");

    $chunkSize = 1024 * 1024; // 1 MB

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

    while (!feof($fpEncrypted)) {
        $encryptedChunk = fread($fpEncrypted, $chunkSize);
        $decryptedChunk = $encrypted->decrypt($encryptedChunk);
        fwrite($fpDest, $decryptedChunk);
    }

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

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

Ошибка

При попытке расшифровать фрагмент я столкнулся со следующей ошибкой:

Illuminate\Contracts\Encryption\DecryptException: полезные данные недействительны.

Вопрос:

Как я могу правильно шифровать и расшифровывать большие файлы по частям, используя класс Encrypter Laravel?

PHP на самом деле не предназначен для этого, это язык более высокого уровня. Для шифрования файлов вы не хотите разбивать их на отдельные значения iv + зашифрованный текст + тег/mac в кодировке JSON. Вызовите функцию C или что-то в этом роде.

Maarten Bodewes 05.06.2024 05:42

При расшифровке не беспокойтесь о размере фрагмента, прочитайте содержимое всего файла обратно.

Chris Haas 05.06.2024 05:42

Кажется, вы пишете только fwrite($fpDest, $encryptedChunk);, но это увеличится в размерах, а затем вы прочитаете фрагменты того же размера. Это не сработает. Если вам действительно нужно, используйте какой-нибудь разделитель и читайте фрагменты таким образом.

Maarten Bodewes 05.06.2024 05:47

Если Config::get('app.cipher') указывает режим блочного шифрования, например CBC (Encrypter по умолчанию использует aes-128-cbc с дополнением PKCS#7), необходимо дополнительно учитывать заполнение (с размером фрагмента открытого текста 1024 * 1024 байт, зашифрованного текста 1024 * Результат 1024 + 16 байт). Используя PHP/OpenSSL напрямую (Encrypter применяет его «под капотом»), один из возможных подходов — отключить отступы, за исключением последнего блока.

Topaco 05.06.2024 11:30
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Symfony Station Communiqué - 7 июля 2023 г
Symfony Station Communiqué - 7 июля 2023 г
Это коммюнике первоначально появилось на Symfony Station .
Оживление вашего приложения Laravel: Понимание режима обслуживания
Оживление вашего приложения Laravel: Понимание режима обслуживания
Здравствуйте, разработчики! В сегодняшней статье мы рассмотрим важный аспект управления приложениями, который часто упускается из виду в суете...
Установка и настройка Nginx и PHP на Ubuntu-сервере
Установка и настройка Nginx и PHP на Ubuntu-сервере
В этот раз я сделаю руководство по установке и настройке nginx и php на Ubuntu OS.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
Как установить PHP на Mac
Как установить PHP на Mac
PHP - это популярный язык программирования, который используется для разработки веб-приложений. Если вы используете Mac и хотите разрабатывать...
1
4
141
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Основная проблема вашего кода в том, что вы получаете от Illuminate\Encryption\Encrypter::encrypt() не только зашифрованный текст.

То, что вы получаете, выглядит примерно так:

base64_encode(
   json_encode([
       'iv' => '...', // initialization vector used for encryption
       'value' => '...', // encrypted text
       'mac' => '...', // HMAC signature
       'tag' => '...', // Tag returned by OpenSSL for some ciphers
   ])
);

Таким образом, шифрование фрагмента размером 1 МБ приведет к получению более 1 МБ выходных данных. Но, как заметил в комментариях Маартен Бодевес, при попытке расшифровки вы читаете только фрагменты размером 1 МБ. Из-за этого вы не читаете весь зашифрованный фрагмент, и это причина ошибки The payload is invalid.

Лучший способ справиться с шифрованием файлов — использовать какую-нибудь библиотеку, которая уже поддерживает шифрование файлов. Например defuse/php-encryption.

Если вы не можете использовать другую библиотеку, возможно, лучше использовать openssl_encrypt() напрямую, чтобы избежать добавления IV и подписи в каждый фрагмент. Если вы пойдете по этому пути, возможно, вам захочется взглянуть на то, как шифрование реализовано в Laravel для вдохновения. Или вы можете посмотреть, как это реализовано в других библиотеках.

Если вы настаиваете на использовании Illuminate\Encryption\Encrypter, возможно, вам подойдет разделитель, предложенный Маартеном Бодевесом. Например, напишите каждый зашифрованный фрагмент в отдельной строке. При расшифровке вы будете читать зашифрованный файл по строкам, а не по частям фиксированного размера.

Спасибо. Вы абсолютно правы. Размер зашифрованного фрагмента составляет 1864336 байт (за исключением последнего, размер которого может отличаться), а я читаю 1048576 байт. Он отлично работает, когда я читаю именно это значение. Я проверю те альтернативы, которые вы упомянули.

9uifranco 05.06.2024 17:39

Другие вопросы по теме