Encrypting uploaded files in PHP


During a recent project, the client requested that uploaded files be encrypted for security reasons. As I already had the uploaded code ready and tested I just needed to add some extra encryption capability to the code. As earlier I’d encountered Zends wonderful Zend_Filter class, I decided to go with it and use the Zend_Filter_Encrypt and Zend_Filter_Decrypt to accomplish the work. The Zend_Filter component provides a set of common useful data filters, among which are the encryption filters. Although my project was not developed in Zend, I could easily integrate the required classes in the code. Note that Zend has a great upload library, Zend_File_Transfer, that lets you easily manage file uploading and also encryption, but as I already had the upload code tested, I decided to just add the encryption part.

Downloading the Zend framework

As the following code requires the Zend framework make sure you download it first. For this code I used Zend 1.11.0 Full version. You can also download the required files at the end of this post.

Which Zend framework files do I need

You only need some selected files to make the below code work. The following is a list of files and directories that I used from the ‘ZendFramework-1.11.0\library\Zend’ directory. The top three in the list are directories. There are also many files in the ‘Filter’ directory that are not required, but for keeping it simple we will use the whole thing.

File (dir)
Filter (dir)
Loader (dir)
Loader.php
Filter.php
Exception.php

Encrypting uploaded files

For the following example I’ve not included the file uploading code. I assume you already have the upload code ready. You can also use the following code by itself to encrypt/decrypt files. The program for file encryption is shown below.

<?php
 
/* Load the Zend file encrypting filter */
require_once('./Zend/Filter/File/Encrypt.php');
 
/*  Set various encryption options. */
$options = array(
                // Encryption type - Openssl or Mcrypt
                'adapter' => 'mcrypt', 
                // Initialization vector
                'vector' => '236587hgtyujkirtfgty5678', 
                // Encryption algorithm
                'algorithm' => 'rijndael-192', 
                // Encryption key
                'key' => 'KFJGKDK$$##^FFS345678FG2' 
                );
 
/* Initialize the library and pass the options */
$encrypt = new Zend_Filter_File_Encrypt($options);
 
/* 
   Set output filename, where the encrypted file will be stored.
   If we omit this, the encrypted file will overwrite the original file.
*/
$encrypt->setFilename('test.enc.pdf');
 
/* Now encrypt a file */
$encrypt->filter('test.pdf');
 
?>

Decrypting the encrypted files is as simple as above. Note that you need to keep the ‘vector’ and ‘key’ values the same as you used for encryption, or you will not be able to correctly decrypt the file.

<?php
 
/* Load the Zend file decrypting filter */
require_once('./Zend/Filter/File/Decrypt.php');
 
/*  Set various decryption options. */
$options = array(
                // Encryption type - Openssl or Mcrypt
                'adapter' => 'mcrypt', 
                // Initialization vector
                'vector' => '236587hgtyujkirtfgty5678', 
                // Decryption algorithm
                'algorithm' => 'rijndael-192', 
                // Decryption key
                'key' => 'KFJGKDK$$##^FFS345678FG2' 
                );
 
/* Initialize the library and pass the options */
$decrypt = new Zend_Filter_File_Decrypt($options);
 
/* 
   Set output filename, where the decrypted file will be stored.
*/
$decrypt->setFilename('test.pdf');
 
/* Now decrypt the previously encrypted file */
$decrypt->filter('test.enc.pdf');
 
?>

Selecting a encryption algorithm

In the example given I’ve used the rijndael-192 (AES) algorithm but you can choose some other according to the availability. ‘rijndael’ is fine for most requirements. First you will need to know what algorithms are supported by your installation. A quick way to find out is to use the following code.

<?php
 
    $algorithms = mcrypt_list_algorithms();
 
    foreach ($algorithms as $cipher) {
        echo "$cipher<br />\n";
    }
 
?>

On my system it lists the following algorithms.

cast-128 , gost , rijndael-128 , twofish , arcfour , cast-256 , loki97 , rijndael-192 , saferplus , wake , blowfish-compat , des , rijndael-256 , serpent , xtea , blowfish , enigma , rc2 , tripledes.

Selecting a random Initialization vector

Before we continue our discussion, a brief overview of Initialization vector (IV). An IV is a random string that can be used along with a key for data encryption. IV is used to prevent a sequence of text that is identical to a previous sequence from producing the same exact cipher-text when encrypted.

