Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Encryption/Decryption of Form Fields in CakePHP 3

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.

like image 740
DanielHolguin Avatar asked Dec 06 '22 20:12

DanielHolguin


1 Answers

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).

A custom database type

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

beforeSave and result formatters

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

  • Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
  • Cookbook > Database Access & ORM > Query Builder > Adding Calculated Fields
  • Cookbook > Tutorials & Examples > Bookmarker Tutorial Part 2 > Persisting the Tag String
  • Cookbook > Database Access & ORM > Behaviors
  • API > \Cake\Datasource\EntityTrait
  • API > \Cake\ORM\Table
like image 105
ndm Avatar answered Dec 18 '22 03:12

ndm