Blog

Bezpečné párování v Baby Monitor Timmy

Jak spolu fungují ECDH, číslo SAS a šifrovaná signalizace.

Než Baby Monitor Timmy začne přenášet zvuk a video, musí se dvě zařízení najít a vzájemně si důvěřovat. Tento krok párování je nejdůležitějším momentem celého procesu. Vysvětlím zde, jak Timmy zařízení páruje, jaká kryptografie za tím stojí a proč útočník v okolí nemůže spojení nepozorovaně převzít.

Problém: Jak moje zařízení ví, s kým komunikuje?

Když se dvě zařízení připojují poprvé, hlavní otázka zní: komunikuje zařízení A opravdu se zařízením B, nebo je mezi nimi někdo další? V kryptografii se tomu říká útok man-in-the-middle (MITM).

Timmy to řeší pomocí výměny klíčů Elliptic Curve Diffie-Hellman (ECDH) přes Firebase v kombinaci s vizuálním ověřením uživatelem.

Následující diagram ukazuje celý postup párování v kostce:

sequenceDiagram
    autonumber
    participant A as 📱 Device A
    participant F as ☁️ Firebase
    participant B as 📱 Device B

    Note over A,B: Phase 1 — Discovery

    A->>A: Generate ECDH key pair (P-256)
    B->>B: Generate ECDH key pair (P-256)

    alt Auto-Pairing (Nearby BLE)
        A-->>B: BLE broadcast: SBM:XKQM
        B-->>A: BLE broadcast: SBM:R7NP
        Note over A,B: Lower code wins → determines creator/joiner
    else Manual Pairing
        A->>A: Display 4-char code
        Note right of A: User reads code
        B->>B: User enters code
    end

    Note over A,B: Phase 2 — ECDH Key Exchange

    A->>F: Write public key (PubA) to meeting doc
    B->>F: Write public key (PubB) to meeting doc
    F-->>B: Read PubA
    F-->>A: Read PubB

    Note over A,B: Phase 3 — Shared Secret

    A->>A: sharedSecret = ECDH(privA, PubB)
    B->>B: sharedSecret = ECDH(privB, PubA)
    Note over A,B: Both compute identical 32-byte secret

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

    Note over A,B: Phase 4 — Visual Verification

    A->>A: Display SAS: 42
    B->>B: Display SAS: 42
    Note over A,B: 👤 User compares numbers on both screens

    A->>A: User confirms ✓
    B->>B: User confirms ✓

    Note over A,B: Phase 5 — Key Derivation

    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: ✅ Paired — all future signaling encrypted with AES-256-GCM
      

Kompletní sekvence párovacího protokolu — upravitelný zdroj: docs/diagrams/pairing-sequence.mmd

Krok 1: Každé zařízení vytvoří pár klíčů

Po otevření obrazovky párování si každé zařízení vytvoří dočasný pár klíčů ECDH na křivce P-256 (secp256r1):

Klíče se vytvářejí pomocí kryptograficky bezpečného generátoru náhodných čísel (Random.secure()) a jsou platné jen pro tento jediný pokus o párování. Pro každý nový pokus se vytvoří nové klíče.

Krok 2: Výměna veřejných klíčů přes Firebase

Aby se dvě zařízení našla, používá Timmy 4znakový kód jako společný bod setkání. Tento kód lze automaticky zjistit pomocí Nearby Connections (Bluetooth Low Energy) nebo zadat ručně. Nemá žádnou kryptografickou hodnotu; pouze zajistí, že obě zařízení najdou stejný dokument ve Firebase Firestore.

Jakmile obě zařízení znají kód, každé zapíše svůj veřejný klíč ECDH do sdíleného dokumentu Firestore. Poté každé zařízení z tohoto dokumentu načte veřejný klíč druhého zařízení.

Důležité: odesílá se pouze veřejný klíč. Soukromý klíč zařízení nikdy neopustí. Kdokoli sleduje provoz Firebase, vidí veřejné klíče, ale nemůže z nich vypočítat sdílené tajemství . To vychází z obtížnosti problému diskrétního logaritmu na eliptické křivce (ECDLP).

Krok 3: Výpočet sdíleného tajemství

Jakmile obě zařízení zjistí veřejný klíč toho druhého, nezávisle vypočítají stejné sdílené tajemství:

sharedSecret = ECDH(myPrivateKey, remotePublicKey)
             → 32 bytes (identical on both devices)

Matematika eliptických křivek zaručuje, že oba výpočty dají stejný výsledek, přestože každé zařízení zná jen svůj soukromý klíč a veřejný klíč druhého zařízení.

Krok 4: Ověřovací číslo (SAS)

Ze sdíleného tajemství se odvodí krátký ověřovací řetězec (SAS) — dvouciferné číslo zobrazené na obou zařízeních:

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

Obě zařízení zobrazí stejné číslo — například 42. Uživatel vizuálně zkontroluje, zda se čísla na obou obrazovkách shodují, a pak potvrzení provede na každém zařízení zvlášť.

Proč to útočník nemůže zfalšovat

Útočník typu man-in-the-middle by musel zachytit výměnu klíčů ve Firebase. Konkrétně by musel:

  1. Nahradit skutečné veřejné klíče uložené v dokumentu Firestore svými vlastními
  2. Navázat s každým zařízením samostatné sdílené tajemství
