Изначально я шифровал файлы напрямую, без использования фрагментов, и начал сталкиваться с ошибкой при попытке зашифровать большие файлы:
Разрешенный объем памяти 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?
При расшифровке не беспокойтесь о размере фрагмента, прочитайте содержимое всего файла обратно.
Кажется, вы пишете только fwrite($fpDest, $encryptedChunk);, но это увеличится в размерах, а затем вы прочитаете фрагменты того же размера. Это не сработает. Если вам действительно нужно, используйте какой-нибудь разделитель и читайте фрагменты таким образом.
Если Config::get('app.cipher') указывает режим блочного шифрования, например CBC (Encrypter по умолчанию использует aes-128-cbc с дополнением PKCS#7), необходимо дополнительно учитывать заполнение (с размером фрагмента открытого текста 1024 * 1024 байт, зашифрованного текста 1024 * Результат 1024 + 16 байт). Используя PHP/OpenSSL напрямую (Encrypter применяет его «под капотом»), один из возможных подходов — отключить отступы, за исключением последнего блока.






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