// the specification

Das v1-Protokoll, exakt.

Alles Folgende ist gegen eingefrorenen, veröffentlichten Code geschrieben. Jede Konstante kann in dem Repository überprüft werden, und die implementierungsübergreifenden Testvektoren fixieren das Byte-Verhalten. Falls diese Seite und der Code je uneins sind, ist der Code die Wahrheit und die Abweichung ein Fehler, den man zu melden sollte.

Die Kurzfassung

  • Der Notiztext wird auf dem Gerät des Absenders verschlüsselt. Kein Server sieht ihn je.
  • Der Entschlüsselungsschlüssel reist nur im Link-Fragment, dem Teil nach #, den Browser nie an einen Server senden.
  • Die Entschlüsselung geschieht im Browser des Empfängers, in JavaScript, das jeder mit Quelltext anzeigen lesen kann.
  • Das Verwaltungs-Token hinter Verbrennen, Ablauf und Status kann eine Notiz steuern, sie aber nie entschlüsseln. Der Server speichert nur ihren SHA-256-Hash.
  • Der vollständige Link kann nach dem Schließen des Erstellungs-Bildschirms nicht rekonstruiert werden. Die App behält nur die Notiz-ID und das Token, nie den Schlüssel.

Der Link

burnpony.app/n/{id}#{key}{flags}. Die id sind 22 base64url-Zeichen, die der Server aus 16 Zufallsbytes prägt. Der key sind 43 Zeichen ungepolstertes base64url, das genau 32 auf dem Absendergerät erzeugte Zufallsbytes kodiert. Das optionale flags-Suffix ist ein Punkt plus Buchstaben: p bedeutet, eine Passphrase ist nötig, r bedeutet, der Absender hat eine Lesebestätigung aktiviert, sodass die Seite dir beides vor dem aufrufverbrauchenden Enthüllen sagen kann. Alles nach # ist ein URL-Fragment, und Browser senden Fragmente nicht in Anfragen. Der Schlüssel erreicht keinen Server, erscheint in keinem Log und ist nicht Teil der gespeicherten Notiz. Diese eine Eigenschaft trägt das ganze Design.

Der Envelope

Was der Server speichert, ist eine opake JSON-Zeichenkette: {"v":1,"pw":false,"salt":"…","nonce":"…","ct":"…"} — eine Formatversion, das Passphrase-Flag, ein 16-Byte-Salt, ein 12-Byte-Nonce und der AES-256-GCM-Chiffretext mit angehängtem 16-Byte-Authentifizierungs-Tag, alles Standard-base64. Die Schlüsselreihenfolge im Envelope ist nicht signifikant; er wird als JSON geparst.

Der Payload, Byte für Byte

Der Klartext unter der Verschlüsselung ist {"v":1,"t":"<text>","ah":<seconds>} — die Notiz (bis zu 50.000 Zeichen) und der Auto-Ausblenden-Timer. Seine Serialisierung ist über jede Implementierung deterministisch: Schlüssel in genau dieser Reihenfolge, kein Whitespace, Escaping beschränkt auf \" \\ \n \r \t \b \f plus \u00XX für verbleibende Steuerzeichen, alles andere wörtlich als UTF-8 ausgegeben. Die Swift-, WebCrypto- und Python-Referenzimplementierungen erzeugen identische Bytes, und die gemeinsamen Vektoren erzwingen es.

Schlüsselableitung

Der Verschlüsselungsschlüssel ist HKDF-SHA256(ikm, salt, info = "BurnPony-v1-key", 32 bytes). Ohne Passphrase ist ikm der Fragment-Schlüssel. Mit einer wird die Passphrase durch PBKDF2-HMAC-SHA256 mit 600.000 Iterationen über dasselbe Salt gestreckt und zuerst an den Fragment-Schlüssel angehängt. Passphrasen werden vor dem Strecken in jeder Implementierung kanonisiert: Unicode-NFC-Normalisierung, dann Trimmen führender und nachfolgender Leerzeichen einschließlich U+FEFF. Ein versehentliches Tastatur-Leerzeichen oder zerlegte Unicode-Eingabe wird verziehen; Änderungen von Groß-/Kleinschreibung und innerer Abstände bewusst nicht. Eine falsche Passphrase und manipulierter Chiffretext scheitern beide einfach an der GCM-Authentifizierung; nichts unterscheidet die beiden.