sequenceDiagram
    autonumber
    participant A as 📱 Device A
    participant M as 🕵️ Attacker (MITM)
    participant B as 📱 Device B

    Note over A,B: Attacker intercepts the Firebase key exchange

    A->>A: Generate key pair (privA, PubA)
    B->>B: Generate key pair (privB, PubB)
    M->>M: Generate TWO key pairs (privM1, PubM1) + (privM2, PubM2)

    A->>M: Write PubA to Firebase
    M->>M: Replace PubA with PubM1
    M->>B: B reads PubM1 (thinks it is PubA)

    B->>M: Write PubB to Firebase
    M->>M: Replace PubB with PubM2
    M->>A: A reads PubM2 (thinks it is PubB)

    Note over A,B: Each device computes a DIFFERENT 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: ❌ User sees DIFFERENT numbers!
        A->>A: Display: 73
        B->>B: Display: 18
        Note over A,B: 👤 User notices mismatch → cancels pairing
    end

    Note over A,B: 🛡️ Attack detected — MITM cannot force SAS match (P = 1/100)
      

Odhalení man-in-the-middle pomocí neshody SAS — upravitelný zdroj: docs/diagrams/mitm-detection.mmd

V tomto případě útočník vypočítá sdílené tajemství S_A se zařízením A a jiné sdílené tajemství S_B se zařízením B. Protože S_A ≠ S_B, zařízení vypočítají různá ověřovací čísla.

Útočník nemůže zařídit, aby se čísla shodovala, protože:

Uživatel na obrazovkách uvidí různá čísla a párování zruší. V tu chvíli je útok odhalen.

Krok 5: Dokončení párování

Párování se dokončí až poté, co uživatel potvrdí ověření na obou zařízeních :

  1. Ze sdíleného tajemství se odvodí 64znakový párovací klíč (256 bitů) : SHA-256("pair:" + sharedSecret) → pairingKey
  2. Klíč dokumentu se odvodí jako SHA-256("doc:" + pairingKey) a slouží jako klíč dokumentu Firestore
  3. Šifrovací klíč se odvodí jako SHA-256("enc:" + pairingKey) a poskytuje klíč AES-256-GCM pro šifrovanou signalizaci
  4. Obě zařízení uloží stejný párovací klíč a přejdou k výběru režimu

Od této chvíle jsou všechny další pokusy o připojení (signalizace přes Firestore, nastavení WebRTC) šifrované sdíleným klíčem AES-256-GCM. Párovací klíč se nikdy neodesílá do backendu; jako identifikátor dokumentu se používá pouze jeho hash SHA-256.

Architektura systému

Následující diagram ukazuje komponenty zapojené do párování a komunikace:

flowchart TB
    BABY["📱 Baby Phone
Baby Mode"] PARENT["📱 Parent Phone
Parent Mode"] BABY <==>|"🔒 WebRTC Peer-to-Peer · DTLS-SRTP
Audio · Video · DataChannel"| PARENT BABY -.-|"🔵 Bluetooth LE · Nearby
Auto-Discovery"| PARENT subgraph FIREBASE["☁️ Firebase (Google Cloud)"] direction LR AUTH["🪪 Anonymous
Authentication"] FS["📄 Firestore
Pairing + Signaling"] CF["⚡ Cloud Functions
getTurnCredentials"] end BABY <-->|"🔐 AES-256-GCM encrypted
SDP · ICE · ECDH keys"| FS FS <-->|"🔐 AES-256-GCM encrypted
SDP · ICE · ECDH keys"| PARENT BABY -.->|Token| AUTH PARENT -.->|Token| AUTH STUN["📡 STUN server
stun.cloudflare.com:3478"] TURN["🔄 TURN relay
local or Cloudflare"] BABY & PARENT -->|Short-lived credentials| CF CF -->|local first, 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

Přehled architektury systému — upravitelný zdroj: docs/diagrams/pairing-architecture.mmd

Komunikační cesty podrobně:

Záložní možnost: Ruční zadání kódu

Pokud Bluetooth není k dispozici (např. u starších zařízení), lze 4znakový kód zadat i ručně. Ruční zadání používá stejnou výměnu klíčů ECDH i stejné ověření SAS jako automatické párování. Jediný rozdíl je v tom, že kód uživatel přečte a zadá místo toho, aby byl zjištěn přes BLE.

Protože výměna klíčů ECDH v obou případech probíhá přes Firebase, je zabezpečení stejné. 4znakový kód slouží jen jako bod setkání; skutečné šifrování vychází z 256bitového klíče odvozeného z ECDH.

Shrnutí

Bezpečnostní mechanismus Chrání před
Výměna klíčů ECDH (P-256) Odposlechem provozu při výměně klíčů
Dočasné páry klíčů Dopředné utajení — minulá párování zůstávají v bezpečí
Vizuální ověřovací číslo (SAS) Man-in-the-middle (MITM) během výměny klíčů
Hash SHA-256 jako klíč dokumentu Získáním kódu z Firestore
Šifrování AES-256-GCM Odposlechem signalizačních dat
Potvrzení na obou stranách Jednostranným párováním bez vědomí uživatele
DTLS-SRTP (WebRTC) Odposlechem zvuku/videa

Tyto vrstvy do sebe zapadají: ECDH chrání výměnu klíčů, ověřovací číslo chrání před MITM, AES-256-GCM chrání signalizaci a WebRTC chrání média. Útočník by musel tento řetězec narušit na několika místech, aniž by si toho zařízení nebo rodiče všimli.


Další články