Innan Baby Monitor Timmy överför ljud och video behöver två enheter hitta varandra och lita på varandra. Detta parkopplingssteg är det mest avgörande ögonblicket i hela processen. Här förklarar jag hur Timmy parkopplar enheter, vilken kryptografi som används och varför en angripare i närheten inte obemärkt kan ta över anslutningen.
Problemet: Hur vet min enhet vem den pratar med?
När två enheter ansluter för första gången är den centrala frågan: pratar enhet A verkligen med enhet B, eller sitter någon emellan? Inom kryptografi kallas det en man-in-the-middle-attack (MITM).
Timmy löser detta med ett Elliptic Curve Diffie-Hellman-nyckelutbyte (ECDH) via Firebase, kombinerat med visuell verifiering av användaren.
Följande diagram visar hela parkopplingsflödet i korthet:
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
Fullständig sekvens för parkopplingsprotokollet — redigerbar källa: docs/diagrams/pairing-sequence.mmd
Steg 1: Varje enhet skapar ett nyckelpar
När parkopplingsskärmen öppnas skapar varje enhet ett tillfälligt ECDH-nyckelpar på P-256-kurvan (secp256r1):
- En privat nyckel — stannar enbart på enheten
- En offentlig nyckel — utbyts via Firebase
Nycklarna skapas med en kryptografiskt säker slumptalsgenerator (Random.secure())
och är giltiga endast för detta enskilda parkopplingsförsök. Nya nycklar skapas
vid varje nytt försök.
Steg 2: Utbyte av publika nycklar via Firebase
För att två enheter ska kunna hitta varandra använder Timmy en kod med 4 tecken som mötespunkt. Koden kan hittas automatiskt via Nearby Connections (Bluetooth Low Energy) eller anges manuellt. Den har inget kryptografiskt värde; den gör bara att båda enheterna hittar samma Firebase Firestore-dokument.
När båda enheterna känner till koden skriver var och en sin offentliga ECDH-nyckel till ett delat Firestore-dokument. Sedan läser varje enhet den andra enhetens offentliga nyckel från dokumentet.
Viktigt: endast den offentliga nyckeln skickas. Den privata nyckeln lämnar aldrig enheten. Den som övervakar Firebase-trafiken ser offentliga nycklar, men kan inte beräkna den delade hemligheten utifrån dem. Det bygger på svårigheten i det diskreta logaritmproblemet för elliptiska kurvor (ECDLP).
Steg 3: Beräkna den delade hemligheten
När båda enheterna har hittat varandras offentliga nyckel beräknar de oberoende av varandra samma delade hemlighet:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
Matematiken bakom elliptiska kurvor garanterar att båda beräkningarna ger samma resultat, även om varje enhet bara känner till sin egen privata nyckel och den andras offentliga nyckel.
Steg 4: Verifieringsnumret (SAS)
Från den delade hemligheten härleds en kort autentiseringssträng (SAS) — ett tvåsiffrigt nummer som visas på båda enheterna:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Båda enheterna visar samma nummer — till exempel 42. Användaren jämför visuellt om numren på båda skärmarna stämmer överens och bekräftar sedan på varje enhet var för sig.
Varför en angripare inte kan förfalska detta
En man-in-the-middle-angripare skulle behöva fånga upp nyckelutbytet i Firebase. Mer specifikt skulle angriparen behöva:
- Ersätta de riktiga offentliga nycklarna i Firestore-dokumentet med sina egna
- Upprätta separata delade hemligheter med varje 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)
Man-in-the-middle-detektering via avvikande SAS — redigerbar källa: docs/diagrams/mitm-detection.mmd
I detta fall beräknar angriparen en delad hemlighet S_A med enhet A och en
annan delad hemlighet S_B med enhet B. Eftersom S_A ≠ S_B,
beräknar enheterna olika verifieringsnummer.
Angriparen kan inte få numren att stämma överens eftersom:
- Angriparen känner inte till enheternas privata nycklar
- SHA-256 är inte reversibel
- Sannolikheten för en slumpmässig träff är bara 1 på 100
Användaren ser olika nummer på skärmarna och avbryter parkopplingen. Då har attacken blivit synlig.
Steg 5: Slutför parkopplingen
Först när användaren har bekräftat verifieringen på båda enheterna slutförs parkopplingen:
- En parkopplingsnyckel med 64 tecken (256 bitar) härleds från den delade hemligheten:
SHA-256("pair:" + sharedSecret) → pairingKey - Dokumentnyckeln härleds som
SHA-256("doc:" + pairingKey)och används som Firestore-dokumentnyckel - Krypteringsnyckeln härleds som
SHA-256("enc:" + pairingKey)och ger AES-256-GCM-nyckeln för krypterad signalering - Båda enheterna sparar samma parkopplingsnyckel och går vidare till lägesval
Från och med nu krypteras alla fortsatta anslutningsförsök (Firestore-signalering, WebRTC-konfiguration) med den gemensamma AES-256-GCM-nyckeln. Parkopplingsnyckeln skickas aldrig till backend; endast dess SHA-256-hash används som dokumentidentifierare.
Systemarkitektur
Följande diagram visar komponenterna som används vid parkoppling och 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
Översikt över systemarkitekturen — redigerbar källa: docs/diagrams/pairing-architecture.mmd
Kommunikationsvägar i detalj:
- WebRTC peer-to-peer (tjock linje): Ljud, video och DataChannel går direkt mellan enheterna — krypterat med DTLS-SRTP. Ingen server ser dessa data.
- Firebase Firestore (heldragen linje): Parkopplingsdata (ECDH-nycklar) och signalering (SDP/ICE) går via Firestore och totalsträckskrypteras med AES-256-GCM. Firebase kan inte dekryptera informationen.
- STUN-server: Båda enheterna hittar sin offentliga IP-adress så att en direkt peer-to-peer-anslutning kan upprättas.
- TURN-relä: Om en direkt anslutning inte är möjlig (t.ex. via mobildata) vidarebefordrar den valda lokala eller Cloudflare TURN-servern krypterad media. Kortlivade inloggningsuppgifter (24 h) hämtas via Firebase Cloud Functions.
- Bluetooth LE (prickad linje): Nearby Connections hittar närliggande enheter automatiskt — endast möteskoden överförs, inget nyckelmaterial.
Reservlösning: Manuell kodinmatning
Om Bluetooth inte är tillgängligt (t.ex. på äldre enheter) kan koden med 4 tecken även skrivas in manuellt. Manuell inmatning använder samma ECDH-nyckelutbyte och samma SAS-verifiering som automatisk parkoppling. Den enda skillnaden är att användaren läser och skriver in koden i stället för att den upptäcks via BLE.
Eftersom ECDH-nyckelutbytet sker via Firebase i båda fallen är säkerheten identisk. Koden med 4 tecken är bara en mötespunkt; den verkliga krypteringen bygger på den 256-bitarsnyckel som härleds från ECDH.
Sammanfattning
| Säkerhetsmekanism | Skyddar mot |
|---|---|
| ECDH-nyckelutbyte (P-256) | Avlyssning av trafik vid nyckelutbyte |
| Tillfälliga nyckelpar | Framåtsekretess — tidigare parkopplingar förblir säkra |
| Visuellt verifieringsnummer (SAS) | Man-in-the-middle (MITM) vid nyckelutbyte |
| SHA-256-hash som dokumentnyckel | Extrahering av kod från Firestore |
| AES-256-GCM-kryptering | Avlyssning av signaleringsdata |
| Bekräftelse från båda sidor | Ensidig parkoppling utan användarens vetskap |
| DTLS-SRTP (WebRTC) | Avlyssning av ljud/video |
Dessa lager samverkar: ECDH skyddar nyckelutbytet, verifieringsnumret skyddar mot MITM, AES-256-GCM skyddar signaleringen och WebRTC skyddar media. En angripare skulle behöva bryta denna kedja på flera ställen utan att enheterna eller föräldrarna märker det.