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):
- En privat nøkkel — blir kun på enheten
- En offentlig nøkkel — utveksles via Firebase
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:
- Bytte ut de ekte offentlige nøklene i Firestore-dokumentet med sine egne
- 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:
- De ikke kjenner enhetenes private nøkler
- SHA-256 ikke kan reverseres
- Sannsynligheten for en tilfeldig match er bare 1 av 100
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:
- En paringsnøkkel med 64 tegn (256 bit) avledes fra den delte hemmeligheten:
SHA-256("pair:" + sharedSecret) → pairingKey - Dokumentnøkkelen avledes som
SHA-256("doc:" + pairingKey)og brukes som Firestore-dokumentnøkkel - Krypteringsnøkkelen avledes som
SHA-256("enc:" + pairingKey)og gir AES-256-GCM-nøkkelen for kryptert signalering - 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:
- WebRTC peer-to-peer (tykk linje): Lyd, video og DataChannel går direkte mellom enhetene — kryptert med DTLS-SRTP. Ingen server ser disse dataene.
- Firebase Firestore (heltrukket linje): Paringsdata (ECDH-nøkler) og signalering (SDP/ICE) går gjennom Firestore — ende-til-ende-kryptert med AES-256-GCM. Firebase kan ikke dekryptere dataene.
- STUN-server: Begge enhetene finner den offentlige IP-adressen sin, slik at en direkte peer-to-peer-forbindelse kan opprettes.
- TURN-relé: Hvis en direkte forbindelse ikke er mulig (for eksempel på mobildata), videresender den valgte lokale eller Cloudflare TURN-serveren de krypterte mediedataene. Kortvarige påloggingsopplysninger (24 t) hentes via Firebase Cloud Functions.
- Bluetooth LE (stiplet linje): Nearby Connections finner enheter i nærheten automatisk — bare møtekoden overføres, ikke noe nøkkelmateriale.
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.