Cómo la sincronización opcional en la nube de Filarr sigue siendo zero-knowledge (un vistazo al código)

La sincronización de Filarr es zero-knowledge: los archivos suben ya cifrados, el servidor solo guarda blobs opacos y nunca recibe tu clave en claro.

Mathis Belouar-Pruvot

· fundador de Filarr · · 18 min de lectura

Te has comprado un segundo portátil, o quieres tus notas de trabajo en el equipo de casa y en el de la oficina. En cuanto tus archivos salen de un dispositivo, te enfrentas a la misma pregunta silenciosa que todas las herramientas en la nube te piden que ignores: ¿quién puede leerlos por el camino, y quién puede leerlos una vez que llegan?

La respuesta de Filarr es que sus servidores solo guardan datos cifrados que no pueden leer. Tus archivos se sincronizan entre tus dispositivos, pero el servidor que los transporta nunca tiene tu clave en claro, nunca ve un nombre de archivo y nunca toca un solo byte de contenido legible. Este artículo explica exactamente cómo funciona, directamente a partir del código de sincronización, porque «zero-knowledge» es una afirmación que solo significa algo si puedes ver el mecanismo que hay detrás.

Una aclaración sobre el alcance antes de empezar: en Filarr, la sincronización en la nube es opcional. El producto es local-first: tus archivos viven cifrados en tu disco, actives o no la sincronización algún día. Todo lo que sigue describe lo que ocurre cuando decides activarla.

Respuesta rápida

Filarr sincroniza tus archivos subiéndolos exactamente como ya están en tu disco: cifrados con AES-256-GCM con la clave de tu cuenta (la FEK), con un valor aleatorio nuevo para cada archivo. La FEK nunca llega al servidor en forma legible. El Cloudflare Worker que gestiona la sincronización solo guarda blobs cifrados opacos con identificadores de archivo opacos (un hash SHA-256 de la ruta local), más un pequeño manifiesto que a su vez está cifrado en AES-256-GCM con tu FEK. El servidor puede ver cuántos blobs tienes y cuánto ocupan, pero no sus nombres, su estructura de carpetas ni su contenido. Un segundo dispositivo obtiene tu clave iniciando sesión, lo que descarga la copia de la FEK envuelta con tu contraseña, o de dispositivo a dispositivo mediante un intercambio de claves ECDH, en el que la FEK viaja envuelta y el servidor solo retransmite claves públicas.

El problema que resuelve de verdad

La mayoría de las sincronizaciones se basan en «confía en el proveedor». Notion guarda tus páginas en claro en sus servidores. Dropbox tiene las claves de los archivos que cifra en reposo, lo que significa que Dropbox (o una autoridad que se lo solicite a Dropbox) puede leerlos. Incluso las herramientas que cifran en tránsito suelen descifrar al llegar para poder indexar, previsualizar o buscar en tu contenido desde el servidor.

Ese modelo es cómodo, y también es todo el problema. Comodidad en el servidor significa legibilidad en el servidor. Si el proveedor puede generar una miniatura de tu documento, el proveedor puede leer tu documento.

La restricción de Filarr era la contraria: el backend de sincronización tenía que ser realmente incapaz de leer nada de lo que guarda, sin dejar de hacer el trabajo útil de mover bytes entre tus equipos y resolver conflictos. «Zero-knowledge» no es aquí un adjetivo de marketing: es un requisito de diseño que determinó dónde ocurre el cifrado y qué tiene permitido saber el servidor.

Cómo funciona, paso a paso

1. El cifrado ya ocurrió, antes de que exista la sincronización

Esta es la idea clave que simplifica todo lo demás. En Filarr, los archivos se cifran en disco con AES-256-GCM, con la clave de tu cuenta y un valor aleatorio nuevo para cada archivo, en el momento en que entran en tu bóveda. La sincronización no añadió el cifrado. Lo hereda.

Cuando el servicio de sincronización lee un archivo para subirlo, lee del disco los bytes ya cifrados y los envía tal cual. No hay un paso de descifrar y volver a cifrar, ni una zona de preparación en texto plano. El comentario al principio del servicio de sincronización lo dice claramente: «Files on disk are already encrypted with FEK, uploaded as-is» (los archivos en disco ya están cifrados con la FEK y se suben tal cual). El servidor recibe texto cifrado porque el texto cifrado es lo único que ha existido fuera de la memoria de la app.