Die API

Sechs Operationen unter /api, alle JSON. POST /notes speichert einen Envelope mit Aufruf-Obergrenze (1–100), Ablauf (300 bis 2.592.000 Sekunden) und dem Bestätigungs-Flag und gibt die Notiz-ID und ein einmaliges 64-Hex-Verwaltungs-Token zurück. GET /notes/{id} verbraucht einen Aufruf in einer Transaktion und nullt den Chiffretext, wenn die Obergrenze erreicht ist. Mit dem Verwaltungs-Token: GET …/status meldet Zähler und Bestätigungszeiten, DELETE verbrennt sofort, PATCH stellt die Zeit einer lebenden Notiz ab jetzt neu ein, und POST /notes/{id}/push registriert bis zu fünf APNs- oder FCM-Tokens für die Bestätigungszustellung. POST /report/{id} reicht eine Missbrauchsmeldung ein, ohne einen Aufruf zu verbrauchen. Das Token wird serverseitig nur als SHA-256-Hash gespeichert, und jeder Fehler, der ein falsches Token von einer verbrannten Notiz von einer nie existierten Notiz unterscheiden könnte, gibt das byte-identische {"error":"not_found"} zurück — die Konstanz ist Absicht.

Was der Server speichert

Chiffretext (bei Verbrennen oder Erschöpfung genullt), Aufrufzähler, Zeitstempel, der Token-Hash, Bestätigungs-Zeitstempel, Push-Tokens für Bestätigungsnotizen, mit einem lokalen Geheimnis gehashte Ratenbegrenzungszähler und Missbrauchsmeldungen. Keine Konten, keine E-Mails, keine rohen IP-Adressen, keine Analytik. Ein Sweeper löscht abgelaufene Zeilen, aber der Ablauf wird auch beim Lesen erzwungen, sodass ein toter Cron das Leben einer Notiz nie verlängert.

Lesebestätigungen, mechanisch

Bestätigungen werden dem Empfänger vor dem Enthüllen offengelegt — dafür existiert das r-Flag. Die Push-Nutzlast ist bewusst allgemein: Der Hinweis sagt nur, dass eine Notiz geöffnet wurde, und der einzige benutzerdefinierte Schlüssel ist die Notiz-ID. Das Gerät des Absenders schreibt die Benachrichtigung lokal mit einem Label um, das nirgendwo außer auf diesem Gerät existiert; der Server kennt kein Label.

Verifiziere es selbst

Drei Prüfungen, kein Vertrauen nötig. Erstens: Die Empfängerseite ist reproduzierbar — scripts/assemble_viewer.py --verify im Repository beweist, dass die ausgelieferte viewer.html Byte für Byte mit ihren drei überprüfbaren Teilen identisch ist, und Quelltext anzeigen bei einer beliebigen Live-Notiz erlaubt den Vergleich mit der veröffentlichten Datei. Zweitens: Die Kryptografie ist fixiert — shared/burnpony_vectors.json enthält Ground Truth aus einer unabhängigen Implementierung, und sowohl die Swift-Testsuite als auch der eingebettete WebCrypto-Kern reproduzieren jeden Chiffretext exakt. Drittens: Die Armut des Servers ist lesbar — server/schema.sql und der API-Router sind kurz genug, um sie in einem Zug zu lesen, und es gibt nichts darin zu leaken. Die ehrlichen Grenzen bleiben ohnehin: Eine enthüllte Notiz kann immer kopiert oder fotografiert werden, und der Betreiber könnte anderen Code als veröffentlicht ausliefern — genau deshalb ist die Seite reproduzierbar und diese Spezifikation existiert, damit Substitution erkennbar ist.

Die zugänglichere Tour durch dieselbe Maschinerie steht in der Dokumentation; das Sicherheitsmodell und der Meldeweg sind auf der Sicherheitsseite.