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):
- Una clau privada — es manté exclusivament al dispositiu
- Una clau pública — s'intercanvia a través de Firebase
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:
- Substituir les claus públiques reals desades al document de Firestore per les seves
- 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è:
- No coneix les claus privades dels dispositius
- SHA-256 no és reversible
- La probabilitat d'una coincidència aleatòria és només 1 de cada 100
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ó:
- Una clau de vinculació de 64 caràcters (256 bits) es deriva del secret compartit:
SHA-256("pair:" + sharedSecret) → pairingKey - La clau del document es deriva com a
SHA-256("doc:" + pairingKey)i serveix com a clau del document de Firestore - La clau de xifratge es deriva com a
SHA-256("enc:" + pairingKey)i proporciona la clau AES-256-GCM per a la senyalització xifrada - 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:
- WebRTC entre iguals (línia gruixuda): l'àudio, el vídeo i el DataChannel flueixen directament entre dispositius — xifrats amb DTLS-SRTP. Cap servidor no veu aquestes dades.
- Firebase Firestore (línia contínua): les dades de vinculació (claus ECDH) i la senyalització (SDP/ICE) passen per Firestore — xifrades d'extrem a extrem amb AES-256-GCM. Firebase no pot desxifrar les dades.
- Servidor STUN: tots dos dispositius descobreixen la seva adreça IP pública per poder establir una connexió directa entre iguals.
- Relleu TURN: si no és possible una connexió directa (per exemple, amb dades mòbils), el servidor TURN local o de Cloudflare seleccionat retransmet el contingut multimèdia xifrat. Les credencials de curta durada (24 h) s'obtenen mitjançant Firebase Cloud Functions.
- Bluetooth LE (línia de punts): Nearby Connections detecta automàticament els dispositius propers — només es transmet el codi de trobada, sense cap material de claus.
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.