Eine App ohne Server: Local-first auf Android
walker kommt ohne Server, Konto und Sync aus. Wie alles auf dem Handy bleibt, wie die verschlüsselten Backups funktionieren und was das kostet.
· Malik · 15 Min. Lesezeit
Was „kein Server“ ausschließt
Eine App ohne Server hat keinen Login-Bildschirm, kein „Passwort vergessen“ und kein „Wir stellen deine Daten für dich wieder her“. Wer sie entwickelt, muss kein Backend betreiben, keine Tabelle mit Konten absichern und keine Rechnung zahlen, die mit jeder neuen Person wächst. Es gibt aber auch kein Dashboard, das zeigt, was auf einem fremden Handy gerade schiefgeht. Das schließt mehr aus, als es klingt.
Ich habe mich trotzdem dafür entschieden. Ein Standortverlauf verrät, wo du wohnst, wo du arbeitest und wo du deine Zeit verbringst, und meinen wollte ich niemandem geben. Deshalb hat walker, die App für Spaziergänge, Wanderungen und Radtouren, die ich für Android entwickle, einen kurzen ersten Grundsatz: Alle Daten liegen auf dem Handy. Es gibt kein Konto, keinen Server und keinen Sync, also keinen Abgleich zwischen Geräten, und auch keine Telemetrie und keine Absturzberichte. Dieser Artikel handelt davon, was an ihre Stelle tritt.
Wo die Daten liegen
Alles, was walker speichert, liegt im privaten Speicher der App. Die Datenbank ist SurrealDB, eingebettet in die App: Sie läuft im selben Prozess auf SurrealKV, einer in Rust geschriebenen Key-Value-Engine, und ihre Dateien liegen im Verzeichnis der App. Es gibt keinen Port und keinen Daemon, und ihre Client-Protokolle sind gar nicht erst einkompiliert. Den Rest des Stacks beschreibt Eine ganze Android-App in Rust statt Kotlin, der Artikel, mit dem du am besten anfängst.
Zwei Arten von Daten bleiben außerhalb der Datenbank. Eine laufende Aufzeichnung geht in eine Journaldatei, an die jeder GPS-Punkt angehängt wird, sobald er ankommt. Die Datenbank sieht so nur einen Schreibvorgang pro gespeicherter Wanderung. Die rohen Satellitenmessungen einer Wanderung landen in einer eigenen Datei, mit LZ4 komprimiert und per SHA-256 geprüft. LZ4 hat die Rohdaten einer Radtour von 8,5 MB auf 3,2 MB verkleinert.
Eine Designentscheidung folgt direkt daraus, dass es keinen Server gibt: Die rohen GPS-Punkte sind das Original, und der geglättete Track wird aus ihnen berechnet. Ich kann niemandes Daten migrieren, aber ich kann ein besseres Verfahren zum Glätten ausliefern, und jede alte Wanderung wird damit besser.
Ins Netz nur auf deinen Wunsch
walker geht durchaus online, aber nur für Downloads, die du anstößt oder erlaubt hast:
- eine Kartenregion, per HTTP-Range-Requests aus dem Planet-Build von Protomaps herausgeschnitten;
- das Gelände dieser Region von Mapterhorn, für Höhen und Höhenlinien;
- ein Gebiet von TopPlusOpen, der topographischen Karte des BKG, oder der Luftbilder eines Bundeslandes, direkt von diesem Land;
- die öffentlichen Satellitenbahnen des Tages vom BKG, um eine Wanderung aus den Rohmessungen zu glätten. Settings → Recording → Download satellite orbits (Satellitenbahnen herunterladen) hat die Werte Always, Ask und Never (immer, fragen, nie). Voreingestellt ist Ask: walker fragt beim Start einer Aufzeichnung und beim Speichern, falls die Bahnen von heute noch fehlen. Mit Always lädt die App sie ohne Nachfrage herunter.
Ein Kartendownload verrät dem Anbieter deine IP-Adresse und die Region, die du haben willst. Der Download der Bahnen verrät dem Host deine IP-Adresse und das Datum. Keiner von beiden enthält jemals eine Position oder eine Route.
All das läuft über einen einzigen HTTP-Client in einer einzigen Quelldatei. Jeder Vorgang landet in einem Netzwerkprotokoll, das Privacy & storage (Datenschutz und Speicher) anzeigt: Datum, Host, Region, Zahl der Anfragen und Bytes. Die Regel, alles zu protokollieren, reichte bis in TLS hinein. Zertifikate werden gegen den Zertifikatsspeicher von Android geprüft. Ob eines widerrufen wurde, erfährt walker nur aus der OCSP-Antwort, die der Server an sein Zertifikat heftet, also gleich mitschickt (OCSP Stapling). Würde Android diese Antwort selbst abrufen, gäbe es Anfragen, die nie im Protokoll auftauchen.
Das Android-Backup, abgeschaltet
Android sichert App-Daten standardmäßig in Google Drive. Bei Apps mit Android 12 oder
neuer als Zielversion schaltet allowBackup="false" das zwar ab, doch je nach Hersteller des Handys
kann die Übertragung von Gerät zu Gerät trotzdem aktiv bleiben. Beides würde die Datenbank mit
jedem Track vom Handy kopieren. walker setzt deshalb beides: allowBackup="false" und
Regeln für die Datenextraktion, die jeden Bereich vom Cloud-Backup und von der Übertragung
zwischen Geräten ausnehmen.
Um fair zu Google zu sein: Ab Android 9 ist die automatische Sicherung Ende-zu-Ende verschlüsselt, geschützt durch die Displaysperre des Handys. Ich wollte die Kopie trotzdem als Datei, die du siehst und selbst verschiebst. Damit ist walker für Backups selbst zuständig.
Backups als verschlüsselte Dateien
Ein Backup ist eine einzige Datei, walker-<date>.walkerbackup. Sie enthält die
Einstellungen, jede Aktivität mit Track und Rohmessungen, die Schritte, die Namen, die du
Routen gegeben hast, die Abzeichen und das Netzwerkprotokoll. Du legst eine Passphrase mit
mindestens 10 Zeichen fest und wählst, wohin die Datei geht: in einen Ordner, auf eine
Speicherkarte oder in einen Cloud-Speicher, über den Speichern-Dialog oder das Teilen-Menü.
So sieht es von außen aus, und davon erzählt der Artikel über GPX-Export und
Backups. Hier geht es darum, was in der Datei
passiert.
Die Datei beginnt mit einem kleinen Header. Er nennt das Verfahren, mit dem der Schlüssel abgeleitet wird, die Chiffre und deren Parameter. Weil jede Datei sie mitträgt, kann ich sie später verschärfen, und alte Dateien lassen sich weiter öffnen.
Der Schlüssel
Der Schlüssel entsteht aus der Passphrase mit Argon2id. Ich habe es gewählt, weil es speicherintensiv ist (memory-hard): Jeder Versuch, die Passphrase zu erraten, kostet Speicher und Zeit.
ist der Schlüssel. ist die Passphrase, genau so, wie du sie eingetippt hast: Ein Leerzeichen am Ende zählt mit. ist das Salt, zufällig und für jede Datei neu. ist der Speicher, den das Verfahren füllt, die Zahl der Durchläufe darüber, die Zahl der Lanes, also der parallelen Stränge, und die Länge des Schlüssels. walkers Einstellungen liegen nahe an der Empfehlung von RFC 9106 für Umgebungen mit wenig Speicher und brauchen auf einem Handy etwa eine Sekunde.
Weil eine Datei ihre Parameter mitbringt, sagt sie walker auch, wie viel Speicher die App belegen soll, und eine bösartige Datei könnte weit mehr verlangen, als ein Handy hat. walker prüft die Parameter deshalb gegen vernünftige Grenzen, bevor die App irgendetwas belegt, und weist eine Datei außerhalb dieser Grenzen als beschädigt ab.
Nach dem Gebrauch überschreibt walker die Passphrase im Speicher, ebenso die Kopien, die egui anlegt. egui ist die UI-Bibliothek, auf der walker aufbaut, und ihr Textfeld führt einen Undo-Verlauf, auch bei einem maskierten Feld. Deshalb löscht walker diesen Verlauf in jedem Frame.
Die Chiffre
Die Chiffre ist XChaCha20-Poly1305, ein AEAD-Verfahren (Authenticated Encryption with Associated Data): Es verschlüsselt und authentifiziert in einem Schritt.
ist der Klartext, hier die Einstellungen und Tracks. ist eine zufällige Nonce mit 192 Bit, eine Zahl, die unter einem Schlüssel nur ein einziges Mal vorkommen darf. sind die zugehörigen Daten (Associated Data) und der Chiffretext, genauso lang wie . ist ein Authentifizierungs-Tag über und . wird authentifiziert, aber nicht verschlüsselt, und walker macht den ganzen Header zu . Ein geänderter Parameter oder ein geändertes Salt scheitert also genauso wie ein geänderter Chiffretext. Eine falsche Passphrase und eine beschädigte Datei ergeben dieselbe Meldung, weil die Chiffre die beiden nicht unterscheiden kann: In beiden Fällen passt der Tag nicht.
Warum 192 Bit? Eine Nonce darf sich unter einem Schlüssel nie wiederholen. Werden Nachrichten unter einem Schlüssel mit zufälligen 192-Bit-Nonces versiegelt, ist die Wahrscheinlichkeit, dass zwei dieselbe Nonce haben, höchstens
Dabei ist die Zahl der Nachrichten und die Zahl der möglichen Nonces. Selbst ergibt weniger als . Beim einfachen ChaCha20-Poly1305 (RFC 8439) hat die Nonce 96 Bit, die Schranke ist , und vier Milliarden Nachrichten erreichen . Für walker spielt die Schranke allerdings kaum eine Rolle: Jede Datei bekommt ein neues Salt, also teilen sich keine zwei Dateien einen Schlüssel.
Streaming
Ein Jahr an Rohmessungen kann Hunderte Megabyte umfassen, und das soll nicht im Speicher liegen. Nach den Einstellungen und Tracks folgen die Rohdateien daher in Blöcken (Chunks) von höchstens 1 MiB, jeder für sich unter demselben Schlüssel versiegelt. Jeder Block ist an seine Position im Datenstrom gebunden, an den Header des Backups, an die Rohdatei, zu der er gehört, und daran, ob er der letzte ist.
Warum sich kein Block verschieben, weglassen oder hinzufügen lässt
Jeder Angriff auf den Datenstrom stößt auf eine dieser Bindungen:
- Geändert: Der Tag passt nicht mehr.
- Verschoben oder vertauscht: Jeder Block ist mit seiner Position versiegelt. An einer anderen Stelle gelesen, scheitert der Tag.
- Umbenannt: Die Kennung der Rohdatei gehört zu dem, was der Tag jedes Blocks abdeckt.
- Weggelassen: Jeder Block nach der Lücke sitzt eine Position zu früh und scheitert, als wäre er verschoben.
- Gekürzt, selbst sauber zwischen zwei Blöcken: walker verlangt den als letzten markierten Block, bevor die Datei endet. Nur der echte letzte Block trägt diese Markierung, und niemand ohne den Schlüssel kann einen weiteren versiegeln.
- Verlängert: Nach dem letzten Block verlangt walker das Dateiende. Ein Byte mehr wird als beschädigt abgewiesen.
- Aus einer anderen Datei eingefügt: Ein anderes Salt bedeutet einen anderen Schlüssel, und jeder Block ist an den Header der Datei gebunden, aus der er stammt.
Das kommt STREAM nahe, der Konstruktion, die Hoang, Reyhanitabar, Rogaway und Vizár 2015 vorgeschlagen haben, um einen Datenstrom in Segmenten zu verschlüsseln. Sie beweisen, dass STREAM sicher ist, sofern das zugrunde liegende AEAD-Verfahren es ist. walkers Schema weicht in Kleinigkeiten davon ab, deshalb nenne ich es nicht STREAM, und einen Beweis für walkers Variante habe ich nicht geschrieben. Das Argument oben stützt sich auf die beiden Tatsachen, auf denen der Beweis für STREAM beruht: Keine zwei Blöcke teilen sich eine Nonce, und der Tag des AEAD-Verfahrens lässt sich nicht fälschen. walkers Tests prüfen jeden Fall der Liste außer dem Einfügen aus einer anderen Datei.
Der Overhead
Der Overhead beträgt ein paar Dutzend Bytes pro Block von 1 MiB: Die Radtour mit 3,2 MB ergibt vier Blöcke, die 228 Bytes hinzufügen, etwa 0,007 %. In einem Test ließ das Versiegeln von zwanzig Dateien mit je 3 MiB den Speicherbedarf der App in der Spitze um 10 MiB steigen, und Versiegeln wie Öffnen dauerten je 0,3 Sekunden.
Wiederherstellen fügt nur hinzu
Eine Wiederherstellung überschreibt nie etwas. Sie zeigt zuerst eine Vorschau: wie viele Aktivitäten die Datei enthält, aus welchem Zeitraum sie stammen und wie viele davon auf diesem Handy neu sind. Dann fügt sie nur hinzu, was fehlt:
- Aktivitäten werden an ihrer Kennung unterschieden. Sie entsteht beim Start der Aufzeichnung und ist daher über alle Handys hinweg eindeutig. Eine Aktivität, die dieses Handy schon hat, wird übersprungen, und die Kopie auf diesem Handy gewinnt.
- Die Einstellungen dieses Handys bleiben, wie sie sind. Sie stehen zwar in der Datei, aber eine Wiederherstellung übernimmt keine davon, abgesehen von zwei Angaben zur Buchführung (siehe unten).
- Schritte kommen ohne den Zählerstand des Schrittzählers, denn der gehört zum Systemstart eines bestimmten Handys. Gezählt wird weiter ab den Tagen, die dieses Handy gezählt hat.
- Das Netzwerkprotokoll wird zusammengeführt, ohne Duplikate.
- Routennamen und Abzeichen werden wie Aktivitäten an ihrer Kennung unterschieden.
Eine Wiederherstellung fügt also die Aktivitäten, Schritt-Tage und Protokolleinträge hinzu, die das Handy noch nicht hat. Bei einer Aktivität entscheidet allein die Kennung, bei einem Schritt-Tag das Datum.
Daraus folgen zwei Eigenschaften, und ich verlasse mich auf beide. Sei das, was das Handy speichert, nachdem die Datei auf die Daten des Handys wiederhergestellt wurde. Die erste: Zweimal wiederherstellen ändert nichts.
Nach der ersten Wiederherstellung ist jede Kennung, jeder Tag und jeder Protokolleintrag aus auf dem Handy, beim zweiten Mal bleibt also nichts hinzuzufügen. Die einzigen Einstellungen, die eine Wiederherstellung ändern darf, sind das Datum des letzten Backups und die Angabe, ob das Handy als eingerichtet gilt, und das nur, wenn das Datum dadurch später wird. Bei der zweiten Wiederherstellung ist das Datum gleich, also bleiben beide, wie sie sind.
Die zweite: Die Backups zweier Handys lassen sich in beliebiger Reihenfolge kombinieren, mit zwei Ausnahmen. Wer erst die eine Datei und dann die andere wiederherstellt, hat danach dieselben Aktivitäten, Tage und Protokolleinträge auf dem Handy wie in umgekehrter Reihenfolge. Die Reihenfolge kann ändern, welche Kopie gewinnt, wenn beide Dateien dieselbe Aktivität oder denselben Tag enthalten und dieses Handy sie noch nicht hat: Es gewinnt die Datei, die zuerst wiederhergestellt wurde. Hat ein Handy diese Wanderung umbenannt, entscheidet die Reihenfolge, welcher Name bleibt. Auch das Datum des letzten Backups kann abweichen, weil es sich nur ändert, wenn eine Datei alles enthält, was das Handy in diesem Moment hat.
Beide Eigenschaften brauchen Kennungen, die über alle Handys hinweg eindeutig sind. Es sind UUIDv7 (RFC 9562) mit etwa 73 Zufallsbits nach dem Startzeitpunkt. Eine Kollision zwischen Handys ist deshalb verschwindend unwahrscheinlich: Zehntausend Aktivitäten, eine Wanderung am Tag über mehr als 27 Jahre, ergeben etwa .
Weil nichts geändert oder gelöscht wird, braucht eine Wiederherstellung keine abschreckende Sicherheitsabfrage. Wer alles ersetzen will, geht zwei bewusste Schritte: Delete all data (alle Daten löschen), dann wiederherstellen.
Der Umzug auf ein neues Handy ist ein Backup auf dem alten und eine Wiederherstellung auf dem neuen, über Backup & restore (Sichern und Wiederherstellen); die Hilfe beschreibt die Schritte. Kartenregionen sind nicht im Backup. Sie sind öffentliche Daten, und das neue Handy lädt sie unter Map packs (die Kartenpakete) erneut herunter.
Ein praktisches Detail für alle, die in SurrealKV wiederherstellen: Eine Transaktion muss in eine Memtable passen, einen Puffer fester Größe im Arbeitsspeicher, und eine einzige Transaktion mit allen Tracks kann darüber hinauswachsen. walker schreibt deshalb zuerst die Tracks in kleinen Portionen und danach die Aktivitäten, die auf sie verweisen. Ein Track wird immer nur über seine Aktivität gelesen, also siehst du in der App alles oder nichts.
Was es kostet
Local-first ist ein Tauschgeschäft, und die Kosten sind echt. Der erste Posten: Es gibt kein Sicherheitsnetz in der Cloud. Ist das Handy weg und gibt es kein Backup, sind die Wanderungen verloren. walker erinnert dich auf der Karte daran, wenn das letzte Backup älter als 30 Tage ist, und diese Frist kannst du ändern. Mehr als erinnern kann die App nicht.
Die Datei und die Passphrase bewahrst du selbst auf. Eine vergessene Passphrase lässt sich nicht wiederherstellen, weder von mir noch von sonst jemandem. Geht ein Backup über das Teilen-Menü an eine andere App, sagt walker „Handed to the app you chose. walker can't tell whether it arrived.“ (An die gewählte App übergeben. walker kann nicht feststellen, ob es angekommen ist.) Das stimmt wörtlich.
walker lebt immer auf genau einem Handy. Es gibt keinen Sync zwischen Geräten: Ein Backup kann Wanderungen von einem Handy auf ein anderes bringen, aber zwei Handys bleiben nicht auf demselben Stand. Sync steht auf der Liste für „vielleicht später“, und nur mit Ende-zu-Ende-Verschlüsselung.
Das Gegenstück zur Rechnung, die mit jeder neuen Person wächst, ist die Art, wie walker bezahlt wird. Es gibt keine Serverkosten, die wieder hereinkommen müssen: Du kaufst walker einmal, ohne Abo, ohne In-App-Käufe und ohne Werbung.
Und das Format bleibt mir für immer. Jede Datei, die walker je geschrieben hat, muss sich weiter öffnen lassen. Eine Änderung am Format erhöht seine Version, und walker liest weiterhin Format 1. Werte eines Enums, die eine ältere walker-Version nicht kennt, fallen auf einen Standardwert zurück, statt einen Fehler auszulösen.
Auch Bugs sind schwerer zu erkennen. Ohne Absturzberichte erfahre ich von einem Problem aus meinen eigenen Wanderungen, aus den Logs eines Handys auf meinem Schreibtisch und von Menschen, die mir schreiben.
Für mich ist genau dieses Tauschgeschäft der Sinn der Sache. Nichts, was ich preisgeben könnte, liegt auf einem Server, weil nichts auf einem Server liegt.