Skip to content

Commit

Permalink
Change command name for encrypting database
Browse files Browse the repository at this point in the history
  • Loading branch information
mogilvie committed Jan 5, 2023
1 parent 1687b1e commit 7fa8ebc
Showing 1 changed file with 40 additions and 130 deletions.
170 changes: 40 additions & 130 deletions Subscribers/DoctrineEncryptSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@
use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\ORM\Events;
use Psr\Log\LoggerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use ReflectionProperty;
use SpecShaper\EncryptBundle\Annotations\Encrypted;
use SpecShaper\EncryptBundle\Encryptors\EncryptorInterface;
use SpecShaper\EncryptBundle\Exception\EncryptException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Psr\Log\LoggerInterface;


/**
* Doctrine event subscriber which encrypt/decrypt entities.
Expand All @@ -33,29 +31,13 @@ class DoctrineEncryptSubscriber implements EventSubscriber, DoctrineEncryptSubsc
*/
protected array $annotationArray;

/**
* Register to avoid multi decode operations for one entity.
*/
private array $decodedRegistry = [];

/**
* An array of decoded values populated during the onLoad event.
* Used to compare any resubmitted values during onFlush event.
* If the flushed unencoded value is the same as in the array then there is no change
* to the value and the entity field update is removed from the Unit of Work change set.
*/
private array $decodedValues = [];

/**
* 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.
*/
protected array $encryptedFieldCache = [];

/**
* Remember which entities have to be decrypted back in postFlush after onFlush.
*/
private array $postFlushDecryptQueue = [];
private array $rawValues = [];

private bool $isDisabled;

Expand All @@ -65,8 +47,7 @@ public function __construct(
private EncryptorInterface $encryptor,
array $annotationArray,
bool $isDisabled
)
{
) {
$this->annotationArray = $annotationArray;
$this->isDisabled = $isDisabled;
}
Expand Down Expand Up @@ -98,8 +79,8 @@ public function getSubscribedEvents(): array
{
return [
Events::postLoad,
Events::postUpdate,
Events::onFlush,
Events::postFlush,
];
}

Expand All @@ -115,59 +96,13 @@ public function onFlush(OnFlushEventArgs $args): void
$em = $args->getObjectManager();
$unitOfWork = $em->getUnitOfWork();

$this->postFlushDecryptQueue = [];

foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) {
$this->entityOnFlush($entity, $em);
$unitOfWork->recomputeSingleEntityChangeSet($em->getClassMetadata(get_class($entity)), $entity);
$this->processFields($entity, $em, true, true);
}

foreach ($unitOfWork->getScheduledEntityUpdates() as $entity) {
$this->entityOnFlush($entity, $em);
$unitOfWork->recomputeSingleEntityChangeSet($em->getClassMetadata(get_class($entity)), $entity);
}
}

/**
* Processes the entity for an onFlush event.
*
* @param $entity
* @param EntityManagerInterface $em
* @throws EncryptException
*/
protected function entityOnFlush($entity, EntityManagerInterface $em): void
{
if ($this->isDisabled) {
return;
}

// Add the entity to a decrypt Queue for postFlush decryption.
$this->postFlushDecryptQueue[] = $entity;
$this->processFields($entity, $em);
}

/**
* After we have persisted the entities, we want to have the
* decrypted information available once more.
*/
public function postFlush(PostFlushEventArgs $args): void
{
if ($this->isDisabled) {
return;
}

foreach ($this->postFlushDecryptQueue as $entity) {
$hasFieldsEncrypted = $this->processFields($entity, $args->getEntityManager(), false);

// If no fields were marked encrypted then skip.
if (false === $hasFieldsEncrypted) {
continue;
}

$this->addToDecodedRegistry($entity);
$this->processFields($entity, $em, true, false);
}

$this->postFlushDecryptQueue = [];
}

