Blog

Sichere Kopplung bei Babyphone Timmy

Wie ECDH, SAS-Zahl und verschlüsselte Signalisierung zusammenarbeiten.

Von Babyphone Timmy · April 2025

Bevor Babyphone Timmy Audio und Video überträgt, müssen sich zwei Geräte finden und einander vertrauen. Diese Kopplung ist der kritischste Moment im ganzen Ablauf. Hier erkläre ich, wie Timmy koppelt, welche Kryptografie dahinter steckt und warum ein Angreifer in der Nähe die Verbindung nicht unbemerkt übernehmen kann.

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

Wenn zwei Geräte sich zum ersten Mal verbinden, gibt es eine zentrale Frage: Spricht Gerät A wirklich mit Gerät B, oder sitzt jemand dazwischen? In der Kryptografie heißt das Man-in-the-Middle-Angriff (MITM).

Timmy löst das mit einem Elliptic Curve Diffie-Hellman (ECDH) Schlüsselaustausch über Firebase und einer visuellen Verifikation durch den Benutzer.

Das folgende Diagramm zeigt den 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 erzeugt jedes Gerät ein ephemeres ECDH-Schlüsselpaar auf der Kurve P-256 (secp256r1):

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

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

Damit sich zwei Geräte finden, nutzt Timmy einen 4-stelligen Code als Treffpunkt. Dieser Code kann automatisch über Nearby Connections (Bluetooth Low Energy) entdeckt oder manuell eingegeben werden. Er hat keinerlei kryptografischen Wert; er sorgt nur dafür, 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. Danach liest jedes Gerät den öffentlichen Schlüssel des anderen aus diesem Dokument.

Entscheidend: Übertragen wird nur der öffentliche Schlüssel. Der private Schlüssel verlässt das Gerät niemals. Wer den Firebase-Verkehr abhört, sieht öffentliche Schlüssel, kann daraus aber das gemeinsame Geheimnis nicht berechnen. Das beruht auf der 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 elliptischer Kurven sorgt dafür, dass beide Berechnungen dasselbe Ergebnis liefern, obwohl jedes Gerät nur den 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 die Zahlen auf beiden Bildschirmen 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. Damit ist der Angriff sichtbar geworden.

Schritt 5: Kopplung abschließen

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 die Komponenten, die am Kopplungs- und Kommunikationsprozess beteiligt sind:

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:#FBF6F0,stroke:#B5734A,stroke-width:2px style PARENT fill:#FBF6F0,stroke:#B5734A,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:#F6E3D2,stroke:#B5734A 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. 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.

Weil 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

Die Schichten greifen ineinander: ECDH schützt den Schlüsselaustausch, die Verifikationszahl schützt vor MITM, AES-256-GCM schützt die Signalisierung und WebRTC schützt die Medien. Ein Angreifer müsste diese Kette an mehreren Stellen brechen, ohne dass Geräte oder Eltern es merken.


Weitere Artikel