Antes de que Baby Monitor Timmy transmita audio y vídeo, los dos dispositivos deben encontrarse y confiar el uno en el otro. Este paso de emparejamiento es el momento más importante de todo el proceso. Aquí explico cómo se empareja Timmy, qué criptografía hay detrás y por qué un atacante cercano no puede tomar el control de la conexión sin que se note.
El problema: ¿cómo sabe mi dispositivo con quién está hablando?
Cuando dos dispositivos se conectan por primera vez, la pregunta clave es: ¿el dispositivo A está hablando realmente con el dispositivo B o hay alguien en medio? En criptografía, esto se llama un ataque de intermediario (MITM).
Timmy resuelve esto mediante un intercambio de claves Diffie-Hellman sobre curvas elípticas (ECDH) a través de Firebase, combinado con una verificación visual.
El siguiente diagrama muestra de un vistazo todo el proceso de emparejamiento:
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 del protocolo de emparejamiento — fuente editable: docs/diagrams/pairing-sequence.mmd
Paso 1: cada dispositivo genera un par de claves
Al abrir la pantalla de emparejamiento, cada dispositivo genera un par de claves ECDH efímero en la curva P-256 (secp256r1):
- Una clave privada — permanece exclusivamente en el dispositivo
- Una clave pública — se intercambia a través de Firebase
Las claves se crean con un generador de números aleatorios criptográficamente seguro (Random.secure())
y son válidas solo para este intento de emparejamiento. Se generan claves nuevas
para cada intento.
Paso 2: intercambio de claves públicas a través de Firebase
Para que dos dispositivos puedan encontrarse, Timmy usa un código de 4 caracteres como punto de encuentro. Este código se puede detectar automáticamente mediante Nearby Connections (Bluetooth Low Energy) o introducirse manualmente. No tiene ningún valor criptográfico; solo hace que ambos dispositivos encuentren el mismo documento de Firebase Firestore.
Cuando ambos dispositivos conocen el código, cada uno escribe su clave ECDH pública en un documento de Firestore. Después, cada dispositivo lee la clave pública del otro desde ese documento.
Lo importante: solo se envía la clave pública . La clave privada nunca sale del dispositivo. Quien observe el tráfico de Firebase verá claves públicas, pero no podrá calcular el secreto compartido a partir de ellas. Esto se basa en la dificultad del problema del logaritmo discreto en curvas elípticas (ECDLP).
Paso 3: cálculo del secreto compartido
Cuando ambos dispositivos han encontrado la clave pública del otro, calculan de forma independiente el mismo secreto compartido:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
Las matemáticas de las curvas elípticas garantizan que ambos cálculos den el mismo resultado, aunque cada dispositivo solo conozca su propia clave privada y la clave pública del otro.
Paso 4: el número de verificación (SAS)
A partir del secreto compartido se deriva una cadena corta de autenticación (SAS) — un número de dos cifras que se muestra en ambos dispositivos:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Los dos dispositivos muestran el mismo número — por ejemplo, 42. Se comprueba visualmente si los números de ambas pantallas coinciden y, a continuación, se confirma en cada dispositivo por separado.
Por qué un atacante no puede falsificarlo
Un atacante intermediario tendría que interceptar el intercambio de claves en Firebase. En concreto, tendría que:
- Sustituir las claves públicas reales guardadas en el documento de Firestore por las suyas
- Establecer secretos compartidos distintos 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 ataque de intermediario mediante falta de coincidencia del SAS — fuente editable: docs/diagrams/mitm-detection.mmd
En este caso, el atacante calcula un secreto compartido S_A con el dispositivo A y un
secreto compartido diferente S_B con el dispositivo B. Como S_A ≠ S_B,
los dispositivos calculan números de verificación diferentes.
El atacante no puede hacer que los números coincidan porque:
- No conoce las claves privadas de los dispositivos
- SHA-256 no es reversible
- La probabilidad de una coincidencia aleatoria es de solo 1 entre 100
Al ver números distintos en las pantallas, se cancela el emparejamiento. En ese momento, el ataque queda al descubierto.
Paso 5: completar el emparejamiento
Solo después de confirmar la verificación en ambos dispositivos se completa el emparejamiento:
- Se deriva una clave de emparejamiento de 64 caracteres (256 bits) a partir del secreto compartido:
SHA-256("pair:" + sharedSecret) → pairingKey - La clave del documento se deriva como
SHA-256("doc:" + pairingKey)y sirve como clave del documento de Firestore - La clave de cifrado se deriva como
SHA-256("enc:" + pairingKey)y proporciona la clave AES-256-GCM para la señalización cifrada - Ambos dispositivos guardan la misma clave de emparejamiento y pasan a la selección de modo
A partir de este momento, todos los intentos de conexión posteriores (señalización de Firestore, configuración de WebRTC) se cifran con la clave AES-256-GCM compartida. La clave de emparejamiento nunca se envía al backend; solo se utiliza su hash SHA-256 como identificador del documento.
Arquitectura del sistema
El siguiente diagrama muestra los componentes que intervienen en el emparejamiento y la 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
Resumen de la arquitectura del sistema — fuente editable: docs/diagrams/pairing-architecture.mmd
Rutas de comunicación en detalle:
- WebRTC entre pares (línea gruesa): el audio, el vídeo y el DataChannel fluyen directamente entre los dispositivos, cifrados con DTLS-SRTP. Ningún servidor ve estos datos.
- Firebase Firestore (línea continua): los datos de emparejamiento (claves ECDH) y la señalización (SDP/ICE) pasan por Firestore, cifrados de extremo a extremo con AES-256-GCM. Firebase no puede descifrar los datos.
- Servidor STUN: ambos dispositivos descubren su dirección IP pública para poder establecer una conexión directa entre pares.
- Retransmisión TURN: si no es posible establecer una conexión directa (por ejemplo, al usar datos móviles), el servidor TURN seleccionado, ya sea local o de Cloudflare, retransmite el contenido multimedia cifrado. Las credenciales de corta duración (24 h) se obtienen mediante Firebase Cloud Functions.
- Bluetooth LE (línea de puntos): Nearby Connections detecta automáticamente los dispositivos cercanos y solo transmite el código de encuentro, sin material criptográfico.
Alternativa: introducción manual del código
Si Bluetooth no está disponible (por ejemplo, en dispositivos antiguos), el código de 4 caracteres también se puede introducir manualmente. La introducción manual usa el mismo intercambio de claves ECDH y la misma verificación SAS que el emparejamiento automático. La única diferencia es que el código se lee y se introduce manualmente, en lugar de detectarse mediante BLE.
Como el intercambio de claves ECDH se realiza a través de Firebase en ambos casos, la seguridad es idéntica. El código de 4 caracteres es solo un punto de encuentro; el cifrado real se basa en la clave de 256 bits derivada de ECDH.
Resumen
| Mecanismo de seguridad | Protege frente a |
|---|---|
| Intercambio de claves ECDH (P-256) | Interceptación del tráfico del intercambio de claves |
| Pares de claves efímeros | Secreto hacia adelante: los emparejamientos anteriores siguen estando protegidos |
| Número de verificación visual (SAS) | Ataque de intermediario (MITM) durante el intercambio de claves |
| Hash SHA-256 como clave del documento | Extracción de códigos desde Firestore |
| Cifrado AES-256-GCM | Interceptación de los datos de señalización |
| Confirmación en ambos dispositivos | Emparejamiento unilateral sin conocimiento del usuario |
| DTLS-SRTP (WebRTC) | Interceptación del audio y el vídeo |
Estas capas encajan entre sí: ECDH protege el intercambio de claves, el número de verificación protege frente a MITM, AES-256-GCM protege la señalización y WebRTC protege el contenido multimedia. Un atacante tendría que romper esta cadena en varios puntos sin que los dispositivos ni los padres lo notaran.