I was initially encrypting files directly without using chunks and started encountering an error when trying to encrypt large files:
Allowed memory size of 134217728 bytes exhausted
After some research, I found out I was making a big mistake by trying to encrypt directly by doing something like:
Storage::put($filePath, $encrypted->encrypt(file_get_contents($file)));
This is a bad idea because it will load the entire file into memory, as well as the encrypted version. For small files, this works fine but not for large files because it will exceed PHP memory_limit.
To address this, I decided to implement encryption and decryption in chunks. I want to use the Encrypter class provided by Laravel but I am running into some issues. This is what I tried:
Note: The following routes are for testing purposes.
Encryption
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');
Decryption:
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');
Error
I'm encountering the following error when trying to decrypt a chunk:
Illuminate\Contracts\Encryption\DecryptException: The payload is invalid.
Question:
How can I properly encrypt and decrypt large files in chunks using Laravel's Encrypter class?
fwrite($fpDest, $encryptedChunk);but that will have expanded in size, you then read back chunks of the same size. That won't work. If you really have to, then use some kind of separator and read the chunks back that way.Config::get('app.cipher')specifies a block cipher mode such as CBC (Encrypterusesaes-128-cbcwith PKCS#7 padding by default), the padding must additionally be taken into account (with a plaintext chunksize of 1024 * 1024 bytes, a ciphertext of 1024 * 1024 + 16 bytes results). Using PHP/OpenSSL directly (Encrypterapplies it under the hood), one possible approach is to disable padding, except for the last block.