April 1, 2021

Luca App Source Code Audit I: der erste Kontakt

Kein rauchender Colt, aber lasst trotzdem mal besser die Finger davon.

Ich lese hier gerade den Source Code der Luca App von culture4life quer (commit 91e012526a58dda35962c6d70a8f60bf4c4c5e9f ). Macht keinen Spaß soweit.

Was direkt negativ auffällt ist, dass in allen Source Dateien der Lizenzhinweis fehlt und auch sonst nur sehr spärlich dokumentiert wurde. Gut, ich bin Schlimmeres gewohnt, aber die Implikation hier ist, dass eine Offenlegung des Quellcodes, entgegen anderslautender Beteuerungen, nicht von Anfang an geplant, sondern nur durch nachträglichen Druck zustande gekommen ist und dann auch nur sehr halbherzig. Dazu passt auch, dass sich in der ursprünglichen temporären “Open Source” Lizenz (commit a30432ec4a01c2ca7ea9ceb26c145e7b620435fc ) folgende kleine Giftpille fand:

 Der Quellcode wird ausschließlich zum Zwecke der Betrachtung für persönliche,
nicht-kommerzielle Zwecke auf nicht-ausschließlicher, nicht-unterlizenzierbarer
und nicht-übertragbarer Basis bereitgestellt. Eine darüberhinausgehende
Einräumung von Rechten erfolgt nicht.


  Der Betrachter darf insbesondere nicht (und darf nicht gestatten, dass Dritte
dies tun) mittelbar oder unmittelbar: (a) den Quellcode anderweitig
vervielfältigen oder verarbeiten; (b) den Quellcode teilen oder öffentlich
wiedergeben;  (c) den Quellcode auf ein öffentliches oder verteiltes Netzwerk
kopieren;
[..]
  Diese Eingeschränkte Lizenz kann von der culture4life GmbH jederzeit und ohne
Vorankündigung widerrufen werden.

Übersetzung: ihr könnt den Quellcode gerne lesen (wenn ihr das könnt), aber öffentlich darüber berichten (ordentlich, mit Zitat und Quellenangabe), dürft ihr nicht - es sei denn, ihr seid scharf darauf, dass wir euch dann die Lizenz entziehen und abmahnen.

Na, hat noch wer Bock eine Geschäftsbeziehung mit einem Anbieter, der so einen Klops raus haut?

Warum muss das Ding eigentlich unbedingt Open Source sein?

Die Erfahrung aus mittlerweile vier Jahrzehnten Microsoft lautet: du setzt auf closed source, wenn…

1. du weißt, dass dein Code Grütze ist.
2. du deine Kunden von dir abhängig machen willst, indem du sie daran hinderst, die Grütze sauber nachzubauen.
3. du planst, mit Kundenbindung als Hebel, überteuerte Lizenzgebühren für Grütze zu verlangen.

Ernsthaft. Warum führen wir diese Diskussion eigentlich immer noch?

Eine Datei, zwei Probleme

Die Lizenz wurde mittlerweile auf GPLv3 geändert, also nutze ich meine neu gewonnenen Freiheiten doch gleich mal, um zu zitieren:

package de.culture4life.luca.network;
// [..]
public class NetworkManager extends Manager {

    private static final String API_BASE_URL_PRODUCTION = "https://app.luca-app.de/api/v3/";
    private static final String API_BASE_URL_STAGING = "https://staging.luca-app.de/api/v3/";
    public static final String API_BASE_URL = BuildConfig.DEBUG ? API_BASE_URL_STAGING : API_BASE_URL_PRODUCTION;
    private static final String USER_AGENT = createUserAgent();

    // [..]
    
    private static String createUserAgent() {
        String appVersionName = BuildConfig.VERSION_NAME;
        String deviceName = Build.MANUFACTURER + " " + Build.MODEL;
        String androidVersionName = Build.VERSION.RELEASE;
        return "luca/" + appVersionName + " (Android " + androidVersionName + ";" + deviceName + ")";
    }

Kommuniziert wird über HTTPS (gut, was auch sonst) und bei dieser Gelegenheit, wird dann halt gleich noch euer Geräteprofil im UserAgent Header mit übertragen. Die Information an sich ist nicht sonderlich sensibel, aber für die Kontaktverfolgung eben auch nicht notwendig (es sei denn natürlich, ihr glaubt ganz fest daran, dass es eine Korrelation zwischen Handymodel und Infektionsrisiko gibt, die gefunden werden kann, wenn man die Daten in 5 Blockchains packt und dann eine KI drüberlaufen lässt).

Drei Dinge kann man an dieser Stelle mit Sicherheit sagen:

  1. Culture4life muss zur Produktverbesserung und Fehlersuche wissen, was ihr für ein Smartphone habt (mir fällt allerdings kein Szenario ein, in dem das relevant wäre) .
  2. In der Marktforschung wird für diese Information Geld bezahlt (“oh, das Gerät ist 1.5 Jahre alt? Ist dann nicht langsam mal Zeit für einen neuen Handyvertrag?”).
  3. Wenn man auch nur halbwegs glaubhaft machen will, dass man Datenschutz ernst nimmt, dann lässt man jede unnötige Information weg!

Der unnötige UserAgent ist ein kleiner Fauxpas. Wesentlich schlimmer ist die Tatsache, dass die Serveradresse fest verdrahtet und die Domain luca-app.de in Privatbesitz ist. Hierdurch entsteht Abhängigkeit. Aus Abhängigkeit folgt, dass der Anbieter Kundenbindung als Hebel nutzen kann, um überteuerte Lizenzgebühren zu verlangen und man obendrein die Arschkarte gezogen hat, wenn die Grütze nicht ordentlich läuft. Hat noch irgendwer Fragen, warum das Ding (vollständig!) Open Source sein muss und warum (wie bei der Corona-Warn-App) Entwickler und Betreiber voneinander getrennt zu sein haben?

Wo ist eigentlich der Servercode?

Luca ist keine App, sondern ein Client/Server System. Die App funktioniert nicht ohne Backend und wenn irgendwo eine Bombe lauert, dann da. Womit wir drei Probleme hätten:

  1. Der Backend code ist (noch?) nicht öffentlich.
  2. Selbst wenn der Backendcode öffentlich wäre, ist das keine Garantie, dass das auch der Code ist, der auf den Servern läuft.
  3. Sollte sich wider erwarten herausstellen, dass culture4life den UserAgent für irgendwas nutzt, wofür sie ihn nicht nutzen sollten (mehr dazu weiter unten), dann müssen wir das halt so hinnehmen.

Vielleicht wäre es an der Stelle eine gute Idee, einen Plan B zu haben, für den Fall, dass app.luca-app.de irgendetwas Dummes zustößt? Momentan hat die culture4Life GmbH ihre Lizenznehmer jedenfalls ziemlich bei den Eiern, weil bereits 1 Millionen Lemminge eine App installiert haben, die genau diesen Server kontaktieren will.

'Vertrau mir', sprach der Skorpion zum Frosch Glückwunsch an Berlin an dieser Stelle. Die culture4life GmbH ist bei euch jetzt systemrelevant. Wenn ihr sie nicht ordentlich füttert, ihr nicht den Bauch pinselt und auch sonst nicht vor Schaden bewahrt, dann fährt sie euch das öffentliche Leben runter. Ich würde sagen, dass ist ein ernsthaftes Sicherheitsproblem, oder wie seht ihr das?

Apropos “etwas Dummes zustoßen”, das Backend läuft nicht auf eigenen Servern, sondern in der Telekom Cloud. Das ist billiger, verspricht eine hohe Ausfallsicherheit (es sei denn, man hat zu wenig Serverkapazität für einen Anne Will Auftritt gekauft - Whoops!) und stellt einen vor das interessante Problem zu begründen, wie man Datenschutz garantieren will, wenn einem die Festplatte nicht gehört. Wer sich auf der Luca Website umsieht, stellt zudem fest, dass man statt einem Smartphone auch einen Schlüsselanhänger nutzen kann. Die FAQ sagt dazu folgendes:

Der Schlüsselanhänger wird teilweise von Gemeinden/Landkreisen vertrieben. Ab Ende April kann der Schlüsselanhänger über unseren Webshop bestellt werden.

Schön. Wozu dann eigentlich noch die App?

Trick 17 mit Selbstüberlistung Der Schlüsselanhänger ist ein einfaches Plastikkärtchen mit aufgedrucktem QR code und einer Registrierungsnummer. Um den QR code zu aktivieren, muss die Registrierungsnummer auf der Luca Website eingegeben und per SMS TAN bestätigt werden.
Hoffen wir also mal, dass Darth Vader, Harry Potter und Micky Maus nicht auf die Idee kommen, einen Schlüsselanhänger auf eine wegwerf Telefonnummer zu registrieren und danach den QR Code im Netz zu teilen.

Tja, macht Luca auf irgendwen den Endruck eines fertigen Systems, oder doch eher einer halbgaren Start-Up Lösung, die mit minimaler Investition versucht auf den Markt zu drängen? Frage für einen Freund.

Zurück zur App

Hier findet sich noch folgendes Schmuckstück:

package de.culture4life.luca.registration;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

import java.util.UUID;

public class RegistrationData {