As you can see from the example code we have used a fixed IV. The disadvantage of this is that there is a possibility that a committed hacker would be able to guess the IV by studying the pattern in the encrypted files and thus break the encryption. The best way then is to let the class generate a random IV for each new encryption. This requires a little change of code, as shown below. The only extra step we now require is to store the generated random IV in a database which will then be used for decryption.

Encryption:

<?php
 
/* Load the Zend file encrypting filter */
require_once('./Zend/Filter/File/Encrypt.php');
 
/*  Set various encryption options. */
$options = array(
                // Encryption type - Openssl or Mcrypt
                'adapter' => 'mcrypt', 
                // Encryption algorithm
                'algorithm' => 'rijndael-192', 
                // Encryption key
                'key' => 'KFJGKDK$$##^FFS345678F54' 
                );
 
/* Initialize the library and pass the options */
$filter = new Zend_Filter_File_Encrypt($options);
 
/* Generate a random vector */
$filter->setVector();
 
/* Set output filename, where the encrypted file will be stored. */
$filter->setFilename('test.enc.pdf');
 
/* Now encrypt a file */
$filter->filter('test.pdf');
 
/* 
    Save the vector in a DB or somewhere else,
    we will need this during decryption
*/
$vector = $filter->getVector();
 
?>

Decryption:

<?php
 
/* Load the Zend file decrypting filter */
require_once('./Zend/Filter/File/Decrypt.php');
 
/*  Set various encryption options. */
$options = array(
                // Encryption type - Openssl or Mcrypt
                'adapter' => 'mcrypt', 
                // Encryption algorithm
                'algorithm' => 'rijndael-192', 
                // Encryption key
                'key' => 'KFJGKDK$$##^FFS345678F54' 
                );
 
/* Initialize the library and pass the options */
$filter = new Zend_Filter_File_Decrypt($options);
 
/* 
   Use the saved vector for decryption.
   Note that using a wrong vector will result in a incorrect decryption.
*/
$filter->setVector('your-saved-vector');
 
/* Set output filename, where the decrypted file will be stored. */
$filter->setFilename('test.dec.pdf');
 
/* Now decrypt the previously encrypted file */
$filter->filter('test.enc.pdf');
 
?>

Selecting the correct ‘vector’ and ‘key’ size

Before you run the code make sure that you set your PHP error reporting to ‘E_ALL’. Mcrypt requires that you use a correct IV and key length, which depends on which algorithm is used. Selecting a wrong IV (if you are using a fixed IV) or key length can generate the following error, which if the errors are disabled will be hidden and you will keep wondering as to why the files are not getting encrypted.:

- in case of a wrong vector length:

Fatal error: Uncaught exception ‘Zend_Filter_Exception’ with message ‘The given vector has a wrong size for the set algorithm’

- in case of a wrong key length:

Fatal error: Uncaught exception ‘Zend_Filter_Exception’ with message ‘The given key has a wrong size for the set algorithm’

To get the correct sizes use the following code. I’ve used ‘rijndael-192′ algorithm here, but you need to substitute whatever algorithm you have selected instead.

<?php
 
$cipher = mcrypt_module_open('rijndael-192', '', 'ofb', '');
$vector_size = mcrypt_enc_get_iv_size($cipher);
$key_size = mcrypt_enc_get_supported_key_sizes($cipher);
print_r($vector_size);
echo "\n";
print_r($key_size);
 
?>

Which on my machine returns the following.

24
 
Array
(
    [0] => 16
    [1] => 24
    [2] => 32
)

So if the IV size returns ’24′ then you need to use a random string of 24 character for the Initialization Vector (IV). The key length can be any from the returned values, 16, 24 or 32.

Downloading encrypted file

To round-off the post, I’ve included code to download a encrypted file via a link, which will be decrypted and passed to the user. For example you can call the download link as below:

http://www.site.com/download-file.php?docname=test.enc.pdf

The code for ‘download-file.php’ is shown below. The code is a bit simplified, for e.g no security validation is done on the $_GET variable.

<?php
 
/* download-file.php */
 
/* 
   The constants UPLOAD_PATH, TEMP_PATH, INIT_VECTOR, ENCRYPTION_KEY
   have to be changed to your particular setup.
*/
 
require_once('Zend/Filter/File/Decrypt.php');
 
$docname = $_GET['docname'];
 
$filename = UPLOAD_PATH . $docname;
$destination = TEMP_PATH . $docname;
 
if(!file_exists($filename)) {
    echo "Error accessing the file.";
    exit();
}
 
