NINA

10.06.2020 Jonas L. App-Kritik

Bei der Warn-App NINA gibt es auf technischer Ebene einiges zu entdecken.

Die Warn-App NINA (Notfall-Informations- und Nachrichten-App) warnt Sie deutschlandweit vor Gefahren, auf Wunsch auch für Ihren aktuellen Standort. Die App wird vom Bundesamt für Bevölkerungsschutz und Katastrophenhilfe (BBK) bereitgestellt.


Das letzte Update (die Version 3.2.1.2720) ist vom 27. Mai, es sieht also betreut aus. Daher kommt die App in eine virtuelle Maschine mit mitmproxy.

Nach dem Öffnen der App gibt es eine Einführung:

Einführung in der NINA-App

Zur kontinuierlichen Verbesserung der App werden anonymisierte Nutzungsdaten erfasst. Im Einstellungsmenü können Sie dies abschalten.


Quelle: NINA-App direkt nach dem ersten Aufruf

Wenn man diesen Text sieht wurden schon “Nutzungsdaten” übermittelt. Die App kann man nicht offline (und auch nicht auf Geräten ohne Google-Apps) einrichten, sodass hier eine Möglichkeit zur Verbesserung besteht.

Dann ein Blick auf das Netzwerk:

Netzwerkverkehr direkt nach dem Starten der NINA-App

Die App registriert sich mit Google Firebase für Absturzberichte, Push-Nachrichten und Remote-Config - erforderlich wäre es an dieser Stelle noch nicht gewesen.

Ich möchte die Remote-Config hervorheben, weil die besonders interessant ist:

Remote-Config der NINA-App

Man kann die App scheinbar mit android_app_update_immediate_version_code fernsperren bzw. sperren bis der Nutzer ein Update installiert - solange man es nur dann verwendet, wenn es wirklich erforderlich ist keine schlechte Idee. Die Rolle von android_police_enabled verstehe ich nicht. Die android_sync_frequency ist bei dem Wert 10 wahrscheinlich die clientseitige Caching-Dauer in Minuten. Warum man das für iOS und Android unabhängig voneinander konfigurieren kann verstehe ich nicht. Da hier auch iOS-Werte sind, wird wahrscheinlich auch in der iOS-Version das Firebase-Zeug verwendet.

Neben bzw. nach der Registrierung bei Google gibt es eine beim eigenen Backend:

Registrierung der NINA-App beim eigenen Backend

Der token stand davor in der Antwort von einer Registrierungsanfrage, die an das Backend vom Firebase Cloud Messaging ging. Wenn man die Base64-Zeichenfolge der authorization dekodiert ergibt es appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f. Wenn man die APK-Datei als ZIP-Datei betrachtet und entpackt, dann kann man mit grep danach suchen:

$ grep -r "appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f" .
$ grep -r "appUser" .
Übereinstimmungen in Binärdatei ./resources.arsc
$ grep -r "40e8e440-f515-49bf-b759-fea7bcd1d56f" .
Übereinstimmungen in Binärdatei ./resources.arsc

Zusammenhängend gibt es die beiden Angaben nicht, aber die beiden Teilwerte stehen in der APK - sie sind also wahrscheinlich bei allen Installationen gleich. Das Senden einer hardkodierten Zeichenfolge ist eine Verschleierungstechnik, wenn auch keine besonders wirkungsvolle.

Die ID in der URL ist hingegen bei jeder Installation anders - die wird bei einigen Anfragen immer mitgesendet. Die ID 7146dcb7-4c1c-4ffc-a6b1-206f2c93d422 aus der URL hingegen ist nicht direkt in der APK enthalten, also wahrscheinlich individuell.

Sinnvolle Inhalte gibt es bis hierher noch nicht:

sinnlose Informationen zu NINA-Updates (da schon die neuste Version verwendet wird) in der Leitung

Das ist aber auch kein Wunder, weil ich ja immer noch Nichts in der App gemacht habe. Nach dem Durchtippen durch das Intro fragt die App nach Daten für ein Onboarding:

NINA ruft Daten für ein Onboarding ab

Aber es scheint kein Onboarding zu geben:

NINA bekommt kein Omboarding

Aber die App braucht es scheinbar nicht bzw. es ist eindeutig, was ich tun soll:

NINA Onboarding in der App

Wenn ich das mache bekomme ich eine Google-Maps-Karten-Einbindung zu sehen. Wenn ich “Halle” eingebe bekomme ich “Halle (Saale)” vorgeschlagen - ohne eine Anfrage dazu. Aber wenn man Halle auswählt wird zoomt die Karte an die passende Stelle, wodurch Kartendaten abgefragt werden und Google indirekt erfährt, welchen Ort ich gewählt habe.

Wenn ich die Auswahl bestätige gibt es 3 Anfragen. Erste Anfrage:

Das NINA-Backend erfährt (scheinbar) meine gewählte Region

Hier erfährt das NINA-Backend, was ich gewählt habe im Zusammenhang mit der Installations-ID; höchstwahrscheinlich für Push-Benachrichtigungen.

Die zweite Anfrage geht an https://warnung.bund.de/api/dashboard/150020000000.json und hat keine individuellen IDs - die URL kann man auch direkt im Browser öffnen und die Inhalte sind gut lesbar.

Überschriften der Inhalte in der NINA-App

Das weist sehr große Ähnlichkeiten mit dem auf, was man in der App zu sehen bekommt:

Überschriften in der NINA-App

Die dritte Anfrage geht nach https://warnung.bund.de/api/appdata/gsb/systemmeldungen/DE/systemmeldungen_v1_android.json. wo es die Neuerungen gibt. In dem Fall ein 304 Not Modified, weil der Client es schon im Cache hat.

Nun hatte die App die Nummern 150020000000 und 7413 zu Halle, aber die kamen nie über die Leitung. Also waren sie wahrscheinlich schon in der App enthalten. Da gibt es in der entpackten APK unter assets/database/NINA2015.sqlite eine Datenbank und dort sind die beiden IDs:

Datenbank aus der NINA-App

An dieser Stelle kann man nun das Nutzungsstatistik-Opt-Out machen. Damit das wirkt soll man die App neu starten. Das mache ich - irgendeine Netzwerkanfrage im Sinne von “Lösche die bisherigen Daten” kann ich nicht entdecken - nur Anfragen an Google Maps mit Gerätemodell und App-Namen in einem Binärdatenhaufen, die es auch davor schon so oder so ähnlich gab.


Dann öffne ich mal den “Sicherheitshinweis”:

"Sicherheitshinweis" in der NINA-App

Google Maps sieht man in der Oberfläche und auch beim Netzwerkverkehr. Interessant sind eher die Anfragen an das NINA-Backend - alle haben keine individuellen IDs in den Anfragen und können daher wieder direkt im Browser betrachtet werden:

Unter https://warnung.bund.de/api/warnings/mow.DE-ST-MD-SE022-20200527-22-001.json gibt es die eigentliche Warnung. Hier sind HTML-Fragmente als JSON-Strings enthalten (z.B. ein <br/> für einen Zeilenumburch). mow.DE-ST-MD-SE022-20200527-22-001 ist die ID, die bei der Warnungsliste mitgesendet wurde.

Unter https://warnung.bund.de/api/warnings/mow.DE-ST-MD-SE022-20200527-22-001.geojson ist der markierte Bereich der Karte. Ich konnte es mit dem Programm Gnome Karten öffnen, auch wenn dort Teile der Markierung gefehlt haben. Mit dem Browser ansehen kann men es nicht direkt, man muss es erst herunterladen.

