Search code examples
phpencryptionopensslphp-opensslsynology

How to encrypt and decrypt strings longer than 65535 characters with PHP


Here is my problem,

I want to encrypt JSON files that may be very long in some cases. (Sometimes containing images in Base64 format).

On the following test servers, everything works:

  • Raspberry Pi 3
  • Dell Poweredge T110
  • IIS on Windows 10
  • Synology DS1815 +

On the other hand, on the following servers, (Which are intended to be used..) the encryption does not work with more than 65535 characters, the server seems to crash.

  • Synology RS212
  • Synology DS112 +

Is there a restriction on the CPU?

Can a parameter of php.ini affect?

I tested exactly the same code on multiple servers, and on both Synology mentioned, it does not work ...

Here is my class of encryption / decryption:

class PHP_AES_Cipher {

    private static $OPENSSL_CIPHER_NAME = "AES-256-CBC"; //Name of OpenSSL Cipher 
    private static $CIPHER_KEY_LEN = 32; 

    static function encrypt($key, $iv, $data) {
        if (strlen($key) < PHP_AES_Cipher::$CIPHER_KEY_LEN) {
            $key = str_pad("$key", PHP_AES_Cipher::$CIPHER_KEY_LEN, "0");
        } else if (strlen($key) > PHP_AES_Cipher::$CIPHER_KEY_LEN) {
            $key = substr($str, 0, PHP_AES_Cipher::$CIPHER_KEY_LEN); 
        }

        $encodedEncryptedData = base64_encode(openssl_encrypt($data, PHP_AES_Cipher::$OPENSSL_CIPHER_NAME, $key, OPENSSL_RAW_DATA, $iv));
        $encodedIV = base64_encode($iv);
        $encryptedPayload = $encodedEncryptedData.":".$encodedIV;

        return $encryptedPayload;

    }


    static function decrypt($key, $data) {
        if (strlen($key) < PHP_AES_Cipher::$CIPHER_KEY_LEN) {
            $key = str_pad("$key", PHP_AES_Cipher::$CIPHER_KEY_LEN, "0");
        } else if (strlen($key) > PHP_AES_Cipher::$CIPHER_KEY_LEN) {
            $key = substr($str, 0, PHP_AES_Cipher::$CIPHER_KEY_LEN);
        }

        $parts = explode(':', $data); //Separate Encrypted data from iv.
        $decryptedData = openssl_decrypt(base64_decode($parts[0]), PHP_AES_Cipher::$OPENSSL_CIPHER_NAME, $key, OPENSSL_RAW_DATA, base64_decode($parts[1]));

        return $decryptedData;
    }
}

I use it like this:

$data = PHP_AES_Cipher::encrypt($key, $iv, $data);

and

$data = PHP_AES_Cipher::decrypt($key, $iv, $data);

Assuming everything works on some servers, I think the code has no problems. I already checked the Apache and PHP logs, nothing to report.

I have been searching for days without understanding the cause of the problem.

In hope that someone can help me :-)


Solution

  • Chunk it,

    This is what I do (Uses PHPSecLib2 )

    /**
     * AES encrypt large files using streams and chunking
     * 
     * @param resource $stream
     * @param resource $outputStream
     * @param string $key
     * @throws SecExecption
     */
    function streamSymEncode($stream, &$outputStream, $key, $chunkSize = 10240){
        if(!is_resource($stream)) throw new Execption('Resource expected[input]');  
        rewind($stream); //make sure the stream is rewound
    
        if(!is_resource($outputStream)) throw new Execption('Resource expected[output]');
    
        $Cipher = new AES(AES::MODE_CBC);
        $Cipher->setKey($key);
        //create the IV
        $iv = Random::string($Cipher->getBlockLength() >> 3);
        $Cipher->setIV($iv);
    
        if(strlen($iv_base64 = rtrim(base64_encode($iv), '=')) != 22) throw new Execption('IV lenght check fail');
    
        fwrite($outputStream, $iv_base64.'$'); //add the IV for later use when we decrypt
    
        while(!feof($stream)){
            $chunk = fread($stream, $chunkSize); 
            fwrite($outputStream, rtrim(base64_encode($Cipher->encrypt($chunk)),'=').':');
        }
    
        $stat = fstat($outputStream);
    
        ftruncate($outputStream, $stat['size'] - 1);    //trim off the last character, hanging ':'    
    }
    
    /**
     * AES decrypt large files that were previously encrypted using streams and chunking 
     * 
     * @param resource $stream
     * @param resource $outputStream
     * @param string $key
     * @throws SecExecption
     */
    function streamSymDecode($stream, &$outputStream, $key){
        if(!is_resource($stream)) throw new Execption('Resource expected[input]');
        rewind($stream); //make sure the stream is rewound
    
        if(!is_resource($outputStream)) throw new Execption('Resource expected[output]');
    
        $Cipher = new AES(AES::MODE_CBC);
        $Cipher->setKey($key);
    
        $iv = base64_decode(fread($stream, 22) . '==');
        $Cipher->setIV($iv);
    
        fread($stream, 1); //advance 1 for the $
    
        $readLine = function(&$stream){
            $line = '';
            while(false !== ($char = fgetc($stream))){
                if($char == ':') break;
                $line .= $char;
            }
            return $line;
        };
    
        while(!feof($stream)){
            $chunk = $readLine($stream);
    
            $decrypted = $Cipher->decrypt(base64_decode($chunk.'=='));
            if(!$decrypted) throw new Execption('Failed to decode!');
    
            fwrite($outputStream, $decrypted);
        }       
    }
    

    It takes two File stream resources like what you get from fopen and a key. Then it uses the same ecryption but chunks the file into $chunkSize separates them with : and when it decodes, it splits it back into chunks and re-assembles everything.

    It winds up like this (for example)

      IV$firstChunk:secondChunk:thirdChunk
    

    This way you don't run out of memory trying to encrypt large files.

    Please Note this was part of a lager class I use so I had to trim some things and make a few changes, that I haven't tested.

    https://github.com/phpseclib/phpseclib

    Cheers.