Blog

So koppelt Timmy sicher

Der Algorithmus hinter der Verifikationszahl und warum MITM-Angriffe scheitern.

Von Babyphone Timmy · April 2025

Bevor Babyphone Timmy Audio und Video übertragen kann, müssen sich zwei Geräte finden und gegenseitig vertrauen. Dieser Vorgang — die Kopplung — ist der sicherheitskritischste Moment im gesamten Ablauf. In diesem Artikel erklären wir Schritt für Schritt, wie die Kopplung funktioniert, welches kryptografische Verfahren dahinter steckt und warum ein Angreifer in der Nähe die Verbindung nicht unbemerkt kapern kann.

Das Problem: Woher weiß mein Gerät, mit wem es spricht?

Wenn zwei Geräte sich zum ersten Mal verbinden, stehen sie vor einer grundlegenden Frage: Woher weiß Gerät A, dass es wirklich mit Gerät B spricht — und nicht mit einem Angreifer, der sich dazwischengeschaltet hat? Dieses Problem ist in der Kryptografie als Man-in-the-Middle-Angriff (MITM) bekannt.

Timmy löst dieses Problem durch einen Elliptic Curve Diffie-Hellman (ECDH) Schlüsselaustausch über Firebase, kombiniert mit einer visuellen Verifikation durch den Benutzer.

Das folgende Diagramm zeigt den gesamten Kopplungsablauf auf einen Blick:

sequenceDiagram
    autonumber
    participant A as 📱 Gerät A
    participant F as ☁️ Firebase
    participant B as 📱 Gerät B

    Note over A,B: Phase 1 — Entdeckung

    A->>A: ECDH-Schlüsselpaar erzeugen (P-256)
    B->>B: ECDH-Schlüsselpaar erzeugen (P-256)

    alt Auto-Kopplung (Nearby BLE)
        A-->>B: BLE-Broadcast: SBM:XKQM
        B-->>A: BLE-Broadcast: SBM:R7NP
        Note over A,B: Niedrigerer Code gewinnt → bestimmt Creator/Joiner
    else Manuelle Kopplung
        A->>A: 4-Zeichen-Code anzeigen
        Note right of A: Benutzer liest Code ab
        B->>B: Benutzer gibt Code ein
    end

    Note over A,B: Phase 2 — ECDH-Schlüsselaustausch

    A->>F: Öffentlichen Schlüssel (PubA) schreiben
    B->>F: Öffentlichen Schlüssel (PubB) schreiben
    F-->>B: PubA lesen
    F-->>A: PubB lesen

    Note over A,B: Phase 3 — Gemeinsames Geheimnis

    A->>A: sharedSecret = ECDH(privA, PubB)
    B->>B: sharedSecret = ECDH(privB, PubA)
    Note over A,B: Beide berechnen identisches 32-Byte-Geheimnis

    A->>A: SAS = SHA-256("sas:" + sort(PubA,PubB) + secret) → 2-stellige Zahl
    B->>B: SAS = SHA-256("sas:" + sort(PubA,PubB) + secret) → 2-stellige Zahl

    Note over A,B: Phase 4 — Visuelle Verifikation

    A->>A: SAS anzeigen: 42
    B->>B: SAS anzeigen: 42
    Note over A,B: 👤 Benutzer vergleicht Zahlen auf beiden Bildschirmen

    A->>A: Benutzer bestätigt ✓
    B->>B: Benutzer bestätigt ✓

    Note over A,B: Phase 5 — Schlüsselableitung

    A->>A: pairingKey = SHA-256("pair:" + secret)
    B->>B: pairingKey = SHA-256("pair:" + secret)
    A->>A: docKey = SHA-256("doc:" + pairingKey)
    A->>A: encKey = SHA-256("enc:" + pairingKey)

    Note over A,B: ✅ Gekoppelt — alle Signalisierung mit AES-256-GCM verschlüsselt
      

Vollständiges Kopplungsprotokoll — editierbare Quelle: docs/diagrams/pairing-sequence.mmd

Schritt 1: Jedes Gerät erzeugt ein Schlüsselpaar

Beim Öffnen des Kopplungsbildschirms generiert jedes Gerät ein ephemeres ECDH-Schlüsselpaar auf der Kurve P-256 (secp256r1):

Die Schlüssel werden mit einem kryptografisch sicheren Zufallsgenerator (Random.secure()) erzeugt und sind nur für diesen einen Kopplungsvorgang gültig. Bei jedem erneuten Versuch werden neue Schlüssel generiert.

Schritt 2: Austausch der öffentlichen Schlüssel über Firebase

Damit sich zwei Geräte finden können, nutzt Timmy einen 4-stelligen Code als Treffpunkt. Dieser Code kann automatisch über Nearby Connections (Bluetooth Low Energy) entdeckt oder manuell eingegeben werden. Der Code hat keinerlei kryptografischen Wert — er dient ausschliesslich dazu, dass beide Geräte dasselbe Firebase-Firestore-Dokument finden.

