# Synchronisation chiffrée La synchronisation est facultative, désactivée après la migration de la base locale. Sans activation, aucune requête n’est envoyée à l’API de synchronisation. Une écriture de journée reste locale et transactionnelle, même sans réseau. ## Parcours Dans **Données et sauvegardes → Synchronisation et appareils** : - Choisir **Créer mon coffre**. L’application prépare alors le coffre et son document de secours, puis amène directement à l’étape obligatoire de sauvegarde. Télécharger `pardis-secours.json`, le conserver à part et confirmer l’activation. Le document donne accès au coffre : il doit être protégé comme un mot de passe de gestionnaire de secrets. - Ajouter un appareil depuis ce même écran. L’invitation expire après cinq minutes, n’accepte qu’un candidat et doit être renouvelée par l’écran visible toutes les quelques secondes. En quittant l’écran ou en passant à l’appareil photo, les battements s’arrêtent et le bail serveur expire sous 25 secondes si l’invitation n’est pas reprise. - Saisir les huit caractères sur le nouvel appareil ou scanner le QR avec la caméra d’un appareil compatible. Le QR ouvre l’application web ; le navigateur/profil qui reçoit ce lien doit être celui dans lequel on veut utiliser Pardis. Les copies installées et les navigateurs peuvent avoir des stockages distincts, notamment sur iOS ; le code manuel permet d’associer directement la bonne copie. - Le nouvel appareil peut scanner le QR de l’appareil existant. Si l’appareil existant est un smartphone, il peut aussi scanner le QR de la demande affiché par le nouvel appareil. Dans les deux cas, la confirmation finale se fait sur l’appareil existant. Les QR contiennent un lien vers l’écran d’association, que la caméra native du téléphone peut ouvrir ; les navigateurs avec lecteur caméra intégré peuvent aussi les lire directement. La saisie manuelle reste disponible. - Vérifier l’empreinte de cinq groupes hexadécimaux, ou faire vérifier les deux empreintes par QR, puis confirmer sur l’appareil existant. Les demandes ne sont ni surveillées ni affichées en dehors de l’écran dédié. - Après confirmation, choisir de conserver tout l’état du coffre, tout l’état de l’appareil courant, ou d’ouvrir la fusion détaillée. Le premier choix remplace les journées locales par celles du coffre. Le second fait de l’état courant la nouvelle version partagée. La fusion détaillée réunit les dates distinctes et permet de départager ensuite les dates ou réglages divergents. Les anciennes révisions chiffrées restent dans l’historique du coffre. Les automatismes ont un responsable. Un autre appareil propose discrètement de prendre le rôle ; ce transfert exige le réseau et un numéro de rôle encore actuel. L’ancien responsable hors ligne ne peut pas apprendre immédiatement le transfert : ses écritures restent conservées et peuvent produire un conflit à la reconnexion. La saisie manuelle fonctionne sur chaque appareil. Les conflits suspendent les transitions automatiques locales jusqu’à leur résolution. La récupération avec le document de secours retire les anciens appareils, renouvelle la clé de données et désigne l’appareil récupéré comme responsable. Elle fonctionne aussi quand la limite d’appareils est atteinte. Les anciens appareils conservés peuvent ensuite être associés à nouveau. Mettre la synchronisation en pause conserve les clés et les changements à envoyer. Déconnecter un appareil retire ses clés locales et propose séparément d’effacer ses données. Pour couper aussi son autorisation serveur, le retirer depuis un autre appareil. Supprimer le coffre supprime les données et les autorisations serveur ; aucun mécanisme ne peut effacer les copies déjà téléchargées à distance. ## Modèle et garanties La base Dexie passe de v2 à v3 sans conversion des journées. Les tables de synchronisation sont distinctes de `settings`, donc un import en remplacement ne peut pas effacer les clés. Un middleware écrit un marqueur durable dans la même transaction IndexedDB que toute modification métier. Les données locales et leur dernier instantané capturé permettent ensuite de matérialiser les révisions dans la file d’envoi, sans fenêtre où une saisie validée serait perdue après un crash. Chaque journée avec ses postes forme un objet indivisible. Chaque réglage métier forme un objet ; thème et suivi des sauvegardes restent locaux. Les ID numériques Dexie des postes ne traversent pas le réseau. Un identifiant HMAC masque la date ou le nom du réglage au serveur. Les révisions forment un graphe de parents, indépendant des horloges des appareils. Plusieurs têtes différentes déclenchent un conflit ; la résolution crée une révision qui référence toutes les têtes. Des têtes au contenu identique sont réunies sans demander un choix. Aucun tri par heure ne choisit un « gagnant ». Les changements faits pendant une requête sont capturés avant l’installation de sa réponse. Valeurs reçues, parents, signatures et cohérence des journées actives sont vérifiés côté client ; l’API ne peut pas valider le texte chiffré. La purge produit des tombstones : un ancien appareil ne peut pas ressusciter une journée en renvoyant une ancienne révision. Un conflit suppression/modification conserve les deux versions. **La purge retire les journées de l’historique courant, mais les anciennes révisions chiffrées sont conservées jusqu’à la suppression du coffre.** Cette première version ne compacte pas les révisions ; elles comptent dans le quota de 50 Mio. Les sauvegardes de l’hébergeur ont une rétention distincte. La sauvegarde locale avant purge et les exports indépendants restent disponibles. Les exports JSON classiques sont en clair et ne contiennent pas les clés de sync. La synchronisation fonctionne à l’ouverture, au retour au premier plan, au retour du réseau, et toutes les 15 secondes tant que l’app est visible. La PWA fermée ou suspendue n’offre aucune garantie d’exécution ni de synchronisation en arrière-plan. Une requête échouée laisse les révisions en attente et n’empêche pas la saisie. Web Locks sérialise les synchronisations entre onglets ; IDs idempotents et transactions protègent aussi les nouvelles tentatives. Le curseur n’avance qu’avec la validation et l’installation transactionnelle de la page reçue. ## Cryptographie et limites de confiance - Données : AES-256-GCM via WebCrypto, IV aléatoire de 96 bits à chaque chiffrement, en-tête de révision (coffre, objet, parents, époque et auteur) authentifié en AAD. - Requêtes et révisions : ECDSA P-256/SHA-256. Chaque requête signe méthode, chemin avec query, empreinte du corps, heure et nonce. Tolérance d’horloge de deux minutes, nonces conservés quatre minutes en SQLite pour refuser les rejeux après redémarrage. - Enveloppes de clés : HPKE RFC 9180 (`@hpke/core`), DHKEM P-256/HKDF-SHA-256 et AES-256-GCM. Contexte lié au coffre et au destinataire. Le mode de base HPKE assure la confidentialité ; l’authentification de l’association vient de la comparaison de l’empreinte liant invitation et clés des deux participants. Le QR épingle en plus l’identité de l’appareil parrain. Le code court ne dérive aucune clé. - L’empreinte humaine a 80 bits. Elle n’est pas remplacée par un petit code décimal qui, sans protocole d’engagement adapté, serait vulnérable à une recherche hors ligne. - Chaque appareil a sa clé de signature et sa clé de réception. Le document de secours possède une identité distincte et une clé privée de déchiffrement HPKE. **Les appareils liés ne reçoivent que ses clés publiques.** La copie temporaire du document préparé est supprimée de la base lors de la confirmation d’activation. - La révocation renouvelle l’époque et la clé de données, enveloppe le trousseau pour les appareils restants et le secours, et refuse les écritures d’une ancienne époque. Un appareil encore autorisé peut reprendre ses changements hors ligne en les chiffrant sous la nouvelle clé. Un appareil retiré garde ses données déjà lues. S’il détenait aussi le document de secours externe, il faut renouveler ce document. - Le renouvellement du secours n’affiche pas l’ancien secret : il produit un nouveau document, à conserver avant confirmation, puis remplace atomiquement l’identité de secours et les enveloppes serveur. L’ancien document ne permet plus de récupérer le trousseau courant. Les anciennes copies chiffrées restent déchiffrables par qui possédait déjà leurs clés. Les noms des appareils sont aussi chiffrés. Le serveur connaît les IP, identifiants, clés publiques, relations entre appareils/révisions, tailles et dates de connexion. Le chiffrement ne masque pas ces métadonnées et ne garantit pas la disponibilité contre un serveur qui supprime ou retient des messages. Le client détecte une substitution de contenu et un retour à un trousseau déjà dépassé ; il ne garantit pas la détection de toute omission ou divergence présentée à un nouveau client. L’ouverture locale reste immédiate : données et clés sont accessibles au code de l’origine et à un utilisateur du navigateur déverrouillé. Un navigateur compromis ou du JavaScript malveillant servi par l’hébergeur peuvent lire les clés. HTTPS, CSP et dépendances empaquetées limitent l’exposition sans supprimer cette limite d’une application web. Pas de tiers chargé à l’exécution, pas d’analytique. ## API et exploitation Node.js 24 minimum, SQLite embarquée, WAL et transactions. En développement : ```sh npm run sync:server # dans un autre terminal npm run dev ``` Vite et son serveur de prévisualisation transmettent `/api/sync` au port local 3001. `SYNC_DB` définit le chemin SQLite ; par défaut `./sync-data/pardis.sqlite` (ignoré par Git et les images Docker). `SYNC_HOST` vaut `127.0.0.1` hors conteneur. Le compose contient Caddy et le service `sync`, dont le port reste privé. Le volume nommé `sync-data` est persistant. Caddy écrase `X-Pardis-Client-IP` avec l’adresse client validée ; ne pas exposer directement l’API avec `SYNC_TRUST_PROXY=1`. `SYNC_ORIGIN` protège l’origine des requêtes navigateur. HTTPS est nécessaire. ```sh docker compose --env-file .env.example config --quiet # Après configuration du domaine et des variables de déploiement : docker compose up -d --build ``` API JSON versionnée dans les enveloppes (`v: 1`), authentifiée par `X-Pardis-Proof` (JSON encodé base64url). Aucun cookie ou mot de passe utilisateur. | Routes sous `/api/sync` | Usage | | --- | --- | | `POST /vaults` | Créer un coffre et son premier appareil | | `GET /state` | Appareils, trousseau enveloppé et responsable | | `POST /invitations`, `POST /join` | Invitation et candidature | | `POST /invitations/:code/heartbeat`, `/approve` | Présence de l’écran et autorisation | | `GET /invitations/:code/result`, `DELETE /invitations/:code` | Remise des clés ou annulation | | `GET /revisions?after=…`, `POST /revisions` | Pages de 100 révisions et envoi idempotent | | `POST /role`, `POST /revoke` | Transfert du rôle et révocation avec rotation | | `GET /recovery`, `POST /recover`, `POST /recovery-reset` | Récupération et remplacement du secours | | `DELETE /vault`, `GET /health` | Suppression du coffre et disponibilité | Limites : 10 appareils actifs/coffre, 100 000 révisions/coffre, 50 Mio de révisions par défaut (`SYNC_MAX_BYTES`), 10 000 coffres (`SYNC_MAX_VAULTS`), requêtes de 2 Mio maximum et contenu chiffré d’une révision inférieur à 200 000 caractères. Un quota atteint bloque les envois, conserve la saisie locale et expose une erreur explicite. Les limites par minute sont 12 candidatures/IP, 5 créations/IP, 10 invitations/appareil, 600 requêtes/IP. Elles sont en mémoire et repartent de zéro au redémarrage ; les nonces anti-rejeu sont persistants. Le service n’écrit pas de contenu métier ni de secrets dans les logs. Le healthcheck contrôle l’HTTP ; superviser aussi l’espace volume, la disponibilité et le succès des sauvegardes. ### Sauvegarde et restauration serveur Sauvegarder avec l’API SQLite, qui inclut les transactions WAL ; ne pas copier uniquement le fichier principal pendant que le serveur tourne. ```sh docker compose exec sync node server/backup.js /data/backup.sqlite docker compose cp sync:/data/backup.sqlite ./pardis-server-backup.sqlite ``` Déplacer la copie hors du serveur, sous contrôle d’accès. Définir explicitement la rétention des sauvegardes chez l’hébergeur et expliquer que les suppressions logiques peuvent y subsister pendant cette période. Un backup contient aussi les anciennes autorisations et identités de secours ; le chiffrement n’annule pas ce fait. Pour restaurer : arrêter `sync`, conserver une copie du volume courant, remplacer la base avec une sauvegarde cohérente et retirer les anciens fichiers `-wal`/`-shm` associés à la base remplacée, puis redémarrer. **Ne pas remettre en production une sauvegarde ancienne sans contrôler les révocations/rotations intervenues depuis.** Une restauration ancienne peut réactiver des autorisations retirées. Les clients ayant vu de nouvelles clés refusent un trousseau plus ancien. Un retour arrière nécessite une procédure de récupération coordonnée ; ce n’est pas une fusion transparente. Tester régulièrement la restauration sur une instance isolée. ## Validation `npm run validate` exécute les tests métier, cryptographiques et API, la vérification Svelte et le build. `npm run test:sync` cible la synchronisation. L’API de test utilise une base temporaire, jamais le volume du service. `npm run test:e2e` démarre aussi un serveur de sync en mémoire pour les contextes navigateur isolés. Les tests couvrent migration v2, journal et rollback, conflits, tombstones, suppression concurrente, journées actives, données modifiées pendant une requête, chiffrement falsifié, mauvais destinataire HPKE, anti-rejeu, séparation des coffres, expiration/annulation, approbation exacte, reprise idempotente, récupération, révocation, rotation et redémarrage SQLite. Les parcours navigateur couvrent les empreintes sur deux appareils, l’absence d’alerte hors écran, les modifications hors ligne, la résolution, le rôle, la révocation et la récupération. La validation automatisée navigateur utilise Chromium. Vérifier aussi les parcours sur de vrais appareils Safari/iOS et Android avant ouverture large du service ; une revue cryptographique indépendante reste souhaitable avant de promettre une protection contre un hébergeur activement malveillant.