wandres.dev
CIFRADO EXTREMO A EXTREMO · sobre datos que convergen

El choque de fondo: un servidor ciego no puede ayudarte

Cifrar de extremo a extremo deja al servidor sin poder leer, y un servidor que no lee no puede fusionar, comprimir, indexar ni arbitrar: toda esa lógica tiene que mudarse al cliente y alguien paga la mudanza.

⏱ 22 min

Hasta aquí el recorrido ha construido un sistema donde el dato vive en el cliente, converge sin árbitro y viaja por una red que no hace falta que sea de confianza. Queda una promesa que casi todo el mundo da por hecha y que nadie ha pagado todavía: que el servidor por el que pasan los cambios no pueda leerlos. Añadir cifrado extremo a extremo suena a una capa más, una llamada a cifrar antes de enviar y otra a descifrar al recibir, y ese es exactamente el malentendido que este nivel viene a deshacer. Cifrar no es una capa: es una redistribución del trabajo. Un servidor que no puede leer no puede fusionar dos versiones, no puede comprimir por diferencia, no puede construir un índice de búsqueda, no puede validar lo que le mandan y no puede decidir quién ganó. Todo eso lo hacía alguien, y a partir del momento en que se cifra lo tiene que hacer el cliente o no lo hace nadie. Esta lección enumera con precisión qué se pierde, por qué se pierde y qué queda en pie, porque las cuatro lecciones siguientes son consecuencias de este reparto.

🎯 Al terminar esta lección sabrás
  • Enumerar las funciones concretas que un servidor de sincronización pierde al no poder leer la carga útil.
  • Entender por qué la compresión por diferencia entre textos cifrados es imposible por diseño y no por falta de ingenio.
  • Reconocer por qué la convergencia sin árbitro y el cifrado encajan bien, y dónde dejan de encajar.
  • Delimitar el conjunto pequeño de cosas que un servidor ciego sí puede seguir haciendo con utilidad.
  • Situar el coste real de la mudanza en el dispositivo más modesto de la flota.

Las cinco cosas que hacía el servidor y deja de hacer

Conviene inventariar el trabajo del servidor de un motor de sincronización antes de quitarle la vista, porque la lista es más larga de lo que la palabra servidor sugiere. La primera función es fusionar: recibir cambios de varios clientes y producir un estado combinado, ya sea aplicando una transformación operacional o simplemente ordenando y reescribiendo. La segunda es compactar: guardar millones de operaciones pequeñas cuesta, y todo motor serio las agrupa periódicamente en instantáneas y las comprime por diferencia contra lo que ya tenía. La tercera es indexar: la búsqueda de texto completo, los filtros, los agregados y las vistas materializadas se construyen donde está el corpus entero. La cuarta es validar: rechazar una operación mal formada, un campo fuera de rango o una escritura que viola una regla de negocio. La quinta es arbitrar: cuando dos clientes proponen algo incompatible, decidir con autoridad cuál queda.

Las cinco requieren leer la carga útil. Al cifrarla, las cinco caen a la vez, y no de forma gradual sino completa: el servidor pasa de entender el noventa por ciento del contenido a entender el cero. No hay término medio útil, porque cualquier cosa que se deje en claro para que el servidor la aproveche es exactamente la cosa que el atacante aprovechará también.

flowchart LR
O[operacion en claro] --> P[cifrar en el cliente]
P --> S[el servidor recibe un sobre opaco]
S --> A[almacenar y reenviar]
S --> B[verificar firma cuota y tamano]
S --> X[fusionar comprimir indexar validar arbitrar]
style A fill:#a6e3a1,color:#11111b
style B fill:#a6e3a1,color:#11111b
style X fill:#f38ba8,color:#11111b

De las cinco, la que más sorprende es la segunda, y merece detenerse porque su imposibilidad es una propiedad deseada del cifrado y no una limitación de las bibliotecas. Un esquema de cifrado decente es indistinguible: dos textos claros parecidos, cifrados con nonces distintos, producen salidas sin ninguna correlación observable. Si un algoritmo de compresión encontrara redundancia entre dos textos cifrados, eso mismo sería una fuga: significaría que del cifrado se deduce algo del claro.

// Dos versiones casi identicas del mismo documento
const a = 'reunion del martes a las diez';
const b = 'reunion del martes a las once';

diferencia(a, b).length;             // unos pocos bytes: se comprime muy bien

// Cifradas, no comparten absolutamente nada explotable
const ca = await cifrar(clave, nonceA, a);
const cb = await cifrar(clave, nonceB, b);
diferencia(ca, cb).length;           // practicamente el tamano entero de cb

