Antes de que Baby Monitor Timmy transmita audio e vídeo, dous dispositivos teñen que atoparse e confiar un no outro. Este emparellamento é o momento máis crítico de todo o proceso. Aquí explico como se emparella Timmy, que criptografía hai detrás e por que un atacante próximo non pode tomar o control da conexión sen que se note.
O problema: como sabe o meu dispositivo con quen está falando?
Cando dous dispositivos se conectan por primeira vez, a pregunta central é: o dispositivo A está a falar realmente co dispositivo B ou hai alguén no medio? En criptografía, isto chámase ataque de intermediario (MITM).
Timmy resolve isto mediante un intercambio de claves Elliptic Curve Diffie-Hellman (ECDH) a través de Firebase, combinado con verificación visual por parte da persoa usuaria.
O seguinte diagrama mostra dunha ollada todo o proceso de emparellamento:
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
Secuencia completa do protocolo de emparellamento — fonte editable: docs/diagrams/pairing-sequence.mmd
Paso 1: cada dispositivo xera un par de claves
Ao abrir a pantalla de emparellamento, cada dispositivo xera un par de claves ECDH efémero na curva P-256 (secp256r1):
- Unha clave privada — permanece exclusivamente no dispositivo
- Unha clave pública — intercámbiase a través de Firebase
As claves créanse cun xerador de números aleatorios criptograficamente seguro (Random.secure())
e son válidas só para este intento de emparellamento. Xéranse claves novas
para cada intento.
Paso 2: intercambio de claves públicas a través de Firebase
Para que dous dispositivos poidan atoparse, Timmy usa un código de 4 caracteres como punto de encontro. Este código pódese descubrir automaticamente mediante Nearby Connections (Bluetooth Low Energy) ou introducirse manualmente. Non ten ningún valor criptográfico; só permite que ambos dispositivos atopen o mesmo documento de Firebase Firestore.
Unha vez que ambos dispositivos coñecen o código, cada un escribe a súa clave ECDH pública nun documento de Firestore. Despois, cada dispositivo le a clave pública do outro nese documento.
O importante: só se envía a clave pública . A clave privada nunca sae do dispositivo. Quen observe o tráfico de Firebase verá claves públicas, pero non poderá calcular o segredo compartido a partir delas. Isto baséase na dificultade do problema do logaritmo discreto en curvas elípticas (ECDLP).
Paso 3: cálculo do segredo compartido
Unha vez que ambos dispositivos descubriron a clave pública do outro, calculan de forma independente o mesmo segredo compartido:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
As matemáticas das curvas elípticas garanten que ambos cálculos dean o mesmo resultado, aínda que cada dispositivo só coñeza a súa propia clave privada e a clave pública do outro.
Paso 4: o número de verificación (SAS)
Do segredo compartido derívase unha cadea curta de autenticación (SAS) — un número de dúas cifras que se mostra nos dous dispositivos:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Os dous dispositivos mostran o mesmo número — por exemplo, 42. A persoa usuaria comproba visualmente se os números de ambas pantallas coinciden e despois confirma en cada dispositivo por separado.
Por que un atacante non pode falsificar isto
Un atacante intermediario tería que interceptar o intercambio de claves en Firebase. En concreto, tería que:
- Substituír as claves públicas reais almacenadas no documento de Firestore polas súas
- Establecer segredos compartidos separados con cada dispositivo
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ón de intermediario mediante discrepancia do SAS — fonte editable: docs/diagrams/mitm-detection.mmd
Neste caso, o atacante calcula un segredo compartido S_A co dispositivo A e un
segredo compartido diferente S_B co dispositivo B. Como S_A ≠ S_B,
os dispositivos calculan números de verificación diferentes.
O atacante non pode facer que os números coincidan porque:
- Non coñece as claves privadas dos dispositivos
- SHA-256 non é reversible
- A probabilidade dunha coincidencia aleatoria é só de 1 entre 100
A persoa usuaria ve números distintos nas pantallas e cancela o emparellamento. Nese momento, o ataque faise visible.
Paso 5: completar o emparellamento
Só despois de que a persoa usuaria confirmase a verificación en ambos dispositivos se completa o emparellamento:
- Unha clave de emparellamento de 64 caracteres (256 bits) derívase do segredo compartido:
SHA-256("pair:" + sharedSecret) → pairingKey - A clave do documento derívase como
SHA-256("doc:" + pairingKey)e serve como clave do documento de Firestore - A clave de cifrado derívase como
SHA-256("enc:" + pairingKey)e proporciona a clave AES-256-GCM para a sinalización cifrada - Ambos dispositivos gardan a mesma clave de emparellamento e pasan á selección de modo
A partir deste momento, todos os intentos de conexión posteriores (sinalización de Firestore, configuración de WebRTC) cífranse coa clave AES-256-GCM compartida. A clave de emparellamento nunca se envía ao backend; só se usa o seu hash SHA-256 como identificador do documento.
Arquitectura do sistema
O seguinte diagrama mostra os compoñentes implicados no emparellamento e na comunicación:
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
Vista xeral da arquitectura do sistema — fonte editable: docs/diagrams/pairing-architecture.mmd
Vías de comunicación en detalle:
- WebRTC entre pares (liña grosa): o audio, o vídeo e DataChannel flúen directamente entre os dispositivos — cifrados con DTLS-SRTP. Ningún servidor ve estes datos.
- Firebase Firestore (liña continua): os datos de emparellamento (claves ECDH) e a sinalización (SDP/ICE) pasan por Firestore — cifrados de extremo a extremo con AES-256-GCM. Firebase non pode descifrar os datos.
- Servidor STUN: ambos dispositivos descobren o seu enderezo IP público para poder establecer unha conexión directa entre pares.
- Relé TURN: se non é posible unha conexión directa (por exemplo, con datos móbiles), o servidor TURN local ou de Cloudflare seleccionado retransmite o contido multimedia cifrado. As credenciais de curta duración (24 h) obtéñense mediante Firebase Cloud Functions.
- Bluetooth LE (liña de puntos): Nearby Connections descobre automaticamente os dispositivos próximos — só se transmite o código de encontro, sen material de claves.
Alternativa: introdución manual do código
Se o Bluetooth non está dispoñible (por exemplo, en dispositivos antigos), o código de 4 caracteres tamén se pode escribir manualmente. A introdución manual usa o mesmo intercambio de claves ECDH e a mesma verificación SAS que o emparellamento automático. A única diferenza é que o código o le e escribe a persoa usuaria, en vez de descubrirse mediante BLE.
Como o intercambio de claves ECDH se realiza a través de Firebase en ambos casos, a seguridade é idéntica. O código de 4 caracteres é só un punto de encontro; o cifrado real baséase na clave de 256 bits derivada de ECDH.
Resumo
| Mecanismo de seguridade | Protexe contra |
|---|---|
| Intercambio de claves ECDH (P-256) | Escoita do tráfico de intercambio de claves |
| Pares de claves efémeros | Segredo perfecto cara adiante — os emparellamentos anteriores seguen seguros |
| Número de verificación visual (SAS) | Ataque de intermediario (MITM) durante o intercambio de claves |
| Hash SHA-256 como clave do documento | Extracción de códigos desde Firestore |
| Cifrado AES-256-GCM | Escoita dos datos de sinalización |
| Confirmación nos dous lados | Emparellamento unilateral sen que a persoa usuaria o saiba |
| DTLS-SRTP (WebRTC) | Escoita de audio/vídeo |
Estas capas encaixan entre si: ECDH protexe o intercambio de claves, o número de verificación protexe contra MITM, AES-256-GCM protexe a sinalización e WebRTC protexe o contido multimedia. Un atacante tería que romper esta cadea en varios puntos sen que os dispositivos nin as familias se decatasen.