← Todos los casos de estudio
Caso de estudio2025 — 26

Arquitectura privacy-first en una plataforma de salud

En una plataforma que maneja datos de salud sensibles, protegerlos no era una funcionalidad — era la arquitectura.

Node.jsNestJSLlaves X25519Argon2id KEKColas Async BullMQHashing SHA-256Retención en Memoria Volátil

Contexto

Trabajé en una plataforma de salud segura que manejaba datos personales altamente sensibles bajo estrictos requisitos de privacidad y cumplimiento. En un producto así, la protección de datos no puede ser algo que se añade al final — tiene que moldear la arquitectura desde la primera decisión.

Desafío

El cifrado a nivel de base de datos, por sí solo, no es suficiente. Protege los datos en reposo, pero deja el texto plano expuesto a cualquier cosa con acceso a la base de datos — bugs de aplicación, consultas demasiado amplias, herramientas internas. Para una plataforma con información de salud sensible, esa exposición residual era inaceptable. Necesitábamos proteger múltiples categorías de datos personales en una capa por encima de la base de datos, sin perder la capacidad de usar realmente los datos.

Restricciones

Un contexto de salud regulado fijó el listón: estrictos requisitos de privacidad y cumplimiento aplicaban a cada categoría de datos personales que la plataforma tocaba, y el acceso a los datos debía seguir el principio de mínimo privilegio en todo momento. Ganara lo que ganara el diseño en protección, el producto seguía necesitando poder buscar registros.

Arquitectura de Privacidad y Matriz de Flujos

El diseño técnico estructuró las interacciones alrededor del criptosistema a través de 6 flujos de datos explícitos:

  1. Envío de Formulario de Intake (Procesamiento Asíncrono de Sobres):
    • El paciente completa el formulario en la PWA Seeker, cifrando la carga útil con la Llave Pública X25519 del proveedor.
    • POST /api/envelopes encola el sobre en BullMQ y responde de inmediato 200 OK al cliente.
    • Un worker en segundo plano descifra el sobre con la Llave Privada del Proveedor, genera el par de llaves del Paciente + ORDER_CODE, cifra los datos sensibles (PHI) con una Llave Maestra aleatoria de 32 bytes y los guarda vía Prisma ORM antes de despachar el código vía SMS/email.
  2. Autenticación del Paciente y Derivación de Llaves:
    • El paciente ingresa con ORDER_CODE + Respuesta Secreta (POST /api/auth/patient). Credenciales verificadas contra hashes SHA-256 en BD.
    • El servidor retorna un JWT de sesión + Llave Privada cifrada del paciente.
    • La PWA deriva una Llave de Cifrado de Llaves (KEK) mediante Argon2id, descifra la Llave Privada del Paciente y la mantiene estrictamente en memoria volátil JS (NUNCA en localStorage o sessionStorage).
  3. Mensajería en Tiempo Real Cifrada (X25519) (Paciente ↔ Doctor):
    • Mensajes enviados desde la PWA cifrados en cliente mediante Llave Privada Paciente + Llave Pública Proveedor (intercambio X25519).
    • Transmitidos por WebSocket (message.send). El servidor WS valida el JWT, descifra usando la Llave Privada del Proveedor y la Llave Pública del Paciente, envuelve la carga con la Llave Maestra para almacenamiento persistente en BD vía Prisma y la transmite en tiempo real a la interfaz del Doctor.
  4. Respuesta del Doctor:
    • El doctor responde desde la Admin UI → el servidor obtiene la llave pública del paciente → cifra mediante el intercambio asimétrico correspondiente → envuelve con la Llave Maestra para la BD → entrega por WebSocket si el paciente está en línea o mantiene la respuesta para consultarla tras la reconexión.
  5. Bootstrapping de la Raíz de Confianza:
    • La configuración del Super Admin (/setup) genera un seed nemónico BIP39 de 12 palabras para derivar el Par de Llaves Raíz del Proveedor (X25519).
    • El Password Maestro deriva una KEK (Argon2id) para cifrar la Llave Privada del Proveedor en provider_keys. La Llave Maestra (32 bytes) se envuelve con la Llave Pública del Proveedor y se guarda en master_keys.
  6. Motor de Despliegue Multi-Tenant:
    • El registro de nuevos clientes automatiza el aprovisionamiento de infraestructura dedicada: el Motor de Despliegue crea instancias aisladas de Provider App (Vercel/Railway) y Seeker PWA (Vercel) con enrutamiento de subdominios DNS.
    • La BD central de la plataforma almacena solo metadatos de despliegue; la PHI permanece en las bases de datos aisladas de cada cliente y no se copia a la plataforma central.

