Skip to content

Instantly share code, notes, and snippets.

@dbu
Last active February 10, 2024 11:18
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dbu/4faa29a7d556e083688092b66a77f6c4 to your computer and use it in GitHub Desktop.
Save dbu/4faa29a7d556e083688092b66a77f6c4 to your computer and use it in GitHub Desktop.
encrypting doctrine fields

An alternate approach to https://github.com/michaeldegroot/DoctrineEncryptBundle

The bundle is more elegant and "feels" more Doctrine, as you simply add an annotation to the entities. However, because the property that is tracked by Doctrine is modified by the decryption, Doctrine sees changes when there are none: absolute-quantum/DoctrineEncryptBundle#25 Rather than digging deeper into the Doctrine internals, with the potential of side effects, this approach is very explicit and Doctrine does not really need to know about encryption/decryption at all.

<?php
namespace App\EventListener;
use App\Doctrine\Encryption\EncryptingEntityInterface;
use App\Doctrine\Encryption\EncryptorInterface;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\ORM\Events;
/**
* Doctrine event subscriber to encrypt/decrypt entity properties.
*
* Inspired from https://github.com/michaeldegroot/DoctrineEncryptBundle
*
* Instead of using an annotation, we programmatically encrypted/decrypted field relations.
*/
class DoctrineEncryptListener implements EventSubscriber
{
/**
* @var EncryptorInterface
*/
private $encryptor;
public function __construct(EncryptorInterface $encryptor)
{
$this->encryptor = $encryptor;
}
public function getSubscribedEvents(): array
{
return [
Events::postLoad,
Events::preFlush,
];
}
/**
* After loading an entity, populate the decrypted properties by decrypting the encrypted tracked property.
*
* Track the entity so that we also update fields in preFlush.
*/
public function postLoad(LifecycleEventArgs $args): void
{
$entity = $args->getEntity();
if (!$entity instanceof EncryptingEntityInterface) {
return;
}
$entity->decrypt($this->encryptor);
}
/**
* Encrypt properties of entities that are newly inserted into the database or that have previously been loaded from the database.
*/
public function preFlush(PreFlushEventArgs $preFlushEventArgs): void
{
// encrypt entities that we are about to insert
$unitOfWork = $preFlushEventArgs->getEntityManager()->getUnitOfWork();
foreach ($unitOfWork->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof EncryptingEntityInterface) {
$entity->encrypt($this->encryptor);
}
}
}
}
<?php
namespace App\Doctrine\Encryption;
/**
* Interface for entities that encrypt/decrypt some of their properties.
*
* Recommended implementation:
* - field: Decrypted, exposed with getter/setter, NOT mapped to Doctrine.
* - encryptedField: Encrypted, not exposed, mapped to Doctrine.
*
* Optionally, use `decryptedField` to copy the value of the field on decryption, to decide
* during encryption whether encryptedField needs to be updated, as the encryption can be resource intensive.
*/
interface EncryptingEntityInterface
{
public function encrypt(EncryptorInterface $encryptor): void;
public function decrypt(EncryptorInterface $encryptor): void;
}
<?php
namespace App\Entity;
use App\Doctrine\Encryption\EncryptingEntityInterface;
use App\Doctrine\Encryption\EncryptorInterface; // https://github.com/michaeldegroot/DoctrineEncryptBundle/blob/master/Encryptors/EncryptorInterface.php
use Doctrine\ORM\Mapping as ORM;
class SampleEntity implements EncryptingEntityInterface
{
/**
* The IBAN of this entity.
*
* @var string
*/
private $bankAccountNumber;
/**
* The decrypted IBAN value when the entity has been loaded from the database, to know if the IBAN changed.
*
* @var string
*/
private $decryptedBankAccountNumber;
/**
* Encrypted field for $bankAccountNumber
*
* @var string
*
* @ORM\Column(type="string", name="bank_account_number")
*/
private $encryptedBankAccountNumber;
public function encrypt(EncryptorInterface $encryptor): void
{
if ($this->decryptedBankAccountNumber !== $this->bankAccountNumber) {
$this->decryptedBankAccountNumber = $this->bankAccountNumber;
$this->encryptedBankAccountNumber = $encryptor->encrypt($this->decryptedBankAccountNumber);
}
// continue if you have more encrypted fields
}
public function decrypt(EncryptorInterface $encryptor): void
{
$this->bankAccountNumber = $this->decryptedBankAccountNumber = $encryptor->decrypt($this->encryptedBankAccountNumber);
// continue if you have more encrypted fields
}
}
@dbu
Copy link
Author

dbu commented Apr 29, 2020

looking at absolute-quantum/DoctrineEncryptBundle#35, we should also listen to onFlush, as preFlush is missing cascading inserts and would lead to not explicitly persisted but cascading inserts not be encrypted.

@maluramichael
Copy link

Thanks @dbu you helped me a lot with your code here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment