Symfony 8.2 Nouveauté Publié le

Retrouver un patient par son adresse chiffrée

#[BlindIndexed] : une empreinte à clé, remplie au flush, rend cherchable une colonne chiffrée qu'aucun WHERE ne retrouve.

Notions abordées : DoctrineChiffrement

Nouveau dans Symfony 8.2 (en développement). Avec #[Encrypted], une colonne est chiffrée en base, et Doctrine la déchiffre en relisant l'entité. Mais le chiffrement est aléatoire : la même adresse, chiffrée deux fois, ne donne jamais les mêmes octets. Un WHERE email = ? ne trouve donc jamais rien, et on ne peut plus retrouver un patient par son adresse. L'index aveugle (blind index) résout ce problème.

Le principe

À côté de la colonne chiffrée, on garde une empreinte à clé de la valeur, un HMAC-SHA256 : la même adresse donne toujours la même empreinte, mais il faut la clé pour la calculer. Qui lit la base voit des empreintes, sans pouvoir les rapprocher d'une adresse ; il voit en revanche quels patients partagent la même, ce que le chiffrement seul cachait. On n'indexe donc que des valeurs variées, cherchées à l'égalité : une adresse, un numéro de dossier, pas un pays ni une année de naissance. Pour chercher, on calcule l'empreinte de l'adresse demandée et on la compare à celles de la base.

Depuis Symfony 8.2

#[ORM\Column]
#[Encrypted(key: 'patient.contact')]
private string $email;

#[ORM\Column(length: 64)]
#[BlindIndexed('email', 'patient-email')]
private string $emailIndex = '';

#[BlindIndexed] dit d'où vient l'empreinte : de la propriété email, par l'index nommé patient-email. Un écouteur la recalcule à chaque flush() qui écrit l'entité, à l'insertion comme à la modification. On n'a donc pas à y penser, ni à risquer une ligne dont l'empreinte n'aurait pas été écrite et qu'aucune recherche ne retrouverait.

L'index est un service, déclaré avec sa clé de données et sa projection, c'est-à-dire la forme de la valeur qu'il indexe. La projection Email normalise le domaine, seule partie de l'adresse que la norme déclare insensible à la casse : ada@Example.ORG et ada@example.org partagent alors une empreinte. La partie avant @, elle, garde sa casse : Ada@example.org est une autre adresse. Si votre application doit confondre les deux, normalisez l'adresse à son entrée, avant de l'enregistrer : ce n'est pas à l'index d'en décider.

La recherche passe par cet index, qui calcule l'empreinte de l'adresse cherchée :

$patients->findOneBy(['emailIndex' => $index->of($email)]);

L'écouteur ne voit que ce qui passe par le flush() : une ligne insérée en SQL n'a pas d'empreinte, et une ligne modifiée par un UPDATE en masse garde l'ancienne, qui désigne alors une adresse qu'elle n'a plus.

À faire

L'adresse des patients est déjà chiffrée, et config/services.yaml déclare l'index patient-email. Mais PatientRepository::trouverParEmail() cherche encore sur la colonne chiffrée, et l'aperçu /patients/recherche ne trouve donc personne. Il montre aussi, sous enBase, ce que la base contient.

  1. Ajoutez à Patient la colonne emailIndex, remplie par l'index patient-email.
  2. Faites chercher trouverParEmail() par l'empreinte de l'adresse.

L'adresse doit rester chiffrée.

D'où vient la fonctionnalité : la pull request d'origine.