Jetzt werden die allgemeinen Inhalte geladen - unter https://warnung.bund.de/api/appdata/gsb/logos/logos.json gibt es eine Liste von Logos von einigen Organisationen, die wahrscheinlich Meldungen in NINA einstellen können. Da gibt es eine ID, einen Namen, ein Dateiname für ein Logo, eine Ausrichtung und ein Timestamp der letzten Änderung. In diesem Fall wurde kein Logo geladen, aber wahrscheinlich konnte man das nichtvorhandensein eines Logos erst nach dem Laden der Liste feststellen, weil man zur Meldung nur die ID des Senders bekommt und man nicht weiß, ob er ein Logo hat.

Als letztes gibt es https://warnung.bund.de/api/appdata/gsb/labels/de_labels.json. Da erfährt man, was alles passieren kann. In der Warnung gab es den Event-Code BBK-EVC-040 und hier erfährt man dazu, dass das Infektionsgefahr bedeutet. Bei einigen Codes gibt es aber auch Handlungsaufforderungen wie Suchen Sie höher liegende Gebiete auf und warten Sie ab. Flutwellen können noch über Stunden hinweg auftreten.. oder Nehmen Sie JETZT die Jodtabletten gemäß Packungsbeilage ein.. Man hofft, all diese Anweisungen nie zu bekommen, aber scheinbar sind zumindest die Formulierungen schon vorbereitet.

Was dabei auffällt: Das (Logoliste + Anweisungstexte) sind Daten, die sich selten ändern, aber ohne Update änderbar sein sollen. Das klingt nach Remote-Config und ist es auch. Und an dieser Stelle klappt das auch ohne die Hilfe von Firebase.


Zurück auf der Startseite der App will ich natürlich etwas über Corona erfahren - dann gibt es eine Anfrage an https://warnung.bund.de/api/appdata/covid/covidinfos/DE/covidinfos.json und die dort genannten Bilder werden jeweils nachgeladen. Es gibt sogar eine extra Corona-Karte - Google Maps mit https://warnung.bund.de/api/appdata/covid/covidmap/DE/covidmap.json befüllt. Ein Beispieleintrag:

{
    "cases": 43,
    "cases_per_100k": 48.0425455845549,
    "deaths": 3,
    "ewz": 89504,
    "lastUpdate": "09.06.2020, 00:00 Uhr",
    "properties": {
        "fillColor": "#EFF3FF",
        "fillOpacity": 0.5,
        "strokeColor": "#474747",
        "strokeOpacity": 1.0,
        "strokeWeight": 1
    },
    "rs": "01001"
}

Das sind die Fallzahlen je Gebiet (und Angaben, wie das Gebiet auf der Karte dargestellt werden soll). Dann gibt es auch noch “aktuelle Informationen” bzw. den Covid-Ticker, wie es bei der Backendkommunikation heißt - Übersicht unter https://warnung.bund.de/api/appdata/covid/covidticker/DE/covidticker.json, für Einträge z.B. https://warnung.bund.de/api/appdata/covid/covidticker/DE/tickermeldungen/14179192.json, wobei da am Ende die id aus der Übersicht steht. Hier sind sogar ganze HTML-Dokumente als JSON-String kodiert.


Dann gibt es noch die Notfalltipps. Die sind alle unter https://warnung.bund.de/api/appdata/gsb/notfalltipps/DE/notfalltipps.json abgelegt - hier als großes Dokument, das die Überschriften und die Texte als HTML-Fragmente enthält. Nur die Bilder werden bei Bedarf nachgeladen.


Unter dem Menüpunkt “Legende” wird https://warnung.bund.de/api/appdata/gsb/eventCodes/eventCodes.json abgerufen. Die Struktur ist etwas redundant - als Bildname ist immer der Event-Code mit der Suffix .png festgelegt. Die zugehörigen Symbole gibt es z.B. unter https://warnung.bund.de/api/appdata/gsb/eventCodes/BBK-EVC-002.png. Interessanterweise gibt es hier nur ein zentrales lastModificationDate und kein Änderungszeitpunkt je Bild.


Die Menüpunkte “Datenschutzerklärung” und “Impressum” weichen vom Rest ab - hier gibt es die HTML-Fragmente https://warnung.bund.de/api/appdata/gsb/html/DE/app/datenschutz.html und https://warnung.bund.de/api/appdata/gsb/html/DE/app/impressum.html, aber anders als sonst ohne JSON-Hülle.


