Blog

Emparelhamento seguro no Baby Monitor Timmy

Como o ECDH, o número SAS e a sinalização criptografada funcionam juntos.

Antes de o Baby Monitor Timmy transmitir áudio e vídeo, os dois dispositivos precisam se encontrar e confiar um no outro. Esta etapa de emparelhamento é o momento mais crítico de todo o processo. Aqui explico como o Timmy faz o emparelhamento, qual criptografia está por trás dele e por que alguém próximo não consegue assumir a conexão sem ser percebido.

O problema: como meu dispositivo sabe com quem está falando?

Quando dois dispositivos se conectam pela primeira vez, a pergunta central é: o Dispositivo A está mesmo falando com o Dispositivo B ou há alguém no meio? Em criptografia, isso é chamado de ataque man-in-the-middle (MITM).

O Timmy resolve isso com uma troca de chaves Elliptic Curve Diffie-Hellman (ECDH) via Firebase, combinada com verificação visual pelo usuário.

O diagrama a seguir apresenta uma visão geral de todo o fluxo de emparelhamento:

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
      

Sequência completa do protocolo de emparelhamento — fonte editável: docs/diagrams/pairing-sequence.mmd

Etapa 1: cada dispositivo gera um par de chaves

Ao abrir a tela de emparelhamento, cada dispositivo gera um par de chaves ECDH efêmero na curva P-256 (secp256r1):

As chaves são criadas usando um gerador de números aleatórios criptograficamente seguro (Random.secure()) e são válidas apenas para esta tentativa de emparelhamento. Novas chaves são geradas a cada nova tentativa.

Etapa 2: trocar chaves públicas via Firebase

Para que dois dispositivos se encontrem, o Timmy usa um código de 4 caracteres como ponto de encontro. Esse código pode ser detectado automaticamente por Nearby Connections (Bluetooth Low Energy) ou digitado manualmente. Ele não tem valor criptográfico; serve apenas para que os dois dispositivos encontrem o mesmo documento do Firebase Firestore.

Quando os dois dispositivos conhecem o código, cada um grava sua chave ECDH pública em um documento do Firestore. Depois, cada dispositivo lê a chave pública do outro nesse documento.

O ponto crucial: apenas a chave pública é enviada. A chave privada nunca sai do dispositivo. Quem observa o tráfego do Firebase vê chaves públicas, mas não consegue calcular o segredo compartilhado a partir delas. Isso depende da dificuldade do Problema do Logaritmo Discreto em Curvas Elípticas (ECDLP).

Etapa 3: calcular o segredo compartilhado

Assim que os dois dispositivos descobrem a chave pública um do outro, eles calculam de forma independente o mesmo segredo compartilhado:

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

A matemática das curvas elípticas garante que os dois cálculos produzam o mesmo resultado, mesmo que cada dispositivo conheça apenas sua própria chave privada e a chave pública do outro.

Etapa 4: o número de verificação (SAS)

A partir do segredo compartilhado, é derivada uma sequência curta de autenticação (Short Authentication String, SAS) — um número de dois dígitos exibido nos dois dispositivos:

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

Os dois dispositivos mostram o mesmo número — por exemplo, 42. O usuário confere visualmente se os números nas duas telas são iguais e então confirma em cada dispositivo separadamente.

Por que alguém não consegue falsificar isso

Em um ataque man-in-the-middle, seria preciso interceptar a troca de chaves no Firebase. Especificamente, seria necessário:

  1. Substituir as chaves públicas reais armazenadas no documento do Firestore pelas próprias chaves
  2. Estabelecer segredos compartilhados separados com 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)
      

Detecção de man-in-the-middle pela divergência do SAS — fonte editável: docs/diagrams/mitm-detection.mmd

Nesse caso, o invasor calcula um segredo compartilhado S_A com o Dispositivo A e um segredo compartilhado diferente S_B com o Dispositivo B. Como S_A ≠ S_B, os dispositivos calculam números de verificação diferentes.

O invasor não consegue fazer os números coincidirem porque:

O usuário vê números diferentes nas telas e cancela o emparelhamento. Nesse ponto, o ataque se tornou visível.

Etapa 5: concluir o emparelhamento

Somente depois que o usuário confirma a verificação em ambos os dispositivos , o emparelhamento é concluído:

  1. Uma chave de emparelhamento de 64 caracteres (256 bits) é derivada do segredo compartilhado: SHA-256("pair:" + sharedSecret) → pairingKey
  2. A chave do documento é derivada como SHA-256("doc:" + pairingKey) e serve como chave do documento do Firestore
  3. A chave de criptografia é derivada como SHA-256("enc:" + pairingKey) e fornece a chave AES-256-GCM para sinalização criptografada
  4. Os dois dispositivos armazenam a mesma chave de emparelhamento e seguem para a seleção de modo

A partir daí, todas as novas tentativas de conexão (sinalização pelo Firestore, configuração do WebRTC) são criptografadas com a chave AES-256-GCM compartilhada. A chave de emparelhamento nunca é enviada ao backend; apenas seu hash SHA-256 é usado como identificador do documento.

Arquitetura do sistema

O diagrama a seguir mostra os componentes envolvidos no emparelhamento e na comunicação:

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

Visão geral da arquitetura do sistema — fonte editável: docs/diagrams/pairing-architecture.mmd

Caminhos de comunicação em detalhe:

Alternativa: digitação manual do código

Se o Bluetooth não estiver disponível (por exemplo, em dispositivos mais antigos), o código de 4 caracteres também pode ser digitado manualmente. A digitação manual usa a mesma troca de chaves ECDH e a mesma verificação SAS do emparelhamento automático. A única diferença é que o código é lido e digitado pelo usuário, em vez de ser detectado por BLE.

Como a troca de chaves ECDH acontece via Firebase nos dois casos, a segurança é idêntica. O código de 4 caracteres é apenas um ponto de encontro; a criptografia real se baseia na chave de 256 bits derivada do ECDH.

Resumo

Mecanismo de segurança Protege contra
Troca de chaves ECDH (P-256) Espionagem do tráfego da troca de chaves
Pares de chaves efêmeros Sigilo futuro — emparelhamentos anteriores continuam seguros
Número de verificação visual (SAS) Man-in-the-middle (MITM) durante a troca de chaves
Hash SHA-256 como chave do documento Extração de código do Firestore
Criptografia AES-256-GCM Espionagem dos dados de sinalização
Confirmação em ambos os dispositivos Emparelhamento unilateral sem o conhecimento do usuário
DTLS-SRTP (WebRTC) Espionagem de áudio/vídeo

Essas camadas trabalham juntas: o ECDH protege a troca de chaves, o número de verificação protege contra MITM, o AES-256-GCM protege a sinalização e o WebRTC protege a mídia. Para ter sucesso, um invasor teria de quebrar essa cadeia em vários pontos sem que os dispositivos ou os pais percebessem.


Mais artigos