Blog

Emparejamiento seguro en Baby Monitor Timmy

Cómo funcionan juntos ECDH, el número SAS y la señalización cifrada.

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

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:

  1. Sustituir las claves públicas reales guardadas en el documento de Firestore por las suyas
  2. 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:

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:

  1. Se deriva una clave de emparejamiento de 64 caracteres (256 bits) a partir del secreto compartido: SHA-256("pair:" + sharedSecret) → pairingKey
  2. La clave del documento se deriva como SHA-256("doc:" + pairingKey) y sirve como clave del documento de Firestore
  3. La clave de cifrado se deriva como SHA-256("enc:" + pairingKey) y proporciona la clave AES-256-GCM para la señalización cifrada
  4. 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:

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.


Más artículos