Tes fichiers sur tous tes appareils, sans que le serveur puisse les lire : comment marche la sync zero-knowledge de Filarr
Comment Filarr synchronise tes fichiers sur tous tes appareils sans que le serveur puisse les lire : FEK, blobs chiffrés, manifest chiffré et transfert de cle ECDH expliques.
Mathis Belouar-Pruvot
Tu bosses sur ton laptop le matin, tu reprends sur ta tour le soir, et tout est là : tes notes, tes dossiers, la structure exacte de ton workspace. C'est le boulot de base d'une sync cloud, et Filarr le fait. La différence, c'est ce que le serveur voit pendant ce trajet : rien de lisible. Ni le contenu de tes fichiers, ni leurs noms, ni le texte de tes notes. Juste des blocs de données chiffrés qu'il stocke et redonne sans jamais pouvoir les ouvrir.
Cet article explique comment, concrètement, la sync optionnelle de Filarr reste "zero-knowledge" : ce qui transite, ce que le backend peut observer, comment tes appareils s'échangent la clé sans jamais la confier au serveur, et où sont les limites honnêtes de ce modèle. C'est un deep-dive orienté implémentation, avec les fichiers et les paramètres réels du code.
Quick answer. La sync cloud de Filarr est optionnelle et zero-knowledge : tes fichiers sont déjà chiffrés sur ton disque en AES-256-GCM avec une clé (la FEK) qui ne quitte jamais tes appareils en clair, et ils sont envoyés tels quels vers le stockage Cloudflare R2. Le serveur ne reçoit que des blobs chiffrés opaques, découpés en chunks de 4 Mo, rangés sous des identifiants dérivés d'un hash SHA-256 (les vrais noms de fichiers ne partent jamais). Même l'index qui décrit ton workspace (le manifest) est chiffré avec la FEK avant l'upload. Pour qu'un deuxième appareil puisse déchiffrer, la FEK est transférée d'appareil à appareil via un échange de clés ECDH (courbe P-256) : le serveur relaie une version chiffrée de la FEK qu'il est mathématiquement incapable d'ouvrir.
Le problème : synchroniser sans faire confiance au serveur
La sync classique repose sur un pacte implicite : tu confies tes données à un serveur, il te les redistribue sur tes appareils, et tu lui fais confiance pour ne pas les lire. Notion stocke tes notes en clair sur ses serveurs. La plupart des services de sync "chiffrés" chiffrent en transit (TLS) et parfois au repos, mais avec une clé que le fournisseur détient : il peut techniquement tout déchiffrer, et un incident, une réquisition ou un employé malveillant suffit à exposer tout le contenu.
Le modèle zero-knowledge inverse ce pacte. Le serveur doit rester utile (stocker, versionner, redistribuer) tout en étant structurellement incapable de lire ce qu'il stocke. Ça pose trois questions concrètes que le code de Filarr doit résoudre :
- Comment envoyer un fichier sans que le serveur puisse en voir le contenu ni le nom ?
- Comment décrire l'organisation du workspace (arborescence, métadonnées) sans révéler cette structure au serveur ?
- Comment donner la clé de déchiffrement à un deuxième appareil sans jamais la donner au serveur qui sert d'intermédiaire ?
Filarr répond aux trois. Voyons comment, étape par étape.
Comment ça marche, étape par étape
1. Tout est déjà chiffré avant de penser à la sync
Le point de départ, c'est que la sync n'a rien à chiffrer : le disque local l'est déjà. Filarr repose sur une clé unique, la FEK (File Encryption Key), une clé AES-256 générée aléatoirement à la première connexion (crypto.subtle.generateKey dans hybridCrypto.ts). Chaque fichier stocké localement est chiffré en AES-256-GCM avec un IV aléatoire de 12 octets, au format IV(12) || ciphertext || tag(16).
Cette FEK n'est jamais stockée en clair sur le disque. Elle est "wrappée" (chiffrée) par une KEK (Key Encryption Key) dérivée de ton mot de passe de vault via PBKDF2-SHA-512, 600 000 itérations (le seuil recommandé par l'OWASP en 2024), Argon2id étant proposé en option. Une copie indépendante de la FEK est aussi wrappée par une clé dérivée de ta phrase de récupération de 24 mots (BIP-39), pour le cas où tu perds ton mot de passe.
Conséquence directe pour la sync : quand un fichier part vers le cloud, il est déjà un blob chiffré. Le module de sync (syncService.ts) le lit sur le disque et l'envoie tel quel, sans étape de chiffrement supplémentaire côté réseau. Le commentaire dans le code est explicite : "Files on disk are already encrypted with FEK, uploaded as-is."
2. Les noms de fichiers ne partent jamais
Un point souvent oublié dans le zero-knowledge : le contenu peut être chiffré mais les noms de fichiers, eux, fuitent des informations ("impots_2025.pdf", "lettre_demission.md"). Filarr évite ça en n'utilisant jamais le vrai nom côté serveur.
Quand un fichier change, syncService.ts calcule un identifiant opaque : sha256(chemin local) tronqué à 32 caractères hexadécimaux. Le commentaire du code le dit noir sur blanc : "Derive opaque fileId from local path, never expose real file names." C'est cet identifiant, pas le nom, qui sert de clé de rangement.
Côté Worker Cloudflare (sync.ts), les chunks sont rangés sous une arborescence purement technique :
users/{userId}/profiles/{profileId}/files/{fileId}/chunk_0.enc
Le serveur voit donc un utilisateur, un profil (un UUID), un identifiant de fichier haché et un numéro de chunk. Il ne voit ni "impots_2025.pdf", ni le dossier "Comptabilité". Les fichiers de plus de 4 Mo sont découpés en chunks de 4 Mo (constante CHUNK_SIZE), chacun uploadé séparément avec l'extension .enc.
3. Le manifest : décrire le workspace sans le révéler
Si le serveur ne connaît que des identifiants hachés, comment un deuxième appareil reconstruit-il l'arborescence, retrouve-t-il quel chunk correspond à quel fichier, dans quel dossier ? Grâce au manifest : un index JSON qui liste les fichiers, leurs chunks, leurs checksums SHA-256, leurs chemins locaux, plus les métadonnées de profil.
Ce manifest est évidemment sensible, puisqu'il contient les vrais chemins et la structure. Il est donc chiffré avant de partir, avec la même FEK, via encryptWithFEK dans storageService.ts. Le format est un marqueur fek: suivi de IV(12) || ciphertext+tag en AES-256-GCM. Le Worker le stocke sous users/{userId}/profiles/{profileId}/manifest.enc et le renvoie en base64 sans jamais l'ouvrir. Là encore, le serveur ne manipule qu'un blob opaque.
Le manifest chiffré transporte aussi la clé de chiffrement des métadonnées de StorageService, ce qui permet à un nouvel appareil, une fois qu'il a la FEK, de déchiffrer non seulement les fichiers mais aussi les metadata.json de chaque dossier et le notes.enc à la racine. Tout ça est imbriqué à l'intérieur du blob protégé par la FEK.
4. Le trajet réseau : un proxy à jetons, jamais de clé en clair
Filarr n'expose pas R2 directement au client. Le flux d'upload (syncR2Client.ts côté Electron, sync.ts côté Worker) suit un motif proxy à deux temps :
- L'appareil fait un appel authentifié à
/sync/presign/uploaden donnantprofileId,fileId,chunkIndexet la taille. Le Worker vérifie le quota de stockage, génère un jeton aléatoire (crypto.randomUUID), stocke l'intention d'upload (userId, clé R2, taille) dans le KV avec une durée de vie de 15 minutes, et renvoie une URL contenant ce jeton. - L'appareil envoie le blob chiffré en PUT sur
/sync/upload/{token}. Le Worker relit l'intention depuis le KV, écrit le blob dans R2, met à jour le compteur de stockage en D1, puis supprime le jeton.
Le download est symétrique avec un jeton à usage unique. À aucun moment le client ne transmet une clé de chiffrement au serveur : ce qui circule, ce sont des octets déjà chiffrés et des jetons de coordination qui ne disent rien du contenu. Le commentaire d'en-tête du Worker résume l'invariant : "All file content is opaque encrypted blobs, zero knowledge."
5. Cohérence multi-appareils : verrouillage optimiste
Quand deux appareils synchronisent le même profil, il faut éviter qu'ils s'écrasent mutuellement le manifest. Filarr utilise un verrouillage optimiste basé sur un numéro de version stocké en D1 (manifest_version). Pour uploader un nouveau manifest, l'appareil annonce la version qu'il pense être à jour ; si le serveur a une version plus récente, il répond par un conflit 409 et l'appareil re-fusionne au cycle suivant. Ce mécanisme ne demande au serveur de comprendre que des entiers de version, jamais le contenu du manifest.
Les conflits de fichiers, eux, sont résolus côté client par une stratégie de fork : la version locale est copiée avec un suffixe _conflict_{timestamp} et l'utilisateur tranche. Le serveur n'arbitre rien, parce qu'il ne peut rien lire.
6. Le moment critique : donner la clé à un deuxième appareil
C'est ici que le zero-knowledge se joue vraiment. Ton laptop a la FEK. Ta tour ne l'a pas. Pour que la sync ait un sens, il faut transférer la FEK. Et il ne faut surtout pas la confier au serveur, sinon tout l'édifice s'effondre.
Filarr utilise un échange de clés Diffie-Hellman sur courbe elliptique (ECDH, courbe P-256), implémenté dans pairingService.ts. Le déroulé :
- L'appareil A (celui qui a la FEK) génère une paire de clés ECDH éphémère et un code de pairage à 6 chiffres. Il publie sa clé publique sur le serveur, indexée par le code.
- L'appareil B saisit le code, récupère la clé publique de A, génère sa propre paire ECDH, publie sa clé publique et calcule un secret partagé de 256 bits via
deriveBits. - L'appareil A récupère la clé publique de B et calcule le même secret partagé de son côté. C'est la magie de Diffie-Hellman : les deux appareils arrivent au même secret sans jamais l'avoir transmis.
- A chiffre (wrappe) la FEK avec ce secret partagé en AES-256-GCM et uploade le résultat.
- B télécharge la FEK wrappée et la déchiffre avec le secret qu'il a lui-même calculé.
Le serveur relaie deux clés publiques et un blob de FEK chiffrée. Il ne possède aucune des deux clés privées ECDH (elles restent en mémoire sur chaque appareil), donc il ne peut pas reconstituer le secret partagé, donc il ne peut pas déballer la FEK. Le commentaire du service est catégorique : "The FEK never leaves a device in cleartext."
Le pairage est durci : le code à 6 chiffres expire après 5 minutes, et la récupération de la clé publique est limitée à 3 tentatives par code (rateLimit dans le Worker), au-delà la session est invalidée. Ça réduit la fenêtre pour deviner un code par force brute.
Une fois la FEK reçue, l'appareil B la re-wrappe localement avec son propre mot de passe de vault et la stocke, puis déclenche une première sync qui télécharge les manifests, reconstruit les profils et rapatrie les fichiers.
Choix de conception et trade-offs (honnêtes)
Le serveur proxifie au lieu d'utiliser de vraies URL présignées S3. Le code note que le binding R2 des Workers ne supporte pas nativement les URL présignées, d'où l'approche par jeton où le Worker relaie l'octet-stream. Trade-off : le Worker "touche" les octets au passage. Mais comme ces octets sont déjà du ciphertext, ça ne casse pas le modèle zero-knowledge ; ça ajoute juste un saut réseau et un peu de charge sur le Worker.
Le fileId est un hash déterministe du chemin. Avantage : pas de table de correspondance à maintenir, et le même fichier produit toujours le même identifiant, ce qui simplifie la déduplication et le suivi. Trade-off : le serveur voit un identifiant stable par fichier, donc il peut observer qu'un "certain fichier" est modifié souvent, même sans jamais savoir lequel. C'est un compromis assumé entre simplicité et minimisation absolue des métadonnées.
Une seule FEK par workspace, pas une clé rotative par version. Ça rend le changement de mot de passe instantané (on ne re-chiffre que la FEK, jamais les fichiers, cf. rewrapFEK) et le pairage simple. Trade-off : si la FEK était compromise, elle donnerait accès à tout le contenu du profil. La défense repose donc entièrement sur le fait que la FEK ne sort jamais en clair et n'est jamais stockée côté serveur.
Zero-knowledge veut dire zéro filet de sécurité côté serveur. Puisque le serveur ne détient aucune clé, il ne peut pas t'aider à récupérer tes données si tu perds à la fois ton mot de passe et ta phrase de 24 mots. C'est le prix direct du modèle. Filarr le compense avec la phrase de récupération BIP-39, mais c'est à toi de la garder.
Threat model : ce que ça protège, et contre quoi
Le modèle protège efficacement contre :
- Une compromission du stockage cloud. Si quelqu'un exfiltre le bucket R2, il obtient des chunks
.encet desmanifest.encinexploitables sans la FEK, qui n'y est pas. - Un accès interne côté fournisseur. Un employé de la plateforme, une réquisition légale ou une erreur de configuration exposent au pire des blobs chiffrés et des métadonnées techniques (tailles, nombre de chunks, horodatages).
- Une interception réseau. Le trafic passe par TLS, et même sans TLS le contenu serait déjà chiffré au repos par la FEK.
- Le relais de la clé pendant le pairage. Le serveur intermédiaire ne peut pas déballer la FEK grâce à l'ECDH.
Le modèle ne protège pas contre :
- Un appareil compromis. Si un attaquant contrôle ta machine pendant que le vault est déverrouillé, la FEK est en mémoire et il peut lire en clair. Le zero-knowledge protège les données en transit et au repos côté serveur, pas un endpoint déjà infecté.
- Un mot de passe de vault faible. La KEK est dérivée de ton mot de passe ; PBKDF2-SHA-512 à 600 000 itérations ralentit fortement une attaque par force brute, mais un mot de passe trivial reste un mot de passe trivial.
- L'analyse de métadonnées. Le serveur voit combien de fichiers tu as, leur taille approximative (au chunk près), et quand tu synchronises. Il ne voit pas ce qu'ils sont.
- Un code de pairage intercepté en temps réel. Le canal de pairage est durci (expiration, limite de 3 tentatives), mais le modèle suppose que tu transfères le code à 6 chiffres vers ton propre appareil dans la fenêtre de 5 minutes.
Limites connues
- Métadonnées visibles. Taille des chunks, nombre de fichiers, fréquence de sync et usage de stockage sont observables côté serveur. C'est inhérent à un service de sync qui doit gérer des quotas et du versioning.
- Le proxy Worker voit passer le ciphertext. Choix pragmatique lié aux limites du binding R2, sans impact sur la confidentialité du contenu, mais à connaître.
- Pas de récupération assistée. Perte du mot de passe et de la phrase de 24 mots égale perte définitive. Le revers direct de l'absence de clé côté serveur.
- Une seule FEK par profil. Pas de rotation de clé par fichier ni de révocation granulaire par appareil dans le flux de sync actuel.
- Écosystème jeune. Filarr est une application de 2026, avec un mobile en cours et une communauté plus petite que celle d'un Obsidian. Le modèle crypto est solide et le desktop est open source (BSL 1.1), ce qui permet de l'auditer, mais c'est un logiciel jeune : la meilleure garantie reste la vérifiabilité du code, pas l'ancienneté.
FAQ technique
Le serveur peut-il déchiffrer mes fichiers s'il est contraint légalement de le faire ? Non. Le serveur ne détient aucune clé de déchiffrement : ni la FEK, ni la KEK, ni les clés privées ECDH. Il ne peut remettre que des blobs chiffrés et des métadonnées techniques. C'est une impossibilité structurelle, pas une politique de confidentialité.
Où est stockée la FEK, exactement ? En clair, uniquement en mémoire de l'appareil quand le vault est déverrouillé. Sur le disque, elle existe seulement sous forme wrappée : chiffrée par une KEK dérivée du mot de passe (PBKDF2-SHA-512, 600k itérations), et dans une copie de secours wrappée par la phrase BIP-39. La version wrappée par le mot de passe n'est pas uploadée sur le cloud ; le seul moment où une FEK wrappée transite par les serveurs, c'est pendant le pairage, et elle y est chiffrée par un secret ECDH éphémère que le serveur ne connaît pas.
Comment un deuxième appareil obtient-il la clé sans passer par le serveur en clair ? Via un échange ECDH sur courbe P-256. Les deux appareils calculent chacun de leur côté le même secret partagé de 256 bits à partir de la clé publique de l'autre et de leur propre clé privée. La FEK est chiffrée avec ce secret avant d'être relayée. Le serveur ne voit que des clés publiques et un blob chiffré.
Les noms de mes fichiers et dossiers fuient-ils vers le cloud ?
Non. Les fichiers sont rangés sous un identifiant sha256(chemin) tronqué à 32 caractères hex, et les vrais chemins ne vivent que dans le manifest, lui-même chiffré avec la FEK avant tout upload. Le serveur voit des UUID de profil et des identifiants hachés, pas des noms lisibles.
La sync est-elle obligatoire ? Non. Filarr est local-first : gratuit pour toujours en usage local, 100% hors-ligne, et la sync cloud est une option payante à partir de 4 euros par mois. Le cloud "ne fait que suivre" ; il n'est jamais le produit.
Puis-je utiliser mon propre stockage plutôt que celui de Filarr ? Le backend de sync repose sur Cloudflare R2, et le modèle est compatible avec un bucket S3-compatible que tu fournis (BYOS). Comme tout ce qui est envoyé est déjà chiffré côté client, changer de destination de stockage ne change rien au fait que le contenu reste opaque pour l'hébergeur.
En quoi c'est différent de la sync chiffrée d'Obsidian ou d'un cloud classique ? La différence tient à qui détient la clé. Dans un modèle zero-knowledge, la clé ne quitte jamais tes appareils et le serveur est incapable de lire quoi que ce soit, y compris l'arborescence. Filarr combine ça avec un chiffrement par fichier natif et un transfert de clé par ECDH, là où beaucoup de solutions chiffrent avec une clé que le fournisseur peut, en théorie, utiliser.