2. Los nombres de archivo se convierten en identificadores opacos

Un blob de texto cifrado no le sirve de nada a un atacante, pero un nombre de archivo como «carta-de-dimision-final.pdf», sí. Así que Filarr nunca envía nombres de archivo.

Cuando un archivo cambia, el servicio de sincronización obtiene un identificador calculando el hash de su ruta local: crypto.createHash('sha256').update(localPath).digest('hex').slice(0, 32). El comentario del código es explícito sobre la intención: «Derive opaque fileId from local path, never expose real file names» (obtener un fileId opaco a partir de la ruta local, no exponer nunca los nombres reales de los archivos). Las carpetas se referencian por sus UUID, que tampoco tienen ningún significado. En el servidor, tus archivos viven en una ruta como users/{userId}/profiles/{profileId}/files/{fileId}/chunk_0.enc, donde cada segmento es un identificador aleatorio o un hash opaco.

3. Los archivos se dividen en bloques

Antes de subirlos, los archivos se dividen en bloques de 4 MB (CHUNK_SIZE = 4 * 1024 * 1024). Cada bloque se guarda como un objeto .enc independiente en Cloudflare R2. Dividir en bloques mantiene cada subida lo bastante pequeña como para reintentarla sin mucho coste en una conexión inestable, y hace que un archivo grande que ha cambiado no obligue a una única transferencia monolítica. Las subidas se ejecutan en pequeños lotes en paralelo (de tres en tres) con una cola de reintentos persistente detrás, de modo que una conexión caída se reanuda en lugar de volver a empezar.

4. El manifiesto: cómo se ponen de acuerdo los dispositivos sobre la realidad

Esta es la parte realmente interesante, porque es donde un sistema zero-knowledge suele tener fugas. Para sincronizarse, dos dispositivos tienen que ponerse de acuerdo sobre qué archivos existen, cuáles han cambiado y cuál ha ganado un conflicto. Esa coordinación normalmente exige que el servidor entienda tu estructura de archivos. El servidor de Filarr no la entiende.

Hay dos manifiestos, y la distinción importa:

El manifiesto local (sync-manifest.json) vive en tu disco y no está cifrado, pero está vacío a propósito de cualquier cosa sensible. Contiene sumas de comprobación SHA-256, tamaños en bytes, estados de sincronización e identificadores de archivo opacos. Ni nombres de archivo ni contenido. El campo localPath, que asocia un identificador con una ruta real, solo existe en esta copia local y se elimina antes de que nada vaya a la nube.

El manifiesto en la nube (manifest.enc en R2) es la fuente de verdad compartida entre dispositivos, y está cifrado con tu FEK en AES-256-GCM (encryptWithFEK). Su formato en disco es un marcador corto («fek:») seguido de un IV de 12 bytes y del texto cifrado con su etiqueta de autenticación GCM. El servidor lo guarda como un blob opaco más. Cuando un segundo dispositivo quiere conocer el estado de las cosas, descarga este blob y lo descifra localmente, porque tiene la FEK. El servidor nunca puede.

Toda la lógica de fusión (qué archivo es más reciente, cuál ha cambiado en ambos lados y es, por tanto, un conflicto) se ejecuta en tu dispositivo, sobre el manifiesto descifrado, en mergeWithRemote. El único trabajo del servidor en torno al manifiesto es el control de versiones.

5. Bloqueo optimista, para que dos dispositivos no se pisen

Cada perfil tiene un contador manifest_version guardado en la base de datos D1 del Worker. Cuando un dispositivo quiere subir un manifiesto nuevo, le dice al servidor qué versión cree que es la actual. Si la versión del servidor no coincide, devuelve un conflicto 409 y la subida se rechaza (if (profileSync.manifest_version !== body.version) return ... 409). El dispositivo vuelve entonces a descargar, vuelve a fusionar y lo intenta de nuevo en el siguiente ciclo.

Este es el truco elegante: el servidor puede imponer «solo gana un escritor por versión» usando únicamente un número entero. Nunca necesita entender el contenido del manifiesto para impedir que dos dispositivos corrompan el estado del otro. Coordinación sin comprensión.

