Blog

Vinculació segura a Baby Monitor Timmy

Com funcionen plegats l'ECDH, el número SAS i la senyalització xifrada.

Abans que Baby Monitor Timmy transmeti àudio i vídeo, dos dispositius s'han de trobar i confiar l'un en l'altre. Aquest pas de vinculació és el moment més crític de tot el procés. Aquí explico com es vincula Timmy, quina criptografia hi ha al darrere i per què un atacant proper no pot prendre el control de la connexió sense que es noti.

El problema: com sap el meu dispositiu amb qui parla?

Quan dos dispositius es connecten per primer cop, la pregunta clau és: el dispositiu A parla realment amb el dispositiu B, o hi ha algú entremig? En criptografia, això s'anomena un atac d'intermediari (MITM).

Timmy ho resol mitjançant un intercanvi de claus Elliptic Curve Diffie-Hellman (ECDH) a través de Firebase, combinat amb verificació visual per part de l'usuari.

El diagrama següent mostra tot el procés de vinculació d'un cop d'ull:

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
      

Seqüència completa del protocol de vinculació — font editable: docs/diagrams/pairing-sequence.mmd

Pas 1: cada dispositiu genera un parell de claus

En obrir la pantalla de vinculació, cada dispositiu genera un parell de claus ECDH efímer a la corba P-256 (secp256r1):

Les claus es creen amb un generador de nombres aleatoris criptogràficament segur (Random.secure()) i només són vàlides per a aquest únic intent de vinculació. Es generen claus noves a cada intent.

Pas 2: intercanvi de claus públiques a través de Firebase

Perquè dos dispositius es puguin trobar, Timmy fa servir un codi de 4 caràcters com a punt de trobada. Aquest codi es pot detectar automàticament mitjançant Nearby Connections (Bluetooth Low Energy) o introduir-lo manualment. No té cap valor criptogràfic; només serveix perquè tots dos dispositius trobin el mateix document de Firebase Firestore.

Quan tots dos dispositius coneixen el codi, cadascun escriu la seva clau ECDH pública en un document de Firestore. Després, cada dispositiu llegeix la clau pública de l'altre des d'aquest document.

El més important: només s'envia la clau pública . La clau privada no surt mai del dispositiu. Qualsevol persona que observi el trànsit de Firebase veu claus públiques, però no pot calcular el secret compartit a partir d'aquestes. Això es basa en la dificultat del problema del logaritme discret de la corba el·líptica (ECDLP).

Pas 3: càlcul del secret compartit

Quan tots dos dispositius han detectat la clau pública de l'altre, calculen de manera independent el mateix secret compartit:

sharedSecret = ECDH(myPrivateKey, remotePublicKey)
             → 32 bytes (identical on both devices)

Les matemàtiques de les corbes el·líptiques garanteixen que tots dos càlculs donen el mateix resultat, tot i que cada dispositiu només coneix la seva pròpia clau privada i la clau pública de l'altre.

Pas 4: el número de verificació (SAS)

A partir del secret compartit, se'n deriva una cadena curta d'autenticació (SAS) — un número de dues xifres que es mostra als dos dispositius:

hash   = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100   → 00 to 99

Tots dos dispositius mostren el mateix número — per exemple, 42. L'usuari compara visualment si els números de totes dues pantalles coincideixen i, després, ho confirma a cada dispositiu per separat.

Per què un atacant no pot falsificar-ho

Un atacant d'intermediari hauria d'interceptar l'intercanvi de claus a Firebase. Concretament, hauria de:

  1. Substituir les claus públiques reals desades al document de Firestore per les seves
  2. Establir secrets compartits diferents amb cada dispositiu
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)
      

Detecció d'un atac d'intermediari mitjançant la discrepància del SAS — font editable: docs/diagrams/mitm-detection.mmd

En aquest cas, l'atacant calcula un secret compartit S_A amb el dispositiu A i un secret compartit diferent S_B amb el dispositiu B. Com que S_A ≠ S_B, els dispositius calculen números de verificació diferents.

L'atacant no pot fer que els números coincideixin perquè:

L'usuari veu números diferents a les pantalles i cancel·la la vinculació. En aquest punt, l'atac ja ha quedat al descobert.

Pas 5: finalització de la vinculació

Només després que l'usuari hagi confirmat la verificació als dos dispositius es completa la vinculació:

  1. Una clau de vinculació de 64 caràcters (256 bits) es deriva del secret compartit: SHA-256("pair:" + sharedSecret) → pairingKey
  2. La clau del document es deriva com a SHA-256("doc:" + pairingKey) i serveix com a clau del document de Firestore
  3. La clau de xifratge es deriva com a SHA-256("enc:" + pairingKey) i proporciona la clau AES-256-GCM per a la senyalització xifrada
  4. Tots dos dispositius desen la mateixa clau de vinculació i passen a la selecció de mode

A partir d'aquest moment, tots els intents de connexió posteriors (senyalització de Firestore, configuració de WebRTC) es xifren amb la clau AES-256-GCM compartida. La clau de vinculació no s'envia mai al backend; només se n'utilitza el hash SHA-256 com a identificador del document.

Arquitectura del sistema

El diagrama següent mostra els components implicats en la vinculació i la comunicació:

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

Visió general de l'arquitectura del sistema — font editable: docs/diagrams/pairing-architecture.mmd

Vies de comunicació en detall:

Alternativa: introducció manual del codi

Si el Bluetooth no està disponible (per exemple, en dispositius antics), el codi de 4 caràcters també es pot escriure manualment. La introducció manual utilitza el mateix intercanvi de claus ECDH i la mateixa verificació SAS que la vinculació automàtica. L'única diferència és que l'usuari llegeix i escriu el codi en lloc de detectar-lo mitjançant BLE.

Com que l'intercanvi de claus ECDH es fa a través de Firebase en tots dos casos, la seguretat és idèntica. El codi de 4 caràcters només és un punt de trobada; el xifratge real es basa en la clau de 256 bits derivada d'ECDH.

Resum

Mecanisme de seguretat Protegeix contra
Intercanvi de claus ECDH (P-256) Escolta del trànsit d'intercanvi de claus
Parells de claus efímers Confidencialitat directa: les vinculacions anteriors es mantenen segures
Número de verificació visual (SAS) Atac d'intermediari (MITM) durant l'intercanvi de claus
Hash SHA-256 com a clau del document Extracció del codi des de Firestore
Xifratge AES-256-GCM Escolta de les dades de senyalització
Confirmació als dos costats Vinculació unilateral sense que l'usuari ho sàpiga
DTLS-SRTP (WebRTC) Escolta d'àudio/vídeo

Aquestes capes encaixen entre si: l'ECDH protegeix l'intercanvi de claus, el número de verificació protegeix contra els MITM, AES-256-GCM protegeix la senyalització i WebRTC protegeix el contingut multimèdia. Un atacant hauria de trencar aquesta cadena en diversos punts sense que els dispositius ni els pares se n'adonessin.


Més articles