Sobald beide Geräte den Code kennen, schreiben sie ihren öffentlichen ECDH-Schlüssel in ein gemeinsames Firestore-Dokument. Jedes Gerät liest dann den öffentlichen Schlüssel des anderen aus diesem Dokument.

Entscheidend: Nur der öffentliche Schlüssel wird übertragen. Der private Schlüssel verlässt das Gerät niemals. Ein Angreifer, der den Firebase-Verkehr abhört, sieht nur die öffentlichen Schlüssel — und kann daraus das gemeinsame Geheimnis nicht berechnen. Dies basiert auf der mathematischen Schwierigkeit des Elliptic Curve Discrete Logarithm Problems (ECDLP).

Schritt 3: Berechnung des gemeinsamen Geheimnisses

Sobald beide Geräte den öffentlichen Schlüssel des jeweils anderen aus Firebase gelesen haben, berechnen sie unabhängig voneinander dasselbe gemeinsame Geheimnis (Shared Secret):

sharedSecret = ECDH(meinPrivaterSchlüssel, remoteÖffentlicherSchlüssel)
             → 32 Bytes (identisch auf beiden Geräten)

Die Mathematik der elliptischen Kurven garantiert, dass beide Berechnungen dasselbe Ergebnis liefern, obwohl jedes Gerät nur seinen eigenen privaten Schlüssel und den öffentlichen Schlüssel des anderen kennt.

Schritt 4: Die Verifikationszahl (SAS)

Aus dem gemeinsamen Geheimnis wird ein Short Authentication String (SAS) abgeleitet — eine zweistellige Zahl, die auf beiden Geräten angezeigt wird:

hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
zahl = (hash[0] × 256 + hash[1]) mod 100   → 00 bis 99

Beide Geräte zeigen dieselbe Zahl an — zum Beispiel 42. Der Benutzer vergleicht visuell, ob die Zahlen auf beiden Bildschirmen übereinstimmen, und bestätigt auf jedem Gerät einzeln.

Warum ein Angreifer dies nicht fälschen kann

Ein Man-in-the-Middle müsste sich zwischen beide Geräte setzen und die öffentlichen Schlüssel in Firebase manipulieren. Konkret müsste er:

  1. Die echten öffentlichen Schlüssel in Firebase abfangen und ersetzen
  2. Eigene Schlüsselpaare mit jedem Gerät austauschen
sequenceDiagram
    autonumber
    participant A as 📱 Gerät A
    participant M as 🕵️ Angreifer (MITM)
    participant B as 📱 Gerät B

    Note over A,B: Angreifer fängt den Firebase-Schlüsselaustausch ab

    A->>A: Schlüsselpaar erzeugen (privA, PubA)
    B->>B: Schlüsselpaar erzeugen (privB, PubB)
    M->>M: ZWEI Schlüsselpaare erzeugen (privM1, PubM1) + (privM2, PubM2)

    A->>M: PubA in Firebase schreiben
    M->>M: PubA durch PubM1 ersetzen
    M->>B: B liest PubM1 (hält es für PubA)

    B->>M: PubB in Firebase schreiben
    M->>M: PubB durch PubM2 ersetzen
    M->>A: A liest PubM2 (hält es für PubB)

    Note over A,B: Jedes Gerät berechnet ein ANDERES Shared Secret

    A->>A: secret_A = ECDH(privA, PubM2)
    M->>M: secret_A = ECDH(privM2, PubA)
    M->>M: secret_B = ECDH(privM1, PubB)
    B->>B: secret_B = ECDH(privB, PubM1)

    Note over A,M: secret_A ≠ secret_B

    A->>A: SAS_A = SHA-256("sas:" + sort(PubA,PubM2) + secret_A) → 73
    B->>B: SAS_B = SHA-256("sas:" + sort(PubM1,PubB) + secret_B) → 18

    rect rgb(255, 230, 230)
        Note over A,B: ❌ Benutzer sieht UNTERSCHIEDLICHE Zahlen!
        A->>A: Anzeige: 73
        B->>B: Anzeige: 18
        Note over A,B: 👤 Benutzer bemerkt Unterschied → bricht Kopplung ab
    end

    Note over A,B: 🛡️ Angriff erkannt — MITM kann SAS nicht fälschen (P = 1/100)
      

MITM-Erkennung durch SAS-Abweichung — editierbare Quelle: docs/diagrams/mitm-detection.mmd

In diesem Fall berechnet der Angreifer mit Gerät A ein Shared Secret S_A und mit Gerät B ein anderes Shared Secret S_B. Da S_A ≠ S_B, berechnen die Geräte unterschiedliche Verifikationszahlen.

Der Angreifer kann die Zahlen nicht gezielt gleich machen, da:

Der Benutzer sieht unterschiedliche Zahlen auf den Bildschirmen und bricht die Kopplung ab. Der Angriff ist gescheitert.

Schritt 5: Kopplung abschliessen