6. Las subidas y descargas pasan por un proxy de tokens

Los Workers no pueden generar directamente URL prefirmadas al estilo clásico de S3, así que Filarr usa un patrón de tokens. El dispositivo hace una petición autenticada para obtener un hueco de subida, el Worker comprueba tu cuota de almacenamiento, guarda en KV un registro de intención de corta duración (TTL de 15 minutos) asociado a un token aleatorio y devuelve una URL con ese token. El dispositivo hace entonces un PUT del bloque cifrado a esa URL, y el Worker lo transmite a R2 e incrementa tu contador de uso. El token es la autorización para esa única operación, y se borra al usarse. Los bytes que pasan son, otra vez, texto cifrado que ya estaba cifrado.

7. Llevar tu clave a un segundo dispositivo sin enviarla nunca en claro

Todo lo anterior supone que los dos dispositivos tienen la FEK. La forma más sencilla de llevarla al nuevo es iniciar sesión en él: el servidor envía la copia de la FEK envuelta con tu contraseña, y el dispositivo la desenvuelve localmente. La otra forma se salta la contraseña: emparejas el dispositivo nuevo con uno que ya está desbloqueado. ¿Cómo consigue entonces el emparejamiento pasar la clave sin que el servidor la vea nunca?

El emparejamiento usa un intercambio de claves ECDH efímero sobre la curva P-256. El flujo, según pairingService.ts:

El dispositivo A (el que ya tiene tu bóveda) genera un par de claves ECDH efímero y un código aleatorio de 6 dígitos. El dispositivo B genera su propio par de claves ECDH. Cada dispositivo publica su clave pública en el servidor, descarga la del otro y deriva por su cuenta el mismo secreto compartido de 256 bits. La clave que envuelve la FEK se deriva de ese secreto compartido y de un secreto de 32 bytes que el dispositivo A pone en el código QR y nunca envía al servidor; en modo manual, sin código QR, las dos pantallas muestran seis dígitos de verificación derivados del intercambio, y confirmas que coinciden antes de que se envuelva nada. El dispositivo A envuelve entonces la FEK con esa clave mediante AES-GCM y sube la clave envuelta. El dispositivo B la descarga, la desenvuelve y la guarda localmente, envuelta de nuevo con su propia contraseña.

La FEK cruza la red exactamente una vez, y solo en una forma que exige el secreto compartido para abrirse. El comentario enuncia la garantía: «The FEK never leaves a device in cleartext» (la FEK nunca sale de un dispositivo en claro). El servidor retransmite claves públicas y un blob envuelto. Nunca tiene el secreto compartido, así que nunca tiene la FEK.

La sesión de emparejamiento está endurecida: el código de 6 dígitos caduca a los 5 minutos, y el servidor limita la obtención de claves públicas a 3 intentos antes de invalidar por completo la sesión, lo que frena los intentos de adivinar el código por fuerza bruta.

8. Dónde reposa de verdad la FEK

En cada dispositivo, la FEK se guarda envuelta. Está cifrada con una KEK (clave de cifrado de claves) derivada de tu contraseña con PBKDF2-SHA512 a 600 000 iteraciones (muy por encima de las 210 000 que recomienda OWASP para PBKDF2-SHA512) y, por separado, en caché en el llavero del sistema mediante el safeStorage de Electron. Tu contraseña deriva la KEK, la KEK desenvuelve la FEK y la FEK descifra tus archivos. El servidor conserva una copia de la FEK envuelta, para que un dispositivo nuevo pueda iniciar sesión, pero nunca la FEK en claro.

Decisiones de diseño y sus compromisos

Cifrar una vez en el límite de la bóveda, no en el límite de la sincronización. Al convertir los archivos en texto cifrado en disco desde el principio, la sincronización se vuelve una simple tubería y la propiedad zero-knowledge sale gratis. El compromiso es que el servidor no puede, literalmente, ayudarte con nada que dependa del contenido: ni búsqueda en el servidor, ni miniaturas, ni vista previa web. Es una pérdida real de funciones, y es el coste honesto de la garantía.

