Blogg

Säker parkoppling i Baby Monitor Timmy

Så samverkar ECDH, SAS-numret och krypterad signalering.

Innan Baby Monitor Timmy överför ljud och video behöver två enheter hitta varandra och lita på varandra. Detta parkopplingssteg är det mest avgörande ögonblicket i hela processen. Här förklarar jag hur Timmy parkopplar enheter, vilken kryptografi som används och varför en angripare i närheten inte obemärkt kan ta över anslutningen.

Problemet: Hur vet min enhet vem den pratar med?

När två enheter ansluter för första gången är den centrala frågan: pratar enhet A verkligen med enhet B, eller sitter någon emellan? Inom kryptografi kallas det en man-in-the-middle-attack (MITM).

Timmy löser detta med ett Elliptic Curve Diffie-Hellman-nyckelutbyte (ECDH) via Firebase, kombinerat med visuell verifiering av användaren.

Följande diagram visar hela parkopplingsflödet i korthet:

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
      

Fullständig sekvens för parkopplingsprotokollet — redigerbar källa: docs/diagrams/pairing-sequence.mmd

Steg 1: Varje enhet skapar ett nyckelpar

När parkopplingsskärmen öppnas skapar varje enhet ett tillfälligt ECDH-nyckelpar på P-256-kurvan (secp256r1):

Nycklarna skapas med en kryptografiskt säker slumptalsgenerator (Random.secure()) och är giltiga endast för detta enskilda parkopplingsförsök. Nya nycklar skapas vid varje nytt försök.

Steg 2: Utbyte av publika nycklar via Firebase

För att två enheter ska kunna hitta varandra använder Timmy en kod med 4 tecken som mötespunkt. Koden kan hittas automatiskt via Nearby Connections (Bluetooth Low Energy) eller anges manuellt. Den har inget kryptografiskt värde; den gör bara att båda enheterna hittar samma Firebase Firestore-dokument.

När båda enheterna känner till koden skriver var och en sin offentliga ECDH-nyckel till ett delat Firestore-dokument. Sedan läser varje enhet den andra enhetens offentliga nyckel från dokumentet.

Viktigt: endast den offentliga nyckeln skickas. Den privata nyckeln lämnar aldrig enheten. Den som övervakar Firebase-trafiken ser offentliga nycklar, men kan inte beräkna den delade hemligheten utifrån dem. Det bygger på svårigheten i det diskreta logaritmproblemet för elliptiska kurvor (ECDLP).

Steg 3: Beräkna den delade hemligheten

När båda enheterna har hittat varandras offentliga nyckel beräknar de oberoende av varandra samma delade hemlighet:

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

Matematiken bakom elliptiska kurvor garanterar att båda beräkningarna ger samma resultat, även om varje enhet bara känner till sin egen privata nyckel och den andras offentliga nyckel.

Steg 4: Verifieringsnumret (SAS)

Från den delade hemligheten härleds en kort autentiseringssträng (SAS) — ett tvåsiffrigt nummer som visas på båda enheterna:

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

Båda enheterna visar samma nummer — till exempel 42. Användaren jämför visuellt om numren på båda skärmarna stämmer överens och bekräftar sedan på varje enhet var för sig.

Varför en angripare inte kan förfalska detta

En man-in-the-middle-angripare skulle behöva fånga upp nyckelutbytet i Firebase. Mer specifikt skulle angriparen behöva:

  1. Ersätta de riktiga offentliga nycklarna i Firestore-dokumentet med sina egna
  2. Upprätta separata delade hemligheter med varje enhet
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)
      

Man-in-the-middle-detektering via avvikande SAS — redigerbar källa: docs/diagrams/mitm-detection.mmd

I detta fall beräknar angriparen en delad hemlighet S_A med enhet A och en annan delad hemlighet S_B med enhet B. Eftersom S_A ≠ S_B, beräknar enheterna olika verifieringsnummer.

Angriparen kan inte få numren att stämma överens eftersom:

Användaren ser olika nummer på skärmarna och avbryter parkopplingen. Då har attacken blivit synlig.

Steg 5: Slutför parkopplingen

Först när användaren har bekräftat verifieringen på båda enheterna slutförs parkopplingen:

  1. En parkopplingsnyckel med 64 tecken (256 bitar) härleds från den delade hemligheten: SHA-256("pair:" + sharedSecret) → pairingKey
  2. Dokumentnyckeln härleds som SHA-256("doc:" + pairingKey) och används som Firestore-dokumentnyckel
  3. Krypteringsnyckeln härleds som SHA-256("enc:" + pairingKey) och ger AES-256-GCM-nyckeln för krypterad signalering
  4. Båda enheterna sparar samma parkopplingsnyckel och går vidare till lägesval

Från och med nu krypteras alla fortsatta anslutningsförsök (Firestore-signalering, WebRTC-konfiguration) med den gemensamma AES-256-GCM-nyckeln. Parkopplingsnyckeln skickas aldrig till backend; endast dess SHA-256-hash används som dokumentidentifierare.

Systemarkitektur

Följande diagram visar komponenterna som används vid parkoppling och kommunikation:

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

Översikt över systemarkitekturen — redigerbar källa: docs/diagrams/pairing-architecture.mmd

Kommunikationsvägar i detalj:

Reservlösning: Manuell kodinmatning

Om Bluetooth inte är tillgängligt (t.ex. på äldre enheter) kan koden med 4 tecken även skrivas in manuellt. Manuell inmatning använder samma ECDH-nyckelutbyte och samma SAS-verifiering som automatisk parkoppling. Den enda skillnaden är att användaren läser och skriver in koden i stället för att den upptäcks via BLE.

Eftersom ECDH-nyckelutbytet sker via Firebase i båda fallen är säkerheten identisk. Koden med 4 tecken är bara en mötespunkt; den verkliga krypteringen bygger på den 256-bitarsnyckel som härleds från ECDH.

Sammanfattning

Säkerhetsmekanism Skyddar mot
ECDH-nyckelutbyte (P-256) Avlyssning av trafik vid nyckelutbyte
Tillfälliga nyckelpar Framåtsekretess — tidigare parkopplingar förblir säkra
Visuellt verifieringsnummer (SAS) Man-in-the-middle (MITM) vid nyckelutbyte
SHA-256-hash som dokumentnyckel Extrahering av kod från Firestore
AES-256-GCM-kryptering Avlyssning av signaleringsdata
Bekräftelse från båda sidor Ensidig parkoppling utan användarens vetskap
DTLS-SRTP (WebRTC) Avlyssning av ljud/video

Dessa lager samverkar: ECDH skyddar nyckelutbytet, verifieringsnumret skyddar mot MITM, AES-256-GCM skyddar signaleringen och WebRTC skyddar media. En angripare skulle behöva bryta denna kedja på flera ställen utan att enheterna eller föräldrarna märker det.


Fler artiklar