/* Copy encrypted file to a temporary folder */
if (!copy($filename, $destination)) {
    die("Error accessing file");
}
 
 /* Zend file decryption */
$filter = new Zend_Filter_File_Decrypt(
                array('adapter'     => 'mcrypt',
                      'vector'      => INIT_VECTOR,
                      'algorithm'   => 'rijndael-192',
                      'key'         => ENCRYPTION_KEY
                      ));
 
$filter->filter($destination);
 
 
$fp = @fopen($destination, 'rb');
 
if (strstr($_SERVER['HTTP_USER_AGENT'], "MSIE"))
{
    header('Content-Type: "application/octet-stream"');
    header('Content-Disposition: attachment; filename="'.$docname.'"');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    header("Content-Transfer-Encoding: binary");
    header('Pragma: public');
    header("Content-Length: ".filesize($destination));
}
else
{
    header('Content-Type: "application/octet-stream"');
    header('Content-Disposition: attachment; filename="'.$docname.'"');
    header("Content-Transfer-Encoding: binary");
    header('Expires: 0');
    header('Pragma: no-cache');
    header("Content-Length: ".filesize($destination));
}
 
fpassthru($fp);
fclose($fp);
 
/* Delete the copied decrypted from the temp folder */
unlink($destination);
 
?>

This site is a digital habitat of Sameer Borate, a freelance web developer working in PHP, MySQL and WordPress. I also provide web scraping services, website design and development and integration of various Open Source API's. Contact me at metapix[at]gmail.com for any new project requirements and price quotes.

16 Responses

1

Name

November 15th, 2010 at 6:10 am

Thank your for this tutorial.

One question is left: How are big files handled ? Does this work or big files a problem ?

sameer

November 15th, 2010 at 6:23 am

I’ve tested it for around 50MB files but not larger. Of course there will be a increased delay as larger files are encrypted.

3

Daniel

November 17th, 2010 at 8:40 am

Thank you very much for this tutorial.
I’ll try it right away when i arrived at home.
I’ll try to use it within cakephp. I think this should work in some way.

4

Srinisha

December 13th, 2010 at 11:17 pm

I tried this code and it works well. Thanks to this useful guide. But i have one doubt, I could not handle this for a big files, Please tell me the reason.. Anyway nice post..

http://godwinsblog.cdtech.in/2010/11/microsoft-now-supports-php.html

sameer

December 15th, 2010 at 2:53 am

How big are we talking about here?

6

Sandeep

February 10th, 2011 at 8:17 am

files about 150MB and more????
cannot handle such big files…

any other way??

7

vincenzo

June 21st, 2011 at 8:51 am

Hi,
I’ve a question.

I implemented the object to encrypt and decrypt but I have a problem…

When I decrypt the decrypting is not correct I suppose for encoding Zend.

How I can set encoding as Zend System default?

My code

Encoding in extended Zend_Form_Element_File
public function __construct($spec, $options = null) {
if (!isset(self::$sslPublicKeyPath) || !isset(self::$sslPrivateKeyPath) || !isset(self::$fingerPrintFile))
throw new Zend_Exception(“Parametri statici non inizializzati.”);

parent::__construct($spec, $options);

if (isset(self::$sslPublicKeyPath) && isset(self::$sslPrivateKeyPath))
{
if (!file_exists(self::$sslPublicKeyPath))
throw new Zend_Exception(“Il file della chiave pubblica ssl non esiste.”);
if (!file_exists(self::$sslPrivateKeyPath))
throw new Zend_Exception(“Il file della chiave pubblica ssl non esiste.”);
$this->addFilter( ‘Encrypt’,
array( ‘adapter’ => self::DEFAULT_ALGORITHM_CRYPTING,
‘public’ => self::$sslPublicKeyPath));
if (isset(self::$fingerPrintFile))
$this->addValidator(‘Hash’,
false,
array( self::$fingerPrintFile,
‘algorithm’ => self::DEFAULT_ALGORITHM_FINGERPRINT));
}
}

For Decrypt I setted a static method in the same class:

