Robust crypt functionality for use in CakePHP and other frameworks

Robust and flexible data encryption functionality inside your web application is an essential part of your toolkit when developing your next fantastic webapp. The right setup can help you through situations that are troublesome and/or painful to deal with otherwise.

The most common scenario is that where you need to pass application state around that involves data that you do not want users to see or tamper with. If you are dealing with your own application you would just use a user session data-store or encode it an drop it into a cookie. What about situations where you need to communicate in between systems that do not have any form of backend communication between them? Similarly, what about situations where you want to manage state that involves an external callback where you have no opportunity to add your own user data? AWS S3 file uploads, Paypal IPNs and Salesforce.com all have scenarios where this can present itself.

So what to do?

Easy - serialize your data, compress it, encrypt the whole lot then pass the resulting string around in the URL itself. You'll end up with a long URL but for most practical usage scenarios you'll be able to pass around a nice little array or object with your state data and be confident that the said data was not observed or tampered with by the user that transported it for you.

Go ahead and use CryptClass for PHP - it's a rewrite of this code and easier to use - http://www.nicholasdejong.com/story/cryptclass-php

In the case of the CakePHP framework I usually set myself up with the following which can all be replicated in any other PHP web framework by using the CryptClass as the basis for your encrypt/decrypt functions:-

cakeapp/config/core.php

Configure::write('Cryptable.mode',MCRYPT_MODE_CBC);
Configure::write('Cryptable.cipher',MCRYPT_RIJNDAEL_192);
Configure::write('Cryptable.key',Configure::read('Security.salt'));
Configure::write('Cryptable.iv',base64_decode(md5(Configure::read('Security.salt'))));

cakeapp/app_controller.php

App::import('Lib','Crypt/CryptClass',array('file'=>'CryptClass.php'));

class AppController extends Controller {

	/**
	 * crypt
	 */
	private $Crypt = null;

	/**
	 * _encrypt()
	 *
	 * @param mixed $data
	 * @return string
	 */
	function _encrypt($data) {
		if(!$this->Crypt) { $this->__makeCrypt(); }
		return $this->Crypt->encrypt($data);
	}

	/**
	 * _decrypt()
	 *
	 * @param mixed $data
	 * @return mixed
	 */
	function _decrypt($data) {
		if(!$this->Crypt) { $this->__makeCrypt(); }
		return $this->Crypt->decrypt($data);
	}

	/**
	 * __makeCrypt()
	 */
	function __makeCrypt() {
	        $this->Crypt = new CryptClass(
			Configure::read('Cryptable.cipher'),
			Configure::read('Cryptable.key'),
			Configure::read('Cryptable.mode'),
			Configure::read('Cryptable.iv')
		);
	}
}

cakeapp/views/helpers/crypt.php

class CryptHelper extends AppHelper {

        private $Crypt;

        /**
         * encrypt()
         *
         * @param mixed $data
         * @return string
         */
        function encrypt($data) {
                if(!$this->Crypt) {
                        $this->Crypt = new CryptClass(
				Configure::read('Cryptable.cipher'),
				Configure::read('Cryptable.key'),
				Configure::read('Cryptable.mode'),
				Configure::read('Cryptable.iv'
			));
                }
                return $this->Crypt->encrypt($data);
        }
}

NOTE:- There is a flaw in CryptClass that I'm surprised was not pointed out to me before - that is - I don't properly handle the IV and it remains constant which in turn means it is probably feasible to back out the encryption key - the problem is similar as with 802.11 WEP and their predictable handling of IVs, in my case its worse as my IV remains constant over time - while it's embarrassing I'm keeping it up here as a lesson to be learned by others - implementing crypto correctly can be hard!
Go ahead and use the updated CryptClass instead - http://www.nicholasdejong.com/story/cryptclass-php

cakeapp/libs/crypt/CryptClass.php

<?php
/**
 * @author Nicholas de Jong
 * @copyright Nicholas de Jong
 * @license All rights and licenses reserved
 *
 **/
class CryptClass {

        /**
         * Enables / Disables compression
         *
         * @var bool
         */
        public $compression = TRUE;

        /**
         * Enables / Disables URL safe data encoding
         *
         * @var bool
         */
        public $url_safe = TRUE;

        /**
         * Enables / Disbles decrypt after encrypt with compare - useful in testing!
         *
         * @var bool
         */
        public $test_decrypt_before_return = FALSE;

        /**
         * The mcrypt setup
         * 
         * @var array
         */
        public $mcrypt;

