Blogg

Sikker paring i Baby Monitor Timmy

Slik fungerer ECDH, SAS-nummeret og kryptert signalering sammen.

Før Baby Monitor Timmy overfører lyd og video, må to enheter finne hverandre og stole på hverandre. Dette parings trinnet er det mest kritiske øyeblikket i hele prosessen. Her forklarer jeg hvordan Timmy pares, hvilken kryptografi som ligger bak, og hvorfor en angriper i nærheten ikke kan overta forbindelsen uten at det oppdages.

Problemet: Hvordan vet enheten min hvem den snakker med?

Når to enheter kobles til hverandre for første gang, er det sentrale spørsmålet: Snakker enhet A virkelig med enhet B, eller sitter noen imellom? I kryptografi kalles dette et mann-i-midten-angrep (MITM).

Timmy løser dette med en Elliptic Curve Diffie-Hellman-nøkkelutveksling (ECDH) via Firebase, kombinert med visuell verifisering fra brukeren.

Diagrammet under viser hele paringsprosessen på et øyeblikk:

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
      

Fullstendig sekvens for paringsprotokollen — redigerbar kilde: docs/diagrams/pairing-sequence.mmd

Trinn 1: Hver enhet oppretter et nøkkelpar

Når paringsskjermen åpnes, oppretter hver enhet et midlertidig ECDH-nøkkelpar på P-256-kurven (secp256r1):

Nøklene opprettes med en kryptografisk sikker tilfeldighetsgenerator (Random.secure()) og er gyldige kun for dette ene paringsforsøket. Nye nøkler opprettes for hvert nye forsøk.

Trinn 2: Utveksle offentlige nøkler via Firebase

For at to enheter skal finne hverandre, bruker Timmy en kode med 4 tegn som møtested. Denne koden kan oppdages automatisk via Nearby Connections (Bluetooth Low Energy) eller skrives inn manuelt. Den har ingen kryptografisk verdi; den gjør bare at begge enhetene finner det samme Firebase Firestore-dokumentet.

Når begge enhetene kjenner koden, skriver hver av dem sin offentlige ECDH-nøkkel til et delt Firestore-dokument. Deretter leser hver enhet den andre enhetens offentlige nøkkel fra dokumentet.

Viktig: kun den offentlige nøkkelen sendes. Den private nøkkelen forlater aldri enheten. Alle som overvåker Firebase-trafikken, ser offentlige nøkler, men kan ikke beregne den delte hemmeligheten ut fra dem. Dette bygger på hvor vanskelig det diskrete logaritmeproblemet for elliptiske kurver (ECDLP) er.

Trinn 3: Beregne den delte hemmeligheten

Når begge enhetene har funnet hverandres offentlige nøkler, beregner de uavhengig den samme delte hemmeligheten:

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

Matematikken bak elliptiske kurver garanterer at begge beregningene gir samme resultat, selv om hver enhet bare kjenner sin egen private nøkkel og den andres offentlige nøkkel.

Trinn 4: Verifiseringsnummeret (SAS)

Fra den delte hemmeligheten avledes en Short Authentication String (SAS) — et tosifret nummer som vises på begge enhetene:

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

Begge enhetene viser det samme nummeret — for eksempel 42. Brukeren sammenligner visuelt om numrene på begge skjermene stemmer, og bekrefter deretter på hver enhet for seg.

Hvorfor en angriper ikke kan forfalske dette

En mann-i-midten måtte avskjære nøkkelutvekslingen i Firebase. Konkret måtte vedkommende:

  1. Bytte ut de ekte offentlige nøklene i Firestore-dokumentet med sine egne
  2. Etablere separate delte hemmeligheter med hver 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)
      

Oppdagelse av mann-i-midten via SAS-avvik — redigerbar kilde: docs/diagrams/mitm-detection.mmd

I dette tilfellet beregner angriperen en delt hemmelighet S_A med enhet A og en annen delt hemmelighet S_B med enhet B. Siden S_A ≠ S_B, beregner enhetene ulike verifiseringsnumre.

Angriperen kan ikke få numrene til å stemme fordi:

Brukeren ser ulike numre på skjermene og avbryter paringen. Da har angrepet blitt synlig.

Trinn 5: Fullføre paringen

Først etter at brukeren har bekreftet verifiseringen på begge enhetene blir paringen fullført:

  1. En paringsnøkkel med 64 tegn (256 bit) avledes fra den delte hemmeligheten: SHA-256("pair:" + sharedSecret) → pairingKey
  2. Dokumentnøkkelen avledes som SHA-256("doc:" + pairingKey) og brukes som Firestore-dokumentnøkkel
  3. Krypteringsnøkkelen avledes som SHA-256("enc:" + pairingKey) og gir AES-256-GCM-nøkkelen for kryptert signalering
  4. Begge enhetene lagrer den samme paringsnøkkelen og går videre til valg av modus

Fra dette tidspunktet krypteres alle videre tilkoblingsforsøk (Firestore-signalering, WebRTC-oppsett) med den delte AES-256-GCM-nøkkelen. Paringsnøkkelen sendes aldri til backend; bare SHA-256-hashen brukes som dokumentidentifikator.

Systemarkitektur

Diagrammet under viser komponentene som brukes til paring og kommunikasjon:

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

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

Kommunikasjonsveier i detalj:

Reserve: Manuell kodeinntasting

Hvis Bluetooth ikke er tilgjengelig (for eksempel på eldre enheter), kan koden med 4 tegn også skrives inn manuelt. Manuell inntasting bruker den samme ECDH-nøkkelutvekslingen og den samme SAS-verifiseringen som automatisk paring. Den eneste forskjellen er at brukeren leser og skriver inn koden i stedet for at den oppdages via BLE.

Fordi ECDH-nøkkelutvekslingen skjer via Firebase i begge tilfeller, er sikkerheten identisk. Koden med 4 tegn er bare et møtested; den egentlige krypteringen bygger på 256-bitsnøkkelen som avledes fra ECDH.

Oppsummering

Sikkerhetsmekanisme Beskytter mot
ECDH-nøkkelutveksling (P-256) Avlytting av nøkkelutvekslingstrafikk
Midlertidige nøkkelpar Fremoversikkerhet — tidligere paringer forblir beskyttet
Visuelt verifiseringsnummer (SAS) Mann-i-midten (MITM) under nøkkelutveksling
SHA-256-hash som dokumentnøkkel Uthenting av kode fra Firestore
AES-256-GCM-kryptering Avlytting av signaleringsdata
Bekreftelse på begge sider Ensidig paring uten at brukeren vet om det
DTLS-SRTP (WebRTC) Avlytting av lyd/video

Disse lagene virker sammen: ECDH beskytter nøkkelutvekslingen, verifiseringsnummeret beskytter mot MITM, AES-256-GCM beskytter signaleringen, og WebRTC beskytter mediedataene. En angriper måtte ha brutt denne kjeden flere steder uten at enhetene eller foreldrene oppdaget det.


Flere artikler