public static function decryptFile($absolutePathFilename) {
if (file_exists($absolutePathFilename)) {
// Define decrypt absolute path temp file
// file exists and copied for decrypt it
$session = new Zend_Session_Namespace(‘Crypting_Decrypting’);
$decrypt = new Zend_Filter_File_Decrypt(array( ‘adapter’ => self::DEFAULT_ALGORITHM_CRYPTING,
‘private’ => self::$sslPrivateKeyPath,
‘envelope’ => $session->envelopeKey));

//Set default output filename where the decrypted file will be stored.
$decrypt->setFilename($absolutePathFilename.’.decrypted’);
// Start decrypt of absolute path of crypted file.
$decrypt->filter($absolutePathFilename);

if (strstr($_SERVER['HTTP_USER_AGENT'], “MSIE”))
{
// From IE 7 version need Cache control or It will generate an error.
header(‘Content-Type: “application/x-pkcs7-mime;smime-type=enveloped-data;name=”‘.$absolutePathFilename.’”"‘);
header(‘Content-Disposition: attachment; filename=”‘.basename($absolutePathFilename).’”‘);
header(‘Expires: 0′);
header(‘Cache-Control: must-revalidate, post-check=0, pre-check=0′);
header(“Content-Transfer-Encoding: base64″);
header(‘Pragma: public’);
header(“Content-Length: “.filesize($absolutePathFilename.’decrypted’));
} else { // Other browser
//print_r(“\r\n\r\n”.’Content-Type: “application/pdf”‘.”\r\n”.’Content-Disposition: attachment; filename=”‘.basename($absolutePathFilename).’”‘.”\r\n”.’Content-Transfer-Encoding: binary’.”\r\n”.’Expires: 0′.”\r\n”.’Pragma: no-cache’.”\r\n”.”Content-Length: “.filesize($absolutePathFilename.’.decrypted’).”\r\n”);
header(‘Content-Type: “application/octet-stream;name=”‘.$absolutePathFilename.’”"‘);
header(“Content-Transfer-Encoding: binary”);
header(‘Content-Disposition: in-line; filename=”‘.basename($absolutePathFilename).’”‘);
header(‘Expires: 0′);
header(‘Pragma: no-cache’);
header(“Content-Length: “.filesize($absolutePathFilename.’.decrypted’));
}
// send file
echo file_get_contents($absolutePathFilename.’.decrypted’);
// Delete the copied decrypted
if (!unlink($absolutePathFilename.’decrypted’))
throw new Zend_Exception(‘Deleting file ‘.$absolutePathFilename.’decrypted’.’ error’);
return true;

8

AC

September 20th, 2012 at 5:43 pm

I would like to use this code. However, when I tried it on our Windows 2003 server with IIS6, nothing happened. I’m thinking the script is not finding the zend files? Where exactly should they be located? Thanks for any help!

sameer

September 20th, 2012 at 8:07 pm

Check your include path, or better set it at the start of the script using:

http://php.net/manual/en/function.set-include-path.php

Also check this reference:

http://php.net/configuration.changes

10

AC

September 23rd, 2012 at 1:12 pm

Sorry…I’m now receiving this error when I run the script. Any ideas? I really need to get this working.

Parse error: syntax error, unexpected T_STRING in Zend\Filter\File\Encrypt.php on line 11

11

GK

September 26th, 2012 at 3:27 am

its working fine but its showing blowfish algorithm.
But how to do aes 256 encryption/decryption.
Its urgent..

sameer

September 26th, 2012 at 6:41 am

Check with the mcrypt_list_algorithms() function if your system supports AES encoding and change the options accordingly.

13

gk

September 27th, 2012 at 6:06 am

Thanks…for the help..I used rijndael -128 and gave the key and iv according to aes 256. Now in the last one( downloading encrypted file) i have to use security validation i.e the user who uploaded the file should only be able to download his encrypted file.
hw we can achieve this?????
Thanx again !!!!

14

tmv

November 21st, 2012 at 4:21 am

thanks! very helpfull

15

Jose Clemente Agudo Montero

February 6th, 2013 at 1:21 pm

Thank you for this article, I obtain a error message and I have found the mistake.

For encripting example, the lines

/* Initialize the library and pass the options */
$encrypt = new Zend_Filter_File_Encrypt($options);

/*
Set output filename, where the encrypted file will be stored.
If we omit this, the encrypted file will overwrite the original file.
*/
// $filter->setFilename(‘test.enc.pdf’); // wrong line

The correct line

$encrypt->setFilename(‘test.enc.pdf’); // corrected line

the

16

Josh

June 24th, 2014 at 7:59 am

The server is storing the key hard-coded in plaintext and the IV in the database. If an attacker gets access to the system, say as the www user running the web app, doesn’t this render encryption completely useless as they have access to everything they need to decrypt? What’s the point of encryption then?

Your thoughts