Înainte ca Baby Monitor Timmy să transmită sunet și imagine, două dispozitive trebuie să se găsească și să aibă încredere unul în celălalt. Acest pas de asociere este momentul cel mai important din întregul proces. Aici explic cum realizează Timmy asocierea, ce mecanisme criptografice stau la bază și de ce un atacator aflat în apropiere nu poate prelua conexiunea fără să fie observat.
Problema: de unde știe dispozitivul meu cu cine vorbește?
Când două dispozitive se conectează pentru prima dată, întrebarea esențială este: Dispozitivul A comunică într-adevăr cu Dispozitivul B sau se află cineva între ele? În criptografie, aceasta se numește atac de tip man-in-the-middle (MITM).
Timmy rezolvă acest lucru printr-un schimb de chei Elliptic Curve Diffie-Hellman (ECDH) prin Firebase, combinat cu verificarea vizuală de către utilizator..
Diagrama următoare arată, pe scurt, întregul flux de asociere:
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
Secvența completă a protocolului de asociere — sursă editabilă: docs/diagrams/pairing-sequence.mmd
Pasul 1: fiecare dispozitiv generează o pereche de chei
Când se deschide ecranul de asociere, fiecare dispozitiv generează o pereche temporară de chei ECDH pe curba P-256 (secp256r1):
- O cheie privată — rămâne exclusiv pe dispozitiv
- O cheie publică — este transmisă prin Firebase
Cheile sunt create folosind un generator de numere aleatorii sigur din punct de vedere criptograficRandom.secure()și sunt valabile doar pentru această singură încercare de asociere. Se generează chei noi
la fiecare încercare nouă.
Pasul 2: schimbul cheilor publice prin Firebase
Pentru ca două dispozitive să se găsească, Timmy folosește un cod din 4 caractere ca punct de întâlnire. Acest cod poate fi descoperit automat prin Nearby Connections (Bluetooth Low Energy) sau introdus manual. Nu are nicio valoare criptografică; doar ajută ambele dispozitive să găsească același document Firebase Firestore.
După ce ambele dispozitive cunosc codul, fiecare își scrie cheia publică ECDH într-un document Firestore. Apoi fiecare dispozitiv citește cheia publică a celuilalt din acel document.
Important: este trimisă doar cheia publică . Cheia privată nu părăsește niciodată dispozitivul. Oricine urmărește traficul Firebase vede chei publice, dar nu poate calcula secretul comun din acestea. Acest lucru se bazează pe dificultatea problemei logaritmului discret pe curbe eliptice (ECDLP).
Pasul 3: calcularea secretului comun
După ce ambele dispozitive au descoperit cheia publică a celuilalt, ele calculează independent același secret comun:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
Matematica curbelor eliptice garantează că ambele calcule dau același rezultat, chiar dacă fiecare dispozitiv cunoaște doar propria cheie privată și cheia publică a celuilalt.
Pasul 4: numărul de verificare (SAS)
Din secretul comun se obține un șir scurt de autentificare (SAS) — un număr de două cifre afișat pe ambele dispozitive.
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Ambele dispozitive afișează același număr. 42Utilizatorul compară vizual numerele de pe cele două ecrane, apoi confirmă pe fiecare dispozitiv în parte.
De ce un atacator nu poate falsifica acest lucru
Un atacator man-in-the-middle ar trebui să intercepteze schimbul de chei în Firebase. Mai exact, ar trebui să:
- Înlocuiască cheile publice reale stocate în documentul Firestore cu propriile chei
- Stabilească secrete comune separate cu fiecare dispozitiv
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)
Detectarea man-in-the-middle prin neconcordanța SAS — sursă editabilă: docs/diagrams/mitm-detection.mmd
În acest caz, atacatorul calculează un secret comun S_A cu Dispozitivul A și un
secret comun diferit S_B cu Dispozitivul B. Ca urmare, S_A ≠ S_Bdispozitivele calculează numere de verificare diferite..
Atacatorul nu poate face ca numerele să coincidă deoarece:
- Nu cunoaște cheile private ale dispozitivelor
- SHA-256 nu este reversibil
- Probabilitatea unei potriviri întâmplătoare este de doar 1 la 100
Utilizatorul vede numere diferite pe ecrane și anulează asocierea. În acel moment, atacul devine vizibil.
Pasul 5: finalizarea asocierii
Abia după ce utilizatorul a confirmat verificarea pe ambele dispozitive se finalizează asocierea:
- O cheie de asociere de 64 de caractere (256 de biți) este derivată din secretul comun.
SHA-256("pair:" + sharedSecret) → pairingKey - Se derivă și o cheie pentru document.
SHA-256("doc:" + pairingKey)Aceasta este folosită drept cheia documentului Firestore. - Se derivă și cheia de criptare.
SHA-256("enc:" + pairingKey)Aceasta furnizează cheia AES-256-GCM pentru semnalizarea criptată. - Ambele dispozitive stochează aceeași cheie de asociere și trec la selectarea modului.
Din acest moment, toate încercările ulterioare de conectare (semnalizarea Firestore, configurarea WebRTC) sunt criptate cu cheia AES-256-GCM comună. Cheia de asociere nu este trimisă niciodată către backend; doar hash-ul ei SHA-256 este folosit ca identificator al documentului.
Arhitectura sistemului
Diagrama următoare arată componentele implicate în asociere și comunicare:
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
Prezentare generală a arhitecturii sistemului — sursă editabilă: docs/diagrams/pairing-architecture.mmd
Căile de comunicare, în detaliu:
- WebRTC peer-to-peer (linie groasă): Fluxurile audio, video și DataChannel circulă direct între dispozitive și sunt criptate cu DTLS-SRTP. Niciun server nu vede aceste date.
- Firebase Firestore (linie continuă): Datele de asociere (cheile ECDH) și semnalizarea (SDP/ICE) trec prin Firestore și sunt criptate de la un capăt la altul cu AES-256-GCM. Firebase nu poate decripta datele.
- Server STUN: Ambele dispozitive își descoperă adresa IP publică pentru a putea stabili o conexiune directă peer-to-peer.
- Releu TURN: Dacă nu se poate stabili o conexiune directă (de exemplu, prin rețeaua mobilă), serverul TURN local sau serverul Cloudflare TURN selectat retransmite conținutul media criptat. Datele de autentificare cu valabilitate de 24 h sunt obținute prin Firebase Cloud Functions.
- Bluetooth LE (linie punctată): Nearby Connections descoperă automat dispozitivele din apropiere — este transmis doar codul de întâlnire, fără material criptografic.
Alternativă: introducerea manuală a codului
Dacă Bluetooth nu este disponibil (de exemplu, pe dispozitive mai vechi), codul din 4 caractere poate fi introdus și manual. Introducerea manuală folosește același schimb de chei ECDH și aceeași verificare SAS ca asocierea automată. Singura diferență este că utilizatorul citește și introduce codul, în loc ca acesta să fie descoperit prin BLE.
Deoarece schimbul de chei ECDH are loc prin Firebase în ambele cazuri, securitatea este identică. Codul din 4 caractere este doar un punct de întâlnire; criptarea reală se bazează pe cheia de 256 de biți derivată din ECDH.
Rezumat
| Mecanism de securitate | Protejează împotriva |
|---|---|
| Schimb de chei ECDH (P-256) | Interceptării traficului de schimb de chei |
| Perechi temporare de chei | Secretizare directă — asocierile anterioare rămân protejate |
| Număr de verificare vizuală (SAS) | Atacurilor de tip man-in-the-middle (MITM) în timpul schimbului de chei |
| Hash SHA-256 ca cheie de document | Extragerii codului din Firestore |
| Criptare AES-256-GCM | Interceptării datelor de semnalizare |
| Confirmare pe ambele părți | Asocierii unilaterale fără știrea utilizatorului |
| DTLS-SRTP (WebRTC) | Interceptării fluxurilor audio și video |
Aceste straturi funcționează împreună: ECDH protejează schimbul de chei, numărul de verificare protejează împotriva MITM, AES-256-GCM protejează semnalizarea, iar WebRTC protejează conținutul media. Un atacator ar trebui să rupă acest lanț în mai multe locuri fără ca dispozitivele sau părinții să observe.