DEV Community

Cover image for Cómo pensamos el cifrado de PII en una app Ionic + Angular, para cumplir el RGPD y la LOPD-GDD
Daniel Morfa Vega
Daniel Morfa Vega

Posted on

Cómo pensamos el cifrado de PII en una app Ionic + Angular, para cumplir el RGPD y la LOPD-GDD

Envelope encryption con clave por usuario, qué se cifra y qué no, cómo lo puso a prueba una auditoría externa, y el incidente de rendimiento que provocó nuestro propio hardening de seguridad.

Montaste tu app con IA rápido: le pides unos datos al usuario, llamas al modelo, guardas el resultado en la base de datos y a producción. Cómodo, sin complicaciones.

Hasta que un día miras bien qué estás guardando.

En Cuentopia generamos cuentos personalizados para niños. Para personalizar, un padre nos cuenta cómo es su peque: su carácter, qué le da miedo, qué está pasando en casa. El modelo no improvisa sobre la marcha: se apoya en un marco de criterios clínicos y pedagógicos para decidir cómo abordar cada situación, y luego lo reescribe todo en prosa. Visto de golpe, lo que teníamos en la base de datos era el diario emocional de un montón de menores.

El RGPD lo trata como categoría especialmente protegida. El sentido común, también. ¿Y si se filtra la base de datos? ¿Y un backup mal guardado? ¿Y un acceso indebido con privilegios de admin?

Relájate (bueno, primero asústate un poco; luego relájate). Te voy a contar cómo pensamos el cifrado en reposo en serio: una arquitectura de tipo envelope encryption, con una clave maestra que no sale nunca de Cloud KMS (Google Cloud) y una clave por usuario que cifra los campos sensibles antes de que toquen la base de datos.

Un aviso antes de seguir: te cuento el criterio y las decisiones, no el plano. No vas a encontrar aquí nombres de recursos, rutas de repositorio, ni el detalle exacto que le serviría de receta a alguien con ganas de probar suerte con nuestros datos. Y porque la seguridad honesta se cuenta entera, también te cuento dónde decidimos no llegar y por qué.

Promesa: al terminar vas a entender, con criterio real de producto, cómo una familia sin ser expertos en cripto se planteó cifrar datos de menores, y por qué ciertas decisiones muy concretas no se hacen públicas nunca, ni en el artículo más honesto.

El mapa

Lo construimos de dentro hacia fuera: primero el núcleo del cifrado, después una auditoría adversarial que nos bajó los humos y forzó varios cambios de endurecimiento, y por último, porque toda decisión de seguridad tiene una factura escondida, un incidente de rendimiento que nos costó un buen susto. Esta es esa historia, contada en fases, no en versiones ni en commits.

  • Diseño y cifrado base: arquitectura envelope, clave por usuario, primeros campos cifrados.
  • Auditoría y hardening: pentest adversarial, cierre de hallazgos, borrado con crypto-shredding.
  • Ajuste de rendimiento: un cambio de seguridad correcto que rompió la experiencia, y su arreglo.

Una aclaración antes de seguir, porque importa: hicimos todo este proceso (diseño, auditoría, endurecimiento) con Cuentopia en beta muy temprana, con mi propia cuenta como único usuario real en la base de datos. Ningún dato de ninguna familia estuvo nunca ahí sin este cifrado; lo construimos antes de abrir la puerta a nadie más, precisamente porque hacerlo después, con usuarios reales y datos ya en producción, habría sido mucho más caro y mucho más arriesgado. Si quieres ver cómo se lo contamos a las familias, sin la jerga de este artículo, ahí está la versión pensada para ellas: cuentopia.es/tu-confianza.

Y ya que estamos, crédito donde toca: todo esto corre sobre Firebase: Firestore para los datos, Cloud Functions para la lógica de servidor, y Google Cloud KMS para la clave maestra. No reinventamos gestión de claves: nos apoyamos en servicios gestionados que ya audita gente con más recursos que nosotros.

