Blog

Bezpieczne parowanie w Baby Monitor Timmy

Jak ECDH, numer SAS i szyfrowana sygnalizacja współpracują ze sobą.

Zanim Baby Monitor Timmy zacznie przesyłać dźwięk i obraz, dwa urządzenia muszą się odnaleźć i zaufać sobie nawzajem. Ten etap parowania jest najważniejszym momentem całego procesu. Wyjaśniam tutaj, jak Timmy paruje urządzenia, jaka kryptografia za tym stoi i dlaczego osoba atakująca w pobliżu nie może przejąć połączenia niezauważenie.

Problem: skąd moje urządzenie wie, z kim rozmawia?

Gdy dwa urządzenia łączą się po raz pierwszy, kluczowe pytanie brzmi: czy urządzenie A naprawdę rozmawia z urządzeniem B, czy ktoś znajduje się pomiędzy nimi? W kryptografii nazywa się to atakiem typu man-in-the-middle (MITM).

Timmy rozwiązuje ten problem dzięki wymianie kluczy Elliptic Curve Diffie-Hellman (ECDH) przez Firebase, połączonej z wizualną weryfikacją przez użytkownika.

Poniższy diagram pokazuje cały proces parowania w skrócie:

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
      

Pełna sekwencja protokołu parowania — edytowalne źródło: docs/diagrams/pairing-sequence.mmd

Krok 1: Każde urządzenie generuje parę kluczy

Po otwarciu ekranu parowania każde urządzenie generuje tymczasową parę kluczy ECDH na krzywej P-256 (secp256r1):

Klucze są tworzone za pomocą kryptograficznie bezpiecznego generatora liczb losowych (Random.secure()) i są ważne tylko dla tej jednej próby parowania. Dla każdej nowej próby generowane są nowe klucze.

Krok 2: Wymiana kluczy publicznych przez Firebase

Aby dwa urządzenia mogły się odnaleźć, Timmy używa 4-znakowego kodu jako punktu spotkania. Kod można wykryć automatycznie przez Nearby Connections (Bluetooth Low Energy) albo wpisać ręcznie. Nie ma on żadnej wartości kryptograficznej; służy jedynie temu, aby oba urządzenia trafiły do tego samego dokumentu Firebase Firestore.

Gdy oba urządzenia znają kod, każde zapisuje swój publiczny klucz ECDH we wspólnym dokumencie Firestore. Następnie każde urządzenie odczytuje z niego klucz publiczny drugiego urządzenia.

Co istotne: wysyłany jest tylko publiczny klucz. Klucz prywatny nigdy nie opuszcza urządzenia. Osoba obserwująca ruch Firebase widzi klucze publiczne, ale nie może wyliczyć wspólnego sekretu na ich podstawie. Wynika to z trudności problemu logarytmu dyskretnego na krzywych eliptycznych (ECDLP).

Krok 3: Wyliczenie wspólnego sekretu

Gdy oba urządzenia poznają klucz publiczny drugiego urządzenia, niezależnie wyliczają ten sam wspólny sekret:

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

Matematyka krzywych eliptycznych gwarantuje, że oba obliczenia dają ten sam wynik, choć każde urządzenie zna tylko własny klucz prywatny i klucz publiczny drugiego urządzenia.

Krok 4: Numer weryfikacyjny (SAS)

Ze wspólnego sekretu wyprowadzany jest krótki ciąg uwierzytelniający (SAS) — dwucyfrowy numer wyświetlany na obu urządzeniach:

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

Oba urządzenia wyświetlają ten sam numer — na przykład 42. Użytkownik porównuje wzrokowo numery na obu ekranach, a następnie potwierdza je na każdym urządzeniu osobno.

Dlaczego osoba atakująca nie może tego sfałszować

Atakujący typu man-in-the-middle musiałby przechwycić wymianę kluczy w Firebase. Konkretnie musiałby:

  1. Zastąpić prawdziwe klucze publiczne zapisane w dokumencie Firestore własnymi
  2. Ustanowić osobne wspólne sekrety z każdym urządzeniem
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)
      