El orden inverso tampoco salva la situación. Comprimir antes de cifrar sí funciona, pero desplaza el problema: el tamaño del resultado pasa a depender del contenido, y de ahí salen las fugas por longitud que la lección siguiente examina. Y comprimir en el cliente significa que la ventana de compresión es la del cliente, no la del servidor, de modo que se pierde toda la redundancia entre documentos y entre usuarios que hacía barato el almacenamiento compartido.

Convergencia y cifrado encajan mejor de lo que parece

La buena noticia es estructural y explica por qué este nivel llega justo aquí y no antes. Una estructura que converge por construcción no necesita que nadie fusione en el centro: dos réplicas que aplican el mismo conjunto de operaciones llegan al mismo estado con independencia del orden en que las reciban. Es decir, la primera y la quinta función de la lista —fusionar y arbitrar— ya se habían mudado al cliente varios niveles atrás, por motivos que no tenían nada que ver con la privacidad. Un sistema que hubiera dependido de fusión en el servidor tendría que rehacerse entero para cifrar; uno que converge solo necesita que el servidor siga reenviando sobres.

Esa afinidad es real y conviene apreciarla, pero también acotarla, porque suele contarse de forma exagerada. Lo que encaja bien es el modelo de propagación: sobres opacos, entrega eventual, sin semántica en el centro. Lo que no encaja es el mantenimiento. Un registro de operaciones crece sin parar, y la única manera de reducirlo es compactarlo, lo cual exige leer, interpretar y descartar lo que ya no aporta. Con cifrado, esa tarea solo puede hacerla un cliente que tenga la clave y el historial completo, y un cliente así no siempre existe: el teléfono no puede sostener el corpus, el portátil lleva seis meses apagado y el servidor, que sería el candidato natural por disponibilidad y capacidad, es precisamente el que no puede.

⚠️
La compactación es el coste oculto que casi nadie presupuesta

Es habitual descubrirlo tarde, cuando el sistema ya está en producción y el documento de un usuario intenso ocupa cien veces lo que ocupa su contenido. Sin cifrado, se soluciona con un proceso nocturno en el servidor que nadie ve. Con cifrado, hay que decidir qué cliente lo hace, cómo se coordina para que no lo hagan dos a la vez, qué pasa si se queda a medias y cómo se avisa a los que estaban sincronizados con la forma antigua. Es un subsistema entero, no una función, y conviene diseñarlo el primer día en lugar del último.

Lo que un servidor ciego sí puede seguir haciendo

Sería un error concluir que el servidor sobra. Sigue teniendo tareas valiosas, y todas comparten una característica: operan sobre el sobre, nunca sobre la carta. Puede almacenar de forma duradera y replicada, que es justo lo que los dispositivos hacen mal. Puede entregar a quien esté desconectado ahora y aparezca dentro de un mes, que es lo que ninguna red entre pares consigue de forma fiable. Puede verificar firmas, porque las claves públicas de los autores son públicas por definición. Puede aplicar autorización de escritura por autor y por documento, porque eso son metadatos. Puede imponer cuotas, limitar tamaños y frenar abusos. Y puede ordenar por llegada, aunque ese orden no signifique nada semánticamente.

// Todo lo que el servidor puede comprobar sin abrir el sobre
function aceptar(sobre) {
  if (!verificarFirma(sobre.autor, sobre.carga, sobre.firma)) return false;
  if (!puedeEscribir(sobre.autor, sobre.documento)) return false;
  if (sobre.carga.byteLength > LIMITE_POR_OPERACION) return false;
  if (cuotaUsada(sobre.autor) > CUOTA) return false;
  return true;                    // aceptado sin haber leido un solo byte util
}
🗄️

Durabilidad

Guardar réplicas en varias máquinas y devolverlas años después es exactamente lo que los dispositivos no saben hacer.

📮

Buzón asíncrono

Entregar a alguien que no estaba conectado cuando se escribió es la función que hace viable el trabajo en equipo real.

✍️

Autoría verificable

Las firmas se comprueban con claves públicas, así que filtrar escrituras por autor no requiere ver el contenido.

🚧

Cuotas y abuso

Contar bytes, limitar ritmo y cortar a quien satura son funciones puramente métricas y siguen intactas.

