Blogi

Turvaline sidumine Baby Monitor Timmy rakenduses

Kuidas ECDH, SAS-number ja krüptitud signaalimine koos toimivad.

Enne kui Baby Monitor Timmy edastab heli ja videot, peavad kaks seadet teineteist leidma ning üksteist usaldama. See sidumise etapp on kogu protsessi kõige kriitilisem hetk. Siin selgitan, kuidas Timmy seadmeid seob, milline krüptograafia selle taga on ja miks lähedal olev ründaja ei saa ühendust märkamatult üle võtta.

Probleem: kuidas mu seade teab, kellega ta suhtleb?

Kui kaks seadet ühenduvad esimest korda, on keskne küsimus järgmine: kas seade A suhtleb tõesti seadmega B või on keegi nende vahel? Krüptograafias nimetatakse seda vahendusründeks (MITM).

Timmy lahendab selle elliptilise kõvera Diffie-Hellmani (ECDH) võtmevahetusega Firebase'i kaudu koos kasutajapoolse visuaalse kontrolliga.

Järgmine diagramm näitab kogu sidumisprotsessi ühe pilguga:

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
      

Täielik sidumisprotokolli järjestus — muudetav lähtefail: docs/diagrams/pairing-sequence.mmd

1. samm: iga seade loob võtmepaari

Sidumiskuvale minnes loob iga seade ajutise ECDH võtmepaari P-256 kõveral (secp256r1):

Võtmed luuakse krüptograafiliselt turvalise juhuarvugeneraatoriga (Random.secure()) ning need on kehtivad ainult selle ühe sidumiskatse jaoks. Iga uue katse jaoks luuakse uued võtmed.

2. samm: avalike võtmete vahetamine Firebase'i kaudu

Selleks et kaks seadet teineteist leiaksid, kasutab Timmy 4-märgilist koodi kohtumispaigana. Seda koodi saab automaatselt tuvastada funktsiooniga Lähedalasuvad ühendused (Bluetooth Low Energy) või sisestada käsitsi. Sellel puudub krüptograafiline väärtus; see aitab vaid mõlemal seadmel leida sama Firebase Firestore'i dokumendi.

Kui mõlemad seadmed koodi teavad, kirjutab kumbki oma avaliku ECDH-võtme ühisesse Firestore'i dokumenti. Seejärel loeb kumbki seade sellest dokumendist teise seadme avaliku võtme.

Oluline: saadetakse ainult avalik võti. Privaatvõti ei lahku kunagi seadmest. Firebase'i liiklust jälgiv inimene näeb avalikke võtmeid, kuid ei saa nende põhjal arvutada ühist saladust . See tugineb elliptilise kõvera diskreetse logaritmi probleemi (ECDLP) keerukusele.

3. samm: ühise saladuse arvutamine

Kui mõlemad seadmed on teineteise avaliku võtme leidnud, arvutavad nad iseseisvalt sama ühise saladuse:

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

. Elliptiliste kõverate matemaatika tagab, et mõlemad arvutused annavad sama tulemuse, kuigi kumbki seade teab vaid oma privaatvõtit ja teise seadme avalikku võtit.

4. samm: kontrollnumber (SAS)

Ühisest saladusest tuletatakse lühike autentimisstring (SAS) — kahekohaline number, mida kuvatakse mõlemas seadmes:

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

Mõlemad seadmed kuvavad sama numbri — näiteks 42. Kasutaja võrdleb visuaalselt, kas mõlema ekraani numbrid kattuvad, ja kinnitab seejärel igas seadmes eraldi.

Miks ründaja ei saa seda võltsida

Vahendusründaja peaks Firebase'i kaudu toimuvasse võtmevahetusse sekkuma. Täpsemalt peaks ta:

  1. asendama Firestore'i dokumendis olevad päris avalikud võtmed enda omadega
  2. looma kummagi seadmega eraldi ühise saladuse
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)
      

Vahendusründe tuvastamine SAS-i mittevastavuse abil — muudetav lähtefail: docs/diagrams/mitm-detection.mmd

Sel juhul arvutab ründaja ühise saladuse S_A seadmega A ja teistsuguse ühise saladuse S_B seadmega B. Kuna S_A ≠ S_B, arvutavad seadmed erinevad kontrollnumbrid.

. Ründaja ei saa numbreid kattuma panna, sest:

. Kasutaja näeb ekraanidel erinevaid numbreid ja katkestab sidumise. Selleks hetkeks on rünne nähtavaks muutunud.

5. samm: sidumise lõpuleviimine

Alles pärast seda, kui kasutaja on kontrolli kinnitanud mõlemas seadmes , jõuab sidumine lõpule:

  1. Üks 64-märgiline sidumisvõti (256 bitti) tuletatakse ühisest saladusest: SHA-256("pair:" + sharedSecret) → pairingKey
  2. Dokumendivõti tuletatakse kui SHA-256("doc:" + pairingKey) ning seda kasutatakse Firestore'i dokumendivõtmena
  3. Krüptovõti tuletatakse kui SHA-256("enc:" + pairingKey) ning see on krüptitud signaalimise AES-256-GCM-võtmeks
  4. Mõlemad seadmed salvestavad sama sidumisvõtme ja liiguvad režiimi valimise juurde

Sellest hetkest alates on kõik järgmised ühenduskatsed (Firestore'i signaalimine, WebRTC seadistamine) krüptitud ühise AES-256-GCM-võtmega. Sidumisvõtit ei saadeta kunagi taustsüsteemi; dokumendi identifikaatorina kasutatakse ainult selle SHA-256 räsi.

Süsteemi arhitektuur

Järgmine diagramm näitab sidumises ja suhtluses osalevaid komponente:

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

Süsteemi arhitektuuri ülevaade — muudetav lähtefail: docs/diagrams/pairing-architecture.mmd

Suhtluskanalid üksikasjalikult:

Varuvariant: koodi käsitsi sisestamine

Kui Bluetooth pole saadaval (nt vanemates seadmetes), saab 4-märgilise koodi sisestada ka käsitsi. Käsitsi sisestamisel kasutatakse sama ECDH võtmevahetust ja sama SAS-i kontrolli nagu automaatsel sidumisel. Ainus erinevus on see, et kasutaja loeb koodi ja sisestab selle ise, selle asemel et seade tuvastaks koodi BLE kaudu.

Kuna ECDH võtmevahetus toimub mõlemal juhul Firebase'i kaudu, on turvalisus identne. 4-märgiline kood on vaid kohtumispaik; tegelik krüptimine põhineb ECDH-st tuletatud 256-bitisel võtmel.

Kokkuvõte

Turvamehhanism Kaitseb selle eest
ECDH võtmevahetus (P-256) Võtmevahetuse liikluse pealtkuulamine
Ajutised võtmepaarid Edasisaladus — varasemad sidumised jäävad turvaliseks
Visuaalne kontrollnumber (SAS) Vahendusrünne (MITM) võtmevahetuse ajal
SHA-256 räsi dokumendivõtmena Koodi väljavõtmine Firestore'ist
AES-256-GCM krüptimine Signaalimisandmete pealtkuulamine
Mõlemapoolne kinnitus Ühepoolne sidumine kasutaja teadmata
DTLS-SRTP (WebRTC) Heli/video pealtkuulamine

Need kihid töötavad koos: ECDH kaitseb võtmevahetust, kontrollnumber kaitseb MITM-i eest, AES-256-GCM kaitseb signaalimist ja WebRTC kaitseb meediat. Ründaja peaks selle ahela mitmest kohast murdma, ilma et seadmed või vanemad seda märkaksid.


Veel artikleid