Aller au contenu
Second Brain 3.0 Ouvrir l’application
Protocole publié

Synchronisation chiffrée de Second Brain — protocole, version 1

Le document de référence de la synchronisation de Second Brain 3.0.0, tel qu’il guide le code et ses vérifications automatiques.

Ce document décrit comment Second Brain synchronise les données d’un utilisateur entre ses appareils, par son propre serveur, sans que ce serveur puisse les lire ni les falsifier. Il sert de référence au code (app/index.html, modules Chiffre, Phrase, Fusion, Synchro ; serveur/secondbrain.php), aux vérifications automatiques, et à toute personne qui veut contrôler ce que fait le logiciel.

Mots-clés : doit (obligatoire), devrait (recommandé), peut (permis).


1. Principes

  1. Local d’abord. Chaque appareil garde toutes ses données et fonctionne hors ligne. Le serveur relie les appareils ; s’il disparaît, rien n’est perdu.
  2. Chiffré de bout en bout. Le serveur ne reçoit que des paquets chiffrés, sous des identifiants opaques. Aucune clé ne lui est confiée.
  3. Aucune perte silencieuse. Une modification n’en écrase jamais une autre en silence :
    • deux modifications de champs différents sont réunies ;
    • pour un même champ modifié des deux côtés, la version écartée entre dans l’historique, et l’utilisateur est prévenu.
  4. L’ordre vient du serveur. L’ordre des changements est fixé par un numéro du serveur, jamais par l’horloge des appareils.
  5. Rien n’est perdu ni dégradé en route. Un paquet que l’appareil ne sait pas lire est mis de côté, pas ignoré. Un enregistrement reçu est rangé tel quel. Un serveur revenu en arrière est détecté.
  6. Un serveur ordinaire. Un seul fichier PHP (8.1 ou plus) avec SQLite, et de quoi vérifier une signature Ed25519 (extension sodium, OpenSSL de PHP 8.4, ou extension GMP), sur un hébergement mutualisé, sans tâche planifiée ni connexion permanente.

Critère « aucun écart, aucune perte » : une fois les appareils synchronisés et sans modification en cours, tous ont le même contenu ; et chaque version écrite par un appareil est soit la version courante, soit dans l’historique du mémo, soit dans les versions gardées par le serveur.


2. Ce qui est synchronisé

SynchroniséNon synchronisé
Les mémos, corbeille et historique des versions comprisLes réglages propres à chaque appareil (thème, dictée, sauvegardes…)
Les collections et les Brain MapsLes modèles de dictée (Whisper)
Les fichiers des mémos : images, audio, vidéo, PDF, vignettesLa configuration de la synchronisation (clés, jeton)
Les suppressions définitivesLes sauvegardes locales et les archives
Les réglages communs du coffre : durée de la corbeille

3. Vocabulaire

TermeSens
CoffreL’ensemble des données d’un utilisateur sur le serveur, et les appareils autorisés. Un serveur porte un seul coffre.
AppareilUne installation de Second Brain (un navigateur, une application de bureau) reliée au coffre.
ÉlémentUn enregistrement synchronisé, de type note (mémo), collection, map (Brain Map) ou communs (réglages communs du coffre).
FichierUn contenu binaire (image, audio, vidéo, PDF, vignette), référencé par un mémo.
Numéro (seq)Compteur du coffre sur le serveur, augmenté de 1 à chaque écriture d’élément.
BasePour un élément, la dernière version du serveur sur laquelle l’appareil s’est aligné : son numéro, son rev et son contenu.
TraceVersion d’un élément qui dit « supprimé définitivement ».
GénérationIdentifiant aléatoire de l’état du serveur. Il change quand un appareil constate que le serveur est revenu en arrière (section 7.7).
InstantanéÉtat du coffre à un numéro donné, conservé selon une règle de rétention.

4. Clés et cryptographie

Toutes les opérations utilisent WebCrypto : HKDF-SHA-256, HMAC-SHA-256, AES-256-GCM, SHA-256. Les octets aléatoires viennent de crypto.getRandomValues.

4.1 Les secrets

SecretTailleOù il vitRôle
K, clé de données32 octets aléatoiresSur chaque appareil du coffreTout le contenu en dérive. Jamais transmise en clair.
R, secret de récupération32 octets aléatoiresSeulement dans la phrase de 24 mots, chez l’utilisateurRejoindre le coffre sans autre appareil ; autoriser les opérations sensibles.
P, secret d’appairage16 octets aléatoiresDans un QR code, un lien ou un code, 15 minutes au plusRejoindre le coffre depuis un appareil existant.
Jeton d’appareil32 octets aléatoiresSur l’appareil ; le serveur n’en garde que l’empreinte SHA-256Authentifier l’appareil auprès du serveur. Révocable.

K et R sont indépendantes. Un appareil ne garde ni R ni rien qui en dérive. Conséquences :

  • un appareil volé puis retiré du coffre ne peut ni s’y réinscrire, ni accomplir une opération sensible ; sa révocation annule aussi un changement de phrase qu’il aurait demandé (section 6.6) ;
  • changer de phrase ne change pas K : aucune donnée n’est à rechiffrer. En contrepartie, une ancienne phrase divulguée permettait d’obtenir K : changer de phrase ne protège pas ce qui a déjà été lu ;
  • un appareil retiré garde K : la version 1 ne la renouvelle pas (époque 1). Il ne peut plus rien recevoir du serveur, mais lirait des paquets du coffre obtenus par un autre chemin (section 6.6).

4.2 Dérivations

HKDF(ikm, info) désigne HKDF-SHA-256 avec le sel "second-brain" (UTF-8) et 32 octets produits.

CléDérivationUsage
CHKDF(K, "second-brain/contenu/1")AES-256-GCM des éléments et des noms d’appareils
FHKDF(K, "second-brain/fichiers/1")AES-256-GCM des morceaux de fichiers
IHKDF(K, "second-brain/identifiants/1")HMAC-SHA-256 des identifiants opaques
SHKDF(R, "second-brain/phrase/signature/1")Graine de la clé de signature Ed25519 de la phrase (section 4.7) ; le serveur n’en connaît que la clé publique
WHKDF(R, "second-brain/phrase/enveloppe/1")Emballe K pour la phrase
AₚHKDF(P, "second-brain/appairage/acces/1")Preuve de possession du secret d’appairage
WₚHKDF(P, "second-brain/appairage/enveloppe/1")Emballe K pour l’appairage

S et W dérivent de R par des étiquettes distinctes : rien de ce que voit le serveur (la clé publique, des signatures) n’apprend quoi que ce soit sur W, donc sur K.

Sur l’appareil, C, F et I sont importées comme clés non extractibles. K n’est relue que pour emballer la clé (appairage, nouvelle phrase).

4.3 Paquets chiffrés

Un paquet est la suite d’octets : en-tête (4 octets) ‖ nonce (12 octets) ‖ texte chiffré et étiquette GCM (16 octets).

  • L’en-tête : version du paquet (1), époque de clé (1), format, 0.
    • Le format vaut 1 pour du JSON UTF-8 (éléments, noms d’appareils) et 2 pour des octets bruts (morceaux de fichiers, clé emballée).
    • L’époque permettra un jour de changer K ; la version 1 n’utilise que l’époque 1.
  • Le nonce est aléatoire à chaque chiffrement.
  • Les données associées (authentifiées, non chiffrées) sont l’en-tête suivi de lignes UTF-8 séparées par \n. Elles lient le paquet à son usage, à son coffre et à sa place : un paquet ne peut être ni déplacé, ni interverti, ni réutilisé ailleurs, et son en-tête ne peut pas être modifié.
PaquetLignes des données associées
Élémentsecond-brain/element/1, identifiant du coffre, identifiant opaque
Morceau de fichiersecond-brain/fichier/1, identifiant du coffre, identifiant opaque du fichier, index/nombre
Nom d’appareilsecond-brain/appareil/1, identifiant du coffre, identifiant de l’appareil
K emballéesecond-brain/cle/1, identifiant du coffre, phrase ou appairage

4.4 Identifiants opaques

  • Élément : base64url(HMAC(I, JSON ["element", type, id])), sans remplissage (43 caractères).
  • Fichier : base64url(HMAC(I, JSON ["fichier", taille, empreinte])).
    • Le contenu est découpé en morceaux de 1 Mio (le dernier plus court ; un fichier vide compte un morceau vide), les mêmes pour tous les appareils.
    • empreinte est le SHA-256 de la suite des SHA-256 de chaque morceau, en base64url.
    • L’identifiant dépend donc du seul contenu : un même contenu a le même identifiant sur tous les appareils, et un fichier ainsi désigné ne change jamais.