TL;DR

Cuentopia genera cuentos personalizados con IA a partir de datos sensibles de menores. La solución: cifrado de campos en reposo con envelope encryption. Una clave maestra vive en Cloud KMS, de Google Cloud, y nunca sale de ahí; cada usuario tiene su propia clave de datos, que la maestra envuelve y que se guarda cifrada junto a sus datos en Firestore. El cliente pide su clave una vez por sesión, la usa en memoria para cifrar y descifrar campo a campo, y Firestore solo ve texto cifrado.


Qué ciframos y qué no (y el criterio que lo gobierna)

Antes de tocar criptografía hubo una decisión de producto, refinada en una sesión de grilling del diseño:

  • Se cifra: el perfil del peque (nombre, rasgos, lo que le preocupa, lo que le hace ilusión), el contenido de los cuentos privados, el contexto libre que escribe el adulto al pedir un cuento a medida, y las notas de los "momentos personalizados" que una familia documenta.
  • No se cifra: los cuentos públicos del catálogo, que no contienen datos reales de nadie: usan personajes ficticios.

De ahí nace el criterio que gobierna todo el sistema:

Público ⇒ texto plano. Privado ⇒ cifrado. Un remix de un cuento público entra por el canal privado, así que nace cifrado.

  • Perfil del peque: lo cifra el cliente, al guardar.
  • Cuentos privados: los cifra el propio motor de generación, del lado servidor.
  • Contexto libre de la petición: lo cifra el cliente, al guardar.
  • Notas y momentos personalizados: las cifra el cliente, al guardar.

Este criterio es el que permite que la lectura sea segura por defecto: si un documento no lleva metadata de cifrado, se asume texto plano y pasa tal cual. Así conviven, sin fricción, los cuentos públicos y los privados.

Envelope encryption, sin misterio

El esquema es de libro, en tres niveles:

  1. Cloud KMS (Google Cloud) guarda la clave maestra: nunca sale de ahí.
  2. La clave maestra envuelve y desenvuelve la clave de datos, por usuario, que se guarda cifrada junto a los datos de cada cuenta.
  3. Esa clave de datos cifra y descifra los campos sensibles, campo a campo: la base de datos solo ve texto cifrado.

Por qué envelope y no cifrar cada campo directo contra el gestor de claves: un servicio gestionado de claves cobra y limita por operación, y no quieres mandarle cada campo del formulario. Con envelope, el gestor solo interviene una vez por sesión (para envolver o desenvolver la clave de datos); el cifrado masivo lo hace el propio cliente, en local.

Dos decisiones de diseño que importan, sin entrar en el formato exacto:

  1. Cada valor cifrado queda atado a quién es su dueño y a qué campo pertenece. Mover un valor cifrado de un campo a otro, o de una cuenta a otra, rompe la verificación. No es un detalle menor: convierte cualquier intento de "recombinar" datos cifrados en un fallo ruidoso, no en una fuga silenciosa.
  2. El formato de cifrado lleva versión desde el primer día. Eso deja la puerta abierta a rotar el esquema completo sin tener que rediseñarlo sobre la marcha.

🩺 Moraleja: envelope > cifrar directo contra el gestor de claves. El gestor interviene una vez por sesión; el trabajo pesado lo hace el cliente, y te cuesta céntimos.

La clave de cada usuario: caché, condiciones de carrera y quién puede pedirla

El cliente nunca ve la clave maestra. Pide su clave de datos una única vez por sesión, a una función serverless de Firebase Cloud Functions, y la mantiene en memoria, en un formato que no se puede exportar ni volcar. Esa clave se cachea localmente. Y ahí aparece una condición de carrera con la que merece la pena tener cuidado: si el usuario cierra sesión justo mientras la petición de clave sigue en vuelo, hay que descartar el resultado cuando llegue. Cachearlo de todos modos asociaría la clave del usuario anterior a la sesión nueva. Es el tipo de bug que no rompe nada en desarrollo y sí en producción, con dos pestañas abiertas.

