From 6940a1904b5d2ea7ce12bfb4b9d97383a4473390 Mon Sep 17 00:00:00 2001 From: Mathieu Girard Date: Tue, 22 Nov 2022 16:55:54 +0100 Subject: [PATCH] Add tests & enhance perfs while performing encryption/decryption --- .gitignore | 9 +++ composer.json | 7 +- phpunit.xml.dist | 22 +++++- src/Encryptors/CiphersweetEncryptor.php | 47 ++++++++++-- src/Services/IndexableFieldsService.php | 4 +- .../ValueEndingByGenerator.php | 6 +- .../ValueStartingByGenerator.php | 2 +- tests/App/Kernel.php | 47 ++++++++++++ tests/App/bin/console | 42 +++++++++++ tests/App/config/bootstrap.php | 25 +++++++ tests/App/config/bundles.php | 7 ++ tests/App/config/packages/doctrine.yaml | 27 +++++++ tests/App/config/packages/framework.yaml | 2 + tests/App/config/packages/routing.yaml | 3 + tests/App/config/services.yaml | 4 ++ .../CiphersweetEncryptorObservable.php | 29 ++++++++ tests/Model/MyEntity.php | 71 +++++++++++++++++++ tests/Repository/MyEntityRepository.php | 19 +++++ .../Encryptors/CiphersweetEncryptorTest.php | 62 ++++++++++++++++ .../TokenizerGeneratorTest.php | 21 ++++++ .../ValueEndingByGeneratorTest.php | 19 +++++ .../ValueStartingByGeneratorTest.php | 19 +++++ .../DoctrineCiphersweetSubscriberTest.php | 31 ++++++++ tests/bootstrap.php | 14 ++++ 24 files changed, 524 insertions(+), 15 deletions(-) create mode 100644 tests/App/Kernel.php create mode 100644 tests/App/bin/console create mode 100644 tests/App/config/bootstrap.php create mode 100644 tests/App/config/bundles.php create mode 100644 tests/App/config/packages/doctrine.yaml create mode 100644 tests/App/config/packages/framework.yaml create mode 100644 tests/App/config/packages/routing.yaml create mode 100644 tests/App/config/services.yaml create mode 100644 tests/Encryptors/CiphersweetEncryptorObservable.php create mode 100644 tests/Model/MyEntity.php create mode 100644 tests/Repository/MyEntityRepository.php create mode 100644 tests/Unit/Encryptors/CiphersweetEncryptorTest.php create mode 100644 tests/Unit/Services/IndexesGenerators/TokenizerGeneratorTest.php create mode 100644 tests/Unit/Services/IndexesGenerators/ValueEndingByGeneratorTest.php create mode 100644 tests/Unit/Services/IndexesGenerators/ValueStartingByGeneratorTest.php create mode 100644 tests/Unit/Subscribers/DoctrineCiphersweetSubscriberTest.php create mode 100644 tests/bootstrap.php diff --git a/.gitignore b/.gitignore index 2538d21..9734907 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,12 @@ phpunit.xml .phpunit.result.cache composer.lock +.idea/ +cache/ +var/ +tests/coverage/ +.DS_Store +.php_cs.cache +.php-cs-fixer.cache +*.sqlite +.preload.php diff --git a/composer.json b/composer.json index 64a8541..14fd114 100644 --- a/composer.json +++ b/composer.json @@ -32,8 +32,13 @@ "symfony/yaml": "^4.4 || ^5.1 || ^6.0" }, "require-dev": { + "doctrine/doctrine-bundle": "^2.7", "roave/security-advisories": "dev-latest", - "symfony/phpunit-bridge": "^5.1 || ^6.0" + "symfony/doctrine-bridge": "^5.1 || ^6.0", + "symfony/dotenv": "^5.1 || ^6.0", + "symfony/framework-bundle": "^5.1 || ^6.0", + "symfony/phpunit-bridge": "^5.1 || ^6.0", + "symfony/test-pack": "^1.0" }, "suggest": { "phpdocumentor/reflection-docblock": "To use the PHPDoc" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7d27a2d..1ec20cb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,6 +3,26 @@ xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" backupGlobals="false" colors="true" - bootstrap="./vendor/autoload.php" + bootstrap="tests/bootstrap.php" + verbose="true" > + + + + + + + + + + + + + + + + + tests + + diff --git a/src/Encryptors/CiphersweetEncryptor.php b/src/Encryptors/CiphersweetEncryptor.php index 59783b2..3dbc5c5 100644 --- a/src/Encryptors/CiphersweetEncryptor.php +++ b/src/Encryptors/CiphersweetEncryptor.php @@ -25,11 +25,25 @@ public function __construct(CipherSweet $engine) 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); + $entitClassName = \get_class($entity); + + $output = []; if (isset($this->cache[$entitClassName][$fieldName][$string])) { - return $this->cache[$entitClassName][$fieldName][$string]; + $output[] = $this->cache[$entitClassName][$fieldName][$string]; + if ($index) { + $output[] = [$fieldName.'_bi' => $this->getBlindIndex($entitClassName, $fieldName, $string, $filterBits, $fastIndexing)]; + } else { + $output[] = []; + } + + return $output; } + return $this->doEncrypt($entitClassName, $fieldName, $string, $index, $filterBits, $fastIndexing); + } + + 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)); if ($index) { $encryptedField->addBlindIndex( @@ -39,18 +53,32 @@ public function prepareForStorage(object $entity, string $fieldName, string $str $result = $encryptedField->prepareForStorage($string); - $this->cache[$entitClassName][$fieldName][$string] = $result; + $this->cache[$entitClassName][$fieldName][$string] = $result[0]; + $this->cache[$entitClassName][$fieldName][$result[0]] = $string; + + $this->biCache[$entitClassName][$fieldName][$string] = $result[1][$fieldName.'_bi']; 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 { - return (new EncryptedField($this->engine, $entity_classname, $fieldName)) - ->addBlindIndex( - new BlindIndex($fieldName.'_bi', [], $filterBits, $fastIndexing) - ) + if (isset($this->cache[$entity_classname][$fieldName][$string])) { + return $this->cache[$entity_classname][$fieldName][$string]; + } + + return $this->doDecrypt($entity_classname, $fieldName, $string); + } + + protected function doDecrypt(string $entity_classname, string $fieldName, string $string): string + { + $decryptedValue = (new EncryptedField($this->engine, $entity_classname, $fieldName)) ->decryptValue($string); + + $this->cache[$entity_classname][$fieldName][$string] = $decryptedValue; + $this->cache[$entity_classname][$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 @@ -59,6 +87,11 @@ public function getBlindIndex($entityName, $fieldName, string $value, int $filte return $this->biCache[$entityName][$fieldName][$value]; } + 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 + { $index = (new EncryptedField($this->engine, $entityName, $fieldName)) ->addBlindIndex( new BlindIndex($fieldName.'_bi', [], $filterBits, $fastIndexing) diff --git a/src/Services/IndexableFieldsService.php b/src/Services/IndexableFieldsService.php index b577f88..ea080bc 100644 --- a/src/Services/IndexableFieldsService.php +++ b/src/Services/IndexableFieldsService.php @@ -6,12 +6,12 @@ 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\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Common\Annotations\Reader; use Doctrine\ORM\EntityManagerInterface; @@ -33,7 +33,7 @@ public function __construct(Reader $annReader, EntityManagerInterface $em, Index public function getChunksForMultiThread(string $className, int $chuncksLength): array { - /** @var ServiceEntityRepository $repo */ + /** @var EntityRepository $repo */ $repo = $this->em->getRepository($className); $result = $repo->createQueryBuilder('c') ->select('c.id') diff --git a/src/Services/IndexesGenerators/ValueEndingByGenerator.php b/src/Services/IndexesGenerators/ValueEndingByGenerator.php index a1f8c64..16cecfd 100644 --- a/src/Services/IndexesGenerators/ValueEndingByGenerator.php +++ b/src/Services/IndexesGenerators/ValueEndingByGenerator.php @@ -13,10 +13,10 @@ class ValueEndingByGenerator implements IndexesGeneratorInterface */ public function generate(string $value): array { - $possibleValues[] = $value; + $possibleValues = []; - for($i=1, $len = mb_strlen($value); $i < $len; $i++) { - $possibleValues[] = mb_substr($value, 0, -$i); + for($i=1, $len = mb_strlen($value); $i <= $len; $i++) { + $possibleValues[] = mb_substr($value, -$i); } return $possibleValues; diff --git a/src/Services/IndexesGenerators/ValueStartingByGenerator.php b/src/Services/IndexesGenerators/ValueStartingByGenerator.php index cfd33f9..96e86ea 100644 --- a/src/Services/IndexesGenerators/ValueStartingByGenerator.php +++ b/src/Services/IndexesGenerators/ValueStartingByGenerator.php @@ -9,7 +9,7 @@ class ValueStartingByGenerator implements IndexesGeneratorInterface { public function generate(string $value): array { - $possibleValues = [$value]; + $possibleValues = []; for($i=1, $len = mb_strlen($value); $i <= $len; $i++) { $possibleValues[] = mb_substr($value, 0, $i); diff --git a/tests/App/Kernel.php b/tests/App/Kernel.php new file mode 100644 index 0000000..15e6b94 --- /dev/null +++ b/tests/App/Kernel.php @@ -0,0 +1,47 @@ +getProjectDir().'/config/bundles.php'; + foreach ($contents as $class => $envs) { + if ($envs[$this->environment] ?? $envs['all'] ?? false) { + yield new $class(); + } + } + } + + public function getProjectDir(): string + { + return \dirname(__DIR__).'/App'; + } + + protected function configureContainer(ContainerBuilder $container, 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($this->getProjectDir().'/config/services'.self::CONFIG_EXTS, 'glob'); + $loader->load($this->getProjectDir().'/config/{packages}/*'.self::CONFIG_EXTS, 'glob'); + + $confDir = $this->getProjectDir().'/../../src/Resources/config'; + $loader->load($confDir.'/encryption-services'.self::CONFIG_EXTS, 'glob'); + } +} diff --git a/tests/App/bin/console b/tests/App/bin/console new file mode 100644 index 0000000..3345c36 --- /dev/null +++ b/tests/App/bin/console @@ -0,0 +1,42 @@ +#!/usr/bin/env php +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/bootstrap.php b/tests/App/config/bootstrap.php new file mode 100644 index 0000000..6ac7ee1 --- /dev/null +++ b/tests/App/config/bootstrap.php @@ -0,0 +1,25 @@ +=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 new file mode 100644 index 0000000..9475f30 --- /dev/null +++ b/tests/App/config/bundles.php @@ -0,0 +1,7 @@ + ['all' => true], + Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], + Odandb\DoctrineCiphersweetEncryptionBundle\OdandbDoctrineCiphersweetEncryptionBundle::class => ['all' => true], +]; diff --git a/tests/App/config/packages/doctrine.yaml b/tests/App/config/packages/doctrine.yaml new file mode 100644 index 0000000..48c0d0e --- /dev/null +++ b/tests/App/config/packages/doctrine.yaml @@ -0,0 +1,27 @@ +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: + App: + is_bundle: false + type: annotation + dir: '%kernel.project_dir%/../Model' + prefix: 'Odandb\DoctrineCiphersweetEncryptionBundle\Tests\Model\MyEntity' + alias: App diff --git a/tests/App/config/packages/framework.yaml b/tests/App/config/packages/framework.yaml new file mode 100644 index 0000000..2ee7eb4 --- /dev/null +++ b/tests/App/config/packages/framework.yaml @@ -0,0 +1,2 @@ +framework: + test: true diff --git a/tests/App/config/packages/routing.yaml b/tests/App/config/packages/routing.yaml new file mode 100644 index 0000000..5ed9bbb --- /dev/null +++ b/tests/App/config/packages/routing.yaml @@ -0,0 +1,3 @@ +framework: + router: + utf8: true diff --git a/tests/App/config/services.yaml b/tests/App/config/services.yaml new file mode 100644 index 0000000..6f7c8ed --- /dev/null +++ b/tests/App/config/services.yaml @@ -0,0 +1,4 @@ +services: + _defaults: + autowire: true + autoconfigure: true diff --git a/tests/Encryptors/CiphersweetEncryptorObservable.php b/tests/Encryptors/CiphersweetEncryptorObservable.php new file mode 100644 index 0000000..7e3572f --- /dev/null +++ b/tests/Encryptors/CiphersweetEncryptorObservable.php @@ -0,0 +1,29 @@ + 0, + 'decrypt' => 0, + ]; + + 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 + { + $this->callsCount['encrypt']++; + return parent::doEncrypt($entitClassName, $fieldName, $string, $index, $filterBits, $fastIndexing); + } + + protected function doDecrypt(string $entity_classname, string $fieldName, string $string): string + { + $this->callsCount['decrypt']++; + return parent::doDecrypt($entity_classname, $fieldName, $string); + } +} diff --git a/tests/Model/MyEntity.php b/tests/Model/MyEntity.php new file mode 100644 index 0000000..ea1d43c --- /dev/null +++ b/tests/Model/MyEntity.php @@ -0,0 +1,71 @@ +accountName = $accountName; + } + + /** + * @return int|null + */ + 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 new file mode 100644 index 0000000..4b2b7e3 --- /dev/null +++ b/tests/Repository/MyEntityRepository.php @@ -0,0 +1,19 @@ +encryptor = new CiphersweetEncryptorObservable($engine); + } + + public function testGetBlindIndex() + { + $bi = $this->encryptor->getBlindIndex('my_entity', 'account_name', 'test'); + $this->assertSame(8, mb_strlen($bi)); + } + + public function testPrepareForStorage() + { + $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->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() + { + $this->assertSame('nacl:', $this->encryptor->getPrefix()); + } + + public function testDecrypt() + { + [$encryptedString] = $this->encryptor->prepareForStorage(new MyEntity('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); + + $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'); + } +} diff --git a/tests/Unit/Services/IndexesGenerators/TokenizerGeneratorTest.php b/tests/Unit/Services/IndexesGenerators/TokenizerGeneratorTest.php new file mode 100644 index 0000000..82092de --- /dev/null +++ b/tests/Unit/Services/IndexesGenerators/TokenizerGeneratorTest.php @@ -0,0 +1,21 @@ +assertEquals(['test'], $generator->generate('test')); + $this->assertEquals(['test', 'test'], $generator->generate('test test')); + $this->assertEquals(['test', 'test', 'test'], $generator->generate('test test test')); + $this->assertEquals(['tes1-test', 'test', 'test'], $generator->generate('tes1-test test test')); + $this->assertEquals(['tes1', 'test', 'test', 'test'], $generator->generate('tes1/test test test')); + } +} diff --git a/tests/Unit/Services/IndexesGenerators/ValueEndingByGeneratorTest.php b/tests/Unit/Services/IndexesGenerators/ValueEndingByGeneratorTest.php new file mode 100644 index 0000000..7bf2d35 --- /dev/null +++ b/tests/Unit/Services/IndexesGenerators/ValueEndingByGeneratorTest.php @@ -0,0 +1,19 @@ +assertEquals(['t', 'st', 'est', 'test'], $generator->generate('test')); + $this->assertEquals(['t'], $generator->generate('t')); + $this->assertEquals([], $generator->generate('')); + } +} diff --git a/tests/Unit/Services/IndexesGenerators/ValueStartingByGeneratorTest.php b/tests/Unit/Services/IndexesGenerators/ValueStartingByGeneratorTest.php new file mode 100644 index 0000000..a2e16f1 --- /dev/null +++ b/tests/Unit/Services/IndexesGenerators/ValueStartingByGeneratorTest.php @@ -0,0 +1,19 @@ +assertEquals(['t', 'te', 'tes', 'test'], $generator->generate('test')); + $this->assertEquals(['t'], $generator->generate('t')); + $this->assertEquals([], $generator->generate('')); + } +} diff --git a/tests/Unit/Subscribers/DoctrineCiphersweetSubscriberTest.php b/tests/Unit/Subscribers/DoctrineCiphersweetSubscriberTest.php new file mode 100644 index 0000000..5ea5bc8 --- /dev/null +++ b/tests/Unit/Subscribers/DoctrineCiphersweetSubscriberTest.php @@ -0,0 +1,31 @@ +get(EntityManagerInterface::class); + $encryptor = static::getContainer()->get(EncryptorInterface::class); + $service = static::getContainer()->get(DoctrineCiphersweetSubscriber::class); + $this->assertNotNull($service); + + $entity = new MyEntity('test'); + $service->processFields($entity, $em); + + $this->assertStringStartsWith($encryptor->getPrefix(), $entity->getAccountName()); + + $service->processFields($entity, $em, false); + $this->assertSame('test', $entity->getAccountName()); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..f34a1f6 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,14 @@ +