PHP - Improve file encryption - php

Disclaimer: English isn't my mother tongue so feel free to ask if something isn't clear.
Hi there,
I have to encrypt files using AES as soon as they are uploaded on the server and send the key needed to decrypt them via mail to the client (not storing it anywhere server side). Files can be as big as 2GB and are deleted 7 days after their upload.
Here is what I'm using to encrypt/decrypt files :
function encrypt_file($source, $destination, $key) {
$iv = md5("\x1B\x3C\x58".$key, true);
$ivsize = openssl_cipher_iv_length('aes-256-cbc');
$fp = fopen($destination, 'wb') or die("Could not open file for writing.");
$handle = fopen($source, "rb");
while (!feof($handle)) {
$e = 0;
$contents = fread($handle, 4 * 1024 * 1024);
$ciphertext = openssl_encrypt($contents, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
$iv = substr($ciphertext, -$ivsize);
while (!fwrite($fp, $ciphertext)) {
$e++;
if ($e == 5) {
die("Couldn't write to file.");
break 2;
}
}
}
fclose($handle);
fclose($fp);
}
function streamdecrypt_file($source, $key) {
$iv = md5("\x1B\x3C\x58".$key, true);
$ivsize = openssl_cipher_iv_length('aes-256-cbc');
$handle = fopen($source, "rb");
while (!feof($handle)) {
$contents = fread($handle, 4 * 1024 * 1024);
$raw = openssl_decrypt($contents, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
$iv = substr($contents, -$ivsize);
print $raw; // Printing because it's directly sent to the user to download
}
fclose($handle);
}
If you're wondering why 4 * 1024 * 1024 it's just that this is the buffer size with which I got the fastest encryptions. My implementation uses the schema proposed here https://stackoverflow.com/a/30742644/3857024
I also made those 2 little functions to encrypt a string to a file using a passphrase :
function encrypt_string($source, $destination, $passphrase) {
$iv = md5("\x1B\x3C\x58".$passphrase, true);
$key = md5("\x2D\xFC\xD8".$passphrase, true);
$ciphertext = openssl_encrypt($source, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
$fp = fopen($destination, 'wb') or die("Could not open file for writing.");
fwrite($fp, $ciphertext) or die("Could not write to file.");
fclose($fp);
}
function decrypt_string($source, $passphrase) {
$iv = md5("\x1B\x3C\x58".$passphrase, true);
$key = md5("\x2D\xFC\xD8".$passphrase, true);
$contents = file_get_contents($source);
return openssl_decrypt($contents, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
}
And here is what I'm finally doing when an upload is complete :
$skey = /* 32 chars random generated string [a-Z0-9]*/
$ukey = /* 10 chars random generated string [a-Z0-9]*/
encrypt_file($originalFile, $encryptedFile, $skey);
encrypt_string($skey, $encryptedKey, $ukey);
I then delete the original file and send a link containing the $ukey to the user via mail.
When they want to decrypt the file to download it, I first decrypt the file containing the $skey using the $ukey, checking if I end up with a 32-chars 256-bits long string made of [a-Z0-9]. If the $skey doesn't match the regexp, I know the $ukey is invalid, so I stop there.
I did this so that I wouldn't have to decrypt the file to check if the key was correct or not.
Now I hope that my questions fit in SO :
Am I doing it right ?
Is there anything that could/should be improved ?
It takes about 60s to encrypt a 2GB file, is that an "ok" result ?
Is it good enough ? The goal is to prevent an attacker gaining access to the server to also gain access to the users files already stored. I know he would then be able to modify the code and access the following uploads, but that should protect the files already stored right ? Am I doing too much for nothing ?
Thank you for your answers !

For an IV, use random bytes.
For password expansion, use PBKDF2 or equivalent; the derivation needs to be slower.
Restricting a key to the characters [a-Z0-9] reduces the 256 key to essentially 36 bytes. That is not very secure. You need at least 128-bits of key material.
You need a better method to authenticate the user's password.

Related

encrypt decrypt a file: SVG or XML with PHP

I use these two functions to encrypt / decrypt files :
private function encrypt_file($source,$destination,$passphrase,$stream=NULL) {
// $source can be a local file...
if($stream) {
$contents = $source;
// OR $source can be a stream if the third argument ($stream flag) exists.
}else{
$handle = fopen($source, "rb");
$contents = #fread($handle, filesize($source));
fclose($handle);
}
$iv = substr(md5("\x1B\x3C\x58".$passphrase, true), 0, 8);
$key = substr(md5("\x2D\xFC\xD8".$passphrase, true) . md5("\x2D\xFC\xD9".$passphrase, true), 0, 24);
$opts = array('iv'=>$iv, 'key'=>$key);
$fp = fopen($destination, 'wb') or die("Could not open file for writing.");
stream_filter_append($fp, 'mcrypt.tripledes', STREAM_FILTER_WRITE, $opts);
fwrite($fp, $contents) or die("Could not write to file.");
fclose($fp);
}
private function decrypt_file($file,$passphrase) {
$iv = substr(md5("\x1B\x3C\x58".$passphrase, true), 0, 8);
$key = substr(md5("\x2D\xFC\xD8".$passphrase, true) .
md5("\x2D\xFC\xD9".$passphrase, true), 0, 24);
$opts = array('iv'=>$iv, 'key'=>$key);
$fp = fopen($file, 'rb');
stream_filter_append($fp, 'mdecrypt.tripledes', STREAM_FILTER_READ, $opts);
return $fp;
}
It works perfectly for most files. But there is a problem with SVG or XML files in general. Decryption of an SVG file for example gives characters "NUL NUL ..." in the last line. As you can see in this picture:
You may have copied the code straight from the PHP documentation. But: As it says on the same page, there are several issues with this code. Basically using md5 for key derivation is far from optimal. See http://www.cryptofails.com/post/70059608390/php-documentation-woes for full description. This and encryption filters are deprecated (see same link), I would recommend abandoning this style of cryptography.
I would also recommend using some tested PHP crypto library like libsodium-php. This will also be integrated into php7 itself. (Source)
Back to topic: What you are seeing is the encryption padding. For the block cipher (in your case DES) to work, each chunk has to have the size given by the algorithm. Since most data doesn't care about chunk size, the algorithm has to apply some kind of padding.
When decrypting, you also receive the padded value. To get to your output value, you need to remove the padding afterwards. In your case this would be to trim the tailing NUL charachters. Its already in the documentation (thanks to #James for pointing this out)
$data = rtrim(stream_get_contents($fp)); //trims off null padding

PHP Encrypting Larger Files

I'm trying to encrypt all files being uploaded to the server, and my method of doing it works; but I've noticed DECRYPTING files over 100kb just returns null, and I'm confused why encrypting works on these files, but decrypting doesn't. Is there something wrong with my code, or is there another approach to this? There is nothing wrong with the allowed upload sizes in php.ini, the upload.php page works perfectly fine, and uploads the files to the server. The only issue is with files over 100kb. I have a feeling it has something to do with the max variable length in PHP, but I'm not sure.
// Encrypt Function
public static function mc_encrypt($encrypt, $key)
{
$encrypt = serialize($encrypt);
$iv = mcrypt_create_iv(mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC), MCRYPT_DEV_URANDOM);
$key = pack('H*', $key);
$mac = hash_hmac('sha256', $encrypt, substr(bin2hex($key), -32));
$passcrypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $encrypt.$mac, MCRYPT_MODE_CBC, $iv);
$encoded = base64_encode($passcrypt).'|'.base64_encode($iv);
return $encoded;
}
// Decrypt Function
public static function mc_decrypt($decrypt, $key)
{
$decrypt = explode('|', $decrypt.'|');
$decoded = base64_decode($decrypt[0]);
$iv = base64_decode($decrypt[1]);
if(strlen($iv)!==mcrypt_get_iv_size(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC)){ return false; }
$key = pack('H*', $key);
$decrypted = trim(mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $decoded, MCRYPT_MODE_CBC, $iv));
$mac = substr($decrypted, -64);
$decrypted = substr($decrypted, 0, -64);
$calcmac = hash_hmac('sha256', $decrypted, substr(bin2hex($key), -32));
if($calcmac!==$mac){ return false; }
$decrypted = unserialize($decrypted);
return $decrypted;
}
Where it should be decrypted:
try
{
$server = $db->prepare("SELECT * FROM `servers` WHERE `ServerIP` = :ip LIMIT 1");
$server->execute([ ":ip" => $ip ]);
$server = $server->fetch();
$sftp = new SFTPConnection($server['ServerIP'], intval($server['ServerPort']));
$sftp->login($server['ServerUser'], $server['ServerPassword']);
$fileData = $sftp->receiveFile($path);
//print $fileData;
header('Content-type: text/plain');
$fileName = $file['FileName'];
header("Content-Disposition: attachment; filename=$fileName");
//print $fileData; (returns the encrypted version)
$fileData = Encryption::mc_decrypt($fileData, $file['EncryptionKey']);
print $fileData; // (returns null on larger files)
}
catch (Exception $e)
{
echo $e->getMessage() . "\n";
}
I'm not sure what the issue is, but I do know a solution. First of all, you probably want to read in the file in chucks. You don't want to store e.g. an entire movie in RAM. So what you can do is to treat the SFTP connection as stream:
According to the sample code here:
$connection = ssh2_connect('shell.example.com', 22);
ssh2_auth_password($connection, 'username', 'password');
$sftp = ssh2_sftp($connection);
$stream = fopen("ssh2.sftp://$sftp/path/to/file", 'rb');
Note that I used 'rb' to force binary mode.
So now you can read in chunks from the stream, the only thing you need to do is to encrypt/decrypt the stream. Mcrypt does actually provide this functionality using a filter implementation.
As for the HMAC, you can stream that as well. You may want to create a filter for it - I could not find one.
So now that you can stream everything, go ahead and implement it.
Security notes:
mcrypt is an old library that should not be used anymore;
use MCRYPT_RIJNDAEL_128 instead of using MCRYPT_RIJNDAEL_256 if you want to use AES (the 256 is the block size, not the key size, the key size is determined by - wait for it - the size of the provided key);
HMAC is secure, but it should be performed over the ciphertext and the IV;
this is not a full transport protocol - but that doesn't matter much if you send the file over sftp.

Zend_Filter_Encrypt and Memory

Is there any way to use Zend_Filter_Encrypt with large files, without rising memory limit to an unacceptable amount?
This is my code so far, but when i have to encrypt files larger than 32 MB (thats my memory limit) it fails, if I set memory to 48MB it works:
$vector = 'XX';
$algorithm = 'rijndael-192';
$options = array(
'adapter' => 'mcrypt',
'vector' => $vector,
'algorithm' => $algorithm,
'key' => $key
);
$encrypt = new Zend_Filter_File_Encrypt($options);
$result = $encrypt->filter($file);
No, there isn't. Zend_Filter_Encrypt works by encrypting/decrypting the data in one pass, thus requiring the full data in order to function.
If you need to decrypt a large file, you can do it manually in smaller chunks.
<?php
$key = 'secret key';
$iv = 'xxxxxxxxxxxxxxxx';
$cipher = mcrypt_module_open('rijndael-192', '', 'cbc', '');
mcrypt_generic_init($cipher, $key, $iv);
$fp = fopen('/tmp/encrypted.txt', 'r+b');
while (!feof($fp)) {
$data = fread($fp, 1154);
$decrypted = mdecrypt_generic($cipher, $data);
echo $decrypted;
}
fclose($fp);
mcrypt_generic_deinit($cipher);
mcrypt_module_close($cipher);
Just make sure the amount of data that you read (fread) is a multiple of the block size used by the algorithm, otherwise the results can be unexpected.

PHP mcrypt_module_open causes 500 error

I have this PHP function (using PHP 5.3) that I use to decrypt files, it used to work just fine, but now that I moved to Amazon EC2 (based on Amazon Linux Image 2012.3), it seems that mcrypt install is either corrupted or not available at all.
Initial tests suggests that file decryption does work on smaller files, but not on 20MB+ files (which is not a particularly large size).
I tracked the problem down to this line, which is causing an Error 500 (I'm not getting mcrypt_module_open is undefined, just 500 server error)
$td = mcrypt_module_open ('rijndael-128', '', 'cbc', '');
What's strange, I checked /etc/php.ini, I can't see mcrypt at all (assuming I'm looking at the correct php.ini/path of course!)
The PHP code/function is:
function decrypt_file ($inputfile, $outputfile)
{
$key = FILE_KEY; // <-- assign private key
$buffersize = 16384;
// Open $inputfile for reading binary
$input = fopen ($inputfile, 'rb');
// Error opening $inputfile, return false
if (!$input)
return false;
// Open $outputfile for writing binary
$output = fopen ($outputfile, 'wb');
// Error opening $outputfile, return false
if (!$output)
return false;
// Open the cipher module
$td = mcrypt_module_open ('rijndael-128', '', 'cbc', '');
// Read the IV from $inputfile
$iv = fread ($input, 16);
// Compute the SHA512 of the IV (salt) and Key and use 32 bytes (256 bit) of the result as the encryption key
$keyhash = substr (hash ('sha512', $iv . $key, true), 0, 32);
// Intialize encryption
mcrypt_generic_init ($td, $keyhash, $iv);
while (!feof ($input))
{
$buffer = fread ($input, $buffersize);
// Encrypt the data
$buffer = mdecrypt_generic ($td, $buffer);
// Remove padding for last block
if (feof ($input))
{
$padsize = ord ($buffer[strlen ($buffer) - 1]);
$buffer = substr ($buffer, 0, strlen ($buffer) - $padsize);
}
// Write the encrypted data to $output
fwrite ($output, $buffer, strlen ($buffer));
}
fclose ($input);
fclose ($output);
// Deinitialize encryption module
mcrypt_generic_deinit ($td);
// Close encryption module
mcrypt_module_close ($td);
return true;
}
Anyone knows how to fix that? I'm using PHP 5.3 with CodeIgniter 2.1 (thought this is most likely not related to CodeIgniter)
It looks like you don't have mcrypt installed. Try running:
sudo yum install php-mcrypt
...from the command line on your instance.

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 :-)
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.

Categories