Del lado servidor, la generación de esa clave por usuario es idempotente: la primera vez se genera, se envuelve con la clave maestra y se persiste; cualquier intento posterior devuelve siempre la misma, incluso si dos dispositivos intentan generarla a la vez en un primer login simultáneo: la que gana la carrera es la única verdad, y el "perdedor" recibe esa misma clave en lugar de crear la suya.

Y aplicamos fail-closed como principio, no como parche puntual: ante cualquier ambigüedad de configuración entre entornos, el sistema prefiere fallar de forma ruidosa antes que degradarse en silencio hacia un camino menos estricto.

Y el acceso: la clave envuelta de cada usuario vive en una parte de la base de datos que el cliente nunca puede leer directamente, solo el código que corre con privilegios de servidor. Esa separación es la que, más adelante, hace posible el borrado de cuentas de verdad (ver más abajo).

Que el servidor no se fíe del cliente

El cifrado no se confía solo al cliente: las reglas de acceso de Firestore rechazan cualquier escritura que no tenga la forma exacta de un valor cifrado completo, de principio a fin: nada de subcadenas sueltas ni de texto en claro con un prefijo que lo disimule. Si el cliente tiene un bug y trata de escribir algo sin cifrar, el servidor lo rechaza antes de que llegue a guardarse.

🩺 Moraleja: si el cliente puede equivocarse y escribir en claro, se equivocará. Haz que el servidor lo rechace. Las reglas de acceso son parte del esquema de cifrado, no un añadido.

El pentest que nos bajó los humos

Tras el rollout inicial nos dejamos auditar por fuera, con un enfoque adversarial: reglas de acceso, criptografía, el motor de generación y el tratamiento general de datos personales. El veredicto fue tranquilizador en lo esencial:

El núcleo aguanta. El aislamiento entre cuentas es sólido: la clave de un usuario es inaccesible para otro, y eso no depende de nada que el cliente pueda manipular. No es cifrado de extremo a extremo: quien consiguiera robar una sesión activa y superar las comprobaciones del cliente, podría obtener la clave desde el servidor. Los problemas reales estaban en canales laterales y en endurecer bordes, no en el núcleo.

Aclarando esa frase, porque es la que más se malinterpreta: "no es E2E" no significa que los datos vayan sin cifrar. Significa que la clave para descifrarlos no vive solo en el dispositivo de cada familia, como en una app de mensajería cifrada de verdad. Vive también en nuestra infraestructura (Cloud KMS y Firestore), porque es el propio servidor quien la genera y la entrega bajo demanda a quien se autentique correctamente. Protege frente a una fuga de base de datos, un backup robado o un acceso indebido a los datos en reposo. No protege frente a un compromiso del propio backend de Cuentopia: ahí seguimos siendo, técnicamente, un punto de confianza.

¿Por qué no fuimos a por un E2E de verdad? Porque parte de lo que ciframos lo escribe el propio motor de generación en el servidor: el cuento en sí. Un esquema E2E puro, donde ni el servidor puede descifrar nunca nada, es incompatible con tener una IA que necesita trabajar con ese contenido para escribirlo. Con esa limitación por delante, elegimos lo que sí podíamos ofrecer con solidez: aislamiento fuerte entre cuentas (la clave de una familia es inaccesible para otra, y esa sí es una garantía dura) y datos cifrados en todo momento en reposo.

Para cerrar ese hueco de verdad haría falta mover el procesamiento del agente a un entorno de confidential computing (enclaves con atestación remota, donde ni nuestra propia infraestructura pueda inspeccionar la memoria mientras se genera el cuento). No lo tenemos hoy; queda documentado como posible siguiente paso, no como excusa.

