Encryption/Decryption of Form Fields in CakePHP 3

前端 未结 2 1715
臣服心动
臣服心动 2020-11-29 10:36

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:

<         


        
相关标签:
2条回答
  • 2020-11-29 11:22

    Edit: @npm was right about the virtual properties not working. Now i'm angry at myself for giving a bad answer. serves me right for not checking it before I posted.

    To make it right, I've implemented a version using behaviors to decrypt the fields as they are read, and encrypt them as they are written to the database.

    Note: This code does not currently incorporate any custom finders, so it will not support searching by the encrypted field.

    eg.

    $this->Patient->findByPatientFirstname('bob'); // this will not work
    

    Behavior

    /src/Model/Behavior/EncryptBehavior.php

    <?php
    /**
     * 
     */
    namespace Cake\ORM\Behavior;
    
    use ArrayObject;
    use Cake\Collection\Collection;
    use Cake\Datasource\EntityInterface;
    use Cake\Datasource\ResultSetInterface;
    use Cake\Event\Event;
    use Cake\ORM\Behavior;
    use Cake\ORM\Entity;
    use Cake\ORM\Query;
    use Cake\ORM\Table;
    use Cake\ORM\TableRegistry;
    use Cake\Utility\Inflector;
    use Cake\Utility\Security;
    use Cake\Log\Log;
    
    /**
     * Encrypt Behavior
     */
    class EncryptBehavior extends Behavior
    {
        /**
         * Default config
         *
         * These are merged with user-provided configuration when the behavior is used.
         *
         * @var array
         */
        protected $_defaultConfig = [
            'key' => 'YOUR_KEY_KERE', /* set them in the EntityTable, not here */
            'fields' => []
        ];
    
    
        /**
         * Before save listener.
         * Transparently manages setting the lft and rght fields if the parent field is
         * included in the parameters to be saved.
         *
         * @param \Cake\Event\Event $event The beforeSave event that was fired
         * @param \Cake\ORM\Entity $entity the entity that is going to be saved
         * @return void
         * @throws \RuntimeException if the parent to set for the node is invalid
         */
        public function beforeSave(Event $event, Entity $entity)
        {
    
            $isNew = $entity->isNew();
            $config = $this->config();
    
    
            $values = $entity->extract($config['fields'], true);
            $fields = array_keys($values);
            $securityKey = $config['key'];
    
            foreach($fields as $field){ 
                if( isset($values[$field]) && !empty($values[$field]) ){
                    $entity->set($field, Security::encrypt($values[$field], $securityKey));
                }
            }
        }
    
        /**
         * Callback method that listens to the `beforeFind` event in the bound
         * table. It modifies the passed query
         *
         * @param \Cake\Event\Event $event The beforeFind event that was fired.
         * @param \Cake\ORM\Query $query Query
         * @param \ArrayObject $options The options for the query
         * @return void
         */
        public function beforeFind(Event $event, Query $query, $options)
        {
            $query->formatResults(function ($results){
                return $this->_rowMapper($results);
            }, $query::PREPEND);
        }
    
        /**
         * Modifies the results from a table find in order to merge the decrypted fields
         * into the results.
         *
         * @param \Cake\Datasource\ResultSetInterface $results Results to map.
         * @return \Cake\Collection\Collection
         */
        protected function _rowMapper($results)
        {
            return $results->map(function ($row) {
                if ($row === null) {
                    return $row;
                }
                $hydrated = !is_array($row);
    
                $fields = $this->_config['fields'];
                $key = $this->_config['key'];
                foreach ($fields as $field) {
                    $row[$field] = Security::decrypt($row[$field], $key);
                }
    
                if ($hydrated) {
                    $row->clean();
                }
    
                return $row;
            });
        }
    }
    

    Table

    /src/Model/Table/PatientsTable.php

    <?php
    namespace App\Model\Table;
    
    use App\Model\Entity\Patient;
    use Cake\ORM\Query;
    use Cake\ORM\RulesChecker;
    use Cake\ORM\Table;
    use Cake\Validation\Validator;
    use Cake\Core\Configure;
    
    /**
     * Patients Model
     *
     */
    class PatientsTable extends Table
    {
    
        /**
         * Initialize method
         *
         * @param array $config The configuration for the Table.
         * @return void
         */
        public function initialize(array $config)
        {
            parent::initialize($config);
    
            $this->table('patients');
            $this->displayField('id');
            $this->primaryKey('id');
    
            // will encrypt these fields automatically
            $this->addBehavior('Encrypt',[
                'key' => Configure::read('Security.key'),
                'fields' => [
                    'patient_surname',
                    'patient_firstname'
                ]
            ]);
    
        }
    }
    

    I feel your pain. the ORM layer in cakephp 3 is radically different from cake2. They split the entity model and the table ORM into two different classes, and afterFind has been removed. I would take a look at using virtual properties. I think it might be suitable for your use case.

    Example below.

    <?php
    
    namespace App\Model\Entity;
    
    use Cake\ORM\Entity;
    use Cake\Utility\Security;
    use Cake\Core\Configure;
    
    class Patient extends Entity
    {
    
        protected function _setPatientSurname($str)
        {
            $this->set('patient_surname', Security::encrypt($str, Configure::read('Security.key'));
        }
    
        protected function _setPatientFirstname($str)
        {
            $this->set('patient_firstname', Security::encrypt($str, Configure::read('Security.key'));
        }
    
        protected function _getPatientSurname()
        {
            return Security::decrypt($this->patient_surname, Configure::read('Security.key'));
        }
    
        protected function _getPatientFirstname()
        {
            return Security::decrypt($this->patient_first_name, Configure::read('Security.key'));
        }
    
    }
    

    0 讨论(0)
  • 2020-11-29 11:23

    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::salt());
        }
    
        public function toPHP($value, Driver $driver)
        {
            if ($value === null) {
                return null;
            }
            return Security::decrypt($value, Security::salt());
        }
    }
    

    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->columnType('patient_surname', 'crypted');
            $table->columnType('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::salt())
                    );
                }
            }
            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::salt());
                            }
                        }
    
                        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
    0 讨论(0)
提交回复
热议问题