        /**
         * __construct()
         * 
         * @param string $cipher
         * @param string $key
         * @param string $mode
         * @param string $iv 
         */
        function __construct($cipher,$key,$mode,$iv) {

                $this->mcrypt['cipher'] = $cipher;
                $this->mcrypt['key'] = $key;
                $this->mcrypt['mode'] = $mode;
                $this->mcrypt['iv'] = $iv;

        }

        /**
         * encrypt()
         *
         * @param mixed $data
         * @return string
         */
        public function encrypt($data) {
                
                // Check mcrypt config looks complete -- we test here because a
                // user could change $this->mcrypt between calls
                $this->__checkMcryptConfig();
                
                // Return early if $data is empty
                if(empty($data)) { return $data; }

                // Make sure $data is cast as a JSON string if it is not an array
                if(is_string($data)) {
                        $encrypt_data = $data;
                } else {
                        $encrypt_data = json_encode($data);
                }

                // Compress if required
                if($this->compression) {
                        $encrypt_data = gzcompress($encrypt_data);
                }

                // Encrypt and base64 the data string
                $encrypted = base64_encode(mcrypt_encrypt(
                        $this->mcrypt['cipher'],
                        $this->mcrypt['key'],
                        $encrypt_data,
                        $this->mcrypt['mode'],
                        $this->mcrypt['iv']
                ));

                // Tweak the string to be url safe if required
                if($this->url_safe) {
                        $encrypted = strtr($encrypted,'+/=','-_,');
                }

                // Decrypt test if we need to
                if($this->test_decrypt_before_return) {

                        if($data != $this->decrypt($encrypted)) {

                                // Because it is possible for a JSON string itself to be passed such cases
                                if(json_decode($data,TRUE) != $this->decrypt($encrypted)) {
                                        throw new Exception('Unable to confirm encrypted data will match decrypted data!');
                                } else {
                                        return $encrypted;
                                }

                        } else {
                                return $encrypted;
                        }
                } else {
                        return $encrypted;
                }
        }

        /**
         * decrypt()
         *
         * @param string $data
         * @return mixed
         */
        public function decrypt($data) {

                // Check mcrypt config looks complete -- we test here because a
                // user could change $this->mcrypt between calls
                $this->__checkMcryptConfig();

                // Return early if $data is empty
                if(empty($data)) { return $data; }

                // Undo the url safe transform
                if($this->url_safe) {
                        $data = strtr($data,'-_,','+/=');
                }

                // base64 encode and encryption
                $data = mcrypt_decrypt(
                        $this->mcrypt['cipher'],
                        $this->mcrypt['key'],
                        base64_decode($data),
                        $this->mcrypt['mode'],
                        $this->mcrypt['iv']
                );

                // Uncompress if required - supress errors due to bad input data
                if($this->compression) {
                        $data = @gzuncompress($data);
                }

                // Attempt to JSON decode
                $json = json_decode($data,TRUE);
                if(is_array($json)) {
                        return $json;
                } else {
                        return $data;
                }
        }

        /**
         * __checkMcryptConfig
         *
         * @param array $mcrypt
         */
        private function __checkMcryptConfig() {

                // Make sure all the $mcrypt components are present
                if(!isset($this->mcrypt['cipher']) || empty($this->mcrypt['cipher'])) {
                        throw new Exception('Missing mcrypt cipher');
                }

                if(!isset($this->mcrypt['key']) || empty($this->mcrypt['key'])) {
                        throw new Exception('Missing mcrypt key');
                }

                if(!isset($this->mcrypt['mode']) || empty($this->mcrypt['mode'])) {
                        throw new Exception('Missing mcrypt mode');
                }

                if(!isset($this->mcrypt['iv'])) { // is optional, thus can be empty
                        throw new Exception('Missing mcrypt iv');
                }

        }

}

Wow! Thanks for this, it

Wow! Thanks for this, it works for me.

I just added the decrypt function in the helper.


function decrypt($data) {
  return $this->Crypt->decrypt($data);
}

You have a nice logo ;)

Hi PA_Rochat, Thanks for the

Hi PA_Rochat,

Thanks for the note.

Worth pointing out, there is some method-to-the-madness in not providing decrypt() in the view helper even though it is quite trivial to implement.

Have a think about why you are decrypting in the view layer of the MVC and decide if it really makes sense - the handful of times I have thought I needed decrypt() in the view I've universally discovered I should be doing it back down in the controller or model. Further, I get anxious about exposing a function public decrypt() in a potentially easy to call location... though I tend to be paranoid about this kind of stuff.

Enjoy,
N

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.