Lo que se corrigió, por categorías:

  • Se ajustó la validación de formato para que fuera más estricta: la versión anterior, sin querer, dejaba colar algo parecido a texto en claro si llevaba el prefijo correcto delante.
  • Se exigió una comprobación de integridad de la app más fuerte en la operación más sensible de todas (la que entrega la clave), cerrando así un camino de ataque que solo dependía de robar una sesión.
  • Se encontró y cerró una fuga activa: un registro de depuración que, sin darnos cuenta, se llevaba datos ya descifrados a un sistema de logs que no tenía ningún motivo para verlos.
  • Se reforzó el principio de fail-closed descrito arriba, cerrando un caso límite de configuración que podía dar lugar a ambigüedad.
  • En vez de cifrar las imágenes de portada, se atacó la raíz del problema: se cambió cómo se le pide la imagen al modelo, para que ya no describa un retrato reconocible del peque.

⚠️ El bug más feo no era de criptografía: era un registro de depuración que se llevaba datos descifrados a un sistema que no pintaba nada ahí. Revisa tus logs. De verdad.

Y dos cambios que cierran obligaciones de RGPD:

  • Borrado de cuenta con crypto-shredding. Borrar una cuenta borra, entre otras cosas, la clave de datos de esa persona. Sin esa clave, cualquier resto cifrado que pudiera quedar en una copia de seguridad es matemáticamente irrecuperable: destruir la clave equivale a destruir el dato, sin tener que perseguir cada copia una por una.
  • Cierre de un hueco de texto en claro residual en uno de los tipos de nota personal, que se nos había quedado fuera del primer barrido.

Cuando el hardening te rompe el producto

Exigir una comprobación de integridad más fuerte en la operación que entrega la clave parecía la decisión correcta. Lo era. Y aun así, rompió el producto.

El síntoma me volvió loco un rato: la función respondía rápido y sin un solo error en sus registros. Pero abrir la ficha de un cuento privado se quedaba cargando casi un minuto.

La trampa estaba fuera de la función. Si pides la clave de forma perezosa, justo al abrir el cuento, y el token que certifica que la petición viene de una app legítima todavía no se ha generado, la llamada se rechaza antes de llegar a entrar en la función. Por eso sus registros estaban limpios: el problema nunca llegaba hasta ahí. Y la pantalla, mientras tanto, reintentaba en bucle hasta agotar su margen de espera.

La cripto no tenía la culpa. La tenía la carrera entre pedir la clave y que ese token de integridad estuviera listo.

El arreglo fue dejar de pedir la clave en el peor momento posible: la calentamos al hacer login, mientras el usuario todavía está en la pantalla de inicio, para que ya esté disponible cuando abra el primer cuento. Es un intento de mejor esfuerzo, con reintentos; si falla, la petición perezosa de siempre sigue actuando de red de seguridad.

🩺 Moraleja: un cambio de seguridad puede ser correcto y aun así arruinar la experiencia. El hardening y el rendimiento se diseñan juntos, o te enteras en producción.

Qué no ciframos, con criterio

Buena parte del trabajo "invisible" fue alinear la residencia de los datos: la base de datos, el gestor de claves, el código que toca estos datos y los modelos de IA que generan los cuentos viven todos dentro de la Unión Europea, backups incluidos.

Y un sistema honesto documenta lo que no hace:

  • El valor cifrado no lleva el identificador del documento al que pertenece, solo el dueño y el campo. En teoría, alguien que ya tuviera la clave de una cuenta podría copiar un valor cifrado entre dos documentos suyos del mismo campo. Lo evaluamos y lo deferimos con criterio: quien ya tiene esa clave puede reescribir cualquier cosa de todos modos, así que el riesgo añadido es bajo frente al coste de rehacer el esquema y volver a cifrar todos los datos existentes. Documentado, no implementado.
  • Las imágenes de portada no van cifradas. Decisión deliberada: en vez de cifrar la imagen, se eliminó el dato personal de su origen: el modelo ya no genera un retrato reconocible del peque, sino un personaje estilizado y ficticio.
  • Los datos del adulto (correo, nombre de la cuenta, datos de facturación) son otro nivel de sensibilidad y no entran en este esquema; cifrarlos rompería la integración con el proveedor de pagos. Decisión de producto, no descuido.
  • Un bloqueo biométrico de la app se evaluó y se aparcó: la versión criptográficamente sólida no compensaba (no cierra el hueco de "sesión robada" y arriesga pérdida de acceso si alguien olvida su PIN); una versión más simple, solo para bloquear la pantalla sin tocar las claves, queda como posible mejora futura.
  • La rotación de la clave maestra queda fuera de alcance por ahora: el formato versionado deja la puerta abierta para el día que haga falta.