La tercera tarjeta encierra una asimetría que conviene fijar desde ahora porque decide el diseño de todo lo que viene. El control de escritura sobrevive al cifrado sin dificultad: basta con firmar y con que el resto rechace lo que no venga firmado por alguien autorizado, y esa comprobación la puede hacer tanto el servidor como cualquier par. El control de lectura, en cambio, no sobrevive: una vez que un sobre ha llegado a un dispositivo y ese dispositivo tiene la clave, ninguna política puede impedir la lectura. Esta asimetría entre escribir y leer atraviesa el nivel entero y es el motivo de que la última lección se llame problema abierto.

Dónde acaba la mudanza: el dispositivo más modesto

Todo trabajo que sale del servidor aterriza en algún cliente, y el presupuesto real del sistema no lo fija el portátil del programador sino el teléfono de gama media del usuario que menos aguanta. Conviene hacer las cuentas antes de prometer nada. La búsqueda de texto completo obliga a tener el corpus descifrado localmente, o al menos un índice construido en local, lo cual descarta de golpe el patrón de cliente ligero sobre un corpus grande. Los agregados y los informes se calculan en el dispositivo, con la memoria y la batería del dispositivo. La compactación pide historial completo y tiempo de proceso. Y la carga inicial de un usuario nuevo deja de poder servirse como una instantánea preparada por el servidor, porque el servidor no puede prepararla: hay que entregar el registro y dejar que el cliente lo reconstruya.

Las mitigaciones existen y todas son parciales. Se puede mantener un índice cifrado con esquemas de búsqueda sobre cifrado, a cambio de fugas de patrón de acceso bien documentadas. Se puede nombrar a un cliente de confianza permanente —un ordenador que siempre está encendido, propiedad del usuario o del equipo— que asuma el papel del servidor sin serlo. Se puede segmentar el corpus para que cada dispositivo cargue solo la parte que le toca. Ninguna es gratuita, y la elección entre ellas es una decisión de producto tanto como de ingeniería.

Cifrar no añade una capa: reasigna quién hace cada trabajo y quién lo paga

Si este nivel deja una sola idea, que sea esta, porque es la que evita el error más caro de esta familia de sistemas. La formulación habitual —añadimos cifrado extremo a extremo— presenta el asunto como una decisión local, un par de llamadas más en la ruta de envío y recepción, y por eso se planifica como una tarea de dos semanas que acaba consumiendo un año. Lo que realmente ocurre es un traslado de responsabilidades: cada función que dependía de que alguien leyera el contenido tiene que cambiar de dueño, y el único dueño disponible es un dispositivo con menos memoria, menos energía, menos disponibilidad y menos tiempo de vida que la máquina que la hacía antes. La consecuencia práctica es que la decisión de cifrar no se toma en la capa de transporte sino en la de producto, porque determina qué funciones puede tener el sistema. Un buscador global instantáneo sobre diez años de documentos, un informe agregado sobre toda la organización, una migración de esquema aplicada de golpe a millones de registros, una moderación automática de contenido: todas ellas son funciones que un servidor ciego no puede prestar, y ninguna cantidad de ingenio en la capa criptográfica las devuelve. Por eso el orden correcto de trabajo es el inverso al habitual. Primero se enumera qué hace hoy el servidor con el contenido en claro, se marca cuáles de esas funciones son imprescindibles y se decide, una por una, si se mudan al cliente, si se sustituyen por una versión degradada o si se renuncian. Solo cuando esa lista está escrita tiene sentido elegir un algoritmo, y a esas alturas la elección del algoritmo será, casi siempre, la parte más fácil y menos discutible del proyecto. El resto del nivel se dedica a las tres consecuencias que esa lista nunca incluye la primera vez: que el servidor sigue viendo la forma del tráfico aunque no vea el contenido, que repartir claves entre dispositivos reintroduce por la puerta de atrás la necesidad de confiar en alguien, y que quitar el acceso a quien ya lo tuvo es, literalmente, imposible.

⚔️ Inventaria tu propio servidor antes de cegarlo
  1. Escribe la lista completa de cosas que tu servidor hace hoy leyendo el contenido de los documentos, sin omitir las que parecen triviales.
  2. Marca cada una como muda al cliente, degrada o renuncia, y anota al lado quién se enfada si la respuesta es renuncia.
  3. Mide cuánto ocupa el registro de operaciones de un documento intenso y calcula cuánto ocuparía sin compactación durante un año.
  4. Cifra dos versiones casi idénticas de un documento y comprueba empíricamente qué le pasa a la compresión por diferencia.
  5. Estima el tiempo de carga inicial de un usuario nuevo en el teléfono más modesto que tengas, sirviendo registro en lugar de instantánea.
  6. Decide qué cliente de tu sistema hará la compactación, cómo se coordina y qué ocurre si se queda a la mitad.