├── .gitignore ├── Tests ├── Unit │ ├── Encryptors │ │ ├── fixtures │ │ │ ├── defuse.key │ │ │ └── halite.key │ │ ├── DefuseEncryptorTest.php │ │ └── HaliteEncryptorTest.php │ ├── Subscribers │ │ ├── fixtures │ │ │ ├── ExtendedUser.php │ │ │ ├── WithUser.php │ │ │ └── User.php │ │ └── DoctrineEncryptSubscriberTest.php │ └── DependencyInjection │ │ └── DoctrineEncryptExtensionTest.php ├── Functional │ ├── fixtures │ │ ├── halite.key │ │ ├── defuse.key │ │ └── Entity │ │ │ ├── CascadeTarget.php │ │ │ └── Owner.php │ ├── BasicQueryTest │ │ ├── BasicQueryDefuseTest.php │ │ ├── BasicQueryHaliteTest.php │ │ └── AbstractBasicQueryTestCase.php │ ├── DoctrineEncryptSubscriber │ │ ├── DoctrineEncryptSubscriberDefuseTestCase.php │ │ ├── DoctrineEncryptSubscriberHaliteTestCase.php │ │ └── AbstractDoctrineEncryptSubscriberTestCase.php │ └── AbstractFunctionalTestCase.php └── bootstrap.php ├── src ├── Configuration │ ├── Annotation.php │ └── Encrypted.php ├── Resources │ ├── doc │ │ ├── configuration_reference.md │ │ ├── custom_encryptor.md │ │ ├── index.md │ │ ├── configuration.md │ │ ├── usage.md │ │ ├── installation.md │ │ ├── commands.md │ │ └── example_of_usage.md │ └── config │ │ └── services.yml ├── AmbtaDoctrineEncryptBundle.php ├── Encryptors │ ├── EncryptorInterface.php │ ├── DefuseEncryptor.php │ └── HaliteEncryptor.php ├── DependencyInjection │ ├── Configuration.php │ └── DoctrineEncryptExtension.php ├── Command │ ├── DoctrineEncryptStatusCommand.php │ ├── AbstractCommand.php │ ├── DoctrineEncryptDatabaseCommand.php │ └── DoctrineDecryptDatabaseCommand.php ├── Mapping │ ├── AttributeReader.php │ └── AttributeAnnotationReader.php └── Subscribers │ └── DoctrineEncryptSubscriber.php ├── LICENSE ├── phpunit.xml.dist ├── composer.json ├── .travis.yml ├── README.md └── .phpunit.result.cache /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /composer.lock 3 | /vendor 4 | -------------------------------------------------------------------------------- /Tests/Unit/Encryptors/fixtures/defuse.key: -------------------------------------------------------------------------------- 1 | abcdefg 2 | -------------------------------------------------------------------------------- /src/Configuration/Annotation.php: -------------------------------------------------------------------------------- 1 | 11 | * @Annotation 12 | * @Target("PROPERTY") 13 | */ 14 | #[Attribute(Attribute::TARGET_PROPERTY)] 15 | class Encrypted implements Annotation 16 | { 17 | // Placeholder 18 | } 19 | -------------------------------------------------------------------------------- /src/Resources/doc/configuration_reference.md: -------------------------------------------------------------------------------- 1 | # Configuration Reference 2 | 3 | All available configuration options are listed below. 4 | 5 | ``` yaml 6 | ambta_doctrine_encrypt: 7 | # If you want, you can use your own Encryptor. Encryptor must implements EncryptorInterface interface 8 | # Default: Halite 9 | encryptor_class: Halite 10 | 11 | # Path where to store the keyfiles 12 | # Default: '%kernel.project_dir%' 13 | secret_directory_path: '%kernel.project_dir%' 14 | ``` 15 | 16 | -------------------------------------------------------------------------------- /Tests/Functional/BasicQueryTest/BasicQueryDefuseTest.php: -------------------------------------------------------------------------------- 1 | extra = $extra; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AmbtaDoctrineEncryptBundle.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface EncryptorInterface 11 | { 12 | /** 13 | * @param string $data Plain text to encrypt 14 | * @return string Encrypted text 15 | */ 16 | public function encrypt(string $data): string; 17 | 18 | /** 19 | * @param string $data Encrypted text 20 | * @return string Plain text 21 | */ 22 | public function decrypt(string $data): string; 23 | } 24 | -------------------------------------------------------------------------------- /Tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberDefuseTestCase.php: -------------------------------------------------------------------------------- 1 | name = $name; 30 | $this->foo = $foo; 31 | $this->user = $user; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/Functional/BasicQueryTest/BasicQueryHaliteTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('This test only runs when the sodium extension is enabled.'); 19 | 20 | return; 21 | } 22 | 23 | parent::setUp(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Tests/Unit/Subscribers/fixtures/User.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | $this->address = $address; 25 | } 26 | 27 | public function getAddress(): ?string 28 | { 29 | return $this->address; 30 | } 31 | 32 | public function setAddress(?string $address): void 33 | { 34 | $this->address = $address; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Resources/doc/index.md: -------------------------------------------------------------------------------- 1 | # DoctrineEncryptBundle 2 | 3 | This bundle is responsible for encryption/decryption of the data in your database. 4 | All encryption/decryption work on the server side. 5 | 6 | The following documents are available: 7 | 8 | * [Installation](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/installation.md) 9 | * [Configuration](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/configuration.md) 10 | * [Usage](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/usage.md) 11 | * [Console commands](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/commands.md) 12 | * [Custom encryptor class](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/custom_encryptor.md) 13 | -------------------------------------------------------------------------------- /Tests/Functional/DoctrineEncryptSubscriber/DoctrineEncryptSubscriberHaliteTestCase.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('This test only runs when the sodium extension is enabled.'); 20 | 21 | return; 22 | } 23 | 24 | parent::setUp(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Resources/doc/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration Reference 2 | 3 | There is only 1 paramater in the configuration of the Doctrine encryption bundle. 4 | This parameter is also optional. 5 | 6 | * **encryptor_class** - Custom class for encrypting data 7 | * Encryptor class, [your own encryptor class](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/custom_encryptor.md) will override encryptor paramater 8 | * Default: Halite 9 | 10 | ## yaml 11 | 12 | ``` yaml 13 | ambta_doctrine_encrypt: 14 | encryptor_class: Halite # or Defuse 15 | secret_directory_path: '%kernel.project_dir%' # Path where to store the keyfiles 16 | ``` 17 | 18 | ## Important! 19 | 20 | If you want to use Defuse, make sure to require it! 21 | 22 | composer require "defuse/php-encryption ^2.0" 23 | 24 | ## Usage 25 | 26 | Read how to use the database encryption bundle in your project. 27 | #### [Usage](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/usage.md) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. 18 | -------------------------------------------------------------------------------- /src/Resources/doc/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ### Entity 4 | 5 | ``` php 6 | namespace Acme\DemoBundle\Entity; 7 | 8 | use Doctrine\ORM\Mapping as ORM; 9 | 10 | // importing @Encrypted annotation 11 | use Ambta\DoctrineEncryptBundle\Configuration\Encrypted; 12 | 13 | /** 14 | * @ORM\Entity 15 | * @ORM\Table(name="user") 16 | */ 17 | class User { 18 | 19 | .. 20 | 21 | /** 22 | * @ORM\Column(type="string", name="email") 23 | * @Encrypted 24 | * @var int 25 | */ 26 | private $email; 27 | 28 | .. 29 | 30 | } 31 | ``` 32 | 33 | It is as simple as that, the field will now be encrypted the first time the users entity gets edited. 34 | We keep an prefix to check if data is encrypted or not so, unencrypted data will still work even if the field is encrypted. 35 | 36 | ## Console commands 37 | 38 | There are some console commands that can help you encrypt your existing database or change encryption methods. 39 | Read more about the database encryption commands provided with this bundle. 40 | 41 | #### [Console commands](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/commands.md) 42 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | ./Tests/Unit 12 | 13 | 14 | ./Tests/Functional 15 | 16 | 17 | 18 | 19 | 20 | ./Command 21 | ./Configuration 22 | ./DependencyInjection 23 | ./Encryptors 24 | ./Subscribers 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "absolute-quantum/doctrine-encrypt-bundle", 3 | "type": "library", 4 | "keywords": ["doctrine", "symfony", "halite", "defuse", "encrypt", "decrypt"], 5 | "license": "MIT", 6 | "description": "Encrypted symfony entity's by verified and standardized libraries", 7 | "require": { 8 | "php": "^8.0", 9 | "paragonie/halite": "^4.6", 10 | "paragonie/sodium_compat": "^1.5", 11 | "doctrine/orm": "^2.5", 12 | "symfony/property-access": "^4.1|^5.0|^6.0", 13 | "symfony/dependency-injection": "^4.1|^5.0|^6.0", 14 | "symfony/yaml": "^4.1|^5.0|^6.0", 15 | "symfony/http-kernel": "^4.1|^5.0|^6.0", 16 | "symfony/config": "^4.1|^5.0|^6.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^8.0|^9.0", 20 | "defuse/php-encryption": "^2.1" 21 | }, 22 | "suggest": { 23 | "defuse/php-encryption": "Alternative for halite for use with older php-versions", 24 | "ext-sodium": "Required to use halite encryption library." 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Ambta\\DoctrineEncryptBundle\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Ambta\\DoctrineEncryptBundle\\Tests\\": "Tests/" 34 | } 35 | }, 36 | "config": { 37 | "allow-plugins": { 38 | "composer/package-versions-deprecated": true 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/Unit/Encryptors/DefuseEncryptorTest.php: -------------------------------------------------------------------------------- 1 | encrypt(self::DATA); 19 | $this->assertNotSame(self::DATA, $encrypted); 20 | $decrypted = $defuse->decrypt($encrypted); 21 | 22 | $this->assertSame(self::DATA, $decrypted); 23 | $newkey = file_get_contents($keyfile); 24 | $this->assertSame($key, $newkey, 'The key must not be modified'); 25 | } 26 | 27 | public function testGenerateKey(): void 28 | { 29 | $keyfile = sys_get_temp_dir().'/defuse-'.md5(time()); 30 | if (file_exists($keyfile)) { 31 | unlink($keyfile); 32 | } 33 | $defuse = new DefuseEncryptor($keyfile); 34 | $defuse->encrypt(self::DATA); 35 | 36 | $this->assertFileExists($keyfile); 37 | $this->assertNotEmpty(file_get_contents($keyfile), 'A key should have been created and saved to the file'); 38 | 39 | unlink($keyfile); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/Functional/fixtures/Entity/CascadeTarget.php: -------------------------------------------------------------------------------- 1 | id; 38 | } 39 | 40 | /** 41 | * @return mixed 42 | */ 43 | public function getSecret() 44 | { 45 | return $this->secret; 46 | } 47 | 48 | /** 49 | * @param mixed $secret 50 | */ 51 | public function setSecret($secret): void 52 | { 53 | $this->secret = $secret; 54 | } 55 | 56 | /** 57 | * @return mixed 58 | */ 59 | public function getNotSecret() 60 | { 61 | return $this->notSecret; 62 | } 63 | 64 | /** 65 | * @param mixed $notSecret 66 | */ 67 | public function setNotSecret($notSecret): void 68 | { 69 | $this->notSecret = $notSecret; 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 27 | } else { 28 | // BC layer for symfony/config 4.1 and older 29 | $rootNode = $treeBuilder->root('ambta_doctrine_encrypt'); 30 | } 31 | 32 | // Grammar of config tree 33 | $rootNode 34 | ->children() 35 | ->scalarNode('encryptor_class') 36 | ->defaultValue('Halite') 37 | ->end() 38 | ->scalarNode('secret_directory_path') 39 | ->defaultValue('%kernel.project_dir%') 40 | ->end() 41 | ->end(); 42 | 43 | return $treeBuilder; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Encryptors/DefuseEncryptor.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | class DefuseEncryptor implements EncryptorInterface 14 | { 15 | private Filesystem $fs; 16 | private ?string $encryptionKey = null; 17 | private string $keyFile; 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function __construct(string $keyFile) 23 | { 24 | $this->keyFile = $keyFile; 25 | $this->fs = new Filesystem(); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function encrypt(string $data): string 32 | { 33 | return \Defuse\Crypto\Crypto::encryptWithPassword($data, $this->getKey()); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function decrypt(string $data): string 40 | { 41 | return \Defuse\Crypto\Crypto::decryptWithPassword($data, $this->getKey()); 42 | } 43 | 44 | private function getKey(): string 45 | { 46 | if ($this->encryptionKey === null) { 47 | if ($this->fs->exists($this->keyFile)) { 48 | $this->encryptionKey = file_get_contents($this->keyFile); 49 | } else { 50 | $string = random_bytes(255); 51 | $this->encryptionKey = bin2hex($string); 52 | $this->fs->dumpFile($this->keyFile, $this->encryptionKey); 53 | } 54 | } 55 | 56 | return $this->encryptionKey; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Resources/doc/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 1. Download DoctrineEncryptBundle using composer 4 | 2. Enable the database encryption bundle 5 | 3. Configure the database encryption bundle 6 | 7 | ### Requirements 8 | 9 | - PHP >=8.0 10 | - Comes with package: [paragonie/sodium_compat](https://github.com/paragonie/sodium_compat) ^1.5 11 | - Comes with package: [Halite](https://github.com/paragonie/halite) ^3.0 12 | - [doctrine/orm](https://packagist.org/packages/doctrine/orm) >= 2.0 13 | - [symfony/framework-bundle](https://packagist.org/packages/symfony/framework-bundle) >= 2.0 14 | 15 | ### Step 1: Download DoctrineEncryptBundle using composer 16 | 17 | DoctrineEncryptBundle should be installed using [Composer](http://getcomposer.org/): 18 | 19 | ``` js 20 | { 21 | "require": { 22 | "michaeldegroot/doctrine-encrypt-bundle": "3.0.*" 23 | } 24 | } 25 | ``` 26 | 27 | Now tell composer to download the bundle by running the command: 28 | 29 | ``` bash 30 | $ php composer.phar update michaeldegroot/doctrine-encrypt-bundle 31 | ``` 32 | 33 | Composer will install the bundle to your project's `vendor/ambta` directory. 34 | 35 | ### Step 2: Enable the bundle 36 | 37 | Enable the bundle in the Symfony2 kernel by adding it in your /app/AppKernel.php file: 38 | 39 | ``` php 40 | public function registerBundles() 41 | { 42 | $bundles = array( 43 | // ... 44 | new Ambta\DoctrineEncryptBundle\AmbtaDoctrineEncryptBundle(), 45 | ); 46 | } 47 | ``` 48 | 49 | ### Step 3: Set your configuration 50 | 51 | All configuration value's are optional. 52 | On the following page you can find the configuration information. 53 | 54 | #### [Configuration](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/configuration.md) 55 | -------------------------------------------------------------------------------- /src/Encryptors/HaliteEncryptor.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | 17 | class HaliteEncryptor implements EncryptorInterface 18 | { 19 | private ?EncryptionKey $encryptionKey = null; 20 | private string $keyFile; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function __construct(string $keyFile) 26 | { 27 | $this->keyFile = $keyFile; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function encrypt(string $data): string 34 | { 35 | return Crypto::encrypt(new HiddenString($data), $this->getKey()); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function decrypt(string $data): string 42 | { 43 | $data = Crypto::decrypt($data, $this->getKey()); 44 | 45 | if ($data instanceof HiddenString) 46 | { 47 | $data = $data->getString(); 48 | } 49 | 50 | return $data; 51 | } 52 | 53 | private function getKey(): EncryptionKey 54 | { 55 | if ($this->encryptionKey === null) { 56 | try { 57 | $this->encryptionKey = KeyFactory::loadEncryptionKey($this->keyFile); 58 | } catch (CannotPerformOperation $e) { 59 | $this->encryptionKey = KeyFactory::generateEncryptionKey(); 60 | KeyFactory::save($this->encryptionKey, $this->keyFile); 61 | } 62 | } 63 | 64 | return $this->encryptionKey; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/Functional/fixtures/Entity/Owner.php: -------------------------------------------------------------------------------- 1 | id; 47 | } 48 | 49 | public function getSecret() 50 | { 51 | return $this->secret; 52 | } 53 | 54 | public function setSecret($secret) 55 | { 56 | $this->secret = $secret; 57 | } 58 | 59 | /** 60 | * @return mixed 61 | */ 62 | public function getNotSecret() 63 | { 64 | return $this->notSecret; 65 | } 66 | 67 | /** 68 | * @param mixed $notSecret 69 | */ 70 | public function setNotSecret($notSecret) 71 | { 72 | $this->notSecret = $notSecret; 73 | } 74 | 75 | /** 76 | * @return mixed 77 | */ 78 | public function getCascaded() 79 | { 80 | return $this->cascaded; 81 | } 82 | 83 | /** 84 | * @param mixed $cascaded 85 | */ 86 | public function setCascaded($cascaded) 87 | { 88 | $this->cascaded = $cascaded; 89 | } 90 | 91 | 92 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | cache: 4 | directories: 5 | - $HOME/.composer/cache/files 6 | 7 | matrix: 8 | fast_finish: true 9 | include: 10 | # Minimum supported versions 11 | - php: 7.2 12 | env: COMPOSER_FLAGS="--prefer-lowest" 13 | ## with sodium extension installed. 14 | - php: 7.2 15 | env: COMPOSER_FLAGS="--prefer-lowest" 16 | before_install: 17 | - sudo add-apt-repository ppa:ondrej/php -y 18 | - sudo apt-get -qq update 19 | - sudo apt-get install -y libsodium-dev 20 | - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi 21 | - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; 22 | install: 23 | - printf "\n" | pecl install libsodium 24 | - composer update $COMPOSER_FLAGS --prefer-dist --no-interaction 25 | 26 | - php: 7.3 27 | env: COVERAGE=true PHPUNIT_FLAGS="-v --coverage-text" 28 | 29 | - php: 7.4 30 | env: COVERAGE=true PHPUNIT_FLAGS="-v --coverage-text" 31 | 32 | - php: 8.0 33 | env: COVERAGE=true PHPUNIT_FLAGS="-v --coverage-text" 34 | 35 | # Latest commit to master 36 | - php: 7.3 37 | env: STABILITY="dev" 38 | 39 | allow_failures: 40 | # Dev-master is allowed to fail. 41 | - env: STABILITY="dev" 42 | 43 | branches: 44 | only: 45 | - master 46 | - travis 47 | # Build maintenance branches for older releases if needed. such branches should be named like "1.2" 48 | - '/^\d+\.\d+$/' 49 | 50 | before_install: 51 | - if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi 52 | - if ! [ -z "$STABILITY" ]; then composer config minimum-stability ${STABILITY}; fi; 53 | 54 | install: 55 | - composer update $COMPOSER_FLAGS --prefer-dist --no-interaction 56 | 57 | script: 58 | - composer validate --strict --no-check-lock 59 | - vendor/bin/phpunit $PHPUNIT_FLAGS 60 | -------------------------------------------------------------------------------- /src/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ambta_doctrine_attribute_reader: 3 | class: Ambta\DoctrineEncryptBundle\Mapping\AttributeReader 4 | 5 | ambta_doctrine_annotation_reader: 6 | class: Ambta\DoctrineEncryptBundle\Mapping\AttributeAnnotationReader 7 | arguments: ["@ambta_doctrine_attribute_reader", "@annotations.reader"] 8 | 9 | ambta_doctrine_encrypt.orm_subscriber: 10 | class: Ambta\DoctrineEncryptBundle\Subscribers\DoctrineEncryptSubscriber 11 | arguments: ["@ambta_doctrine_annotation_reader", "@ambta_doctrine_encrypt.encryptor"] 12 | tags: 13 | - { name: doctrine.event_subscriber } 14 | 15 | ambta_doctrine_encrypt.subscriber: 16 | alias: ambta_doctrine_encrypt.orm_subscriber 17 | 18 | ambta_doctrine_encrypt.encryptor: 19 | class: "%ambta_doctrine_encrypt.encryptor_class_name%" 20 | arguments: 21 | - "%ambta_doctrine_encrypt.secret_key_path%" 22 | 23 | ambta_doctrine_encrypt.command.decrypt.database: 24 | class: Ambta\DoctrineEncryptBundle\Command\DoctrineDecryptDatabaseCommand 25 | tags: ['console.command'] 26 | arguments: 27 | - "@doctrine.orm.entity_manager" 28 | - "@ambta_doctrine_annotation_reader" 29 | - "@ambta_doctrine_encrypt.subscriber" 30 | 31 | ambta_doctrine_encrypt.command.encrypt.database: 32 | class: Ambta\DoctrineEncryptBundle\Command\DoctrineEncryptDatabaseCommand 33 | tags: ['console.command'] 34 | arguments: 35 | - "@doctrine.orm.entity_manager" 36 | - "@ambta_doctrine_annotation_reader" 37 | - "@ambta_doctrine_encrypt.subscriber" 38 | 39 | ambta_doctrine_encrypt.command.encrypt.status: 40 | class: Ambta\DoctrineEncryptBundle\Command\DoctrineEncryptStatusCommand 41 | tags: ['console.command'] 42 | arguments: 43 | - "@doctrine.orm.entity_manager" 44 | - "@ambta_doctrine_annotation_reader" 45 | - "@ambta_doctrine_encrypt.subscriber" -------------------------------------------------------------------------------- /src/Command/DoctrineEncryptStatusCommand.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Michael Feinbier 14 | */ 15 | class DoctrineEncryptStatusCommand extends AbstractCommand 16 | { 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | protected function configure() 21 | { 22 | $this 23 | ->setName('doctrine:encrypt:status') 24 | ->setDescription('Get status of doctrine encrypt bundle and the database'); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function execute(InputInterface $input, OutputInterface $output): int 31 | { 32 | $metaDataArray = $this->entityManager->getMetadataFactory()->getAllMetadata(); 33 | 34 | $totalCount = 0; 35 | foreach ($metaDataArray as $metaData) { 36 | if ($metaData instanceof ClassMetadataInfo and $metaData->isMappedSuperclass) { 37 | continue; 38 | } 39 | 40 | $count = 0; 41 | $encryptedPropertiesCount = count($this->getEncryptionableProperties($metaData)); 42 | if ($encryptedPropertiesCount > 0) { 43 | $totalCount += $encryptedPropertiesCount; 44 | $count += $encryptedPropertiesCount; 45 | } 46 | 47 | if ($count > 0) { 48 | $output->writeln(sprintf('%s has %d properties which are encrypted.', $metaData->name, $count)); 49 | } else { 50 | $output->writeln(sprintf('%s has no properties which are encrypted.', $metaData->name)); 51 | } 52 | } 53 | 54 | $output->writeln(''); 55 | $output->writeln(sprintf('%d entities found which are containing %d encrypted properties.', count($metaDataArray), $totalCount)); 56 | return 1; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/Unit/DependencyInjection/DoctrineEncryptExtensionTest.php: -------------------------------------------------------------------------------- 1 | extension = new DoctrineEncryptExtension(); 22 | } 23 | 24 | public function testConfigLoadHalite(): void 25 | { 26 | $container = $this->createContainer(); 27 | $this->extension->load([[]], $container); 28 | 29 | $this->assertSame(HaliteEncryptor::class, $container->getParameter('ambta_doctrine_encrypt.encryptor_class_name')); 30 | } 31 | 32 | public function testConfigLoadDefuse(): void 33 | { 34 | $container = $this->createContainer(); 35 | 36 | $config = [ 37 | 'encryptor_class' => 'Defuse', 38 | ]; 39 | $this->extension->load([$config], $container); 40 | 41 | $this->assertSame(DefuseEncryptor::class, $container->getParameter('ambta_doctrine_encrypt.encryptor_class_name')); 42 | } 43 | 44 | public function testConfigLoadCustom(): void 45 | { 46 | $container = $this->createContainer(); 47 | $config = [ 48 | 'encryptor_class' => self::class, 49 | ]; 50 | $this->extension->load([$config], $container); 51 | 52 | $this->markTestSkipped(); 53 | 54 | $this->assertSame(self::class, $container->getParameter('ambta_doctrine_encrypt.encryptor_class_name')); 55 | } 56 | 57 | private function createContainer(): ContainerBuilder 58 | { 59 | $container = new ContainerBuilder( 60 | new ParameterBag(['kernel.debug' => false]) 61 | ); 62 | 63 | return $container; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/Unit/Encryptors/HaliteEncryptorTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('This test only runs when the sodium extension is enabled.'); 16 | } 17 | $keyfile = __DIR__.'/fixtures/halite.key'; 18 | $key = file_get_contents($keyfile); 19 | $halite = new HaliteEncryptor($keyfile); 20 | 21 | $encrypted = $halite->encrypt(self::DATA); 22 | $this->assertNotSame(self::DATA, $encrypted); 23 | $decrypted = $halite->decrypt($encrypted); 24 | 25 | $this->assertSame(self::DATA, $decrypted); 26 | $newkey = file_get_contents($keyfile); 27 | $this->assertSame($key, $newkey, 'The key must not be modified'); 28 | } 29 | 30 | public function testGenerateKey(): void 31 | { 32 | if (! extension_loaded('sodium')) { 33 | $this->markTestSkipped('This test only runs when the sodium extension is enabled.'); 34 | } 35 | $keyfile = sys_get_temp_dir().'/halite-'.md5(time()); 36 | if (file_exists($keyfile)) { 37 | unlink($keyfile); 38 | } 39 | $halite = new HaliteEncryptor($keyfile); 40 | $halite->encrypt(self::DATA); 41 | 42 | $this->assertFileExists($keyfile); 43 | $this->assertNotEmpty(file_get_contents($keyfile), 'A key should have been created and saved to the file'); 44 | 45 | unlink($keyfile); 46 | } 47 | 48 | 49 | public function testEncryptWithoutExtensionThrowsException(): void 50 | { 51 | if (extension_loaded('sodium')) { 52 | $this->markTestSkipped('This only runs when the sodium extension is disabled.'); 53 | } 54 | $keyfile = __DIR__.'/fixtures/halite.key'; 55 | $halite = new HaliteEncryptor($keyfile); 56 | 57 | $this->expectException(\SodiumException::class); 58 | $halite->encrypt(self::DATA); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/DependencyInjection/DoctrineEncryptExtension.php: -------------------------------------------------------------------------------- 1 | 'Ambta\DoctrineEncryptBundle\Encryptors\DefuseEncryptor', 21 | 'Halite' => 'Ambta\DoctrineEncryptBundle\Encryptors\HaliteEncryptor', 22 | ); 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function load(array $configs, ContainerBuilder $container) 28 | { 29 | // Create configuration object 30 | $configuration = new Configuration(); 31 | $config = $this->processConfiguration($configuration, $configs); 32 | 33 | // If empty encryptor class, use Halite encryptor 34 | if (in_array($config['encryptor_class'], array_keys(self::SupportedEncryptorClasses))) { 35 | $config['encryptor_class_full'] = self::SupportedEncryptorClasses[$config['encryptor_class']]; 36 | } else { 37 | $config['encryptor_class_full'] = $config['encryptor_class']; 38 | } 39 | 40 | // Set parameters 41 | $container->setParameter('ambta_doctrine_encrypt.encryptor_class_name', $config['encryptor_class_full']); 42 | $container->setParameter('ambta_doctrine_encrypt.secret_key_path',$config['secret_directory_path'].'/.'.$config['encryptor_class'].'.key'); 43 | 44 | // Load service file 45 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); 46 | $loader->load('services.yml'); 47 | } 48 | 49 | /** 50 | * Get alias for configuration 51 | * 52 | * @return string 53 | */ 54 | public function getAlias(): string 55 | { 56 | return 'ambta_doctrine_encrypt'; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Logo](https://i.imgur.com/sfmU6wt.png)](https://github.com/michaeldegroot/DoctrineEncryptBundle) 2 | 3 | [![Build status](https://travis-ci.org/michaeldegroot/DoctrineEncryptBundle.svg?branch=master)](https://travis-ci.org/michaeldegroot/DoctrineEncryptBundle) 4 | [![License](https://img.shields.io/github/license/michaeldegroot/DoctrineEncryptBundle.svg)](https://raw.githubusercontent.com/michaeldegroot/DoctrineEncryptBundle/master/LICENSE) 5 | [![Latest version](https://poser.pugx.org/michaeldegroot/doctrine-encrypt-bundle/version)](https://packagist.org/packages/michaeldegroot/doctrine-encrypt-bundle) 6 | [![Latest Unstable Version](https://poser.pugx.org/michaeldegroot/doctrine-encrypt-bundle/v/unstable)](https://packagist.org/packages/michaeldegroot/doctrine-encrypt-bundle) 7 | [![Total downloads](https://poser.pugx.org/michaeldegroot/doctrine-encrypt-bundle/downloads)](https://packagist.org/packages/michaeldegroot/doctrine-encrypt-bundle) 8 | [![Downloads this month](https://poser.pugx.org/michaeldegroot/doctrine-encrypt-bundle/d/monthly)](https://packagist.org/packages/michaeldegroot/doctrine-encrypt-bundle) 9 | 10 | ### Introduction 11 | 12 | This is a fork from the original bundle created by ambta which can be found here: 13 | [ambta/DoctrineEncryptBundle](https://github.com/ambta/DoctrineEncryptBundle) 14 | 15 | This bundle has updated security by not rolling it's own encryption and using verified standardized library's from the field. 16 | 17 | ### Using [Halite](https://github.com/paragonie/halite) 18 | 19 | *All deps are already installed with this package* 20 | 21 | ```yml 22 | // Config.yml 23 | ambta_doctrine_encrypt: 24 | encryptor_class: Halite 25 | ``` 26 | 27 | ### Using [Defuse](https://github.com/defuse/php-encryption) 28 | 29 | *You will need to require Defuse yourself* 30 | 31 | `composer require "defuse/php-encryption ^2.0"` 32 | 33 | ```yml 34 | // Config.yml 35 | ambta_doctrine_encrypt: 36 | encryptor_class: Defuse 37 | ``` 38 | 39 | 40 | 41 | ### Secret key 42 | 43 | The secret key should be a max 32 byte hexadecimal string (`[0-9a-fA-F]`). 44 | 45 | Secret key is generated if there is no key found. This is automatically generated and stored in the folder defined in the configuration 46 | 47 | ```yml 48 | // Config.yml 49 | ambta_doctrine_encrypt: 50 | secret_directory_path: '%kernel.project_dir%' # Default value 51 | ``` 52 | 53 | Filename example: `.DefuseEncryptor.key` or `.HaliteEncryptor.key` 54 | 55 | **Do not forget to add these files to your .gitignore file, you do not want this on your repository!** 56 | 57 | ### Documentation 58 | 59 | * [Installation](src/Resources/doc/installation.md) 60 | * [Requirements](src/Resources/doc/installation.md#requirements) 61 | * [Configuration](src/Resources/doc/configuration.md) 62 | * [Usage](src/Resources/doc/usage.md) 63 | * [Console commands](src/Resources/doc/commands.md) 64 | * [Custom encryption class](src/Resources/doc/custom_encryptor.md) 65 | -------------------------------------------------------------------------------- /src/Resources/doc/commands.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | To make your life a little easier we created some commands that you can use for encrypting and decrypting your current database. 4 | 5 | ## 1) Get status 6 | 7 | You can use the comment `doctrine:encrypt:status` to get the current database and encryption information. 8 | 9 | ``` 10 | $ php app/console doctrine:encrypt:status 11 | ``` 12 | 13 | This command will return the amount of entities and the amount of properties with the @Encrypted tag for each entity. 14 | The result will look like this: 15 | 16 | ``` 17 | DoctrineEncrypt\Entity\User has 3 properties which are encrypted. 18 | DoctrineEncrypt\Entity\UserDetail has 13 properties which are encrypted. 19 | 20 | 2 entities found which are containing 16 encrypted properties. 21 | ``` 22 | 23 | ## 2) Encrypt current database 24 | 25 | You can use the comment `doctrine:encrypt:database [encryptor]` to encrypt the current database. 26 | 27 | * Optional parameter [encryptor] 28 | * An encryptor provided by the bundle (Defuse or Halite) or your own [encryption class](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/custom_encryptor.md). 29 | * Default: Your encryptor set in the configuration file or the default encryption class when not set in the configuration file 30 | 31 | ``` 32 | $ php app/console doctrine:encrypt:database 33 | ``` 34 | 35 | or you can provide an encryptor (optional). 36 | 37 | ``` 38 | $ php app/console doctrine:encrypt:database Defuse 39 | ``` 40 | 41 | ``` 42 | $ php app/console doctrine:encrypt:database Halite 43 | ``` 44 | 45 | This command will return the amount of values encrypted in the database. 46 | 47 | ``` 48 | Encryption finished values encrypted: 203 values. 49 | ``` 50 | 51 | 52 | ## 3) Decrypt current database 53 | 54 | You can use the comment `doctrine:decrypt:database [encryptor]` to decrypt the current database. 55 | 56 | * Optional parameter [encryptor] 57 | * An encryptor provided by the bundle (Defuse or Halite) or your own [encryption class](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/custom_encryptor.md). 58 | * Default: Your encryptor set in the configuration file or the default encryption class when not set in the configuration file 59 | 60 | ``` 61 | $ php app/console doctrine:decrypt:database 62 | ``` 63 | 64 | or you can provide an encryptor (optional). 65 | 66 | ``` 67 | $ php app/console doctrine:decrypt:database Defuse 68 | ``` 69 | 70 | ``` 71 | $ php app/console doctrine:decrypt:database Halite 72 | ``` 73 | 74 | This command will return the amount of entities and the amount of values decrypted in the database. 75 | 76 | ``` 77 | Decryption finished entities found: 26, decrypted 195 values. 78 | ``` 79 | 80 | ## Custom encryption class 81 | 82 | You may want to use your own encryption class learn how here: 83 | 84 | #### [Custom encryption class](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/custom_encryptor.md) -------------------------------------------------------------------------------- /src/Mapping/AttributeReader.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * @internal 13 | */ 14 | final class AttributeReader 15 | { 16 | /** @var array */ 17 | private array $isRepeatableAttribute = []; 18 | 19 | /** 20 | * @param ReflectionClass $class 21 | * @return Annotation[] 22 | */ 23 | public function getClassAnnotations(ReflectionClass $class): array 24 | { 25 | return $this->convertToAttributeInstances($class->getAttributes()); 26 | } 27 | 28 | /** 29 | * @phpstan-param class-string $annotationName 30 | * 31 | * @return Annotation|Annotation[]|null 32 | */ 33 | public function getClassAnnotation(ReflectionClass $class, string $annotationName): array|Annotation|null 34 | { 35 | return $this->getClassAnnotations($class)[$annotationName] ?? null; 36 | } 37 | 38 | /** 39 | * @param \ReflectionProperty $property 40 | * @return Annotation[] 41 | */ 42 | public function getPropertyAnnotations(\ReflectionProperty $property): array 43 | { 44 | return $this->convertToAttributeInstances($property->getAttributes()); 45 | } 46 | 47 | /** 48 | * @phpstan-param class-string $annotationName 49 | * 50 | * @return Annotation|Annotation[]|null 51 | */ 52 | public function getPropertyAnnotation(\ReflectionProperty $property, string $annotationName): array|Annotation|null 53 | { 54 | return $this->getPropertyAnnotations($property)[$annotationName] ?? null; 55 | } 56 | 57 | /** 58 | * @param array<\ReflectionAttribute> $attributes 59 | * 60 | * @return array 61 | */ 62 | private function convertToAttributeInstances(array $attributes): array 63 | { 64 | $instances = []; 65 | 66 | foreach ($attributes as $attribute) { 67 | $attributeName = $attribute->getName(); 68 | assert(is_string($attributeName)); 69 | // Make sure we only get Gedmo Annotations 70 | if (!is_subclass_of($attributeName, Annotation::class)) { 71 | continue; 72 | } 73 | 74 | $instance = $attribute->newInstance(); 75 | assert($instance instanceof Annotation); 76 | 77 | if ($this->isRepeatable($attributeName)) { 78 | if (!isset($instances[$attributeName])) { 79 | $instances[$attributeName] = []; 80 | } 81 | 82 | $instances[$attributeName][] = $instance; 83 | } else { 84 | $instances[$attributeName] = $instance; 85 | } 86 | } 87 | 88 | return $instances; 89 | } 90 | 91 | private function isRepeatable(string $attributeClassName): bool 92 | { 93 | if (isset($this->isRepeatableAttribute[$attributeClassName])) { 94 | return $this->isRepeatableAttribute[$attributeClassName]; 95 | } 96 | 97 | $reflectionClass = new ReflectionClass($attributeClassName); 98 | $attribute = $reflectionClass->getAttributes()[0]->newInstance(); 99 | 100 | return $this->isRepeatableAttribute[$attributeClassName] = ($attribute->flags & Attribute::IS_REPEATABLE) > 0; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Mapping/AttributeAnnotationReader.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * @internal 14 | */ 15 | final class AttributeAnnotationReader implements Reader 16 | { 17 | /** 18 | * @var Reader 19 | */ 20 | private Reader $annotationReader; 21 | 22 | /** 23 | * @var AttributeReader 24 | */ 25 | private AttributeReader $attributeReader; 26 | 27 | public function __construct(AttributeReader $attributeReader, Reader $annotationReader) 28 | { 29 | $this->attributeReader = $attributeReader; 30 | $this->annotationReader = $annotationReader; 31 | } 32 | 33 | /** 34 | * @return Annotation[] 35 | */ 36 | public function getClassAnnotations(ReflectionClass $class): array 37 | { 38 | $annotations = $this->attributeReader->getClassAnnotations($class); 39 | 40 | if ([] !== $annotations) { 41 | return $annotations; 42 | } 43 | 44 | return $this->annotationReader->getClassAnnotations($class); 45 | } 46 | 47 | /** 48 | * @param class-string $annotationName the name of the annotation 49 | * 50 | * @return T|null the Annotation or NULL, if the requested annotation does not exist 51 | * 52 | * @template T 53 | */ 54 | public function getClassAnnotation(ReflectionClass $class, $annotationName) 55 | { 56 | $annotation = $this->attributeReader->getClassAnnotation($class, $annotationName); 57 | 58 | if (null !== $annotation) { 59 | return $annotation; 60 | } 61 | 62 | return $this->annotationReader->getClassAnnotation($class, $annotationName); 63 | } 64 | 65 | /** 66 | * @return Annotation[] 67 | */ 68 | public function getPropertyAnnotations(\ReflectionProperty $property): array 69 | { 70 | $propertyAnnotations = $this->attributeReader->getPropertyAnnotations($property); 71 | 72 | if ([] !== $propertyAnnotations) { 73 | return $propertyAnnotations; 74 | } 75 | 76 | return $this->annotationReader->getPropertyAnnotations($property); 77 | } 78 | 79 | /** 80 | * @param class-string $annotationName the name of the annotation 81 | * 82 | * @return T|null the Annotation or NULL, if the requested annotation does not exist 83 | * 84 | * @template T 85 | */ 86 | public function getPropertyAnnotation(\ReflectionProperty $property, $annotationName) 87 | { 88 | $annotation = $this->attributeReader->getPropertyAnnotation($property, $annotationName); 89 | 90 | if (null !== $annotation) { 91 | return $annotation; 92 | } 93 | 94 | return $this->annotationReader->getPropertyAnnotation($property, $annotationName); 95 | } 96 | 97 | public function getMethodAnnotations(ReflectionMethod $method): array 98 | { 99 | throw new \BadMethodCallException('Not implemented'); 100 | } 101 | 102 | /** 103 | * @param ReflectionMethod $method 104 | * @param $annotationName 105 | * @return mixed 106 | */ 107 | public function getMethodAnnotation(ReflectionMethod $method, $annotationName): mixed 108 | { 109 | throw new \BadMethodCallException('Not implemented'); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Tests/Functional/BasicQueryTest/AbstractBasicQueryTestCase.php: -------------------------------------------------------------------------------- 1 | setNotSecret('My public information'); 15 | $user->setSecret('top secret information'); 16 | $this->entityManager->persist($user); 17 | $this->entityManager->flush(); 18 | 19 | // Start transaction; insert; commit 20 | $this->assertEquals('top secret information',$user->getSecret()); 21 | $this->assertEquals(3,$this->getCurrentQueryCount()); 22 | } 23 | 24 | public function testNoUpdateOnReadEncrypted(): void 25 | { 26 | $this->entityManager->beginTransaction(); 27 | $this->assertEquals(1,$this->getCurrentQueryCount()); 28 | 29 | $user = new CascadeTarget(); 30 | $user->setNotSecret('My public information'); 31 | $user->setSecret('top secret information'); 32 | $this->entityManager->persist($user); 33 | $this->entityManager->flush(); 34 | $this->assertEquals(2,$this->getCurrentQueryCount()); 35 | 36 | // Test if no query is executed when doing nothing 37 | $this->entityManager->flush(); 38 | $this->assertEquals(2,$this->getCurrentQueryCount()); 39 | 40 | // Test if no query is executed when reading unrelated field 41 | $user->getNotSecret(); 42 | $this->entityManager->flush(); 43 | $this->assertEquals(2,$this->getCurrentQueryCount()); 44 | 45 | // Test if no query is executed when reading related field and if field is valid 46 | $this->assertEquals('top secret information',$user->getSecret()); 47 | $this->entityManager->flush(); 48 | $this->assertEquals(2,$this->getCurrentQueryCount()); 49 | 50 | // Test if 1 query is executed when updating entity 51 | $user->setSecret('top secret information change'); 52 | $this->entityManager->flush(); 53 | $this->assertEquals(3,$this->getCurrentQueryCount()); 54 | $this->assertEquals('top secret information change',$user->getSecret()); 55 | 56 | $this->entityManager->rollback(); 57 | $this->assertEquals(4,$this->getCurrentQueryCount()); 58 | } 59 | 60 | public function testStoredDataIsEncrypted(): void 61 | { 62 | $user = new CascadeTarget(); 63 | $user->setNotSecret('My public information'); 64 | $user->setSecret('my secret'); 65 | $this->entityManager->persist($user); 66 | $this->entityManager->flush(); 67 | 68 | $queryData = $this->getLatestInsertQuery(); 69 | $params = array_values($queryData['params']); 70 | $passwordData = $params[0] === 'My public information' ? $params[1] : $params[0]; 71 | 72 | $this->assertStringEndsWith(DoctrineEncryptSubscriber::ENCRYPTION_MARKER,$passwordData); 73 | $this->assertStringDoesNotContain('my secret',$passwordData); 74 | 75 | $user->setSecret('my secret has changed'); 76 | $this->entityManager->flush(); 77 | 78 | $queryData = $this->getLatestUpdateQuery(); 79 | $passwordData = array_values($queryData['params'])[0]; 80 | 81 | $this->assertStringEndsWith(DoctrineEncryptSubscriber::ENCRYPTION_MARKER,$passwordData); 82 | $this->assertStringDoesNotContain('my secret',$passwordData); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Resources/doc/example_of_usage.md: -------------------------------------------------------------------------------- 1 | # Example Of Usage 2 | 3 | ```php 4 | namespace Acme\DemoBundle\Entity; 5 | 6 | use Doctrine\ORM\Mapping as ORM; 7 | 8 | // importing @Encrypted annotation 9 | use Ambta\DoctrineEncryptBundle\Configuration\Encrypted; 10 | 11 | /** 12 | * @ORM\Entity 13 | * @ORM\Table(name="user_v") 14 | */ 15 | class UserV { 16 | 17 | /** 18 | * @ORM\Id 19 | * @ORM\GeneratedValue(strategy="AUTO") 20 | * @ORM\Column(type="integer") 21 | * @var int 22 | */ 23 | private $id; 24 | 25 | /** 26 | * @ORM\Column(type="text", name="total_money") 27 | * @Encrypted 28 | * @var int 29 | */ 30 | private $totalMoney; 31 | 32 | /** 33 | * @ORM\Column(type="string", length=100, name="first_name") 34 | * @var string 35 | */ 36 | private $firstName; 37 | 38 | /** 39 | * @ORM\Column(type="string", length=100, name="last_name") 40 | * @var string 41 | */ 42 | private $lastName; 43 | 44 | /** 45 | * @ORM\Column(type="text", name="credit_card_number") 46 | * @Encrypted 47 | * @var string 48 | */ 49 | private $creditCardNumber; 50 | 51 | //common getters/setters here... 52 | 53 | } 54 | ``` 55 | 56 | ### Fixtures 57 | 58 | ```php 59 | 60 | namespace Acme\DemoBundle\DataFixtures\ORM; 61 | 62 | use Doctrine\Common\Persistence\ObjectManager; 63 | use Doctrine\Common\DataFixtures\FixtureInterface; 64 | use Acme\DemoBundle\Entity\UserV; 65 | 66 | class LoadUserData implements FixtureInterface 67 | { 68 | public function load(ObjectManager $manager) 69 | { 70 | $user = new UserV(); 71 | $user->setFirstName('Victor'); 72 | $user->setLastName('Melnik'); 73 | $user->setTotalMoney(20); 74 | $user->setCreditCardNumber('1234567890'); 75 | 76 | $manager->persist($user); 77 | $manager->flush(); 78 | } 79 | } 80 | ``` 81 | 82 | ### Controller 83 | 84 | ```php 85 | 86 | namespace Acme\DemoBundle\Controller; 87 | 88 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 89 | 90 | // these import the "@Route" and "@Template" annotations 91 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 92 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; 93 | 94 | // our entity 95 | use Acme\DemoBundle\Entity\UserV; 96 | 97 | class DemoController extends Controller 98 | { 99 | /** 100 | * @Route("/show-user/{id}", name="_ambta_decrypt_test", requirements={"id" = "\d+"}) 101 | * @Template 102 | */ 103 | public function getUserAction(UserV $user) {} 104 | } 105 | ``` 106 | 107 | ### Template 108 | 109 | ```twig 110 |
Common info: {{ user.lastName ~ ' ' ~ user.firstName }}
111 |
112 | Decoded info: 113 |
114 |
Total money
115 |
{{ user.totalMoney }}
116 |
Credit card
117 |
{{ user.creditCardNumber }}
118 |
119 |
120 | ``` 121 | 122 | When we follow link /show-user/{x}, where x - id of our user in DB, we will see that 123 | user's information is decoded and in the same time information in database will 124 | be encrypted. In database we'll have something like this: 125 | 126 | ``` 127 | id | 1 128 | total_money | def50200100cd243434bc5fbbe5ecc87c153cda9d62e4c2f5ffb27c29b37df0cacd6d4a4b51408b3cefa950ea6b7ed22ab3b98344c8723f5ccee9c6d0aca8f48169c175bbdaba96d8c8106f1132ba5774954434a030df00771 129 | first_name | Victor 130 | last_name | Melnik 131 | credit_card_number | def50200af8d084c22099d29b3940334de4c5c57df8517934dfd567e2d04f9a16a60e455690ab5e118ad007054845351df31a9d9370fdfac97ebdeb3e9589e3a1c094202e715c5c1607acb24667a1a3981e2fa626058a8d8 132 | ``` 133 | 134 | So our information is encrypted, and unless someone has your .DefuseEncryptor.key file they cannot access this information. 135 | 136 | ### Requirements 137 | 138 | You need `DoctrineFixturesBundle` and `defuse/php-encryption` extension for this example 139 | 140 | #### [Back to index](https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/src/Resources/doc/index.md) 141 | -------------------------------------------------------------------------------- /src/Command/AbstractCommand.php: -------------------------------------------------------------------------------- 1 | 15 | **/ 16 | abstract class AbstractCommand extends Command 17 | { 18 | /** 19 | * @var EntityManagerInterface 20 | */ 21 | protected EntityManagerInterface|EntityManager $entityManager; 22 | 23 | /** 24 | * @var DoctrineEncryptSubscriber 25 | */ 26 | protected DoctrineEncryptSubscriber $subscriber; 27 | 28 | /** 29 | * @var Reader 30 | */ 31 | protected Reader $annotationReader; 32 | 33 | /** 34 | * AbstractCommand constructor. 35 | * 36 | * @param EntityManager $entityManager 37 | * @param Reader $annotationReader 38 | * @param DoctrineEncryptSubscriber $subscriber 39 | */ 40 | public function __construct( 41 | EntityManagerInterface $entityManager, 42 | Reader $annotationReader, 43 | DoctrineEncryptSubscriber $subscriber 44 | ) { 45 | parent::__construct(); 46 | $this->entityManager = $entityManager; 47 | $this->annotationReader = $annotationReader; 48 | $this->subscriber = $subscriber; 49 | } 50 | 51 | /** 52 | * Get an result iterator over the whole table of an entity. 53 | * 54 | * @param string $entityName 55 | * @return iterable|array 56 | */ 57 | protected function getEntityIterator(string $entityName): iterable 58 | { 59 | $query = $this->entityManager->createQuery(sprintf('SELECT o FROM %s o', $entityName)); 60 | 61 | return $query->toIterable(); 62 | } 63 | 64 | /** 65 | * Get the number of rows in an entity-table 66 | * 67 | * @param string $entityName 68 | * 69 | * @return int 70 | */ 71 | protected function getTableCount(string $entityName): int 72 | { 73 | $query = $this->entityManager->createQuery(sprintf('SELECT COUNT(o) FROM %s o', $entityName)); 74 | 75 | return (int) $query->getSingleScalarResult(); 76 | } 77 | 78 | /** 79 | * Return an array of entity-metadata for all entities 80 | * that have at least one encrypted property. 81 | * 82 | * @return array 83 | */ 84 | protected function getEncryptionableEntityMetaData(): array 85 | { 86 | $validMetaData = []; 87 | $metaDataArray = $this->entityManager->getMetadataFactory()->getAllMetadata(); 88 | 89 | foreach ($metaDataArray as $entityMetaData) 90 | { 91 | if ($entityMetaData instanceof ClassMetadataInfo and $entityMetaData->isMappedSuperclass) { 92 | continue; 93 | } 94 | 95 | $properties = $this->getEncryptionableProperties($entityMetaData); 96 | if (count($properties) == 0) { 97 | continue; 98 | } 99 | 100 | $validMetaData[] = $entityMetaData; 101 | } 102 | 103 | return $validMetaData; 104 | } 105 | 106 | /** 107 | * @param $entityMetaData 108 | * 109 | * @return array 110 | */ 111 | protected function getEncryptionableProperties($entityMetaData): array 112 | { 113 | //Create reflectionClass for each meta data object 114 | $reflectionClass = new \ReflectionClass($entityMetaData->name); 115 | $propertyArray = $reflectionClass->getProperties(); 116 | $properties = []; 117 | 118 | foreach ($propertyArray as $property) { 119 | if ($this->annotationReader->getPropertyAnnotation($property, 'Ambta\DoctrineEncryptBundle\Configuration\Encrypted')) { 120 | $properties[] = $property; 121 | } 122 | } 123 | 124 | return $properties; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":{"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testPersistEntity":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testNoUpdateOnReadEncrypted":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testStoredDataIsEncrypted":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testPersistEntity":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testNoUpdateOnReadEncrypted":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testStoredDataIsEncrypted":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\DependencyInjection\\DoctrineEncryptExtensionTest::testConfigLoadCustom":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testEncryptWithoutExtensionThrowsException":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\DefuseEncryptorTest::testEncrypt":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\DefuseEncryptorTest::testGenerateKey":4,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testEncryptExtension":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testGenerateKey":1,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsNoEncryptor":4},"times":{"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testPersistEntity":0.456,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testNoUpdateOnReadEncrypted":1.046,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryDefuseTest::testStoredDataIsEncrypted":0.654,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testPersistEntity":0.106,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testNoUpdateOnReadEncrypted":0.142,"Ambta\\DoctrineEncryptBundle\\Tests\\Functional\\BasicQueryTest\\BasicQueryHaliteTest::testStoredDataIsEncrypted":0.126,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\DependencyInjection\\DoctrineEncryptExtensionTest::testConfigLoadHalite":0.031,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\DependencyInjection\\DoctrineEncryptExtensionTest::testConfigLoadDefuse":0.012,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\DependencyInjection\\DoctrineEncryptExtensionTest::testConfigLoadCustom":0.013,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\DefuseEncryptorTest::testEncrypt":0.252,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\DefuseEncryptorTest::testGenerateKey":0.132,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testEncryptExtension":0.005,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testGenerateKey":0.008,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Encryptors\\HaliteEncryptorTest::testEncryptWithoutExtensionThrowsException":0.001,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testSetRestorEncryptor":0.01,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsEncrypt":0.003,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsEncryptExtend":0.003,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsEncryptEmbedded":0.004,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsEncryptNull":0.002,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsNoEncryptor":0.001,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecrypt":0.002,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecryptExtended":0.003,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecryptEmbedded":0.003,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecryptNull":0.002,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testProcessFieldsDecryptNonEncrypted":0.002,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testOnFlush":0.052,"Ambta\\DoctrineEncryptBundle\\Tests\\Unit\\Subscribers\\DoctrineEncryptSubscriberTest::testPostFlush":0.006}} -------------------------------------------------------------------------------- /src/Command/DoctrineEncryptDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Michael Feinbier 17 | */ 18 | class DoctrineEncryptDatabaseCommand extends AbstractCommand 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function configure() 24 | { 25 | $this 26 | ->setName('doctrine:encrypt:database') 27 | ->setDescription('Encrypt whole database on tables which are not encrypted yet') 28 | ->addArgument('encryptor', InputArgument::OPTIONAL, 'The encryptor you want to decrypt the database with') 29 | ->addArgument('batchSize', InputArgument::OPTIONAL, 'The update/flush batch size', 20); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function execute(InputInterface $input, OutputInterface $output): int 36 | { 37 | // Get entity manager, question helper, subscriber service and annotation reader 38 | $question = $this->getHelper('question'); 39 | $batchSize = $input->getArgument('batchSize'); 40 | 41 | // Get list of supported encryptors 42 | $supportedExtensions = DoctrineEncryptExtension::SupportedEncryptorClasses; 43 | 44 | // If encryptor has been set use that encryptor else use default 45 | if ($input->getArgument('encryptor')) { 46 | if (isset($supportedExtensions[$input->getArgument('encryptor')])) { 47 | $reflection = new \ReflectionClass($supportedExtensions[$input->getArgument('encryptor')]); 48 | $encryptor = $reflection->newInstance(); 49 | $this->subscriber->setEncryptor($encryptor); 50 | } else { 51 | if (class_exists($input->getArgument('encryptor'))) { 52 | $this->subscriber->setEncryptor($input->getArgument('encryptor')); 53 | } else { 54 | $output->writeln('Given encryptor does not exists'); 55 | 56 | return $output->writeln('Supported encryptors: ' . implode(', ', array_keys($supportedExtensions))); 57 | } 58 | } 59 | } 60 | 61 | // Get entity manager metadata 62 | $metaDataArray = $this->getEncryptionableEntityMetaData(); 63 | $confirmationQuestion = new ConfirmationQuestion( 64 | '' . count($metaDataArray) . ' entities found which are containing properties with the encryption tag.' . PHP_EOL . '' . 65 | 'Which are going to be encrypted with [' . get_class($this->subscriber->getEncryptor()) . ']. ' . PHP_EOL . ''. 66 | 'Wrong settings can mess up your data and it will be unrecoverable. ' . PHP_EOL . '' . 67 | 'I advise you to make a backup. ' . PHP_EOL . '' . 68 | 'Continue with this action? (y/yes)', false 69 | ); 70 | 71 | if (!$question->ask($input, $output, $confirmationQuestion)) { 72 | return 1; 73 | } 74 | 75 | // Start decrypting database 76 | $output->writeln('' . PHP_EOL . 'Encrypting all fields can take up to several minutes depending on the database size.'); 77 | 78 | // Loop through entity manager meta data 79 | foreach ($metaDataArray as $metaData) { 80 | $i = 0; 81 | $iterator = $this->getEntityIterator($metaData->name); 82 | $totalCount = $this->getTableCount($metaData->name); 83 | 84 | $output->writeln(sprintf('Processing %s', $metaData->name)); 85 | $progressBar = new ProgressBar($output, $totalCount); 86 | foreach ($iterator as $row) { 87 | $this->subscriber->processFields((is_array($row) ? $row[0] : $row)); 88 | 89 | if (($i % $batchSize) === 0) { 90 | $this->entityManager->flush(); 91 | $this->entityManager->clear(); 92 | $progressBar->advance($batchSize); 93 | } 94 | $i++; 95 | } 96 | 97 | $progressBar->finish(); 98 | $output->writeln(''); 99 | $this->entityManager->flush(); 100 | } 101 | 102 | // Say it is finished 103 | $output->writeln('Encryption finished. Values encrypted: ' . $this->subscriber->encryptCounter . ' values.' . PHP_EOL . 'All values are now encrypted.'); 104 | return 1; 105 | } 106 | 107 | 108 | } 109 | -------------------------------------------------------------------------------- /Tests/Functional/AbstractFunctionalTestCase.php: -------------------------------------------------------------------------------- 1 | dbFile = tempnam(sys_get_temp_dir(), 'amb_db'); 53 | $conn = array( 54 | 'driver' => 'pdo_sqlite', 55 | 'path' => $this->dbFile, 56 | ); 57 | 58 | // obtaining the entity manager 59 | $this->entityManager = EntityManager::create($conn, $config); 60 | 61 | $schemaTool = new SchemaTool($this->entityManager); 62 | $classes = $this->entityManager->getMetadataFactory()->getAllMetadata(); 63 | $schemaTool->dropSchema($classes); 64 | $schemaTool->createSchema($classes); 65 | 66 | $this->sqlLoggerStack = new DebugStack(); 67 | $this->entityManager->getConnection()->getConfiguration()->setSQLLogger($this->sqlLoggerStack); 68 | 69 | $this->encryptor = $this->getEncryptor(); 70 | $this->subscriber = new DoctrineEncryptSubscriber(new AnnotationReader(),$this->encryptor); 71 | $this->entityManager->getEventManager()->addEventSubscriber($this->subscriber); 72 | 73 | error_reporting(E_ALL); 74 | } 75 | 76 | public function tearDown(): void 77 | { 78 | $this->entityManager->getConnection()->close(); 79 | unlink($this->dbFile); 80 | } 81 | 82 | protected function getLatestInsertQuery(): ?array 83 | { 84 | $insertQueries = array_values(array_filter($this->sqlLoggerStack->queries, static function ($queryData) { 85 | return stripos($queryData['sql'], 'INSERT ') === 0; 86 | })); 87 | 88 | return current(array_reverse($insertQueries)) ?: null; 89 | } 90 | 91 | protected function getLatestUpdateQuery(): ?array 92 | { 93 | $insertQueries = array_values(array_filter($this->sqlLoggerStack->queries,static function ($queryData) { 94 | return stripos($queryData['sql'], 'UPDATE ') === 0; 95 | })); 96 | 97 | return current(array_reverse($insertQueries)) ?: null; 98 | } 99 | 100 | /** 101 | * Using the SQL Logger Stack this method retrieves the current query count executed in this test. 102 | */ 103 | protected function getCurrentQueryCount(): int 104 | { 105 | return count($this->sqlLoggerStack->queries); 106 | } 107 | 108 | /** 109 | * Asserts that a string starts with a given prefix. 110 | * 111 | * @param string $stringn 112 | * @param string $string 113 | * @param string $message 114 | */ 115 | public function assertStringDoesNotContain($needle, $string, $ignoreCase = false, $message = ''): void 116 | { 117 | if (!\is_string($needle)) { 118 | throw InvalidArgumentHelper::factory(1, 'string'); 119 | } 120 | 121 | if (!\is_string($string)) { 122 | throw InvalidArgumentHelper::factory(2, 'string'); 123 | } 124 | 125 | if (!\is_bool($ignoreCase)) { 126 | throw InvalidArgumentHelper::factory(3, 'bool'); 127 | } 128 | 129 | $constraint = new LogicalNot(new StringContains( 130 | $needle, 131 | $ignoreCase 132 | )); 133 | 134 | static::assertThat($string, $constraint, $message); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Command/DoctrineDecryptDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Michael Feinbier 18 | */ 19 | class DoctrineDecryptDatabaseCommand extends AbstractCommand 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function configure() 25 | { 26 | $this 27 | ->setName('doctrine:decrypt:database') 28 | ->setDescription('Decrypt whole database on tables which are encrypted') 29 | ->addArgument('encryptor', InputArgument::OPTIONAL, 'The encryptor you want to decrypt the database with') 30 | ->addArgument('batchSize', InputArgument::OPTIONAL, 'The update/flush batch size', 20); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function execute(InputInterface $input, OutputInterface $output): int 37 | { 38 | // Get entity manager, question helper, subscriber service and annotation reader 39 | $question = $this->getHelper('question'); 40 | 41 | // Get list of supported encryptors 42 | $supportedExtensions = DoctrineEncryptExtension::SupportedEncryptorClasses; 43 | $batchSize = $input->getArgument('batchSize'); 44 | 45 | // If encryptor has been set use that encryptor else use default 46 | if ($input->getArgument('encryptor')) { 47 | if (isset($supportedExtensions[$input->getArgument('encryptor')])) { 48 | $reflection = new \ReflectionClass($supportedExtensions[$input->getArgument('encryptor')]); 49 | $encryptor = $reflection->newInstance(); 50 | $this->subscriber->setEncryptor($encryptor); 51 | } else { 52 | if (class_exists($input->getArgument('encryptor'))) { 53 | $this->subscriber->setEncryptor($input->getArgument('encryptor')); 54 | } else { 55 | $output->writeln('Given encryptor does not exists'); 56 | 57 | return $output->writeln('Supported encryptors: ' . implode(', ', array_keys($supportedExtensions))); 58 | } 59 | } 60 | } 61 | 62 | // Get entity manager metadata 63 | $metaDataArray = $this->entityManager->getMetadataFactory()->getAllMetadata(); 64 | 65 | // Set counter and loop through entity manager meta data 66 | $propertyCount = 0; 67 | foreach ($metaDataArray as $metaData) { 68 | if ($metaData instanceof ClassMetadataInfo and $metaData->isMappedSuperclass) { 69 | continue; 70 | } 71 | 72 | $countProperties = count($this->getEncryptionableProperties($metaData)); 73 | $propertyCount += $countProperties; 74 | } 75 | 76 | $confirmationQuestion = new ConfirmationQuestion( 77 | '' . count($metaDataArray) . ' entities found which are containing ' . $propertyCount . ' properties with the encryption tag. ' . PHP_EOL . '' . 78 | 'Which are going to be decrypted with [' . get_class($this->subscriber->getEncryptor()) . ']. ' . PHP_EOL . '' . 79 | 'Wrong settings can mess up your data and it will be unrecoverable. ' . PHP_EOL . '' . 80 | 'I advise you to make a backup. ' . PHP_EOL . '' . 81 | 'Continue with this action? (y/yes)', false 82 | ); 83 | 84 | if (!$question->ask($input, $output, $confirmationQuestion)) { 85 | return 1; 86 | } 87 | 88 | // Start decrypting database 89 | $output->writeln('' . PHP_EOL . 'Decrypting all fields. This can take up to several minutes depending on the database size.'); 90 | 91 | $valueCounter = 0; 92 | 93 | // Loop through entity manager meta data 94 | foreach ($this->getEncryptionableEntityMetaData() as $metaData) { 95 | $i = 0; 96 | $iterator = $this->getEntityIterator($metaData->name); 97 | $totalCount = $this->getTableCount($metaData->name); 98 | 99 | $output->writeln(sprintf('Processing %s', $metaData->name)); 100 | $progressBar = new ProgressBar($output, $totalCount); 101 | foreach ($iterator as $row) { 102 | $entity = $row[0]; 103 | 104 | // Create reflectionClass for each entity 105 | $entityReflectionClass = new \ReflectionClass($entity); 106 | 107 | //Get the current encryptor used 108 | $encryptorUsed = $this->subscriber->getEncryptor(); 109 | 110 | //Loop through the property's in the entity 111 | foreach ($this->getEncryptionableProperties($metaData) as $property) { 112 | $methodeName = ucfirst($property->getName()); 113 | 114 | $getter = 'get' . $methodeName; 115 | $setter = 'set' . $methodeName; 116 | 117 | //Check if getter and setter are set 118 | if ($entityReflectionClass->hasMethod($getter) && $entityReflectionClass->hasMethod($setter)) { 119 | $unencrypted = $entity->$getter(); 120 | $entity->$setter($unencrypted); 121 | $valueCounter++; 122 | } 123 | } 124 | 125 | $this->subscriber->setEncryptor(null); 126 | $this->entityManager->persist($entity); 127 | 128 | if (($i % $batchSize) === 0) { 129 | $this->entityManager->flush(); 130 | $this->entityManager->clear(); 131 | } 132 | $progressBar->advance(1); 133 | $i++; 134 | 135 | $this->subscriber->setEncryptor($encryptorUsed); 136 | } 137 | 138 | 139 | $progressBar->finish(); 140 | $output->writeln(''); 141 | $encryptorUsed = $this->subscriber->getEncryptor(); 142 | $this->subscriber->setEncryptor(null); 143 | $this->entityManager->flush(); 144 | $this->entityManager->clear(); 145 | $this->subscriber->setEncryptor($encryptorUsed); 146 | } 147 | 148 | $output->writeln('' . PHP_EOL . 'Decryption finished values found: ' . $valueCounter . ', decrypted: ' . $this->subscriber->decryptCounter . '.' . PHP_EOL . 'All values are now decrypted.'); 149 | return 1; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Tests/Functional/DoctrineEncryptSubscriber/AbstractDoctrineEncryptSubscriberTestCase.php: -------------------------------------------------------------------------------- 1 | entityManager; 19 | $owner = new Owner(); 20 | $owner->setSecret($secret); 21 | $owner->setNotSecret($notSecret); 22 | $em->persist($owner); 23 | $em->flush(); 24 | $em->clear(); 25 | unset($owner); 26 | 27 | $connection = $em->getConnection(); 28 | $stmt = $connection->prepare('SELECT * from owner WHERE id = ?'); 29 | $owners = $em->getRepository(Owner::class)->findAll(); 30 | $this->assertCount(1, $owners); 31 | /** @var Owner $owner */ 32 | $owner = $owners[0]; 33 | $this->assertEquals($secret, $owner->getSecret()); 34 | $this->assertEquals($notSecret, $owner->getNotSecret()); 35 | $stmt->bindValue(1, $owner->getId()); 36 | $stmt->execute(); 37 | $results = $stmt->fetchAll(); 38 | $this->assertCount(1, $results); 39 | $result = $results[0]; 40 | $this->assertEquals($notSecret, $result['notSecret']); 41 | $this->assertNotEquals($secret, $result['secret']); 42 | $this->assertStringEndsWith('', $result['secret']); 43 | $decrypted = $this->encryptor->decrypt(str_replace('', '', $result['secret'])); 44 | $this->assertEquals($secret, $decrypted); 45 | } 46 | 47 | public function testEncryptionCascades(): void 48 | { 49 | $secret = "It's a secret"; 50 | $notSecret = "You're all welcome to know this."; 51 | $em = $this->entityManager; 52 | $owner = new Owner(); 53 | $em->persist($owner); // persist cascades 54 | $em->flush(); 55 | 56 | $cascadeTarget = new CascadeTarget(); 57 | $cascadeTarget->setSecret($secret); 58 | $cascadeTarget->setNotSecret($notSecret); 59 | $owner->setCascaded($cascadeTarget); 60 | $em->flush(); 61 | $em->clear(); 62 | unset($owner); 63 | unset($cascadeTarget); 64 | 65 | $connection = $em->getConnection(); 66 | $stmt = $connection->prepare('SELECT * from cascadeTarget WHERE id = ?'); 67 | $cascadeTargets = $em->getRepository(CascadeTarget::class)->findAll(); 68 | $this->assertCount(1, $cascadeTargets); 69 | /** @var CascadeTarget $cascadeTarget */ 70 | $cascadeTarget = $cascadeTargets[0]; 71 | $this->assertEquals($secret, $cascadeTarget->getSecret()); 72 | $this->assertEquals($notSecret, $cascadeTarget->getNotSecret()); 73 | $stmt->bindValue(1, $cascadeTarget->getId()); 74 | $stmt->execute(); 75 | $results = $stmt->fetchAll(); 76 | $this->assertCount(1, $results); 77 | $result = $results[0]; 78 | $this->assertEquals($notSecret, $result['notSecret']); 79 | $this->assertNotEquals($secret, $result['secret']); 80 | $this->assertStringEndsWith('', $result['secret']); 81 | $decrypted = $this->encryptor->decrypt(str_replace('', '', $result['secret'])); 82 | $this->assertEquals($secret, $decrypted); 83 | } 84 | 85 | 86 | /** 87 | * @throws \Doctrine\DBAL\DBALException 88 | * @throws \Doctrine\ORM\OptimisticLockException 89 | */ 90 | public function testEncryptionDoesNotHappenWhenThereIsNoChange(): void 91 | { 92 | $secret = "It's a secret"; 93 | $notSecret = "You're all welcome to know this."; 94 | $em = $this->entityManager; 95 | $owner1 = new Owner(); 96 | $owner1->setSecret($secret); 97 | $owner1->setNotSecret($notSecret); 98 | $em->persist($owner1); 99 | $owner2 = new Owner(); 100 | $owner2->setSecret($secret); 101 | $owner2->setNotSecret($notSecret); 102 | $em->persist($owner2); 103 | 104 | $em->flush(); 105 | $em->clear(); 106 | $owner1Id = $owner1->getId(); 107 | unset($owner1); 108 | unset($owner2); 109 | 110 | // test that it was encrypted correctly 111 | $connection = $em->getConnection(); 112 | $stmt = $connection->prepare('SELECT * from owner WHERE id = ?'); 113 | $stmt->bindValue(1, $owner1Id); 114 | $stmt->execute(); 115 | $results = $stmt->fetchAll(); 116 | $this->assertCount(1, $results); 117 | $result = $results[0]; 118 | $originalEncryption = $result['secret']; 119 | $this->assertStringEndsWith('', $originalEncryption); // is encrypted 120 | 121 | $owners = $em->getRepository(Owner::class)->findAll(); 122 | /** @var Owner $owner */ 123 | foreach ($owners as $owner) { 124 | $this->assertEquals($secret, $owner->getSecret()); 125 | $this->assertEquals($notSecret, $owner->getNotSecret()); 126 | } 127 | $stack = new DebugStack(); 128 | $connection->getConfiguration()->setSQLLogger($stack); 129 | $this->assertCount(0, $stack->queries); 130 | $beforeFlush = $this->subscriber->encryptCounter; 131 | $em->flush(); 132 | $afterFlush = $this->subscriber->encryptCounter; 133 | // No encryption should have happened because we didn't change anything. 134 | $this->assertEquals($beforeFlush, $afterFlush); 135 | // No queries happened because we didn't change anything. 136 | $this->assertCount(0, $stack->queries, "Unexpected queries:\n".var_export($stack->queries, true)); 137 | 138 | // flush again 139 | $beforeFlush = $this->subscriber->encryptCounter; 140 | $em->flush(); 141 | $afterFlush = $this->subscriber->encryptCounter; 142 | // No encryption should have happened because we didn't change anything. 143 | $this->assertEquals($beforeFlush, $afterFlush); 144 | // No queries happened because we didn't change anything. 145 | $this->assertCount(0, $stack->queries, "Unexpected queries:\n".var_export($stack->queries, true)); 146 | 147 | $stmt->bindValue(1, $owner1Id); 148 | $stmt->execute(); 149 | $results = $stmt->fetchAll(); 150 | $this->assertCount(1, $results); 151 | $result = $results[0]; 152 | $shouldBeTheSameAsBefore = $result['secret']; 153 | $this->assertStringEndsWith('', $shouldBeTheSameAsBefore); // is encrypted 154 | $this->assertEquals($originalEncryption, $shouldBeTheSameAsBefore); 155 | 156 | } 157 | 158 | public function testEncryptionDoesHappenWhenASecretIsChanged(): void 159 | { 160 | $secret = "It's a secret"; 161 | $notSecret = "You're all welcome to know this."; 162 | $em = $this->entityManager; 163 | $owner = new Owner(); 164 | $owner->setSecret($secret); 165 | $owner->setNotSecret($notSecret); 166 | $em->persist($owner); 167 | $em->flush(); 168 | $em->clear(); 169 | $ownerId = $owner->getId(); 170 | unset($owner); 171 | 172 | // test that it was encrypted correctly 173 | $connection = $em->getConnection(); 174 | $stmt = $connection->prepare('SELECT * from owner WHERE id = ?'); 175 | $stmt->bindValue(1, $ownerId); 176 | $stmt->execute(); 177 | $results = $stmt->fetchAll(); 178 | $this->assertCount(1, $results); 179 | $result = $results[0]; 180 | $originalEncryption = $result['secret']; 181 | $this->assertStringEndsWith('', $originalEncryption); // is encrypted 182 | 183 | /** @var Owner $owner */ 184 | $owner = $em->getRepository(Owner::class)->find($ownerId); 185 | $owner->setSecret('A NEW SECRET!!!'); 186 | $beforeFlush = $this->subscriber->encryptCounter; 187 | $em->flush(); 188 | $afterFlush = $this->subscriber->encryptCounter; 189 | // No encryption should have happened because we didn't change anything. 190 | $this->assertGreaterThan($beforeFlush, $afterFlush); 191 | 192 | $stmt->bindValue(1, $ownerId); 193 | $stmt->execute(); 194 | $results = $stmt->fetchAll(); 195 | $this->assertCount(1, $results); 196 | $result = $results[0]; 197 | $shouldBeDifferentFromBefore = $result['secret']; 198 | $this->assertStringEndsWith('', $shouldBeDifferentFromBefore); // is encrypted 199 | $this->assertNotEquals($originalEncryption, $shouldBeDifferentFromBefore); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /Tests/Unit/Subscribers/DoctrineEncryptSubscriberTest.php: -------------------------------------------------------------------------------- 1 | encryptor = $this->createMock(EncryptorInterface::class); 41 | $this->encryptor 42 | ->expects($this->any()) 43 | ->method('encrypt') 44 | ->willReturnCallback(function (string $arg) { 45 | return 'encrypted-'.$arg; 46 | }) 47 | ; 48 | $this->encryptor 49 | ->expects($this->any()) 50 | ->method('decrypt') 51 | ->willReturnCallback(function (string $arg) { 52 | return preg_replace('/^encrypted-/', '', $arg); 53 | }) 54 | ; 55 | 56 | $this->reader = $this->createMock(Reader::class); 57 | $this->reader->expects($this->any()) 58 | ->method('getPropertyAnnotation') 59 | ->willReturnCallback(function (\ReflectionProperty $reflProperty, string $class) { 60 | if (Encrypted::class === $class) { 61 | return \in_array($reflProperty->getName(), ['name', 'address', 'extra']); 62 | } 63 | if (Embedded::class === $class) { 64 | return 'user' === $reflProperty->getName(); 65 | } 66 | 67 | return false; 68 | }) 69 | ; 70 | 71 | $this->subscriber = new DoctrineEncryptSubscriber($this->reader, $this->encryptor); 72 | } 73 | 74 | public function testSetRestorEncryptor(): void 75 | { 76 | $replaceEncryptor = $this->createMock(EncryptorInterface::class); 77 | 78 | $this->assertSame($this->encryptor, $this->subscriber->getEncryptor()); 79 | $this->subscriber->setEncryptor($replaceEncryptor); 80 | $this->assertSame($replaceEncryptor, $this->subscriber->getEncryptor()); 81 | $this->subscriber->restoreEncryptor(); 82 | $this->assertSame($this->encryptor, $this->subscriber->getEncryptor()); 83 | } 84 | 85 | public function testProcessFieldsEncrypt(): void 86 | { 87 | $user = new User('David', 'Switzerland'); 88 | 89 | $this->subscriber->processFields($user, true); 90 | 91 | $this->assertStringStartsWith('encrypted-', $user->name); 92 | $this->assertStringStartsWith('encrypted-', $user->getAddress()); 93 | } 94 | 95 | public function testProcessFieldsEncryptExtend(): void 96 | { 97 | $user = new ExtendedUser('David', 'Switzerland', 'extra'); 98 | 99 | $this->subscriber->processFields($user, true); 100 | 101 | $this->assertStringStartsWith('encrypted-', $user->name); 102 | $this->assertStringStartsWith('encrypted-', $user->getAddress()); 103 | $this->assertStringStartsWith('encrypted-', $user->extra); 104 | } 105 | 106 | public function testProcessFieldsEncryptEmbedded(): void 107 | { 108 | $withUser = new WithUser('Thing', 'foo', new User('David', 'Switzerland')); 109 | 110 | $this->subscriber->processFields($withUser, true); 111 | 112 | $this->assertStringStartsWith('encrypted-', $withUser->name); 113 | $this->assertSame('foo', $withUser->foo); 114 | $this->assertStringStartsWith('encrypted-', $withUser->user->name); 115 | $this->assertStringStartsWith('encrypted-', $withUser->user->getAddress()); 116 | } 117 | 118 | public function testProcessFieldsEncryptNull(): void 119 | { 120 | $user = new User('David', null); 121 | 122 | $this->subscriber->processFields($user, true); 123 | 124 | $this->assertStringStartsWith('encrypted-', $user->name); 125 | $this->assertNull($user->getAddress()); 126 | } 127 | 128 | public function testProcessFieldsNoEncryptor(): void 129 | { 130 | $user = new User('David', 'Switzerland'); 131 | 132 | $this->subscriber->setEncryptor(null); 133 | $this->subscriber->processFields($user, true); 134 | 135 | $this->assertSame('David', $user->name); 136 | $this->assertSame('Switzerland', $user->getAddress()); 137 | } 138 | 139 | public function testProcessFieldsDecrypt(): void 140 | { 141 | $user = new User('encrypted-David', 'encrypted-Switzerland'); 142 | 143 | $this->subscriber->processFields($user, false); 144 | 145 | $this->assertSame('David', $user->name); 146 | $this->assertSame('Switzerland', $user->getAddress()); 147 | } 148 | 149 | public function testProcessFieldsDecryptExtended(): void 150 | { 151 | $user = new ExtendedUser('encrypted-David', 'encrypted-Switzerland', 'encrypted-extra'); 152 | 153 | $this->subscriber->processFields($user, false); 154 | 155 | $this->assertSame('David', $user->name); 156 | $this->assertSame('Switzerland', $user->getAddress()); 157 | $this->assertSame('extra', $user->extra); 158 | } 159 | 160 | public function testProcessFieldsDecryptEmbedded(): void 161 | { 162 | $withUser = new WithUser('encrypted-Thing', 'foo', new User('encrypted-David', 'encrypted-Switzerland')); 163 | 164 | $this->subscriber->processFields($withUser, false); 165 | 166 | $this->assertSame('Thing', $withUser->name); 167 | $this->assertSame('foo', $withUser->foo); 168 | $this->assertSame('David', $withUser->user->name); 169 | $this->assertSame('Switzerland', $withUser->user->getAddress()); 170 | } 171 | 172 | public function testProcessFieldsDecryptNull(): void 173 | { 174 | $user = new User('encrypted-David', null); 175 | 176 | $this->subscriber->processFields($user, false); 177 | 178 | $this->assertSame('David', $user->name); 179 | $this->assertNull($user->getAddress()); 180 | } 181 | 182 | public function testProcessFieldsDecryptNonEncrypted(): void 183 | { 184 | // no trailing but somethint that our mock decrypt would change if called 185 | $user = new User('encrypted-David', 'encrypted-Switzerland'); 186 | 187 | $this->subscriber->processFields($user, false); 188 | 189 | $this->assertSame('encrypted-David', $user->name); 190 | $this->assertSame('encrypted-Switzerland', $user->getAddress()); 191 | } 192 | 193 | /** 194 | * Test that fields are encrypted before flushing. 195 | */ 196 | public function testOnFlush(): void 197 | { 198 | $user = new User('David', 'Switzerland'); 199 | 200 | $uow = $this->createMock(UnitOfWork::class); 201 | $uow->expects($this->any()) 202 | ->method('getScheduledEntityInsertions') 203 | ->willReturn([$user]) 204 | ; 205 | $em = $this->createMock(EntityManagerInterface::class); 206 | $em->expects($this->any()) 207 | ->method('getUnitOfWork') 208 | ->willReturn($uow) 209 | ; 210 | $classMetaData = $this->createMock(ClassMetadata::class); 211 | $em->expects($this->once())->method('getClassMetadata')->willReturn($classMetaData); 212 | $uow->expects($this->once())->method('recomputeSingleEntityChangeSet'); 213 | 214 | $onFlush = new OnFlushEventArgs($em); 215 | 216 | $this->subscriber->onFlush($onFlush); 217 | 218 | $this->assertStringStartsWith('encrypted-', $user->name); 219 | $this->assertStringStartsWith('encrypted-', $user->getAddress()); 220 | } 221 | 222 | /** 223 | * Test that fields are decrypted again after flushing 224 | */ 225 | public function testPostFlush(): void 226 | { 227 | $user = new User('encrypted-David', 'encrypted-Switzerland'); 228 | 229 | $uow = $this->createMock(UnitOfWork::class); 230 | $uow->expects($this->any()) 231 | ->method('getIdentityMap') 232 | ->willReturn([[$user]]) 233 | ; 234 | $em = $this->createMock(EntityManagerInterface::class); 235 | $em->expects($this->any()) 236 | ->method('getUnitOfWork') 237 | ->willReturn($uow) 238 | ; 239 | $postFlush = new PostFlushEventArgs($em); 240 | 241 | $this->subscriber->postFlush($postFlush); 242 | 243 | $this->assertSame('David', $user->name); 244 | $this->assertSame('Switzerland', $user->getAddress()); 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/Subscribers/DoctrineEncryptSubscriber.php: -------------------------------------------------------------------------------- 1 | '; 28 | 29 | /** 30 | * Encryptor interface namespace 31 | */ 32 | const ENCRYPTOR_INTERFACE_NS = 'Ambta\DoctrineEncryptBundle\Encryptors\EncryptorInterface'; 33 | 34 | /** 35 | * Encrypted annotation full name 36 | */ 37 | const ENCRYPTED_ANN_NAME = 'Ambta\DoctrineEncryptBundle\Configuration\Encrypted'; 38 | 39 | /** 40 | * Encryptor 41 | * @var EncryptorInterface|null 42 | */ 43 | private ?EncryptorInterface $encryptor; 44 | 45 | /** 46 | * Annotation reader 47 | * @var Reader 48 | */ 49 | private Reader $annReader; 50 | 51 | /** 52 | * Used for restoring the encryptor after changing it 53 | * @var EncryptorInterface|string 54 | */ 55 | private EncryptorInterface|string $restoreEncryptor; 56 | 57 | /** 58 | * Count amount of decrypted values in this service 59 | * @var integer 60 | */ 61 | public int $decryptCounter = 0; 62 | 63 | /** 64 | * Count amount of encrypted values in this service 65 | * @var integer 66 | */ 67 | public int $encryptCounter = 0; 68 | 69 | /** @var array */ 70 | private array $cachedDecryptions = []; 71 | 72 | /** 73 | * Initialization of subscriber 74 | * 75 | * @param Reader $annReader 76 | * @param EncryptorInterface $encryptor (Optional) An EncryptorInterface. 77 | */ 78 | public function __construct(Reader $annReader, EncryptorInterface $encryptor) 79 | { 80 | $this->annReader = $annReader; 81 | $this->encryptor = $encryptor; 82 | $this->restoreEncryptor = $this->encryptor; 83 | } 84 | 85 | /** 86 | * Change the encryptor 87 | * 88 | * @param EncryptorInterface|null $encryptor 89 | */ 90 | public function setEncryptor(?EncryptorInterface $encryptor = null) 91 | { 92 | $this->encryptor = $encryptor; 93 | } 94 | 95 | /** 96 | * Get the current encryptor 97 | * 98 | * @return EncryptorInterface|null returns the encryptor class or null 99 | */ 100 | public function getEncryptor(): ?EncryptorInterface 101 | { 102 | return $this->encryptor; 103 | } 104 | 105 | /** 106 | * Restore encryptor to the one set in the constructor. 107 | */ 108 | public function restoreEncryptor() 109 | { 110 | $this->encryptor = $this->restoreEncryptor; 111 | } 112 | 113 | /** 114 | * Listen a postUpdate lifecycle event. 115 | * Decrypt entities property's values when post updated. 116 | * 117 | * So for example after form submit the preUpdate encrypted the entity 118 | * We have to decrypt them before showing them again. 119 | * 120 | * @param LifecycleEventArgs $args 121 | */ 122 | public function postUpdate(LifecycleEventArgs $args) 123 | { 124 | $entity = $args->getEntity(); 125 | $this->processFields($entity, false); 126 | } 127 | 128 | /** 129 | * Listen a preUpdate lifecycle event. 130 | * Encrypt entities property's values on preUpdate, so they will be stored encrypted 131 | * 132 | * @param PreUpdateEventArgs $args 133 | */ 134 | public function preUpdate(PreUpdateEventArgs $args) 135 | { 136 | $entity = $args->getEntity(); 137 | $this->processFields($entity); 138 | } 139 | 140 | /** 141 | * Listen a postLoad lifecycle event. 142 | * Decrypt entities property's values when loaded into the entity manger 143 | * 144 | * @param LifecycleEventArgs $args 145 | */ 146 | public function postLoad(LifecycleEventArgs $args) 147 | { 148 | $entity = $args->getEntity(); 149 | $this->processFields($entity, false); 150 | } 151 | 152 | /** 153 | * Listen to onflush event 154 | * Encrypt entities that are inserted into the database 155 | * 156 | * @param PreFlushEventArgs $preFlushEventArgs 157 | */ 158 | public function preFlush(PreFlushEventArgs $preFlushEventArgs) 159 | { 160 | $unitOfWOrk = $preFlushEventArgs->getEntityManager()->getUnitOfWork(); 161 | foreach ($unitOfWOrk->getIdentityMap() as $entityName => $entityArray) { 162 | if (isset($this->cachedDecryptions[$entityName])) { 163 | foreach ($entityArray as $entityId => $instance) { 164 | $this->processFields($instance); 165 | } 166 | } 167 | } 168 | $this->cachedDecryptions = []; 169 | } 170 | 171 | /** 172 | * Listen to onflush event 173 | * Encrypt entities that are inserted into the database 174 | * 175 | * @param OnFlushEventArgs $onFlushEventArgs 176 | */ 177 | public function onFlush(OnFlushEventArgs $onFlushEventArgs) 178 | { 179 | $unitOfWork = $onFlushEventArgs->getEntityManager()->getUnitOfWork(); 180 | foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) { 181 | $encryptCounterBefore = $this->encryptCounter; 182 | $this->processFields($entity); 183 | if ($this->encryptCounter > $encryptCounterBefore ) { 184 | $classMetadata = $onFlushEventArgs->getEntityManager()->getClassMetadata(get_class($entity)); 185 | $unitOfWork->recomputeSingleEntityChangeSet($classMetadata, $entity); 186 | } 187 | } 188 | } 189 | 190 | /** 191 | * Listen to postFlush event 192 | * Decrypt entities after having been inserted into the database 193 | * 194 | * @param PostFlushEventArgs $postFlushEventArgs 195 | */ 196 | public function postFlush(PostFlushEventArgs $postFlushEventArgs) 197 | { 198 | $unitOfWork = $postFlushEventArgs->getEntityManager()->getUnitOfWork(); 199 | foreach ($unitOfWork->getIdentityMap() as $entityMap) { 200 | foreach ($entityMap as $entity) { 201 | $this->processFields($entity, false); 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * Realization of EventSubscriber interface method. 208 | * 209 | * @return array Return all events which this subscriber is listening 210 | */ 211 | public function getSubscribedEvents(): array 212 | { 213 | return array( 214 | Events::postUpdate, 215 | Events::preUpdate, 216 | Events::postLoad, 217 | Events::onFlush, 218 | Events::preFlush, 219 | Events::postFlush, 220 | ); 221 | } 222 | 223 | /** 224 | * Process (encrypt/decrypt) entities fields 225 | * 226 | * @param Object $entity doctrine entity 227 | * @param Boolean $isEncryptOperation If true - encrypt, false - decrypt entity 228 | * 229 | * @return object|null 230 | *@throws \RuntimeException 231 | * 232 | */ 233 | public function processFields(object $entity, bool $isEncryptOperation = true): ?object 234 | { 235 | if (!empty($this->encryptor)) { 236 | // Check which operation to be used 237 | $encryptorMethod = $isEncryptOperation ? 'encrypt' : 'decrypt'; 238 | 239 | $realClass = ClassUtils::getClass($entity); 240 | 241 | // Get ReflectionClass of our entity 242 | $properties = $this->getClassProperties($realClass); 243 | 244 | // Foreach property in the reflection class 245 | foreach ($properties as $refProperty) { 246 | if ($this->annReader->getPropertyAnnotation($refProperty, 'Doctrine\ORM\Mapping\Embedded')) { 247 | $this->handleEmbeddedAnnotation($entity, $refProperty, $isEncryptOperation); 248 | continue; 249 | } 250 | 251 | /** 252 | * If property is an normal value and contains the Encrypt tag, lets encrypt/decrypt that property 253 | */ 254 | if ($this->annReader->getPropertyAnnotation($refProperty, self::ENCRYPTED_ANN_NAME)) { 255 | $pac = PropertyAccess::createPropertyAccessor(); 256 | $value = $pac->getValue($entity, $refProperty->getName()); 257 | if ($encryptorMethod == 'decrypt') { 258 | if (!is_null($value) and !empty($value)) { 259 | if (substr($value, -strlen(self::ENCRYPTION_MARKER)) == self::ENCRYPTION_MARKER) { 260 | $this->decryptCounter++; 261 | $currentPropValue = $this->encryptor->decrypt(substr($value, 0, -5)); 262 | $pac->setValue($entity, $refProperty->getName(), $currentPropValue); 263 | $this->cachedDecryptions[get_class($entity)][spl_object_id($entity)][$refProperty->getName()][$currentPropValue] = $value; 264 | } 265 | } 266 | } else { 267 | if (!is_null($value) and !empty($value)) { 268 | if (isset($this->cachedDecryptions[get_class($entity)][spl_object_id($entity)][$refProperty->getName()][$value])) { 269 | $pac->setValue($entity, $refProperty->getName(), $this->cachedDecryptions[get_class($entity)][spl_object_id($entity)][$refProperty->getName()][$value]); 270 | } elseif (substr($value, -strlen(self::ENCRYPTION_MARKER)) != self::ENCRYPTION_MARKER) { 271 | $this->encryptCounter++; 272 | $currentPropValue = $this->encryptor->encrypt($value).self::ENCRYPTION_MARKER; 273 | $pac->setValue($entity, $refProperty->getName(), $currentPropValue); 274 | } 275 | } 276 | } 277 | } 278 | } 279 | 280 | return $entity; 281 | } 282 | 283 | return $entity; 284 | } 285 | 286 | private function handleEmbeddedAnnotation($entity, ReflectionProperty $embeddedProperty, bool $isEncryptOperation = true) 287 | { 288 | $propName = $embeddedProperty->getName(); 289 | 290 | $pac = PropertyAccess::createPropertyAccessor(); 291 | 292 | $embeddedEntity = $pac->getValue($entity, $propName); 293 | 294 | if ($embeddedEntity) { 295 | $this->processFields($embeddedEntity, $isEncryptOperation); 296 | } 297 | } 298 | 299 | /** 300 | * Recursive function to get an associative array of class properties 301 | * including inherited ones from extended classes 302 | * 303 | * @param string $className Class name 304 | * 305 | * @return array 306 | */ 307 | private function getClassProperties(string $className): array 308 | { 309 | $reflectionClass = new ReflectionClass($className); 310 | $properties = $reflectionClass->getProperties(); 311 | $propertiesArray = array(); 312 | 313 | foreach ($properties as $property) { 314 | $propertyName = $property->getName(); 315 | $propertiesArray[$propertyName] = $property; 316 | } 317 | 318 | if ($parentClass = $reflectionClass->getParentClass()) { 319 | $parentPropertiesArray = $this->getClassProperties($parentClass->getName()); 320 | if (count($parentPropertiesArray) > 0) { 321 | $propertiesArray = array_merge($parentPropertiesArray, $propertiesArray); 322 | } 323 | } 324 | 325 | return $propertiesArray; 326 | } 327 | } 328 | --------------------------------------------------------------------------------