    @SerializedName("id")
    @Expose
    private UUID id;

    @SerializedName("firstName")
    @Expose
    private String firstName;

    @SerializedName("lastName")
    @Expose
    private String lastName;

    @SerializedName("phoneNumber")
    @Expose
    private String phoneNumber;

    @SerializedName("email")
    @Expose
    private String email;

    @SerializedName("street")
    @Expose
    private String street;

    @SerializedName("houseNumber")
    @Expose
    private String houseNumber;

    @SerializedName("city")
    @Expose
    private String city;

    @SerializedName("postalCode")
    @Expose
    private String postalCode;

    public RegistrationData() {

    }

    @Override
    public String toString() {
        return "RegistrationData{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", phoneNumber='" + phoneNumber + '\'' +
                ", email='" + email + '\'' +
                ", street='" + street + '\'' +
                ", houseNumber='" + houseNumber + '\'' +
                ", city='" + city + '\'' +
                ", postalCode='" + postalCode + '\'' +
                '}';
    }
    
    // [..]
}

Ja Moment mal, hatten wir uns nicht bei der Corona-Warn-App bereits darauf geeinigt, dass Kontaktverfolgung gefälligst anonym zu sein hat, um dann gleich im Sommer 2020 bestätigt zu bekommen, warum wir das wollten, als die Polizei anfing Gästelisten von Restaurants einzusammeln, um Zeugen für Bagatelldelikte zu finden?

Fun fact Nur die Telefonnummer kann/muss per SMS TAN bestätigt werden. Bei dem Rest könnt ihr lügen. Womit sich dann natürlich sofort die Frage stellt, warum diese Daten überhaupt erhoben werden.

Wahrscheinlich sind wir uns alle einig darüber, dass obiger Datensatz für Adresshändler Gold wert ist (insbesondere, wenn noch die Geräteinformationen aus dem UserAgent , sowie GeoDaten hinzukommen) und damit nichts in den Händen der culture4life GmbH verloren hat. Dummerweise landen sie genau dort, allerdings verschlüsselt und das ist so auch im Code nachvollziehbar:

package de.culture4life.luca.network.endpoints;

// [..]

public interface LucaEndpointsV3 {

    /*
        Keys
     */

    @GET("keys/daily/current")
    Single<JsonObject> getDailyKeyPairPublicKey();

    @GET("keys/issuers/{issuerId}")
    Single<JsonObject> getKeyIssuer(@Path("issuerId") String issuerId);

    // [..]
    
    /*
        Users
     */

    @POST("users")
    @Headers("Content-Type: application/json")
    Single<JsonObject> registerUser(@Body UserRegistrationRequestData data);

    @PATCH("users/{userId}")
    @Headers("Content-Type: application/json")
    Completable updateUser(@Path("userId") String userId, @Body UserRegistrationRequestData data);

    // [..]
}

Das übertragene JSON Objekt basiert auf RegistrationData und sieht auf den ersten Blick auch recht vernünftig aus (auf den zweiten Blick wundert man sich eventuell etwas, warum zusätzlich der publicKey mit übertragen wird):

package de.culture4life.luca.network.pojo;

import com.google.gson.annotations.SerializedName;

/**
 * Example:
 *
 * <pre>
 * {
 *   "data": "u1GUhaHK3lCY+AJuc8m5ZN0ncE+Yypc7o4VeYavV8pbwbFM+5mpOWTzhisCbsLcUiApARPrHAaEyT75lVA/hdRUEJ4Z6p/uinEkb9N6KJ4fgQYK2PoYYhaSRdapTH3ETYXkWVmxdFBBDYKTZV1eaCBYCgZqF3Ydx1Pxxt9TMaMCFimUVW2CyiZXbofWhnVfXIBnvEFHnfgQbJjxWCZ2IP4JoCT28Rmvi/AHC4PGs2hysLv7WbXE9IUEnLw7PvplK",
 *   "iv": "ZM+ygFEw7YJgqizwNE/k2A==",
 *   "publicKey": "BIMFVAOglk1B4PIlpaVspeWeFwO5eUusqxFAUUDFNJYGpbp9iu0jRHQAipDTVgFSudcm9tF5kh4+wILrAm3vHWg=",
 *   "signature": "MEUCIQCEbDo2u2IZ2mEQV5xLpZH7m9Xy6yum61eXsqHZo+k3wQIgAnCteTv20ERfJm7vP+cfoUZwbmTKK/i9SmmNW42eB9k=",
 *   "keyId": 0
 * }
 * </pre>
 */
public class UserRegistrationRequestData {
  // [..]
}

Entschlüsseln können nur die Gesundheitsämter und hier wird es lustig:

