diff --git a/.gitattributes b/.gitattributes
index 304e5fa..b9724f1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,8 +2,8 @@
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
# Ignore all test and documentation with "export-ignore".
-/.github export-ignore
-/tests export-ignore
-/.gitattributes export-ignore
-/.gitignore export-ignore
-/phpunit.xml.dist export-ignore
+/.github export-ignore
+/tests export-ignore
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/phpunit.xml export-ignore
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000..84ecd4c
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,22 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 60
+
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 7
+
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - bug
+ - enhancement
+ - RFC
+
+# Label to use when marking an issue as stale
+staleLabel: stale
+
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index 9a67b46..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-name: ci
-
-on:
- pull_request:
- branches: [ master ]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- strategy:
- fail-fast: false
- matrix:
- php-version: [ '7.4', '8.0', '8.1', '8.2' ]
-
- steps:
- - name: checkout
- uses: actions/checkout@v3
-
- - name: php-setup
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php-version }}
-
- - name: composer-validate
- run: composer validate
-
- - name: composer-cache
- id: composer-cache
- uses: actions/cache@v3
- with:
- path: vendor
- key: ${{ runner.os }}-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
- restore-keys: |
- ${{ runner.os }}-${{ matrix.php-version }}-composer-
-
- - name: composer-install
- if: steps.composer-cache.outputs.cache-hit != 'true'
- run: composer install --prefer-dist --no-progress --no-suggest
-
- - name: phpunit
- run: vendor/bin/phpunit
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
new file mode 100644
index 0000000..99dcb1f
--- /dev/null
+++ b/.github/workflows/phpunit.yml
@@ -0,0 +1,93 @@
+name: PHPUnit
+
+on:
+ push:
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+ cancel-in-progress: true
+
+jobs:
+
+ test:
+ name: "PHP ${{ matrix.php }} - Symfony ${{ matrix.symfony }}"
+ runs-on: ubuntu-latest
+ continue-on-error: ${{ matrix.can-fail }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # Lowest Deps
+ - php: 8.0
+ symfony: 5.4.*
+ coverage: 'none'
+ composer-flags: '--prefer-stable --prefer-lowest'
+ can-fail: false
+ # LTS with latest stable PHP
+ - php: latest
+ symfony: 5.4.*
+ coverage: 'none'
+ composer-flags: '--prefer-stable'
+ can-fail: false
+ # Active release
+ - php: latest
+ symfony: 6.2.*
+ coverage: pcov
+ composer-flags: '--prefer-stable'
+ can-fail: false
+ # Development release
+ - php: nightly
+ symfony: 6.3.*@dev
+ coverage: 'none'
+ composer-flags: ''
+ can-fail: true
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 2
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ tools: composer:v2, flex
+ coverage: ${{ matrix.coverage }}
+ ini-values: date.timezone=UTC,memory_limit=-1,session.gc_probability=0,apc.enable_cli=1,zend.assertions=1
+ env:
+ fail-fast: true
+
+ - name: Set Composer stability
+ if: matrix.symfony == '6.3.*@dev'
+ run: "composer config minimum-stability dev"
+
+ - name: Get composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Cache composer dependencies
+ uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-composer-
+
+ - name: Install Composer dependencies
+ run: composer update ${{ matrix.composer-flags }} --no-interaction --no-progress --optimize-autoloader
+ env:
+ SYMFONY_REQUIRE: ${{ matrix.symfony }}
+
+ - name: Run tests
+ run: composer test
+
+ - name: Monitor coverage
+ if: matrix.coverage != 'none'
+ uses: slavcodev/coverage-monitor-action@v1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ coverage_path: coverage-report.xml
+ threshold_alert: 60
+ threshold_warning: 80
diff --git a/.gitignore b/.gitignore
index 5cf8cb5..f49ca77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,5 @@
-/.idea/
-/tests/App/var
-/tests/coverage/
/vendor/
-.DS_Store
-composer.lock
+.php_cs.cache
.phpunit.result.cache
-phpunit.xml
+composer.lock
+coverage-report.xml
diff --git a/LICENSE b/LICENSE
index 8aa2645..d345d08 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,13 +1,11 @@
-MIT License
-
-Copyright (c) [year] [fullname]
+Copyright (c) 2021-present OD&B
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
@@ -17,5 +15,5 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
index beebc9d..a6e2355 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# Doctrine Ciphersweet Encryption Bundle
## Introduction
+
This bundle aims to make life easier to developers who want to set encrypted fields in their entities thanks to Ciphersweet library.
This bundle is inspired from the talk given at Afup ForumPHP : [REX sur le chiffrement de base de données](https://afup.org/talks/3455-rex-sur-le-chiffrement-de-base-de-donnees).
We also used the WIP public repository : https://github.com/PhilETaylor/doctrine-ciphersweet.
@@ -40,7 +41,8 @@ return [
## Usage
### 1. Setup DOCTRINE_CIPHERSWEET_KEY environment key
-This bundle comes with several commands and annotations but first of all, you'll need to setup the `DOCTRINE_CIPHERSWEET_KEY` secret environment key.
+
+This bundle comes with several commands and annotations/attributes but first of all, you'll need to setup the `DOCTRINE_CIPHERSWEET_KEY` secret environment key.
First of all, you can just init a random one in your environment file.
```php
@@ -60,31 +62,33 @@ php bin/console odb:enc:generate-string-key | php bin/console secrets:set DOCTRI
```
Then remove the entry in your .env file
-### 2. Add annotations to your entities
-This bundle comes with 2 annotations in order to set encryption fields :
+### 2. Add annotations / attributes to your entities
+
+This bundle comes with 2 annotations / attributes in order to set encryption fields :
-- @EncryptedField : Marks the field as encrypted and will automatically use Ciphersweet library to encrypt/decrypt on onFlush and onLoad events
-- @IndexableField : Marks the field as searchable and several indexes can be generated in a separate table in order to search by terms starting by or ending by.
+- **EncryptedField** : Marks the field as encrypted and will automatically use Ciphersweet library to encrypt/decrypt on onFlush and onLoad events
+- **IndexableField** : Marks the field as searchable and several indexes can be generated in a separate table in order to search by terms starting by or ending by.
-#### @EncrytedField
-This annotation comes with 3 options:
+#### EncrytedField
+This annotation/attribute comes with 3 options:
-- (int) $filterBits : Number of bits used for encryption (Default : 32)
-- (bool) $indexable : Activate a default index for exact search apart of the @IndexableField annotation. If true, will try to set Index string data into a field named with a "_bi" suffix
-- (string) $mappedTypedProperty: If you want to encrypt data other than strings (other types currenty supported : int, float, bool), you'll need to set a string field used by doctrine for persistance purpose instead of your raw field. Then use this parameter to automatically hydrate decrypted data into the target field.
+- (int) $filterBits : Number of bits used for encryption. (Default : `32`)
+- (bool) $indexable : Activate a default index for exact search apart of the **IndexableField** annotation. If true, will try to set **index** string data into a field named with a "_bi" suffix. (Default : `true`)
+- (string) $mappedTypedProperty: If you want to encrypt data other than strings (other types currenty supported : int, float, bool), you'll need to set a string field used by doctrine for persistance purpose instead of your raw field. Then use this parameter to automatically hydrate decrypted data into the target field. (Default : `null`)
-#### @IndexableField
-This annotation comes with 4 options:
+#### IndexableField
+This annotation/attribute comes with 4 options:
-- (bool) $autoRefresh : Automatically regenerate related indexes to an entity upon persist or update event.
- (string) $indexesEntityClass : Name of the entity class that will store the indexes (can be mutualized)
-- (string) $valuePreprocessMethod : Before indexes generation, you may need to clean your input in order to reduce the number of indexes to generate (trim value, slug it, etc.). You can do it by setting this option. For the moment, the method mention can only by related to the current entity class.
-- (array) $indexesGenerationMethods : List of methods used to generate several searchable values from the original one. For example the `ValueStartingByGenerator` can take the value "abcdef" in order to generate indexes for ["a", "ab", "abc', "abcd", ...]. So that you can search entities with a field starting by those values.
-- (bool) $fastIndexing : If true, will use a faster indexing method.
+- (bool) $autoRefresh : Automatically regenerate related indexes to an entity upon persist or update event. (Default : `true`)
+- (array) $indexesGenerationMethods : List of methods used to generate several searchable values from the original one. For example the `ValueStartingByGenerator` can take the value "abcdef" in order to generate indexes for ["a", "ab", "abc', "abcd", ...]. So that you can search entities with a field starting by those values. (Default : `[]`)
+- (string) $valuePreprocessMethod : Before indexes generation, you may need to clean your input in order to reduce the number of indexes to generate (trim value, slug it, etc.). You can do it by setting this option. For the moment, the method mention can only by related to the current entity class. (Default : `null`)
+- (bool) $fastIndexing : If true, will use a faster indexing method. (Default : `true`)
### 3. Generating indexes
+
To make the entities searchable, the library provides a feature called "Blind Index" which is a unique index calculated from the original value.
-By default, we provide a default index field to every encrypted ones (using the $indexable option). If you need to setup a search of values starting by a term, you'll need the `@IndexableField` annotation and set a dedicated indexes table.
+By default, we provide a default index field to every encrypted ones (using the $indexable option). If you need to setup a search of values starting by a term, you'll need the `IndexableField` annotation/attribute and set a dedicated indexes table.
This dedicated entity must implement the `Odandb\DoctrineCiphersweetEncryptionBundle\Entity\IndexedEntityInterface` and you can use the `Odandb\DoctrineCiphersweetEncryptionBundle\Entity\IndexedEntityTrait` to make your life easier.
Basically, this table will be composed of those columns :
@@ -117,55 +121,38 @@ In this mode, the command will split the work in smaller chuncks and start subpr
Refer to the command for more informations.
### 4. Here is a full example :
+
```php
-/**
- *
- * @ORM\Entity(repositoryClass="App\Repository\MySecretEntityRepository")
- * @ORM\Table(indexes={
- * @ORM\Index(name="anum_blind_idx", columns={"account_number_bi"}),
- * })
- */
+#[ORM\Entity(repositoryClass: App\Repository\MySecretEntityRepository::class)
+#[ORM\Index(name: 'anum_blind_idx', columns: ['account_number_bi'])]
class MySecretEntity
{
- /**
- * @ORM\Id
- * @ORM\GeneratedValue
- * @ORM\Column(type="integer")
- */
+ #[ORM\Id]
+ #[ORM\GeneratedValue]
+ #[ORM\Column(type: 'integer')]
private int $id;
- /**
- * @var string
- * @ORM\Column(type="string", length=36)
- */
+ #[ORM\Column(type: 'string', length: 36)]
private string $uuid;
- /**
- * @ORM\Column(type="string")
- *
- * @EncryptedField
- * @IndexableField(indexesEntityClass="App\Entity\MySecretEntityIndexes", autoRefresh=false, indexesGenerationMethods={"ValueStartingBy"}, valuePreprocessMethod="cleanAccountNumber")
- */
+ #[ORM\Column(type: 'string')]
+ #[EncryptedField]
+ #[IndexableField(indexesEntityClass: App\Entity\MySecretEntityIndexes::class, autoRefresh: false, indexesGenerationMethods: ['ValueStartingBy'], valuePreprocessMethod: 'cleanAccountNumber')]
private string $accountNumber;
- /**
- * @ORM\Column(type="string", length=10)
- */
+ #[ORM\Column(type: 'string', length: 10)]
private string $accountNumberBi;
private int $secretNumber;
- /**
- * @ORM\Column(type="string")
- *
- * @EncryptedField(mappedTypedProperty="secretNumber", indexable=false)
- */
+ #[ORM\Column(type: 'string')]
+ #[EncryptedField(mappedTypedProperty: 'secretNumber', indexable: false)]
private string $secretNumberEncrypted;
/**
* @var Collection|null
- * @ORM\OneToMany(targetEntity="MySecretEntityIndexes", mappedBy="targetEntity", cascade={"persist"})
*/
+ #[ORM\OneToMany(targetEntity: MySecretEntityIndexes::class, mappedBy: 'targetEntity', cascade: ['persist'])]
private ?Collection $indexes;
/**
@@ -191,33 +178,22 @@ class MySecretEntity
use Odandb\DoctrineCiphersweetEncryptionBundle\Entity\IndexedEntityInterface;
use Odandb\DoctrineCiphersweetEncryptionBundle\Entity\IndexedEntityTrait;
+
/**
* Class storing indexes for MySecretEntity.
- *
- * @ORM\Entity(repositoryClass="App\Repository\MySecretEntityIndexesRepository")
- * @ORM\Table(indexes={
- * @ORM\Index(name="blind_idx", columns={"index_bi"}),
- * @ORM\Index(name="field_and_blind_idx", columns={"fieldname", "index_bi"})
- * })
*/
+#[ORM\Entity(repositoryClass: App\Repository\MySecretEntityIndexesRepository::class)
+#[ORM\Index(name: 'blind_idx', columns: ['index_bi'])]
+#[ORM\Index(name: 'field_and_blind_idx', columns: ['fieldname', 'index_bi'])]
class MySecretEntityIndexes implements IndexedEntityInterface
{
use IndexedEntityTrait;
/**
* @var MySecretEntity|null
- *
- * @ORM\ManyToOne(targetEntity="App\Entity\MySecretEntity", inversedBy="indexes")
- * @ORM\JoinColumn(name="target_entity_id", referencedColumnName="id", onDelete="CASCADE")
*/
+ #[ORM\ManyToOne(targetEntity: App\Entity\MySecretEntity::class, inversedBy: 'indexes')]
+ #[ORM\JoinColumn(name: 'target_entity_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
protected object $targetEntity;
}
```
-
-## More to come
-
-- [ ] Tests
-- [ ] Add Indexes generator methods
-- [ ] Your suggestions here ...
-
-
diff --git a/composer.json b/composer.json
index cffb404..8b6ac4b 100644
--- a/composer.json
+++ b/composer.json
@@ -1,13 +1,15 @@
{
"name": "odandb/doctrine-ciphersweet-encryption-bundle",
- "type": "symfony-bundle",
"description": "Bridge between Doctrine and Ciphersweet libary in order to make encrypted and searchable fields",
+ "license": "MIT",
+ "type": "symfony-bundle",
"keywords": [
+ "symfony",
+ "bundle",
"doctrine",
"encryption",
"cipher"
],
- "license": "MIT",
"authors": [
{
"name": "Mathieu Girard",
@@ -20,34 +22,33 @@
],
"require": {
"php": ">=7.4",
- "doctrine/annotations": "^2.0",
- "doctrine/orm": "^2.7",
+ "ext-mbstring": "*",
+ "doctrine/annotations": "^1.10 || ^2.0",
+ "doctrine/doctrine-bundle": "^2.7",
+ "doctrine/orm": "^2.14",
"paragonie/ciphersweet": "^3.0 || ^4.0",
"symfony/config": "^5.4 || ^6.0",
"symfony/console": "^5.4 || ^6.0",
"symfony/dependency-injection": "^5.4 || ^6.0",
"symfony/deprecation-contracts": "^2.5 || ^3.2",
+ "symfony/framework-bundle": "^5.4 || ^6.0",
"symfony/http-kernel": "^5.4 || ^6.0",
"symfony/process": "^5.4 || ^6.0",
"symfony/property-access": "^5.4 || ^6.0",
"symfony/property-info": "^5.4 || ^6.0",
- "symfony/yaml": "^5.4 || ^6.0"
+ "symfony/service-contracts": "^2.2 || ^3.2"
},
"require-dev": {
- "doctrine/doctrine-bundle": "^2.7",
"phpunit/phpunit": "^9.5",
- "roave/security-advisories": "dev-latest",
+ "sensio/framework-extra-bundle": "^6.2.2",
+ "symfony/browser-kit": "^5.4 || ^6.0",
"symfony/doctrine-bridge": "^5.4 || ^6.0",
- "symfony/dotenv": "^5.4 || ^6.0",
- "symfony/framework-bundle": "^5.4 || ^6.0",
- "symfony/phpunit-bridge": "^6.0"
+ "symfony/phpunit-bridge": "^6.2.3",
+ "symfony/yaml": "^5.4 || ^6.0"
},
"suggest": {
"phpdocumentor/reflection-docblock": "To use the PHPDoc"
},
- "config": {
- "sort-packages": true
- },
"autoload": {
"psr-4": {
"Odandb\\DoctrineCiphersweetEncryptionBundle\\": "src/"
@@ -57,5 +58,11 @@
"psr-4": {
"Odandb\\DoctrineCiphersweetEncryptionBundle\\Tests\\": "tests/"
}
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit --coverage-clover=coverage-report.xml"
}
}
diff --git a/config/services.php b/config/services.php
new file mode 100644
index 0000000..b8da82b
--- /dev/null
+++ b/config/services.php
@@ -0,0 +1,114 @@
+services()
+ ->instanceof(IndexesGeneratorInterface::class)
+ ->tag('encryption.index_generator')
+
+ // Paragon
+ ->set('encryption.paragon.string_provider', StringProvider::class)
+ ->args([
+ env('DOCTRINE_CIPHERSWEET_KEY')
+ ])
+ ->set('encryption.paragon.cipher_sweet', CipherSweet::class)
+ ->args([
+ service('encryption.paragon.string_provider')
+ ])
+
+ // Command
+ ->set('encryption.console.check_encryption', CheckEncryptionCommand::class)
+ ->args([
+ service(EntityManagerInterface::class),
+ service('encryption.encrypted_fields')
+ ])
+ ->tag('console.command')
+ ->set('encryption.console.encryption_data', EncryptionDataCommand::class)
+ ->args([
+ service(EntityManagerInterface::class),
+ service(EncryptorInterface::class)
+ ])
+ ->tag('console.command')
+ ->set('encryption.console.key_string_provider_generator', EncryptionKeyStringProviderGeneratorCommand::class)
+ ->tag('console.command')
+ ->set('encryption.console.field_index_planner', FieldIndexPlannerCommand::class)
+ ->tag('console.command')
+ ->set('encryption.console.generate_indexes', GenerateIndexesCommand::class)
+ ->args([
+ service('encryption.indexable_field')
+ ])
+ ->tag('console.command')
+
+ // Encryptors
+ ->set('encryption.encryptor.cipher_sweet', CiphersweetEncryptor::class)
+ ->args([
+ service('encryption.paragon.cipher_sweet')
+ ])
+ ->alias(EncryptorInterface::class, 'encryption.encryptor.cipher_sweet')
+
+ // Indexes Generators
+ ->set('encryption.indexes_generator', IndexesGenerator::class)
+ ->args([
+ abstract_arg('All services with tag "encryption.index_generator" are stored in a service locator by IndexGeneratorPass'),
+ service(EncryptorInterface::class)
+ ])
+ ->set('encryption.indexes_generator.tokenizer', TokenizerGenerator::class)
+ ->tag('encryption.index_generator', ['key' => 'TokenizerGenerator'])
+ ->set('encryption.indexes_generator.value_starting_by', ValueStartingByGenerator::class)
+ ->tag('encryption.index_generator', ['key' => 'ValueStartingByGenerator'])
+ ->set('encryption.indexes_generator.value_ending_by', ValueEndingByGenerator::class)
+ ->tag('encryption.index_generator', ['key' => 'ValueEndingByGenerator'])
+
+ ->set('encryption.indexable_field', IndexableFieldsService::class)
+ ->args([
+ service('annotation_reader')->nullOnInvalid(), // @deprecated
+ service(EntityManagerInterface::class),
+ service('encryption.indexes_generator'),
+ service('property_accessor')
+ ])
+
+ // Property
+ ->set('encryption.property_hydrator', PropertyHydratorService::class)
+ ->args([
+ service('property_info'),
+ service('property_accessor')
+ ])
+
+ ->set('encryption.subscriber', DoctrineCiphersweetSubscriber::class)
+ ->args([
+ service('annotation_reader'), // @deprecated
+ service('encryption.encrypted_fields'),
+ service(EncryptorInterface::class),
+ service('encryption.indexable_field'),
+ service('encryption.property_hydrator')
+ ])
+ ->tag('doctrine.event_subscriber')
+
+ ->set('encryption.encrypted_fields', EncryptedFieldsService::class)
+ ->args([
+ service('annotation_reader'), // @deprecated
+ ])
+ ;
+};
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..8ff1c17
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ./tests/Unit
+
+
+ ./tests/Functional
+
+
+
+
+
+ ./src/
+
+
+ src/Entity
+ src/Exception
+
+
+
+
+
+
+
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
deleted file mode 100644
index 1ec20cb..0000000
--- a/phpunit.xml.dist
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- tests
-
-
-
diff --git a/src/Command/CheckEncryptionCommand.php b/src/Command/CheckEncryptionCommand.php
index b16c38a..0114a7e 100644
--- a/src/Command/CheckEncryptionCommand.php
+++ b/src/Command/CheckEncryptionCommand.php
@@ -32,7 +32,7 @@ public function __construct(EntityManagerInterface $entityManager, EncryptedFiel
$this->entityManager = $entityManager;
$this->encryptedFieldsService = $encryptedFieldsService;
- parent::__construct(self::$defaultName);
+ parent::__construct();
}
protected function configure(): void
diff --git a/src/Command/EncryptionDataCommand.php b/src/Command/EncryptionDataCommand.php
index 5a46f42..6fc6c62 100644
--- a/src/Command/EncryptionDataCommand.php
+++ b/src/Command/EncryptionDataCommand.php
@@ -38,10 +38,10 @@ public function __construct(EntityManagerInterface $entityManager, EncryptorInte
$this->entityManager = $entityManager;
$this->encryptor = $encryptor;
- parent::__construct(self::$defaultName);
+ parent::__construct();
}
- public function configure()
+ public function configure(): void
{
$this
->setAliases([self::$defaultAlias])
diff --git a/src/Command/EncryptionKeyStringProviderGenerator.php b/src/Command/EncryptionKeyStringProviderGeneratorCommand.php
similarity index 93%
rename from src/Command/EncryptionKeyStringProviderGenerator.php
rename to src/Command/EncryptionKeyStringProviderGeneratorCommand.php
index 4d15eae..52f2ff6 100644
--- a/src/Command/EncryptionKeyStringProviderGenerator.php
+++ b/src/Command/EncryptionKeyStringProviderGeneratorCommand.php
@@ -2,7 +2,6 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Command;
use ParagonIE\ConstantTime\Hex;
@@ -13,7 +12,7 @@
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(name: 'odb:enc:generate-string-key', description: 'Generate default encryption key for StringProvider (one of the different key provider managed by Ciphersweet library).')]
-class EncryptionKeyStringProviderGenerator extends Command
+class EncryptionKeyStringProviderGeneratorCommand extends Command
{
/** @deprecated */
protected static $defaultName = 'odb:enc:generate-string-key';
@@ -29,6 +28,9 @@ protected function configure(): void
;
}
+ /**
+ * @throws \Exception
+ */
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
diff --git a/src/Command/FieldIndexPlannerCommand.php b/src/Command/FieldIndexPlannerCommand.php
index 973b9fd..f845bb6 100644
--- a/src/Command/FieldIndexPlannerCommand.php
+++ b/src/Command/FieldIndexPlannerCommand.php
@@ -2,10 +2,9 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Command;
-
+use ParagonIE\CipherSweet\Exception\PlannerException;
use ParagonIE\CipherSweet\Planner\FieldIndexPlanner;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -30,6 +29,9 @@ protected function configure(): void
;
}
+ /**
+ * @throws PlannerException
+ */
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
diff --git a/src/Command/GenerateIndexesCommand.php b/src/Command/GenerateIndexesCommand.php
index 4b2e2d0..3cecb2c 100644
--- a/src/Command/GenerateIndexesCommand.php
+++ b/src/Command/GenerateIndexesCommand.php
@@ -2,9 +2,9 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Command;
+use Odandb\DoctrineCiphersweetEncryptionBundle\Exception\MissingPropertyFromReflectionException;
use Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexableFieldsService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -20,13 +20,14 @@
class GenerateIndexesCommand extends Command
{
/** @deprecated */
- protected static $defaultName = 'odb:enc:indexes';
+ protected static $defaultName = self::CONSOLE_CMD;
/** @deprecated */
protected static $defaultDescription = 'Determine the Blind Index plan for a given field.';
protected static string $defaultAlias = 'o:e:i';
protected const CONSOLE_ENTRYPOINT = 'bin/console';
+ protected const CONSOLE_CMD = 'odb:enc:indexes';
protected const NB_RUNNING_PROCESSES = 5;
protected const CHUNCKS = 50;
protected const SUBPROCESS_TIMEOUT = 600; // Timeout in seconds
@@ -35,11 +36,11 @@ class GenerateIndexesCommand extends Command
protected IndexableFieldsService $indexableFieldsService;
- public function __construct(IndexableFieldsService $indexableFieldsService, string $name = null)
+ public function __construct(IndexableFieldsService $indexableFieldsService)
{
- parent::__construct($name);
-
$this->indexableFieldsService = $indexableFieldsService;
+
+ parent::__construct();
}
protected function configure(): void
@@ -59,6 +60,9 @@ protected function configure(): void
;
}
+ /**
+ * @throws MissingPropertyFromReflectionException
+ */
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->io = new SymfonyStyle($input, $output);
@@ -98,6 +102,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return 0;
}
+ /**
+ * @return array
+ */
protected function validateParallelOptions(InputInterface $input): array
{
$optionsInError = [];
@@ -112,6 +119,11 @@ protected function validateParallelOptions(InputInterface $input): array
return $optionsInError;
}
+ /**
+ * @param array{nb_process: int, timeout: int, chuncks: int} $parallelConfig
+ *
+ * @throws MissingPropertyFromReflectionException
+ */
protected function initAndRunFiltersGenerationSubProcesses(string $className, array $parallelConfig): void
{
@@ -148,15 +160,15 @@ protected function initAndRunFiltersGenerationSubProcesses(string $className, ar
$this->io->success('Done in ' . (time() - $start).'s');
}
+ /**
+ * @param array $pools
+ */
private function runProcesses(array $pools): void
{
$finishedProcesses = [];
$isSomethingRunning = true;
while ($isSomethingRunning) {
$isSomethingRunning = false;
- /**
- * @var Process $process
- */
foreach ($pools as $key => $process) {
if ($process->isRunning()) {
$isSomethingRunning = true;
@@ -175,6 +187,9 @@ private function runProcesses(array $pools): void
}
}
+ /**
+ * @throws MissingPropertyFromReflectionException
+ */
protected function regenerateFiltersByFieldnameAndIds(string $className, ?string $fieldnames, ?string $ids, bool $purge = false): void
{
$fieldnamesAr = $fieldnames !== null ? explode(',', $fieldnames) : null;
@@ -188,7 +203,7 @@ protected function regenerateFiltersByFieldnameAndIds(string $className, ?string
}
$this->io->comment('Generating Indexes');
- $this->indexableFieldsService->handleFilterableFieldsForChunck($className, $idsAr, $contexts, false);
+ $this->indexableFieldsService->handleFilterableFieldsForChunck($className, $idsAr, $contexts);
if ($idsAr !== null) {
$this->io->success(sprintf('Done for %s class and %d ids', $className, count($idsAr)));
} else {
diff --git a/src/Configuration/EncryptedField.php b/src/Configuration/EncryptedField.php
index 4cd82de..2c8f7e3 100644
--- a/src/Configuration/EncryptedField.php
+++ b/src/Configuration/EncryptedField.php
@@ -2,7 +2,6 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Configuration;
use Attribute;
@@ -22,18 +21,18 @@ class EncryptedField
public int $filterBits = EncryptorInterface::DEFAULT_FILTER_BITS;
/** @readonly */
- public ?string $mappedTypedProperty = null;
+ public bool $indexable = true;
/** @readonly */
- public bool $indexable = true;
+ public ?string $mappedTypedProperty = null;
public function __construct(
int $filterBits = EncryptorInterface::DEFAULT_FILTER_BITS,
- ?string $mappedTypedProperty = null,
- bool $indexable = true
+ bool $indexable = true,
+ ?string $mappedTypedProperty = null
) {
$this->filterBits = $filterBits;
- $this->mappedTypedProperty = $mappedTypedProperty;
$this->indexable = $indexable;
+ $this->mappedTypedProperty = $mappedTypedProperty;
}
}
diff --git a/src/Configuration/IndexableField.php b/src/Configuration/IndexableField.php
index 5e0c421..2c105de 100644
--- a/src/Configuration/IndexableField.php
+++ b/src/Configuration/IndexableField.php
@@ -2,7 +2,6 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Configuration;
use Attribute;
@@ -24,7 +23,10 @@ class IndexableField
/** @readonly */
public bool $autoRefresh = true;
- /** @readonly */
+ /**
+ * @readonly
+ * @var array
+ */
public array $indexesGenerationMethods = [];
/** @readonly */
diff --git a/src/DependencyInjection/Compiler/IndexGeneratorPass.php b/src/DependencyInjection/Compiler/IndexGeneratorPass.php
new file mode 100644
index 0000000..81bda69
--- /dev/null
+++ b/src/DependencyInjection/Compiler/IndexGeneratorPass.php
@@ -0,0 +1,49 @@
+has('encryption.indexes_generator')) {
+ return;
+ }
+
+ $indexesGeneratorDefinition = $container->getDefinition('encryption.indexes_generator');
+
+ $mapping = [];
+ foreach ($container->findTaggedServiceIds('encryption.index_generator') as $id => $attributes) {
+ $class = $container->getDefinition($id)->getClass();
+ if (method_exists($class, 'getIndexKey')) {
+ $mapping[$class::getIndexKey()] = new Reference($id);
+
+ continue;
+ }
+
+ $mapping[$attributes['key']] = new Reference($id);
+ }
+
+ $services = $container->findTaggedServiceIds('odb.index_generator');
+ if (\count($services) > 0) {
+ trigger_deprecation(
+ 'odandb/doctrine-ciphersweet-encryption-bundle',
+ '0.11',
+ 'The tag "odb.index_generator" is deprecated and will be remove in doctrine-ciphersweet-encryption-bundle 1.0'
+ );
+
+ foreach ($services as $id => $attributes) {
+ $mapping[$attributes['key']] = new Reference($id);
+ }
+ }
+
+ $indexesGeneratorDefinition->replaceArgument(0, ServiceLocatorTagPass::register($container, $mapping));
+ }
+}
diff --git a/src/DependencyInjection/DoctrineCiphersweetEncryptionExtension.php b/src/DependencyInjection/DoctrineCiphersweetEncryptionExtension.php
index 9f34c7f..8548b1c 100644
--- a/src/DependencyInjection/DoctrineCiphersweetEncryptionExtension.php
+++ b/src/DependencyInjection/DoctrineCiphersweetEncryptionExtension.php
@@ -2,22 +2,22 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\DependencyInjection;
+use Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\IndexesGeneratorInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
-use Symfony\Component\DependencyInjection\Loader;
+use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
class DoctrineCiphersweetEncryptionExtension extends Extension
{
- /**
- * @inheritDoc
- */
- public function load(array $configs, ContainerBuilder $container)
+ public function load(array $configs, ContainerBuilder $container): void
{
- $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
- $loader->load('encryption-services.yml');
+ $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config'));
+ $loader->load('services.php');
+
+ $container->registerForAutoconfiguration(IndexesGeneratorInterface::class)
+ ->addTag('encryption.index_generator');
}
}
diff --git a/src/Encryptors/CiphersweetEncryptor.php b/src/Encryptors/CiphersweetEncryptor.php
index 5c862ed..c40719c 100644
--- a/src/Encryptors/CiphersweetEncryptor.php
+++ b/src/Encryptors/CiphersweetEncryptor.php
@@ -2,14 +2,18 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors;
use ParagonIE\CipherSweet\BlindIndex;
use ParagonIE\CipherSweet\CipherSweet;
use ParagonIE\CipherSweet\EncryptedField;
+use ParagonIE\CipherSweet\Exception\BlindIndexNameCollisionException;
+use ParagonIE\CipherSweet\Exception\BlindIndexNotFoundException;
+use ParagonIE\CipherSweet\Exception\CipherSweetException;
+use ParagonIE\CipherSweet\Exception\CryptoOperationException;
+use Symfony\Contracts\Service\ResetInterface;
-class CiphersweetEncryptor implements EncryptorInterface
+class CiphersweetEncryptor implements EncryptorInterface, ResetInterface
{
private CipherSweet $engine;
@@ -23,6 +27,15 @@ public function __construct(CipherSweet $engine)
$this->biCache = [];
}
+ /**
+ * {@inheritdoc}
+ *
+ * @throws CipherSweetException
+ * @throws CryptoOperationException
+ * @throws BlindIndexNotFoundException
+ * @throws BlindIndexNameCollisionException
+ * @throws \SodiumException
+ */
public function prepareForStorage(object $entity, string $fieldName, string $string, bool $index = true, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): array
{
$entitClassName = \get_class($entity);
@@ -54,6 +67,13 @@ public function prepareForStorage(object $entity, string $fieldName, string $str
return $this->doEncrypt($entitClassName, $fieldName, $string, $index, $filterBits, $fastIndexing);
}
+ /**
+ * @throws CipherSweetException
+ * @throws CryptoOperationException
+ * @throws BlindIndexNotFoundException
+ * @throws BlindIndexNameCollisionException
+ * @throws \SodiumException
+ */
protected function doEncrypt(string $entitClassName, string $fieldName, string $string, bool $index = true, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): array
{
$encryptedField = (new EncryptedField($this->engine, $entitClassName, $fieldName));
@@ -65,9 +85,11 @@ protected function doEncrypt(string $entitClassName, string $fieldName, string $
$result = $encryptedField->prepareForStorage($string);
+ // Cache for encrypt/decrypt
$this->cache[$entitClassName][$fieldName][$string] = $result[0];
$this->cache[$entitClassName][$fieldName][$result[0]] = $string;
+ // Cache blind index
if ($index) {
$this->biCache[$entitClassName][$fieldName][$string] = $result[1][$fieldName.'_bi'];
}
@@ -75,32 +97,51 @@ protected function doEncrypt(string $entitClassName, string $fieldName, string $
return $result;
}
- public function decrypt(string $entity_classname, string $fieldName, string $string, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string
+ /**
+ * {@inheritdoc}
+ *
+ * @throws CipherSweetException
+ * @throws CryptoOperationException
+ */
+ public function decrypt(string $entityClassName, string $fieldName, string $string, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string
{
// If $string is not encrypted, we return it as is.
if (!$this->isValueEncrypted($string)) {
return $string;
}
- if (isset($this->cache[$entity_classname][$fieldName][$string])) {
- return $this->cache[$entity_classname][$fieldName][$string];
+ if (isset($this->cache[$entityClassName][$fieldName][$string])) {
+ return $this->cache[$entityClassName][$fieldName][$string];
}
- return $this->doDecrypt($entity_classname, $fieldName, $string);
+ return $this->doDecrypt($entityClassName, $fieldName, $string);
}
- protected function doDecrypt(string $entity_classname, string $fieldName, string $string): string
+ /**
+ * @throws CipherSweetException
+ * @throws CryptoOperationException
+ */
+ protected function doDecrypt(string $entityClassName, string $fieldName, string $string): string
{
- $decryptedValue = (new EncryptedField($this->engine, $entity_classname, $fieldName))
+ $decryptedValue = (new EncryptedField($this->engine, $entityClassName, $fieldName))
->decryptValue($string);
- $this->cache[$entity_classname][$fieldName][$string] = $decryptedValue;
- $this->cache[$entity_classname][$fieldName][$decryptedValue] = $string;
+ $this->cache[$entityClassName][$fieldName][$string] = $decryptedValue;
+ $this->cache[$entityClassName][$fieldName][$decryptedValue] = $string;
return $decryptedValue;
}
- public function getBlindIndex($entityName, $fieldName, string $value, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string
+ /**
+ * {@inheritdoc}
+ *
+ * @throws CryptoOperationException
+ * @throws CipherSweetException
+ * @throws BlindIndexNotFoundException
+ * @throws BlindIndexNameCollisionException
+ * @throws \SodiumException
+ */
+ public function getBlindIndex(string $entityName, string $fieldName, string $value, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string
{
if (isset($this->biCache[$entityName][$fieldName][$value])) {
return $this->biCache[$entityName][$fieldName][$value];
@@ -109,7 +150,14 @@ public function getBlindIndex($entityName, $fieldName, string $value, int $filte
return $this->doGetBlindIndex($entityName, $fieldName, $value, $filterBits, $fastIndexing);
}
- private function doGetBlindIndex($entityName, $fieldName, string $value, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string
+ /**
+ * @throws CryptoOperationException
+ * @throws CipherSweetException
+ * @throws BlindIndexNotFoundException
+ * @throws BlindIndexNameCollisionException
+ * @throws \SodiumException
+ */
+ protected function doGetBlindIndex(string $entityName, string $fieldName, string $value, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string
{
$index = (new EncryptedField($this->engine, $entityName, $fieldName))
->addBlindIndex(
@@ -122,6 +170,9 @@ private function doGetBlindIndex($entityName, $fieldName, string $value, int $fi
return $index;
}
+ /**
+ * {@inheritdoc}
+ */
public function getPrefix(): string
{
return $this->engine->getBackend()->getPrefix();
@@ -132,5 +183,9 @@ public function isValueEncrypted(?string $value): bool
return $value !== null && strpos($value, $this->getPrefix()) === 0;
}
-
+ public function reset(): void
+ {
+ $this->cache = [];
+ $this->biCache = [];
+ }
}
diff --git a/src/Encryptors/EncryptorInterface.php b/src/Encryptors/EncryptorInterface.php
index 252298b..d99b691 100644
--- a/src/Encryptors/EncryptorInterface.php
+++ b/src/Encryptors/EncryptorInterface.php
@@ -2,7 +2,6 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors;
use ParagonIE\CipherSweet\CipherSweet;
@@ -14,12 +13,26 @@ interface EncryptorInterface
public function __construct(CipherSweet $engine);
+ /**
+ * Encrypt a value and calculate this blind indices
+ *
+ * @return array{0:string, 1: array}
+ */
public function prepareForStorage(object $entity, string $fieldName, string $string, bool $index = true, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): array;
- public function decrypt(string $entity_classname, string $fieldName, string $string, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string;
+ /**
+ * Decrypt a value
+ */
+ public function decrypt(string $entityClassName, string $fieldName, string $string, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string;
- public function getBlindIndex($entityName, $fieldName, string $value, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string;
+ /**
+ * Get the blind index of the field
+ */
+ public function getBlindIndex(string $entityName, string $fieldName, string $value, int $filterBits = self::DEFAULT_FILTER_BITS, bool $fastIndexing = self::DEFAULT_FAST_INDEXING): string;
+ /**
+ * Get the prefix of the encryptor
+ */
public function getPrefix(): string;
public function isValueEncrypted(?string $value): bool;
diff --git a/src/Entity/IndexedEntityAttributeTrait.php b/src/Entity/IndexedEntityAttributeTrait.php
index 502868b..ee10b87 100644
--- a/src/Entity/IndexedEntityAttributeTrait.php
+++ b/src/Entity/IndexedEntityAttributeTrait.php
@@ -2,7 +2,6 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
diff --git a/src/Entity/IndexedEntityInterface.php b/src/Entity/IndexedEntityInterface.php
index 7b4351e..45688ff 100644
--- a/src/Entity/IndexedEntityInterface.php
+++ b/src/Entity/IndexedEntityInterface.php
@@ -2,10 +2,8 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Entity;
-
interface IndexedEntityInterface
{
public function setTargetEntity(?object $targetEntity): self;
diff --git a/src/Entity/IndexedEntityTrait.php b/src/Entity/IndexedEntityTrait.php
index bb6a583..9a4377e 100644
--- a/src/Entity/IndexedEntityTrait.php
+++ b/src/Entity/IndexedEntityTrait.php
@@ -2,7 +2,6 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
diff --git a/src/Exception/MissingPropertyFromReflectionException.php b/src/Exception/MissingPropertyFromReflectionException.php
index 60da974..a649e51 100644
--- a/src/Exception/MissingPropertyFromReflectionException.php
+++ b/src/Exception/MissingPropertyFromReflectionException.php
@@ -2,11 +2,8 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Exception;
-
class MissingPropertyFromReflectionException extends \Exception
{
-
}
diff --git a/src/Exception/UndefinedGeneratorException.php b/src/Exception/UndefinedGeneratorException.php
index 342a3b0..6e3eea9 100644
--- a/src/Exception/UndefinedGeneratorException.php
+++ b/src/Exception/UndefinedGeneratorException.php
@@ -2,11 +2,8 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Exception;
-
class UndefinedGeneratorException extends \Exception
{
-
}
diff --git a/src/OdandbDoctrineCiphersweetEncryptionBundle.php b/src/OdandbDoctrineCiphersweetEncryptionBundle.php
index 235ed13..2eb9ba7 100644
--- a/src/OdandbDoctrineCiphersweetEncryptionBundle.php
+++ b/src/OdandbDoctrineCiphersweetEncryptionBundle.php
@@ -2,12 +2,12 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle;
-
+use Odandb\DoctrineCiphersweetEncryptionBundle\DependencyInjection\Compiler\IndexGeneratorPass;
use Odandb\DoctrineCiphersweetEncryptionBundle\DependencyInjection\DoctrineCiphersweetEncryptionExtension;
-use Symfony\Component\DependencyInjection\Extension\Extension;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Extension\ExtensionInterface;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class OdandbDoctrineCiphersweetEncryptionBundle extends Bundle
@@ -15,7 +15,7 @@ class OdandbDoctrineCiphersweetEncryptionBundle extends Bundle
/**
* Overridden to allow for the custom extension alias.
*/
- public function getContainerExtension(): Extension
+ public function getContainerExtension(): ?ExtensionInterface
{
if (null === $this->extension) {
$this->extension = new DoctrineCiphersweetEncryptionExtension();
@@ -23,4 +23,14 @@ public function getContainerExtension(): Extension
return $this->extension;
}
+
+ public function build(ContainerBuilder $container): void
+ {
+ $container->addCompilerPass(new IndexGeneratorPass());
+ }
+
+ public function getPath(): string
+ {
+ return \dirname(__DIR__);
+ }
}
diff --git a/src/Resources/config/encryption-services.yml b/src/Resources/config/encryption-services.yml
deleted file mode 100644
index a507c9e..0000000
--- a/src/Resources/config/encryption-services.yml
+++ /dev/null
@@ -1,114 +0,0 @@
-services:
- Odandb\DoctrineCiphersweetEncryptionBundle\Subscribers\DoctrineCiphersweetSubscriber:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Subscribers\DoctrineCiphersweetSubscriber
- arguments:
- - "@annotation_reader"
- - "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Services\\EncryptedFieldsService"
- - "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Encryptors\\CiphersweetEncryptor"
- - "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Services\\IndexableFieldsService"
- - "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Services\\PropertyHydratorService"
- tags:
- - { name: doctrine.event_subscriber }
-
- ParagonIE\CipherSweet\KeyProvider\StringProvider:
- class: ParagonIE\CipherSweet\KeyProvider\StringProvider
- public: true
- arguments: ["%env(DOCTRINE_CIPHERSWEET_KEY)%"]
-
- ParagonIE\CipherSweet\CipherSweet:
- class: ParagonIE\CipherSweet\CipherSweet
- public: true
- arguments: ["@ParagonIE\\CipherSweet\\KeyProvider\\StringProvider"]
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\CiphersweetEncryptor:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\CiphersweetEncryptor
- public: true
- arguments: ["@ParagonIE\\CipherSweet\\CipherSweet"]
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Services\EncryptedFieldsService:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Services\EncryptedFieldsService
- public: true
- arguments:
- - "@annotation_reader"
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexableFieldsService:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexableFieldsService
- public: true
- arguments:
- - "@annotation_reader"
- - "@Doctrine\\ORM\\EntityManagerInterface"
- - "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Services\\IndexesGenerator"
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Command\CheckEncryptionCommand:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Command\CheckEncryptionCommand
- public: true
- arguments:
- - "@Doctrine\\ORM\\EntityManagerInterface"
- - "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Services\\EncryptedFieldsService"
- tags:
- - { name: console.command }
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Command\EncryptionDataCommand:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Command\EncryptionDataCommand
- public: true
- arguments:
- - "@Doctrine\\ORM\\EntityManagerInterface"
- - "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Encryptors\\EncryptorInterface"
- tags:
- - { name: console.command }
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Command\GenerateIndexesCommand:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Command\GenerateIndexesCommand
- public: true
- arguments:
- - "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Services\\IndexableFieldsService"
- tags:
- - { name: console.command }
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Command\FieldIndexPlannerCommand:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Command\FieldIndexPlannerCommand
- public: true
- tags:
- - { name: console.command }
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Command\EncryptionKeyStringProviderGenerator:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Command\EncryptionKeyStringProviderGenerator
- public: true
- tags:
- - { name: console.command }
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\TokenizerGenerator:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\TokenizerGenerator
- tags:
- - { name: 'odb.index_generator', key: 'TokenizerGenerator' }
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\ValueStartingByGenerator:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\ValueStartingByGenerator
- tags:
- - { name: 'odb.index_generator', key: 'ValueStartingByGenerator'}
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\ValueEndingByGenerator:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\ValueEndingByGenerator
- tags:
- - { name: 'odb.index_generator', key: 'ValueEndingByGenerator' }
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerator:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerator
- public: true
- arguments:
- - !tagged_locator { tag: 'odb.index_generator', index_by: 'key' }
- - "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Encryptors\\EncryptorInterface"
- tags:
- - { name: container.service_subscriber }
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Services\PropertyHydratorService:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Services\PropertyHydratorService
- arguments:
- - '@property_info'
- - '@property_accessor'
-
- Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\EncryptorInterface:
- class: Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\CiphersweetEncryptor
- public: true
- arguments:
- - "@ParagonIE\\CipherSweet\\CipherSweet"
diff --git a/src/Services/EncryptedFieldsService.php b/src/Services/EncryptedFieldsService.php
index 1f99606..660e73f 100644
--- a/src/Services/EncryptedFieldsService.php
+++ b/src/Services/EncryptedFieldsService.php
@@ -12,9 +12,11 @@
class EncryptedFieldsService
{
+ /** @deprecated */
private Reader $annReader;
- public function __construct(Reader $annReader) {
+ public function __construct(Reader $annReader)
+ {
$this->annReader = $annReader;
}
diff --git a/src/Services/IndexableFieldsService.php b/src/Services/IndexableFieldsService.php
index 0a89f16..fd21a9e 100644
--- a/src/Services/IndexableFieldsService.php
+++ b/src/Services/IndexableFieldsService.php
@@ -2,34 +2,37 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Services;
-
-use Doctrine\ORM\EntityRepository;
-use Odandb\DoctrineCiphersweetEncryptionBundle\Configuration\EncryptedField;
use Odandb\DoctrineCiphersweetEncryptionBundle\Configuration\IndexableField;
-use Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\EncryptorInterface;
use Odandb\DoctrineCiphersweetEncryptionBundle\Entity\IndexedEntityInterface;
use Odandb\DoctrineCiphersweetEncryptionBundle\Exception\MissingPropertyFromReflectionException;
use Doctrine\Common\Annotations\Reader;
use Doctrine\ORM\EntityManagerInterface;
+use Odandb\DoctrineCiphersweetEncryptionBundle\Exception\UndefinedGeneratorException;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class IndexableFieldsService
{
public const INDEXABLE_ANN_NAME = IndexableField::class;
- private Reader $annReader;
+ /** @deprecated */
+ private ?Reader $annReader;
private EntityManagerInterface $em;
private IndexesGenerator $indexesGenerator;
+ private PropertyAccessorInterface $propertyAccessor;
- public function __construct(Reader $annReader, EntityManagerInterface $em, IndexesGenerator $generator)
+ public function __construct(?Reader $annReader, EntityManagerInterface $em, IndexesGenerator $generator, PropertyAccessorInterface $propertyAccessor)
{
$this->annReader = $annReader;
$this->em = $em;
$this->indexesGenerator = $generator;
+ $this->propertyAccessor = $propertyAccessor;
}
+ /**
+ * Chunks all data ID of the entity
+ */
public function getChunksForMultiThread(string $className, int $chuncksLength): array
{
$repo = $this->em->getRepository($className);
@@ -41,22 +44,28 @@ public function getChunksForMultiThread(string $className, int $chuncksLength):
return array_chunk(array_column($result, 'id'), $chuncksLength);
}
- public function buildContext(string $className, ?array $fieldnames): array
+ /**
+ * @param null|array $fieldNames
+ *
+ * @return array
+ *
+ * @throws MissingPropertyFromReflectionException
+ */
+ public function buildContext(string $className, ?array $fieldNames): array
{
$contexts = [];
$classMetadata = $this->em->getClassMetadata($className);
- if ($fieldnames === [] || $fieldnames === null) {
- $fieldnames = array_map(
+ if (empty($fieldNames)) {
+ $fieldNames = array_map(
static function (\ReflectionProperty $refProperty): string {return $refProperty->name;},
$classMetadata->getReflectionProperties()
);
}
- foreach ($fieldnames as $fieldname) {
+ foreach ($fieldNames as $fieldname) {
$refProperty = $classMetadata->getReflectionProperty($fieldname);
-
if ($refProperty === null) {
throw new MissingPropertyFromReflectionException(sprintf("No refProperty found for fieldname %s", $fieldname));
}
@@ -66,7 +75,7 @@ static function (\ReflectionProperty $refProperty): string {return $refProperty-
$indexableAnnotationConfig = $refAttribute->newInstance();
}
- if (null === $indexableAnnotationConfig) {
+ if (null === $indexableAnnotationConfig && null !== $this->annReader) {
$indexableAnnotationConfig = $this->annReader->getPropertyAnnotation($refProperty, self::INDEXABLE_ANN_NAME);
if (PHP_VERSION_ID >= 80000) {
trigger_deprecation(
@@ -85,20 +94,23 @@ static function (\ReflectionProperty $refProperty): string {return $refProperty-
return $contexts;
}
+ /**
+ * Remove all (or by ids) the search possibilities of an entity field
+ *
+ * @param array $fieldsContexts
+ * @param null|array $ids
+ */
public function purgeFiltersForContextAndIds(array $fieldsContexts, ?array $ids): void
{
- /**
- * @var \ReflectionProperty $refProperty
- * @var IndexableField $indexableAnnotationConfig
- */
foreach($fieldsContexts as ['refProperty' => $refProperty, 'indexableConfig' => $indexableAnnotationConfig]) {
$qb = $this->em->createQueryBuilder()
->delete()
- ->from($indexableAnnotationConfig->indexesEntityClass, 'f');
- $qb->where('f.fieldname=:fieldname')
- ->setParameter('fieldname', $refProperty->name);
+ ->from($indexableAnnotationConfig->indexesEntityClass, 'f')
+ ->where('f.fieldname=:fieldname')
+ ->setParameter('fieldname', $refProperty->name)
+ ;
- if ($ids !== null && $ids !== []) {
+ if (!empty($ids)) {
$qb->andWhere('f.targetEntity IN (:ids)')
->setParameter('ids', $ids);
}
@@ -108,13 +120,14 @@ public function purgeFiltersForContextAndIds(array $fieldsContexts, ?array $ids)
}
/**
- * @throws \ReflectionException
- * @throws \Odandb\DoctrineCiphersweetEncryptionBundle\Exception\UndefinedGeneratorException
+ * Generate and save all (or by ids) the search possibilities of an entity
+ *
+ * @param null|array $ids
+ * @param array $fieldsContexts
*/
public function handleFilterableFieldsForChunck(string $className, ?array $ids, array $fieldsContexts, bool $needsToComputeChangeset = false): void
{
- $criteria = $ids !== null && $ids !== [] ? ['id' => $ids] : [];
- $chunck = $this->em->getRepository($className)->findBy($criteria);
+ $chunck = $this->em->getRepository($className)->findBy(!empty($ids) ? ['id' => $ids] : []);
foreach ($chunck as $entity) {
$this->handleIndexableFieldsForEntity($entity, $fieldsContexts, $needsToComputeChangeset);
$this->em->flush();
@@ -122,61 +135,17 @@ public function handleFilterableFieldsForChunck(string $className, ?array $ids,
}
/**
- * Permet de générer les valeurs indexables pour une entité et un contexte donné.
+ * Generate and save the search possibilities of an entity field
*
- * @param object $entity
- * @param array $fieldsContexts
- * @return array
- * @throws \Odandb\DoctrineCiphersweetEncryptionBundle\Exception\UndefinedGeneratorException
- */
- public function generateIndexableValuesForEntity(object $entity, array $fieldsContexts): array
- {
- $searchIndexes = [];
-
- foreach ($fieldsContexts as ['refProperty' => $refProperty, 'indexableConfig' => $indexableAnnotationConfig]) {
- $value = $refProperty->getValue($entity);
- if ($value === null || $value === '') {
- continue;
- }
-
- $cleanValue = $value;
- $valueCleanerMethod = $indexableAnnotationConfig->valuePreprocessMethod;
- if ($valueCleanerMethod !== null && (method_exists($entity, $valueCleanerMethod) || method_exists(get_class($entity), $valueCleanerMethod))) {
- $cleanValue = $entity->$valueCleanerMethod($value);
- }
-
- // On appelle le service de génération des index de filtre qui va créer la collection de pattern possibles
- // en fonction de la ou des méthodes renseignées en annotation
- // Puis récupérer chaque "blind_index" associé à enregistrer en base
- $indexesMethods = $indexableAnnotationConfig->indexesGenerationMethods;
-
- $indexesToEncrypt = $this->indexesGenerator->generateAndEncryptFilters($cleanValue, $indexesMethods);
- $indexesToEncrypt [] = $value;
- $indexesToEncrypt = array_unique($indexesToEncrypt);
-
- $searchIndexes[$refProperty->getName()] = $indexesToEncrypt;
- }
-
- return $searchIndexes;
- }
-
- /**
- * @param object $entity
- * @param array{'refProperty': \ReflectionProperty, 'indexableConfig': IndexableField} $fieldsContexts
- * @param bool $needsToComputeChangeset
+ * @param array{refProperty: \ReflectionProperty, indexableConfig: IndexableField} $fieldsContexts
*
- * @throws \Odandb\DoctrineCiphersweetEncryptionBundle\Exception\UndefinedGeneratorException
- * @throws \ReflectionException
+ * @throws UndefinedGeneratorException|\ReflectionException
*/
public function handleIndexableFieldsForEntity(object $entity, array $fieldsContexts, bool $needsToComputeChangeset = false): void
{
+ $className = get_class($entity);
$searchIndexes = $this->generateIndexableValuesForEntity($entity, $fieldsContexts);
- /**
- * @var \ReflectionProperty $refProperty
- * @var EncryptedField $annotationConfig
- * @var IndexableField $indexableAnnotationConfig
- */
foreach ($fieldsContexts as ['refProperty' => $refProperty, 'indexableConfig' => $indexableAnnotationConfig]) {
if (!isset($searchIndexes[$refProperty->getName()])) {
continue;
@@ -184,9 +153,9 @@ public function handleIndexableFieldsForEntity(object $entity, array $fieldsCont
$indexesToEncrypt = $searchIndexes[$refProperty->getName()];
- $indexes = $this->indexesGenerator->generateBlindIndexesFromPossibleValues(get_class($entity), $refProperty->getName(), $indexesToEncrypt, $indexableAnnotationConfig->fastIndexing);
+ $indexes = $this->indexesGenerator->generateBlindIndexesFromPossibleValues($className, $refProperty->getName(), $indexesToEncrypt, $indexableAnnotationConfig->fastIndexing);
- // On crée les instances d'objet filtre et on les associe à l'entité parente
+ // We create the filter object instances and associate them to the parent entity
$indexEntities = [];
$indexEntityClass = $indexableAnnotationConfig->indexesEntityClass;
@@ -201,13 +170,57 @@ public function handleIndexableFieldsForEntity(object $entity, array $fieldsCont
$indexEntities [] = $indexEntity;
$this->em->persist($indexEntity);
+
if ($needsToComputeChangeset) {
$this->em->getUnitOfWork()->computeChangeSet($classMetadata, $indexEntity);
}
}
}
+
$setter = 'set' . $refClass->getShortName();
- $entity->$setter($indexEntities);
+ if ($this->propertyAccessor->isWritable($entity, $setter)) {
+ $this->propertyAccessor->setValue($entity, $setter, $indexEntities);
+ }
}
}
+
+ /**
+ * Generate the search possibilities of an entity field
+ *
+ * @param array{refProperty: \ReflectionProperty, indexableConfig: IndexableField} $fieldsContexts
+ *
+ * @return array>
+ *
+ * @throws UndefinedGeneratorException
+ */
+ public function generateIndexableValuesForEntity(object $entity, array $fieldsContexts): array
+ {
+ $searchIndexes = [];
+
+ foreach ($fieldsContexts as ['refProperty' => $refProperty, 'indexableConfig' => $indexableAnnotationConfig]) {
+ $value = $refProperty->getValue($entity);
+ if ($value === null || $value === '') {
+ continue;
+ }
+
+ $cleanValue = $value;
+ $valueCleanerMethod = $indexableAnnotationConfig->valuePreprocessMethod;
+ if ($valueCleanerMethod !== null && method_exists($entity, $valueCleanerMethod)) {
+ $cleanValue = $entity->$valueCleanerMethod($value);
+ }
+
+ // We call the filter index generation service which will create the collection of possible patterns
+ // according to the method(s) specified in the annotation
+ // Then retrieve each associated "blind_index" to save in database
+ $indexesMethods = $indexableAnnotationConfig->indexesGenerationMethods;
+
+ $indexesToEncrypt = $this->indexesGenerator->generateAndEncryptFilters($cleanValue, $indexesMethods);
+ $indexesToEncrypt[] = $value;
+ $indexesToEncrypt = array_unique($indexesToEncrypt);
+
+ $searchIndexes[$refProperty->getName()] = $indexesToEncrypt;
+ }
+
+ return $searchIndexes;
+ }
}
diff --git a/src/Services/IndexesGenerator.php b/src/Services/IndexesGenerator.php
index 78d7a80..4ce3126 100644
--- a/src/Services/IndexesGenerator.php
+++ b/src/Services/IndexesGenerator.php
@@ -2,45 +2,33 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Services;
-
use Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\EncryptorInterface;
use Odandb\DoctrineCiphersweetEncryptionBundle\Exception\UndefinedGeneratorException;
use Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\IndexesGeneratorInterface;
-use Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\ValueStartingByGenerator;
-use Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators\ValueEndingByGenerator;
-use Psr\Container\ContainerInterface;
-use Symfony\Contracts\Service\ServiceSubscriberInterface;
+use Symfony\Component\DependencyInjection\ServiceLocator;
-class IndexesGenerator implements ServiceSubscriberInterface
+class IndexesGenerator
{
protected EncryptorInterface $encryptor;
- protected ContainerInterface $container;
+ protected ServiceLocator $container;
- public function __construct(ContainerInterface $container, EncryptorInterface $encryptor)
+ public function __construct(ServiceLocator $container, EncryptorInterface $encryptor)
{
$this->container = $container;
$this->encryptor = $encryptor;
}
/**
- * @required
+ * Generates all possible search for the value
+ *
+ * @param string[] $methods
+ *
+ * @return string[]
+ *
+ * @throws UndefinedGeneratorException
*/
- public function setContainer(ContainerInterface $container): void
- {
- $this->container = $container;
- }
-
- public static function getSubscribedServices(): array
- {
- return [
- 'ValueStartingByGenerator' => '?'.ValueStartingByGenerator::class,
- 'ValueEndingByGenerator' => '?'.ValueEndingByGenerator::class,
- ];
- }
-
public function generateAndEncryptFilters(string $value, array $methods): array
{
$possibleValuesAr = [$value];
@@ -53,7 +41,7 @@ public function generateAndEncryptFilters(string $value, array $methods): array
}
$generator = $this->container->get($method);
- if ($generator instanceof IndexesGeneratorInterface === false) {
+ if (!$generator instanceof IndexesGeneratorInterface) {
throw new \TypeError(sprintf("The generator is not an instance of %s", IndexesGeneratorInterface::class));
}
@@ -65,13 +53,13 @@ public function generateAndEncryptFilters(string $value, array $methods): array
}
/**
- * @param string $entityName
- * @param string $fieldname
+ * Generates all blind indexes for the all possible values
+ *
* @param string[] $possibleValues
- * @param bool $fastIndexing
- * @return array
+ *
+ * @return array
*/
- public function generateBlindIndexesFromPossibleValues(string $entityName, string $fieldname, array $possibleValues, bool $fastIndexing): array
+ public function generateBlindIndexesFromPossibleValues(string $entityName, string $fieldName, array $possibleValues, bool $fastIndexing): array
{
$possibleValues = array_unique($possibleValues);
@@ -80,7 +68,7 @@ public function generateBlindIndexesFromPossibleValues(string $entityName, strin
if ($pvalue === '' || $pvalue === null) {
continue;
}
- $indexes[] = $this->encryptor->getBlindIndex($entityName, $fieldname, $pvalue, EncryptorInterface::DEFAULT_FILTER_BITS, $fastIndexing);
+ $indexes[] = $this->encryptor->getBlindIndex($entityName, $fieldName, $pvalue, EncryptorInterface::DEFAULT_FILTER_BITS, $fastIndexing);
}
return $indexes;
diff --git a/src/Services/IndexesGenerators/IndexesGeneratorInterface.php b/src/Services/IndexesGenerators/IndexesGeneratorInterface.php
index bf2856f..c0c6f57 100644
--- a/src/Services/IndexesGenerators/IndexesGeneratorInterface.php
+++ b/src/Services/IndexesGenerators/IndexesGeneratorInterface.php
@@ -2,11 +2,15 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators;
-
+/**
+ * @method static string getIndexKey()
+ */
interface IndexesGeneratorInterface
{
+ /**
+ * @return array
+ */
public function generate(string $value): array;
}
diff --git a/src/Services/IndexesGenerators/TokenizerGenerator.php b/src/Services/IndexesGenerators/TokenizerGenerator.php
index 92d6f76..547166b 100644
--- a/src/Services/IndexesGenerators/TokenizerGenerator.php
+++ b/src/Services/IndexesGenerators/TokenizerGenerator.php
@@ -2,19 +2,18 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators;
-
-class TokenizerGenerator
+class TokenizerGenerator implements IndexesGeneratorInterface
{
- /**
- * @param string $string
- * @return array
- */
- public function generate(string $string): array
+ public function generate(string $value): array
+ {
+ $value = trim(preg_replace(array('/[^a-zA-Z0-9\-]/', '/\s+/'), ' ', $value));
+ return explode(' ', $value);
+ }
+
+ public static function getIndexKey(): string
{
- $string = trim(preg_replace(array('/[^a-zA-Z0-9\-]/', '/\s+/'), ' ', $string));
- return explode(' ', $string);
+ return 'TokenizerGenerator';
}
}
diff --git a/src/Services/IndexesGenerators/ValueEndingByGenerator.php b/src/Services/IndexesGenerators/ValueEndingByGenerator.php
index 16cecfd..32d3339 100644
--- a/src/Services/IndexesGenerators/ValueEndingByGenerator.php
+++ b/src/Services/IndexesGenerators/ValueEndingByGenerator.php
@@ -2,15 +2,10 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators;
class ValueEndingByGenerator implements IndexesGeneratorInterface
{
- /**
- * @param string $value
- * @return array
- */
public function generate(string $value): array
{
$possibleValues = [];
@@ -21,4 +16,9 @@ public function generate(string $value): array
return $possibleValues;
}
+
+ public static function getIndexKey(): string
+ {
+ return 'ValueEndingByGenerator';
+ }
}
diff --git a/src/Services/IndexesGenerators/ValueStartingByGenerator.php b/src/Services/IndexesGenerators/ValueStartingByGenerator.php
index 96e86ea..7d8e15f 100644
--- a/src/Services/IndexesGenerators/ValueStartingByGenerator.php
+++ b/src/Services/IndexesGenerators/ValueStartingByGenerator.php
@@ -2,7 +2,6 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexesGenerators;
class ValueStartingByGenerator implements IndexesGeneratorInterface
@@ -17,4 +16,9 @@ public function generate(string $value): array
return $possibleValues;
}
+
+ public static function getIndexKey(): string
+ {
+ return 'ValueStartingByGenerator';
+ }
}
diff --git a/src/Services/PropertyHydratorService.php b/src/Services/PropertyHydratorService.php
index 1b9f25b..d9c7857 100644
--- a/src/Services/PropertyHydratorService.php
+++ b/src/Services/PropertyHydratorService.php
@@ -2,11 +2,8 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Services;
-
-use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
@@ -14,20 +11,16 @@
class PropertyHydratorService
{
private PropertyInfoExtractorInterface $propertyInfoExtractor;
-
private PropertyAccessorInterface $propertyAccessor;
- public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, PropertyAccessorInterface $propertyAccessor = null)
+ public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, PropertyAccessorInterface $propertyAccessor)
{
$this->propertyInfoExtractor = $propertyInfoExtractor;
- $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
+ $this->propertyAccessor = $propertyAccessor;
}
/**
- * @param object $entity
- * @param string|null $propertyName
* @param mixed $value
- * @return string
*/
public function getMappedFieldValueAsString(object $entity, ?string $propertyName, $value): string
{
@@ -38,22 +31,19 @@ public function getMappedFieldValueAsString(object $entity, ?string $propertyNam
return (string) $value;
}
- /**
- * @param object $entity
- * @param string $value
- * @param string|null $propertyName
- */
public function setValueToMappedField(object $entity, string $value, ?string $propertyName): void
{
- if ($propertyName !== null) {
- $propertyInfoType = $this->propertyInfoExtractor->getTypes(get_class($entity), $propertyName)[0];
- $targetType = $propertyInfoType->getBuiltinType();
+ if ($propertyName === null) {
+ return;
+ }
- if ($targetType !== Type::BUILTIN_TYPE_STRING) {
- settype($value, $targetType);
- }
+ $propertyInfoType = $this->propertyInfoExtractor->getTypes(get_class($entity), $propertyName)[0];
+ $targetType = $propertyInfoType->getBuiltinType();
- $this->propertyAccessor->setValue($entity, $propertyName, $value);
+ if ($targetType !== Type::BUILTIN_TYPE_STRING) {
+ settype($value, $targetType);
}
+
+ $this->propertyAccessor->setValue($entity, $propertyName, $value);
}
}
diff --git a/src/Subscribers/DoctrineCiphersweetSubscriber.php b/src/Subscribers/DoctrineCiphersweetSubscriber.php
index b329dc2..c78effe 100644
--- a/src/Subscribers/DoctrineCiphersweetSubscriber.php
+++ b/src/Subscribers/DoctrineCiphersweetSubscriber.php
@@ -2,9 +2,11 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Subscribers;
+use Doctrine\Bundle\DoctrineBundle\EventSubscriber\EventSubscriberInterface;
+use Doctrine\ORM\Event\PostLoadEventArgs;
+use Doctrine\Persistence\ObjectManager;
use Doctrine\ORM\UnitOfWork;
use Odandb\DoctrineCiphersweetEncryptionBundle\Configuration\EncryptedField;
use Odandb\DoctrineCiphersweetEncryptionBundle\Configuration\IndexableField;
@@ -13,33 +15,45 @@
use Odandb\DoctrineCiphersweetEncryptionBundle\Services\IndexableFieldsService;
use Odandb\DoctrineCiphersweetEncryptionBundle\Services\PropertyHydratorService;
use Doctrine\Common\Annotations\Reader;
-use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
-use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnClearEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Events;
+use Symfony\Contracts\Service\ResetInterface;
-/**
- *
- */
-class DoctrineCiphersweetSubscriber implements EventSubscriber
+class DoctrineCiphersweetSubscriber implements EventSubscriberInterface, ResetInterface
{
public const ENCRYPTED_ANN_NAME = EncryptedField::class;
public const INDEXABLE_ANN_NAME = IndexableField::class;
- private EncryptorInterface $encryptor;
+ /** @deprecated */
private Reader $annReader;
+ private EncryptorInterface $encryptor;
+ private IndexableFieldsService $indexableFieldsService;
+ private PropertyHydratorService $propertyHydratorService;
- private EncryptedFieldsService $encryptedFieldsService;
+ private EncryptedFieldsService $encryptedFieldsService;
- public array $_originalValues = [];
+ /**
+ * Caches the original encrypt value of an entity field
+ *
+ * @var array>
+ */
+ private array $_originalValues = [];
+ /**
+ * Cache the entities SPL ID that have already been decrypted
+ *
+ * @var array
+ */
private array $decodedRegistry = [];
+
/**
* Caches information on an entity's encrypted fields in an array keyed on
* the entity's class name. The value will be a list of Reflected fields that are encrypted.
+ *
+ * @var array
*/
private array $encryptedFieldCache = [];
@@ -47,18 +61,18 @@ class DoctrineCiphersweetSubscriber implements EventSubscriber
* Before flushing the objects out to the database, we modify their password value to the
* encrypted value. Since we want the password to remain decrypted on the entity after a flush,
* we have to write the decrypted value back to the entity.
+ *
+ * @var array}>
*/
private array $postFlushDecryptQueue = [];
- private array $entitiesToEncrypt = [];
-
- private IndexableFieldsService $indexableFieldsService;
-
- private PropertyHydratorService $propertyHydratorService;
-
/**
- * Initialization of subscriber.
+ * Entity that remains to be encrypted (converting an existing field to encryption)
+ *
+ * @var array
*/
+ private array $entitiesToEncrypt = [];
+
public function __construct(
Reader $annReader,
EncryptedFieldsService $encryptedFieldsService,
@@ -74,12 +88,49 @@ public function __construct(
$this->propertyHydratorService = $propertyHydratorService;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getSubscribedEvents(): array
+ {
+ return [
+ Events::postLoad,
+ Events::onFlush,
+ Events::postFlush,
+ Events::onClear,
+ ];
+ }
+
+ public function reset(): void
+ {
+ $this->_originalValues = [];
+ $this->decodedRegistry = [];
+ $this->encryptedFieldCache = [];
+ $this->postFlushDecryptQueue = [];
+ $this->entitiesToEncrypt = [];
+ }
+
+ /**
+ * Listen a postLoad lifecycle event. Checking and decrypt entities which have `EncryptedField` annotations/attributes.
+ */
+ public function postLoad(PostLoadEventArgs $args): void
+ {
+ $entity = $args->getObject();
+ if ($this->hasInDecodedRegistry($entity)) {
+ return;
+ }
+
+ if ($this->processFields($entity, $args->getObjectManager(), false)) {
+ $this->addToDecodedRegistry($entity);
+ }
+ }
+
/**
* Encrypt the password before it is written to the database.
*/
public function onFlush(OnFlushEventArgs $args): void
{
- $em = $args->getEntityManager();
+ $em = $args->getObjectManager();
$unitOfWork = $em->getUnitOfWork();
$this->postFlushDecryptQueue = [];
@@ -105,6 +156,31 @@ public function onFlush(OnFlushEventArgs $args): void
$this->entitiesToEncrypt = [];
}
+ /**
+ * After we have persisted the entities, we want to have the
+ * decrypted information available once more.
+ */
+ public function postFlush(PostFlushEventArgs $args): void
+ {
+ $unitOfWork = $args->getObjectManager()->getUnitOfWork();
+
+ foreach ($this->postFlushDecryptQueue as $pair) {
+ $fieldPairs = $pair['fields'];
+ $entity = $pair['entity'];
+ $oid = spl_object_id($entity);
+
+ foreach ($fieldPairs as $fieldPair) {
+ /** @var \ReflectionProperty $field */
+ $field = $fieldPair['field'];
+ $field->setValue($entity, $fieldPair['value']);
+ $unitOfWork->setOriginalEntityProperty($oid, $field->getName(), $fieldPair['value']);
+ }
+
+ $this->addToDecodedRegistry($entity);
+ }
+ $this->postFlushDecryptQueue = [];
+ }
+
public function onClear(OnClearEventArgs $args): void
{
unset($this->_originalValues, $this->decodedRegistry, $this->encryptedFieldCache, $this->postFlushDecryptQueue, $this->entitiesToEncrypt);
@@ -127,7 +203,6 @@ private function entityOnFlush(object $entity, EntityManagerInterface $em): void
$objId = spl_object_id($entity);
$fields = [];
-
$ecnryptedFields = $this->getEncryptedFields($entity, $em);
// If no encryptedFields detected we early return as we don't need to process anything
@@ -150,25 +225,20 @@ private function entityOnFlush(object $entity, EntityManagerInterface $em): void
}
/**
- * @param object $entity
- * @param EntityManagerInterface $em
- *
* @return \ReflectionProperty[]
*/
- private function getEncryptedFields(object $entity, EntityManagerInterface $em): array
+ private function getEncryptedFields(object $entity, ObjectManager $em): array
{
$className = \get_class($entity);
-
if (isset($this->encryptedFieldCache[$className])) {
return $this->encryptedFieldCache[$className];
}
$meta = $em->getClassMetadata($className);
- $encryptedFields = $this->encryptedFieldsService->getEncryptedFields($meta);
- $this->encryptedFieldCache[$className] = $encryptedFields;
+ $encryptedFields = $this->encryptedFieldsService->getEncryptedFields($meta);
- return $encryptedFields;
+ return $this->encryptedFieldCache[$className] = $encryptedFields;
}
/**
@@ -179,7 +249,7 @@ private function getEncryptedFields(object $entity, EntityManagerInterface $em):
* Make sure you call first $unitOfWork->computeChangeSet or $unitOfWork->recomputeSingleEntityChangeSet
* if you think your entity should be updated and has not been handled by entity manager.
*/
- public function processFields(object $entity, EntityManagerInterface $em, $isEncryptOperation = true, $force = null): bool
+ public function processFields(object $entity, ObjectManager $em, bool $isEncryptOperation = true, bool $force = false): bool
{
$properties = $this->getEncryptedFields($entity, $em);
$unitOfWork = $em->getUnitOfWork();
@@ -202,8 +272,7 @@ public function processFields(object $entity, EntityManagerInterface $em, $isEnc
$entityClassName = $em->getClassMetadata(get_class($entity))->getName();
foreach ($properties as $refProperty) {
- $value = $refProperty->getValue($entity) ?? '';
-
+ $value = $refProperty->getValue($entity);
if ($value === null) {
continue;
}
@@ -213,8 +282,7 @@ public function processFields(object $entity, EntityManagerInterface $em, $isEnc
if ($isEncryptOperation) {
$value = $this->handleEncryptOperation($entity, $oid, $value, $refProperty, $context, $force);
} else {
- $oldValue = $value;
- if (!$this->isValueEncrypted($oldValue)) {
+ if (!$this->isValueEncrypted($value)) {
$this->entitiesToEncrypt[$oid] = $entity;
}
$value = $this->handleDecryptOperation($oid, $value, $refProperty, $context);
@@ -229,13 +297,20 @@ public function processFields(object $entity, EntityManagerInterface $em, $isEnc
if (!$isEncryptOperation && !\defined('_DONOTENCRYPT')) {
//we don't want the object to be dirty immediately after reading
- $unitOfWork->setOriginalEntityProperty(spl_object_id($entity), $refProperty->getName(), $value);
+ $unitOfWork->setOriginalEntityProperty($oid, $refProperty->getName(), $value);
}
}
return !empty($properties);
}
+ /**
+ * @return array{
+ * annotationConfig: array{blindIndex: bool, filterBits: int, mappedTypedProperty: null|string},
+ * indexableAnnotation: null|IndexableField,
+ * entityClassName: string
+ * }
+ */
private function buildContext(string $entityClassName, \ReflectionProperty $refProperty): array
{
$annotationConfig = null;
@@ -287,22 +362,15 @@ private function buildContext(string $entityClassName, \ReflectionProperty $refP
}
/**
- * @param object $entity
- * @param int $oid
* @param mixed $value
- * @param \ReflectionProperty $refProperty
- * @param array $context
- * @param string|null $force
- * @return mixed|string|null
- *
- * @throws \Odandb\DoctrineCiphersweetEncryptionBundle\Exception\UndefinedGeneratorException
- * @throws \ReflectionException
+ * @param array{
+ * annotationConfig: array{blindIndex: bool, filterBits: int, mappedTypedProperty: null|string},
+ * indexableAnnotation: null|IndexableField,
+ * entityClassName: string
+ * } $context
*/
- private function handleEncryptOperation(object $entity, int $oid, $value, \ReflectionProperty $refProperty, array $context, ?string $force = null)
+ private function handleEncryptOperation(object $entity, int $oid, $value, \ReflectionProperty $refProperty, array $context, bool $force): ?string
{
- /**
- * @var null|IndexableField $indexableAnnotationConfig
- */
[
'annotationConfig' => [
'blindIndex' => $storeBlindIndex,
@@ -318,39 +386,33 @@ private function handleEncryptOperation(object $entity, int $oid, $value, \Refle
return null;
}
- if ('encrypt' === $force) {
- $originalValue = $value;
+ // Force encryption
+ if ($force) {
$value = $this->storeValue($entity, $refProperty, $value, $storeBlindIndex, $filterBits, $indexableAnnotationConfig->fastIndexing ?? EncryptorInterface::DEFAULT_FAST_INDEXING);
- $this->storeIndexes($entity, $refProperty, $indexableAnnotationConfig, $originalValue);
- } else {
- if (isset($this->_originalValues[$oid][$refProperty->getName()])) {
- $oldValue = $this->_originalValues[$oid][$refProperty->getName()];
+ $this->storeIndexes($entity, $refProperty, $indexableAnnotationConfig);
- if ($this->isValueEncrypted($oldValue)) {
- $oldValue = $this->encryptor->decrypt($entityClassName, $refProperty->getName(), $oldValue, $filterBits, $indexableAnnotationConfig->fastIndexing ?? EncryptorInterface::DEFAULT_FAST_INDEXING);
- }
- } else {
- $oldValue = null;
- }
+ return $value;
+ }
- if (($this->isValueEncrypted($oldValue) && $oldValue === $value) || (null === $oldValue && null === $value)) {
- $value = $oldValue;
- } else {
- $originalValue = $value;
- $value = $this->storeValue($entity, $refProperty, $value, $storeBlindIndex, $filterBits, $indexableAnnotationConfig->fastIndexing ?? EncryptorInterface::DEFAULT_FAST_INDEXING);
- $this->storeIndexes($entity, $refProperty, $indexableAnnotationConfig, $originalValue);
+ // Get the original value
+ $oldValue = null;
+ if (isset($this->_originalValues[$oid][$refProperty->getName()])) {
+ $oldValue = $this->_originalValues[$oid][$refProperty->getName()];
+ if ($this->isValueEncrypted($oldValue)) {
+ $oldValue = $this->encryptor->decrypt($entityClassName, $refProperty->getName(), $oldValue, $filterBits, $indexableAnnotationConfig->fastIndexing ?? EncryptorInterface::DEFAULT_FAST_INDEXING);
}
}
+ if (!$this->isValueEncrypted($oldValue) || $oldValue !== $value) {
+ $value = $this->storeValue($entity, $refProperty, $value, $storeBlindIndex, $filterBits, $indexableAnnotationConfig->fastIndexing ?? EncryptorInterface::DEFAULT_FAST_INDEXING);
+ $this->storeIndexes($entity, $refProperty, $indexableAnnotationConfig);
+ }
+
return $value;
}
/**
- * @param int $oid
* @param mixed $value
- * @param \ReflectionProperty $refProperty
- * @param array $context
- * @return string
*/
private function handleDecryptOperation(int $oid, $value, \ReflectionProperty $refProperty, array $context): string
{
@@ -374,30 +436,13 @@ private function handleDecryptOperation(int $oid, $value, \ReflectionProperty $r
return $value;
}
- /**
- * @param null|string $value
- * @return bool
- */
private function isValueEncrypted(?string $value): bool
{
return $this->encryptor->isValueEncrypted($value);
}
- /**
- * @param object $entity
- * @param \ReflectionProperty $refProperty
- * @param $value
- * @param bool $storeBlindIndex
- * @param int $filterBits
- * @param bool $fastIndexing
- * @return mixed
- */
- private function storeValue(object $entity, \ReflectionProperty $refProperty, $value, bool $storeBlindIndex, int $filterBits, bool $fastIndexing = true)
+ private function storeValue(object $entity, \ReflectionProperty $refProperty, string $value, bool $storeBlindIndex, int $filterBits, bool $fastIndexing = true)
{
- if ($value === '') {
- return '';
- }
-
[$value, $indexes] = $this->encryptor->prepareForStorage($entity, $refProperty->getName(), $value, $storeBlindIndex, $filterBits, $fastIndexing);
if ($storeBlindIndex === true) {
@@ -411,54 +456,19 @@ private function storeValue(object $entity, \ReflectionProperty $refProperty, $v
}
/**
- * @param object $entity
- * @param \ReflectionProperty $refProperty
- * @param IndexableField|null $indexableAnnotationConfig
- * @param mixed $value
- * @throws \Odandb\DoctrineCiphersweetEncryptionBundle\Exception\UndefinedGeneratorException
- * @throws \ReflectionException
+ * Generate and save indexable value
*/
- private function storeIndexes(object $entity, \ReflectionProperty $refProperty, ?IndexableField $indexableAnnotationConfig, $value): void
+ private function storeIndexes(object $entity, \ReflectionProperty $refProperty, ?IndexableField $indexableAnnotationConfig): void
{
if ($indexableAnnotationConfig === null) {
return;
}
- $autoRefresh = $indexableAnnotationConfig->autoRefresh;
- if ($autoRefresh === false) {
+ if (!$indexableAnnotationConfig->autoRefresh) {
return;
}
- if (is_string($value) === false) {
- throw new \TypeError("Value is supposed to be of type string in order to build related indexes.");
- }
-
- $this->indexableFieldsService->handleIndexableFieldsForEntity($entity, ['refProperty' => $refProperty, 'indexableConfig' => $indexableAnnotationConfig], true);
- }
-
- /**
- * After we have persisted the entities, we want to have the
- * decrypted information available once more.
- */
- public function postFlush(PostFlushEventArgs $args): void
- {
- $unitOfWork = $args->getObjectManager()->getUnitOfWork();
-
- foreach ($this->postFlushDecryptQueue as $pair) {
- $fieldPairs = $pair['fields'];
- $entity = $pair['entity'];
- $oid = spl_object_id($entity);
-
- foreach ($fieldPairs as $fieldPair) {
- /** @var \ReflectionProperty $field */
- $field = $fieldPair['field'];
- $field->setValue($entity, $fieldPair['value']);
- $unitOfWork->setOriginalEntityProperty($oid, $field->getName(), $fieldPair['value']);
- }
-
- $this->addToDecodedRegistry($entity);
- }
- $this->postFlushDecryptQueue = [];
+ $this->indexableFieldsService->handleIndexableFieldsForEntity($entity, [['refProperty' => $refProperty, 'indexableConfig' => $indexableAnnotationConfig]], true);
}
/**
@@ -466,23 +476,11 @@ public function postFlush(PostFlushEventArgs $args): void
*
* @param object $entity Some doctrine entity
*/
- private function addToDecodedRegistry($entity): void
+ private function addToDecodedRegistry(object $entity): void
{
$this->decodedRegistry[spl_object_id($entity)] = true;
}
- /**
- * Listen a postLoad lifecycle event. Checking and decrypt entities
- * which have @EncryptedField annotations.
- */
- public function postLoad(LifecycleEventArgs $args): void
- {
- $entity = $args->getObject();
- if (!$this->hasInDecodedRegistry($entity) && $this->processFields($entity, $args->getObjectManager(), false)) {
- $this->addToDecodedRegistry($entity);
- }
- }
-
/**
* Check if we have entity in decoded registry.
*
@@ -492,17 +490,4 @@ private function hasInDecodedRegistry(object $entity): bool
{
return isset($this->decodedRegistry[spl_object_id($entity)]);
}
-
- /**
- * {@inheritdoc}
- */
- public function getSubscribedEvents(): array
- {
- return [
- Events::postLoad,
- Events::onFlush,
- Events::postFlush,
- Events::onClear,
- ];
- }
}
diff --git a/tests/App/.gitignore b/tests/App/.gitignore
new file mode 100644
index 0000000..3fd2129
--- /dev/null
+++ b/tests/App/.gitignore
@@ -0,0 +1,2 @@
+/data.sqlite
+/var
diff --git a/tests/App/Controller/TestController.php b/tests/App/Controller/TestController.php
new file mode 100644
index 0000000..ac1bd5f
--- /dev/null
+++ b/tests/App/Controller/TestController.php
@@ -0,0 +1,60 @@
+em = $em;
+ }
+
+ public function entityList(): JsonResponse
+ {
+ $entities = $this->em->createQueryBuilder()
+ ->select('e')
+ ->from(MyEntityAttribute::class, 'e')
+ ->getQuery()
+ ->getResult()
+ ;
+
+ $data = [];
+ /** @var MyEntityAttribute $entity */
+ foreach ($entities as $key => $entity) {
+ $data[$key]['accountName'] = $entity->getAccountName();
+ $data[$key]['accountNumber'] = $entity->getAccountNumberType();
+ }
+
+ return new JsonResponse(['data' => $data]);
+ }
+
+ public function createEntity(): JsonResponse
+ {
+ $entity = new MyEntityAttribute('test', 1305);
+
+ $this->em->persist($entity);
+ $this->em->flush();
+
+ return new JsonResponse();
+ }
+
+ public function updateEntity(MyEntityAttribute $entity): JsonResponse
+ {
+ $entity->setAccountNumberType(1994);
+
+ $this->em->flush();
+
+ return new JsonResponse();
+ }
+}
diff --git a/tests/Encryptors/CiphersweetEncryptorObservable.php b/tests/App/Encryptors/CiphersweetEncryptorObservable.php
similarity index 66%
rename from tests/Encryptors/CiphersweetEncryptorObservable.php
rename to tests/App/Encryptors/CiphersweetEncryptorObservable.php
index 7e3572f..c84c25e 100644
--- a/tests/Encryptors/CiphersweetEncryptorObservable.php
+++ b/tests/App/Encryptors/CiphersweetEncryptorObservable.php
@@ -2,13 +2,12 @@
declare(strict_types=1);
-
-namespace Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Encryptors;
-
+namespace Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Encryptors;
use Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\CiphersweetEncryptor;
+use Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\EncryptorInterface;
-class CiphersweetEncryptorObservable extends CiphersweetEncryptor
+class CiphersweetEncryptorObservable extends CiphersweetEncryptor implements EncryptorInterface
{
public array $callsCount = [
'encrypt' => 0,
@@ -21,9 +20,9 @@ protected function doEncrypt(string $entitClassName, string $fieldName, string $
return parent::doEncrypt($entitClassName, $fieldName, $string, $index, $filterBits, $fastIndexing);
}
- protected function doDecrypt(string $entity_classname, string $fieldName, string $string): string
+ protected function doDecrypt(string $entityClassName, string $fieldName, string $string): string
{
$this->callsCount['decrypt']++;
- return parent::doDecrypt($entity_classname, $fieldName, $string);
+ return parent::doDecrypt($entityClassName, $fieldName, $string);
}
}
diff --git a/tests/App/Kernel.php b/tests/App/Kernel.php
index fde2026..41666d4 100644
--- a/tests/App/Kernel.php
+++ b/tests/App/Kernel.php
@@ -2,52 +2,39 @@
declare(strict_types=1);
-
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App;
-use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
+use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
+use Odandb\DoctrineCiphersweetEncryptionBundle\OdandbDoctrineCiphersweetEncryptionBundle;
+use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
+use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
-use Symfony\Component\Config\Resource\FileResource;
-use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
- use MicroKernelTrait;
-
- private const CONFIG_EXTS = '.{yaml,yml}';
-
public function registerBundles(): iterable
{
- $contents = require $this->getProjectDir().'/config/bundles.php';
- foreach ($contents as $class => $envs) {
- if ($envs[$this->environment] ?? $envs['all'] ?? false) {
- yield new $class();
- }
+ yield new FrameworkBundle();
+ yield new DoctrineBundle();
+ if (self::VERSION_ID < 60200) {
+ yield new SensioFrameworkExtraBundle();
}
+ yield new OdandbDoctrineCiphersweetEncryptionBundle();
}
public function getProjectDir(): string
{
- return \dirname(__DIR__).'/App';
+ return __DIR__;
}
- protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void
+ public function registerContainerConfiguration(LoaderInterface $loader): void
{
- $container->addResource(new FileResource($this->getProjectDir().'/config/bundles.php'));
- $container->setParameter('container.dumper.inline_class_loader', \PHP_VERSION_ID < 70400 || $this->debug);
- $container->setParameter('container.dumper.inline_factories', true);
+ $loader->load(__DIR__ . '/config.yaml');
+ $loader->load(__DIR__ . '/doctrine.yaml');
- $loader->load($this->getProjectDir().'/config/services'.self::CONFIG_EXTS, 'glob');
- $loader->load($this->getProjectDir().'/config/{packages}/*'.self::CONFIG_EXTS, 'glob');
-
- if (PHP_VERSION_ID >= 80000) {
- $loader->load($this->getProjectDir().'/config/doctrine80'.self::CONFIG_EXTS, 'glob');
- } else {
- $loader->load($this->getProjectDir().'/config/doctrine74'.self::CONFIG_EXTS, 'glob');
+ if (self::VERSION_ID < 60200) {
+ $loader->load(__DIR__ . '/deprecated.yaml');
}
-
- $confDir = $this->getProjectDir().'/../../src/Resources/config';
- $loader->load($confDir.'/encryption-services'.self::CONFIG_EXTS, 'glob');
}
}
diff --git a/tests/App/Model/MyEntityAttribute.php b/tests/App/Model/MyEntityAttribute.php
new file mode 100644
index 0000000..9d5445a
--- /dev/null
+++ b/tests/App/Model/MyEntityAttribute.php
@@ -0,0 +1,97 @@
+accountName = $accountName;
+ $this->accountNumberType = $accountNumberType;
+ $this->accountNumber = null !== $this->accountNumberType ? (string) $this->accountNumberType : null;
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getAccountName(): string
+ {
+ return $this->accountName;
+ }
+
+ public static function cleanAccountNumber(string $value): string
+ {
+ return trim($value);
+ }
+
+ public function setAccountName(string $accountName): void
+ {
+ $this->accountName = $accountName;
+ }
+
+ public function getAccountNameBi(): string
+ {
+ return $this->accountNameBi;
+ }
+
+ public function setAccountNameBi(string $accountNameBi): self
+ {
+ $this->accountNameBi = $accountNameBi;
+
+ return $this;
+ }
+
+ public function getAccountNumber(): string
+ {
+ return $this->accountNumber;
+ }
+
+ public function setAccountNumber(string $accountNumber): self
+ {
+ $this->accountNumber = $accountNumber;
+
+ return $this;
+ }
+
+ public function getAccountNumberType(): int
+ {
+ return $this->accountNumberType;
+ }
+
+ public function setAccountNumberType(int $accountNumberType): self
+ {
+ $this->accountNumberType = $accountNumberType;
+ $this->accountNumber = (string) $this->accountNumberType;
+
+ return $this;
+ }
+}
diff --git a/tests/App/Model/MyEntityAttributeIndexes.php b/tests/App/Model/MyEntityAttributeIndexes.php
new file mode 100644
index 0000000..1d41503
--- /dev/null
+++ b/tests/App/Model/MyEntityAttributeIndexes.php
@@ -0,0 +1,22 @@
+getParameterOption(['--env', '-e'], null, true)) {
- putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env);
-}
-
-if ($input->hasParameterOption('--no-debug', true)) {
- putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0');
-}
-
-require dirname(__DIR__) . '/config/bootstrap.php';
-
-if ($_SERVER['APP_DEBUG']) {
- umask(0000);
-
- if (class_exists(Debug::class)) {
- Debug::enable();
- }
-}
-
-$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
-$application = new Application($kernel);
-$application->run($input);
diff --git a/tests/App/config.yaml b/tests/App/config.yaml
new file mode 100644
index 0000000..bafc2ad
--- /dev/null
+++ b/tests/App/config.yaml
@@ -0,0 +1,24 @@
+imports:
+ - { resource: services.yaml }
+
+framework:
+ secret: '123456789'
+ http_method_override: false
+ router:
+ utf8: true
+ resource: '%kernel.project_dir%/routing.yaml'
+ strict_requirements: ~
+ test: true
+ profiler:
+ collect: false
+ cache:
+ pools:
+ doctrine.result_cache_pool:
+ adapter: cache.app
+ doctrine.system_cache_pool:
+ adapter: cache.system
+
+doctrine:
+ dbal:
+ driver: 'pdo_sqlite'
+ path: '%kernel.project_dir%/data.sqlite'
diff --git a/tests/App/config/bootstrap.php b/tests/App/config/bootstrap.php
deleted file mode 100644
index 6ac7ee1..0000000
--- a/tests/App/config/bootstrap.php
+++ /dev/null
@@ -1,25 +0,0 @@
-=1.2)
-// if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env['APP_ENV']) || ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? $env['APP_ENV']) === $env['APP_ENV'])) {
-// foreach ($env as $k => $v) {
-// $_ENV[$k] = $_ENV[$k] ?? (isset($_SERVER[$k]) && 0 !== strpos($k, 'HTTP_') ? $_SERVER[$k] : $v);
-// }
-// } elseif (!class_exists(Dotenv::class)) {
-// throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.');
-// } else {
-// // load all the .env files
-// (new Dotenv(false))->loadEnv(dirname(__DIR__).'/.env');
-// }
-
-$_SERVER += $_ENV;
-$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev';
-$_SERVER['APP_DEBUG'] ??= $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV'];
-$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0';
diff --git a/tests/App/config/bundles.php b/tests/App/config/bundles.php
deleted file mode 100644
index 9475f30..0000000
--- a/tests/App/config/bundles.php
+++ /dev/null
@@ -1,7 +0,0 @@
- ['all' => true],
- Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
- Odandb\DoctrineCiphersweetEncryptionBundle\OdandbDoctrineCiphersweetEncryptionBundle::class => ['all' => true],
-];
diff --git a/tests/App/config/doctrine74.yaml b/tests/App/config/doctrine74.yaml
deleted file mode 100644
index 3e3419e..0000000
--- a/tests/App/config/doctrine74.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-doctrine:
- dbal:
- default_connection: default
- connections:
- default:
- # configure these for your database server
- driver: 'pdo_sqlite'
- charset: utf8mb4
- default_table_options:
- charset: utf8mb4
- collate: utf8mb4_unicode_ci
- url: 'sqlite:///%kernel.project_dir%/var/data.db'
- orm:
- default_entity_manager: default
- auto_generate_proxy_classes: true
- entity_managers:
- default:
- connection: default
- naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
- auto_mapping: true
- mappings:
- Annotation:
- is_bundle: false
- type: annotation
- dir: '%kernel.project_dir%/../Model/Annotations'
- prefix: 'Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Model\Annotations'
- alias: Annotation
diff --git a/tests/App/config/doctrine80.yaml b/tests/App/config/doctrine80.yaml
deleted file mode 100644
index c388fb0..0000000
--- a/tests/App/config/doctrine80.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-doctrine:
- dbal:
- default_connection: default
- connections:
- default:
- # configure these for your database server
- driver: 'pdo_sqlite'
- charset: utf8mb4
- default_table_options:
- charset: utf8mb4
- collate: utf8mb4_unicode_ci
- url: 'sqlite:///%kernel.project_dir%/var/data.db'
- orm:
- default_entity_manager: default
- auto_generate_proxy_classes: true
- entity_managers:
- default:
- connection: default
- naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
- auto_mapping: true
- mappings:
- Annotation:
- is_bundle: false
- type: annotation
- dir: '%kernel.project_dir%/../Model/Annotations'
- prefix: 'Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Model\Annotations'
- alias: Annotation
- Attribute:
- is_bundle: false
- type: attribute
- dir: '%kernel.project_dir%/../Model/Attributes'
- prefix: 'Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Model\Attributes'
- alias: Attribute
diff --git a/tests/App/config/packages/framework.yaml b/tests/App/config/packages/framework.yaml
deleted file mode 100644
index 2ee7eb4..0000000
--- a/tests/App/config/packages/framework.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-framework:
- test: true
diff --git a/tests/App/config/packages/routing.yaml b/tests/App/config/packages/routing.yaml
deleted file mode 100644
index 5ed9bbb..0000000
--- a/tests/App/config/packages/routing.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-framework:
- router:
- utf8: true
diff --git a/tests/App/config/services.yaml b/tests/App/config/services.yaml
deleted file mode 100644
index 6f7c8ed..0000000
--- a/tests/App/config/services.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
-services:
- _defaults:
- autowire: true
- autoconfigure: true
diff --git a/tests/App/deprecated.yaml b/tests/App/deprecated.yaml
new file mode 100644
index 0000000..8cc7096
--- /dev/null
+++ b/tests/App/deprecated.yaml
@@ -0,0 +1,3 @@
+sensio_framework_extra:
+ router:
+ annotations: true # Deprecated; use routing annotations of Symfony core instead
diff --git a/tests/App/doctrine.yaml b/tests/App/doctrine.yaml
new file mode 100644
index 0000000..c745a20
--- /dev/null
+++ b/tests/App/doctrine.yaml
@@ -0,0 +1,19 @@
+doctrine:
+ orm:
+ default_entity_manager: default
+ auto_generate_proxy_classes: true
+ naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
+ auto_mapping: true
+ mappings:
+ Attribute:
+ is_bundle: false
+ type: attribute
+ dir: '%kernel.project_dir%/Model'
+ prefix: 'Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Model'
+ alias: Attribute
+ query_cache_driver:
+ type: pool
+ pool: doctrine.system_cache_pool
+ result_cache_driver:
+ type: pool
+ pool: doctrine.result_cache_pool
diff --git a/tests/App/routing.yaml b/tests/App/routing.yaml
new file mode 100644
index 0000000..ca8ca3d
--- /dev/null
+++ b/tests/App/routing.yaml
@@ -0,0 +1,14 @@
+list_entities:
+ path: /entities
+ controller: Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Controller\TestController::entityList
+ methods: GET
+
+create_entity:
+ path: /entity
+ controller: Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Controller\TestController::createEntity
+ methods: POST
+
+update_entity:
+ path: /entity/{id}
+ controller: Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Controller\TestController::updateEntity
+ methods: PUT
diff --git a/tests/App/services.yaml b/tests/App/services.yaml
new file mode 100644
index 0000000..4a02a86
--- /dev/null
+++ b/tests/App/services.yaml
@@ -0,0 +1,19 @@
+services:
+ _defaults:
+ autowire: true
+ autoconfigure: true
+
+ # Controller
+ Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Controller\:
+ resource: 'Controller'
+ tags: [ 'controller.service_arguments' ]
+
+ # Encrypt
+ Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Encryptors\CiphersweetEncryptorObservable:
+ arguments:
+ - '@encryption.paragon.cipher_sweet'
+ Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\EncryptorInterface: '@Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Encryptors\CiphersweetEncryptorObservable'
+
+ # Repository
+ Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Repository\MyEntityAttributeIndexesRepository: ~
+ Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Repository\MyEntityAttributeRepository: ~
diff --git a/tests/Functional/FunctionalTest.php b/tests/Functional/FunctionalTest.php
new file mode 100644
index 0000000..edd8fb2
--- /dev/null
+++ b/tests/Functional/FunctionalTest.php
@@ -0,0 +1,55 @@
+client = self::createClient();
+
+ $this->encryptor = static::getContainer()->get(EncryptorInterface::class);
+ }
+
+ public function testEntityListDecrypt(): void
+ {
+ static::getContainer()->get('services_resetter')->reset();
+
+ $this->client->request('GET', '/entities');
+
+ self::assertResponseIsSuccessful();
+ self::assertSame(8, $this->encryptor->callsCount['decrypt'], 'Decrypt method is not called the required number of times'); // 4 entities loaded with 2 fields
+
+ $json = json_decode($this->client->getResponse()->getContent(), true);
+ self::assertIsArray($json['data']);
+ self::assertIsInt($json['data'][0]['accountNumber']);
+ }
+
+ public function testCreateEntityEncrypt(): void
+ {
+ static::getContainer()->get('services_resetter')->reset();
+
+ $this->client->request('POST', '/entity');
+
+ self::assertResponseIsSuccessful();
+ self::assertSame(2, $this->encryptor->callsCount['encrypt'], 'Encrypt method is not called the required number of times'); // 1 entity loaded with 2 fields
+ }
+
+ public function testUpdateEntityDecryptAndEncrypt(): void
+ {
+ static::getContainer()->get('services_resetter')->reset();
+
+ $this->client->request('PUT', '/entity/1');
+
+ self::assertResponseIsSuccessful();
+ self::assertSame(2, $this->encryptor->callsCount['decrypt'], 'Decrypt method is not called the required number of times'); // 1 entity loaded with 2 fields
+ self::assertSame(1, $this->encryptor->callsCount['encrypt'], 'Encrypt method is not called the required number of times'); // 1 field edited
+ }
+}
diff --git a/tests/Model/Annotations/MyEntity.php b/tests/Model/Annotations/MyEntity.php
deleted file mode 100644
index d312f3b..0000000
--- a/tests/Model/Annotations/MyEntity.php
+++ /dev/null
@@ -1,66 +0,0 @@
-accountName = $accountName;
- }
-
- public function getId(): ?int
- {
- return $this->id;
- }
-
- public function getAccountName(): string
- {
- return $this->accountName;
- }
-
- public function setAccountName(string $accountName): void
- {
- $this->accountName = $accountName;
- }
-
- public function getAccountNameBi(): string
- {
- return $this->accountNameBi;
- }
-
- public function setAccountNameBi(string $accountNameBi): self
- {
- $this->accountNameBi = $accountNameBi;
-
- return $this;
- }
-}
diff --git a/tests/Model/Attributes/MyEntityAttribute.php b/tests/Model/Attributes/MyEntityAttribute.php
deleted file mode 100644
index 98c65a6..0000000
--- a/tests/Model/Attributes/MyEntityAttribute.php
+++ /dev/null
@@ -1,59 +0,0 @@
-accountName = $accountName;
- }
-
- public function getId(): ?int
- {
- return $this->id;
- }
-
- public function getAccountName(): string
- {
- return $this->accountName;
- }
-
- public function setAccountName(string $accountName): void
- {
- $this->accountName = $accountName;
- }
-
- public function getAccountNameBi(): string
- {
- return $this->accountNameBi;
- }
-
- public function setAccountNameBi(string $accountNameBi): self
- {
- $this->accountNameBi = $accountNameBi;
-
- return $this;
- }
-}
diff --git a/tests/Repository/MyEntityRepository.php b/tests/Repository/MyEntityRepository.php
deleted file mode 100644
index f4fd165..0000000
--- a/tests/Repository/MyEntityRepository.php
+++ /dev/null
@@ -1,19 +0,0 @@
-find('odb:enc:data');
-
- $commandTester = new CommandTester($command);
- // Equals to a user inputting "This", "That" and hitting ENTER
- // This can be used for answering two separated questions for instance
-
- $className = 'Odandb\\DoctrineCiphersweetEncryptionBundle\\Tests\\Model\\Annotations\\MyEntity';
-
- $commandTester->setInputs([$className, 'accountName', 'Test']);
- $commandTester->execute(['command' => $command->getName(), '--encrypt' => true]);
- $output = $commandTester->getDisplay();
- $this->assertStringContainsString('[brng:', $output);
- }
-
public function testEncryptAndDecryptDataCommandOnAttribute()
{
- if (PHP_VERSION_ID < 80000) {
- $this->markTestSkipped('require PHP 8.0');
- }
-
$kernel = static::createKernel();
$application = new Application($kernel);
$command = $application->find('odb:enc:data');
@@ -40,7 +18,7 @@ public function testEncryptAndDecryptDataCommandOnAttribute()
// Equals to a user inputting "This", "That" and hitting ENTER
// This can be used for answering two separated questions for instance
- $className = 'Odandb\\DoctrineCiphersweetEncryptionBundle\\Tests\\Model\\Attributes\\MyEntityAttribute';
+ $className = 'Odandb\\DoctrineCiphersweetEncryptionBundle\\Tests\\App\\Model\\MyEntityAttribute';
$commandTester->setInputs([$className, 'accountName', 'Test']);
$commandTester->execute(['command' => $command->getName(), '--encrypt' => true]);
diff --git a/tests/Unit/Encryptors/CiphersweetEncryptorTest.php b/tests/Unit/Encryptors/CiphersweetEncryptorTest.php
index b59b756..59235a3 100644
--- a/tests/Unit/Encryptors/CiphersweetEncryptorTest.php
+++ b/tests/Unit/Encryptors/CiphersweetEncryptorTest.php
@@ -3,8 +3,8 @@
namespace Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Unit\Encryptors;
use Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\CiphersweetEncryptor;
-use Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Encryptors\CiphersweetEncryptorObservable;
-use Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Model\Annotations\MyEntity;
+use Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Encryptors\CiphersweetEncryptorObservable;
+use Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Model\MyEntityAttribute;
use ParagonIE\CipherSweet\Backend\ModernCrypto;
use ParagonIE\CipherSweet\CipherSweet;
use ParagonIE\CipherSweet\KeyProvider\RandomProvider;
@@ -12,7 +12,7 @@
class CiphersweetEncryptorTest extends TestCase
{
- private CiphersweetEncryptor $encryptor;
+ private ?CiphersweetEncryptor $encryptor;
protected function setUp(): void
{
@@ -21,49 +21,45 @@ protected function setUp(): void
$this->encryptor = new CiphersweetEncryptorObservable($engine);
}
- public function testGetBlindIndex()
+ public function testGetBlindIndex(): void
{
$bi = $this->encryptor->getBlindIndex('my_entity', 'account_name', 'test');
$this->assertSame(8, mb_strlen($bi));
}
- public function testPrepareForStorage()
+ public function testPrepareForStorage(): void
{
- $this->encryptor->prepareForStorage(new MyEntity('132456'), 'account_name', 'test1');
- $this->encryptor->prepareForStorage(new MyEntity('132456'), 'account_name', 'test1');
- $this->encryptor->prepareForStorage(new MyEntity('132456'), 'account_name', 'test1');
- $result = $this->encryptor->prepareForStorage(new MyEntity('132456'), 'account_name', 'test1');
+ $this->encryptor->prepareForStorage(new MyEntityAttribute('132456'), 'account_name', 'test1');
+ $result = $this->encryptor->prepareForStorage(new MyEntityAttribute('132456'), 'account_name', 'test1');
+
$this->assertSame(2, count($result));
$this->assertSame(65, mb_strlen($result[0]));
$this->assertSame(8, mb_strlen($result[1]['account_name_bi']));
-
$this->assertSame(1, $this->encryptor->callsCount['encrypt']);
}
- public function testGetPrefix()
+ public function testGetPrefix(): void
{
$this->assertSame('nacl:', $this->encryptor->getPrefix());
}
- public function testDecrypt()
+ public function testDecrypt(): void
{
- [$encryptedString] = $this->encryptor->prepareForStorage(new MyEntity('132456'), 'account_name', 'test');
+ [$encryptedString] = $this->encryptor->prepareForStorage(new MyEntityAttribute('132456'), 'account_name', 'test');
- $this->encryptor->decrypt(MyEntity::class, 'account_name', $encryptedString);
- $this->encryptor->decrypt(MyEntity::class, 'account_name', $encryptedString);
- $this->encryptor->decrypt(MyEntity::class, 'account_name', $encryptedString);
+ $this->encryptor->decrypt(MyEntityAttribute::class, 'account_name', $encryptedString);
+ $result = $this->encryptor->decrypt(MyEntityAttribute::class, 'account_name', $encryptedString);
- $result = $this->encryptor->decrypt(MyEntity::class, 'account_name', $encryptedString);
$this->assertSame('test', $result);
$this->assertSame(0, $this->encryptor->callsCount['decrypt'], 'doDecrypt is never called because cache is set upon prepareForStorage call');
}
public function testDecryptNonEncryptedValue()
{
- [$encryptedString] = $this->encryptor->prepareForStorage(new MyEntity('132456'), 'account_name', 'test');
+ [$encryptedString] = $this->encryptor->prepareForStorage(new MyEntityAttribute('132456'), 'account_name', 'test');
- $decryptedString = $this->encryptor->decrypt(MyEntity::class, 'account_name', $encryptedString);
- $untouchedString = $this->encryptor->decrypt(MyEntity::class, 'account_name', $decryptedString);
+ $decryptedString = $this->encryptor->decrypt(MyEntityAttribute::class, 'account_name', $encryptedString);
+ $untouchedString = $this->encryptor->decrypt(MyEntityAttribute::class, 'account_name', $decryptedString);
$this->assertSame($decryptedString, $untouchedString);
$this->assertSame(1, $this->encryptor->callsCount['encrypt']);
@@ -72,9 +68,9 @@ public function testDecryptNonEncryptedValue()
public function testEncryptAlreadyEncryptedValue()
{
- [$encryptedString] = $this->encryptor->prepareForStorage(new MyEntity('132456'), 'account_name', 'test', false);
- [$unTouchedEncryptedString] = $this->encryptor->prepareForStorage(new MyEntity('132456'), 'account_name', 'test', false);
- [$unTouchedEncryptedStringBis, $bi] = $this->encryptor->prepareForStorage(new MyEntity('132456'), 'account_name', 'test', true);
+ [$encryptedString] = $this->encryptor->prepareForStorage(new MyEntityAttribute('132456'), 'account_name', 'test', false);
+ [$unTouchedEncryptedString] = $this->encryptor->prepareForStorage(new MyEntityAttribute('132456'), 'account_name', 'test', false);
+ [$unTouchedEncryptedStringBis, $bi] = $this->encryptor->prepareForStorage(new MyEntityAttribute('132456'), 'account_name', 'test', true);
$this->assertSame($encryptedString, $unTouchedEncryptedString);
$this->assertSame($encryptedString, $unTouchedEncryptedStringBis);
diff --git a/tests/Unit/Services/IndexesGenerators/TokenizerGeneratorTest.php b/tests/Unit/Services/IndexesGenerators/TokenizerGeneratorTest.php
index 82092de..4ded0c2 100644
--- a/tests/Unit/Services/IndexesGenerators/TokenizerGeneratorTest.php
+++ b/tests/Unit/Services/IndexesGenerators/TokenizerGeneratorTest.php
@@ -7,8 +7,7 @@
class TokenizerGeneratorTest extends TestCase
{
-
- public function testGenerate()
+ public function testGenerate(): void
{
$generator = new TokenizerGenerator();
diff --git a/tests/Unit/Services/IndexesGenerators/ValueEndingByGeneratorTest.php b/tests/Unit/Services/IndexesGenerators/ValueEndingByGeneratorTest.php
index 7bf2d35..e5275d1 100644
--- a/tests/Unit/Services/IndexesGenerators/ValueEndingByGeneratorTest.php
+++ b/tests/Unit/Services/IndexesGenerators/ValueEndingByGeneratorTest.php
@@ -7,8 +7,7 @@
class ValueEndingByGeneratorTest extends TestCase
{
-
- public function testGenerate()
+ public function testGenerate(): void
{
$generator = new ValueEndingByGenerator();
diff --git a/tests/Unit/Services/IndexesGenerators/ValueStartingByGeneratorTest.php b/tests/Unit/Services/IndexesGenerators/ValueStartingByGeneratorTest.php
index a2e16f1..f007557 100644
--- a/tests/Unit/Services/IndexesGenerators/ValueStartingByGeneratorTest.php
+++ b/tests/Unit/Services/IndexesGenerators/ValueStartingByGeneratorTest.php
@@ -7,8 +7,7 @@
class ValueStartingByGeneratorTest extends TestCase
{
-
- public function testGenerate()
+ public function testGenerate(): void
{
$generator = new ValueStartingByGenerator();
diff --git a/tests/Unit/Subscribers/DoctrineCiphersweetSubscriberTest.php b/tests/Unit/Subscribers/DoctrineCiphersweetSubscriberTest.php
index f139ce1..545eef3 100644
--- a/tests/Unit/Subscribers/DoctrineCiphersweetSubscriberTest.php
+++ b/tests/Unit/Subscribers/DoctrineCiphersweetSubscriberTest.php
@@ -5,8 +5,7 @@
use Doctrine\ORM\EntityManagerInterface;
use Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\EncryptorInterface;
use Odandb\DoctrineCiphersweetEncryptionBundle\Subscribers\DoctrineCiphersweetSubscriber;
-use Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Model\Annotations\MyEntity;
-use Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Model\Attributes\MyEntityAttribute;
+use Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Model\MyEntityAttribute;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class DoctrineCiphersweetSubscriberTest extends KernelTestCase
@@ -15,35 +14,17 @@ class DoctrineCiphersweetSubscriberTest extends KernelTestCase
private ?EncryptorInterface $encryptor;
private ?DoctrineCiphersweetSubscriber $service;
- public function setUp(): void
+ protected function setUp(): void
{
static::bootKernel();
$this->em = static::getContainer()->get(EntityManagerInterface::class);
$this->encryptor = static::getContainer()->get(EncryptorInterface::class);
- $this->service = static::getContainer()->get(DoctrineCiphersweetSubscriber::class);
- }
-
- /**
- * @group legacy
- */
- public function testProcessFieldsAnnotations()
- {
- $entity = new MyEntity('test');
- $this->service->processFields($entity, $this->em);
-
- $this->assertStringStartsWith($this->encryptor->getPrefix(), $entity->getAccountName());
-
- $this->service->processFields($entity, $this->em, false);
- $this->assertSame('test', $entity->getAccountName());
+ $this->service = static::getContainer()->get('encryption.subscriber');
}
public function testProcessFieldsAttributes()
{
- if (PHP_VERSION_ID < 80000) {
- $this->markTestSkipped('require PHP 8.0');
- }
-
$entity = new MyEntityAttribute('test');
$this->service->processFields($entity, $this->em);
diff --git a/tests/bootstrap.php b/tests/phpunit-bootstrap.php
similarity index 61%
rename from tests/bootstrap.php
rename to tests/phpunit-bootstrap.php
index fdde357..92dfae5 100644
--- a/tests/bootstrap.php
+++ b/tests/phpunit-bootstrap.php
@@ -2,24 +2,20 @@
declare(strict_types=1);
-if (isset($_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'])) {
- // executes the "php bin/console cache:clear" command
- passthru(sprintf(
- 'APP_ENV=%s php "%s/App/bin/console" cache:clear --no-warmup --quiet',
- $_ENV['BOOTSTRAP_CLEAR_CACHE_ENV'],
- __DIR__
- ));
-}
-
use Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Kernel;
+use Odandb\DoctrineCiphersweetEncryptionBundle\Tests\App\Model\MyEntityAttribute;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\ConsoleOutput;
-require __DIR__.'/App/config/bootstrap.php';
+require __DIR__ . '/../vendor/autoload.php';
+// Clean up from previous runs
+@exec('rm -rf ' . escapeshellarg(__DIR__ . '/App/var'));
+@exec('mkdir ' . escapeshellarg(__DIR__ . '/App/var'));
-$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
+// Create schema
+$kernel = new Kernel('test', false);
$output = new ConsoleOutput();
$application = new Application($kernel);
$application->setAutoExit(false);
@@ -37,3 +33,12 @@
'--full-database' => true,
]);
$runCommand('doctrine:schema:create', []);
+
+
+$em = $kernel->getContainer()->get('doctrine')->getManager();
+$entities = [];
+for ($i = 0; $i < 4; ++$i) {
+ $entities[] = $entity = new MyEntityAttribute('ODB' . $i, $i);
+ $em->persist($entity);
+}
+$em->flush();