Fusión en el cliente y no en el servidor. Ejecutar la fusión en el dispositivo mantiene al servidor en la ignorancia, pero hace que la resolución de conflictos dependa de los relojes de los dispositivos y de que los manifiestos se descarguen en el orden correcto. El algoritmo de fusión se apoya en las marcas de tiempo updatedAt y syncedAt, y cuando ambos lados han cambiado desde la última sincronización no elige un ganador en silencio: bifurca, y escribe una copia _conflict_{timestamp} para que no se pierda nada. Más seguro, pero puede dejarte duplicados que reconciliar a mano.

Un proxy de tokens en lugar de prefirmar directamente hacia R2. Hacer pasar los bytes por el Worker añade un salto y algo de latencia frente a una verdadera URL prefirmada directa al almacenamiento. A cambio, el control de cuotas y de acceso sigue centralizado y es sencillo, y como la carga útil ya está cifrada, el salto adicional no ve nada.

Bloques de 4 MB. Los bloques pequeños hacen baratos los reintentos y fácil el paralelismo, a cambio de más objetos y más idas y vueltas en los archivos grandes. Para un espacio de trabajo de notas y documentos, es la inclinación correcta.

Modelo de amenazas: lo que protege y lo que no

Lo que obtiene un atacante que copia los datos del servidor (o una autoridad que solicita al proveedor que se los transmita): un montón de blobs .enc opacos, un manifiesto cifrado que no puede abrir, identificadores de archivo opacos, un contador de versiones y la copia envuelta de tu FEK. Sin tu contraseña o tu frase de recuperación, no puede leer tus archivos, tus notas, tus nombres de archivo ni tu estructura de carpetas. Un atacante que controle el servidor mientras lo usas no puede quitarte la contraseña en el inicio de sesión de una app actualizada: desde el 16 de octubre de 2026, iniciar sesión envía una clave derivada de tu contraseña, no la contraseña, y esa clave no puede desenvolver la FEK. Sin embargo, durante una transición de unos dos meses, una cuenta que todavía no ha vuelto a iniciar sesión, o una versión antigua de la app, sigue enviando la propia contraseña (por HTTPS), y un servidor modificado podría capturarla y desenvolver esa copia.

Lo que ese mismo atacante todavía puede observar (hay que ser honestos): metadatos. En concreto, el número de archivos que tienes, el tamaño de cada blob, el número de bloques, tu uso total de almacenamiento y el momento de tus sincronizaciones. Los tamaños de los archivos y la frecuencia de los cambios pueden revelar patrones. Es la limitación clásica de cualquier almacenamiento cifrado que guarda los tamaños reales de los objetos, y Filarr no rellena ni agrupa para ocultarlos. Si tu modelo de amenazas incluye a un adversario que analiza el tráfico y los tamaños, debes saber que esta capa no está diseñada para frustrarlo.

El momento del emparejamiento merece una advertencia específica. Durante el emparejamiento, el servidor retransmite las dos claves públicas, así que un servidor malicioso podría intentar un ataque de intermediario contra el intercambio ECDH sustituyéndolas por sus propias claves públicas. En la primera versión del protocolo, ese ataque funcionaba: el código de 6 dígitos, que servía de sal para la clave de envoltura, también lo conoce el servidor. La versión actual lo cierra: la clave de envoltura depende además del secreto que lleva el código QR, que ninguna ruta del servidor recibe nunca; el dispositivo A se compromete con su clave pública antes de ver la de B, y en modo manual los seis dígitos de verificación que muestran las dos pantallas delatan una clave sustituida. Vale la pena nombrarlo en lugar de hacer como si no existiera.

Límites conocidos

  • Los metadatos no están ocultos. El tamaño de los blobs, el número de archivos y el momento de las sincronizaciones son visibles para el servidor. El contenido y los nombres, no.
  • El zero-knowledge corta en los dos sentidos. Como el servidor solo tiene tu clave envuelta, no existe ningún restablecimiento de «he olvidado mi contraseña» que recupere tus datos. Si pierdes tu contraseña y tu frase de recuperación de 24 palabras, el texto cifrado es irrecuperable. Es el precio de que el servidor no pueda leerte.
  • Ninguna función en el servidor sobre tu contenido. La búsqueda, las vistas previas y el acceso web al contenido de los archivos no pueden existir en el servidor, por diseño.
  • El abandono del inicio de sesión con contraseña no ha terminado. Desde el 16 de octubre de 2026, una app actualizada inicia sesión con una clave derivada. Durante unos dos meses, una cuenta que todavía no ha vuelto a iniciar sesión, o una versión antigua de la app, sigue enviando la contraseña (que se comprueba contra un hash bcrypt y no se conserva), y un servidor comprometido podría capturarla, como se ha descrito más arriba.
  • Manifiestos antiguos. Los manifiestos antiguos cifrados con una clave local del dispositivo se detectan y migran automáticamente, algo que se gestiona de forma transparente pero que recuerda que el formato ha evolucionado.

