Wie Tutanota Google’s FCM durch ihr eigenes Benachrichtigungssystem ersetzte

Wie in Diese Woche in F-Droid 17 erwähnt, ist Tutanota nun auf F-Droid.

In diesem Sonderbeitrag erzählt uns Ivan von Tutanota, wie es dazu kam.

Hi, ich bin Ivan und ich entwickle Tutanota, um dabei mitzuhelfen das Internet der Zukunft aufzubauen, in dem unser Recht auf Privatsphäre respektiert wird. Ich glaube, dass Datenschutz kein Luxus für Reiche und Computererfahrene sein sollte, es sollte ein grundlegendes Menschenrecht sein.

GCM (oder, wie es jetzt heißt, FCM, Firebase Cloud Messaging) ist ein google-eigener Dienst. Wir bei Tutanota verwendeten FCM für unsere alte Android-App. Bedauerlicherweise beinhaltet FCM Google’s Tracking Code zu Analysezwecken, die wir nicht nutzen wollten. Und wesentlich wichtiger: Um FCM nutzen zu können, müssen alle Benachrichtigungsdaten an Google übermittelt werden. Es müssen außerdem ihre proprietären Bibliotheken verwendet werden. Aufgrund der Bedenken hinsichtlich Datenschutz und Sicherheit, die damit natürlich verbunden sind, versandten wir keine Informationen in den Benachrichtigungsmeldungen mit der alten App (was, verständlicherweise, zu Beschwerden von unseren Anwendern führte). Deshalb meldete die Push-Benachrichtigung in der alten App lediglich, dass eine neue Nachricht angekommen sei ohne jeden Verweis auf die E-Mail selbst oder den Posteingang, in dem die Nachricht eingegangen war.

FCM ist ziemlich komfortabel zu verwenden, über die Jahre hat Google an Android Änderungen durchgeführt, die es schwerer machten, ihren Dienst für Benachrichtigungen nicht zu nutzen. Auf der anderen Seite würde uns die Abschaffung des Google-Benachrichtigungsdienstes davon befreien, dass unsere Anwender dazu gezwungen sind, Google Play Services auf ihren Geräten zu haben.

Die Herausforderung Google’s FCM zu ersetzen

Tutanota-Apps sind Libre Software und wir wollten unsere Android-App auf F-Droid veröffentlichen. Wir wollten, dass unsere Anwender in der Lage sind, Tutanota mit jedem ROM und auf jedem Gerät zu verwenden, ohne Überwachung durch einen Drittanbieter wie Google. Wir entschieden, die Herausforderung anzunehmen und unseren eigenen Push-Benachrichtungsdienst zu programmieren.

Als wir damit begannen, unser Push-System zu entwickeln, hatten wir verschiedene Ziele vor Augen:

  • es muss sicher sein
  • es muss schnell sein
  • es muss energiesparend sein

Wir recherchierten, wie andere (Signal, Wire, Conversations, Riot, Facebook, Mastodon) ähnliche Probleme gelöst hatten. Wir überdachten verschiedene Möglichkeiten, dazu gehörten WebSockets, MQTT, Server Sent Events und HTTP/2 Server Push.

Ersatz von FCM durch SSE

Wir legten uns auf SSE (Server Sent Events) fest, weil es als einfache Lösung nahelag. Damit meine ich, “leicht zu implementieren, leicht auszutesten”. Die Fehlerbereinigung bei dieser Art von Dingen kann große Kopfschmerzen bereiten, somit sollte man diesen Faktor nicht unterschätzen. Ein weiteres Argument, das für SSE sprach, war die relative Energieeffizienz: Wir brauchten keine Upstream-Nachrichten und eine dauerhafte Verbindung war nicht unser Ziel.

Also, was ist SSE?

SSE ist eine Internetschnittstelle, die es einem Server ermöglicht, Ereignisse an verbundene Clients zu senden. Es ist eine relativ alte API, die meiner Meinung nach brachliegt. Ich hatte von SSE noch nie gehört, bis ich mir das föderale Netzwerk Mastodon ansah: Sie nutzen SSE zur Aktualisierung der Zeitleiste in Echtzeit und das funktioniert wunderbar.

Das Protokoll selbst ist sehr einfach und ähnelt dem guten alten Polling: Der Client öffnet eine Verbindung und der Server hält sie offen. Der Unterschied zum klassischen Polling ist, dass wir die Verbindung für mehrere Ereignisse offen halten. Der Server kann Ereignisse und Datennachrichten senden; sie werden lediglich durch neue Zeilen getrennt. Somit muss der Client nur eine Sache tun, eine Verbindung mit langer Auszeit öffnen und den Datenstrom in einer Dauerschleife einlesen.

SSE passt besser zu unseren Anforderungen als WebSocket (es ist kostengünstiger und fließt schneller zusammen, weil es kein Duplex ist). Wir haben mehrere Chat-Apps betrachtet, die versuchen, WebSocket für Push-Benachrichtigungen zu nutzen, und es scheint nicht so energieeffizient zu sein.

Wir hatten bereits einige Erfahrung mit WebSocket und wir wussten, dass Firewalls dauerhaft bestehende Verbindungen nicht mögen. Um dies zu lösen, verwendeten wir für SSE dieselbe Hilfskonstruktion wie für WebSocket: Wir senden alle paar Minuten “Herzschlag”-Leernachrichten. Wir machten dieses Intervall von Server-Seite einstellbar und randomisierten es, um den Server nicht zu überlasten.

