← Blog

Die Rohmessungen einer Radtour, 3.201.331 Bytes, so versiegelt das Backup sieBlock 0: 1.048.576 BBlock 1: 1.048.576 BBlock 2: 1.048.576 BBlock 3: 55.603 B4 Blöcke, jeder einzeln versiegelt. Zusatz: 228 Bytes, 0,007 %Wer einen Block verschiebt, weglässt oder hinzufügt, kann nichts mehr entschlüsseln
Wie ein Backup die rohen Satellitenmessungen einer Radtour versiegelt: vier Blöcke von höchstens 1 MiB, jeder einzeln versiegelt und an seinen Platz gebunden. Das Versiegeln fügt insgesamt 228 Bytes hinzu.
Unter der Haube

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:

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.

K=Argon2id(P,S; m,t,p,ℓ)K = \text{Argon2id}(P, S;\ m, t, p, \ell)
(1)

KK ist der Schlüssel. PP ist die Passphrase, genau so, wie du sie eingetippt hast: Ein Leerzeichen am Ende zählt mit. SS ist das Salt, zufällig und für jede Datei neu. mm ist der Speicher, den das Verfahren füllt, tt die Zahl der Durchläufe darüber, pp die Zahl der Lanes, also der parallelen Stränge, und ℓ\ell 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.

(C,T)=XChaCha20-Poly1305(K,N,A,M)(C, T) = \text{XChaCha20-Poly1305}(K, N, A, M)
(2)

MM ist der Klartext, hier die Einstellungen und Tracks. NN ist eine zufällige Nonce mit 192 Bit, eine Zahl, die unter einem Schlüssel nur ein einziges Mal vorkommen darf. AA sind die zugehörigen Daten (Associated Data) und CC der Chiffretext, genauso lang wie MM. TT ist ein Authentifizierungs-Tag über AA und CC. AA wird authentifiziert, aber nicht verschlüsselt, und walker macht den ganzen Header zu AA. 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 qq Nachrichten unter einem Schlüssel mit zufälligen 192-Bit-Nonces versiegelt, ist die Wahrscheinlichkeit, dass zwei dieselbe Nonce haben, höchstens

q(q−1)2⋅2−192<q22193\frac{q(q-1)}{2} \cdot 2^{-192} < \frac{q^2}{2^{193}}
(3)

Dabei ist qq die Zahl der Nachrichten und 21922^{192} die Zahl der möglichen Nonces. Selbst q=264q = 2^{64} ergibt weniger als 2−652^{-65}. Beim einfachen ChaCha20-Poly1305 (RFC 8439) hat die Nonce 96 Bit, die Schranke ist q2/297q^2/2^{97}, und vier Milliarden Nachrichten erreichen 2−332^{-33}. 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:

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:

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 R(D,F)R(D, F) das, was das Handy speichert, nachdem die Datei FF auf die Daten DD des Handys wiederhergestellt wurde. Die erste: Zweimal wiederherstellen ändert nichts.

R(R(D,F),F)=R(D,F)R(R(D, F), F) = R(D, F)
(4)

Nach der ersten Wiederherstellung ist jede Kennung, jeder Tag und jeder Protokolleintrag aus FF 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 5×10−155 \times 10^{-15}.

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.

walker Zeichnet Spaziergänge, Wanderungen und Radtouren mit Offline-Karten auf. Nichts verlässt dein Handy. Bald bei Google Play.