Added optionnal online storage and cross-client sync
This commit is contained in:
219
SYNC.md
Normal file
219
SYNC.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# 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** :
|
||||
|
||||
- Préparer le coffre, télécharger `pardis-secours.json`, le conserver à part,
|
||||
puis 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. Quitter ou masquer l’écran l’annule ; en cas de fermeture
|
||||
brutale, le bail serveur expire sous 25 secondes.
|
||||
- Saisir les huit caractères sur le nouvel appareil ou scanner le QR avec l’appareil
|
||||
photo du téléphone. 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.
|
||||
- Comparer **toute** l’empreinte de huit groupes hexadécimaux sur les deux écrans,
|
||||
cocher leur égalité et confirmer sur l’appareil existant. Les demandes ne sont
|
||||
ni surveillées ni affichées en dehors de l’écran dédié.
|
||||
- Les données déjà présentes sur le nouvel appareil sont conservées. Les journées
|
||||
distinctes sont réunies et les dates communes divergentes deviennent des conflits.
|
||||
Les réglages déjà enregistrés peuvent eux aussi nécessiter un choix.
|
||||
|
||||
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 128 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.
|
||||
Reference in New Issue
Block a user