Wykrywanie man-in-the-middle przez niezgodność SAS — edytowalne źródło: docs/diagrams/mitm-detection.mmd

W takim przypadku atakujący wylicza wspólny sekret S_A z urządzeniem A i inny wspólny sekret S_B z urządzeniem B. Ponieważ S_A ≠ S_B, urządzenia wyliczają różne numery weryfikacyjne.

Atakujący nie może sprawić, by numery były identyczne, ponieważ:

Użytkownik widzi różne numery na ekranach i anuluje parowanie. W tym momencie atak staje się widoczny.

Krok 5: Zakończenie parowania

Dopiero po potwierdzeniu weryfikacji przez użytkownika na obu urządzeniach parowanie zostaje zakończone:

  1. Wyprowadzany jest 64-znakowy klucz parowania (256 bitów) ze wspólnego sekretu: SHA-256("pair:" + sharedSecret) → pairingKey
  2. Klucz dokumentu jest wyprowadzany jako SHA-256("doc:" + pairingKey) i służy jako klucz dokumentu Firestore
  3. Klucz szyfrowania jest wyprowadzany jako SHA-256("enc:" + pairingKey) i zapewnia klucz AES-256-GCM do szyfrowanej sygnalizacji
  4. Oba urządzenia zapisują ten sam klucz parowania i przechodzą do wyboru trybu

Od tego momentu wszystkie kolejne próby połączenia (sygnalizacja Firestore, konfiguracja WebRTC) są szyfrowane wspólnym kluczem AES-256-GCM. Klucz parowania nigdy nie jest wysyłany do backendu; jako identyfikator dokumentu używany jest wyłącznie jego hash SHA-256.

Architektura systemu

Poniższy diagram pokazuje komponenty biorące udział w parowaniu i komunikacji:

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

Przegląd architektury systemu — edytowalne źródło: docs/diagrams/pairing-architecture.mmd

Szczegóły ścieżek komunikacji:

Opcja awaryjna: ręczne wpisanie kodu

Jeśli Bluetooth jest niedostępny (np. na starszych urządzeniach), 4-znakowy kod można również wpisać ręcznie. Ręczne wpisanie wykorzystuje tę samą wymianę kluczy ECDH i tę samą weryfikację SAS co parowanie automatyczne. Jedyna różnica polega na tym, że użytkownik odczytuje i wpisuje kod zamiast wykrywać go przez BLE.

Ponieważ wymiana kluczy ECDH w obu przypadkach odbywa się przez Firebase, poziom bezpieczeństwa jest identyczny. 4-znakowy kod jest tylko punktem spotkania; właściwe szyfrowanie opiera się na 256-bitowym kluczu wyprowadzonym z ECDH.

Podsumowanie

Mechanizm bezpieczeństwa Chroni przed
Wymiana kluczy ECDH (P-256) Podsłuchem ruchu podczas wymiany kluczy
Tymczasowe pary kluczy Utajnienie z wyprzedzeniem — wcześniejsze parowania pozostają bezpieczne
Wizualny numer weryfikacyjny (SAS) Atakiem man-in-the-middle (MITM) podczas wymiany kluczy
Hash SHA-256 jako klucz dokumentu Wydobyciem kodu z Firestore
Szyfrowanie AES-256-GCM Podsłuchem danych sygnalizacyjnych
Potwierdzenie po obu stronach Jednostronnym parowaniem bez wiedzy użytkownika
DTLS-SRTP (WebRTC) Podsłuchem dźwięku/obrazu

Te warstwy współpracują ze sobą: ECDH chroni wymianę kluczy, numer weryfikacyjny chroni przed MITM, AES-256-GCM chroni sygnalizację, a WebRTC chroni multimedia. Atakujący musiałby złamać ten łańcuch w kilku miejscach, nie wzbudzając podejrzeń urządzeń ani rodziców.


Więcej artykułów