Sicherheit & Messaging

doublethink: ntfy, dem du mit privaten Daten vertrauen kannst

ntfy ist wunderbar, weil es nichts von dir verlangt. Genau deshalb sind seine Topics praktisch oeffentlich. Ich wollte den ersten Teil ohne den zweiten, und der ehrliche Weg dorthin fuehrte mitten durch einen Entwurf, den ich wegwerfen musste.

Ramazan Yavuz
Ramazan Yavuz ·

Falls du ntfy noch nicht kennst, der Pitch passt in einen Satz: such dir einen Topic-Namen aus, POSTe darauf, abonniere ihn, fertig. Keine Konten, kein Setup, kein SDK. Du kannst aus einem Shell-Skript eine Benachrichtigung an dein Handy schicken, in der Zeit, die du zum Lesen dieses Absatzes brauchst. Ich nutze es staendig, und genau diese Muehelosigkeit ist der Punkt. Aber in der Einfachheit steckt ein Haken, und sobald man ihn sieht, sieht man ihn immer: der Topic-Name ist das Einzige, was deine Daten schuetzt. Wer den Namen kennt oder errraet, kann das Topic lesen und beschreiben. Es gibt keinen Begriff von "dieser Kanal gehoert diesen zwei Parteien und niemandem sonst". Fuer Alarme ist das in Ordnung. Fuer alles, was du ungern von einer fremden Person gelesen saehest, nicht. doublethink ist mein Versuch, ntfys Leichtigkeit zu behalten und genau das hinzuzufuegen, was ntfy bewusst weglaesst: wirklich private Kanaele.


Die erste Entscheidung war, ob ich ueberhaupt etwas bauen sollte. Es gibt viele sichere Message-Broker. Also habe ich vor der ersten Codezeile die ehrliche Recherche gemacht: sitzt schon etwas an genau dem Schnittpunkt, den ich wollte, naemlich ntfy-Setup-Aufwand und echte Privatsphaere? Ich habe mir ntfys eigene Zugriffskontrolle angesehen, MQTT-Broker mit ACLs, NATS mit seinen Accounts und signierten JWTs, gehostete Dienste wie Pusher und Ably, und Matrix als Ende-zu-Ende-verschluesseltes Substrat. Der Befund war klar und ein wenig ueberraschend: die beiden Eigenschaften stehen in jedem existierenden selbst-hostbaren Werkzeug im Widerspruch. Die ntfy-einfachen sind standardmaessig offen. Die mit echter Pro-Kanal-Autorisierung, allen voran NATS, koennen das wirklich gut, kosten aber weit mehr Setup-Zeremonie als ntfy. Und fast keines der selbst-hostbaren verschluesselt so, dass der Betreiber die Nachrichten nicht lesen kann. Die gehosteten Dienste, die Ende-zu-Ende-Verschluesselung bieten, sind geschlossen und nur als Service verfuegbar. Die Luecke war also echt: einen selbst-gehosteten Broker, der ntfy-einfach ist und den Betreiber aus deinem privaten Verkehr heraushaelt, gab es nicht. Das ist die Zelle, die doublethink fuellt.


Mein erster Entwurf war falsch, und es lohnt sich zu sagen, warum, denn der Fehler ist ein verbreiteter. Ich griff zur schweren, "offensichtlich sicheren" Maschinerie: jede Partei haette ein kryptografisches Geraete-Schluesselpaar, der Broker wuerde Verbindungen mit einer signierten Challenge authentifizieren, und das Verpaaren zweier Parteien wuerde einen einmaligen Einladungscode plus eine kurze Bestaetigungs-Zeichenkette umfassen, die beide Menschen ausserhalb des Kanals vergleichen, um einen Man-in-the-Middle auszuschliessen. Auf dem Papier war das der starke Entwurf. Ich habe ihn gebaut. Dann habe ich versucht, ihn zu benutzen, und gemerkt, dass ich die ganze Praemisse stillschweigend verraten hatte. Jeder private Kanal verlangte jetzt eine mehrstufige Zeremonie mit einem Betreiber in der Schleife. Das, was ntfy nachahmenswert machte, dass man es in Minuten aufsetzt und Dinge darauf zeigt, war weg. Ich hatte einen sicheren Broker gebaut, der nicht mehr ntfy-einfach war, also eines der Dinge, die es schon gab.

