Desarrollamos tu web presencial SPA DESDE 300€. Si, es una locura. Web SPA DESDE 300€ — Si, es una locura. Hablemos →

Cómo anonimizar datos personales y poder recuperarlos cuando haga falta

Cómo anonimizar datos personales y poder recuperarlos cuando haga falta

Casi cualquier aplicación seria acaba mandando texto fuera: a un modelo de IA, a un servicio de traducción, a un sistema de logs. Y el texto casi nunca va limpio. Este artículo explica cómo montar una capa que sustituye los datos personales antes de que salgan y los devuelve al recibir la respuesta, por qué eso no se resuelve con cuatro expresiones regulares y qué hace falta para que la operación sea reversible sin abrir un agujero nuevo. El código va en Laravel porque es una de nuestras especialidades en AndorraDev, pero el problema es de arquitectura y se resuelve igual en cualquier lenguaje.

Mandar un texto a un modelo es mandarle todo lo que contiene

Cuando tu aplicación le pasa un documento a un modelo de lenguaje, no le pasa "el contenido". Le pasa el DNI, el IBAN, el teléfono y el nombre del cliente que estén ahí dentro. Y lo mismo vale para un registro de log, un servicio de traducción o cualquier tercero.

Hasta hace poco solo había dos respuestas: no mandarlo, y quedarte sin la funcionalidad, o mandarlo entero y confiar. Hay una tercera, y es la que usamos: sustituir los datos antes de que salgan y devolverlos al recibir la respuesta.

Nuestro cliente Juan Pérez, DNI 12345678Z, pide la transferencia a ES9121000418450200051332

sale como

Nuestro cliente Juan «AP_1», DNI «DNI_1», pide la transferencia a «IBAN_1»

El modelo trabaja con esa versión, contesta usando los mismos marcadores, y al volver se deshace la sustitución. El proveedor nunca ve el dato real y tú no pierdes nada.

Una expresión regular no distingue un DNI de ocho dígitos cualesquiera

Aquí es donde casi todas las soluciones caseras fallan. Buscar ocho dígitos seguidos de una letra encuentra el DNI, y encuentra también cualquier referencia interna con esa forma.

La diferencia entre un detector de verdad y una expresión regular es validar el dígito de control. Un DNI lleva una letra que se calcula con el resto de dividir el número entre 23. Un IBAN valida con módulo 97 según la norma ISO 13616. Una tarjeta pasa por Luhn y por su prefijo de emisor.

Con esa comprobación, 12345678A no se toca, porque la letra no corresponde. Y 12345678Z sí. Esa distinción es la que hace la diferencia entre una herramienta usable y una que llena el texto de marcadores falsos hasta que el modelo deja de entender nada.

Los nombres no tienen dígito de control

Los identificadores se validan. Los nombres de persona, no, y ahí hace falta otra estrategia.

La que funciona combina dos señales. La primera son los tratamientos: "D.", "Dña.", "Sr.", "Dr." introducen a una persona sin ambigüedad. La segunda son los diccionarios de nombres y apellidos, que en el caso español salen del censo del INE: más de 16.000 nombres y 24.000 apellidos con frecuencia suficiente.

Una secuencia de dos o más palabras capitalizadas solo cuenta como persona cuando su primera palabra está en el diccionario de nombres. Eso descarta "Tribunal Supremo" y "Audiencia Provincial" sin necesidad de listarlos.

Y luego está el detalle que más me gusta de todo esto. En español hay nombres que son también sustantivos comunes, sobre todo los de tradición mariana: Luz, Milagros, Consuelo, Rosario. Están en una lista aparte, y la regla es que solo cuentan si además se confirma un apellido:

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" se tokeniza porque hay apellido. Y la "Luz" suelta del final se recupera después, porque una vez que sabes que en este texto Luz es una persona, puedes barrer sus menciones sueltas.

Andie recomienda

