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):
- En privat nøgle — bliver kun på enheden
- En offentlig nøgle — udveksles via Firebase
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:
- Erstatte de rigtige offentlige nøgler i Firestore-dokumentet med sine egne
- 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:
- De kender ikke enhedernes private nøgler
- SHA-256 kan ikke vendes om
- Sandsynligheden for et tilfældigt match er kun 1 ud af 100
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:
- En parringsnøgle på 64 tegn (256 bit) udledes af den delte hemmelighed:
SHA-256("pair:" + sharedSecret) → pairingKey - Dokumentnøglen udledes som
SHA-256("doc:" + pairingKey)og bruges som Firestore-dokumentnøgle - Krypteringsnøglen udledes som
SHA-256("enc:" + pairingKey)og giver AES-256-GCM-nøglen til krypteret signalering - 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:
- WebRTC peer-to-peer (tyk linje): Lyd, video og DataChannel går direkte mellem enhederne — krypteret med DTLS-SRTP. Ingen server ser disse data.
- Firebase Firestore (fuld linje): Parringsdata (ECDH-nøgler) og signalering (SDP/ICE) går via Firestore — end-to-end-krypteret med AES-256-GCM. Firebase kan ikke dekryptere dataene.
- STUN-server: Begge enheder finder deres offentlige IP-adresse, så der kan oprettes en direkte peer-to-peer-forbindelse.
- TURN-relæ: Hvis en direkte forbindelse ikke er mulig (f.eks. på mobildata), videresender den valgte lokale eller Cloudflare TURN-server de krypterede medier. Kortvarige legitimationsoplysninger (24 t.) hentes via Firebase Cloud Functions.
- Bluetooth LE (prikket linje): Nearby Connections finder automatisk enheder i nærheden — kun mødekoden overføres, intet nøglemateriale.
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.