I want to have some form-fields encrypted when they are added/edited and decrypted when they are looked up by cake. Here is the code that works for me in v2.7.2:
core.php
Configure::write('Security.key','secretkey');
app/model/patient.php.
public $encryptedFields = array('patient_surname', 'patient_first_name');
public function beforeSave($options = array()) {
foreach($this->encryptedFields as $fieldName){
if(!empty($this->data[$this->alias][$fieldName])){
$this->data[$this->alias][$fieldName] = Security::encrypt(
$this->data[$this->alias][$fieldName],
Configure::read('Security.key')
);
}
}
return true;
}
public function afterFind($results, $primary = false) {
foreach ($results as $key => $val) {
foreach($this->encryptedFields as $fieldName) {
if (@is_array($results[$key][$this->alias])) {
$results[$key][$this->alias][$fieldName] = Security::decrypt(
$results[$key][$this->alias][$fieldName],
Configure::read('Security.key')
);
}
}
}
return $results;
}
As I understand it I have to replace $this->data[] with the generated entities for the model and the afterFind method with virtual fields, but I just can't put it all together.
There's more than one way to solve this (please note that the following code is untested example code! You should get a grasp on the new basics first before using any of this).
One would be a custom database type, which would encrypt when binding the values to the database statement, and decrypt when results are being fetched. That's the option that I would prefer.
Here's simple example, assuming the db columns can hold binary data.
src/Database/Type/CryptedType.php
This should be rather self explantory, encrypt when casting to database, decrypt when casting to PHP.
<?php
namespace App\Database\Type;
use Cake\Database\Driver;
use Cake\Database\Type;
use Cake\Utility\Security;
class CryptedType extends Type
{
public function toDatabase($value, Driver $driver)
{
return Security::encrypt($value, Security::getSalt());
}
public function toPHP($value, Driver $driver)
{
if ($value === null) {
return null;
}
return Security::decrypt($value, Security::getSalt());
}
}
src/config/bootstrap.php
Register the custom type.
use Cake\Database\Type;
Type::map('crypted', 'App\Database\Type\CryptedType');
src/Model/Table/PatientsTable.php
Finally map the cryptable columns to the registered type, and that's it, from now on everything's being handled automatically.
// ...
use Cake\Database\Schema\Table as Schema;
class PatientsTable extends Table
{
// ...
protected function _initializeSchema(Schema $table)
{
$table->setColumnType('patient_surname', 'crypted');
$table->setColumnType('patient_first_name', 'crypted');
return $table;
}
// ...
}
See Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
A less dry and tighter coupled approach, and basically a port of your 2.x code, would be to use the beforeSave
callback/event, and a result formatter. The result formatter could for example be attached in the beforeFind
event/callback.
In beforeSave
just set/get the values to/from the passed entity instance, you can utilize Entity::has()
, Entity::get()
and Entity::set()
, or even use array access since entities implement ArrayAccess
.
The result formatter is basically an after find hook, and you can use it to easily iterate over results, and modify them.
Here's a basic example, which shouldn't need much further explanation:
// ...
use Cake\Event\Event;
use Cake\ORM\Query;
class PatientsTable extends Table
{
// ...
public $encryptedFields = [
'patient_surname',
'patient_first_name'
];
public function beforeSave(Event $event, Entity $entity, \ArrayObject $options)
{
foreach($this->encryptedFields as $fieldName) {
if($entity->has($fieldName)) {
$entity->set(
$fieldName,
Security::encrypt($entity->get($fieldName), Security::getSalt())
);
}
}
return true;
}
public function beforeFind(Event $event, Query $query, \ArrayObject $options, boolean $primary)
{
$query->formatResults(
function ($results) {
/* @var $results \Cake\Datasource\ResultSetInterface|\Cake\Collection\CollectionInterface */
return $results->map(function ($row) {
/* @var $row array|\Cake\DataSource\EntityInterface */
foreach($this->encryptedFields as $fieldName) {
if(isset($row[$fieldName])) {
$row[$fieldName] = Security::decrypt($row[$fieldName], Security::getSalt());
}
}
return $row;
});
}
);
}
// ...
}
To decouple this a little, you could also move this into a behavior so that you can easily share it across multiple models.
See also
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With