Encryption/Decryption of Form Fields in CakePHP 3

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

Leave a Comment