Una nota sobre licencias para los curiosos de la técnica: el cliente de escritorio de Filarr se publica bajo BSL 1.1 («source available»), así que la forma en que cifra tus archivos en disco se puede inspeccionar. El código descrito en este artículo, no: sync.ts, syncService.ts y pairingService.ts pertenecen a los módulos en la nube, que no están publicados. Las citas anteriores proceden de ese código privado.

Preguntas frecuentes técnicas

P: ¿Se descifra alguna vez el contenido cifrado en el servidor, aunque sea un instante? No. Los archivos ya son texto cifrado AES-256-GCM en disco antes de que se ejecute la sincronización, y se suben byte a byte en esa forma. El Worker transmite los blobs a R2 sin tener nunca una clave. No hay ningún paso de descifrado en ninguna parte del servidor.

P: ¿Cómo coordina la sincronización un segundo dispositivo si el servidor no puede leer el manifiesto? El manifiesto en la nube está a su vez cifrado con tu FEK. Cada dispositivo lo descarga, lo descifra localmente y ejecuta la fusión completa en el propio dispositivo. El servidor solo impone un contador de versiones entero (bloqueo optimista) para impedir que dos dispositivos se sobrescriban entre sí, algo que puede hacer sin entender el contenido.

P: ¿Cómo llega la clave de cifrado a mi dispositivo nuevo sin pasar en claro por el servidor? O bien iniciando sesión, lo que descarga la copia de la FEK envuelta con tu contraseña, o bien mediante un intercambio ECDH efímero sobre la curva P-256. En el segundo caso, los dos dispositivos derivan un secreto compartido, la FEK se envuelve con AES-GCM con una clave derivada de él y solo se retransmite el blob envuelto. El servidor nunca conoce la clave de envoltura, así que nunca conoce la FEK. El código de emparejamiento caduca a los 5 minutos y está limitado a 3 intentos.

P: ¿Qué ocurre con mis archivos ante una solicitud de una autoridad? Lo que Filarr guarda no permite leerlos. Filarr solo tiene tu clave envuelta, con tu contraseña y tu frase de recuperación. El servidor guarda blobs opacos y un manifiesto cifrado que no puede abrir, así que no hay nada legible que transmitir. El límite es el que se ha nombrado más arriba: durante la transición, un servidor modificado para capturar la contraseña de una cuenta que todavía la envía podría después desenvolver la clave de esa cuenta. La otra cara es que Filarr tampoco puede recuperar tus datos si pierdes a la vez tu contraseña y tu frase de recuperación.

P: ¿Puedo usar mi propio almacenamiento en lugar del de Filarr? Sí. La sincronización admite BYOS (bring your own storage, trae tu propio almacenamiento) con cualquier bucket compatible con S3, así que los blobs cifrados acaban en un almacenamiento que controlas tú, con las mismas propiedades zero-knowledge.

P: ¿Qué metadatos puede seguir deduciendo el servidor sobre mí? El número de archivos, el tamaño de cada blob, el número de bloques, el almacenamiento total usado y las marcas de tiempo de las sincronizaciones. No puede deducir nombres de archivo, nombres de carpeta ni contenido. Si ocultar tamaños y tiempos te importa, ten en cuenta que esta capa no los rellena ni los ofusca.

Si quieres comprobar parte de esto por ti mismo, el código del cliente de escritorio es público (BSL 1.1): muestra cómo se cifran tus archivos en disco. Los tres archivos citados aquí (infra/cloudflare-worker/src/sync.ts para el lado del servidor, electron/sync/syncService.ts para la orquestación en el dispositivo y electron/pairingService.ts para el intercambio de claves) pertenecen a los módulos en la nube privados, no al repositorio público.