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
- 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.
- 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.
- 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.
- L’ordre vient du serveur. L’ordre des changements est fixé par un numéro du serveur, jamais par l’horloge des appareils.
- 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é.
- 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 compris | Les réglages propres à chaque appareil (thème, dictée, sauvegardes…) |
| Les collections et les Brain Maps | Les modèles de dictée (Whisper) |
| Les fichiers des mémos : images, audio, vidéo, PDF, vignettes | La configuration de la synchronisation (clés, jeton) |
| Les suppressions définitives | Les sauvegardes locales et les archives |
| Les réglages communs du coffre : durée de la corbeille |
3. Vocabulaire
| Terme | Sens |
|---|---|
| Coffre | L’ensemble des données d’un utilisateur sur le serveur, et les appareils autorisés. Un serveur porte un seul coffre. |
| Appareil | Une installation de Second Brain (un navigateur, une application de bureau) reliée au coffre. |
| Élément | Un enregistrement synchronisé, de type note (mémo), collection, map (Brain Map) ou communs (réglages communs du coffre). |
| Fichier | Un 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. |
| Base | Pour un élément, la dernière version du serveur sur laquelle l’appareil s’est aligné : son numéro, son rev et son contenu. |
| Trace | Version d’un élément qui dit « supprimé définitivement ». |
| Génération | Identifiant 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
| Secret | Taille | Où il vit | Rôle |
|---|---|---|---|
| K, clé de données | 32 octets aléatoires | Sur chaque appareil du coffre | Tout le contenu en dérive. Jamais transmise en clair. |
| R, secret de récupération | 32 octets aléatoires | Seulement dans la phrase de 24 mots, chez l’utilisateur | Rejoindre le coffre sans autre appareil ; autoriser les opérations sensibles. |
| P, secret d’appairage | 16 octets aléatoires | Dans un QR code, un lien ou un code, 15 minutes au plus | Rejoindre le coffre depuis un appareil existant. |
| Jeton d’appareil | 32 octets aléatoires | Sur l’appareil ; le serveur n’en garde que l’empreinte SHA-256 | Authentifier 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érivation | Usage |
|---|---|---|
| C | HKDF(K, "second-brain/contenu/1") | AES-256-GCM des éléments et des noms d’appareils |
| F | HKDF(K, "second-brain/fichiers/1") | AES-256-GCM des morceaux de fichiers |
| I | HKDF(K, "second-brain/identifiants/1") | HMAC-SHA-256 des identifiants opaques |
| S | HKDF(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 |
| W | HKDF(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
1pour du JSON UTF-8 (éléments, noms d’appareils) et2pour 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 format vaut
- 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é.
| Paquet | Lignes des données associées |
|---|---|
| Élément | second-brain/element/1, identifiant du coffre, identifiant opaque |
| Morceau de fichier | second-brain/fichier/1, identifiant du coffre, identifiant opaque du fichier, index/nombre |
| Nom d’appareil | second-brain/appareil/1, identifiant du coffre, identifiant de l’appareil |
| K emballée | second-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.
empreinteest 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), estebc3959ab7801a1df6bac4fa7d970652f1df76b683cd2f4003c941c63d517e59, celle du fichier officielbip-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–Zet2–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é :
- 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. - 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). - É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> - Appel. Le corps porte
{parametres: "<texte JSON>", preuve: {defi, signature}}; le serveur ne lit les paramètres que dans ce texte signé. - 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
Hostde 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
adressedanssecondbrain.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": trueet"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 unfiddonné.
rev: compteur propre à l’élément. Nouvelle version = (plus grandrevconnu de l’appareil pour cet élément, envois refusés compris) + 1.- Une version reçue dont le
revne 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.
- Une version reçue dont le
- Contrôle de l’identifiant : l’appareil recalcule l’identifiant opaque à partir de
typeetidet vérifie qu’il correspond à celui du paquet. - Forme :
typeest l’un des quatre types (une propriété de la liste elle-même, jamais héritée, commeconstructor),idune chaîne de 200 caractères au plus (communspour les réglages communs),revun entier d’au moins 1,donneesun objet dont l’idest celui de l’enveloppe. vetapp: 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 exemplehttps://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 unindex.htmlvide.
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 classiqueDELETE, attente de verrou 15 s,auto_vacuum = INCREMENTALré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.accescontient la clé publique Ed25519 de la phrase (section 4.7) ;jetonetappairages.accesdes 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).citationsdit 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.morceauxdit 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: 1sur 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 pasAuthorization, que beaucoup d’hébergements ne transmettent pas à PHP.X-Second-Brain-App: <version>: la version de l’application. Le serveur refuseenvoyer(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.
| Fonction | Rôle | Corps → réponse |
|---|---|---|
etat | Version, API, état de l’installation, limites. Ne révèle aucun chemin. | — → {version, api, installe, limites: {corps, element, morceau}, signature} |
controle | Répond avant tout contrôle d’origine et sans ouvrir la base (contrôle d’une mise à jour) | — → {version} |
installer | Première installation (code d’installation exigé) | {installation} → {controles: […]} |
coffre-creer | Crée le coffre (code d’installation exigé, une seule fois) | {installation, coffre, cle_phrase, enveloppe, appareil: {id, jeton, nom}} → {} |
defi | Défi à usage unique d’une preuve de la phrase (section 4.7) | {} → {defi, expire} |
inscrire | Rejoindre 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-utiliser | Rejoindre 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éro | depuis=<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 appareil | fid=…&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 heure | jour=…&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 pages | oid=…&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 :
seqdu coffre + 1 ; écriture de la version dansversions(avecfids) puis danselements. - Résultat :
{oid, seq}.
- Effet :
- 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
revdoivent 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.
- Résultat :
- Fichiers manquants :
manquantsliste 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’instantaneou deversionstient 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 surcourant.
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
changementscompte 1 000 éléments au plus, d’identifiants opaques bien formés (43 caractères), aux numéros strictement croissants au-delà dedepuis;dernierest le numéro du dernier élément ; une page vide doit annoncer la fin ; - la réponse d’
envoyerdonne 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 :- élague les versions et les instantanés (section 8), par transactions courtes ;
- 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 ;
- contrôle l’intégrité de la base (
PRAGMA quick_check), puis, chaque semaine, copie la base danscopies/; - récupère l’espace libéré (
PRAGMA incremental_vacuum).
6.5 Ce que le serveur voit, et ce qu’il peut faire
| Il voit | Il ne voit pas |
|---|---|
| Le nombre, la taille et les dates des paquets | Le contenu des éléments et des fichiers |
| Les identifiants opaques, et quels fichiers cite chaque version | Les identifiants réels, les titres, les mots-clés |
| Les adresses IP et l’heure des appels | Les noms des appareils (chiffrés) |
| Les empreintes des secrets d’accès et des jetons | K, 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 enhttp://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 :
- réinstaller le serveur (ou en installer un autre) ;
- créer un nouveau coffre, avec une nouvelle clé K ;
- y relier les appareils restants : la fusion sans base réunit leurs données.
6.7 Codes d’erreur
| Code | Statut | Sens |
|---|---|---|
https-exige | 403 | Appel en http hors de la machine locale |
origine-refusee | 403 | Origine (CORS) non autorisée |
interdit | 403 | En-tête X-Second-Brain absent |
methode, format, fonction | 405, 400, 404 | Appel mal formé |
trop-gros | 413 | Corps trop volumineux |
non-installe | 503 | Serveur pas encore installé |
installation-refusee | 403 | Code d’installation absent ou faux |
environnement | 412 | Installation impossible (contrôle bloquant) |
donnees-exposees | 500 | Impossible de ranger les données à l’abri du web |
coffre-existe | 409 | Le serveur porte déjà un autre coffre |
appareil-existe | 409 | Identifiant d’appareil déjà pris avec un autre jeton |
jeton-invalide | 401 | Jeton absent ou inconnu |
appareil-revoque | 401 | Appareil retiré du coffre : il cesse toute tentative |
phrase-inconnue | 403 | Phrase de récupération fausse |
phrase-exigee | 403 | Opération sensible sans preuve de la phrase |
code-invalide, code-utilise | 403 | Code d’appairage inconnu, expiré, ou déjà utilisé |
trop-d-essais | 429 | Trop d’essais manqués depuis cette adresse, ou trop de fichiers signalés aujourd’hui par cet appareil |
version-trop-ancienne | 426 | Application plus ancienne que la version minimale du coffre : lecture seule |
plafond | 507 | Espace du coffre épuisé : nouveaux fichiers refusés |
absent | 404 | Fichier pas encore reçu par le serveur |
signature-indisponible | 501 | Aucun 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-echec | 502 | Point 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-cours | 409 | Une mise à jour est déjà en cours |
taille | 400 | Morceau de taille inattendue |
introuvable | 404 | Envoi de fichier ou instantané inconnus |
ecriture | 500 | Écriture impossible à côté du serveur (droits du dossier) : rien n’est changé |
base | 503 | Base momentanément indisponible : réessayer |
interne | 500 | Erreur 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 |
|---|---|
config | Adresse 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 |
conflits | Les derniers conflits résolus, pour les montrer à l’utilisateur |
suppressions, suppressions-recentes | Suppressions 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 :
- Lire
changementsdepuis 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é (raisonerreur, nouvel essai au lancement suivant) : les autres continuent. - 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.
- 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. - 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.
| Cas | Effet |
|---|---|
| É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 base | Rien à faire |
| Numéro inférieur à celui de la base | Anomalie (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 locale | Effet |
|---|---|
| Élément absent, sans suppression locale en attente | Créé |
| Élément non modifié localement | Remplacé (ou supprimé, si c’est une trace) |
| Élément modifié localement, contenu identique à celui reçu | Alignement silencieux |
| Élément modifié localement | Fusion à trois (7.4) |
| Supprimé ici, pas encore envoyé ; modifié ailleurs | La modification l’emporte : l’élément revient, l’utilisateur est prévenu |
| Modifié ici ; trace reçue | La modification l’emporte : l’élément est renvoyé, l’utilisateur est prévenu |
| Supprimé ici et ailleurs | Accord : 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 :
| Champs | Règle |
|---|---|
title, description, pinned, deletedAt, type, journalDay, champs inconnus | Règle générale |
content.text, content.notes, content.transcript, content.transcriptBy, content.url, content.source, content.coverAttId, content.thumbnailId | Règle générale |
Média principal : content.fileId avec mimeType, duration et peaks | Un seul bloc, règle générale |
tags, linkedMemos | Ensembles : 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. |
revisions | Réunion des deux historiques, sans doublons, par date |
createdAt | Le plus ancien |
updatedAt | Le 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,raisonet l’état canonique. La réunion des deux historiques élimine les doublons par cette clé. - Le
atd’une entrée « conflit » est l’updatedAtde 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 :
namesuit la règle générale ;linkedMapsest un ensemble ;linkedMapsPositionsest une liste à clé.nodesest une liste à clé (identifiant du nœud). Pour chaque nœud,text,color,x/y(ensemble),parentIdetisCentralsuivent la règle générale ;linkedMemosest un ensemble ;memoPositionsest 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
creeet letypede 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é ;
courantest inférieur au curseur ;- un élément arrive avec un numéro inférieur à celui de sa base ;
- un
revreçu ne dépasse pas celui de la base, hors écho ; - un écho attendu n’est jamais arrivé.
Procédure :
- 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). - Il remet son curseur à 0 et note
reprise. - Il relit tout le coffre et compare chaque élément E reçu à son état S :
revde 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 ;revde 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 unrevnouveau.
- 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.
- Les fichiers que l’appareil croit envoyés sont vérifiés (
fichiers-etat) ; les manquants sont renvoyés. - La reprise se termine :
repriseest 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 :
- fait une sauvegarde locale : une archive dans le dossier des sauvegardes (application de bureau), ou un export obligatoire (navigateur) ;
- termine un tour de lecture ;
- 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é ;
- 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 ;
- écrit les éléments comme de nouvelles modifications, datées de maintenant, avec un
revnouveau.
- 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-debutindique 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-debutne compte que les morceaux de l’appelant, etfichier-finne 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 constanteINSTALLATIONvide : la configuration du serveur installé garde l’empreinte de son code d’installation. - Manifeste. Le manifeste signé
version.jsonannonce, dans son entréeserveur, 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 (constanteSECOURS) 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,SECOURSetSOURCE, 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.
- refuse un manifeste plus ancien que le dernier qu’il a vu (
- 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) :- Téléchargement et vérification complète du paquet : taille, empreinte, signature, version inscrite dans le code.
- Copie de la base (
VACUUM INTO) danscopies/avant-maj-<version>.sqlite, gardée pour un secours manuel (les deux dernières). - Copie du nouveau code sous un nom temporaire (
secondbrain.maj-<aléa>.php, à côté du fichier), puis appel de cette copie en modecontrole, qui n’ouvre pas la base, à l’adresse notée à l’installation (jamais déduite de l’en-têteHostd’un appel). Elle doit répondre avec la nouvelle version ; sinon elle est effacée et rien n’est changé. - L’ancien code est rangé dans
precedent/(les trois dernières versions). Remplacement d’un seul coup (rename), puisopcache_invalidate. - 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.
- 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
majAutodu 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
INSTALLATIONpeut différer), sous un nom temporaire renommé ensuite, puis appelleinstalleret 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.
- l’application tire 32 octets au hasard et inscrit leur empreinte SHA-256 dans le fichier qu’elle prépare (constante
- 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,inscrireetappairage-utilisersont 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.
- un appareil révoqué cesse toute tentative dès la réponse
- 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
| Exigence | Vérification |
|---|---|
| Trois appareils, modifications croisées, hors ligne, coupures, horloges décalées d’une heure (et de deux jours), 1 Go de médias | Suite « 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 conflit | Mê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és | Même suite |
| Aucune donnée lisible sur le serveur | Inspection 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ée | Mê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 illisibles | Suite « moteur » : les vrais modules de l’application, sans navigateur, face à un serveur simulé |
| Serveur restauré d’une sauvegarde ancienne | Resynchronisation 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 massives | Suites « 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 échoue | Suite « interface » |