🩺 Moraleja: un sistema honesto enumera lo que NO hace. "No es cifrado de extremo a extremo" no es una vergüenza; es una decisión de diseño que se escribe.

Qué te llevas (y qué nos llevamos)

  1. Envelope encryption es el patrón correcto para datos personales en reposo en una app móvil/web con backend gestionado: el servicio de claves guarda la maestra, el cliente hace el trabajo masivo, y el coste es de céntimos.
  2. Atar cada valor cifrado a su dueño y su campo vale oro. Convierte cualquier intento de mover datos cifrados de sitio en un fallo de verificación, no en una fuga silenciosa.
  3. Las reglas de acceso son parte del esquema de cifrado, no un añadido: rechazar texto en claro en el servidor es lo que hace que un bug del cliente no se convierta en una fuga.
  4. Crypto-shredding hace operativo el "derecho al olvido". Borrar una clave es instantáneo y alcanza incluso las copias de seguridad; borrar datos a mano, no.
  5. Un sistema de cifrado honesto enumera su modelo de amenaza y sus huecos. "No es de extremo a extremo" no es una vergüenza: es una decisión de diseño que hay que escribir.
  6. El hardening tiene secuelas. Una comprobación de seguridad correcta puede, aun así, dejar el producto colgado casi un minuto. Seguridad y rendimiento se prueban juntos.

(Cómo piensa el agente que escribe estos cuentos, con qué criterio decide qué contar y cómo, para que no sea una tirada de dados de un modelo de lenguaje, es tema para otro artículo.)

Cifrar la PII de los peques no era "una feature de seguridad" más en el backlog: era ponerle un seguro de salud al producto y a la confianza de quien lo usa. Y costó céntimos al mes.

Checklist de mentalidad (cripto de PII en serio):

  • Envelope, no cifrado directo contra el gestor de claves. La clave maestra no sale de ahí; el cifrado masivo lo hace el cliente. Céntimos, no facturas.
  • Ata cada valor cifrado a su dueño y su campo. Es prácticamente gratis y convierte cualquier swap de datos en un fallo de verificación.
  • Que el servidor rechace el texto en claro. Las reglas de acceso son parte del esquema, no un adorno.
  • Crypto-shredding = derecho al olvido operativo. Borrar una clave es instantáneo y alcanza las copias de seguridad. Borrar datos a mano, no.
  • Enumera tus huecos. "No es de extremo a extremo" escrito no es debilidad: es la diferencia entre un sistema seguro y uno opaco.
  • Prueba seguridad y rendimiento juntos. Una comprobación de integridad correcta te puede dejar el producto colgado casi un minuto. Pregúntame cómo lo sé.

Y una última cosa. Nadie nos obligó a esto: ni un cliente, ni una auditoría externa que nos llegara de sorpresa. Pero si vas a pedirle a una familia que te cuente lo más íntimo de su hijo, lo mínimo es tratarlo como lo que es. Monta tu envelope, ata tus datos cifrados a su dueño, destruye la llave al borrar la cuenta… y duerme tranquilo sabiendo que lo de tus usuarios está cifrado de verdad. Si le encuentras un fallo a lo nuestro, escríbeme: la versión honesta de la seguridad incluye que alguien de fuera te diga dónde te equivocaste. Tu yo del futuro, y el padre que confió en ti, te lo agradecerán.

Top comments (0)