Prima che Baby Monitor Timmy trasmetta audio e video, due dispositivi devono trovarsi e fidarsi l'uno dell'altro. Questo abbinamento è il momento più critico dell'intero processo. Qui spiego come Timmy abbina i dispositivi, quale crittografia c'è dietro e perché un aggressore nelle vicinanze non può prendere il controllo della connessione senza farsi notare.
Il problema: come fa il mio dispositivo a sapere con chi sta parlando?
Quando due dispositivi si collegano per la prima volta, la domanda centrale è: il dispositivo A sta davvero parlando con il dispositivo B, oppure qualcuno si trova nel mezzo? In crittografia, questo si chiama un attacco man-in-the-middle (MITM).
Timmy risolve il problema con uno scambio di chiavi Elliptic Curve Diffie-Hellman (ECDH) tramite Firebase, combinato con una verifica visiva da parte dell'utente.
Il diagramma seguente mostra l'intero flusso di abbinamento in sintesi:
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
Sequenza completa del protocollo di abbinamento — sorgente modificabile: docs/diagrams/pairing-sequence.mmd
Passaggio 1: ogni dispositivo genera una coppia di chiavi
Quando apre la schermata di abbinamento, ogni dispositivo genera una coppia di chiavi ECDH effimera sulla curva P-256 (secp256r1):
- Una chiave privata — rimane esclusivamente sul dispositivo
- Una chiave pubblica — viene scambiata tramite Firebase
Le chiavi vengono create usando un generatore di numeri casuali crittograficamente sicuro (Random.secure())
e sono valide solo per questo singolo tentativo di abbinamento. Vengono generate nuove chiavi
per ogni nuovo tentativo.
Passaggio 2: scambio delle chiavi pubbliche tramite Firebase
Per permettere a due dispositivi di trovarsi, Timmy usa un codice di 4 caratteri come punto d'incontro. Questo codice può essere rilevato automaticamente tramite Nearby Connections (Bluetooth Low Energy) oppure inserito manualmente. Non ha alcun valore crittografico; serve solo a far trovare a entrambi i dispositivi lo stesso documento Firebase Firestore.
Quando entrambi i dispositivi conoscono il codice, ciascuno scrive la propria chiave ECDH pubblica in un documento Firestore condiviso. Poi ogni dispositivo legge da quel documento la chiave pubblica dell'altro dispositivo.
Fondamentale: viene inviata solo la chiave pubblica . La chiave privata non lascia mai il dispositivo. Chiunque osservi il traffico Firebase vede le chiavi pubbliche, ma non può calcolare il segreto condiviso a partire da esse. Questo dipende dalla difficoltà del problema del logaritmo discreto su curve ellittiche (ECDLP).
Passaggio 3: calcolare il segreto condiviso
Dopo aver rilevato le rispettive chiavi pubbliche, entrambi i dispositivi calcolano indipendentemente lo stesso segreto condiviso:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
La matematica delle curve ellittiche garantisce che entrambi i calcoli producano lo stesso risultato, anche se ogni dispositivo conosce solo la propria chiave privata e la chiave pubblica dell'altro.
Passaggio 4: il numero di verifica (SAS)
Dal segreto condiviso si ricava una Short Authentication String (SAS) — un numero di due cifre visualizzato su entrambi i dispositivi:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
Entrambi i dispositivi mostrano lo stesso numero — per esempio, 42. L'utente confronta visivamente i numeri sui due schermi e, se corrispondono, conferma su ciascun dispositivo separatamente.
Perché un aggressore non può falsificarlo
Un aggressore man-in-the-middle dovrebbe intercettare lo scambio di chiavi in Firebase. In particolare, dovrebbe:
- Sostituire le vere chiavi pubbliche memorizzate nel documento Firestore con le proprie
- Stabilire segreti condivisi separati con ciascun 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)
Rilevamento man-in-the-middle tramite mancata corrispondenza del SAS — sorgente modificabile: docs/diagrams/mitm-detection.mmd
In questo caso, l'aggressore calcola un segreto condiviso S_A con il dispositivo A e un
segreto condiviso diverso S_B con il dispositivo B. Poiché S_A ≠ S_B,
i dispositivi calcolano numeri di verifica diversi.
L'aggressore non può far corrispondere i numeri perché:
- Non conosce le chiavi private dei dispositivi
- SHA-256 non è reversibile
- La probabilità di una corrispondenza casuale è solo 1 su 100
L'utente vede numeri diversi sugli schermi e annulla l'abbinamento. A quel punto, l'attacco è diventato visibile.
Passaggio 5: completare l'abbinamento
Solo dopo che l'utente ha confermato la verifica su entrambi i dispositivi l'abbinamento viene completato:
- Una chiave di abbinamento di 64 caratteri (256 bit) viene ricavata dal segreto condiviso:
SHA-256("pair:" + sharedSecret) → pairingKey - La chiave del documento è ricavata come
SHA-256("doc:" + pairingKey)e funge da chiave del documento Firestore - La chiave di crittografia è ricavata come
SHA-256("enc:" + pairingKey)e fornisce la chiave AES-256-GCM per la segnalazione crittografata - Entrambi i dispositivi memorizzano la stessa chiave di abbinamento e passano alla selezione della modalità
Da questo momento in poi, tutti i successivi tentativi di connessione (segnalazione Firestore, configurazione WebRTC) sono crittografati con la chiave AES-256-GCM condivisa. La chiave di abbinamento non viene mai inviata al backend; solo il suo hash SHA-256 viene usato come identificatore del documento.
Architettura del sistema
Il diagramma seguente mostra i componenti coinvolti nell'abbinamento e nella comunicazione:
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
Panoramica dell'architettura del sistema — sorgente modificabile: docs/diagrams/pairing-architecture.mmd
Percorsi di comunicazione in dettaglio:
- WebRTC peer-to-peer (linea spessa): audio, video e DataChannel fluiscono direttamente tra i dispositivi — crittografati con DTLS-SRTP. Nessun server vede questi dati.
- Firebase Firestore (linea continua): i dati di abbinamento (chiavi ECDH) e la segnalazione (SDP/ICE) passano attraverso Firestore — crittografati end-to-end con AES-256-GCM. Firebase non può decrittografare i dati.
- Server STUN: entrambi i dispositivi rilevano il proprio indirizzo IP pubblico, così da poter stabilire una connessione peer-to-peer diretta.
- Relay TURN: se non è possibile una connessione diretta (ad esempio con i dati mobili), il server TURN locale o Cloudflare selezionato inoltra i contenuti multimediali crittografati. Le credenziali di breve durata (24 h) vengono recuperate tramite Firebase Cloud Functions.
- Bluetooth LE (linea tratteggiata): Nearby Connections rileva automaticamente i dispositivi vicini — viene trasmesso solo il codice d'incontro, senza materiale di chiave.
Alternativa: inserimento manuale del codice
Se il Bluetooth non è disponibile (ad esempio sui dispositivi più vecchi), il codice di 4 caratteri può essere inserito anche manualmente. L'inserimento manuale usa lo stesso scambio di chiavi ECDH e la stessa verifica SAS dell'abbinamento automatico. L'unica differenza è che il codice viene letto e digitato dall'utente anziché rilevato tramite BLE.
Poiché lo scambio di chiavi ECDH avviene tramite Firebase in entrambi i casi, la sicurezza è identica. Il codice di 4 caratteri è solo un punto d'incontro; la vera crittografia si basa sulla chiave a 256 bit ricavata da ECDH.
Riepilogo
| Meccanismo di sicurezza | Protegge da |
|---|---|
| Scambio di chiavi ECDH (P-256) | Intercettazione del traffico di scambio delle chiavi |
| Coppie di chiavi effimere | Forward secrecy — gli abbinamenti passati restano sicuri |
| Numero di verifica visivo (SAS) | Man-in-the-middle (MITM) durante lo scambio delle chiavi |
| Hash SHA-256 come chiave del documento | Estrazione del codice da Firestore |
| Crittografia AES-256-GCM | Intercettazione dei dati di segnalazione |
| Conferma da entrambi i lati | Abbinamento unilaterale senza che l'utente lo sappia |
| DTLS-SRTP (WebRTC) | Intercettazione di audio/video |
Questi livelli lavorano insieme: ECDH protegge lo scambio di chiavi, il numero di verifica protegge dai MITM, AES-256-GCM protegge la segnalazione e WebRTC protegge i contenuti multimediali. Un aggressore dovrebbe compromettere questa catena in più punti senza che i dispositivi o i genitori se ne accorgano.