Luca App Source Code Audit VI: Game Over!

Die gute Nachricht lautet, man kann Luca datenschutzkonform nutzen. Die schlechte könnte den Steuerzahler verunsichern (holt euch schonmal nen Eimer).

Im Sicherheitskonzept findet sich folgender Abschnitt zur Telefonnummer Validierung mittels SMS TAN:

Guest’s Contact Data is encrypted in luca’s client application before being uploaded to the Luca Server. Hence, luca cannot validate any personal data provided by the Guest. On the other hand, Health Departments are dependent on valid Contact Data to be able to contact Guests if necessary.

This poses a trade-off between data validity and personal data protection (cf. O1). Therefore, luca merely implements a client-side phone number validation via an SMS TAN process before registering a Guest with their encrypted Contact Data. As with any other client-side check this can be circumvented by manipulating the client software.

Der letzte Satz ist Dynamit. Da müssen wir uns mal im Code anschauen, wie das SMS TAN Verfahren in der App implementiert ist. Zunächst die Anfrage, für eine neue TAN:

package de.culture4life.luca.registration;

// [..]

public class RegistrationManager extends Manager {

// [..]
    /**
     * Request a TAN for the given phone number.
     *
     * @param formattedPhoneNumber Phone number in E.164 (FQTN) format
     */
    public Single<String> requestPhoneNumberVerificationTan(String formattedPhoneNumber) {
        return Single.defer(() -> {
            JsonObject message = new JsonObject();
            message.addProperty("phone", formattedPhoneNumber);
            return networkManager.getLucaEndpoints().requestPhoneNumberVerificationTan(message)
                    .doOnSubscribe(disposable -> Timber.i("Requesting TAN for %s", formattedPhoneNumber))
                    .map(jsonObject -> jsonObject.get("challengeId").getAsString());
        });
    }

// [..]
}

Was hier in wunderbar lesbarem ReactiveX beschrieben steht, lässt sich auch ganz einfach auf der Kommandozeile simulieren:

$  curl -X POST -H "Content-Type: application/json"  -d '{"phone":"+49XXXXXXXXX"}' https://app.luca-app.de/api/v3/sms/request
{"challengeId":"2618630a-AAAA-BBBB-CCCC-c40fb1e16c78"}

Die Methode nimmt eine E.164 Telefonnummer entgegen, veranlasst serverseitig den Versand einer SMS und liefert eine challengeId zurück, welche dann im zweitem Schritt, zusammen mit der TAN zur Bestätigung übermittelt werden muss. Das wäre(n) dann folgende Methode(n) in derselben Klasse:

    public Completable verifyPhoneNumberWithVerificationTan(String verificationTan, String challengeId) {
        return verifyPhoneNumberWithVerificationTan(verificationTan, Collections.singletonList(challengeId));
    }

    public Completable verifyPhoneNumberWithVerificationTan(String verificationTan, List<String> challengeIds) {
        return Completable.defer(() -> {
            JsonObject jsonObject = new JsonObject();
            JsonArray challengeIdArray = new JsonArray(challengeIds.size());
            for (String challengeId : challengeIds) {
                challengeIdArray.add(challengeId);
            }
            jsonObject.add("challengeIds", challengeIdArray);
            jsonObject.addProperty("tan", verificationTan);
            return networkManager.getLucaEndpoints().verifyPhoneNumberBulk(jsonObject);
        });
    }

Fällt irgendwem an der Stelle etwas auf? Richtig! die Methode liefert lediglich ein io.reactivex.rxjava3.core.Completable (API doc) zurück, d.h. einen Status, ob die serverseitige Überprüfung von TAN und challengeId erfolgreich war.

Warum eine Liste von challengeIds? Es ist durchaus möglich, dass eine SMS nicht oder verspätet zugestellt, bzw. versehentlich vom Anwender beim Empfang gelöscht werden. Sobald der Nutzer eine challengeId bestätigt, braucht man auf die anderen nicht mehr zu warten und kann sie gleich mit löschen, um ein überquellen der Datenbank zu vermeiden.

Das SMS TAN Verfahren hat keinen Rückgabewert, der als Eingabe für irgendeine kryptographische Routine verwendet wird (und das Konzept fordert das auch so!). Die verifyPhoneNumberWithVerificationTan() Methode gibt einfach ihr OK und das wars (könnte man also auch einfach auskommentieren). Danach darf die App beliebige Kontaktdaten verschlüsseln und hochladen. Telefonnummer Validierung und Upload der verschlüsselten Kontaktdaten sind voneinander völlig getrennte Prozesse!

Ja, Holla die Waldfee! Sagt mal, culture4life, bei euch bastelt doch auch Briegel der Busch mit, oder?

Der ganze Crypto Klimbim ist nichts anderes als smoke and mirrors! Eine Blendgranate, die mit unglaublicher Komplexität versucht davon abzulenken, dass keine der Angaben wirklich von Luca überprüft wird, bzw. überprüft werden kann oder darf. Die gesamte Systemsicherheit beruht damit einzig und alleine auf der Geheimhaltung des Quellcodes, d.h. der (idiotischen) Hoffnung, dass niemand das Binary reverse engineered kriegt, bzw. der (noch idiotischeren) Annahme, dass niemand das Ding als Mock nachbauen kann.

Man muss es sich mal auf der Zunge zergehen lassen: culture4life ist mit dem Wissen angetreten, dass die Zettelwirtschaft in Restaurants nicht funktioniert, weil Gäste falsche Angeben machen und hat mit Luca das exakt selbe Problem nochmal als App nachgebaut, nur dass…

  • jetzt (mit ein paar Codeänderungen), die Polizei deutschlandweit Gästelisten einsammeln kann,
  • dafür aber nicht mehr in der Lage ist, vor Ort Gästelisten mit Ausweisen abzugleichen,
  • die ganze überflüssige Grütze den Steuerzahler schon wieder einen zweistelligen Millionenbetrag (Tendenz steigend) gekostet hat.

Wir hätten an der Stelle auch einfach eine Registrierung per Emailaddresse (der Rest optional) machen können und der Käse wäre gegessen gewesen. Einfacher, billiger, weniger peinlich. Der ganze Crypt-Fu wäre überflüssig und jeder hätte direkt verstanden, worauf er sich einlässt und wie er sich mit einer simplen Weiterleitung schützen kann, falls gewünscht.

Irgendwo lauert doch hier Count Olaf als Endgegner…