Skip to content

Commit

Permalink
Add two commands for toolboxing
Browse files Browse the repository at this point in the history
  • Loading branch information
magi-web committed Apr 7, 2023
1 parent a5d3997 commit 176ae6d
Show file tree
Hide file tree
Showing 8 changed files with 496 additions and 24 deletions.
145 changes: 145 additions & 0 deletions src/Command/CheckEncryptionCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

namespace Odandb\DoctrineCiphersweetEncryptionBundle\Command;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Odandb\DoctrineCiphersweetEncryptionBundle\Services\EncryptedFieldsService;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'odb:enc:check', description: 'Command to check encrypted data is not corrupted.')]
class CheckEncryptionCommand extends Command
{
/** @deprecated */
protected static $defaultName = 'odb:enc:check';
/** @deprecated */
protected static $defaultDescription = 'Command to check encrypted data is not corrupted.';

protected static string $defaultAlias = 'o:e:c';

private EntityManagerInterface $entityManager;

private EncryptedFieldsService $encryptedFieldsService;

public function __construct(EntityManagerInterface $entityManager, EncryptedFieldsService $encryptedFieldsService)
{
$this->entityManager = $entityManager;
$this->encryptedFieldsService = $encryptedFieldsService;

parent::__construct(self::$defaultName);
}

protected function configure(): void
{
$this
->setAliases([self::$defaultAlias])
->addOption('interactive', 'i', InputOption::VALUE_NONE, 'Interactive mode. It will ask you to choose an entity class to check')
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$metas = $this->entityManager->getMetadataFactory()->getAllMetadata();
$metaToCheck = [];
foreach ($metas as $meta) {
if ($this->encryptedFieldsService->getEncryptedFields($meta) !== []) {
$metaToCheck[$meta->getName()] = $meta;
}
}

if ($input->getOption('interactive')) {
$question = new Question('Please enter an entity className'. PHP_EOL );
$question->setAutocompleterValues(array_keys($metaToCheck));
$io->newLine();

$question->setNormalizer(static function ($value) use ($metaToCheck) {
// $value can be null here
return $metaToCheck[$value] ?? null;
});

$question->setValidator(static function ($answer) use ($metaToCheck) {
if ($answer instanceof ClassMetadata === false) {
throw new \RuntimeException(
'The className does not exists nor has encrypted fields in its definition.'
);
}

return $answer;
});
$question->setMaxAttempts(2);

$helper = $this->getHelper('question');
$metaClassName = $helper->ask($input, $output, $question);
if (!$this->checkEncryption($io, $metaClassName)) {
return Command::FAILURE;
}

} else {
foreach ($metaToCheck as $meta) {
if (!$this->checkEncryption($io, $meta)) {
return Command::FAILURE;
}
}
}

return Command::SUCCESS;
}

private function checkEncryption(SymfonyStyle $io, ClassMetadata $classMetadata): bool
{
$progress = $io->createProgressBar(
$this->entityManager->createQueryBuilder()->select('count(a)')->from($classMetadata->getName(), 'a')->getQuery()->getSingleScalarResult()
);

$io->title(sprintf('Check encryption for %s', $classMetadata->getName()));
$progress->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s% -- [n°%id%]');
$progress->start();
$previousIdentifier = null;
try {
foreach ($this->entityManager->createQueryBuilder()
->select('a')->from($classMetadata->getName(), 'a')
->getQuery()->toIterable() as $item) {
$identifierValue = $this->getIdentifierValue($classMetadata, $item);
$progress->setMessage($identifierValue, 'id');
$progress->advance();
$previousIdentifier = $identifierValue;
}

$progress->finish();

$io->newLine();
$io->success('Done!');
} catch (\Throwable $e) {
$progress->finish();

$io->newLine();
$io->error($e->getMessage());

if (null !== $previousIdentifier) {
$io->error(sprintf('Previous item : %s [%d]', $classMetadata->getName(), $previousIdentifier));
}

return false;
}

return true;
}

private function getIdentifierValue(ClassMetadata $classMetadata, $item): string
{
$identifierValues = $classMetadata->getIdentifierValues($item);
if ($identifierValues === []) {
return '';
}

return (string) array_values($identifierValues)[0];
}
}
163 changes: 163 additions & 0 deletions src/Command/EncryptionDataCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

declare(strict_types=1);


namespace Odandb\DoctrineCiphersweetEncryptionBundle\Command;


