Blog

Veilig koppelen in Baby Monitor Timmy

Hoe ECDH, het SAS-nummer en versleutelde signalering samenwerken.

Voordat Baby Monitor Timmy audio en video verstuurt, moeten twee apparaten elkaar vinden en elkaar vertrouwen. Deze koppelings stap is het belangrijkste moment in het hele proces. Hier leg ik uit hoe Timmy koppelt, welke cryptografie daarachter zit en waarom een aanvaller in de buurt de verbinding niet ongemerkt kan overnemen.

Het probleem: hoe weet mijn apparaat met wie het praat?

Wanneer twee apparaten voor het eerst verbinding maken, is de centrale vraag: praat apparaat A echt met apparaat B, of zit er iemand tussenin? In de cryptografie heet dat een man-in-the-middleaanval (MITM).

Timmy lost dit op met een Elliptic Curve Diffie-Hellman-sleuteluitwisseling (ECDH) via Firebase, gecombineerd met visuele verificatie door de gebruiker.

Het volgende diagram toont in één oogopslag het volledige koppelingsproces:

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
      

Volledige volgorde van het koppelingsprotocol — bewerkbare bron: docs/diagrams/pairing-sequence.mmd

Stap 1: Elk apparaat genereert een sleutelpaar

Bij het openen van het koppelscherm genereert elk apparaat een tijdelijk ECDH-sleutelpaar op de P-256-curve (secp256r1):

De sleutels worden gemaakt met een cryptografisch veilige generator voor willekeurige getallen (Random.secure()) en zijn alleen geldig voor deze ene koppelingspoging. Voor elke nieuwe poging worden nieuwe sleutels gegenereerd.

Stap 2: Openbare sleutels uitwisselen via Firebase

Om twee apparaten elkaar te laten vinden, gebruikt Timmy een code van 4 tekens als ontmoetingspunt. Deze code kan automatisch worden gevonden via Nearby Connections (Bluetooth Low Energy) of handmatig worden ingevoerd. De code heeft geen cryptografische waarde; hij zorgt er alleen voor dat beide apparaten hetzelfde Firebase Firestore-document vinden.

Zodra beide apparaten de code kennen, schrijft elk zijn openbare ECDH-sleutel naar een gedeeld Firestore-document. Daarna leest elk apparaat de openbare sleutel van het andere apparaat uit dat document.

Belangrijk: alleen de openbare sleutel wordt verzonden. De privésleutel verlaat het apparaat nooit. Wie het Firebase-verkeer bekijkt, ziet openbare sleutels, maar kan het gedeelde geheim niet berekenen op basis daarvan. Dit berust op de moeilijkheid van het Elliptic Curve Discrete Logarithm Problem (ECDLP).

Stap 3: Het gedeelde geheim berekenen

Zodra beide apparaten elkaars openbare sleutel hebben gevonden, berekenen ze onafhankelijk van elkaar hetzelfde gedeelde geheim:

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

De wiskunde van elliptische krommen garandeert dat beide berekeningen hetzelfde resultaat opleveren, ook al kent elk apparaat alleen zijn eigen privésleutel en de openbare sleutel van het andere apparaat.

Stap 4: Het verificatienummer (SAS)

Uit het gedeelde geheim wordt een Short Authentication String (SAS) afgeleid — een tweecijferig nummer dat op beide apparaten wordt weergegeven:

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

Beide apparaten tonen hetzelfde nummer — bijvoorbeeld 42. De gebruiker vergelijkt visueel of de nummers op beide schermen overeenkomen en bevestigt daarna op elk apparaat afzonderlijk.

Waarom een aanvaller dit niet kan vervalsen

Een man-in-the-middle zou de sleuteluitwisseling in Firebase moeten onderscheppen. Concreet zou diegene:

  1. De echte openbare sleutels in het Firestore-document moeten vervangen door eigen sleutels
  2. Met elk apparaat een afzonderlijk gedeeld geheim moeten opzetten
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)
      

Detectie van man-in-the-middle via een afwijkend SAS — bewerkbare bron: docs/diagrams/mitm-detection.mmd

In dit geval berekent de aanvaller een gedeeld geheim S_A met apparaat A en een ander gedeeld geheim S_B met apparaat B. Omdat S_A ≠ S_B, berekenen de apparaten verschillende verificatienummers.

De aanvaller kan de nummers niet gelijk krijgen, omdat:

De gebruiker ziet verschillende nummers op de schermen en annuleert het koppelen. Op dat moment is de aanval zichtbaar geworden.

Stap 5: Het koppelen voltooien

Pas nadat de gebruiker de verificatie op beide apparaten heeft bevestigd, wordt het koppelen voltooid:

  1. Een koppelingssleutel van 64 tekens (256 bits) wordt afgeleid uit het gedeelde geheim: SHA-256("pair:" + sharedSecret) → pairingKey
  2. De documentsleutel wordt afgeleid als SHA-256("doc:" + pairingKey) en dient als sleutel voor het Firestore-document
  3. De versleutelingssleutel wordt afgeleid als SHA-256("enc:" + pairingKey) en levert de AES-256-GCM-sleutel voor versleutelde signalering
  4. Beide apparaten slaan dezelfde koppelingssleutel op en gaan naar de moduskeuze

Vanaf dit moment worden alle verdere verbindingspogingen (Firestore-signalering, WebRTC-instelling) versleuteld met de gedeelde AES-256-GCM-sleutel. De koppelingssleutel wordt nooit naar de backend verzonden; alleen de SHA-256-hash ervan wordt gebruikt als document-ID.

Systeemarchitectuur

Het volgende diagram toont de onderdelen die betrokken zijn bij koppelen en communicatie:

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

Overzicht van de systeemarchitectuur — bewerkbare bron: docs/diagrams/pairing-architecture.mmd

Communicatiepaden in detail:

Terugvaloptie: code handmatig invoeren

Als Bluetooth niet beschikbaar is (bijvoorbeeld op oudere apparaten), kan de code van 4 tekens ook handmatig worden ingevoerd. Bij handmatige invoer worden dezelfde ECDH-sleuteluitwisseling en dezelfde SAS-verificatie gebruikt als bij automatisch koppelen. Het enige verschil is dat de gebruiker de code leest en invoert in plaats van dat deze via BLE wordt gevonden.

Omdat de ECDH-sleuteluitwisseling in beide gevallen via Firebase verloopt, is de beveiliging identiek. De code van 4 tekens is alleen een ontmoetingspunt; de echte versleuteling is gebaseerd op de uit ECDH afgeleide 256-bits sleutel.

Samenvatting

Beveiligingsmechanisme Beschermt tegen
ECDH-sleuteluitwisseling (P-256) Afluisteren van sleuteluitwisselingsverkeer
Tijdelijke sleutelparen Forward secrecy — eerdere koppelingen blijven veilig
Visueel verificatienummer (SAS) Man-in-the-middle (MITM) tijdens sleuteluitwisseling
SHA-256-hash als documentsleutel De code uit Firestore achterhalen
AES-256-GCM-versleuteling Afluisteren van signaleringsgegevens
Bevestiging aan beide kanten Eenzijdig koppelen zonder medeweten van de gebruiker
DTLS-SRTP (WebRTC) Afluisteren van audio/video

Deze lagen vullen elkaar aan: ECDH beschermt de sleuteluitwisseling, het verificatienummer beschermt tegen MITM, AES-256-GCM beschermt de signalering en WebRTC beschermt de media. Een aanvaller zou deze keten op meerdere punten moeten doorbreken zonder dat de apparaten of ouders het merken.


Meer artikelen