Vorbildlich ist die Liste der enthaltenen Open-Source-Komponenten. Die ist sehr ausführlich.


Damit endet die Laufzeit-Betrachtung. In der entpackten APK gibt es im Assets-Ordner noch andere interessante Elemente.

Beispielsweise gibt es die Datei channelsuche.json, die Einträge wie z.B. den folgenden hat:

"15002": {
  "NAME": "Kreisfreie Stadt Halle (Saale)",
  "KREISFREIE_STADT": 1,
  "LOWERLEFT": "11.8571, 51.4029",
  "UPPERRIGHT": "12.0884, 51.5432"
},

Warum das nicht mit in der sqlite-Datenbank steht? Eventuell eine Altlast, die nicht mehr verwendet wird.

$ grep -r "NINA2015.sqlite" .
Übereinstimmungen in Binärdatei ./classes.dex
./META-INF/CERT.SF:Name: assets/database/NINA2015.sqlite
./META-INF/MANIFEST.MF:Name: assets/database/NINA2015.sqlite
$ grep -r "channelsuche.json" .
./META-INF/CERT.SF:Name: assets/channelsuche.json
./META-INF/MANIFEST.MF:Name: assets/channelsuche.json

Der Name der sqlite-Datenbank kommt in classes.dex vor, die channelsuche nicht.

Interessanterweise gibt es auch ein alte Version vom Impressum in der APK-Datei:

ein altes Impressum in der NINA-APK-Datei

Dazu gibt es nur ein Treffer in classes.dex und der sieht wie folgt aus:

das einzige Vorkommen der Zeichenfolge "impressum.html"

Also noch eine Dateileiche.

Insgesamt gibt es aber einen großen Inhalt in der APK-Datei:

große Datenbanken in der NINA-App

Warum lädt man das nicht zur Laufzeit herunter und ermöglicht damit eine Remote Config? Inzwischen ist der Play Store in der Lage, nur die Änderungen an APK-Dateien herunterzuladen, sodass das hier zumindest keine Verschwendung von Datenvolumen ist.


In den Antwortkopfzeilen war bisher server: myracloud ein auffälliger Wert - dahinter verbirgt sich https://www.myrasecurity.com/de/. Wenn man die IP-Adresse von warnung.bund.de im Browser direkt eingibt erscheint auch das Logo von Myra Security und der Schriftzug “Protected”.

Auf https://www.myrasecurity.com/de/ gibt es 3 Stichwörter: Abwehr, Performance und Compliance. Das entspricht fast dem aktuellen Marketing von Cloudflare mit Security, Performance and Reliability.

Gesetzliche und interne Vorgaben zu IT-Sicherheit und Datenschutz erfordern auditierte Prozesse. Myra ist qualifizierter KRITIS-Sicherheitsdienstleister, BSI-zertifiziert und Ihr Compliance-Garant für strengste Anforderungen.


Bei einer Integration vom Google-Tag-Manager, einer Anfrage an https://www.cloudflare.com/cdn-cgi/trace und dem Verzicht auf HSTS-Preloading und einer Content-Security-Policy kommen mir da mindestens Zweifel auf zu der IT-Sicherheit und dem Datenschutz bei der Marketingabteilung. Selbst wenn es bei den anderen Abteilungen besser wäre hinterlässt das bei mir keinen guten Eindruck.


Das Backend von push.warnung.bund.de sah interessant aus, daher spiele ich jetzt damit:

$ torify curl -X PUT -H 'Content-Type: application/json' --data '{"token":"invalid"}' -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/address/gcm/abcde
{"code":"OK","statusMessage":"OK","ok":true}
# es geht, auch wenn die Geräte-ID nicht wie sonst aussieht

$ torify curl -X PUT -H 'Content-Type: application/json' --data '{"token":"invalid"}' -u 'appUser:41e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/address/gcm/abcde
# mit einem anderen "Passwort" geht es nicht