Erst wenn der Benutzer auf beiden Geräten die Verifikation bestätigt hat, wird die Kopplung abgeschlossen:

  1. Aus dem Shared Secret wird ein 64-Zeichen Pairing-Key (256 Bit) abgeleitet: SHA-256("pair:" + sharedSecret) → pairingKey
  2. Aus dem Pairing-Key wird der Firestore-Dokumentschlüssel abgeleitet: SHA-256("doc:" + pairingKey) → documentKey
  3. Ein weiterer Hash liefert den AES-256-GCM-Schlüssel für die verschlüsselte Signalisierung: SHA-256("enc:" + pairingKey) → encryptionKey
  4. Beide Geräte speichern denselben Pairing-Key und navigieren zur Modusauswahl

Ab diesem Moment sind alle weiteren Verbindungsversuche (Signalisierung über Firestore, WebRTC-Aufbau) mit dem gemeinsamen AES-256-GCM-Schlüssel verschlüsselt. Der Pairing-Key wird niemals an das Backend gesendet — nur sein SHA-256-Hash dient als Dokumentkennung.

Systemarchitektur

Das folgende Diagramm zeigt alle Komponenten, die am Kopplungs- und Kommunikationsprozess beteiligt sind, und wie sie zusammenspielen:

flowchart TB
    BABY["📱 Kind-Handy
Baby-Modus"] PARENT["📱 Eltern-Handy
Eltern-Modus"] BABY <==>|"🔒 WebRTC Peer-to-Peer · DTLS-SRTP
Audio · Video · DataChannel"| PARENT BABY -.-|"🔵 Bluetooth LE · Nearby
Auto-Entdeckung"| PARENT subgraph FIREBASE["☁️ Firebase (Google Cloud)"] direction LR AUTH["🪪 Anonyme
Authentifizierung"] FS["📄 Firestore
Kopplung + Signalisierung"] CF["⚡ Cloud Functions
getTurnCredentials"] end BABY <-->|"🔐 AES-256-GCM verschlüsselt
SDP · ICE · ECDH-Schlüssel"| FS FS <-->|"🔐 AES-256-GCM verschlüsselt
SDP · ICE · ECDH-Schlüssel"| PARENT BABY -.->|Token| AUTH PARENT -.->|Token| AUTH STUN["📡 STUN-Server
stun.cloudflare.com:3478"] TURN["🔄 TURN-Relay
lokal oder Cloudflare"] BABY & PARENT -->|Kurzlebige Zugangsdaten| CF CF -->|lokal zuerst, Cloudflare-Fallback| TURN BABY & PARENT -.->|NAT-Traversal| STUN BABY -.->|"Relay-Fallback"| TURN TURN -.->|"Relay-Fallback"| PARENT style BABY fill:#f0f7ff,stroke:#6BAFB2,stroke-width:2px style PARENT fill:#f0f7ff,stroke:#6BAFB2,stroke-width:2px style FIREBASE fill:#fff5f5,stroke:#E9B44C,stroke-width:2px style AUTH fill:#E9B44C,stroke:#2B2D42 style FS fill:#E9B44C,stroke:#2B2D42 style CF fill:#E9B44C,stroke:#2B2D42 style STUN fill:#D4EEEF,stroke:#6BAFB2 style TURN fill:#7BC47F,stroke:#2B2D42

Systemarchitektur-Übersicht — editierbare Quelle: docs/diagrams/pairing-architecture.mmd

Die Kommunikationspfade im Detail:

Fallback: Manuelle Codeeingabe

Falls Bluetooth nicht verfügbar ist (z.B. auf älteren Geräten), kann der 4-stellige Code auch manuell eingegeben werden. Wichtig: Die manuelle Eingabe nutzt denselben ECDH-Schlüsselaustausch und dieselbe SAS-Verifikation wie die automatische Kopplung. Der einzige Unterschied ist, dass der Code nicht per BLE entdeckt, sondern vom Benutzer abgelesen und eingetippt wird.

Da der ECDH-Schlüsselaustausch in beiden Fällen über Firebase stattfindet, ist die Sicherheit identisch. Der 4-stellige Code dient nur als Treffpunkt — die eigentliche Verschlüsselung basiert auf dem 256-Bit-Schlüssel aus dem ECDH-Verfahren.

Zusammenfassung

Schutzmechanismus Schützt gegen
ECDH-Schlüsselaustausch (P-256) Abhören des Firebase-Verkehrs
Ephemere Schlüsselpaare Forward Secrecy — vergangene Kopplungen bleiben sicher
Visuelle Verifikationszahl (SAS) Man-in-the-Middle (MITM) bei Firebase-Kopplung
SHA-256-Hash als Dokumentkennung Code-Extraktion aus Firestore
AES-256-GCM-Verschlüsselung Abhören der Signalisierungsdaten
Beidseitige Bestätigung Einseitige Kopplung ohne Benutzerwissen
DTLS-SRTP (WebRTC) Abhören von Audio/Video

Jede Sicherheitsebene ergänzt die anderen. ECDH schützt den Schlüsselaustausch. Die Verifikationszahl schützt vor MITM. AES-256-GCM schützt die Signalisierung. WebRTC schützt die Kommunikation. Zusammen bilden sie eine Kette, die ein Angreifer an keiner Stelle unbemerkt durchbrechen kann.


Weitere Artikel