anonyfy
Pseudonymisation réversible et déterministe des données personnelles françaises, avant transmission à un modèle de langage.
anonyfy substitue les identifiants personnels d'un texte par des valeurs plausibles et de même type, de façon réversible. Le clair ne quitte jamais l'infrastructure du client : ni vers un service, ni vers un disque, ni vers un journal. Conçu pour les équipes françaises qui branchent un LLM sur des données métier et veulent une réponse défendable à la question « et le RGPD ? ».
Pourquoi
Toute équipe française qui branche un LLM sur des données métier se heurte à la même question dans la première semaine : qu'est-ce qu'on a le droit d'envoyer chez le fournisseur ? Les réponses actuelles sont mauvaises.
- Ne rien envoyer : on renonce à l'usage.
- Tout envoyer : on transfère des données personnelles à un sous-traitant hors périmètre, souvent hors UE, sans base solide.
- Caviarder (
[REDACTED]) : on détruit le raisonnement du modèle et on ne peut plus recoller la réponse aux données réelles. - Presidio : l'outil de référence est anglo-centré. Rien n'est natif sur le NIR, le SIRET, l'IBAN français ou les formats d'adresse, et il n'offre pas de pseudonymisation réversible à coffre clé.
anonyfy comble le vide laissé : substituer par des valeurs de même type, de façon déterministe et réversible, sans que le clair ne quitte le client.
Ce que c'est
Une bibliothèque Python et une CLI. Pas un service hébergé, jamais.
from anonyfy import Vault
v = Vault(key=..., scope="dossier-1234")
masked = v.mask(texte) # -> MaskedText(.text, .entities)
clair = v.unmask(reponse_du_modele)
v.report() # journal exploitable par un DPO
v.flush() # optionnel : commit le registre avant publication
v.close() # commit + fermeture (ou contexte `with`)
Le registre est durable « au batch près » (commits par lots). Un opérateur qui
publie un texte masqué juste après mask() appelle flush() (ou close())
pour que les réservations soient sur disque avant la publication ; les
réservations restent idempotentes.
anonyfy scan fichier.txt # mode observation : détecte, ne modifie rien
anonyfy scan dossier/*.txt --format json --out rapport.json # corpus -> contrat v1
anonyfy mask fichier.txt --scope d1234
anonyfy unmask reponse.txt --scope d1234
scan --format json accepte 1 à 50 fichiers et produit le contrat public
anonyfy.report.v1 : agrégats et codes contrôlés uniquement (aucun nom de
fichier, aucune valeur source, aucun clair, aucun substitut). Le mode JSON
utilise une clé éphémère et ne crée aucun registre persistant ; il refuse
--scope, --registry, --audit, --key et --key-file. Le format par
défaut reste markdown (mono-fichier, clé et registre historiques).
Le schéma normatif est embarqué dans le paquet et lisible par
anonyfy.schemas.load_report_schema() ($id: urn:anonyfy:report:v1, JSON
Schema draft 2020-12).
mask_json / unmask_json parcourent un payload JSON et ne masquent que
les feuilles chaîne (jamais les clés, jamais function.name). C'est une
primitive, pas un proxy (OBJ-025) : le proxy compatible OpenAI est
prévu en v2 ; mask_json expose déjà la primitive de parcours pour que
l'intégrateur puisse câbler le masquage de payloads structurés en
attendant.
Installation
uv add anonyfy
Python ≥ 3.11. Le cœur ne dépend que de la bibliothèque standard et d'une
bibliothèque cryptographique (FPE FF3-1 via ff3, isolée derrière
surrogate/fpe.py — voir ADR 0001). Aucun téléchargement de modèle, fonctionne
hors ligne.
Migration (phase 27)
Les registres créés avant la phase 27 (gazetteer 5k noms) sont
incompatibles avec le gazetteer INSEE complet (879k noms). À la première
ouverture, GazetteerVersionMismatch est levée. Supprimer les registres
obsolètes (rm ~/.anonyfy/registries/*.db) ou exporter les données avant
migration.
Couverture de détection (v1)
Les identifiants structurés sont traités par expression régulière plus validation
arithmétique : c'est ce qui écrase les faux positifs. Les patronymes et adresses
reposent sur des gazetteers (INSEE, COG) et des déclencheurs contextuels
(M., Mme, Maître, né(e) le, demeurant).
| Type | Méthode | Validation | Substitut |
|---|---|---|---|
| NIR (n° sécu) | regex | clé mod 97 | FPE, clé recalculée |
| SIREN / SIRET | regex | Luhn | FPE, Luhn recalculé |
| IBAN FR | regex | mod 97 | FPE, clé recalculée |
| TVA intracomm. FR | regex | clé | FPE |
| Carte bancaire | regex | Luhn | FPE |
| Téléphone FR | regex | plan de numérotation | FPE, préfixe préservé |
| regex | syntaxe | partie locale FPE, domaine gazetteer | |
| Plaque SIV | regex | format | FPE |
| Prénom / nom | gazetteer INSEE + déclencheurs | — | gazetteer + registre de scope |
| Commune / code postal | gazetteer COG INSEE + base La Poste | cohérence CP/commune | gazetteer ; code postal valide La Poste |
| Date de naissance | regex + déclencheur | calendaire | décalage déterministe borné |
Depuis la 0.1.6, le substitut CODE_POSTAL est un code postal valide : le
code_postal de la commune substituée, via le mapping déterministe
code_commune → code_postal de la base officielle des codes postaux La Poste
(dataset laposte-hexasmal, Licence Ouverte 2.0), au lieu d'un code commune
Insee.
Limitation annoncée - Communes sans indice d'adresse fort
- Limitation annoncée (réduite en 0.1.6) : les communes en en-tête de
lettre
<Commune>, le <date>en début de ligne sont désormais masquées (dates en chiffres) ; les communes en initiale SANS date et les toponymes de prose (« Je vais à Paris ») restent préservés enpermissive. - Limitation résiduelle : une date en toutes lettres (« le vingt-trois août ») ne déclenche pas l'indice d'en-tête et la commune d'en-tête reste en clair.
Le positionnement juridique — à lire avant de l'utiliser
La pseudonymisation n'est pas l'anonymisation. Écrire « conforme RGPD » ici serait faux et discréditerait le projet auprès du public juridique visé.
Le cadrage défendable est plus intéressant. Dans EDPS c. CRU (CJUE, septembre 2025), la Cour a retenu une approche relative de la notion de donnée personnelle : des données pseudonymisées transmises à un destinataire qui ne dispose d'aucun moyen raisonnable de ré-identifier peuvent ne pas constituer des données personnelles pour ce destinataire. Si la clé et le registre restent chez le client, c'est exactement la configuration vis-à-vis du fournisseur de LLM.
C'est un raisonnement contextuel et encore discuté par le CEPD. anonyfy le présente comme un argument à instruire au cas par cas avec son conseil, pas comme un blanc-seing. Cette honnêteté est un différenciateur : personne d'autre ne l'écrit.
Ce que anonyfy ne fait pas
Ces limites sont volontaires et assumées. Les dissimuler discréditerait l'outil.
- Ce n'est pas de l'anonymisation au sens du RGPD. Voir le positionnement ci-dessus.
- La ré-identification par le contexte. « Le dirigeant de la société de menuiserie de Moulidars » reste identifiant même si le nom est substitué. Aucun outil ne résout ça, et prétendre le contraire serait malhonnête.
- Le dictionnaire de code. Un mapping déterministe est un code book. Qui obtient beaucoup de couples clair/substitut peut inverser. Mitigations : clé secrète par déploiement, sel par scope. La rotation de clé est reportée à v2 : elle exigerait un registre stockant du clair, interdit par l'invariant 1 en v1.
- La compromission de la clé. La clé permet de tout inverser. Elle doit vivre dans le gestionnaire de secrets du client, jamais dans le dépôt, jamais dans le journal.
- Le FPE sur petits domaines. FF3-1 est faible quand l'espace des valeurs possibles est réduit. anonyfy tranche par type : FPE pur sur les grands domaines (NIR, SIREN, SIRET, IBAN, TVA, carte bancaire, téléphone) ; permutation keyée Feistel (ADR 0003) sur les petits domaines non-FPE (patronyme, prénom, commune, voie, plaque SIV, référence de dossier, date, email local-part). La bijectivité est garantie, mais les points fixes existent (D23, probabilité ~1/N par clair, détectés et alertés). Détail dans l'ADR 0001 et l'ADR 0003.
- Les dates par bucket de mois. Le décalage par bucket de mois (D8) préserve le mois et l'année mais pas le jour (clampé à [1, 28]). Une date substituée reste ré-identifiable par contexte si le bucket de date ou le mois est unique dans le contexte. La limite est assumée (§8 point 1) : le décalage ne prétend pas empêcher la ré-identification. Détail dans l'ADR 0001 §10.
- La cohérence inter-type. SIREN, SIRET et TVA intracommunautaire partagent le même SIREN sous-jacent. anonyfy applique FPE indépendamment par type : le SIRET substitué et la TVA substituée d'un même dossier peuvent reposer sur des SIREN différents. La cohérence métier n'est pas garantie en v1. Reportée à v2 (OBJ-008).
- Les faux positifs sur les noms-mots courants. « Boulanger », « rue Pierre », « Mme Rose » produiront des faux positifs. C'est le prix de l'auditabilité, et c'est la justification d'une couche modèle optionnelle plus tard. Le mode observation existe pour les découvrir avant la production.
- Pas de service hébergé, jamais. Le coffre et la clé restent chez le client. C'est une décision d'architecture, pas une étape de feuille de route, et c'est l'argument le plus fort.
Principes d'architecture
Quatre invariants. Si une décision de conception les contredit, c'est la décision qui a tort.
- Le clair ne franchit jamais la frontière. Ni vers un service, ni vers un disque, ni vers un journal.
- Déterminisme scopé. Dans un scope, une valeur produit toujours le même substitut. C'est ce qui préserve le fil d'une conversation et le cache de préfixe du fournisseur.
- Injectivité dans le scope. Deux valeurs distinctes ne partagent jamais un
substitut. Sans cette garantie,
unmaskest ambigu et le modèle fusionne deux personnes. - Rien n'est démasqué qui n'ait été masqué.
unmaskne transforme que des substituts réellement émis. Un identifiant inventé par le modèle reste tel quel.
Détail dans architecture.md : flux aller/retour, génération des substituts (FPE
pour les identifiants, gazetteer + registre de scope pour le texte libre),
résolution des collisions, traitement des appels d'outils.
Les décisions cryptographiques (isolation de ff3 derrière surrogate/fpe.py,
FPE par type vs mécanisme registre, vecteurs FF3-1 du NIST, empreinte d'audit
HMAC, politique de logging, figage du gazetteer, dates par bucket de mois,
rotation de clé reportée à v2) sont figées dans l'ADR 0001
(docs/ADR/0001-fpe-ff3.md), qui est la source de vérité avant l'implémentation.
Statut
v0.1 · Août 2026. Jalons M0 à M4 livrés (phases 01 à 19) : paquet
installable, CI verte, validateurs arithmétiques et de format, FPE FF3-1
sur les grands domaines, registre de scope SQLite, Aho-Corasick, API
publique Vault (mask/unmask/mask_json/unmask_json/report),
gazetteers figés, arbitrage complet, journal d'audit HMAC, rapport DPO,
CLI scan/mask/unmask, mode observation, politique de fermeture,
corpus de test. La phase 20 (cette documentation) finalise le jalon M4.
Feuille de route :
| Jalon | Contenu | Statut |
|---|---|---|
| M0 | Squelette du paquet, CI, licence, README | Livré |
| M1 | Validateurs structurés + FPE + aller-retour | Livré |
| M2 | Gazetteers, registre de scope, résolution de collisions | Livré |
| M3 | CLI, mode observation, journal, rapport | Livré |
| M4 | Documentation, corpus de test public, billet de lancement | En cours |
Voir CHANGELOG.md pour le détail des phases livrées et
docs/TUTORIAL.md pour le guide d'intégration.
Licence
Apache-2.0 sur l'ensemble du cœur. La version libre résout entièrement le problème d'un utilisateur seul : détection, substituts, réversibilité, registre, CLI, mode observation. Une version support/entreprise pourra venir plus tard ; une version hébergée du coffre ne viendra jamais, l'architecture l'interdit.
Voir PRD.md pour le cahier des charges complet et architecture.md pour la
conception détaillée.
Metadata
Release files for anonyfy 0.1.10
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| anonyfy-0.1.10.tar.gz | 4.5 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| anonyfy-0.1.10-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 9.0 MB
Release files / anonyfy-0.1.10.tar.gz
| Download URL | anonyfy-0.1.10.tar.gz |
|---|---|
| Size | 4.5 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
772f2828d2ebaccee9bddda93692dee94f54084ff0c247f7b116ddf5a2761f0e
|
|
BLAKE2b-256 checksum How to use checksums |
b1a742fc6129bf2f44d1ce99a2d134681b096306f05ac7069edb90e68b447454
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.13
|
Release files / anonyfy-0.1.10-py3-none-any.whl
| Download URL | anonyfy-0.1.10-py3-none-any.whl |
|---|---|
| Size | 4.5 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b6b236e7302db1d79f27fa159155641f4977f8e1832ca436a5f10feea37af4fb
|
|
BLAKE2b-256 checksum How to use checksums |
3d51c47d909462fd8742bf57476a7d259ffa36c3999a1403a6645cb54722fd9c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.13
|