use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Odandb\DoctrineCiphersweetEncryptionBundle\Encryptors\EncryptorInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'odb:enc:data', description: 'Encrypts or Decrypt data manually.')]
class EncryptionDataCommand extends Command
{
/** @deprecated */
protected static $defaultName = 'odb:enc:data';

/** @deprecated */
protected static $defaultDescription = 'Encrypts or Decrypt data manually.';

protected static string $defaultAlias = 'o:e:d';

private EntityManagerInterface $entityManager;

private EncryptorInterface $encryptor;

public function __construct(EntityManagerInterface $entityManager, EncryptorInterface $encryptor)
{
$this->entityManager = $entityManager;
$this->encryptor = $encryptor;

parent::__construct(self::$defaultName);
}

public function configure()
{
$this
->setAliases([self::$defaultAlias])
->addOption('encrypt', null, InputOption::VALUE_NONE, 'Encrypt data')
->addOption('decrypt', null, InputOption::VALUE_NONE, 'Decrypt data')
->addArgument('class', InputArgument::OPTIONAL, 'Class name of the entity')
->addArgument('field', InputArgument::OPTIONAL, 'Field name of the entity')
->addArgument('value', InputArgument::OPTIONAL, 'Value of the entity');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
if ($input->getOption('encrypt')) {
$encryptDecrypt = 'encrypt';
} elseif ($input->getOption('decrypt')) {
$encryptDecrypt = 'decrypt';
} else {
$encryptDecrypt = $io->choice('Do you want to encrypt or decrypt data?', ['encrypt', 'decrypt']);
}

$metas = $this->entityManager->getMetadataFactory()->getAllMetadata();
$metaData = [];
foreach ($metas as $meta) {
$metaData[$meta->getName()] = $meta;
}

$className = $input->getArgument('class');
if ($className !== null && !class_exists($className)) {
$io->error(sprintf('The class %s does not exist', $className));
$className = null;
}

if ($className === null) {
$className = $this->askClassName($metaData, $input, $output);
}

$fieldName = $input->getArgument('field');
if ($fieldName !== null && !property_exists($className, $fieldName)) {
$io->error(sprintf('The field %s does not exist', $fieldName));
$fieldName = null;
}

if($fieldName === null) {
$fieldName = $this->askFieldName($className, $input, $output);
}

if ($encryptDecrypt === 'encrypt') {
$value = $input->getArgument('value') ?? $io->ask('What is the value of the entity you want to encrypt ?');
[$result,] = $this->encryptor->prepareForStorage((new \ReflectionClass($className))->newInstanceWithoutConstructor(), $fieldName, $value, false);
} else {
$value = $input->getArgument('value') ?? $io->ask('What is the value of the entity you want to decrypt ?');
$result = $this->encryptor->decrypt($className->getName(), $fieldName, $value);
}
$io->success(sprintf('Result: [%s]', $result));

return Command::SUCCESS;
}

private function askClassName(array $metaData, InputInterface $input, OutputInterface $output): ClassMetadata
{
$io = new SymfonyStyle($input, $output);
$question = new Question('Please enter an entity className'. PHP_EOL );
$question->setAutocompleterValues(array_keys($metaData));
$io->newLine();

$question->setNormalizer(static function ($value) use ($metaData) {
// $value can be null here
return $metaData[$value] ?? null;
});

$question->setValidator(static function ($answer) use ($metaData) {
if ($answer instanceof ClassMetadata === false) {
throw new \RuntimeException(
'The className does not exists nor has encrypted fields in its definition.'
);
}

return $answer;
});
$question->setMaxAttempts(2);

$helper = $this->getHelper('question');
return $helper->ask($input, $output, $question);
}

private function askFieldName(ClassMetadata $metaData, InputInterface $input, OutputInterface $output): ?string
{
$io = new SymfonyStyle($input, $output);
$question = new Question('Please enter an existing property of the className'. PHP_EOL );
$question->setAutocompleterValues(
array_map(
static function (\ReflectionProperty $property) {return $property->getName();},
$metaData->getReflectionClass()->getProperties())
);
$io->newLine();

$question->setValidator(static function ($answer) use ($metaData) {
if ($answer === null) {
throw new \RuntimeException(
'The fieldname is mandatory.'
);
}
try {
$metaData->getReflectionClass()->getProperty($answer);
} catch (\ReflectionException $e) {
throw new \RuntimeException(
'The fieldname does not exists.'
);
}

return $answer;
});
$question->setMaxAttempts(2);

$helper = $this->getHelper('question');
return $helper->ask($input, $output, $question);
}
}
25 changes: 25 additions & 0 deletions src/Resources/config/encryption-services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ services:
class: Odandb\DoctrineCiphersweetEncryptionBundle\Subscribers\DoctrineCiphersweetSubscriber
arguments:
- "@annotation_reader"
- "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Services\\EncryptedFieldsService"
- "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Encryptors\\CiphersweetEncryptor"
- "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Services\\IndexableFieldsService"
- "@Odandb\\DoctrineCiphersweetEncryptionBundle\\Services\\PropertyHydratorService"
Expand All @@ -24,6 +25,12 @@ services:
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
Expand All @@ -32,6 +39,24 @@ services:
- "@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
Expand Down
Loading

0 comments on commit 176ae6d

Please sign in to comment.