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.
Fase En qué consistió 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.
Qué Quién lo cifra Perfil del peque el cliente, al guardar Cuentos privados el propio motor de generación, del lado servidor Contexto libre de la petición el cliente, al guardar Notas y momentos personalizados 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: Cloud KMS (Google Cloud) guarda la clave maestra — nunca sale de ahí.
La clave maestra envuelve y desenvuelve la clave de datos, por usuario, que se guarda cifrada junto a los datos de cada cuenta.
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: 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.
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 es mejor que 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 s