Blog

Sikker parring i Baby Monitor Timmy

Sådan fungerer ECDH, SAS-nummeret og krypteret signalering sammen.

Før Baby Monitor Timmy sender lyd og video, skal to enheder finde hinanden og have tillid til hinanden. Dette parrings trin er det mest kritiske øjeblik i hele forløbet. Her forklarer jeg, hvordan Timmy parrer, hvilken kryptografi der ligger bag, og hvorfor en angriber i nærheden ikke ubemærket kan overtage forbindelsen.

Problemet: Hvordan ved min enhed, hvem den taler med?

Når to enheder forbindes første gang, er det centrale spørgsmål: Taler enhed A virkelig med enhed B, eller sidder der nogen imellem? I kryptografi kaldes det et man-in-the-middle-angreb (MITM).

Timmy løser dette med en Elliptic Curve Diffie-Hellman-nøgleudveksling (ECDH) over Firebase kombineret med visuel verificering af brugeren.

Diagrammet herunder viser hele parringsforløbet på én gang:

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
      

Komplet sekvens for parringsprotokollen — redigerbar kilde: docs/diagrams/pairing-sequence.mmd

Trin 1: Hver enhed opretter et nøglepar

Når parringsskærmen åbnes, opretter hver enhed et midlertidigt ECDH-nøglepar på P-256-kurven (secp256r1):

Nøglerne oprettes med en kryptografisk sikker tilfældighedsgenerator (Random.secure()) og er kun gyldige for dette ene parringsforsøg. Der oprettes nye nøgler ved hvert nyt forsøg.

Trin 2: Udveksling af offentlige nøgler via Firebase

For at to enheder kan finde hinanden, bruger Timmy en kode på 4 tegn som mødested. Koden kan findes automatisk via Nearby Connections (Bluetooth Low Energy) eller indtastes manuelt. Den har ingen kryptografisk værdi; den får blot begge enheder til at finde det samme Firebase Firestore-dokument.

Når begge enheder kender koden, skriver hver sin offentlige ECDH-nøgle i et delt Firestore-dokument. Derefter læser hver enhed den anden enheds offentlige nøgle fra dokumentet.

Vigtigt: kun den offentlige nøgle sendes. Den private nøgle forlader aldrig enheden. Alle, der overvåger Firebase-trafikken, kan se offentlige nøgler, men kan ikke beregne den delte hemmelighed ud fra dem. Det bygger på vanskeligheden ved det diskrete logaritmeproblem på elliptiske kurver (ECDLP).

Trin 3: Beregning af den delte hemmelighed

Når begge enheder har fundet hinandens offentlige nøgler, beregner de uafhængigt den samme delte hemmelighed:

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

Matematikken bag elliptiske kurver sikrer, at begge beregninger giver samme resultat, selv om hver enhed kun kender sin egen private nøgle og den andens offentlige nøgle.

Trin 4: Verificeringsnummeret (SAS)

Fra den delte hemmelighed udledes en Short Authentication String (SAS) — et tocifret tal, der vises på begge enheder:

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

Begge enheder viser det samme tal — for eksempel 42. Brugeren sammenligner visuelt, om tallene på begge skærme er ens, og bekræfter derefter på hver enhed for sig.

Hvorfor en angriber ikke kan forfalske dette

En man-in-the-middle-angriber skal opfange nøgleudvekslingen i Firebase. Konkret skal angriberen:

  1. Erstatte de rigtige offentlige nøgler i Firestore-dokumentet med sine egne
  2. Etablere separate delte hemmeligheder med hver enhed
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)
      

Registrering af man-in-the-middle via SAS-mismatch — redigerbar kilde: docs/diagrams/mitm-detection.mmd

I dette tilfælde beregner angriberen en delt hemmelighed S_A med enhed A og en anden delt hemmelighed S_B med enhed B. Da S_A ≠ S_B, beregner enhederne forskellige verificeringsnumre.

Angriberen kan ikke få tallene til at stemme, fordi:

Brugeren ser forskellige tal på skærmene og annullerer parringen. På det tidspunkt er angrebet blevet synligt.

Trin 5: Fuldførelse af parringen

Først når brugeren har bekræftet verificeringen på begge enheder fuldføres parringen:

  1. En parringsnøgle på 64 tegn (256 bit) udledes af den delte hemmelighed: SHA-256("pair:" + sharedSecret) → pairingKey
  2. Dokumentnøglen udledes som SHA-256("doc:" + pairingKey) og bruges som Firestore-dokumentnøgle
  3. Krypteringsnøglen udledes som SHA-256("enc:" + pairingKey) og giver AES-256-GCM-nøglen til krypteret signalering
  4. Begge enheder gemmer den samme parringsnøgle og går videre til valg af tilstand

Fra dette punkt krypteres alle efterfølgende forbindelsesforsøg (Firestore-signalering, WebRTC-opsætning) med den delte AES-256-GCM-nøgle. Parringsnøglen sendes aldrig til backend; kun dens SHA-256-hash bruges som dokument-id.

Systemarkitektur

Diagrammet herunder viser de komponenter, der indgår i parring og 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

Oversigt over systemarkitekturen — redigerbar kilde: docs/diagrams/pairing-architecture.mmd

Kommunikationsveje i detaljer:

Alternativ: Manuel indtastning af kode

Hvis Bluetooth ikke er tilgængeligt (f.eks. på ældre enheder), kan koden på 4 tegn også indtastes manuelt. Manuel indtastning bruger den samme ECDH-nøgleudveksling og den samme SAS-verificering som automatisk parring. Den eneste forskel er, at brugeren læser og indtaster koden i stedet for, at den findes via BLE.

Da ECDH-nøgleudvekslingen i begge tilfælde sker via Firebase, er sikkerheden identisk. Koden på 4 tegn er kun et mødested; den egentlige kryptering bygger på den 256-bit-nøgle, der udledes fra ECDH.

Opsummering

Sikkerhedsmekanisme Beskytter mod
ECDH-nøgleudveksling (P-256) Aflytning af nøgleudvekslingstrafik
Midlertidige nøglepar Forward secrecy — tidligere parringer forbliver sikre
Visuelt verificeringsnummer (SAS) Man-in-the-middle (MITM) under nøgleudveksling
SHA-256-hash som dokumentnøgle Udtrækning af kode fra Firestore
AES-256-GCM-kryptering Aflytning af signaleringsdata
Bekræftelse på begge sider Ensidig parring uden brugerens viden
DTLS-SRTP (WebRTC) Aflytning af lyd/video

Disse lag passer sammen: ECDH beskytter nøgleudvekslingen, verificeringsnummeret beskytter mod MITM, AES-256-GCM beskytter signaleringen, og WebRTC beskytter medierne. En angriber skulle bryde denne kæde flere steder uden at enhederne eller forældrene opdagede det.


Flere artikler