Mehrfach-Konten-Unterstützung wirft Extraaufgaben auf

Es sollte bekannt sein, dass die Tutanota-App einen Multi-Account-Support hat, und der stellte eine Herausforderung für uns dar: Wir wollten nur eine Verbindung pro Gerät offen halten. Nach einigen Iterationen, fanden wir das Design, das uns zufriedenstellte. Jedes Gerät besitzt nur einen Identifikator. Bei Eröffnung der Verbindung sendet der Client die Liste der Benutzer, für die er Benachrichtigungen erhalten möchte. Der Server gleicht diese Liste gegen Benutzeraufzeichnungen ab und filtert die ungültigen aus.

Benutzer können einen Benachrichtigungs-Token aus ihren Einstellungen löschen, aber das würde keine anderen Logins auf diesem Gerät beeinträchtigen. Zusätzlich mussten wir einen Zustellungsüberwachungsmechanismus für den Erhalt einer Benachrichtigung konstruieren. Unglücklicherweise entdeckten wir, dass unser Server nicht in der Lage ist, einen Verbindungsabbruch zu ermitteln, somit mussten wir von der Client-Seite Bestätigungen verschicken.

Um Benachrichtigungen zu erhalten, nutzen wir die Android-Fähigkeiten zu unserem Vorteil. Wir betreiben einen Hintergrunddienst, der die Verbindung zum Server offen hält, ähnlich dem, was der FCM-Prozess macht. Eine weitere Schwierigkeit wurde durch den Doze-Modus verursacht, der in Android M eingeführt wurde. Doze, das sich nach einer Zeitspanne der Inaktivität einschaltet, hindert, unter anderen Dingen, Hintergrundprozesse ins Netz zu gelangen. Wie Sie sich vorstellen können, hindert es unsere App am Empfangen von Benachrichtigungen.

Wir entschärfen dieses Problem, indem wir die Anwender bitten, eine Ausnahme für die Akku-Optimierung für unsere App zu machen. Es funktioniert einigermaßen gut. Ein ähnliches Problem, allerdings nicht von Doze abhängig, sind anbieterspezifische Akku-Optimierungen. Um die Akkulaufzeit ihrer Geräte zu verlängern, aktivieren Telefonhersteller, wie Xiaomi, standardmäßig strenge Akku-Optimierungen. Glücklicherweise können Anwender sie deaktivieren, aber das müssen wir noch besser vermitteln.

Ein weiteres Problem wurde durch die Android O-Änderungen ausgelöst. Eine von ihnen sind Hintergrundprozesseinschränkungen: Wenn deine App für den Anwender nicht sichtbar ist, werden deine Hintergrundprozesse angehalten und du hast keine Möglichkeit neue zu starten.

Zunächst dachten wir, wir können das lösen, indem wir eine dauerhafte Benachrichtigung mit minimaler Priorität einblenden, die im Benachrichtigungsauszug aber nicht in der Statusleiste sichtbar ist. Das funktionierte für Oreo nicht: Wenn du versuchst einen Hintergrunddienst zu starten und die Minimalpriorität für seine Benachrichtigung verwendest, wird die Benachrichtigungspriorität in eine höhere Prioritätsstufe (Immer sichtbar) umgewandelt und zusätzlich blendet das System eine weitere dauerhafte Benachrichtigung ein: “App X verbraucht Akku”.

Wir planten ursprünglich, den Nutzern zu erklären, wie sie diese dauerhaften Benachrichtigungen ausblenden können, aber das war kein tolles Anwendererlebnis, so mussten wir eine bessere Lösung finden. Wir nutzten den Android Job-Mechanismus aus, um unseren Dienst regelmäßig (wenigstens alle 15 Minuten) zu starten, und wir versuchen auch, ihn danach am Leben zu halten. Wir führen manuell keine WakeLocks durch – das System macht das für uns. Wir konnten die Dauerbenachrichtigungen ganz und gar verwerfen. Selbst wenn die Benachrichtigungen manchmal eine kleine Verspätung haben, werden sie immer ankommen und die E-Mails sind sofort da.

Am Ende hatten wir einiges an Arbeit zu leisten, aber die war es absolut wert. Unsere neue App ist noch eine Beta-Version aber dank der IO-Entsperrung, sind wir in der Lage tausende simultaner Verbindungen ohne Probleme zu unterhalten. Wir haben unsere Anwender von der “Google Play Services“-Pflicht erlöst. Schlussendlich kann jeder die Tutanota-App auf F-Droid erhalten. Das System kombiniert nun beides: gute Energieeffizienz und Geschwindigkeit.

Schlussgedanke: Jeder Nutzer sollte die Möglichkeit haben, für jede App einen “Benachrichtigungsanbieter” auszuwählen

Wäre es nicht wunderbar, wenn der Anwender einfach einen “Push-Benachrichtigungsanbieter” in den Telefoneinstellungen wählen könnte und das OS regelt all diese schwierigen Details von selbst? So müsste nicht jede App, die nicht durch den Plattformeigentümer kontrolliert werden möchte, das System neu erfinden? Es könnte zwischen der App und dem App-Server End-zu-End-verschlüsselt sein. Es gibt keine wirklichen technischen Schwierigkeiten damit, aber so lange unsere Systeme von Großkonzernen kontrolliert werden, die das nicht zulassen, müssen wir es selbst regeln.