Trade-offs

Decisión de ingeniería Confianza alta

Cifrado de PII a nivel de aplicación en lugar de cifrado solo en base de datos

Una plataforma de salud segura manejaba datos personales altamente sensibles bajo estrictos requisitos de privacidad y cumplimiento. El cifrado a nivel de base de datos, por sí solo, deja el texto plano expuesto a cualquier cosa con acceso a la base de datos.

Cifrar múltiples categorías de PII en la capa de aplicación mediante envelope encryption con X25519, derivar las llaves de cifrado de llaves del cliente con Argon2id, mantener las llaves privadas descifradas solo en memoria volátil del navegador y usar hashes determinísticos SHA-256 cuando los campos cifrados debían seguir siendo consultables. El acceso a los datos siguió el principio de mínimo privilegio.

Se ganó
  • Defensa en profundidad más allá del límite de la base de datos
  • Separación llaves/datos vía envelope encryption y una mejor postura de gestión de llaves
  • Menor exposición por almacenamiento persistente de llaves en el cliente gracias a la retención solo en memoria
  • Postura de cumplimiento y auditabilidad apropiada para PII de salud
Se cedió
  • Mayor complejidad de implementación
  • Los campos cifrados no pueden consultarse por igualdad normal — se necesitaron hashes determinísticos SHA-256, aceptando la filtración de igualdad donde las búsquedas eran necesarias
  • El procesamiento asíncrono del intake y la operación de workers añadieron complejidad de implementación
  • La retención solo en memoria exige autenticarse de nuevo tras recargar la página o cerrar la pestaña

La seguridad debe ser parte de la arquitectura desde el inicio — no puede añadirse al final.

La consecuencia dura de cifrar en la capa de aplicación es que consultar campos cifrados deja de ser gratis — ya no puedes buscar registros por valor. Usamos hashing determinístico SHA-256: aplicar hash al valor sensible de modo que entradas iguales produzcan el mismo hash, lo que permite búsquedas seguras por igualdad sin descifrar ni exponer nunca el texto plano. El costo honesto es que el hashing determinístico filtra igualdad — valores idénticos producen hashes idénticos — algo que aceptamos donde las búsquedas eran necesarias.

Implementación

Participé en la implementación y el mantenimiento de estos mecanismos de seguridad como parte del esfuerzo de ingeniería backend y full-stack. En la práctica eso significó trabajar dentro de los patrones que la arquitectura exigía: decisiones de cifrado a nivel de campo, derivación de llaves Argon2id, colas asíncronas BullMQ para descifrado de sobres, gestión del ciclo de vida de llaves solo en memoria, rutas de búsqueda basadas en hash que se mantienen indexables, flujos de validación que respetan el límite cifrado, y logging que nunca toca el texto plano.

Lecciones

El cambio duradero para mí fue tratar la seguridad como un requisito arquitectónico, decidido desde el inicio — no una funcionalidad añadida al final. Retroadaptar privacidad en un sistema maduro es una crisis; diseñarla desde el día uno es manejable. También redefinió “terminado”: una funcionalidad no está terminada cuando funciona — está terminada cuando funciona y los datos que toca están protegidos por defecto.

Qué mejoraría

Profundizaría en el trade-off entre cifrado determinístico y aleatorizado para campos consultables — el hashing determinístico habilita búsquedas por igualdad pero filtra igualdad — y en patrones más sólidos para consultar datos cifrados sin esa filtración.

↑↓ navegar · abrir · esc cerrar