How to decrypt large files by chunks with PHP's openssl_decrypt function?

Viewed 724

I'm working on a file store & share web application with Laravel on the API backend and VueJS as the front app.

I have to encrypt the uploaded files with a BASH script launched every minute with a CRON routine, but I need to decrypt them with Laravel/PHP from a controller into a StreamDownload response (I need to decrypt the files chunks by chunks because the large ones use too much memory for our server).

We decided to encrypt the files from an external routine in order to prevent the user to wait for it to be encrypted, sometimes several minutes after the file upload.

I'm using Laravel 5.7 with PHP 7.3 on a Debian 4.9 server, but I do my tests on a local computer on Windows 10, with WAMP and PHP 7.3 as well. I'm using Git Bash to run and test my shell commands.

My current FileController contains many methods, including "Create" and "Download".

"Create" just stores the file into the Laravel's storage directory and create a new "File" resource in the database, while "Download" tries to retrieve the encrypted file, decrypt it and send it to the client.

Here is my Create method. It just creates the resource and store the file with the '.decrypted' prefix extension. (I let a lot of blanks because the logic of this method is not about encryption)

//App\Http\Controllers\Files\FileController.php

public function create(Request $request)
    {
       ...

       $file = File::create([
            'name' => $name,
            'uuid' => $uuid,
            ...
        ]);

        ...

        $output->move($userPath, $uuid.'.decrypted');

        ...

        return new FileResource($file);
    }

Then, here's the BASH script I wrote to encrypt the suffixed files every minute (I replaced sensible information with some '###', don't be worried.)

#encrypt.sh

#!/bin/bash

set -euo pipefail

# PARAMETERS
APP_KEY='######'
FILES_PATH='/###/.../storage/app/files/'
FILES_LIST=$(find "$FILES_PATH" -type f -name '*.decrypted' )
KEY=$(echo "$APP_KEY" | base64 -d -i | xxd -p -c 64)

while read -r file; do
  INPUT_PATH=$file
  OUTPUT_PATH=${file%.decrypted}
  IV=$(openssl rand -hex 16)
  openssl AES-256-CBC -K $KEY -iv $IV -in $INPUT_PATH -out $OUTPUT_PATH
done < <(echo "$FILES_LIST")

echo 'Done'

This code works well as far as I know.

Then, here is my last piece of code: the Download method.

//App\Http\Controllers\Files\FileController.php

public function download(File $file, Request $request)
    {
        ...

        $dlFile = Storage::disk('files')->path($file->path);
        
        ...

        return response()->streamDownload(
            /* Note: $dlFile is the path, $file is the Laravel resource */
            function () use ($dlFile, $log, $file) {
                $cipher = config('app.cipher'); // AES-256-CBC
                /* Note: the app key is stored in format "base64:#####...", this is why there's a substr() inside a base64() */
                $key = base64_decode(substr(config('app.key'), 7));
                if ($fpIn = fopen($dlFile, 'rb')) {
                    $iv = fread($fpIn, 16);
                    while (!feof($fpIn)) {
                        $ciphertext = fread($fpIn, $this->memoryLimit());
                        $plaintext = openssl_decrypt($ciphertext, $cipher, $key, OPENSSL_RAW_DATA, $iv);
                        print($plaintext);
                    }
                    fclose($fpIn);
                }
            },
            $fileName,
            [
                'Content-Type' => $file->mime,
                'Content-Length' => $file->size
            ],
            'inline'
        );
    }

I got this last piece of code from this page.

I guess that my PHP script is not well made because the decrypted outputs are wrong. Does someone have an idea to help me ?

1 Answers

There isn't an easy answer to this problem, due to how Laravel's encryption is designed.

You could reimplement the logic used by Defuse Security's PHP Encryption library for encrypting/decrypting large files on low-memory devices. That's a very complex problem to solve, however, and you'll have to emulate the JSON serialization that gets fed into the HMAC, too.

You could also switch from Laravel's built-in encryption library to Defuse's library for these files. It has the built-in File API that does what you want already. That's probably the least effort solution.

Related