/**
Expand All @@ -179,20 +114,8 @@ public function postFlush(PostFlushEventArgs $args): void
public function postLoad(LifecycleEventArgs $args): void
{
$entity = $args->getObject();
$em = $args->getObjectManager();

// If this entity has already been decoded in an earlier postFlush event then do nothing.
if ($this->hasInDecodedRegistry($entity)) {
return;
}

// Decrypt the entity fields.
$hasFieldsEncrypted = $this->processFields($entity, $em, false);

// If the entity contained encrypted fields that were decrypted then add to a registry.
if ($hasFieldsEncrypted) {
$this->addToDecodedRegistry($entity);
}
$this->processFields($entity, $args->getObjectManager(), false, false);
}

/**
Expand Down Expand Up @@ -223,7 +146,7 @@ public function getEncryptionableProperties(array $allProperties): array
/**
* Process (encrypt/decrypt) entities fields.
*/
protected function processFields(object $entity, EntityManagerInterface $em, ?bool $isEncryptOperation = true): bool
protected function processFields(object $entity, EntityManagerInterface $em, bool $isEncryptOperation, bool $isInsert): bool
{
// Get the encrypted properties in the entity.
$properties = $this->getEncryptedFields($entity, $em);
Expand All @@ -236,12 +159,7 @@ protected function processFields(object $entity, EntityManagerInterface $em, ?bo
$unitOfWork = $em->getUnitOfWork();
$oid = spl_object_id($entity);

if(!array_key_exists($oid, $this->decodedValues)){
$this->decodedValues[$oid] = [];
}

foreach ($properties as $key => $refProperty) {

// Get the value in the entity.
$value = $refProperty->getValue($entity);

Expand All @@ -256,60 +174,51 @@ protected function processFields(object $entity, EntityManagerInterface $em, ?bo

// Encryption is fired by onFlush event, else it is an onLoad event.
if ($isEncryptOperation) {
$changeSet = $unitOfWork->getEntityChangeSet($entity);

// If the field has already been decrypted by the onLoad event, and the flushed value is the same
if(isset($this->decodedValues[$oid][$refProperty->getName()]) && $this->decodedValues[$oid][$refProperty->getName()][1] === $value){

// Remove the field from the UoW change set.
unset($unitOfWork->getEntityChangeSet($entity)[$refProperty->getName()]);

// Get the originally created encrypted value.
$encryptedValue = $this->decodedValues[$oid][$refProperty->getName()][0];

// Reset that to the original in the UoW.
$unitOfWork->setOriginalEntityProperty($oid, $refProperty->getName(), $encryptedValue);
} else {
// The field is part of an insert or the value of the field has changed, then create a new encrypted value.
// Encrypt value only if change has been detected by Doctrine (comparing unencrypted values, see postLoad flow)
if (isset($changeSet[$refProperty->getName()])) {
$encryptedValue = $this->encryptor->encrypt($value);
$refProperty->setValue($entity, $encryptedValue);
$unitOfWork->recomputeSingleEntityChangeSet($em->getClassMetadata(get_class($entity)), $entity);

if ($isInsert) {
// Restore the decrypted value after the change set update
$refProperty->setValue($entity, $value);
} else {
// Will be restored during postUpdate cycle
$this->rawValues[$oid][$refProperty->getName()] = $value;
}
}

// Replace the unencrypted value with the encrypted value on the entity.
$refProperty->setValue($entity, $encryptedValue);

} else {
// Decryption is fired by onLoad and postFlush events.
$decryptedValue = $this->decryptValue($value);
$refProperty->setValue($entity, $decryptedValue);

// Store the decrypted value for comparison during a flush event.
$this->decodedValues[$oid][$refProperty->getName()] = [$value, $decryptedValue];

// We don't want the object to be dirty immediately after reading
$unitOfWork->setOriginalEntityProperty($oid, $refProperty->getName(), $value);
// Tell Doctrine the original value was the decrypted one.
$unitOfWork->setOriginalEntityProperty($oid, $refProperty->getName(), $decryptedValue);
}
}

return !empty($properties);
}

/**
* Check if we have entity in decoded registry.
*
* @param object $entity Some doctrine entity
*/
protected function hasInDecodedRegistry(object $entity): bool
public function postUpdate(LifecycleEventArgs $args): void
{
return isset($this->decodedRegistry[spl_object_id($entity)]);
}
$entity = $args->getObject();
$em = $args->getObjectManager();

/**
* Adds entity to decoded registry.
*
* @param object $entity Some doctrine entity
*/
protected function addToDecodedRegistry(object $entity): void
{
$this->decodedRegistry[spl_object_id($entity)] = true;
$oid = spl_object_id($entity);
if (isset($this->rawValues[$oid])) {
$className = get_class($entity);
$meta = $em->getClassMetadata($className);
foreach ($this->rawValues[$oid] as $prop => $rawValue) {
$refProperty = $meta->getReflectionProperty($prop);
$refProperty->setValue($entity, $rawValue);
}

unset($this->rawValues[$oid]);
}
}

/**
Expand Down Expand Up @@ -354,6 +263,7 @@ private function isEncryptedProperty(ReflectionProperty $refProperty)
Please use #[Encrypted] attribute instead.',
$refProperty
));

return true;
}
}
Expand Down

0 comments on commit 7fa8ebc

Please sign in to comment.