232 lines
15 KiB
Markdown
232 lines
15 KiB
Markdown
# 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.
|