Gairebé qualsevol aplicació seriosa acaba enviant text a fora: a un model d'IA, a un servei de traducció, a un sistema de logs. I el text gairebé mai no va net. Aquest article explica com muntar una capa que substitueix les dades personals abans que surtin i les torna en rebre la resposta, per què això no es resol amb quatre expressions regulars i què cal perquè l'operació sigui reversible sense obrir un forat nou. El codi va en Laravel perquè és una de les nostres especialitats a AndorraDev, però el problema és d'arquitectura i es resol igual en qualsevol llenguatge.
Enviar un text a un model és enviar-li tot el que conté
Quan la teva aplicació passa un document a un model de llenguatge, no li passa "el contingut". Li passa el DNI, l'IBAN, el telèfon i el nom del client que hi hagi a dins. I el mateix val per a un registre de log, un servei de traducció o qualsevol tercer.
Fins fa poc només hi havia dues respostes: no enviar-ho, i quedar-te sense la funcionalitat, o enviar-ho sencer i confiar. N'hi ha una tercera, i és la que fem servir: substituir les dades abans que surtin i tornar-les en rebre la resposta.
El nostre client Joan Pérez, DNI 12345678Z, demana la transferència a ES9121000418450200051332
surt com
El nostre client Joan «AP_1», DNI «DNI_1», demana la transferència a «IBAN_1»
El model treballa amb aquesta versió, contesta fent servir els mateixos marcadors, i en tornar es desfà la substitució. El proveïdor no veu mai la dada real i tu no perds res.
Una expressió regular no distingeix un DNI de vuit dígits qualssevol
Aquí és on fallen gairebé totes les solucions casolanes. Buscar vuit dígits seguits d'una lletra troba el DNI, i troba també qualsevol referència interna amb aquesta forma.
La diferència entre un detector de debò i una expressió regular és validar el dígit de control. Un DNI porta una lletra que es calcula amb la resta de dividir el número entre 23. Un IBAN valida amb mòdul 97 segons la norma ISO 13616. Una targeta passa per Luhn i pel seu prefix d'emissor.
Amb aquesta comprovació, 12345678A no es toca, perquè la lletra no correspon. I 12345678Z sí. Aquesta distinció és la que marca la diferència entre una eina utilitzable i una que omple el text de marcadors falsos fins que el model deixa d'entendre res.
Els noms no tenen dígit de control
Els identificadors es validen. Els noms de persona, no, i aquí cal una altra estratègia.
La que funciona combina dos senyals. El primer són els tractaments: "D.", "Dña.", "Sr.", "Dr." introdueixen una persona sense ambigüitat. El segon són els diccionaris de noms i cognoms, que en el cas espanyol surten del cens de l'INE: més de 16.000 noms i 24.000 cognoms amb freqüència suficient.
Una seqüència de dues o més paraules amb majúscula inicial només compta com a persona quan la seva primera paraula és al diccionari de noms. Això descarta "Tribunal Supremo" i "Audiencia Provincial" sense necessitat de llistar-los.
I després hi ha el detall que més m'agrada de tot això. En castellà hi ha noms que també són substantius comuns, sobretot els de tradició mariana: Luz, Milagros, Consuelo, Rosario. Són en una llista a part, i la regla és que només compten si a més es confirma un cognom:
La luz del sol. Luz Martínez vino. Luego Luz habló.
↓
La luz del sol. «PER_1» «AP_1» vino. Luego «PER_1» habló.
La "luz" en minúscula queda intacta. "Luz Martínez" es tokenitza perquè hi ha cognom. I la "Luz" solta del final es recupera després, perquè un cop saps que en aquest text Luz és una persona, pots escombrar-ne les mencions soltes.
Els noms se substitueixen paraula a paraula, no per persona. Sembla un detall menor i no ho és: endevinar que "Joan Pérez" i "el Sr. Pérez" són el mateix individu és una decisió d'identitat, i equivocar-se aquí barreja les dades de dues persones diferents. Tokenitzant cada paraula pel seu compte, el text queda protegit sense que el sistema hagi d'apostar sobre qui és qui.
El truc que vam descobrir amb el gènere
Substituir un nom per un identificador sembla una operació neutra, i en anglès gairebé ho és. En català no: el nom és el que sosté la concordança de tot el que ve al darrere, i en treure'l s'endú una informació gramatical que el model necessitava.
Si tokenitzes el nom de pila, el model perd el gènere i en castellà això trenca la concordança. "Juan Pérez ha sido informado" es converteix en un text on el model no sap si escriure informado o informada, i encerta la meitat de les vegades.
La solució va ser deixar el nom de pila en clar i tokenitzar tota la resta:
app('laranon')->except('person')->newSession();
Surt el cognom, surten el DNI, l'IBAN, el telèfon i el correu. Es queda "Juan". I aquesta decisió, que sembla una concessió, resulta ser la correcta també des del punt de vista legal: un nom de pila solt, sense cognom i sense cap identificador, té un poder de reidentificació molt baix. Estàs protegint el que de debò identifica.
De passada resol un altre problema. Els topònims compostos espanyols ("Santa María", "San Sebastián", "Palma de Mallorca") comencen per paraules que són al diccionari de noms, així que un detector basat en diccionaris els marca com a persones. És una limitació inherent al mètode, no del programa: sense context semàntic, "Santa María" i una senyora que es diu María són la mateixa forma. Deixant fora els noms de pila, aquest soroll baixa molt.
On viu el mapa d'equivalències, que és la decisió important
Per poder desfer la substitució cal desar en algun lloc quin marcador correspon a quin valor. Aquest mapa és tan sensible com les dades originals, perquè és exactament la clau per reconstruir-les.
Hi ha dues postures i convé triar a consciència.
Desar-lo (en memòria cau o en base de dades, xifrat) et permet que el mateix client rebi sempre el mateix marcador entre converses diferents. És còmode i és una dada més a custodiar, amb la seva política de retenció i el seu dret de supressió.
No desar-lo significa que el mapa viu en memòria durant la petició i mor amb ella. Cada torn torna a analitzar el text des de zero. És el que fem nosaltres, i és la postura de risc mínim: no pot filtrar-se el que no existeix en cap disc.
I hi ha una tercera peça que el reglament agraeix: poder esborrar el mapa expressament. Tan bon punt el llences, els marcadors deixen de ser reversibles i el que era seudonimització passa a ser anonimització efectiva. És una operació d'un sol pas, i és justament la que vols tenir a mà quan algú exerceix el seu dret a l'oblit.
Això protegeix la frontera, no el magatzem
Quan algú munta això per primera vegada sol quedar-se amb la idea que ja té les dades protegides, i no és així. Una capa d'anonimització és un filtre de sortida, i tot el que no travessa aquest filtre continua exactament on era.
Substituir dades abans que surtin no xifra la teva base de dades. La conversa se segueix desant amb els valors reals, perquè la teva aplicació els necessita per treballar. El que es protegeix és el moment en què el text creua cap a fora: cap al model, cap al log, cap al tercer.
Són problemes diferents i necessiten respostes diferents. Si la teva obligació és que la dada no surti de la teva infraestructura, això ho resol. Si és que no existeixi en clar a dins, això no és l'eina.
Com es comprova que funciona de debò
Una prova que verifiqui que la funció retorna el text substituït no demostra res, perquè l'error interessant no és aquí. És a la petició que surt pel cable.
La manera correcta de provar-ho és interceptar la crida HTTP al proveïdor i afirmar sobre el cos real:
$this->assertStringNotContainsString('12345678Z', $enviat, 'el DNI va sortir al proveïdor');
$this->assertStringNotContainsString('ES9121000418450200051332', $enviat, 'l\'IBAN va sortir al proveïdor');
$this->assertStringNotContainsString('Pérez', $enviat, 'el cognom va sortir al proveïdor');
$this->assertStringContainsString('Juan', $enviat, 'el nom de pila ha d\'anar en clar');
Aquesta última línia és la que converteix la prova en documentació: diu que deixar el nom és intencionat i no un descuit.
I hi ha un motiu per provar-ho així. Els camins que s'obliden no són el principal: són el resum de l'historial i la destil·lació de memòries, que també envien la conversa al proveïdor i que ningú mira. Si comproves el cos HTTP, els cobreixes tots per construcció.
Què hi ha al mercat i en què es diferencia
La detecció de dades personals fa anys que és un problema de Python, i és allà on hi ha les eines madures. En PHP el panorama és més pobre, així que la decisió no és només quina biblioteca fer servir, sinó si val la pena muntar un servei a part per tenir la bona.
Microsoft Presidio és la referència oberta. Detecta dades personals combinant patrons amb reconeixedors basats en models de llenguatge, i porta un anonimitzador a part. És l'opció més completa si et pots permetre un servei en Python al costat i la latència que afegeix cada crida.
Google Cloud DLP i Amazon Comprehend fan una cosa semblant com a servei gestionat, amb una ironia que convé mirar de cara: per no enviar dades personals a un tercer, les envies a un altre tercer. Segons el contracte que tinguis amb cadascun, això pot ser suficient o no ser-ho gens.
Les llibreries d'expressions regulars són el que s'acaba fent servir en PHP la majoria de vegades. Cobreixen bé el que té format, DNI, IBAN, telèfon, i es queden curtes exactament on comença el difícil, que són els noms propis.
Laranon, que és el que mantenim, aposta per resoldre-ho dins del procés, sense servei extern i amb el mapa d'equivalències sota el teu control, que és el que fa l'operació reversible. Si el teu volum justifica muntar Presidio, munta Presidio. Si el que vols és que cap text surti net de la teva aplicació sense afegir infraestructura nova, aquest és el compromís.
I si no treballes amb Laravel
Els criteris es traslladen, i són els que hauries d'exigir a qualsevol solució que valoris:
- Que validi, no només que busqui. Un detector sense dígit de control produeix tants falsos positius que acabes desactivant-lo.
- Que sigui reversible i que tu decideixis on viu el mapa. Si l'eina desa les equivalències al seu propi servei, has mogut el problema, no l'has resolt.
- Que puguis excloure tipus. El cas del gènere en castellà no el preveu ningú fins que se'l troba, i sense poder dir "això no" et quedes sense sortida.
- I que la comprovació sigui sobre la petició que surt. És l'únic lloc on la veritat es veu.
On és el codi
El paquet està publicat a Packagist, el codi a GitHub i la documentació del paquet recull totes les opcions. És MIT, així que no cal que et refiïs que anonimitza bé: pots llegir-ho. És en producció a Crowd Legal, on els expedients que veu el copilot d'IA passen per aquí abans de sortir.
Si treballes amb dades personals i estàs valorant posar IA al teu producte, aquest és exactament el punt on es decideix si el projecte és viable o no. Ho tractem a intel·ligència artificial i programari legal, i si ens expliques el cas et diem què es pot i què no.