Antes de o Baby Monitor Timmy transmitir áudio e vídeo, os dois dispositivos têm de se encontrar e confiar um no outro. Esta fase de emparelhamento é o momento mais crítico de todo o processo. Aqui explico como o Timmy emparelha, que criptografia está por trás disso e porque é que um atacante próximo não consegue tomar controlo da ligação sem ser detetado.
O problema: como é que o meu dispositivo sabe com quem está a falar?
Quando dois dispositivos se ligam pela primeira vez, a questão central é: será que o Dispositivo A está mesmo a falar com o Dispositivo B, ou está alguém pelo meio? Em criptografia, isto chama-se um ataque man-in-the-middle (MITM).
O Timmy resolve isto através de uma troca de chaves Elliptic Curve Diffie-Hellman (ECDH) através do Firebase, combinada com verificação visual pelo utilizador.
O diagrama seguinte mostra, de relance, todo o processo 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
Passo 1: cada dispositivo gera um par de chaves
Ao abrir o ecrã de emparelhamento, cada dispositivo gera um par de chaves ECDH efémero na curva P-256 (secp256r1):
- Uma chave privada — fica exclusivamente no dispositivo
- Uma chave pública — é trocada através do Firebase
As chaves são criadas com um gerador de números aleatórios criptograficamente seguro (Random.secure())
e são válidas apenas para esta única tentativa de emparelhamento. São geradas chaves novas
em cada nova tentativa.
Passo 2: trocar chaves públicas através do Firebase
Para permitir que dois dispositivos se encontrem, o Timmy usa um código de 4 caracteres como ponto de encontro. Este código pode ser detetado automaticamente através de Nearby Connections (Bluetooth Low Energy) ou introduzido manualmente. Não tem qualquer valor criptográfico; serve apenas para fazer com que ambos os dispositivos encontrem o mesmo documento do Firebase Firestore.
Quando ambos os dispositivos conhecem o código, cada um escreve a sua chave ECDH pública num documento Firestorepartilhado. Depois, cada dispositivo lê a chave pública do outro dispositivo nesse documento.
Importante: apenas a chave pública é enviada. A chave privada nunca sai do dispositivo. Quem monitorizar o tráfego do Firebase vê as chaves públicas, mas não consegue calcular o segredo partilhado a partir delas. Isto baseia-se na dificuldade do Problema do Logaritmo Discreto em Curvas Elípticas (ECDLP).
Passo 3: calcular o segredo partilhado
Quando ambos os dispositivos descobrem a chave pública um do outro, calculam independentemente o mesmo segredo partilhado:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
A matemática das curvas elípticas garante que ambos os cálculos produzem o mesmo resultado, embora cada dispositivo conheça apenas a sua própria chave privada e a chave pública do outro.
Passo 4: o número de verificação (SAS)
A partir do segredo partilhado, é derivada uma Short Authentication String (SAS) — um número de dois algarismos apresentado em ambos os dispositivos:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Ambos os dispositivos apresentam o mesmo número — por exemplo, 42. O utilizador compara visualmente se os números nos dois ecrãs coincidem e confirma depois em cada dispositivo, individualmente.
Porque é que um atacante não consegue falsificar isto
Um atacante man-in-the-middle teria de intercetar a troca de chaves no Firebase. Em concreto, teria de:
- Substituir as verdadeiras chaves públicas guardadas no documento Firestore pelas suas
- Estabelecer segredos partilhados 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)
Deteção de man-in-the-middle através de divergência no SAS — fonte editável: docs/diagrams/mitm-detection.mmd
Neste caso, o atacante calcula um segredo partilhado S_A com o Dispositivo A e um
segredo partilhado diferente S_B com o Dispositivo B. Como S_A ≠ S_B,
os dispositivos calculam números de verificação diferentes.
O atacante não consegue fazer os números coincidir porque:
- Não conhece as chaves privadas dos dispositivos
- O SHA-256 não é reversível
- A probabilidade de uma coincidência aleatória é de apenas 1 em 100
O utilizador vê números diferentes nos ecrãs e cancela o emparelhamento. Nesse momento, o ataque tornou-se visível.
Passo 5: concluir o emparelhamento
Só depois de o utilizador confirmar a verificação em ambos os dispositivos é que o emparelhamento fica concluído:
- É derivada uma chave de emparelhamento de 64 caracteres (256 bits) a partir do segredo partilhado:
SHA-256("pair:" + sharedSecret) → pairingKey - A chave do documento é derivada como
SHA-256("doc:" + pairingKey)e serve como chave do documento Firestore - A chave de encriptação é derivada como
SHA-256("enc:" + pairingKey)e fornece a chave AES-256-GCM para sinalização encriptada - Ambos os dispositivos guardam a mesma chave de emparelhamento e avançam para a seleção de modo
A partir deste momento, todas as tentativas de ligação seguintes (sinalização Firestore, configuração WebRTC) são encriptadas com a chave AES-256-GCM partilhada. A chave de emparelhamento nunca é enviada para o backend; apenas o respetivo hash SHA-256 é usado como identificador do documento.
Arquitetura do sistema
O diagrama seguinte 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
Percursos de comunicação em detalhe:
- WebRTC ponto a ponto (linha espessa): o áudio, o vídeo e o DataChannel fluem diretamente entre os dispositivos — encriptados com DTLS-SRTP. Nenhum servidor vê estes dados.
- Firebase Firestore (linha contínua): os dados de emparelhamento (chaves ECDH) e a sinalização (SDP/ICE) passam pelo Firestore — encriptados de ponta a ponta com AES-256-GCM. O Firebase não consegue desencriptar os dados.
- Servidor STUN: ambos os dispositivos descobrem o seu endereço IP público para que possa ser estabelecida uma ligação direta ponto a ponto.
- Relay TURN: se não for possível uma ligação direta (por exemplo, através de dados móveis), o servidor TURN local ou Cloudflare selecionado retransmite os conteúdos multimédia encriptados. As credenciais de curta duração (24 h) são obtidas através de Firebase Cloud Functions.
- Bluetooth LE (linha pontilhada): o Nearby Connections deteta automaticamente dispositivos próximos — é transmitido apenas o código de encontro, sem material de chave.
Alternativa: introduçã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 introduzido manualmente. A introdução manual usa a mesma troca de chaves ECDH e a mesma verificação SAS que o emparelhamento automático. A única diferença é que o código é lido e introduzido pelo utilizador, em vez de ser detetado por BLE.
Como a troca de chaves ECDH acontece através do Firebase em ambos os casos, a segurança é idêntica. O código de 4 caracteres é apenas um ponto de encontro; a verdadeira encriptação baseia-se na chave de 256 bits derivada do ECDH.
Resumo
| Mecanismo de segurança | Protege contra |
|---|---|
| Troca de chaves ECDH (P-256) | Escuta do tráfego de troca de chaves |
| Pares de chaves efémeros | Confidencialidade futura (forward secrecy) — os emparelhamentos anteriores mantêm-se 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 |
| Encriptação AES-256-GCM | Escuta dos dados de sinalização |
| Confirmação nos dois lados | Emparelhamento unilateral sem conhecimento do utilizador |
| DTLS-SRTP (WebRTC) | Escuta de áudio/vídeo |
Estas camadas complementam-se: 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 os conteúdos multimédia. Um atacante teria de quebrar esta cadeia em vários pontos sem que os dispositivos ou os pais se apercebessem.