Los nombres se sustituyen palabra a palabra, no por persona. Parece un detalle menor y no lo es: adivinar que "Juan Pérez" y "el Sr. Pérez" son el mismo individuo es una decisión de identidad, y equivocarse ahí mezcla los datos de dos personas distintas. Tokenizando cada palabra por su cuenta, el texto queda protegido sin que el sistema tenga que apostar sobre quién es quién.

El truco que descubrimos con el género

Sustituir un nombre por un identificador parece una operación neutra, y en inglés casi lo es. En castellano no: el nombre es lo que sostiene la concordancia de todo lo que viene detrás, y al quitarlo se lleva por delante una información gramatical que el modelo necesitaba.

Si tokenizas el nombre de pila, el modelo pierde el género y en castellano eso rompe la concordancia. "Juan Pérez ha sido informado" se convierte en un texto donde el modelo no sabe si escribir informado o informada, y acierta la mitad de las veces.

La solución fue dejar el nombre de pila en claro y tokenizar todo lo demás:

app('laranon')->except('person')->newSession();

Sale el apellido, salen el DNI, el IBAN, el teléfono y el correo. Se queda "Juan". Y esa decisión, que parece una concesión, resulta ser la correcta también desde el punto de vista legal: un nombre de pila suelto, sin apellido y sin ningún identificador, tiene un poder de reidentificación muy bajo. Estás protegiendo lo que de verdad identifica.

De paso resuelve otro problema. Los topónimos compuestos españoles ("Santa María", "San Sebastián", "Palma de Mallorca") empiezan por palabras que están en el diccionario de nombres, así que un detector basado en diccionarios los marca como personas. Es una limitación inherente al método, no del programa: sin contexto semántico, "Santa María" y una señora llamada María son la misma forma. Dejando fuera los nombres de pila, ese ruido baja mucho.

Dónde vive el mapa de equivalencias, que es la decisión importante

Para poder deshacer la sustitución hay que guardar en algún sitio qué marcador corresponde a qué valor. Ese mapa es tan sensible como los datos originales, porque es exactamente la llave para reconstruirlos.

Hay dos posturas y conviene elegir a conciencia.

Guardarlo (en caché o en base de datos, cifrado) te permite que el mismo cliente reciba siempre el mismo marcador entre conversaciones distintas. Es cómodo y es un dato más que custodiar, con su política de retención y su derecho de supresión.

No guardarlo significa que el mapa vive en memoria durante la petición y muere con ella. Cada turno vuelve a analizar el texto desde cero. Es lo que hacemos nosotros, y es la postura de mínimo riesgo: no puede filtrarse lo que no existe en ningún disco.

Y hay una tercera pieza que el reglamento agradece: poder borrar el mapa a propósito. En cuanto lo tiras, los marcadores dejan de ser reversibles y lo que era seudonimización pasa a ser anonimización efectiva. Es una operación de un solo paso, y es justo la que quieres tener a mano cuando alguien ejerce su derecho al olvido.

Esto protege la frontera, no el almacén

Cuando alguien monta esto por primera vez suele quedarse con la idea de que ya tiene los datos protegidos, y no es así. Una capa de anonimización es un filtro de salida, y todo lo que no cruza ese filtro sigue exactamente donde estaba.

Sustituir datos antes de que salgan no cifra tu base de datos. La conversación se sigue guardando con los valores reales, porque tu aplicación los necesita para trabajar. Lo que se protege es el momento en que el texto cruza hacia fuera: hacia el modelo, hacia el log, hacia el tercero.

Son problemas distintos y necesitan respuestas distintas. Si tu obligación es que el dato no salga de tu infraestructura, esto lo resuelve. Si es que no exista en claro dentro, esto no es la herramienta.

Cómo se comprueba que funciona de verdad

Una prueba que verifique que la función devuelve el texto sustituido no demuestra nada, porque el fallo interesante no está ahí. Está en la petición que sale por el cable.

La forma correcta de probarlo es interceptar la llamada HTTP al proveedor y afirmar sobre el cuerpo real:

