ソースを参照

add encryption behavior

Kevin Pfeifer 1 年間 前
コミット
2091f1198f

+ 111 - 0
src/Model/Behavior/EncryptionBehavior.php

@@ -0,0 +1,111 @@
+<?php
+/**
+ * @author Mark Scherer
+ * @license http://opensource.org/licenses/mit-license.php MIT
+ */
+
+namespace Tools\Model\Behavior;
+
+use ArrayObject;
+use Cake\Collection\CollectionInterface;
+use Cake\Datasource\EntityInterface;
+use Cake\Event\EventInterface;
+use Cake\ORM\Behavior;
+use Cake\ORM\Query\SelectQuery;
+use Cake\Utility\Security;
+
+/**
+ * Allows entity fields to be automatically encrypted when saving/updating and
+ * decrypted when fetching the data
+ */
+class EncryptionBehavior extends Behavior {
+
+	/**
+	 * Default configuration.
+	 *
+	 * @var array<string, mixed>
+	 */
+	protected array $_defaultConfig = [
+		'fields' => [],
+		'key' => '',
+	];
+
+	/**
+	 * @param array $config The config passed to the behavior
+	 * @return void
+	 */
+	public function initialize(array $config): void {
+		if (isset($config['fields'])) {
+			$this->setConfig('fields', $config['fields']);
+			$this->_config['fields'] = array_unique($this->_config['fields']);
+		}
+	}
+
+	/**
+	 * Events this listener is interested in.
+	 *
+	 * @return array<string, mixed>
+	 */
+	public function implementedEvents(): array {
+		return [
+			// Trigger this after app models beforeSave hook
+			'Model.beforeSave' => [
+				'callable' => 'beforeSave',
+				'priority' => 100,
+			],
+			'Model.beforeFind' => 'beforeFind',
+		];
+	}
+
+	/**
+	 * Encrypting the fields
+	 *
+	 * @param \Cake\Event\EventInterface $event The event
+	 * @param \Cake\Datasource\EntityInterface $entity The associated entity
+	 * @param \ArrayObject $options Options passed to the event
+	 * @return void
+	 */
+	public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void {
+		$fields = $this->getConfig('fields');
+		$key = $this->getConfig('key');
+		foreach ($fields as $fieldName) :
+			if ($entity->has($fieldName)) :
+				$content = $entity->get($fieldName);
+				if (!empty($content)) :
+					$entity->set($fieldName, Security::encrypt($content, $key));
+				endif;
+			endif;
+		endforeach;
+	}
+
+	/**
+	 * Decrypting the fields
+	 *
+	 * @param \Cake\Event\EventInterface $event The event
+	 * @param \Cake\ORM\Query\SelectQuery $query The query to adjust
+	 * @param \ArrayObject $options The options passed to the event
+	 * @param bool $primary Whether the query is the root query or an associated query
+	 * @return void
+	 */
+	public function beforeFind(EventInterface $event, SelectQuery $query, ArrayObject $options, bool $primary): void {
+		$query->formatResults(function (CollectionInterface $results) {
+			return $results->map(function ($row) {
+				$fields = $this->getConfig('fields');
+				$key = $this->getConfig('key');
+				foreach ($fields as $fieldName) :
+					if (isset($row->$fieldName) && is_resource($row->$fieldName)) :
+						$content = stream_get_contents($row->$fieldName);
+						if (!empty($content)) {
+							$row[$fieldName] = Security::decrypt($content, $key);
+						} else {
+							$row[$fieldName] = '';
+						}
+					endif;
+				endforeach;
+
+				return $row;
+			});
+		});
+	}
+
+}

+ 69 - 0
tests/TestCase/Model/Behavior/EncryptionBehaviorTest.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace Tools\Test\TestCase\Model\Behavior;
+
+use Cake\Datasource\ConnectionManager;
+use Shim\TestSuite\TestCase;
+
+class EncryptionBehaviorTest extends TestCase {
+
+	/**
+	 * @var array
+	 */
+	protected array $fixtures = [
+		'plugin.Tools.Sessions',
+	];
+
+	/**
+	 * @var \Tools\Model\Table\Table|\Tools\Model\Behavior\EncryptionBehavior
+	 */
+	protected $table;
+
+	/**
+	 * @return void
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->table = $this->getTableLocator()->get('Sessions');
+		$this->table->addBehavior('Tools.Encryption', [
+			'fields' => ['data'],
+			'key' => 'some-very-long-key-which-needs-to-be-at-least-32-chars-long',
+		]);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testSaveBasic() {
+		$data = [
+			'id' => 10,
+			'data' => 'test save',
+		];
+
+		$entity = $this->table->newEntity($data);
+		$entityAfter = $this->table->save($entity);
+		$this->assertTrue((bool)$entityAfter);
+
+		$connection = ConnectionManager::get('default');
+		$lastInsertedId = $connection->getDriver()->lastInsertId();
+		$result = $connection->getDriver()->execute('SELECT data FROM sessions WHERE id = :id', ['id' => $lastInsertedId])->fetchAll();
+		$this->assertNotEquals($data['data'], $result[0][0]);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testFindBasic() {
+		$data = [
+			'id' => 10,
+			'data' => 'test save',
+		];
+		$entity = $this->table->newEntity($data);
+		$this->table->save($entity);
+
+		$entity = $this->table->get(10);
+		$this->assertEquals('test save', $entity->data);
+	}
+
+}