  1. Wenn ein Münchner an einer Party in Berlin teilnimmt, welches Gesundheitsamt sollte dann entschlüsseln können?
  2. In Gesundheitsämtern findet man eher Faxgeräte als Mitarbeiter, die sich mit Kryptographie auskennen. Aber was genau macht man eigentlich, wenn der Praktikant den Schlüssel rausträgt?

Die Antwort auf beide Fragen lautet, dass alle Gesundheitsämter sich denselben private key teilen(!) und dieser regelmäßig ausgetauscht wird:

As Health Departments are federated in Germany, they need to share a common keypair (namely the daily keypair). This keypair is generated and distributed among all Health Departments on a daily basis. For the distribution, we use the HDEKPs (that are uniquely owned by each health department) to encrypt the daily keypair’s private key for each Health Department. These encrypted private key objects are then uploaded to luca.

Man kann den Satz 10 mal lesen, ohne dass er dabei klarer wird. Der entsprechende Code würde sich vermutlich im Backend code finden, welcher allerdings nicht öffentlich ist, also spekuliere ich hier mal, was da gemacht wird:

Ihr zieht euch die App aufs Handy und gebt dann erstmal einen Haufen unnötiger Daten ein (nichts davon ist sonderlich sensibel, es sei denn, ihr reagiert sensibel auf Werbeanrufe). Sobald ihr mit der App dann irgendwo einen Checkin durchführt, holt sich diese den daily public key von app.luca-app.de , verschlüsselt damit eure Angaben und lädt sie hoch. Wenn ein Gesundheitsamt dass dann entschlüsseln will, lässt es sich von app.luca-app.de den zugehörigen daily private key geben und schickt euch ein Fax.

Das würde zumindest erklären, warum der Server code nicht offen liegt.

Kryptographie 101 Wenn Alice (Lemming mit installierter App) und Bob (Mitarbeiter des Gesundheitsamtes mit Fax Gerät) sicher miteinander kommunizieren wollen, dann sollten sie dazu vielleicht nicht das daily keypair nutzen, welches sie von Charlie (culture4life) zur Verfügung gestellt bekommen haben.

Ich weiß nicht wie es euch geht, aber ich fühle mich hier gerade extrem verarscht…

UPDATE: Laut Konzept wird das daily keypair nicht von Luca bereit gestellt, sondern nur verteilt. Genutzt wird das daily keypair des Amtes, dass sich an dem Tag als erstes anmeldet. Das macht die Sache allerdings auch nicht besser, da sich Luca selbst als Gesundheitsamt im Verteiler registrieren und somit als berechtigter Absender oder Empfänger auftreten könnte.

Die wirkliche Gefahr lauert nicht im Code

Kontaktverfolgungsapps hätten eine gewisse Daseinsberechtigung, wenn sie dafür genutzt würden, unvermeidbare Begegnungen abzusichern. Aber genau dafür wurde Luca eben nicht entwickelt! Luca soll es ermöglichen, “endlich wieder mehr zu wagen” und treibt damit den grundlegenden Designfehler aller Kontaktverfolgungsapps auf die Spitze: Ansteckungen erstmal zuzulassen, in der Hoffnung, dass man dann schon (irgendwie) alle Infizierten finden und unter Quarantäne stellen kann, bevor sie die Krankheit weiter geben.

Luca ist kein Weg aus der Pandemie heraus, sondern ein Spiel mit dem Feuer!

Update: Hier geht’s zum nächstem Teil.