Blog

Appairage sécurisé dans Baby Monitor Timmy

Comment ECDH, le numéro SAS et la signalisation chiffrée fonctionnent ensemble.

Avant que Baby Monitor Timmy ne transmette l’audio et la vidéo, deux appareils doivent se trouver et établir une relation de confiance. Cette étape d’appairage est le moment le plus critique de tout le processus. J’explique ici comment Timmy s’appaire, quelle cryptographie se cache derrière et pourquoi un attaquant à proximité ne peut pas prendre le contrôle de la connexion sans être détecté.

Le problème : comment mon appareil sait-il à qui il parle ?

Lorsque deux appareils se connectent pour la première fois, la question centrale est la suivante : l’appareil A parle-t-il vraiment à l’appareil B, ou quelqu’un s’intercale-t-il entre les deux ? En cryptographie, on appelle cela une attaque de l’homme du milieu (MITM).

Timmy résout ce problème grâce à un échange de clés Diffie-Hellman sur courbe elliptique (ECDH) via Firebase, associé à une vérification visuelle par l’utilisateur.

Le diagramme suivant présente l’ensemble du processus d’appairage :

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
      

Séquence complète du protocole d’appairage — source modifiable : docs/diagrams/pairing-sequence.mmd

Étape 1 : chaque appareil génère une paire de clés

En ouvrant l’écran d’appairage, chaque appareil génère une paire de clés ECDH éphémère sur la courbe P-256 (secp256r1) :

Les clés sont créées à l’aide d’un générateur de nombres aléatoires cryptographiquement sûr (Random.secure()) et sont valables uniquement pour cette tentative d’appairage. De nouvelles clés sont générées à chaque nouvelle tentative.

Étape 2 : échanger les clés publiques via Firebase

Pour que deux appareils puissent se trouver, Timmy utilise un code à 4 caractères comme point de rendez-vous. Ce code peut être détecté automatiquement via Nearby Connections (Bluetooth Low Energy) ou saisi manuellement. Il n’a aucune valeur cryptographique ; il permet seulement aux deux appareils de retrouver le même document Firebase Firestore.

Une fois que les deux appareils connaissent le code, chacun écrit sa clé ECDH publique dans un document Firestore partagé. Chaque appareil lit ensuite la clé publique de l’autre dans ce document.

Point essentiel : seule la clé publique est envoyée. La clé privée ne quitte jamais l’appareil. Toute personne qui observe le trafic Firebase voit des clés publiques, mais ne peut pas calculer le secret partagé à partir de celles-ci. Cela repose sur la difficulté du problème du logarithme discret sur les courbes elliptiques (ECDLP).

Étape 3 : calculer le secret partagé

Une fois que les deux appareils ont découvert la clé publique de l’autre, ils calculent indépendamment le même secret partagé:

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

Les mathématiques des courbes elliptiques garantissent que les deux calculs donnent le même résultat, même si chaque appareil ne connaît que sa propre clé privée et la clé publique de l’autre.

Étape 4 : le numéro de vérification (SAS)

À partir du secret partagé, on dérive une chaîne d’authentification courte (SAS) — un nombre à deux chiffres affiché sur les deux appareils :

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

Les deux appareils affichent le même nombre — par exemple, 42. L’utilisateur vérifie visuellement que les nombres affichés sur les deux écrans correspondent, puis confirme sur chaque appareil séparément.

Pourquoi un attaquant ne peut pas le falsifier

Un attaquant de type homme du milieu devrait intercepter l’échange de clés dans Firebase. Plus précisément, il devrait :

  1. Remplacer les véritables clés publiques stockées dans le document Firestore par les siennes
  2. Établir des secrets partagés distincts avec chaque appareil
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)
      

Détection de l’homme du milieu grâce à une différence de SAS — source modifiable : docs/diagrams/mitm-detection.mmd

Dans ce cas, l’attaquant calcule un secret partagé S_A avec l’appareil A et un secret partagé différent S_B avec l’appareil B. Comme S_A ≠ S_B, les appareils calculent des numéros de vérification différents.

L’attaquant ne peut pas faire correspondre les nombres car :

L’utilisateur voit des nombres différents à l’écran et annule l’appairage. L’attaque est alors devenue visible.

Étape 5 : finaliser l’appairage

Ce n’est qu’après que l’utilisateur a confirmé la vérification sur les deux appareils que l’appairage se termine :

  1. Une clé d’appairage de 64 caractères (256 bits) est dérivée du secret partagé : SHA-256("pair:" + sharedSecret) → pairingKey
  2. La clé du document est dérivée sous la forme SHA-256("doc:" + pairingKey) et sert de clé pour le document Firestore
  3. La clé de chiffrement est dérivée sous la forme SHA-256("enc:" + pairingKey) et fournit la clé AES-256-GCM pour la signalisation chiffrée
  4. Les deux appareils enregistrent la même clé d’appairage et passent à la sélection du mode

À partir de là, toutes les tentatives de connexion suivantes (signalisation Firestore, configuration WebRTC) sont chiffrées avec la clé AES-256-GCM partagée. La clé d’appairage n’est jamais envoyée à l’infrastructure serveur ; seul son hachage SHA-256 est utilisé comme identifiant du document.

Architecture du système

Le diagramme suivant présente les composants impliqués dans l’appairage et la communication :

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

Vue d’ensemble de l’architecture du système — source modifiable : docs/diagrams/pairing-architecture.mmd

Les chemins de communication en détail :

Solution de secours : saisie manuelle du code

Si le Bluetooth n’est pas disponible (par exemple sur des appareils plus anciens), le code à 4 caractères peut aussi être saisi manuellement. La saisie manuelle utilise le même échange de clés ECDH et la même vérification SAS que l’appairage automatique. La seule différence est que le code est lu et saisi par l’utilisateur au lieu d’être détecté via BLE.

Comme l’échange de clés ECDH passe par Firebase dans les deux cas, la sécurité est identique. Le code à 4 caractères n’est qu’un point de rendez-vous ; le véritable chiffrement repose sur la clé de 256 bits dérivée d’ECDH.

Résumé

Mécanisme de sécurité Protège contre
Échange de clés ECDH (P-256) L’écoute du trafic lors de l’échange de clés
Paires de clés éphémères Confidentialité persistante — les appairages passés restent sûrs
Numéro de vérification visuelle (SAS) Homme du milieu (MITM) lors de l’échange de clés
Hachage SHA-256 comme clé de document Extraction du code depuis Firestore
Chiffrement AES-256-GCM Écoute des données de signalisation
Confirmation des deux côtés Appairage d’un seul côté à l’insu de l’utilisateur
DTLS-SRTP (WebRTC) Écoute de l’audio/de la vidéo

Ces couches se complètent : ECDH protège l’échange de clés, le numéro de vérification protège contre les attaques MITM, AES-256-GCM protège la signalisation et WebRTC protège les médias. Un attaquant devrait briser cette chaîne à plusieurs endroits sans que les appareils ni les parents ne s’en aperçoivent.


Autres articles