Also habe ich ihn geloescht. Der Ersatz ist fast peinlich einfacher, und diese Einfachheit ist das Feature. Ein privater Kanal wird durch ein einziges, hochentropisches geteiltes Geheimnis geschuetzt. Du erstellst einen Kanal mit einer einzigen Anfrage und bekommst das Geheimnis zurueck. Du gibst dieses Geheimnis ueber einen vertrauenswuerdigen Weg, den du ohnehin hast, an die andere Partei. Wer das Geheimnis hat, kann dem Kanal beitreten; sonst niemand. Es ist genau das ntfy-Denkmodell, such dir etwas aus und teile es, nur ist das Etwas ein unerratbares Geheimnis statt eines menschenlesbaren Namens, und diese eine Aenderung schliesst das Loch.


Der entscheidende Teil ist, wie das Geheimnis verwendet wird, denn es muss zwei verschiedene Aufgaben erfuellen, ohne dem Broker eine dritte zu erlauben. Aus dem Geheimnis leitet jede Seite mit einer Schlusselableitungsfunktion zwei unabhaengige Schluessel ab. Ein Schluessel authentifiziert dich gegenueber dem Broker: der Broker speichert ihn und laesst dich per Challenge-Response zu, kann also pruefen, dass du das Geheimnis hast, ohne dass das Geheimnis je ueber die Leitung geht. Der andere Schluessel verschluesselt deine Nachrichten, und er wird mit einem anderen Label abgeleitet, sodass der Broker, der den ersten Schluessel hat, den zweiten nicht berechnen kann. Der Broker weiss also, dass du auf dem Kanal erlaubt bist, und leitet deine Nachrichten treu weiter, aber die Nachrichten selbst sind Ende-zu-Ende zwischen den Parteien verschluesselt, die das Geheimnis teilen. Der Betreiber des Brokers, ich, auf meinem eigenen Server, kann sie nicht lesen. Diese Eigenschaft, der Betreiber kann privaten Verkehr nicht lesen, ist der ganze Grund, warum es das Projekt gibt, und ich habe sie sorgfaeltig gegenpruefen lassen, statt meinem ersten Instinkt bei der Krypto zu vertrauen.

Ich moechte praezise sein, was das bringt und was nicht, denn Verschluesselung zu ueberverkaufen ist eine eigene Art Unehrlichkeit. doublethink ist payload-blind, nicht metadaten-blind. Der Broker kann deine Nachrichteninhalte nicht lesen, sieht aber die Kanal-ID, das Timing und die Groessen. Das Modell ist ausserdem symmetrisch: beide Halter des Geheimnisses koennen senden und lesen, eine Nachricht beweist also, dass ein Halter sie gesendet hat, nicht welcher. Das sind bewusste, dokumentierte Grenzen, nicht Dinge, die ich in einer Fussnote verstecke. Fuer "zwei Parteien, die einander ohnehin vertrauen und eine private Leitung dazwischen wollen", was der eigentliche Anwendungsfall ist, sind sie der richtige Kompromiss.


Hier hoert es auf, Theorie zu sein. Das Erste, wofuer ich doublethink nutze, ist das Debuggen von Android-Builds. Wer je einem Bug nachgejagt ist, der sich nur auf einem echten Geraet reproduziert, im wackligen Netz, fern vom Debugger, kennt den Schmerz: adb logcat haengt nicht dran, der Crash ist weg, und du bist aufs Raten zurueckgeworfen. Also lasse ich meine in Entwicklung befindliche APK ihren Debug-Strom, strukturierte Logs, den Zustand im Moment, in dem etwas schiefgeht, einen Stacktrace, an einen privaten doublethink-Kanal POSTen, und ich abonniere diesen Kanal auf meinem Laptop. Das Handy kann im Mobilfunk auf der anderen Seite der Stadt sein. Die Debug-Infos erscheinen in Echtzeit auf meinem Bildschirm, sie sind Ende-zu-Ende verschluesselt, sodass die Erkenntnisse des Geraets und was auch immer sie enthalten nicht im Klartext auf einem Relay liegen, und der Test-Build braucht nur das Kanal-Geheimnis eingebacken, kein ganzes Logging-Backend. Wenn der Bug sich reproduziert, habe ich den Beweis statt eines Achselzuckens.

