diff --git a/.gitignore b/.gitignore index 5f58300..d2ab08e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ .test-output /composer.lock *.log +public/ diff --git a/README.md b/README.md index 10f1124..6a7cf53 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This module allows Silverstripe CMS ORM data to be encrypted before being stored ## Requirements -* SilverStripe CMS 5.0 +* SilverStripe CMS 6.0 ## Installation Install via Composer: @@ -32,7 +32,7 @@ For development environments you can set this in your `.env` e.g: ENCRYPT_AT_REST_KEY="{generated defuse key}" ``` -For more information view SilverStripe [Environment Management](https://docs.silverstripe.org/en/4/getting_started/environment_management/). +For more information view SilverStripe [Environment Management](https://docs.silverstripe.org/en/6/getting_started/environment_management/). ## Usage diff --git a/composer.json b/composer.json index e7c2a82..7ed19da 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,12 @@ "source": "https://github.com/madmatt/silverstripe-encrypt-at-rest" }, "require": { - "php": "^8.0", - "silverstripe/framework": "^5", + "php": "^8.3", + "silverstripe/framework": "^6", "defuse/php-encryption": "^2.2" }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^11.3" }, "autoload": { "psr-4": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index c450414..79bd924 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + src/ diff --git a/src/AtRestCryptoService.php b/src/AtRestCryptoService.php index 1b00736..8fbeb58 100644 --- a/src/AtRestCryptoService.php +++ b/src/AtRestCryptoService.php @@ -47,7 +47,7 @@ public function encrypt($raw, $key = null) public function decrypt($ciphertext, $key = null) { $key = $this->getKey($key); - return Crypto::Decrypt($ciphertext, $key); + return trim(Crypto::Decrypt($ciphertext, $key)); } /** diff --git a/src/Extension/DecryptDataObjectFieldsExtension.php b/src/Extension/DecryptDataObjectFieldsExtension.php index 882ea01..0ef0827 100644 --- a/src/Extension/DecryptDataObjectFieldsExtension.php +++ b/src/Extension/DecryptDataObjectFieldsExtension.php @@ -2,7 +2,7 @@ namespace Madmatt\EncryptAtRest\Extension; -use SilverStripe\ORM\DataExtension; +use SilverStripe\Core\Extension; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Injector\Injector; use SilverStripe\ORM\DataObject; @@ -18,7 +18,7 @@ * @package EncryptAtRest\Extension * @property DecryptDataObjectFieldsExtension|DataObject $owner */ -class DecryptDataObjectFieldsExtension extends DataExtension +class DecryptDataObjectFieldsExtension extends Extension { /** * During hydration of an existing DataObject retrieved from the database, this extension method will be called. We diff --git a/src/FieldType/EncryptedDatetime.php b/src/FieldType/EncryptedDatetime.php index 681d08f..0cb15d8 100644 --- a/src/FieldType/EncryptedDatetime.php +++ b/src/FieldType/EncryptedDatetime.php @@ -5,6 +5,7 @@ use Exception; use Madmatt\EncryptAtRest\Traits\EncryptedFieldGetValueTrait; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Model\ModelData; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBDatetime; use Madmatt\EncryptAtRest\AtRestCryptoService; @@ -29,10 +30,11 @@ class EncryptedDatetime extends DBDatetime public function __construct($name = null, $options = []) { parent::__construct($name, $options); + $this->service = Injector::inst()->get(AtRestCryptoService::class); } - public function setValue($value, $record = null, $markChanged = true) + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static { if (is_array($record) && array_key_exists($this->name, $record) && $value === null) { $this->value = $record[$this->name]; @@ -42,9 +44,11 @@ public function setValue($value, $record = null, $markChanged = true) } else { $this->value = $value; } + + return $this; } - public function getDecryptedValue(string $value = '') + public function getDecryptedValue(?string $value = null) { // Test if we're actually an encrypted value; if (ctype_xdigit($value) && strlen($value) > 130) { @@ -55,10 +59,12 @@ public function getDecryptedValue(string $value = '') return $value; } } - return $value; + + // If the decrypted value is empty, return null, so that the validate is skipped + return empty($value) ? null : $value; } - public function requireField() + public function requireField(): void { $values = array( 'type' => 'text', @@ -72,7 +78,7 @@ public function requireField() DB::require_field($this->tableName, $this->name, $values); } - public function prepValueForDB($value) + public function prepValueForDB(mixed $value): mixed { $value = parent::prepValueForDB($value); $ciphertext = $this->service->encrypt($value); diff --git a/src/FieldType/EncryptedDecimal.php b/src/FieldType/EncryptedDecimal.php index 26ff244..5b976a4 100644 --- a/src/FieldType/EncryptedDecimal.php +++ b/src/FieldType/EncryptedDecimal.php @@ -5,6 +5,7 @@ use Exception; use Madmatt\EncryptAtRest\Traits\EncryptedFieldGetValueTrait; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Model\ModelData; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBDecimal; use Madmatt\EncryptAtRest\AtRestCryptoService; @@ -31,7 +32,7 @@ public function __construct($name = null, $wholeSize = 9, $decimalSize = 2, $def $this->service = Injector::inst()->get(AtRestCryptoService::class); } - public function setValue($value, $record = null, $markChanged = true) + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static { if (is_array($record) && array_key_exists($this->name, $record) && $value === null) { $this->value = $record[$this->name]; @@ -41,6 +42,8 @@ public function setValue($value, $record = null, $markChanged = true) } else { $this->value = $value; } + + return $this; } public function getDecryptedValue(string $value = '') @@ -57,7 +60,7 @@ public function getDecryptedValue(string $value = '') return (float)$value; } - public function requireField() + public function requireField(): void { $values = array( 'type' => 'text', @@ -71,7 +74,7 @@ public function requireField() DB::require_field($this->tableName, $this->name, $values); } - public function prepValueForDB($value) + public function prepEncryptedValueForDB(mixed $value): string { $value = parent::prepValueForDB($value); $ciphertext = $this->service->encrypt($value); diff --git a/src/FieldType/EncryptedEnum.php b/src/FieldType/EncryptedEnum.php index 9960b03..6d18e9a 100644 --- a/src/FieldType/EncryptedEnum.php +++ b/src/FieldType/EncryptedEnum.php @@ -5,9 +5,10 @@ use Exception; use Madmatt\EncryptAtRest\Traits\EncryptedFieldGetValueTrait; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Core\Validation\FieldValidation\OptionFieldValidator; +use SilverStripe\Model\ModelData; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBEnum; -use SilverStripe\ORM\ArrayLib; use Madmatt\EncryptAtRest\AtRestCryptoService; /** @@ -26,13 +27,21 @@ class EncryptedEnum extends DBEnum */ protected $service; + /** + * Disable validation added in CMS6 but todo in future release + */ + private static array $field_validators = [ + OptionFieldValidator::class => null, + ]; + public function __construct($name = null, $enum = null, $default = 0, $options = []) { parent::__construct($name, $enum, $default, $options); + $this->service = Injector::inst()->get(AtRestCryptoService::class); } - public function setValue($value, $record = null, $markChanged = true) + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static { if (is_array($record) && array_key_exists($this->name, $record) && $value === null) { $this->value = $record[$this->name]; @@ -42,6 +51,8 @@ public function setValue($value, $record = null, $markChanged = true) } else { $this->value = $value; } + + return $this; } public function getDecryptedValue(string $value = '') @@ -58,7 +69,7 @@ public function getDecryptedValue(string $value = '') return $value; } - public function requireField() + public function requireField(): void { $values = array( 'type' => 'text', @@ -72,26 +83,11 @@ public function requireField() DB::require_field($this->tableName, $this->name, $values); } - public function prepValueForDB($value) + public function prepValueForDB(mixed $value): array|string|null { $value = parent::prepValueForDB($value); $ciphertext = $this->service->encrypt($value); $this->value = $ciphertext; return $ciphertext; } - - /** - * Returns the values of this enum as an array, suitable for insertion into - * a {@link DropdownField} - * - * @param boolean - * - * @return array - */ - public function enumValues($hasEmpty = true) { - $this->enum = array(); - return ($hasEmpty) - ? array_merge(array('' => ''), ArrayLib::valuekey($this->enum)) - : ArrayLib::valuekey($this->enum); - } } diff --git a/src/FieldType/EncryptedInt.php b/src/FieldType/EncryptedInt.php index 300e41d..a25f72c 100644 --- a/src/FieldType/EncryptedInt.php +++ b/src/FieldType/EncryptedInt.php @@ -5,6 +5,7 @@ use Exception; use Madmatt\EncryptAtRest\Traits\EncryptedFieldGetValueTrait; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Model\ModelData; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBInt; use Madmatt\EncryptAtRest\AtRestCryptoService; @@ -31,7 +32,7 @@ public function __construct($name = null, $defaultVal = 0) $this->service = Injector::inst()->get(AtRestCryptoService::class); } - public function setValue($value, $record = null, $markChanged = true) + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static { if (is_array($record) && array_key_exists($this->name, $record) && $value === null) { $this->value = $record[$this->name]; @@ -41,6 +42,8 @@ public function setValue($value, $record = null, $markChanged = true) } else { $this->value = $value; } + + return $this; } public function getDecryptedValue(string $value = '') @@ -48,16 +51,16 @@ public function getDecryptedValue(string $value = '') // Test if we're actually an encrypted value; if (ctype_xdigit($value) && strlen($value) > 130) { try { - return $this->service->decrypt($value); + return (int)$this->service->decrypt($value); } catch (Exception $e) { // We were unable to decrypt. Possibly a false positive, but return the unencrypted value - return $value; + return (int)$value; } } - return $value; + return (int)$value; } - public function requireField() + public function requireField(): void { $values = array( 'type' => 'text', @@ -71,7 +74,7 @@ public function requireField() DB::require_field($this->tableName, $this->name, $values); } - public function prepValueForDB($value) + public function prepEncryptedValueForDB(mixed $value): string { $value = parent::prepValueForDB($value); $ciphertext = $this->service->encrypt($value); diff --git a/src/FieldType/EncryptedText.php b/src/FieldType/EncryptedText.php index f2d0df8..df2888c 100644 --- a/src/FieldType/EncryptedText.php +++ b/src/FieldType/EncryptedText.php @@ -5,6 +5,7 @@ use Exception; use Madmatt\EncryptAtRest\Traits\EncryptedFieldGetValueTrait; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Model\ModelData; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBText; use Madmatt\EncryptAtRest\AtRestCryptoService; @@ -24,7 +25,7 @@ public function __construct($name = null, $options = []) $this->service = Injector::inst()->get(AtRestCryptoService::class); } - public function setValue($value, $record = null, $markChanged = true) + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static { if (is_array($record) && array_key_exists($this->name, $record) && $value === null) { $this->value = $record[$this->name]; @@ -34,6 +35,8 @@ public function setValue($value, $record = null, $markChanged = true) } else { $this->value = $value; } + + return $this; } public function getDecryptedValue(string $value = '') @@ -50,7 +53,7 @@ public function getDecryptedValue(string $value = '') return $value; } - public function requireField() + public function requireField(): void { $values = array( 'type' => 'text', @@ -64,7 +67,7 @@ public function requireField() DB::require_field($this->tableName, $this->name, $values); } - public function prepValueForDB($value) + public function prepValueForDB(mixed $value): array|string|null { $value = parent::prepValueForDB($value); $ciphertext = $this->service->encrypt($value); diff --git a/src/FieldType/EncryptedVarchar.php b/src/FieldType/EncryptedVarchar.php index 43aff7d..90854e4 100644 --- a/src/FieldType/EncryptedVarchar.php +++ b/src/FieldType/EncryptedVarchar.php @@ -6,6 +6,7 @@ use Madmatt\EncryptAtRest\AtRestCryptoService; use Madmatt\EncryptAtRest\Traits\EncryptedFieldGetValueTrait; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Model\ModelData; use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBVarchar; @@ -32,7 +33,7 @@ public function __construct($name = null, $size = 255, $options = array()) $this->service = Injector::inst()->get(AtRestCryptoService::class); } - public function setValue($value, $record = null, $markChanged = true) + public function setValue(mixed $value, null|array|ModelData $record = null, bool $markChanged = true): static { if (is_array($record) && array_key_exists($this->name, $record) && $value === null) { $this->value = $record[$this->name]; @@ -42,6 +43,8 @@ public function setValue($value, $record = null, $markChanged = true) } else { $this->value = $value; } + + return $this; } public function getDecryptedValue(string $value = '') @@ -58,7 +61,7 @@ public function getDecryptedValue(string $value = '') return $value; } - public function requireField() + public function requireField(): void { $values = array( 'type' => 'text', @@ -72,7 +75,7 @@ public function requireField() DB::require_field($this->tableName, $this->name, $values); } - public function prepValueForDB($value) + public function prepValueForDB(mixed $value): array|string|null { $value = parent::prepValueForDB($value); $ciphertext = $this->service->encrypt($value); diff --git a/src/Traits/EncryptedFieldGetValueTrait.php b/src/Traits/EncryptedFieldGetValueTrait.php index 43dd591..38f41ac 100644 --- a/src/Traits/EncryptedFieldGetValueTrait.php +++ b/src/Traits/EncryptedFieldGetValueTrait.php @@ -5,7 +5,7 @@ trait EncryptedFieldGetValueTrait { - public function getValue() + public function getValue(): mixed { // Type hardening for PHP 8.1+ $value = (string)$this->value; @@ -13,4 +13,15 @@ public function getValue() return $this->getDecryptedValue($value); } + public function writeToManipulation(array &$manipulation): void + { + if (!method_exists($this, 'prepEncryptedValueForDB')) { + $manipulation['fields'][$this->name] = $this->exists() + ? $this->prepValueForDB($this->value) : $this->nullValue(); + } else { + $manipulation['fields'][$this->name] = $this->exists() + ? $this->prepEncryptedValueForDB($this->value) : $this->nullValue(); + } + } + } diff --git a/tests/AtRestCryptoServiceTest.php b/tests/AtRestCryptoServiceTest.php index 7566470..9c9d1a5 100644 --- a/tests/AtRestCryptoServiceTest.php +++ b/tests/AtRestCryptoServiceTest.php @@ -182,7 +182,7 @@ public function testEncryptFile($filename, $contents, $visibility) /** * @see testEncryptFile */ - public function dataEncryptFile() + public static function dataEncryptFile(): array { return [ ['test-public-filename.txt', 'This is a test file', AssetStore::VISIBILITY_PUBLIC], diff --git a/tests/Extension/DecryptDataObjectFieldsExtensionTest.php b/tests/Extension/DecryptDataObjectFieldsExtensionTest.php index b149741..458c782 100644 --- a/tests/Extension/DecryptDataObjectFieldsExtensionTest.php +++ b/tests/Extension/DecryptDataObjectFieldsExtensionTest.php @@ -4,6 +4,7 @@ use Madmatt\EncryptAtRest\Tests\Model\EncryptedTestDataObject; use SilverStripe\Dev\SapphireTest; +use SilverStripe\ORM\DataObject; /** * Test encryption on dataobjects. diff --git a/tests/FieldType/EncryptedDecimalTest.php b/tests/FieldType/EncryptedDecimalTest.php index fca6a4b..ac0ee48 100644 --- a/tests/FieldType/EncryptedDecimalTest.php +++ b/tests/FieldType/EncryptedDecimalTest.php @@ -44,7 +44,7 @@ public function testDecimalStorage() $object = EncryptedTestDataObject::get()->byID($id); // Check that both ways we can access the data is correct - $this->assertEquals($expected, $object->DecimalTest); - $this->assertEquals($expected, $object->dbObject('DecimalTest')->getValue()); + $this->assertEquals($expected, (float)$object->DecimalTest); + $this->assertEquals($expected, (float)$object->dbObject('DecimalTest')->getValue()); } } diff --git a/tests/FieldType/EncryptedIntTest.php b/tests/FieldType/EncryptedIntTest.php index baeca44..2d6c2ab 100644 --- a/tests/FieldType/EncryptedIntTest.php +++ b/tests/FieldType/EncryptedIntTest.php @@ -44,7 +44,7 @@ public function testIntStorage() $object = EncryptedTestDataObject::get()->byID($id); // Check that both ways we can access the data is correct - $this->assertEquals($expected, $object->IntTest); - $this->assertEquals($expected, $object->dbObject('IntTest')->getValue()); + $this->assertEquals($expected, (int)$object->IntTest); + $this->assertEquals($expected, (int)$object->dbObject('IntTest')->getValue()); } } diff --git a/tests/Model/EncryptedTestDataObject.php b/tests/Model/EncryptedTestDataObject.php index 1919fd5..43f370b 100644 --- a/tests/Model/EncryptedTestDataObject.php +++ b/tests/Model/EncryptedTestDataObject.php @@ -19,7 +19,7 @@ class EncryptedTestDataObject extends DataObject implements TestOnly { private static $table_name = 'EncryptedTestDataObject'; - + private static $db = array( 'EncryptedText' => EncryptedVarchar::class, 'UnencryptedText' => 'Varchar(255)',