Prieš Baby Monitor Timmy perduodant garsą ir vaizdą, du įrenginiai turi rasti vienas kitą ir vienas kitu pasitikėti. Šis susiejimo žingsnis yra svarbiausias visame procese. Čia paaiškinu, kaip Timmy susieja įrenginius, kokia kriptografija tam naudojama ir kodėl šalia esantis užpuolikas negali nepastebimai perimti ryšio.
Problema: kaip mano įrenginys žino, su kuo jis kalba?
Kai du įrenginiai jungiasi pirmą kartą, pagrindinis klausimas yra toks: ar A įrenginys tikrai kalba su B įrenginiu, ar kas nors įsiterpė tarp jų? Kriptografijoje tai vadinama tarpininko ataka (MITM).
Ši problema sprendžiama pasitelkiant elipsinių kreivių Diffie–Hellmano (ECDH) raktų apsikeitimą per Firebase kartu su naudotojo atliekama vaizdine patikra.
Toliau pateikta schema iš karto parodo visą susiejimo eigą:
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
Visa susiejimo protokolo seka — redaguojamas šaltinis: docs/diagrams/pairing-sequence.mmd
1 žingsnis: kiekvienas įrenginys sukuria raktų porą
Atidarius susiejimo ekraną, kiekvienas įrenginys sukuria laikinąją ECDH raktų porą P-256 kreivėje (secp256r1):
- Privatusis raktas — lieka tik įrenginyje
- Viešasis raktas — juo apsikeičiama per Firebase
Raktai sukuriami naudojant kriptografiškai saugų atsitiktinių skaičių generatorių (Random.secure())
ir galioja tik šiam vienam susiejimo bandymui. Kiekvienam naujam bandymui sukuriami nauji raktai.
2 žingsnis: viešųjų raktų apsikeitimas per Firebase
Kad du įrenginiai rastų vienas kitą, Timmy naudoja 4 simbolių kodą kaip susitikimo tašką. Šis kodas gali būti automatiškai aptiktas naudojant Nearby Connections (Bluetooth Low Energy) arba įvestas ranka. Jis neturi kriptografinės vertės; tai tik padeda abiem įrenginiams rasti tą patį „Firebase Firestore“ dokumentą.
Kai abu įrenginiai žino kodą, kiekvienas įrašo savo viešąjį ECDH raktą į bendrą Firestore dokumentą. Tada kiekvienas įrenginys iš to dokumento nuskaito kito įrenginio viešąjį raktą.
Svarbiausia: siunčiamas tik viešasis raktas. Privatusis raktas niekada nepalieka įrenginio. Kiekvienas, stebintis Firebase srautą, mato viešuosius raktus, bet negali apskaičiuoti bendros paslapties iš jų. Tai remiasi elipsinių kreivių diskrečiojo logaritmo problemos (ECDLP) sudėtingumu.
3 žingsnis: bendros paslapties apskaičiavimas
Kai abu įrenginiai aptinka vienas kito viešąjį raktą, jie nepriklausomai apskaičiuoja tą pačią bendrą paslaptį:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
Elipsinių kreivių matematika užtikrina, kad abiejų skaičiavimų rezultatas bus toks pat, nors kiekvienas įrenginys žino tik savo privatųjį raktą ir kito įrenginio viešąjį raktą.
4 žingsnis: patikros numeris (SAS)
Iš bendros paslapties gaunama trumpa autentifikavimo eilutė (SAS) — dviženklis skaičius, rodomas abiejuose įrenginiuose:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Abiejuose įrenginiuose rodomas tas pats skaičius — pavyzdžiui, 42. Naudotojas vizualiai palygina, ar abiejuose ekranuose rodomi skaičiai sutampa, ir tada patvirtina atskirai kiekviename įrenginyje.
Kodėl užpuolikas negali to suklastoti
Tarpininkui reikėtų perimti raktų apsikeitimą per Firebase. Konkrečiai, užpuolikui reikėtų:
- Pakeisti Firestore dokumente saugomus tikruosius viešuosius raktus savaisiais
- Sukurti atskiras bendras paslaptis su kiekvienu įrenginiu
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)
Tarpininko atakos aptikimas pagal nesutampančius SAS numerius — redaguojamas šaltinis: docs/diagrams/mitm-detection.mmd
Tokiu atveju užpuolikas apskaičiuoja bendrą paslaptį S_A su A įrenginiu ir kitą
bendrą paslaptį S_B su B įrenginiu. Kadangi S_A ≠ S_B,
įrenginiai apskaičiuoja skirtingus patikros numerius.
Užpuolikas negali priversti skaičių sutapti, nes:
- Jis nežino įrenginių privačiųjų raktų
- SHA-256 nėra grįžtama funkcija
- Atsitiktinio sutapimo tikimybė yra tik 1 iš 100
Naudotojas ekranuose mato skirtingus skaičius ir atšaukia susiejimą. Tuo metu ataka tampa matoma.
5 žingsnis: susiejimo užbaigimas
Tik naudotojui patvirtinus patikrą abiejuose įrenginiuose susiejimas užbaigiamas:
- Iš bendros paslapties gaunamas 64 simbolių susiejimo raktas (256 bitai) :
SHA-256("pair:" + sharedSecret) → pairingKey - Dokumento raktas gaunamas kaip
SHA-256("doc:" + pairingKey)ir naudojamas kaip Firestore dokumento raktas - Šifravimo raktas gaunamas kaip
SHA-256("enc:" + pairingKey)ir suteikia AES-256-GCM raktą šifruotam signalizavimui - Abu įrenginiai išsaugo tą patį susiejimo raktą ir pereina prie režimo pasirinkimo
Nuo šio momento visi tolesni prisijungimo bandymai (Firestore signalizavimas, WebRTC nustatymas) šifruojami bendru AES-256-GCM raktu. Susiejimo raktas niekada nesiunčiamas į serverį; kaip dokumento identifikatorius naudojama tik jo SHA-256 maišos reikšmė.
Sistemos architektūra
Toliau pateikta schema rodo komponentus, dalyvaujančius susiejant ir bendraujant:
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
Sistemos architektūros apžvalga — redaguojamas šaltinis: docs/diagrams/pairing-architecture.mmd
Ryšio keliai išsamiau:
- WebRTC tiesioginis (peer-to-peer) ryšys (stora linija): garsas, vaizdas ir DataChannel perduodami tiesiogiai tarp įrenginių — šifruojami DTLS-SRTP. Joks serveris šių duomenų nemato.
- Firebase Firestore (vientisa linija): susiejimo duomenys (ECDH raktai) ir signalizavimo duomenys (SDP/ICE) perduodami per Firestore ir šifruojami nuo galo iki galo naudojant AES-256-GCM. Firebase negali šių duomenų iššifruoti.
- STUN serveris: Abu įrenginiai nustato savo viešąjį IP adresą, kad būtų galima užmegzti tiesioginį ryšį tarp jų.
- TURN retransliavimo serveris: Jei tiesioginis ryšys neįmanomas (pvz., naudojant mobiliuosius duomenis), pasirinktas vietinis arba Cloudflare TURN serveris retransliuoja užšifruotą garso ir vaizdo srautą. Trumpalaikiai prisijungimo duomenys (24 val.) gaunami per Firebase Cloud Functions.
- Bluetooth LE (punktyrinė linija): Nearby Connections automatiškai aptinka netoliese esančius įrenginius — perduodamas tik susitikimo kodas, jokia raktų medžiaga neperduodama.
Atsarginis būdas: kodo įvedimas ranka
Jei Bluetooth neveikia (pvz., senesniuose įrenginiuose), 4 simbolių kodą galima įvesti ir ranka. Įvedant ranka naudojamas tas pats ECDH raktų apsikeitimas ir tas pats SAS patikrinimas kaip ir automatiniam susiejimui. Skiriasi tik tai, kad kodą naudotojas perskaito ir įveda, užuot jį aptikęs per BLE.
Kadangi ECDH raktų apsikeitimas abiem atvejais vyksta per Firebase, saugumas yra toks pats. 4 simbolių kodas yra tik susitikimo taškas; tikrasis šifravimas paremtas iš ECDH gautu 256 bitų raktu.
Santrauka
| Saugumo mechanizmas | Nuo ko saugo |
|---|---|
| ECDH raktų apsikeitimas (P-256) | Raktų apsikeitimo srauto pasiklausymo |
| Laikinosios raktų poros | Tiesioginis slaptumas — ankstesni susiejimai išlieka saugūs |
| Vaizdinis patikros numeris (SAS) | Tarpininko (MITM) atakos per raktų apsikeitimą |
| SHA-256 maišos reikšmė kaip dokumento raktas | Kodo išgavimo iš Firestore |
| AES-256-GCM šifravimas | Signalizavimo duomenų pasiklausymo |
| Patvirtinimas abiejose pusėse | Vienpusio susiejimo be naudotojo žinios |
| DTLS-SRTP (WebRTC) | Garso ir vaizdo pasiklausymo |
Šie sluoksniai veikia kartu: ECDH apsaugo raktų apsikeitimą, patikros numeris — nuo MITM atakų, AES-256-GCM apsaugo signalizavimo duomenis, o WebRTC — garso ir vaizdo duomenis. Užpuolikui tektų nepastebėtam įveikti kelias šios apsaugos grandis.