Die zweite Nutzung ist das Verbinden von Werkzeugen und Agenten. Vieles, was ich heute baue, umfasst getrennte Prozesse, die miteinander reden muessen: ein lokaler Agent und ein Browser-Frontend, eine langlaufende Aufgabe, die Fortschritt streamt, zwei CLIs, die Arbeit hin- und herreichen wollen. Der Kanal ist bidirektional, asynchron und streamend, sodass ein Hintergrundjob viele Nachrichten ueber die Zeit ausgeben kann und die andere Seite sie erhaelt, sobald sie ankommen, statt in einem Schwung am Ende. Es ist dieselbe Form wie ntfy, weshalb es angenehm ist, ein Werkzeug darauf zu zeigen, aber ich kann echte Anweisungen und echte Ausgaben hindurchschicken, ohne das ungute Gefuehl, dass der Topic-Name das Einzige zwischen meinen Daten und einer fremden Person ist.


Oeffentlich zu gehen erzwang noch eine Runde Ehrlichkeit. In dem Moment, in dem ein Broker fuer jeden erreichbar ist, hoert "jeder kann einen Kanal erstellen" auf, ein charmanter ntfy-Zug zu sein, und wird zu einem offenen Relay, das du dem Internet uebergeben hast. Die neueste Version fuegt also die unglamouroese Maschinerie hinzu, die eine oeffentliche Instanz wirklich braucht: leichtgewichtige Konten mit einem API-Schluessel fuer alle, die Persistenz wollen, optionale Nachrichten-Aufbewahrung, damit eine Gegenstelle, die offline war, sich wieder verbinden und genau das nachholen kann, was sie verpasst hat, eine Lebensdauer, die alte Nachrichten altern laesst, Pro-Kanal- und Pro-Konto-Speicherquoten, damit niemand die Platte fuellt, und Ratenbegrenzungen auf den lauten Pfaden. Ephemere Kanaele bleiben anonym und ohne Zeremonie, der ntfy-Pfad; aufbewahrte Kanaele, die Speicher kosten, brauchen ein Konto, damit die Kosten zugeordnet werden koennen. Es gibt ausserdem einen Betreiber-Schluessel, in einer Umgebungsvariable gesetzt, mit dem ich Limits fuer einen Kanal anheben kann, der mir wichtig ist, ohne irgendwem einen Weg zu geben, seine Inhalte zu lesen.


Dann bat ein echter Nutzer um etwas, das der Entwurf noch nicht konnte, und es schob das Projekt dorthin, wo es mir besser gefaellt. Der Fall: ein kleiner Webring wollte Besuchern erlauben, von einer Seite aus eigene dauerhafte Topics zu erstellen, im Grunde ein Gaestebuch. Daraus fielen zwei Probleme. Erstens ist das Erstellen eines Topics, das nie ablaeuft, eine privilegierte Handlung, du willst nicht, dass irgendwer im Internet dauerhaften Speicher auf deiner Platte anlegt, also braucht es den Segen eines Betreibers. Aber zweitens darf der Betreiber das Geheimnis des Kanals nicht erfahren, sonst ist das Privatsphaere-Versprechen eine Luege. Die Antwort ist ein Grant-Ticket. Der Betreiber, der den Admin-Schluessel haelt, stellt ein einmaliges, zeitlich begrenztes Ticket aus, das einen dauerhaften, gedeckelten Kanal unter einem festen Namensraum autorisiert. Der Nutzer loest dieses Ticket ein, waehrend er den Kanal mit seinem eigenen Geheimnis erstellt. So autorisiert der Betreiber den dauerhaften Kanal, ohne das Geheimnis je zu sehen, und kann ihn weiterhin nicht lesen; die Richtlinie kommt aus dem Ticket, nie vom Client, sodass ein durchgesickertes Ticket seine eigenen Rechte nicht erweitern kann. Der Admin-Schluessel lebt nur auf dem Server, nie im Browser, was bedeutete, einen winzigen separaten Dienst aufzusetzen, der diese Grants vermittelt, der eine Ort, an dem der Schluessel sein darf.

