Blog

Asociere securizată în Baby Monitor Timmy

Cum funcționează împreună ECDH, numărul SAS și semnalizarea criptată.

Î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):

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ă:

  1. Înlocuiască cheile publice reale stocate în documentul Firestore cu propriile chei
  2. 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:

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:

  1. O cheie de asociere de 64 de caractere (256 de biți) este derivată din secretul comun. SHA-256("pair:" + sharedSecret) → pairingKey
  2. Se derivă și o cheie pentru document. SHA-256("doc:" + pairingKey) Aceasta este folosită drept cheia documentului Firestore.
  3. Se derivă și cheia de criptare. SHA-256("enc:" + pairingKey) Aceasta furnizează cheia AES-256-GCM pentru semnalizarea criptată.
  4. 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:

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.


Mai multe articole