$ torify curl -X PUT -H 'Content-Type: application/json' --data '{"token":"invalid"}' -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/address/gcm/
{"timestamp":"2020-06-10T09:05:47.986+0000","status":500,"error":"Internal Server Error","message":"Request does not contain a service name, illegal request..","path":"/v1/nina3/address/gcm/"}
# ohne ID geht es nicht

$ torify curl -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
{"preferences":[]}
$ torify curl -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcdef
{"code":"NOT_FOUND","statusMessage":"Device: abcdef could not be found, illegal request..","ok":false}
# Die Registrierung hat also funktioniert ... unter dem Namen konnte ich die Einstellungen lesen, unter einem anderen nicht

$ torify curl -X PUT -H 'Content-Type: application/json' --data '{"preferences":[{"name":"a","type":"STRING","value":"Input-Validierung ..."}]}' -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
{"code":"OK","statusMessage":"OK","ok":true}
$ torify curl -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
{"preferences":[{"name":"a","type":"STRING","value":"Input-Validierung ..."}]}
# mein völlig sinnloser Wert wurde gespeichert

$ torify curl -X PUT -H 'Content-Type: application/json' --data '{"preferences":[{"name":"a","type":"STRINGS","value":"Input-Validierung ..."}]}' -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
# keine Ausgabe ...
$ torify curl -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
{"preferences":[{"name":"a","type":"STRING","value":"Input-Validierung ..."}]}
# und Nichts gespeichert

$ torify curl -X PUT -H 'Content-Type: application/json' --data '{"preferences":[{"name":"a","type":"INT","value":"Input-Validierung ..."}]}' -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
# keine Ausgabe ...
$ torify curl -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
{"preferences":[{"name":"a","type":"STRING","value":"Input-Validierung ..."}]}
# und Nichts gespeichert

$ torify curl -X PUT -H 'Content-Type: application/json' --data '{"preferences":[{"name":"b","type":"INT","value":"Input-Validierung ..."}]}' -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
# keine Ausgabe ...
$ torify curl -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
{"preferences":[{"name":"a","type":"STRING","value":"Input-Validierung ..."}]}
# und wieder Nichts gespeichert

$ torify curl -X PUT -H 'Content-Type: application/json' --data '{"preferences":[{"name":"b","type":"INTEGER","value":"Input-Validierung ..."}]}' -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
{"timestamp":"(entfernt)","status":500,"error":"Internal Server Error","message":"For input string: \"Input-Validierung ...\"","path":"/v1/nina3/preference/abcde"}
# jetzt gab es mal eine Fehlermeldung ...
$ torify curl -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/preference/abcde
{"preferences":[{"name":"a","type":"STRING","value":"Input-Validierung ..."}]}
# und wieder Nichts gespeichert

$ torify curl -X PUT -H 'Content-Type: application/json' --data '{"token":""}' -u 'appUser:40e8e440-f515-49bf-b759-fea7bcd1d56f' https:/push.warnung.bund.de/v1/nina3/address/gcm/abcdefg
# keine Ausgabe => der Token darf nicht leer sein

Also gibt es eine Input-Validierung, wobei man diese noch geringfügig ausbauen könnte. Wieso dann ein “Passwort” verlangt wird kann ich nicht nachvollziehen.


Die Erkenntnisse dieser Analyse würden einen alternativen Open-Source-Client ermöglichen, wobei man dort auf den Push und damit den komischsten Teil der API verzichten könnte. Eine bessere Lösung wäre es natürlich, den Firebase-Zwang abzuschaffen (ohne/ mit verzögertem Push, wenn man kein Firebase nutzen möchte), Google Maps optional zu machen (mit der Alternative, auf Karten zu verzichten oder einen alternativen Kartendienst zu verwenden) und es als Open Source zu veröffentlichen. Einige Aspekte waren auffällig, aber das Programm sieht nicht nach einer schlechten Codequalität aus.