L’encodage JSON (tableau) évite toute ambiguïté de concaténation.

4.5 Phrase de récupération

R est écrite en 24 mots selon l’encodage BIP-39 :

  • 256 bits d’entropie et une somme de contrôle de 8 bits (premiers bits de SHA-256(R)) ;
  • 24 indices de 11 bits dans la liste française normalisée de BIP-39 (2 048 mots, dans l’ordre officiel). Son empreinte, SHA-256 des mots en NFKD suivis chacun de \n (le dernier compris), est ebc3959ab7801a1df6bac4fa7d970652f1df76b683cd2f4003c941c63d517e59, celle du fichier officiel bip-0039/french.txt.

La dérivation de graine de BIP-39 (PBKDF2) n’est pas utilisée : R est l’entropie elle-même.

À la saisie, les mots sont comparés sans accents ni majuscules ; les quatre premières lettres suffisent, car elles sont uniques dans la liste. Une faute de frappe est signalée par la somme de contrôle.

4.6 Secret d’appairage

  • Code : P s’écrit en base32 (26 caractères, alphabet A–Z et 2–7), suivi de 2 caractères de contrôle (les 10 premiers bits de SHA-256(P)), groupé par quatre. Une faute de frappe est signalée par l’appareil avant tout appel au serveur.
  • Lien : il porte le secret dans le fragment, qui n’est jamais envoyé à un serveur :
    https://<application>/#rejoindre=<base64url(JSON {"v":1,"s":"<adresse du serveur>","p":"<code>"})>
  • Effacement : l’application doit effacer le fragment de l’adresse dès sa lecture (history.replaceState), au lancement comme dans une page déjà ouverte.
  • Confirmation (un lien ou un QR code peut venir d’un tiers) : avant tout appel, l’appareil montre l’adresse du serveur et avertit que ses données partiront vers ce coffre, lisibles par qui le détient ; il signale un serveur qu’il n’a jamais utilisé. Aucun choix n’est fait d’avance pour les données déjà présentes. Un appareil déjà relié ne quitte son coffre qu’une fois inscrit dans le nouveau. L’adresse venue d’un lien ne sert jamais à la méthode « phrase de récupération ».

4.7 Preuve de la phrase

