Monthly Archives: July 2011

CakePHP – Updated Cryptable Behavior

Almost 2 years ago, I created the Cryptable behavior, documented here. It worked well, and served it’s purpose, but I missed a feature that I soon wished I’d added. So, today, I was working on a new project, and I decided it was time to fix the behavior to add the missing feature.

The old version used the same Initialization Vector for every string it encrypted. This isn’t HORRIBLE, but it does somewhat reduce the security of the encrypted strings. The ‘best practice’ is to generate a unique IV every time you encrypt something, and just store it with the encrypted string to use when you need to decrypt it later. It’s considered safe to store them together, and even transmit them together in the clear without further encryption.

So, without further ado, here’s the updated code for the behavior:

<?php
class CryptableBehavior extends ModelBehavior {
	public $settings = array();
 
	private $iv_size;
	private $base64_iv_size;
 
	function setup(&$model, $settings) {
		if (!isset($this->settings[$model->alias])) {
			$this->settings[$model->alias] = array(
				'fields' => array()
			);
		}
 
		$this->settings[$model->alias] = array_merge($this->settings[$model->alias], $settings);
		$this->iv_size = mcrypt_get_iv_size(Configure::read('Cryptable.cipher'), 'cbc');
		$this->base64_iv_size = strlen(base64_encode(mcrypt_create_iv($this->iv_size, MCRYPT_DEV_URANDOM)));
	}
 
	function beforeFind(&$model, $queryData) {
		foreach ($this->settings[$model->alias]['fields'] AS $field) {
			if (isset($queryData['conditions'][$model->alias.'.'.$field])) {
				$queryData['conditions'][$model->alias.'.'.$field] = $this->encrypt($queryData['conditions'][$model->alias.'.'.$field]);
			}
		}
		return $queryData;
	}
 
	function afterFind(&$model, $results, $primary) {
		foreach ($this->settings[$model->alias]['fields'] AS $field) {
			if ($primary) {
				foreach ($results AS $key => $value) {
					if (isset($value[$model->alias][$field])) {
						$results[$key][$model->alias][$field] = $this->decrypt($value[$model->alias][$field]);
					}
				}
			} else {
				if (isset($results[$field])) {
					$results[$field] = $this->decrypt($results[$field]);
				}
			}
		}
 
		return $results;
	}
 
	function beforeSave(&$model) {
		foreach ($this->settings[$model->alias]['fields'] AS $field) {
			if (isset($model->data[$model->alias][$field])) {
				$model->data[$model->alias]['cleartext_'.$field] = $model->data[$model->alias][$field];
				$model->data[$model->alias][$field] = $this->encrypt($model->data[$model->alias][$field]);
			}
		}
 
		return true;
	}
 
	public function encrypt($data) {
		if ($data !== '') {
			$iv = mcrypt_create_iv($this->iv_size, MCRYPT_DEV_URANDOM);
 
			return base64_encode($iv).base64_encode(mcrypt_encrypt(Configure::read('Cryptable.cipher'), Configure::read('Cryptable.key'), $data, 'cbc', $iv));
		} else {
			return '';
		}
	}
 
	public function decrypt($data, $data2 = null) {
		if (is_object($data)) {
			unset($data);
			$data = $data2;
		}
 
		if ($data != '') {
			$iv = base64_decode(substr($data, 0, $this->base64_iv_size));
			$data = base64_decode(substr($data, $this->base64_iv_size));
 
			return trim(mcrypt_decrypt(Configure::read('Cryptable.cipher'), Configure::read('Cryptable.key'), $data, 'cbc', $iv));
		} else {
			return '';
		}
	}
}

For those using the old behavior, it is possible to upgrade, but you need to do a little bit of work. This amounts to running queries to inject your current IV onto the start of every encrypted field in the database. (PLEASE be careful doing this, and make sure to keep backups.) Also note that you may need to provide more room in the fields, because the data will be somewhat longer now. You can find out how many characters long it will be by running this command in PHP:

echo strlen(base64_encode(mcrypt_create_iv(mcrypt_get_iv_size('CIPHER_HERE', 'cbc'), MCRYPT_DEV_URANDOM)))

The next time the encrypted records are saved, the old default IV will be replaced with a new random one. In fact, any time a record is re-encrypted, it will get a new IV, since I don’t bother with storing the old one when the value is decrypted.

You may also remove the Configure line that holds your old IV, once you’ve injected it into the database on all the records, and tested that the new Cryptable behavior is working as intended.

Please see the original post for any other questions, or feel free to ask in the comments and I’ll do my best to help. 🙂