Das zu bauen brachte eine schaerfere Frage ans Licht, die der Nutzer klar stellte: ein Gaestebuch ist oeffentlich, warum also ueberhaupt Ende-zu-Ende-Verschluesselung bezahlen? Er hatte recht. Also wurde der Grant verschluesselungs-agnostisch, und die Wahl wanderte zu der Person, der die Daten gehoeren, wohin sie gehoert. Loese ein Ticket mit einem Schluessel ein, und du bekommst den verschluesselten Kanal wie zuvor. Loese es ohne einen ein, und du bekommst ein Klartext-Dauer-Topic, erreichbar auf dem offenen ntfy-Pfad ohne Schluessel-Zeremonie: jeder kann posten, jeder kann lesen, und ein frischer Besucher holt den ganzen Verlauf nach. Diese Art gibt die Eigenschaft "der Betreiber kann es nicht lesen" bewusst auf, denn fuer wirklich oeffentliche Daten bringt dir diese Eigenschaft nichts und kostet dich Schluesselverwaltung. Das Ehrliche war, das zu einer sichtbaren, dokumentierten Wahl zu machen, die der Nutzer beim Erstellen trifft, nicht zu einem Default, den ich fuer ihn waehle. Irgendwo hier habe ich auch die Speicher-Engine auf Redis umgestellt, weil die Last hochdurchsatzig statt hochvolumig ist, und genau aufgeschrieben, wie viel ein harter Absturz verlieren kann (etwa eine Sekunde), denn "dauerhaft" sollte heissen, dass die Daten nicht altern, nicht dass ich mehr Haltbarkeit verspreche, als die Platte hergibt.

Nichts in diesem letzten Absatz ist aufregend, und das ist der Punkt, den ich immer wieder neu lerne. Die interessante Idee in doublethink, ntfy-Leichtigkeit plus ein geteiltes Geheimnis, das der Broker nicht entschluesseln kann, passt in einen Satz. Die Arbeit, die daraus etwas macht, dem man echten Verkehr im offenen Internet anvertrauen kann, steckt in den langweiligen Teilen: die Quote, die innerhalb einer Transaktion durchgesetzt wird, damit sie nicht in eine Race Condition geraet, die Aufbewahrung, die bei Ablauf loescht, statt Speicher zu lecken, der Secret-Scan, der mich dabei erwischte, wie ich gerade einen echten Admin-Schluessel als Test-Fixture in ein oeffentliches Repository committen wollte. doublethink ist Open Source, laeuft in Docker oder als einzelne Binaerdatei und traegt einen unmissverstaendlichen Gewaehrleistungsausschluss, weil ein Werkzeug, dessen einziger Job das Halten privaten Verkehrs anderer Leute ist, ehrlich darueber sein sollte, die Arbeit einer einzelnen Person zu sein, geprueft, aber nicht unfehlbar. Wenn du je ntfys Leichtigkeit fuer etwas wolltest, das eine fremde Person nicht lesen sollte, ist das die Luecke, die es zu fuellen versucht. Auf der Projektseite gibt es eine Live-Demo, die vollstaendig in deinem Browser laeuft: Sie zeigt dir die genauen verschluesselten Bytes, die der Broker weiterleitet, neben dem Klartext, den nur der Geheimnis-Inhaber wiederherstellen kann, sodass du der Privatsphaere beim Arbeiten zusehen kannst, statt mir glauben zu muessen.