Le protocole v1, exactement.
Tout ce qui suit est écrit d'après un code figé et publié. Chaque constante peut être vérifiée dans le dépôt, et les vecteurs de test inter-implémentations fixent le comportement au niveau de l'octet. Si cette page et le code divergent un jour, le code fait foi et la divergence est un bug qu'il vaut la peine de signaler.
La version courte
- Le texte de la note est chiffré sur l'appareil de l'expéditeur. Aucun serveur ne le voit jamais.
- La clé de déchiffrement ne voyage que dans le fragment du lien, la partie après
#, que les navigateurs n'envoient jamais à aucun serveur. - Le déchiffrement se fait dans le navigateur du destinataire, en JavaScript que chacun peut lire avec Afficher la source.
- Le jeton de gestion derrière le brûlage, l'expiration et le statut peut contrôler une note mais jamais la déchiffrer. Le serveur ne stocke que son hachage SHA-256.
- Le lien complet ne peut pas être reconstruit après la fermeture de l'écran de note créée. L'appli ne conserve que l'id de la note et le jeton, jamais la clé.
Le lien
burnpony.app/n/{id}#{key}{flags}. L'id est composé de 22 caractères base64url forgés par le serveur à partir de 16 octets aléatoires. La key est composée de 43 caractères de base64url sans remplissage encodant exactement 32 octets aléatoires, générés sur l'appareil de l'expéditeur. Le suffixe facultatif flags est un point suivi de lettres : p signifie qu'une phrase secrète est requise, r que l'expéditeur a activé un accusé de lecture, afin que la page puisse vous indiquer les deux choses avant la révélation qui consomme une vue. Tout ce qui suit # est un fragment d'URL, et les navigateurs n'envoient pas les fragments dans les requêtes. La clé n'atteint aucun serveur, n'apparaît dans aucun journal et ne fait pas partie de la note stockée. Cette seule propriété porte toute la conception.
L'enveloppe
Ce que le serveur stocke est une chaîne JSON opaque : {"v":1,"pw":false,"salt":"…","nonce":"…","ct":"…"} — une version de format, l'indicateur de phrase secrète, un sel de 16 octets, un nonce de 12 octets et le texte chiffré AES-256-GCM avec sa balise d'authentification de 16 octets ajoutée, le tout en base64 standard. L'ordre des clés dans l'enveloppe n'est pas significatif ; elle est analysée comme du JSON.
La charge, octet par octet
Le texte en clair sous le chiffrement est {"v":1,"t":"<text>","ah":<seconds>} — la note (jusqu'à 50 000 caractères) et le minuteur de masquage automatique. Sa sérialisation est déterministe dans toute implémentation : clés exactement dans cet ordre, aucun espace, échappement limité à \" \\ \n \r \t \b \f plus \u00XX pour les caractères de contrôle restants, tout le reste émis littéralement en UTF-8. Les implémentations de référence Swift, WebCrypto et Python produisent des octets identiques, et les vecteurs partagés l'imposent.
Dérivation de clé
La clé de chiffrement est HKDF-SHA256(ikm, salt, info = "BurnPony-v1-key", 32 bytes). Sans phrase secrète, ikm est la clé de fragment. Avec une, la phrase est renforcée par PBKDF2-HMAC-SHA256 sur 600 000 itérations avec le même sel et ajoutée d'abord à la clé de fragment. Les phrases sont canonicalisées avant renforcement dans toute implémentation : normalisation Unicode NFC, puis suppression des espaces de début et de fin y compris U+FEFF. Un espace clavier parasite ou une saisie Unicode décomposée est pardonné ; les changements de casse et d'espacement intérieur, délibérément non. Une mauvaise phrase et un texte chiffré altéré échouent tous deux simplement à l'authentification GCM ; rien ne les distingue.
L'API
Six opérations sous /api, toutes en JSON. POST /notes stocke une enveloppe avec un plafond de vues (1–100), une expiration (300 à 2 592 000 secondes) et l'indicateur d'accusé, renvoyant l'id de la note et un jeton de gestion à usage unique de 64 hex. GET /notes/{id} consomme une vue dans une transaction et annule le texte chiffré quand le plafond est atteint. Avec le jeton de gestion : GET …/status rapporte les compteurs et les heures d'accusé, DELETE brûle immédiatement, PATCH re-programme une note vivante comptée à partir de maintenant, et POST /notes/{id}/push enregistre jusqu'à cinq jetons APNs ou FCM pour la livraison des accusés. POST /report/{id} dépose un signalement d'abus sans consommer de vue. Le jeton n'est stocké côté serveur que comme hachage SHA-256, et chaque échec qui pourrait distinguer un mauvais jeton d'une note brûlée d'une note qui n'a jamais existé renvoie le {"error":"not_found"} identique octet pour octet — la constance est délibérée.
Ce que le serveur stocke
Texte chiffré (annulé au brûlage ou à l'épuisement), compteurs de vues, horodatages, le hachage du jeton, horodatages d'accusé, jetons push pour les notes avec accusé, compteurs de limitation de débit hachés avec un secret local, et signalements d'abus. Pas de comptes, pas d'e-mails, pas d'adresses IP brutes, pas d'analytique. Un balayeur supprime les lignes expirées, mais l'expiration est aussi appliquée à la lecture, donc un cron mort ne prolonge jamais la vie d'une note.
Accusés de lecture, mécaniquement
Les accusés sont divulgués au destinataire avant qu'il ne révèle — c'est à cela que sert l'indicateur r. La charge du push est volontairement générique : l'alerte dit seulement qu'une note a été ouverte, et la seule clé personnalisée est l'id de la note. L'appareil de l'expéditeur réécrit la notification localement avec une étiquette qui n'existe nulle part sauf sur cet appareil ; le serveur ne connaît aucune étiquette.
Vérifiez-le vous-même
Trois vérifications, sans confiance requise. Une : la page du destinataire est reproductible — scripts/assemble_viewer.py --verify dans le dépôt prouve que la viewer.html livrée est identique octet pour octet à ses trois parties vérifiables, et Afficher la source sur n'importe quelle note vivante permet de comparer au fichier publié. Deux : la cryptographie est fixée — shared/burnpony_vectors.json détient la vérité de référence d'une implémentation indépendante, et la suite de tests Swift comme le cœur WebCrypto intégré reproduisent chaque texte chiffré exactement. Trois : la pauvreté du serveur est lisible — server/schema.sql et le routeur de l'API sont assez courts pour être lus d'une traite, et il n'y a rien à y fuiter. Les limites honnêtes tiennent quoi qu'il en soit : une note révélée peut toujours être copiée ou photographiée, et l'opérateur pourrait servir un code différent de celui publié — c'est précisément pourquoi la page est reproductible et pourquoi cette spécification existe, afin que la substitution soit détectable.
La visite plus accessible de la même machinerie est dans la documentation ; le modèle de sécurité et la voie de signalement sont sur la page sécurité.