Les opérations sensibles (rejoindre avec la phrase, retirer un autre appareil, changer la phrase sur-le-champ, affaiblir les réglages du coffre ou changer sa version minimale) exigent une preuve de possession de la phrase, liée au serveur contacté :

  1. Clé. La graine S (section 4.2) donne une paire de clés Ed25519 (WebCrypto : clé PKCS#8 importée, clé publique lue dans son export JWK). À la création du coffre, l’appareil envoie la seule clé publique ; le serveur la garde (coffres.acces, en hexadécimal). Un changement de phrase envoie la nouvelle clé publique.
  2. Défi. L’appareil demande un défi au serveur (defi) : 16 octets aléatoires, à usage unique, valables 5 minutes (500 en attente au plus).
  3. Énoncé signé (lignes UTF-8, chacune terminée par \n) :
    second-brain/preuve/1
    <origine du serveur, telle que l'appareil l'a contacté : https://hôte[:port]>
    <défi>
    <opération : inscrire, appareil-revoquer, phrase-changer ou reglages>
    <SHA-256, en hexadécimal, du texte JSON des paramètres de l'opération>
  4. Appel. Le corps porte {parametres: "<texte JSON>", preuve: {defi, signature}} ; le serveur ne lit les paramètres que dans ce texte signé.
  5. Vérification. Le serveur consomme le défi, puis vérifie la signature avec l’origine qu’il a notée à l’installation (et sa variante avec ou sans « www. »), jamais avec l’en-tête Host de l’appel. Il vérifie par l’extension sodium, par OpenSSL (PHP 8.4 et plus), ou à défaut par un vérificateur en PHP sur l’extension GMP (RFC 8032, mêmes refus que libsodium : S non canonique, points hors de la courbe ou de petit ordre). L’installation exige l’un des trois.

Conséquences :

  • un faux serveur à qui l’on saisirait sa phrase n’obtient qu’une signature pour son origine et son défi : elle ne vaut rien auprès du vrai serveur, même relayée aussitôt ;
  • une preuve ne sert qu’une fois, pour une seule opération et ses paramètres (l’appareil retiré, la nouvelle clé, les réglages demandés) ;
  • le serveur ne garde aucun secret tiré de la phrase : une copie de sa base ne permet aucune opération sensible ;
  • changer le serveur d’adresse (nouveau domaine) : corriger adresse dans secondbrain.config.php, sans quoi les preuves faites pour la nouvelle adresse sont refusées.

5. Enveloppe d’un élément

Avant chiffrement, chaque élément est un objet JSON :

{
  "v": 1,
  "app": "3.0.0",
  "type": "note",
  "id": "identifiant réel",
  "rev": 7,
  "appareil": "identifiant de l'appareil dans le coffre",
  "nomAppareil": "Firefox sur Android",
  "date": "2026-10-10T09:15:00.000Z",
  "supprime": false,
  "donnees": { "…": "l'enregistrement tel que stocké (FORMAT.md, section 6)" },
  "fichiers": {
    "idFichier": { "fid": "…", "taille": 182734, "type": "image/jpeg" }
  }
}
  • donnees : l’enregistrement exact de la base, champs inconnus compris. Pour une trace : "supprime": true et "donnees": null. L’appareil ne croit jamais l’indication de suppression transmise en clair par le serveur : seule compte celle de l’enveloppe déchiffrée.
  • fichiers : chaque fichier référencé par le mémo, son historique compris, avec de quoi le télécharger et le vérifier.
    • Ce descripteur découle de l’enregistrement : il ne se fusionne pas, il se recalcule.
    • L’empreinte d’un élément ne couvre que la correspondance « identifiant local → fid », jamais ces métadonnées. Un appareil adopte celles qu’il reçoit en premier pour un fid donné.
  • rev : compteur propre à l’élément. Nouvelle version = (plus grand rev connu de l’appareil pour cet élément, envois refusés compris) + 1.
    • Une version reçue dont le rev ne dépasse pas celui de la base, et qui n’est pas l’écho d’un envoi de l’appareil, est une anomalie (section 7.7). La version jointe à une réponse « conflit » est contrôlée de même (section 6.3).
    • Un appareil qui a déjà vu une version ne peut donc pas se la voir remplacer en silence par une plus ancienne.
  • Contrôle de l’identifiant : l’appareil recalcule l’identifiant opaque à partir de type et id et vérifie qu’il correspond à celui du paquet.
  • Forme : type est l’un des quatre types (une propriété de la liste elle-même, jamais héritée, comme constructor), id une chaîne de 200 caractères au plus (communs pour les réglages communs), rev un entier d’au moins 1, donnees un objet dont l’id est celui de l’enveloppe.
  • v et app : une version d’enveloppe, un type inconnus ou une forme inattendue mettent le paquet de côté (section 7.3), jamais à la poubelle.

6. Le serveur

6.1 Fichier et données

  • Un fichier, secondbrain.php, déposé dans un dossier du site, par exemple https://exemple.ch/secondbrain/.
  • Les données vont dans un dossier créé à l’installation :
    • de préférence hors du dossier public du site (dans le dossier parent de sa racine, s’il est accessible en écriture), sinon à côté du fichier ;
    • dans les deux cas sous un nom aléatoire (secondbrain-donnees-<16 caractères>), avec un .htaccess (Require all denied) et un index.html vide.
    Quand le dossier est dans le site, l’installation vérifie elle-même, par des requêtes HTTP à l’adresse notée à l’installation : un témoin placé à côté du fichier doit être lisible (preuve que l’adresse est la bonne), et le témoin du dossier de données ne doit pas l’être. Faute de pouvoir le vérifier, elle refuse de continuer. Un dossier hors de la racine du site (DOCUMENT_ROOT) n’est pas vérifié par le web : ce site ne peut pas le servir (un autre site du même hébergement qui aurait sa racine au-dessus le pourrait : cas inhabituel, non contrôlé).
  • secondbrain.config.php, créé à côté du fichier, note le chemin du dossier de données et l’empreinte du code d’installation. Ce fichier PHP ne renvoie rien quand on l’appelle par le web, et une mise à jour du serveur ne le touche pas.
  • Contenu du dossier de données :
    • base.sqlite : SQLite 3.27 au moins (VACUUM INTO), journal classique DELETE, attente de verrou 15 s, auto_vacuum = INCREMENTAL réglé avant la première table ;
    • copies/ : deux copies hebdomadaires de la base (VACUUM INTO) ;
    • fichiers/<2 premiers caractères>/<fid>/<n> : un fichier par morceau ;
    • temp/ ;
    • precedent/ : la version précédente du serveur ;
    • journal.txt, tourné à 1 Mo.

6.2 Schéma SQLite (version 3)

CREATE TABLE config      (cle TEXT PRIMARY KEY, valeur TEXT NOT NULL);          -- generation, installation…
CREATE TABLE coffres     (id TEXT PRIMARY KEY, acces TEXT NOT NULL, enveloppe BLOB NOT NULL, seq INTEGER NOT NULL,
                          cree TEXT NOT NULL, reglages TEXT NOT NULL, phrase_attente TEXT);
CREATE TABLE appareils   (id TEXT PRIMARY KEY, jeton TEXT UNIQUE NOT NULL, nom BLOB, par TEXT,
                          cree TEXT NOT NULL, vu TEXT, revoque TEXT);
CREATE TABLE appairages  (acces TEXT PRIMARY KEY, enveloppe BLOB NOT NULL, expire INTEGER NOT NULL,
                          par TEXT NOT NULL, utilise_par TEXT);
CREATE TABLE elements    (oid TEXT PRIMARY KEY, seq INTEGER NOT NULL, supprime INTEGER NOT NULL);
CREATE INDEX elements_seq ON elements (seq);
CREATE TABLE versions    (oid TEXT NOT NULL, seq INTEGER NOT NULL, supprime INTEGER NOT NULL, donnees BLOB NOT NULL,
                          date TEXT NOT NULL, par TEXT, PRIMARY KEY (oid, seq));                 -- par : appareil auteur (schéma 2)
CREATE TABLE citations   (fid TEXT NOT NULL, oid TEXT NOT NULL, seq INTEGER NOT NULL, PRIMARY KEY (fid, oid, seq));
CREATE TABLE instantanes (jour TEXT PRIMARY KEY, seq INTEGER NOT NULL, date TEXT NOT NULL, verrou INTEGER);
CREATE TABLE fichiers    (fid TEXT PRIMARY KEY, taille INTEGER NOT NULL, morceaux INTEGER NOT NULL,
                          complet INTEGER NOT NULL, cree TEXT NOT NULL);
CREATE TABLE tentatives  (cle TEXT NOT NULL, quand INTEGER NOT NULL);
CREATE TABLE morceaux    (fid TEXT NOT NULL, n INTEGER NOT NULL, par TEXT NOT NULL, PRIMARY KEY (fid, n));   -- schéma 2
  • coffres.acces contient la clé publique Ed25519 de la phrase (section 4.7) ; jeton et appairages.acces des empreintes SHA-256 ; tous en hexadécimal, jamais un secret.
  • defis (schéma 3) garde les défis des preuves de la phrase en attente : defis (defi TEXT PRIMARY KEY, expire INTEGER NOT NULL).
  • citations dit quels fichiers cite chaque version gardée : le serveur sait ainsi quels fichiers restent utiles, sans rien lire. Une citation disparaît avec sa version.
  • morceaux dit quel appareil a envoyé chaque morceau d’un fichier en cours d’envoi (section 9) ; ses lignes disparaissent quand le fichier est complet.
  • coffres.reglages (en clair) porte la rétention, le plafond, les origines ajoutées, les mises à jour d’office et la version minimale de l’application.
  • Les migrations sont additives : une version plus ancienne du serveur fonctionne sur le schéma d’une version plus récente.

6.3 Appels

Une seule adresse : secondbrain.php?f=<fonction>. Requêtes et réponses en JSON (UTF-8), sauf les morceaux de fichiers (application/octet-stream). Les octets dans le JSON sont en base64url sans remplissage.

En-têtes exigés :

  • X-Second-Brain: 1 sur tout appel : il impose aux navigateurs une vérification préalable (CORS) et écarte les formulaires d’autres sites ;
  • X-Second-Brain-Jeton: <jeton> sur les appels réservés aux appareils (🔑). Ce n’est pas Authorization, que beaucoup d’hébergements ne transmettent pas à PHP.
  • X-Second-Brain-App: <version> : la version de l’application. Le serveur refuse envoyer (version-trop-ancienne) à une application plus ancienne que la version minimale du coffre.

Réponses :

  • succès : {"ok": true, "generation": "…", "courant": <seq>, "date": "<heure du serveur>", "versionMin": "…", "alertes": […], …} ;
  • erreur : {"ok": false, "erreur": "<code>", "message": "<texte en français>"}, avec le statut HTTP correspondant.
FonctionRôleCorps → réponse
etatVersion, API, état de l’installation, limites. Ne révèle aucun chemin.— → {version, api, installe, limites: {corps, element, morceau}, signature}
controleRépond avant tout contrôle d’origine et sans ouvrir la base (contrôle d’une mise à jour)— → {version}
installerPremière installation (code d’installation exigé){installation} → {controles: […]}
coffre-creerCrée le coffre (code d’installation exigé, une seule fois){installation, coffre, cle_phrase, enveloppe, appareil: {id, jeton, nom}} → {}
defiDéfi à usage unique d’une preuve de la phrase (section 4.7){} → {defi, expire}
inscrireRejoindre avec la phrase (le nom, chiffré par K, vient ensuite par appareil-renommer)preuve de {appareil: {id, jeton}} → {coffre, enveloppe}
appairage-creer 🔑Prépare un appairage{acces: Aₚ, enveloppe, duree ≤ 900} → {expire}
appairage-utiliserRejoindre avec un code ou un lien (usage unique ; nom ensuite){acces: Aₚ, appareil: {id, jeton}} → {coffre, enveloppe}
appairage-etat 🔑L’appairage a-t-il été utilisé ?{acces} → {utilise_par, expire}
appairage-annuler 🔑Annule un appairage pas encore utilisé (fenêtre fermée){acces} → {}
appareils 🔑Liste des appareils— → {appareils: [{id, nom, par, cree, vu, revoque}]}
appareil-renommer 🔑Renomme l’appareil appelant{nom} → {}
appareil-revoquer 🔑Retire un appareil. Pour un autre que soi, la preuve de la phrase est exigée. Annule le changement de phrase qu’il avait demandé.{appareil} (soi-même) ou preuve de {appareil} → {ajoutes_par_lui: […], phrase_annulee?}
phrase-changer 🔑Nouvelle phrase : immédiate avec la preuve de l’ancienne, sinon après 72 heures (une seule demande en attente){cle, enveloppe}, ou preuve de {cle, enveloppe} → {effet}
phrase-annuler 🔑Annule un changement de phrase en attente— → {}
generation-renouveler 🔑Nouvelle génération (anomalie constatée, section 7.7)— → {generation}
changements 🔑Éléments modifiés depuis un numérodepuis=<seq>&max=<n> → {elements: [{oid, seq, donnees}], dernier, fin}
envoyer 🔑Écritures, chacune avec sa base{elements: [{oid, base, supprime, fids, donnees}]} → {resultats: […], manquants: [fid…]}
fichier-debut 🔑Prépare l’envoi d’un fichier{fid, taille, morceaux} → {complet} ou {recus: [indices des morceaux de l'appelant]}
fichier-morceau 🔑Un morceau chiffré, noté avec son appareilfid=…&n=…, corps binaire → {}
fichier-fin 🔑Complet si tous les morceaux sont là et viennent de l’appelant{fid} → {complet}
fichier-signaler 🔑Fichier reçu altéré : le serveur le remet à recevoir (100 signalements par appareil et par jour){fid} → {remis}
fichier-lire 🔑Lit un morceau ; absent tant que le fichier n’est pas complet (l’appareil réessaie plus tard)fid=…&n=… → binaire
fichiers-etat 🔑Lesquels de ces fichiers manquent au serveur ?{fids: […]} → {manquants: […]}
instantanes 🔑Instantanés retenus— → {instantanes: [{jour, seq, date}]}
instantane 🔑Éléments d’un instantané, par pages ; verrouille l’instantané une heurejour=…&apres=<oid>&max=<n> → {elements: [{oid, seq, donnees}], fin}
versions 🔑Versions d’un élément gardées sur le serveur, de la plus récente à la plus ancienne, par pagesoid=…&avant=<seq> → {versions: [{seq, date, par, donnees}], fin}
reglages 🔑Lire ; modifier, dans des bornes (jours 1–365, semaines 0–104, mois 0–120, plafond 100 Mio–1 Tio). La preuve de la phrase est exigée pour réduire la rétention ou le plafond, ajouter une origine, couper les mises à jour d’office et changer la version minimale{reglages}, ou preuve de {reglages} → {reglages, espace}
maj-verifier 🔑Le serveur lit le manifeste signé{} → {actuelle, disponible, version, date, nouveautes, signature}
maj-installer 🔑Le serveur se met à jour (section 10){} → {version, precedente} ou {version, deja: true}
relais 🔑Relais PDF et titres de pages, réservé aux appareils du coffre (prévu avec l’hébergement de l’application chez soi ; absent du serveur 1.0)comme relais.php

Écriture (envoyer). Dans une transaction exclusive (BEGIN IMMEDIATE), pour chaque élément :

  • Base exacte : le numéro courant de l’élément sur le serveur (0 s’il n’existe pas) est égal à base.
    • Effet : seq du coffre + 1 ; écriture de la version dans versions (avec fids) puis dans elements.
    • Résultat : {oid, seq}.
  • Base périmée : sinon, rien n’est écrit.
    • Résultat : {oid, conflit: true, seq, donnees}, qui porte la version courante du serveur ; l’appareil fusionne sans aller-retour.
    • Cette version passe les mêmes contrôles qu’une version lue (section 7.3) : son numéro et son rev doivent dépasser ceux de la base de l’appareil. Sinon c’est une anomalie (section 7.7), jamais une fusion : un serveur ne peut pas faire revenir en arrière, par ce chemin, les champs que l’appareil n’a pas touchés.
  • Fichiers manquants : manquants liste les fichiers cités qui ne sont pas complets sur le serveur, établie dans la même transaction. L’élément est accepté quand même, et l’appareil renvoie ces fichiers s’il les a.

Ordre des numéros. Les écritures sont sérialisées : un appareil qui lit changements ne voit jamais un numéro avant un numéro plus petit encore en cours d’écriture.

Lecture (changements). Les éléments dont le numéro dépasse depuis, par numéro croissant, avec leur version courante, lus dans une seule transaction.

  • Une page de changements, d’instantane ou de versions tient en 4 Mo, mais contient toujours au moins un élément.
  • L’appareil place son curseur sur dernier (le numéro du dernier élément de la page), jamais sur courant.

Réponses contrôlées par l’appareil. Un serveur ne peut ni épuiser la mémoire de l’appareil, ni le faire tourner sans fin :

  • une réponse JSON est lue jusqu’à 12 Mo, un morceau jusqu’à 1 Mio + 32 octets ; au-delà, elle est refusée (trop-gros) ;
  • une page de changements compte 1 000 éléments au plus, d’identifiants opaques bien formés (43 caractères), aux numéros strictement croissants au-delà de depuis ; dernier est le numéro du dernier élément ; une page vide doit annoncer la fin ;
  • la réponse d’envoyer donne un résultat par élément envoyé, aux numéros entiers ; un envoi accepté a un numéro supérieur à sa base.

Limites. Elles sont annoncées par etat et déduites de la configuration de PHP (post_max_size, memory_limit) :

  • corps d’un appel : 8 Mo au plus ;
  • élément : 3 Mo ;
  • morceau : 1 Mio de contenu (taille fixe) ;
  • fichier : 4 Gio ;
  • plafond d’espace : 10 Gio par défaut.

Limitation des essais. Elle porte sur installer, coffre-creer, inscrire et appairage-utiliser (et une adresse bloquée n’obtient plus de défi) : 5 essais manqués par adresse IP (préfixe /64 en IPv6 ; une adresse IPv4 présentée en IPv6, ::ffff:a.b.c.d, compte comme IPv4) et par quart d’heure. Chaque essai est compté et réservé dans une transaction exclusive : des essais simultanés ne dépassent pas la limite. Un jeton valide n’est jamais refusé, d’où qu’il vienne.

Origines (CORS). Le serveur ne répond qu’aux origines autorisées :

  • https://secondbrain.victorkalix.com, la version en ligne, servie sur une origine qui lui est réservée (la clé d’un coffre ne vit jamais dans un navigateur sous une origine partagée avec d’autres pages) ; la correspondance est exacte : ni le domaine parent, ni une autre adresse qui commence pareil ;
  • app://second-brain, l’application de bureau ;
  • les origines ajoutées dans les réglages (la preuve de la phrase est exigée).

Il n’utilise aucun cookie : l’authentification se fait par jeton, envoyé explicitement. CORS n’est qu’une défense d’appoint.

https. L’application refuse toute adresse de serveur en http://. Seule exception : un serveur de la machine locale, pour une application elle-même servie en local ou sur un domaine d’essai (.test), ou pour l’application de bureau lancée par les vérifications automatiques ; un lien reçu ne peut donc pas diriger un appareil vers un service local. Le serveur refuse aussi le http hors de la machine locale ; derrière un mandataire, il croit l’en-tête X-Forwarded-Proto (limite connue : un hébergement qui le laisserait passer sans le filtrer permettrait d’appeler le serveur en clair ; l’application, elle, n’appelle jamais un serveur distant en clair).

6.4 Tâches du serveur

Le serveur n’a pas de tâche planifiée : il agit au premier appel authentifié de chaque jour (UTC). Ses erreurs PHP ne sont jamais affichées dans les réponses : elles vont au journal du serveur.

  • Avant de traiter cet appel, il note l’instantané du jour.
  • Après avoir répondu quand PHP le permet (fastcgi_finish_request), sinon dans un temps borné et repris aux appels suivants, il :
    1. élague les versions et les instantanés (section 8), par transactions courtes ;
    2. supprime les fichiers que plus aucune version gardée ne cite et qui ont plus de 7 jours. Fichier par fichier, dans une transaction exclusive, il revérifie les citations, efface la ligne du fichier, puis renomme son dossier d’un seul coup avant de l’effacer ;
    3. contrôle l’intégrité de la base (PRAGMA quick_check), puis, chaque semaine, copie la base dans copies/ ;
    4. récupère l’espace libéré (PRAGMA incremental_vacuum).

6.5 Ce que le serveur voit, et ce qu’il peut faire

Il voitIl ne voit pas
Le nombre, la taille et les dates des paquetsLe contenu des éléments et des fichiers
Les identifiants opaques, et quels fichiers cite chaque versionLes identifiants réels, les titres, les mots-clés
Les adresses IP et l’heure des appelsLes noms des appareils (chiffrés)
Les empreintes des secrets d’accès et des jetonsK, R, P, et les clés qui en dérivent

Un serveur malveillant peut :

  • retarder ou retenir des changements ;
  • refuser le service ;
  • prétendre avoir enregistré un envoi (l’appareil le détecte s’il ne revoit pas son envoi ; section 7.3) ;
  • présenter une version ancienne d’un élément à un appareil qui ne l’a jamais vu ;
  • bloquer l’envoi d’un élément par un appareil en lui présentant, sous son identifiant, un paquet d’une version inconnue : l’élément cesse de partir de cet appareil, qui le signale (section 7.3) ; rien n’est perdu ;
  • recevoir une preuve de la phrase d’un utilisateur qui saisirait sa phrase pour l’adresse de ce serveur : elle est liée à l’origine de ce faux serveur et à son défi, et ne vaut rien auprès du vrai (section 4.7). L’application ne propose de toute façon jamais pour la phrase une adresse venue d’un lien (section 4.6).

Il ne peut pas lire, modifier, intervertir ou fabriquer un élément, un fichier ou une suppression sans que l’appareil le détecte. Un paquet altéré n’est jamais appliqué ; l’appareil qui le voit y remet sa version (section 7.3).

L’application elle-même. Ces garanties supposent que le code qui tourne sur l’appareil est le bon :

  • l’application de bureau n’accepte que des mises à jour signées par FTW.group ;
  • la version en ligne est servie par un site web : qui contrôlerait ce site (son hébergeur, ou une faille d’une autre page de la même origine) pourrait servir une page modifiée, ou lire la clé du coffre gardée par le navigateur pour cette origine. Elle est donc servie sur une origine qui lui est réservée, https://secondbrain.victorkalix.com (l’application dans /app/ ; toute adresse en http:// y est redirigée), que ne partage que la page de présentation du logiciel, statique et sans aucun script ; le serveur de synchronisation n’y est jamais installé. La page de l’application n’exécute que ses propres scripts, désignés par leurs empreintes (CSP sans 'unsafe-inline') : une erreur d’échappement dans l’interface ne suffit pas à exécuter du code.

6.6 Ce que peut faire un appareil volé

Avant sa révocation, un appareil volé (ou son jeton et K) peut :

  • lire tout le coffre ;
  • y écrire, y compris des suppressions.

Ses suppressions et ses réécritures sont limitées par plusieurs parades :

  • les instantanés : le serveur garde les versions supprimées, sans que l’appareil volé puisse réduire la rétention ;
  • la dernière version de chaque appareil : le serveur garde 30 jours la dernière version qu’a écrite chacun des appareils (section 8) ; quelques réécritures ne font donc pas disparaître la dernière modification d’un autre appareil, qui se restaure depuis l’historique du mémo ;
  • le garde-fou : un appareil qui reçoit de nombreuses suppressions demande confirmation (section 7.3) ;
  • la mise de côté : chaque appareil garde 30 jours ce qu’on lui a fait supprimer à distance ;
  • la corbeille : un mémo mis à la corbeille à distance y reste au moins 7 jours sur chaque appareil, comptés depuis son arrivée sur l’appareil, quelle que soit la date reçue ; dix mises à la corbeille ou plus d’un coup sont signalées (section 7.3).

Limite connue : les réécritures en masse (contenus vidés par de simples modifications) ne passent pas par le garde-fou ; les versions gardées par le serveur permettent de les défaire.

Il ne peut pas sans la phrase :

  • révoquer un autre appareil ;
  • changer la phrase immédiatement ;
  • réduire la rétention ou le plafond, ajouter une origine, couper les mises à jour d’office ;
  • changer la version minimale (il ne peut donc pas bloquer les écritures des autres appareils).

Il ne peut pas non plus rendre un fichier inutilisable : un fichier n’est complet qu’avec des morceaux venus d’un même appareil, et un fichier reçu altéré est signalé au serveur, qui le remet à recevoir (section 9).

Un changement de phrase sans l’ancienne ne prend effet qu’après 72 heures, annoncé à tous les appareils, qui peuvent l’annuler ; l’ancienne phrase reste valable jusque-là. Un second changement demandé pendant l’attente annule le premier et le signale. La révocation d’un appareil annule ses appairages en cours, annule le changement de phrase qu’il avait demandé (alerte phrase-annulee), et signale les appareils qu’il a ajoutés ; une demande dont l’auteur est retiré ou inconnu n’est jamais appliquée.

Ce que garde un appareil retiré. Il garde ce qu’il a reçu, et K. Il ne peut plus rien recevoir du serveur ; mais s’il obtenait plus tard des paquets du coffre par un autre chemin (hébergeur, copie de la base), il pourrait les lire, y compris ce qui a été écrit après son retrait. La version 1 ne renouvelle pas K. Si l’appareil a été volé et qu’une fuite ultérieure des données du serveur est à craindre, il faut créer un nouveau coffre, comme ci-dessous.

Impasse. Avec la phrase perdue et un appareil volé, l’utilisateur ne peut ni révoquer cet appareil, ni changer la phrase (l’appareil volé peut annuler le changement). La sortie est de repartir à neuf :

  1. réinstaller le serveur (ou en installer un autre) ;
  2. créer un nouveau coffre, avec une nouvelle clé K ;
  3. y relier les appareils restants : la fusion sans base réunit leurs données.

6.7 Codes d’erreur

CodeStatutSens
https-exige403Appel en http hors de la machine locale
origine-refusee403Origine (CORS) non autorisée
interdit403En-tête X-Second-Brain absent
methode, format, fonction405, 400, 404Appel mal formé
trop-gros413Corps trop volumineux
non-installe503Serveur pas encore installé
installation-refusee403Code d’installation absent ou faux
environnement412Installation impossible (contrôle bloquant)
donnees-exposees500Impossible de ranger les données à l’abri du web
coffre-existe409Le serveur porte déjà un autre coffre
appareil-existe409Identifiant d’appareil déjà pris avec un autre jeton
jeton-invalide401Jeton absent ou inconnu
appareil-revoque401Appareil retiré du coffre : il cesse toute tentative
phrase-inconnue403Phrase de récupération fausse
phrase-exigee403Opération sensible sans preuve de la phrase
code-invalide, code-utilise403Code d’appairage inconnu, expiré, ou déjà utilisé
trop-d-essais429Trop d’essais manqués depuis cette adresse, ou trop de fichiers signalés aujourd’hui par cet appareil
version-trop-ancienne426Application plus ancienne que la version minimale du coffre : lecture seule
plafond507Espace du coffre épuisé : nouveaux fichiers refusés
absent404Fichier pas encore reçu par le serveur
signature-indisponible501Aucun moyen de vérifier une signature (ni sodium, ni OpenSSL avec Ed25519, ni GMP) : le serveur ne se met pas à jour seul ; l’installation l’exige déjà
maj-injoignable, maj-refusee, maj-echec502Point de distribution injoignable ; manifeste, révocation ou paquet refusés ; nouvelle version qui ne répond pas (rien n’est changé, ou la précédente est remise)
maj-en-cours409Une mise à jour est déjà en cours
taille400Morceau de taille inattendue
introuvable404Envoi de fichier ou instantané inconnus
ecriture500Écriture impossible à côté du serveur (droits du dossier) : rien n’est changé
base503Base momentanément indisponible : réessayer
interne500Erreur interne

7. Algorithme de synchronisation de l’appareil

7.1 État local

La base SecondBrain2 passe au schéma 3 avec un magasin synchro :

CléContenu
configAdresse du serveur, coffre, appareil, jeton, K, réglage des médias, date de liaison
curseur{seq, generation, reprise} (reprise : resynchronisation par comparaison en cours)
e:<type>:<id>État d’un élément : oid, seq, rev et empreinte de la base, base (contenu de la base, sans l’historique), supprime, envoi ({rev, empreinte, seq} du dernier envoi, jusqu’à ce que son écho soit vu), cascade (références retirées par une suppression locale)
f:<idFichier>État d’un fichier : fid, taille, type, envoye (le serveur l’a), distant (connu, pas encore téléchargé)
q:<oid>Paquet mis de côté : numéro, raison (illisible, altere, erreur), version de l’application ; jamais le paquet, qui se relit sur le serveur. 1 000 au plus, les plus anciens oubliés
r:<type>:<id>Élément supprimé par un autre appareil, gardé 30 jours
conflitsLes derniers conflits résolus, pour les montrer à l’utilisateur
suppressions, suppressions-recentesSuppressions reçues en attente de décision ; dates des suppressions appliquées depuis 24 heures (garde-fou)

Hors du magasin synchro, l’enregistrement corbeille-vue du magasin meta note, pour chaque mémo de la corbeille, le moment où cet appareil l’y a vu entrer (donnée locale, jamais synchronisée).

  • JSON canonique : la valeur passe par JSON.parse(JSON.stringify(…)), puis ses clés sont triées à tous les niveaux, sans espaces.
  • Empreinte d’un élément : SHA-256 du JSON canonique de [enregistrement, {identifiant local → fid}], calculée sur l’enregistrement tel qu’il est stocké.
  • Élément modifié : son empreinte diffère de celle de sa base, ou il n’a pas de base. Ce critère ne dépend d’aucun signal d’écriture.
  • Liste des éléments à examiner : elle est alimentée par les écritures locales et par les autres onglets. L’appareil fait un examen complet au lancement, après un import et toutes les 6 heures.

7.2 Un tour de synchronisation

Un seul onglet synchronise à la fois (verrou navigator.locks, nom second-brain/synchro). Un tour :

  1. Lire changements depuis le curseur, page par page. Chaque page est contrôlée (section 6.3) puis appliquée (7.3), et le curseur avance à dernier. Un élément dont le traitement échoue est mis de côté (raison erreur, nouvel essai au lancement suivant) : les autres continuent.
  2. Envoyer les éléments modifiés et les suppressions, par lots de 2 Mo au plus, chacun avec sa base et la liste de ses fichiers.
    • Les éléments dont les fichiers sont déjà sur le serveur partent d’abord.
    • Accepté : la base devient le contenu envoyé, dans la même transaction que la réponse. Si l’utilisateur a modifié l’élément pendant l’envoi, l’élément reste donc à envoyer.
    • Conflit : fusion (7.4), puis nouvel envoi. Après 5 essais, l’élément attend le tour suivant.
  3. Envoyer les fichiers que le serveur n’a pas (manquants, ou fichiers locaux jamais envoyés), morceau par morceau, avec reprise. Une fois par jour, l’appareil vérifie (fichiers-etat) que le serveur a toujours les fichiers qu’il lui a envoyés, et renvoie ceux qui manquent.
  4. Télécharger les fichiers manquants, selon le réglage des médias.

Les écritures du moteur ne relancent pas de tour.

Déclencheurs :

  • au lancement ;
  • au retour dans l’application ;
  • au retour de la connexion ;
  • 5 secondes après une modification ;
  • toutes les 3 minutes quand l’application est visible.

Après un échec, l’attente double, de 30 secondes à 10 minutes.

Retour en arrière du serveur. Une génération changée, ou une anomalie (lecture ou réponse « conflit »), déclenche la reprise par comparaison (section 7.7).

Heure du serveur. Elle date les modifications (Horloge) tant qu’elle ne s’écarte pas de plus de 24 heures de celle de l’appareil ; au-delà, elle est ignorée et l’écart est signalé.

Plafond atteint. Quand le serveur refuse ses fichiers (plafond), l’appareil espace ses essais (de 10 minutes à 6 heures) et prévient l’utilisateur.

7.3 Appliquer un élément reçu

L’application est atomique : l’enregistrement, ses fichiers locaux, les traces et l’état de synchronisation sont écrits dans une seule transaction.

L’appareil déchiffre le paquet et vérifie l’identifiant opaque.

CasEffet
Échec d’authentification, ou identifiant opaque qui ne correspond pas (paquet altéré ou déplacé)Alerte ; jamais appliqué. La version de l’appareil le remplace : la base prend le numéro du paquet et l’élément est renvoyé
Version de paquet ou d’enveloppe, type ou format inconnus, forme inattendue (paquet illisible)Mis de côté, sans alerte immédiate ; nouvel essai après une mise à jour de l’application. D’ici là, l’élément ne part plus de cet appareil, pour ne pas écraser une version qu’il ne sait pas lire ; l’appareil le signale (alerte dans les réglages, état « en attente », bandeau dans l’éditeur du mémo). Les modifications faites ici restent, et seront réunies à la version lisible
Numéro égal à celui de la baseRien à faire
Numéro inférieur à celui de la baseAnomalie (section 7.7)
Enveloppe de cet appareil, avec le rev et l’empreinte de l’envoi en attenteÉcho : la base s’aligne sur cette version, sans fusion
rev inférieur ou égal à celui de la base (et pas un écho)Anomalie (section 7.7)

Sinon :

Situation localeEffet
Élément absent, sans suppression locale en attenteCréé
Élément non modifié localementRemplacé (ou supprimé, si c’est une trace)
Élément modifié localement, contenu identique à celui reçuAlignement silencieux
Élément modifié localementFusion à trois (7.4)
Supprimé ici, pas encore envoyé ; modifié ailleursLa modification l’emporte : l’élément revient, l’utilisateur est prévenu
Modifié ici ; trace reçueLa modification l’emporte : l’élément est renvoyé, l’utilisateur est prévenu
Supprimé ici et ailleursAccord : rien à faire

Pendant une reprise (section 7.7), les deux dernières lignes sont remplacées par les règles de comparaison.

Règles d’écriture :

  • Tel quel. L’enregistrement reçu est rangé tel quel, sans normalisation ni mise à jour de updatedAt. La normalisation en mémoire est déterministe et ne rend jamais, à elle seule, un élément « modifié ».
  • Pas de cascade. Une trace reçue supprime l’élément et ses fichiers locaux devenus inutiles. Elle ne déclenche pas les nettoyages en cascade (liens, collections) : l’appareil qui a supprimé les fait et les envoie.
  • Cascade défaite. Une suppression locale garde, avec sa trace, la liste des références qu’elle a retirées (collections, mémos liés, nœuds de cartes). Si la suppression perd (l’élément revient, modifié ailleurs), ces références sont remises, comme de nouvelles modifications.
  • Mise de côté. L’élément supprimé à distance est gardé 30 jours dans r:.

Garde-fou. Si un tour apporte plus de 20 suppressions, ou plus de 10 % des éléments (5 suppressions au moins), ou si les suppressions reçues en 24 heures dépassent 50, l’appareil ne les applique pas et demande confirmation ; les suppressions qui arrivent pendant l’attente rejoignent la même décision. S’il refuse, les éléments sont renvoyés comme modifications : ils reviennent sur tous les appareils. Une suppression attendue ne s’applique jamais à un élément dont une version plus récente est arrivée entre-temps.

Corbeille reçue. Un mémo mis à la corbeille par un autre appareil (deletedAt reçu) reste dans la corbeille de cet appareil au moins 7 jours, ou la durée du réglage commun si elle est plus longue. Le délai part du moment où l’appareil l’y a vu entrer, jamais d’une date reçue, et l’échéance se compte à la plus petite des deux heures (appareil, serveur) : ni un serveur qui avance son heure, ni un appareil qui date sa mise à la corbeille de 1970 ne provoquent de purge. Dix mises à la corbeille ou plus reçues d’un coup sont signalées à l’utilisateur, avec un accès à la corbeille.

Écho attendu. Après un envoi accepté au numéro S, l’écho est vu dès qu’une version de l’élément de numéro supérieur ou égal à S arrive (un autre appareil a pu le modifier depuis). Si une lecture complète s’achève sans cette version, le serveur a perdu l’envoi : c’est une anomalie (section 7.7), jamais un renvoi aveugle.

7.4 Fusion à trois

Entrées : la base B (le contenu de la dernière version alignée ; absente pour un élément jamais aligné), la version locale L et la version reçue R.

Règle générale, champ par champ :

  • un champ modifié d’un seul côté prend ce côté ;
  • un champ modifié des deux côtés, à la même valeur, prend cette valeur ;
  • un champ modifié des deux côtés, à des valeurs différentes, prend la valeur la plus récente selon updatedAt (en cas d’égalité, celle de l’appareil au plus grand identifiant) : c’est un conflit.

Sans base, tout champ différent compte comme modifié des deux côtés.

Mémos :

ChampsRègle
title, description, pinned, deletedAt, type, journalDay, champs inconnusRègle générale
content.text, content.notes, content.transcript, content.transcriptBy, content.url, content.source, content.coverAttId, content.thumbnailIdRègle générale
Média principal : content.fileId avec mimeType, duration et peaksUn seul bloc, règle générale
tags, linkedMemosEnsembles : base, plus les ajouts des deux côtés, moins les retraits des deux côtés
content.links (par adresse), content.photos (par identifiant), content.attachments (par identifiant)Listes à clé : ajouts et retraits comme pour un ensemble ; un même élément modifié des deux côtés suit la règle générale, champ par champ. L’ordre est celui de la version la plus récente, suivi des ajouts de l’autre.
revisionsRéunion des deux historiques, sans doublons, par date
createdAtLe plus ancien
updatedAtLe plus grand des deux, plus 1 ms

En cas de conflit sur un mémo :

  • si le conflit porte sur un champ textuel (titre, description, texte, notes, transcription, titres de liens, légendes), l’état complet de la version écartée entre dans l’historique, avec la mention « conflit » et le nom de l’appareil ;
  • si la version écartée avait un autre média principal, ce fichier est cité par l’entrée d’historique : il est gardé et synchronisé ;
  • l’utilisateur est prévenu.

Historique :

  • Une entrée se reconnaît à sa clé : at, raison et l’état canonique. La réunion des deux historiques élimine les doublons par cette clé.
  • Le at d’une entrée « conflit » est l’updatedAt de la version écartée.
  • Plafond : 20 entrées et 512 Ko. On retire d’abord les plus anciennes entrées qui ne sont pas des conflits, puis les plus anciens conflits. La règle ne dépend que du contenu synchronisé : tous les appareils rognent de la même façon.

Fichiers des éléments fusionnés : le descripteur se recalcule à partir de l’enregistrement fusionné. Si un même identifiant local désigne deux contenus différents (données antérieures à la 3.0), celui de la version écartée reçoit un nouvel identifiant local.

Collections : name, description, color et icon suivent la règle générale. memoIds est un ensemble ordonné : l’ordre est celui de la version la plus récente, suivi des ajouts de l’autre.

Brain Maps :

  • name suit la règle générale ; linkedMaps est un ensemble ; linkedMapsPositions est une liste à clé.
  • nodes est une liste à clé (identifiant du nœud). Pour chaque nœud, text, color, x/y (ensemble), parentId et isCentral suivent la règle générale ; linkedMemos est un ensemble ; memoPositions est une liste à clé.
  • Un nœud supprimé d’un côté et modifié de l’autre est gardé.
  • timestamp : le plus grand des deux, plus 1 ms.
  • Un conflit sur le texte d’un nœud est noté dans conflits (ancien et nouveau texte), et l’utilisateur est prévenu.

Réglages communs (communs) : règle générale.

Corbeille : deletedAt est un champ comme un autre. Mettre à la corbeille et restaurer mettent à jour updatedAt, pour que la règle générale les date.

Horloge : updatedAt d’une modification locale = le plus grand de (maintenant, updatedAt précédent + 1 ms).

7.5 Éditeurs ouverts pendant une réception

Un éditeur (mémo, collection, Brain Map) garde l’état de l’élément à l’ouverture, puis l’état qu’il a lui-même écrit à chaque enregistrement. À l’enregistrement suivant, si l’élément a changé depuis, l’éditeur fait la même fusion à trois :

  • base : le dernier état connu de l’éditeur (ouverture ou enregistrement précédent) ;
  • L : le résultat de l’éditeur ;
  • R : l’élément courant.

Conséquences :

  • l’éditeur ne supprime que les fichiers que l’utilisateur a retirés (différence entre l’ouverture et le résultat) ;
  • les collections d’un mémo sont modifiées par différence (ajouts et retraits), jamais par remplacement de la liste.

7.6 Fichiers locaux

  • Un nouveau contenu reçoit toujours un nouvel identifiant local : un fichier n’est jamais réécrit sous le même identifiant avec un autre contenu. Si un élément reçu cite, sous un identifiant local déjà connu ici, un autre contenu (celui d’un autre mémo, par exemple), ce contenu prend un nouvel identifiant (<identifiant>_r<8 caractères>) et les références de l’élément reçu sont réécrites (média, vignette, photos, pièces jointes, historique).
  • Un fichier reçu altéré est refusé et signalé au serveur (fichier-signaler, une fois par jour et par fichier), qui le remet à recevoir : un appareil qui a l’original le renvoie.
  • Un fichier téléchargé est rangé avec le cree et le type de son descripteur.
  • En mode « à la demande », un fichier connu mais pas encore téléchargé n’est ni « manquant » ni « orphelin » : le contrôle d’intégrité le compte à part.
  • La réparation des liens ne retire pas une référence vers un élément connu du serveur et pas encore reçu.
  • Effacement local : un fichier est gardé tant que le cite un mémo (son historique compris) ou un élément mis de côté (r:, 30 jours) ; ensuite il est effacé de l’appareil.

7.7 Reprise par comparaison

Déclencheurs :

  • la génération a changé ;
  • courant est inférieur au curseur ;
  • un élément arrive avec un numéro inférieur à celui de sa base ;
  • un rev reçu ne dépasse pas celui de la base, hors écho ;
  • un écho attendu n’est jamais arrivé.

Procédure :

  1. L’appareil qui constate une anomalie demande d’abord une nouvelle génération (generation-renouveler), sauf si elle vient de changer. Ainsi, tous les appareils voient la génération changer et reprennent à leur tour. L’utilisateur est prévenu (au plus une fois par heure).
  2. Il remet son curseur à 0 et note reprise.
  3. Il relit tout le coffre et compare chaque élément E reçu à son état S :
    • rev de E supérieur à celui de S : traitement ordinaire (section 7.3) ;
    • rev égal, même contenu : la base s’aligne sur le nouveau numéro ;
    • rev égal, contenu différent : fusion sans base, avec entrées de conflit ;
    • rev de E inférieur : la version de l’appareil est plus récente. La base prend le numéro de E, et l’élément est renvoyé avec un rev nouveau.
  4. Les éléments que l’appareil a déjà alignés (traces comprises), mais que le serveur n’a plus, sont renvoyés avec la base 0.
  5. Les fichiers que l’appareil croit envoyés sont vérifiés (fichiers-etat) ; les manquants sont renvoyés.
  6. La reprise se termine : reprise est effacé.

8. Instantanés et versions gardées

  • Création : au premier appel authentifié d’un jour (UTC), le serveur note l’instantané (jour, seq courant), avant de traiter l’appel. Aucune copie n’est faite : un instantané est un numéro.
  • Rétention (réglable ; la réduire exige la preuve de la phrase) :
    • les instantanés des 7 derniers jours ;
    • le premier de chacune des 4 dernières semaines ;
    • le premier de chacun des 12 derniers mois.
  • Versions gardées :
    • pour chaque élément, sa version courante, toujours (pour un élément supprimé, c’est sa trace) ;
    • ses 2 versions précédentes, si elles ont moins de 30 jours ;
    • la dernière version écrite par chaque appareil (colonne par), si elle a moins de 30 jours ;
    • pour chaque instantané retenu, la plus récente version de l’élément dont le numéro ne dépasse pas le sien (une trace comprise).
    • Les autres versions sont effacées lors des tâches du jour. Un élément supprimé ou un média remplacé quitte donc le serveur quand plus aucun instantané retenu ne le cite.
  • Fichiers : un fichier est gardé tant qu’une version gardée le cite.
  • Plafond d’espace :
    • à 80 %, les appareils préviennent ;
    • au-delà du plafond, le serveur refuse les nouveaux fichiers (le texte continue de passer) ;
    • aucun instantané n’est jamais abandonné automatiquement : c’est à l’utilisateur de réduire la rétention ou d’augmenter le plafond.
  • Restaurer un instantané entier. L’appareil :
    1. fait une sauvegarde locale : une archive dans le dossier des sauvegardes (application de bureau), ou un export obligatoire (navigateur) ;
    2. termine un tour de lecture ;
    3. lit tous les éléments de l’instantané, que le serveur verrouille pendant ce temps, avant d’écrire quoi que ce soit ; un instantané vide n’est jamais appliqué ;
    4. si la restauration doit mettre à la corbeille plus de 20 mémos (ou 5 au moins et plus de 10 % des mémos), en demande confirmation à l’utilisateur, avec leur nombre ;
    5. écrit les éléments comme de nouvelles modifications, datées de maintenant, avec un rev nouveau.
    Les mémos créés depuis l’instantané vont à la corbeille. Les collections et les Brain Maps créées depuis sont gardées et signalées. Rien n’est supprimé définitivement.
  • Restaurer un mémo : l’historique du mémo montre aussi ses versions gardées sur le serveur. La version choisie devient une nouvelle modification, et la version remplacée entre dans l’historique.

9. Fichiers

  • Découpage : morceaux de 1 Mio, les mêmes pour tous (section 4.4), chiffrés un par un. Les données associées portent l’index et le nombre de morceaux : un morceau ne peut être ni déplacé, ni omis, ni pris d’un autre fichier.
  • Envoi :
    • fichier-debut indique les morceaux déjà reçus, et l’envoi reprend là où il s’était arrêté ;
    • le serveur écrit chaque morceau dans temp/ en flux, contrôle sa taille exacte (déduite de la taille du fichier et de l’index), puis le renomme et note l’appareil qui l’a envoyé ;
    • fichier-debut ne compte que les morceaux de l’appelant, et fichier-fin ne valide le fichier que si tous ses morceaux viennent de l’appelant : un autre appareil ne peut pas glisser un morceau abîmé dans un envoi.
  • Réception : la taille et le type viennent de l’enveloppe chiffrée du mémo. L’appareil vérifie l’empreinte du contenu reçu (elle redonne le fid). Un fichier incomplet ou altéré est refusé et signalé (fichier-signaler : le serveur le remet à recevoir) ; un fichier pas encore reçu par le serveur (absent) est redemandé plus tard.
  • Médias : chaque appareil choisit l’une de deux options.
    • Tout (par défaut sur ordinateur).
    • À la demande (par défaut sur téléphone) : les vignettes arrivent tout de suite, le reste quand on l’ouvre.

10. Mises à jour du serveur

  • Paquet signé. Le paquet serveur/secondbrain-<version>.txt (extension .txt : l’hébergement du point de distribution ne l’exécute jamais) est signé par la clé de publication de FTW.group. La signature Ed25519 porte sur l’énoncé second-brain/serveur/1\n<version>\n<sha256>\n, dans le format des autres signatures ({format, cle, signature}). Un paquet publié a une constante INSTALLATION vide : la configuration du serveur installé garde l’empreinte de son code d’installation.
  • Manifeste. Le manifeste signé version.json annonce, dans son entrée serveur, la version, le fichier, la taille, l’empreinte SHA-256, la version minimale de PHP et les nouveautés. Le serveur :
    • refuse un manifeste plus ancien que le dernier qu’il a vu (publie), ou daté de plus de 48 heures dans le futur ;
    • applique la liste de révocation des clés (revocation.json), signée par une clé de secours (constante SECOURS) non révoquée ; les révocations vues sont gardées (cles_revoquees), même si la liste cesse d’être servie ;
    • fait confiance aux mêmes clés, à la même clé de secours et au même point de distribution que l’application de bureau (constantes CLES, SECOURS et SOURCE, contrôlées par les vérifications automatiques).
    • Limite connue : le manifeste n’a pas de date d’expiration. Quelqu’un qui contrôlerait le point de distribution, sans la clé, pourrait geler les mises à jour en servant le dernier manifeste vu ; il ne pourrait faire installer aucune autre version.
  • Vérification. Elle se fait par l’extension sodium, par OpenSSL (PHP 8.4 et plus), ou à défaut par le vérificateur en PHP sur l’extension GMP (section 4.7). L’installation exige l’un des trois ; un serveur qui les aurait perdus depuis (signature-indisponible) ne se met plus à jour seul : l’application propose alors de télécharger la version qu’elle contient, à déposer à la place de l’ancien fichier, ou l’application de bureau la dépose par FTP.
  • Installation, sous verrou (flock), après un nouveau contrôle « déjà à jour ? » (la version lue dans le fichier sur le disque) :
    1. Téléchargement et vérification complète du paquet : taille, empreinte, signature, version inscrite dans le code.
    2. Copie de la base (VACUUM INTO) dans copies/avant-maj-<version>.sqlite, gardée pour un secours manuel (les deux dernières).
    3. Copie du nouveau code sous un nom temporaire (secondbrain.maj-<aléa>.php, à côté du fichier), puis appel de cette copie en mode controle, qui n’ouvre pas la base, à l’adresse notée à l’installation (jamais déduite de l’en-tête Host d’un appel). Elle doit répondre avec la nouvelle version ; sinon elle est effacée et rien n’est changé.
    4. L’ancien code est rangé dans precedent/ (les trois dernières versions). Remplacement d’un seul coup (rename), puis opcache_invalidate.
    5. Appel de contrôle sous le vrai nom ; en cas d’échec, l’ancien code revient. La base n’est pas remise : les migrations étant additives, l’ancien code fonctionne sur le nouveau schéma, et les écritures faites entre-temps sont gardées. Si une base devait un jour être remise, la génération serait renouvelée.
    Les copies temporaires orphelines (plus d’une heure) sont effacées par les tâches du jour.
  • Accord. Les mises à jour sont proposées dans Réglages › Synchronisation (au plus une vérification toutes les 6 heures par appareil). L’option « Mises à jour d’office » (réglage majAuto du coffre) les fait installer par les tâches du jour.

11. Installation, appairage, appareils

11.1 Installation

  • Automatique, depuis l’application de bureau :
    • connexion par FTP avec TLS explicite (FTPS, TLS 1.2 au moins, certificat vérifié pour le nom de l’hôte, TLS exigé avant l’envoi des identifiants) ; le FTP en clair est refusé ; le SFTP n’est pas proposé dans la version 3.0 ;
    • l’hôte doit avoir une adresse IP publique (adresse vérifiée puis épinglée) ;
    • le mot de passe sert une fois et n’est jamais conservé ;
    • un compte FTP limité à un dossier est conseillé ;
    • l’application ne dépose que le serveur qu’elle contient (seule la constante INSTALLATION peut différer), sous un nom temporaire renommé ensuite, puis appelle installer et crée le coffre.
  • Manuelle, depuis n’importe quel appareil :
    • l’application prépare le fichier, l’utilisateur le dépose (Web FTP du Manager d’Infomaniak) ;
    • il indique ensuite l’adresse du fichier.
  • Code d’installation :
    • l’application tire 32 octets au hasard et inscrit leur empreinte SHA-256 dans le fichier qu’elle prépare (constante INSTALLATION) ;
    • seul cet appareil peut installer le serveur et créer le coffre ;
    • un paquet publié (constante vide) refuse l’installation ;
    • après la création du coffre, le code ne sert plus.
  • Identifiants tirés par l’appareil. L’appareil tire l’identifiant du coffre, le sien et son jeton (dont le serveur ne garde que l’empreinte). coffre-creer, inscrire et appairage-utiliser sont ainsi idempotents : une réponse perdue se rattrape en rejouant l’appel. Un identifiant d’appareil déjà inscrit n’est accepté qu’avec le même jeton : on ne peut pas prendre l’identité d’un autre appareil.

11.2 Appairage

  • L’appareil existant :
    • affiche le QR code, le lien et le code ;
    • affiche aussi l’adresse du serveur ;
    • surveille l’appairage (appairage-etat) et annonce le nouvel appareil quand il se présente.
  • Le nouvel appareil :
    • lit le QR code avec sa caméra (décodeur intégré à l’application), ou reçoit le lien, ou se fait saisir le code ;
    • affiche l’adresse du serveur, avertit que ses données partiront vers ce coffre (n’accepter qu’un code affiché par l’un de ses propres appareils), signale un serveur qu’il n’a jamais utilisé, et demande confirmation ;
    • s’il a déjà des données, propose deux choix, sans en présélectionner aucun : les réunir à celles du coffre (elles partiront vers ce serveur), ou repartir du coffre (elles sont d’abord sauvegardées dans une archive, puis effacées de l’appareil) ;
    • s’il est déjà relié à un coffre, il ne le quitte (en s’en retirant lui-même) qu’une fois inscrit dans le nouveau ; si l’ancien serveur ne répond pas, l’utilisateur est invité à retirer l’appareil depuis un autre.
  • Sur iPhone, l’application installée sur l’écran d’accueil a un stockage séparé de Safari. Ouverte dans Safari, l’application demande d’abord de s’installer, puis de scanner ou de saisir le code depuis l’application installée.

11.3 Appareils

  • Révocation :
    • un appareil révoqué cesse toute tentative dès la réponse appareil-revoque ;
    • il garde ses données (et K : section 6.6), et peut rejoindre de nouveau le coffre en conservant son état de synchronisation.
  • Changer de phrase : l’utilisateur confirme avoir noté la nouvelle phrase avant son envoi au serveur. Rejouer l’appel après une réponse perdue est sans effet supplémentaire.
  • Phrase perdue : tout appareil du coffre peut en préparer une nouvelle (il connaît K), effective après 72 heures si l’ancienne n’est pas fournie.
  • Quitter le coffre : l’appareil se révoque lui-même et oublie clés et jeton ; ses données restent sur l’appareil.
  • Changer de serveur : l’appareil rejoint un coffre sur le nouveau serveur, puis quitte l’ancien (section 11.2), ou quitte l’ancien coffre et crée le nouveau. La fusion à trois, sans base, réunit alors les données.
  • Rejoindre un autre coffre efface l’état de synchronisation local (e:, f:, q:, r:, curseur), jamais les données.
  • Version minimale : les réglages du serveur portent, en clair, la version minimale de l’application qui peut écrire dans le coffre (3.0.0 par défaut).
    • La changer, pour la relever comme pour l’abaisser, exige la preuve de la phrase (Réglages du serveur) : un appareil volé ne peut pas bloquer les écritures des autres.
    • Une version future de l’application qui ajoutera un type de mémo ou changera la forme d’un champ fera d’abord relever la version minimale par l’utilisateur, avec sa phrase, avant d’écrire sous la nouvelle forme.
    • Un appareil plus ancien reste en lecture seule et invite à se mettre à jour ; le serveur refuse ses écritures.
    • La normalisation d’un appareil ne réécrit jamais une valeur qu’il ne connaît pas.

12. Vérifications automatiques

ExigenceVérification
Trois appareils, modifications croisées, hors ligne, coupures, horloges décalées d’une heure (et de deux jours), 1 Go de médiasSuite « synchronisation » : trois profils de navigateur contre un serveur PHP local
Deux versions conservées en cas de conflit, utilisateur prévenu ; champs différents réunis sans conflitMême suite
Éditeur ouvert pendant une réception ; réponse perdue après écriture ; mise à la corbeille concurrente ; suppression contre modification ; import d’une même archive des deux côtésMême suite
Aucune donnée lisible sur le serveurInspection de la base et du dossier : aucun titre, texte, mot-clé ni identifiant réel
Paquet altéré, rejoué, déplacé ou forgé ; en-tête modifié ; trace forgéeMême suite : paquets modifiés sur le serveur, refusés, puis remplacés par la version authentique
Serveur ou appareil malveillants : version ancienne jointe à un « conflit », reprise piégée, heure du serveur et dates de corbeille truquées, élément empoisonné, quarantaine gonflée, fichier détourné, pagination sans fin, paquets altérés ou illisiblesSuite « moteur » : les vrais modules de l’application, sans navigateur, face à un serveur simulé
Serveur restauré d’une sauvegarde ancienneResynchronisation par comparaison, sans perte
Ménage pendant un envoi interrompu ; fichier manquant renvoyéSuites « serveur » et « synchronisation »
Mise à jour du serveur à la signature fausse refusée, échec suivi d’un retour arrière (code et base)Suite « serveur »
Instantané restauré en entier, mémo isolé restauréSuites « serveur » et « synchronisation »
Opérations sensibles refusées sans la phrase ; garde-fou des suppressions massivesSuites « serveur » et « synchronisation »
Installation par FTP avec TLS (FTPS), FTP en clair refusé, certificat d’un autre nom refusé, contenu modifié refuséSuite de l’application de bureau, avec un serveur FTPS de test
Lien d’appairage piégé : serveur montré et signalé, aucun choix d’avance, l’appareil relié ne quitte pas son coffre si l’inscription échoueSuite « interface »
Retour à la présentation