UPDATE: I’ve posted an improved version of this behavior here
I’ve written the following behavior for a project I recently completed in Cake, and I thought it would be worth sharing:
class CryptableBehavior extends ModelBehavior { var $settings = array(); 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); } 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 !== '') { return base64_encode(mcrypt_encrypt(Configure::read('Cryptable.cipher'), Configure::read('Cryptable.key'), $data, 'cbc', Configure::read('Cryptable.iv'))); } else { return ''; } } public function decrypt($data, $data2 = null) { if (is_object($data)) { unset($data); $data = $data2; } if ($data != '') { return trim(mcrypt_decrypt(Configure::read('Cryptable.cipher'), Configure::read('Cryptable.key'), base64_decode($data), 'cbc', Configure::read('Cryptable.iv'))); } else { return ''; } } }
All you need to do is add three lines to your bootstrap, and then load the behavior in any model you want to use it.
Here are the lines for your bootstrap:
Configure::write('Cryptable.cipher', 'rijndael-192'); Configure::write('Cryptable.key','random key string here'); Configure::write('Cryptable.iv', base64_decode('base64 encoded IV here')); // Create with mcrypt_create_iv with the appropriate size for your cipher
Here’s an example of how to load it in your model:
var $actsAs = array( 'Cryptable' => array( 'fields' => array( 'password' ) ) );
If you need to encrypt or decrypt a field outside of the normal find methods, you can simply call those methods on the model, passing in the string that needs worked on.
Download Cyptable Behavior
I would suppose you could just use config/core.php salt for this too?
For the encryption key itself, yes. But you still need to set the IV and the encryption scheme to use.
What about the Security::cipher method? Wouldn’t be easier to use that instead?
It would (arguably) be easier, but it’s not as flexible.
This allows you to choose the exact cipher system you want to use, including the IV and the passphrase, and does it all automatically on find and save calls, so you rarely ever have to call the encrypt and decrypt methods manually. It also avoids the extra layer of objects that you would add by using the Security class. Not a major savings, but it adds up.
Also, I just went and looked at the source code for Security::cipher. I’m not confident that it is cryptographically secure. It’s probably secure against a casual attack, but it doesn’t look very robust.
Great behaviour, a part from being useful, it’s very nice to see people that know how to use cake!
Could you elaborate a bit more on creating the IV please? I bet it’s dead obvious, but i’m not sure how to create that.
Thanks
Will
This is the basic idea for creating the IV.
This was EXACTLY what I needed, thank you SO much! You rock!!
I do not have sufficient knowledge about cryptography but a simple need to encode some data. I successfully used above behavior but am not sure which MCRYPT_MODE_mode I should use. I used these:
mcrypt_get_iv_size(MCRYPT_RIJNDAEL_192, MCRYPT_MODE_NOFB)
Can you advice please?
I use CBC for mine, but if you have your own preference, there’s nothing wrong with using it, as long as it works. This behavior is just supposed to automate the handling of it for you.
Decoding does not work when I call ModelWithEncryptedFields in containable behavior, i.e.:
$TopModel = $this->TopModel->find('all',array(
'contain'=>array(
'RelatedModel' => array('SubModelWithEncryptedFields'),
)
));
Above produces fields that are not decrypted automatically. Works fine outside Containable behavior.
Have you got any idea how to overcome this but still use Containable behavior in find?
Unfortunately, this isn’t solvable in any automatic way. The root of the problem is that cake doesn’t run callbacks when retrieving data for associated models. The easy solution is to just call $this->Model->decrypt() on the field you need to use.
Can I do it somehow in the view while displaying the value or shall I re-process find in the controller using foreach on the find result and applying decrypt function to fields in question?
Thanks for previous answers. They have been helpful.
In the controller, after retrieving the data, do something like this:
$data['Mode']['password'] = $this->Model->decrypt($data['Model']['password']);
You can do it in a loop if you have multiple results.
I was hoping I could avoid reprocessing the find since I already go through all the records displaying them and I need to just show one encrypted field at that view. Anyway, its a valid solution and thanks again.
You could technically do it in the view like this:
echo ClassRegistry::init(‘ModelClass’)->decrypt($data['Model']['field']);
Works perfectly. Although this is my preference I am not sure about consequence of doing it.
Is calling ClassRegistry::init() in a loop (up to 100 records) resulting in a bigger performance issue then just re-doing the loop after find in the controller with $this->TopModel->RelatedModel->EncryptedModel->decrypt() ?
I’ve done some some research on ClassRegistry::init(). Are you able to shed light on the following:
I’ve read a comment that subsequent ClassRegistry::init() will not load a new model but work on the same instance. Yet, at another comment I’ve read that following will re-use the instance of the same object:
// before the loop
$LocalModel = ClassRegistry::init(‘Profile’);
// within the loop:
echo $LocalProfile->decrypt($Array['EncryptedField'])
I’d have to go check, but I’m pretty sure ClassRegistry uses the object cache, so it shouldn’t create a whole new instance each time. That said, it would still be more efficient to store the instance locally and use it directly than go through the ClassRegistry every time, regardless.
Both solutions above I implemented and they work but I see no noticeable impact on performance.
Yeah. Unless you’re dealing with a significant (10,000+) number of rows, it’s not going to make a noticeable difference. But still better to go the most efficient route.
Thank you. I’ll stay with the $LocalProfile->decrypt($Array['EncryptedField']) solution. Doing more research into this would not be effective time management, asking you more would be negatively impacting your time management
. Great help!
Hi. Tnx for sharing. I was just in the middle of writing very similiar behavior. Thank you for saving me precious minutes
No problem. If you want slightly improved security (marginal, but an improvement), make sure to use the version in the post I link to at the top of this one now.