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?
The main problem with your code is that what you get from Illuminate\Encryption\Encrypter::encrypt()
is not only encrypted text.
What you get is more like this:
base64_encode(
json_encode([
'iv' => '...', // initialization vector used for encryption
'value' => '...', // encrypted text
'mac' => '...', // HMAC signature
'tag' => '...', // Tag returned by OpenSSL for some ciphers
])
);
So encrypting chunk with size of 1MB will result in more then 1MB of output.
But as Maarten Bodewes noted in comments, you are only reading 1MB chunks when attempting decryption. Because of that you are not reading whole encrypted chunk and that's reason for The payload is invalid
error.
The best way to deal with encryption of file would be using some library that already has support for file encryption. For example defuse/php-encryption
.
If you can't use other library it might be better to use openssl_encrypt()
directly to avoid adding IV and signature to each chunk. If you go this way you might want to take a look on how the encryption is implemented in Laravel for inspiration. Or you can check out how it's implemented in other libraries.
If you insist on using Illuminate\Encryption\Encrypter
then using some separator as suggested by Maarten Bodewes might be a way to go. For example write each encrypted chunk in separate line. When decrypting you will read the encrypted file by lines instead of fixed chunk size.