$this->assertStringNotContainsString('12345678Z', $enviado, 'el DNI salió al proveedor');
$this->assertStringNotContainsString('ES9121000418450200051332', $enviado, 'el IBAN salió al proveedor');
$this->assertStringNotContainsString('Pérez', $enviado, 'el apellido salió al proveedor');
$this->assertStringContainsString('Juan', $enviado, 'el nombre de pila debe ir en claro');

Esa última línea es la que convierte la prueba en documentación: dice que dejar el nombre es intencionado y no un descuido.

Y hay un motivo para probarlo así. Los caminos que se olvidan no son el principal: son el resumen del historial y la destilación de memorias, que también mandan la conversación al proveedor y a los que nadie mira. Si compruebas el cuerpo HTTP, los cubres todos por construcción.

Qué hay en el mercado y en qué se diferencia

La detección de datos personales lleva años siendo un problema de Python, y ahí es donde están las herramientas maduras. En PHP el panorama es más pobre, así que la decisión no es solo qué librería usar, sino si merece la pena montar un servicio aparte para tener la buena.

Microsoft Presidio es la referencia abierta. Detecta datos personales combinando patrones con reconocedores basados en modelos de lenguaje, y trae un anonimizador aparte. Es la opción más completa si puedes permitirte un servicio en Python al lado y la latencia que añade cada llamada.

Google Cloud DLP y Amazon Comprehend hacen algo parecido como servicio gestionado, con una ironía que conviene mirar de frente: para no mandarle datos personales a un tercero, se los mandas a otro tercero. Según el contrato que tengas con cada uno, eso puede ser suficiente o no serlo en absoluto.

Las librerías de expresiones regulares son lo que se acaba usando en PHP la mayoría de las veces. Cubren bien lo que tiene formato, DNI, IBAN, teléfono, y se quedan cortas exactamente donde empieza lo difícil, que son los nombres propios.

Laranon, que es el que mantenemos, apuesta por resolverlo dentro del proceso, sin servicio externo y con el mapa de equivalencias bajo tu control, que es lo que hace la operación reversible. Si tu volumen justifica montar Presidio, monta Presidio. Si lo que quieres es que ningún texto salga limpio de tu aplicación sin añadir infraestructura nueva, este es el compromiso.

Y si no trabajas con Laravel

Los criterios se trasladan, y son los que deberías exigirle a cualquier solución que valores:

  • Que valide, no solo que busque. Un detector sin dígito de control produce tantos falsos positivos que acabas desactivándolo.
  • Que sea reversible y que tú decidas dónde vive el mapa. Si la herramienta guarda las equivalencias en su propio servicio, has movido el problema, no lo has resuelto.
  • Que puedas excluir tipos. El caso del género en castellano no lo prevé nadie hasta que se lo encuentra, y sin poder decir "esto no" te quedas sin salida.
  • Y que la comprobación sea sobre la petición que sale. Es el único sitio donde la verdad se ve.

Dónde está el código

El paquete está publicado en Packagist, el código en GitHub y la documentación del paquete recoge todas las opciones. Es MIT, así que no hace falta que te fíes de que anonimiza bien: puedes leerlo. Está en producción en Crowd Legal, donde los expedientes que ve el copiloto de IA pasan por aquí antes de salir.

Si trabajas con datos personales y estás valorando meter IA en tu producto, este es exactamente el punto donde se decide si el proyecto es viable o no. Lo tratamos en inteligencia artificial y software legal, y si nos cuentas el caso te decimos qué se puede y qué no.

Escrito por
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Desarrollador full-stack con más de 15 años de experiencia en Laravel, React, Node.js y arquitecturas cloud. Ayudo a empresas en Andorra a construir su presencia digital.

Partner de diseño · ionospace.
Necesitas ayuda? ×
Andie by AndorraDev
Asistente IA + equipo humano
Asistente IA de AndorraDev
Andie
Hola! Soy Andie, el asistente IA de AndorraDev. ¿En qué puedo ayudarte? Si necesitas hablar con Edu, solo pídelo.
17:27