├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── example ├── README.md ├── bootstrap.php ├── cli-config.php ├── composer.json ├── entities │ ├── CreditCardDetails.php │ └── Entity.php ├── genkey.php ├── insert.php ├── read.php └── update.php ├── phpunit.xml.dist ├── src ├── Container │ ├── KeyContainer.php │ ├── NotFoundException.php │ ├── VersionedContainer.php │ └── VersionedInterface.php ├── Dbal │ ├── EncryptedColumn.php │ └── EncryptedColumnLegacySupport.php ├── Encryptor │ ├── EncryptorInterface.php │ ├── HaliteEncryptor.php │ └── LegacyEncryptor.php ├── Exception │ └── PopArtPenguinException.php ├── Serializer │ ├── LegacySerializer.php │ ├── PhpSerializer.php │ └── SerializerInterface.php ├── Service │ └── EncryptionService.php ├── Setup.php └── ValueObject │ ├── EncryptedColumn.php │ ├── EncryptorIdentity.php │ ├── IdentityInterface.php │ ├── Key.php │ ├── KeyIdentity.php │ ├── SerializerIdentity.php │ └── ValueHolder.php └── test ├── Functional ├── Fixtures │ ├── CreditCardDetails.php │ ├── Entity.php │ ├── enc-alt.key │ └── enc.key └── ReadWriteTest.php └── Migration ├── FiftyOneSystemsTest.php └── Fixtures ├── 51systems ├── Entity.php └── db.sqlite └── Migrated ├── Entity.php └── enc.key /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | .idea 3 | composer.lock 4 | composer.phar 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Doctrine Encryted Column 2 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/carnage/doctrine-encrypted-column/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/carnage/doctrine-encrypted-column/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/carnage/doctrine-encrypted-column/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/carnage/doctrine-encrypted-column/?branch=master) 3 | 4 | # Motivation 5 | 6 | Currently there are about a dozen encrypted column extensions for doctrine. None of them are very well implemented and are 7 | thus insecure (eg using Pop-art mode (ECB) or auto decrypting data on load) most also are tied to a framework making them 8 | useless unless you use that framework. 9 | 10 | This lib intends to resolve these two issues and provide an obvious choice library for anyone needing to encrypt data they 11 | are storing through doctrine ORM. 12 | 13 | Every endeavour will be taken to ensure that future versions of this library will be able to read data encrypted with 14 | older versions and re-encrypt to take advantage of any security fixes or improvements. In the event that this isn't 15 | possible automatically, guidance will be provided to allow you to migrate your data manually, to ensure that this process 16 | is as smooth as possible, we suggest making a note of the versions of lib sodium, halite and this library that you initially 17 | install. 18 | 19 | # Features 20 | 21 | - Encrypted column type for doctrine 22 | - Functionally similar to object column type 23 | - Transparent to end user 24 | - Uses proxies to avoid decrypting data that isn't needed 25 | - Best in class cryptography (LibSodium) 26 | 27 | # Pull requests 28 | 29 | I will accept pull requests for the following: 30 | 31 | - New serialisation support (JMS is desirable here) 32 | - Support for doctrine ODM 33 | - Support for different crypto backends which use a good implementation (eg Zend crypt, defuse, easyrsa) 34 | - bug fixes 35 | 36 | I will not accept: 37 | 38 | - Integration into << your favorite framework >> create a lib for that which uses this and PR a link for the readme 39 | - Support for poor crypto implementations (eg anything using mcrypt) 40 | 41 | 42 | # Security issues 43 | 44 | You can use my keys from keybase https://keybase.io/carnage to contact me regarding any security issues. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carnage/doctrine-encrypted-column", 3 | "description": "Provides a secure way of transparently encrypting data in doctrine ORM", 4 | "minimum-stability": "stable", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Chris Riley" 9 | } 10 | ], 11 | "require": { 12 | "php": "^7.0", 13 | "doctrine/orm": "^2.5", 14 | "ocramius/proxy-manager": "^1.0.2", 15 | "paragonie/halite": "^2.0", 16 | "psr/container": "^1.0", 17 | "phpseclib/phpseclib": "~2.0" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit":"^5" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Carnage\\EncryptedColumn\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Carnage\\EncryptedColumn\\Tests\\": "test/" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | composer install 3 | 4 | vendor/bin/doctrine orm:schema-tool:create 5 | 6 | php genkey.php 7 | php insert.php 8 | php read.php 9 | php update.php 10 | php read.php 11 | 12 | ``` -------------------------------------------------------------------------------- /example/bootstrap.php: -------------------------------------------------------------------------------- 1 | 'pdo_sqlite', 15 | 'path' => __DIR__ . '/db.sqlite', 16 | ); 17 | 18 | // obtaining the entity manager 19 | $entityManager = EntityManager::create($conn, $config); 20 | 21 | (new \Carnage\EncryptedColumn\Setup()) 22 | ->withKeyPath('./enc.key') 23 | ->register($entityManager); -------------------------------------------------------------------------------- /example/cli-config.php: -------------------------------------------------------------------------------- 1 | number = $number; 18 | $this->expiry = $expiry; 19 | } 20 | 21 | /** 22 | * @return mixed 23 | */ 24 | public function getNumber() 25 | { 26 | return $this->number; 27 | } 28 | 29 | /** 30 | * @return mixed 31 | */ 32 | public function getExpiry() 33 | { 34 | return $this->expiry; 35 | } 36 | } -------------------------------------------------------------------------------- /example/entities/Entity.php: -------------------------------------------------------------------------------- 1 | id; 32 | } 33 | 34 | /** 35 | * @param int $id 36 | */ 37 | public function setId($id) 38 | { 39 | $this->id = $id; 40 | } 41 | 42 | /** 43 | * @return CreditCardDetails 44 | */ 45 | public function getCreditCardDetails() 46 | { 47 | return $this->creditCardDetails; 48 | } 49 | 50 | /** 51 | * @param CreditCardDetails $creditCardDetails 52 | */ 53 | public function setCreditCardDetails($creditCardDetails) 54 | { 55 | $this->creditCardDetails = $creditCardDetails; 56 | } 57 | } -------------------------------------------------------------------------------- /example/genkey.php: -------------------------------------------------------------------------------- 1 | setCreditCardDetails(new \Carnage\DceTest\CreditCardDetails('1234567812345678', '04/19')); 8 | 9 | $entityManager->persist($entity); 10 | $entityManager->flush(); -------------------------------------------------------------------------------- /example/read.php: -------------------------------------------------------------------------------- 1 | find(\Carnage\DceTest\Entity::class, 1); 7 | 8 | //var_dump($entity); 9 | 10 | echo $entity->getCreditCardDetails()->getNumber(); 11 | -------------------------------------------------------------------------------- /example/update.php: -------------------------------------------------------------------------------- 1 | find(\Carnage\DceTest\Entity::class, 1); 7 | $entity->setCreditCardDetails(new \Carnage\DceTest\CreditCardDetails('000012340001234', '04/19')); 8 | 9 | $entityManager->flush(); -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./test/Functional 7 | 8 | 9 | ./test/Migration 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Container/KeyContainer.php: -------------------------------------------------------------------------------- 1 | keys[$key->getIdentifier()->toString()] = $key; 18 | } 19 | 20 | public function tagKey($tag, $id) 21 | { 22 | $this->keys[$tag] = $this->keys[$id]; 23 | } 24 | 25 | public function get($id) 26 | { 27 | if (!$this->has($id)) { 28 | throw NotFoundException::serviceNotFoundInContainer($id, $this->keys); 29 | } 30 | 31 | return $this->keys[$id]; 32 | } 33 | 34 | public function has($id): bool 35 | { 36 | return isset($this->keys[$id]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Container/NotFoundException.php: -------------------------------------------------------------------------------- 1 | services[$service->getIdentifier()->toString()] = $service; 18 | } 19 | } 20 | 21 | public function get($id): VersionedInterface 22 | { 23 | if (!$this->has($id)) { 24 | throw NotFoundException::serviceNotFoundInContainer($id, $this->services); 25 | } 26 | 27 | return $this->services[$id]; 28 | } 29 | 30 | public function has($id): bool 31 | { 32 | return isset($this->services[$id]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Container/VersionedInterface.php: -------------------------------------------------------------------------------- 1 | encryptionService = $encryptionService; 30 | } 31 | 32 | public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) 33 | { 34 | return $platform->getClobTypeDeclarationSQL($fieldDeclaration); 35 | } 36 | 37 | public function requiresSQLCommentHint(AbstractPlatform $platform) 38 | { 39 | return true; 40 | } 41 | 42 | public function getName() 43 | { 44 | return self::ENCRYPTED; 45 | } 46 | 47 | public function convertToPHPValue($value, AbstractPlatform $platform) 48 | { 49 | if ($value === null) { 50 | return null; 51 | } 52 | 53 | $decoded = $this->decodeJson($value); 54 | 55 | return $this->encryptionService->decryptField(EncryptedColumnVO::fromArray($decoded)); 56 | } 57 | 58 | public function convertToDatabaseValue($value, AbstractPlatform $platform) 59 | { 60 | if ($value === null) { 61 | return null; 62 | } 63 | 64 | return json_encode($this->encryptionService->encryptField($value)); 65 | } 66 | 67 | /** 68 | * Based on: https://github.com/schmittjoh/serializer/blob/master/src/JMS/Serializer/JsonDeserializationVisitor.php 69 | * 70 | * @param $value 71 | * @return mixed 72 | * @throws ConversionException 73 | */ 74 | private function decodeJson($value) 75 | { 76 | $decoded = json_decode($value, true); 77 | 78 | switch (json_last_error()) { 79 | case JSON_ERROR_NONE: 80 | if (!is_array($decoded)) { 81 | throw ConversionException::conversionFailed($value, 'Json was not an array'); 82 | } 83 | return $decoded; 84 | case JSON_ERROR_DEPTH: 85 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, maximum stack depth exceeded.'); 86 | case JSON_ERROR_STATE_MISMATCH: 87 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, underflow or the nodes mismatch.'); 88 | case JSON_ERROR_CTRL_CHAR: 89 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, unexpected control character found.'); 90 | case JSON_ERROR_SYNTAX: 91 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, syntax error - malformed JSON.'); 92 | case JSON_ERROR_UTF8: 93 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, malformed UTF-8 characters (incorrectly encoded?)'); 94 | default: 95 | throw ConversionException::conversionFailed($value, 'Could not decode Json'); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Dbal/EncryptedColumnLegacySupport.php: -------------------------------------------------------------------------------- 1 | encryptionService = $encryptionService; 35 | } 36 | 37 | public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) 38 | { 39 | return $platform->getClobTypeDeclarationSQL($fieldDeclaration); 40 | } 41 | 42 | public function requiresSQLCommentHint(AbstractPlatform $platform) 43 | { 44 | return true; 45 | } 46 | 47 | public function getName() 48 | { 49 | return self::ENCRYPTED; 50 | } 51 | 52 | public function convertToPHPValue($value, AbstractPlatform $platform) 53 | { 54 | if ($value === null) { 55 | return null; 56 | } 57 | 58 | try { 59 | $decoded = $this->decodeJson($value); 60 | } catch (ConversionException $e) { 61 | //The data wasn't in the format we expected, assume it is legacy data which needs converting 62 | //Drop in some defaults to allow the library to handle it. 63 | $decoded = [ 64 | 'data' => $value, 65 | 'classname' => ValueHolder::class, 66 | 'serializer' => 'legacy', 67 | 'encryptor' => 'legacy', 68 | 'keyid' => 'legacy', 69 | ]; 70 | } 71 | 72 | return $this->encryptionService->decryptField(EncryptedColumnVO::fromArray($decoded)); 73 | } 74 | 75 | public function convertToDatabaseValue($value, AbstractPlatform $platform) 76 | { 77 | if ($value === null) { 78 | return null; 79 | } 80 | 81 | return json_encode($this->encryptionService->encryptField($value)); 82 | } 83 | 84 | /** 85 | * Based on: https://github.com/schmittjoh/serializer/blob/master/src/JMS/Serializer/JsonDeserializationVisitor.php 86 | * 87 | * @param $value 88 | * @return mixed 89 | * @throws ConversionException 90 | */ 91 | private function decodeJson($value) 92 | { 93 | $decoded = json_decode($value, true); 94 | 95 | switch (json_last_error()) { 96 | case JSON_ERROR_NONE: 97 | if (!is_array($decoded)) { 98 | throw ConversionException::conversionFailed($value, 'Json was not an array'); 99 | } 100 | return $decoded; 101 | case JSON_ERROR_DEPTH: 102 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, maximum stack depth exceeded.'); 103 | case JSON_ERROR_STATE_MISMATCH: 104 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, underflow or the nodes mismatch.'); 105 | case JSON_ERROR_CTRL_CHAR: 106 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, unexpected control character found.'); 107 | case JSON_ERROR_SYNTAX: 108 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, syntax error - malformed JSON.'); 109 | case JSON_ERROR_UTF8: 110 | throw ConversionException::conversionFailed($value, 'Could not decode JSON, malformed UTF-8 characters (incorrectly encoded?)'); 111 | default: 112 | throw ConversionException::conversionFailed($value, 'Could not decode Json'); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Encryptor/EncryptorInterface.php: -------------------------------------------------------------------------------- 1 | loadKey($key)); 18 | } 19 | 20 | public function decrypt($data, Key $key) 21 | { 22 | return Symmetric\Crypto::decrypt($data, $this->loadKey($key)); 23 | } 24 | 25 | public function getIdentifier(): IdentityInterface 26 | { 27 | return new EncryptorIdentity(self::IDENTITY); 28 | } 29 | 30 | /** 31 | * @return Symmetric\EncryptionKey 32 | * @throws \ParagonIE\Halite\Alerts\CannotPerformOperation 33 | */ 34 | private function loadKey(Key $key) 35 | { 36 | return KeyFactory::loadEncryptionKey($key->getKeyInfo()); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Encryptor/LegacyEncryptor.php: -------------------------------------------------------------------------------- 1 | setBlockLength(256); 25 | $cipher->setKey($key->getKeyInfo()); 26 | $cipher->padding = false; 27 | 28 | return trim($cipher->decrypt(base64_decode($data))); 29 | } 30 | 31 | public function getIdentifier(): IdentityInterface 32 | { 33 | return new EncryptorIdentity(self::IDENTITY); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Exception/PopArtPenguinException.php: -------------------------------------------------------------------------------- 1 | encryptor = $encryptor; 53 | $this->serializer = $serializer; 54 | $this->encryptors = $encryptors; 55 | $this->serializers = $serializers; 56 | $this->keys = $keys; 57 | } 58 | 59 | public function decryptField(EncryptedColumnVO $value) 60 | { 61 | $initializer = $this->createInitializer($value); 62 | $factory = new LazyLoadingValueHolderFactory(); 63 | $proxy = $factory->createProxy($value->getClassname(), $initializer); 64 | 65 | $this->originalValues[spl_object_hash($proxy)] = $value; 66 | 67 | return $proxy; 68 | } 69 | 70 | public function encryptField($value): EncryptedColumnVO 71 | { 72 | if ($value instanceof LazyLoadingInterface) { 73 | /** @var VirtualProxyInterface $value */ 74 | // If the value hasn't been decrypted; it hasn't been changed. Don't bother reencrypting unless it 75 | // was encrypted using a different configuration 76 | if (!$value->isProxyInitialized()) { 77 | $original = $this->originalValues[spl_object_hash($value)]; 78 | if ( 79 | !$original->needsReencryption($this->encryptor->getIdentifier(), $this->serializer->getIdentifier()) 80 | ) { 81 | return $original; 82 | } 83 | } 84 | 85 | //we don't want to encrypt a proxy. 86 | $value = $value->getWrappedValueHolderValue(); 87 | } 88 | 89 | if (!is_object($value)) { 90 | throw new \Exception('This column type only supports encrypting objects'); 91 | } 92 | 93 | $key = $this->keys->get('default'); 94 | $data = $this->encryptor->encrypt($this->serializer->serialize($value), $key); 95 | 96 | return new EncryptedColumnVO( 97 | get_class($value), 98 | $data, 99 | $this->encryptor->getIdentifier(), 100 | $this->serializer->getIdentifier(), 101 | $key->getIdentifier() 102 | ); 103 | } 104 | 105 | /** 106 | * @param EncryptedColumnVO $value 107 | * @return \Closure 108 | */ 109 | private function createInitializer(EncryptedColumnVO $value): \Closure 110 | { 111 | $serializer = $this->serializers->get($value->getSerializerIdentifier()->toString()); 112 | $encryptor = $this->encryptors->get($value->getEncryptorIdentifier()->toString()); 113 | $key = $this->keys->get($value->getKeyIdentifier()->toString()); 114 | 115 | return function(& $wrappedObject, LazyLoadingInterface $proxy, $method, array $parameters, & $initializer) use ($serializer, $encryptor, $key, $value) { 116 | $initializer = null; 117 | $wrappedObject = $serializer->unserialize($encryptor->decrypt($value->getData(), $key)); 118 | 119 | return true; 120 | }; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Setup.php: -------------------------------------------------------------------------------- 1 | keyContainer = new KeyContainer(); 28 | } 29 | 30 | public function register(EntityManagerInterface $em) 31 | { 32 | if ($this->enableLegacy) { 33 | $this->doRegisterLegacy($em); 34 | } else { 35 | $this->doRegister($em); 36 | } 37 | } 38 | 39 | public function enableLegacy(string $legacyKey) 40 | { 41 | $this->enableLegacy = true; 42 | $this->legacyKey = $legacyKey; 43 | 44 | $key = new Key($legacyKey); 45 | $this->keyContainer->addKey($key); 46 | $this->keyContainer->tagKey('legacy', $key->getIdentifier()->toString()); 47 | 48 | return $this; 49 | } 50 | 51 | public function withKeyPath(string $keypath) 52 | { 53 | $this->keyPath = $keypath; 54 | 55 | $key = new Key($keypath); 56 | $this->keyContainer->addKey($key); 57 | $this->keyContainer->tagKey('default', $key->getIdentifier()->toString()); 58 | 59 | return $this; 60 | } 61 | 62 | public function withKey(string $key, array $tags = []) 63 | { 64 | $key = new Key($key); 65 | $keyId = $key->getIdentifier()->toString(); 66 | $this->keyContainer->addKey($key); 67 | 68 | foreach ($tags as $tag) { 69 | $this->keyContainer->tagKey($tag, $keyId); 70 | } 71 | 72 | return $this; 73 | } 74 | 75 | private function buildEncryptionService(): EncryptionService 76 | { 77 | $encryptors = self::buildEncryptorsContainer(); 78 | $serializers = self::buildSerilaizerContainer(); 79 | return new EncryptionService( 80 | $encryptors->get(HaliteEncryptor::IDENTITY), 81 | $serializers->get(PhpSerializer::IDENTITY), 82 | $encryptors, 83 | $serializers, 84 | $this->keyContainer 85 | ); 86 | } 87 | 88 | private function buildEncryptorsContainer(): VersionedContainer 89 | { 90 | $services = [new HaliteEncryptor($this->keyPath)]; 91 | if ($this->enableLegacy) { 92 | $services[] = new LegacyEncryptor($this->legacyKey); 93 | } 94 | //@TODO add legacy encryptor, throw exceptions if required keys aren't specified 95 | return new VersionedContainer(...$services); 96 | } 97 | 98 | private function buildSerilaizerContainer(): VersionedContainer 99 | { 100 | $services = [new PhpSerializer()]; 101 | if ($this->enableLegacy) { 102 | $services[] = new LegacySerializer(); 103 | } 104 | return new VersionedContainer(...$services); 105 | } 106 | 107 | /** 108 | * @param EntityManagerInterface $em 109 | */ 110 | private function doRegister(EntityManagerInterface $em) 111 | { 112 | EncryptedColumn::create($this->buildEncryptionService()); 113 | $conn = $em->getConnection(); 114 | $conn->getDatabasePlatform()->registerDoctrineTypeMapping( 115 | EncryptedColumn::ENCRYPTED, 116 | EncryptedColumn::ENCRYPTED 117 | ); 118 | } 119 | 120 | /** 121 | * @param EntityManagerInterface $em 122 | */ 123 | private function doRegisterLegacy(EntityManagerInterface $em) 124 | { 125 | EncryptedColumnLegacySupport::create($this->buildEncryptionService()); 126 | $conn = $em->getConnection(); 127 | $conn->getDatabasePlatform()->registerDoctrineTypeMapping( 128 | EncryptedColumnLegacySupport::ENCRYPTED, 129 | EncryptedColumnLegacySupport::ENCRYPTED 130 | ); 131 | } 132 | } -------------------------------------------------------------------------------- /src/ValueObject/EncryptedColumn.php: -------------------------------------------------------------------------------- 1 | classname = $classname; 43 | $this->data = $data; 44 | $this->encryptor = $encryptor; 45 | $this->serializer = $serializer; 46 | $this->key = $key; 47 | } 48 | 49 | public static function fromArray(array $data) 50 | { 51 | return new self( 52 | $data['classname'], 53 | $data['data'], 54 | new EncryptorIdentity($data['encryptor']), 55 | new SerializerIdentity($data['serializer']), 56 | new KeyIdentity($data['keyid']) 57 | ); 58 | } 59 | 60 | public function jsonSerialize(): array 61 | { 62 | return [ 63 | 'classname' => $this->classname, 64 | 'data' => $this->data, 65 | 'encryptor' => $this->encryptor->toString(), 66 | 'serializer' => $this->serializer->toString(), 67 | 'keyid' => $this->key->toString(), 68 | ]; 69 | } 70 | 71 | /** 72 | * @return mixed 73 | */ 74 | public function getClassname(): string 75 | { 76 | return $this->classname; 77 | } 78 | 79 | /** 80 | * @return mixed 81 | */ 82 | public function getData(): string 83 | { 84 | return $this->data; 85 | } 86 | 87 | /** 88 | * @return EncryptorIdentity 89 | */ 90 | public function getEncryptorIdentifier(): EncryptorIdentity 91 | { 92 | return $this->encryptor; 93 | } 94 | 95 | /** 96 | * @return SerializerIdentity 97 | */ 98 | public function getSerializerIdentifier(): SerializerIdentity 99 | { 100 | return $this->serializer; 101 | } 102 | 103 | /** 104 | * @return KeyIdentity 105 | */ 106 | public function getKeyIdentifier(): KeyIdentity 107 | { 108 | return $this->key; 109 | } 110 | 111 | public function needsReencryption(EncryptorIdentity $encryptor, SerializerIdentity $serializer): bool 112 | { 113 | return $encryptor->equals($this->encryptor) && $serializer->equals($this->serializer); 114 | } 115 | } -------------------------------------------------------------------------------- /src/ValueObject/EncryptorIdentity.php: -------------------------------------------------------------------------------- 1 | identity = $identity; 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | public function getIdentity(): string 21 | { 22 | return $this->identity; 23 | } 24 | 25 | public function toString(): string 26 | { 27 | return $this->identity; 28 | } 29 | 30 | public function equals(IdentityInterface $other): bool 31 | { 32 | return $other instanceof EncryptorIdentity && $this->identity === $other->identity; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ValueObject/IdentityInterface.php: -------------------------------------------------------------------------------- 1 | identifier = new KeyIdentity(Util::safeSubstr(hash('sha256', $keyInfo), 0, 8)); 22 | $this->keyInfo = $keyInfo; 23 | } 24 | 25 | public function getIdentifier(): KeyIdentity 26 | { 27 | return $this->identifier; 28 | } 29 | 30 | public function getKeyInfo(): string 31 | { 32 | return $this->keyInfo; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ValueObject/KeyIdentity.php: -------------------------------------------------------------------------------- 1 | identity = $identity; 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | public function getIdentity(): string 21 | { 22 | return $this->identity; 23 | } 24 | 25 | public function toString(): string 26 | { 27 | return $this->identity; 28 | } 29 | 30 | public function equals(IdentityInterface $other): bool 31 | { 32 | return $other instanceof KeyIdentity && $this->identity === $other->identity; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ValueObject/SerializerIdentity.php: -------------------------------------------------------------------------------- 1 | identity = $identity; 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | public function getIdentity(): string 21 | { 22 | return $this->identity; 23 | } 24 | 25 | public function toString(): string 26 | { 27 | return $this->identity; 28 | } 29 | 30 | public function equals(IdentityInterface $other): bool 31 | { 32 | return $other instanceof SerializerIdentity && $this->identity === $other->identity; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ValueObject/ValueHolder.php: -------------------------------------------------------------------------------- 1 | value = $value; 12 | } 13 | 14 | /** 15 | * @return mixed 16 | */ 17 | public function getValue() 18 | { 19 | return $this->value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/Functional/Fixtures/CreditCardDetails.php: -------------------------------------------------------------------------------- 1 | number = $number; 18 | $this->expiry = $expiry; 19 | } 20 | 21 | /** 22 | * @return mixed 23 | */ 24 | public function getNumber() 25 | { 26 | return $this->number; 27 | } 28 | 29 | /** 30 | * @return mixed 31 | */ 32 | public function getExpiry() 33 | { 34 | return $this->expiry; 35 | } 36 | } -------------------------------------------------------------------------------- /test/Functional/Fixtures/Entity.php: -------------------------------------------------------------------------------- 1 | id; 37 | } 38 | 39 | /** 40 | * @param int $id 41 | */ 42 | public function setId($id) 43 | { 44 | $this->id = $id; 45 | } 46 | 47 | /** 48 | * @return CreditCardDetails 49 | */ 50 | public function getCreditCardDetails() 51 | { 52 | return $this->creditCardDetails; 53 | } 54 | 55 | /** 56 | * @param CreditCardDetails $creditCardDetails 57 | */ 58 | public function setCreditCardDetails($creditCardDetails) 59 | { 60 | $this->creditCardDetails = $creditCardDetails; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getUnrelated() 67 | { 68 | return $this->unrelated; 69 | } 70 | 71 | /** 72 | * @param string $unrelated 73 | */ 74 | public function setUnrelated($unrelated) 75 | { 76 | $this->unrelated = $unrelated; 77 | } 78 | } -------------------------------------------------------------------------------- /test/Functional/Fixtures/enc-alt.key: -------------------------------------------------------------------------------- 1 | 31400201bf84730414a7aa1acb562264a319cd56b7c41d76dc0606c05186e1575203911e7d447ea033b08735e3af9c50819c0e52775e1afa848bf2d87754ceb5f3be7f09eb579febe7710478e08f0e5d86067c8b31cce7cc3d8edfd62e9cff518f301278 -------------------------------------------------------------------------------- /test/Functional/Fixtures/enc.key: -------------------------------------------------------------------------------- 1 | 3140020116a8b2228171a56c19892cf5fa26cafa2d1ab2a2bd83f2cc7ca55e812bf65abdc429b99b9d70ad37a89f718a3b2adecb2b8f267e8487d75e2a48031b1ab4668beaa5a38d437ad3e6dc637e55d5a82e6d3647c84df40796be1d0893f6eb4971a4 -------------------------------------------------------------------------------- /test/Functional/ReadWriteTest.php: -------------------------------------------------------------------------------- 1 | 'pdo_sqlite', 54 | 'path' => ':memory:', 55 | ); 56 | 57 | self::$_em = EntityManager::create($conn, $config); 58 | 59 | self::$_setup = new ECSetup(); 60 | 61 | self::$_setup 62 | ->withKeyPath( __DIR__ . '/Fixtures/enc.key') 63 | ->register(self::$_em); 64 | 65 | $schemaTool = new SchemaTool(self::$_em); 66 | 67 | $classes = [ 68 | self::$_em->getClassMetadata(Entity::class) 69 | ]; 70 | 71 | $schemaTool->createSchema($classes); 72 | } 73 | 74 | $this->em = self::$_em; 75 | $this->setup = self::$_setup; 76 | } 77 | 78 | public function testInsert() 79 | { 80 | $entity = new Entity(); 81 | $entity->setCreditCardDetails(new CreditCardDetails('1234567812345678', '04/19')); 82 | 83 | $this->em->persist($entity); 84 | $this->em->flush(); 85 | 86 | $data = $this->em->getConnection()->fetchAll('SELECT * FROM Entity'); 87 | $savedData = json_decode($data[0]['creditCardDetails']); 88 | 89 | $this->assertObjectHasAttribute('classname', $savedData); 90 | $this->assertObjectHasAttribute('data', $savedData); 91 | } 92 | 93 | public function testRead() 94 | { 95 | $entity = new Entity(); 96 | $creditCardDetails = new CreditCardDetails('1234567812345678', '04/19'); 97 | $entity->setCreditCardDetails($creditCardDetails); 98 | 99 | $this->em->persist($entity); 100 | $this->em->flush(); 101 | 102 | $this->em->clear(); 103 | 104 | $entity = $this->em->find(Entity::class, 1); 105 | 106 | $this->assertEquals($creditCardDetails->getNumber(), $entity->getCreditCardDetails()->getNumber()); 107 | $this->assertEquals($creditCardDetails->getExpiry(), $entity->getCreditCardDetails()->getExpiry()); 108 | } 109 | 110 | public function testUpdateUnrelated() 111 | { 112 | $entity = new Entity(); 113 | $entity->setCreditCardDetails(new CreditCardDetails('1234567812345678', '04/19')); 114 | 115 | $this->em->persist($entity); 116 | $this->em->flush(); 117 | $this->em->clear(); 118 | 119 | $data = $this->em->getConnection()->fetchAll('SELECT * FROM Entity'); 120 | $savedData = json_decode($data[0]['creditCardDetails']); 121 | 122 | $entity = $this->em->find(Entity::class, 1); 123 | $entity->setUnrelated('unrelated'); 124 | $this->em->flush($entity); 125 | 126 | $data = $this->em->getConnection()->fetchAll('SELECT * FROM Entity'); 127 | 128 | $this->assertEquals($savedData, json_decode($data[0]['creditCardDetails'])); 129 | } 130 | 131 | public function testUpdate() 132 | { 133 | $entity = new Entity(); 134 | $entity->setCreditCardDetails(new CreditCardDetails('1234567812345678', '04/19')); 135 | 136 | $this->em->persist($entity); 137 | $this->em->flush(); 138 | $this->em->clear(); 139 | 140 | $data = $this->em->getConnection()->fetchAll('SELECT * FROM Entity'); 141 | $savedData = json_decode($data[0]['creditCardDetails']); 142 | 143 | $entity = $this->em->find(Entity::class, 1); 144 | $entity->setCreditCardDetails(new CreditCardDetails('1234567812345678', '04/19')); 145 | $this->em->flush($entity); 146 | 147 | $data = $this->em->getConnection()->fetchAll('SELECT * FROM Entity'); 148 | 149 | $this->assertNotEquals($savedData, json_decode($data[0]['creditCardDetails'])); 150 | } 151 | 152 | public function testReadAfterKeyChange() 153 | { 154 | $entity = new Entity(); 155 | $creditCardDetails = new CreditCardDetails('1234567812345678', '04/19'); 156 | $entity->setCreditCardDetails($creditCardDetails); 157 | 158 | $this->em->persist($entity); 159 | $this->em->flush(); 160 | 161 | $this->em->clear(); 162 | 163 | $this->setup->withKey(__DIR__ . '/Fixtures/enc-alt.key', ['default']); 164 | 165 | $entity = $this->em->find(Entity::class, 1); 166 | 167 | $this->assertEquals($creditCardDetails->getNumber(), $entity->getCreditCardDetails()->getNumber()); 168 | $this->assertEquals($creditCardDetails->getExpiry(), $entity->getCreditCardDetails()->getExpiry()); 169 | } 170 | } -------------------------------------------------------------------------------- /test/Migration/FiftyOneSystemsTest.php: -------------------------------------------------------------------------------- 1 | 'pdo_sqlite', 43 | 'path' => __DIR__ . "/Fixtures/Migrated/db.sqlite", 44 | ); 45 | 46 | self::$_em = EntityManager::create($conn, $config); 47 | 48 | (new ECSetup()) 49 | ->withKeyPath(__DIR__ . '/Fixtures/Migrated/enc.key') 50 | ->enableLegacy(pack("H*", "dda8e5b978e05346f08b312a8c2eac03670bb5661097f8bc13212c31be66384c")) 51 | ->register(self::$_em); 52 | 53 | $schemaTool = new SchemaTool(self::$_em); 54 | 55 | $classes = [ 56 | self::$_em->getClassMetadata(Entity::class) 57 | ]; 58 | 59 | $schemaTool->updateSchema($classes); 60 | } 61 | 62 | $this->em = self::$_em; 63 | } 64 | 65 | public function testRead() 66 | { 67 | /** @var Entity $entity */ 68 | $entity = $this->em->find(Entity::class, 1); 69 | $this->assertEquals('secret code', $entity->getSecretData()); 70 | } 71 | 72 | public function testWrite() 73 | { 74 | /** @var Entity $entity */ 75 | $entity = $this->em->find(Entity::class, 1); 76 | $entity->setSecretData('top secret code'); 77 | 78 | $this->em->flush($entity); 79 | $this->em->clear(); 80 | 81 | $entity = $this->em->find(Entity::class, 1); 82 | $this->assertEquals('top secret code', $entity->getSecretData()); 83 | } 84 | } -------------------------------------------------------------------------------- /test/Migration/Fixtures/51systems/Entity.php: -------------------------------------------------------------------------------- 1 | id; 34 | } 35 | 36 | /** 37 | * @param mixed $id 38 | */ 39 | public function setId($id) 40 | { 41 | $this->id = $id; 42 | } 43 | 44 | /** 45 | * @return mixed 46 | */ 47 | public function getSecretData() 48 | { 49 | return $this->secret_data; 50 | } 51 | 52 | /** 53 | * @param mixed $secret_data 54 | */ 55 | public function setSecretData($secret_data) 56 | { 57 | $this->secret_data = $secret_data; 58 | } 59 | } -------------------------------------------------------------------------------- /test/Migration/Fixtures/51systems/db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carnage/doctrine-encrypted-column/c15db0964e447cc0c653bba2de9aef0a4612ed60/test/Migration/Fixtures/51systems/db.sqlite -------------------------------------------------------------------------------- /test/Migration/Fixtures/Migrated/Entity.php: -------------------------------------------------------------------------------- 1 | id; 33 | } 34 | 35 | /** 36 | * @param mixed $id 37 | */ 38 | public function setId($id) 39 | { 40 | $this->id = $id; 41 | } 42 | 43 | /** 44 | * @return mixed 45 | */ 46 | public function getSecretData() 47 | { 48 | return $this->secret_data->getValue(); 49 | } 50 | 51 | /** 52 | * @param mixed $secret_data 53 | */ 54 | public function setSecretData($secret_data) 55 | { 56 | $this->secret_data = new ValueHolder($secret_data); 57 | } 58 | } -------------------------------------------------------------------------------- /test/Migration/Fixtures/Migrated/enc.key: -------------------------------------------------------------------------------- 1 | 3140020116a8b2228171a56c19892cf5fa26cafa2d1ab2a2bd83f2cc7ca55e812bf65abdc429b99b9d70ad37a89f718a3b2adecb2b8f267e8487d75e2a48031b1ab4668beaa5a38d437ad3e6dc637e55d5a82e6d3647c84df40796be1d0893f6eb4971a4 --------------------------------------------------------------------------------