Blogi

Baby Monitor Timmy -sovelluksen turvallinen paritus

Näin ECDH, SAS-numero ja salattu signalointi toimivat yhdessä.

Ennen kuin Baby Monitor Timmy siirtää ääntä ja videota, kahden laitteen on löydettävä toisensa ja luotettava toisiinsa. Tämä paritus vaihe on koko prosessin kriittisin hetki. Tässä selitän, miten Timmy parittaa laitteet, mitä salaustekniikkaa sen taustalla on ja miksi lähellä oleva hyökkääjä ei voi kaapata yhteyttä huomaamatta.

Ongelma: Mistä laitteeni tietää, kenelle se puhuu?

Kun kaksi laitetta yhdistetään ensimmäistä kertaa, keskeinen kysymys on: puhuuko laite A todella laitteelle B vai onko joku niiden välissä? Salaustekniikassa tätä kutsutaan man-in-the-middle-hyökkäykseksi (MITM).

Timmy ratkaisee tämän Elliptic Curve Diffie-Hellman (ECDH) -avaimenvaihdolla Firebase-palvelun kautta sekä käyttäjän tekemällä visuaalisella varmistuksella.

Seuraava kaavio näyttää koko paritusprosessin yhdellä silmäyksellä:

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
      

Koko paritusprotokollan kulku — muokattava lähde: docs/diagrams/pairing-sequence.mmd

Vaihe 1: Kumpikin laite luo avainparin

Kun paritusnäkymä avataan, kumpikin laite luo tilapäisen ECDH-avainparin P-256-käyrälle (secp256r1):

Avaimet luodaan kryptografisesti turvallisella satunnaislukugeneraattorilla (Random.secure()) ja ne ovat voimassa vain tämän yhden paritusyrityksen ajan. Uudet avaimet luodaan jokaista uutta yritystä varten.

Vaihe 2: Julkisten avainten vaihto Firebase-palvelun kautta

Jotta kaksi laitetta löytäisivät toisensa, Timmy käyttää 4-merkkistä koodia kohtaamispaikkana. Koodi voidaan löytää automaattisesti Nearby Connectionsin (Bluetooth Low Energy) avulla tai syöttää käsin. Sillä ei ole salausteknistä merkitystä; sen avulla laitteet vain löytävät saman Firebase Firestore -asiakirjan.

Kun molemmat laitteet tietävät koodin, kumpikin kirjoittaa julkisen ECDH-avaimensa jaettuun Firestore-asiakirjaan. Sen jälkeen kumpikin laite lukee toisen laitteen julkisen avaimen asiakirjasta.

Tärkeää: vain julkinen avain lähetetään. Yksityinen avain ei koskaan poistu laitteesta. Firebase-liikennettä seuraava näkee julkiset avaimet, mutta ei voi laskea jaettua salaisuutta niiden perusteella. Tämä perustuu elliptisten käyrien diskreetin logaritmin ongelman (ECDLP) vaikeuteen.

Vaihe 3: Jaetun salaisuuden laskeminen

Kun molemmat laitteet ovat löytäneet toistensa julkiset avaimet, ne laskevat itsenäisesti saman jaetun salaisuuden:

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

Elliptisten käyrien matematiikka takaa, että molemmat laskutoimitukset tuottavat saman tuloksen, vaikka kumpikin laite tuntee vain oman yksityisen avaimensa ja toisen julkisen avaimen.

Vaihe 4: Varmistusnumero (SAS)

Jaetusta salaisuudesta johdetaan lyhyt tunnistusmerkkijono (Short Authentication String, SAS) — kaksinumeroinen luku, joka näytetään molemmissa laitteissa:

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

Molemmat laitteet näyttävät saman numeron — esimerkiksi 42. Käyttäjä tarkistaa silmämääräisesti, että numero on sama molemmissa näytöissä, ja vahvistaa sen sitten kummallakin laitteella erikseen.

Miksi hyökkääjä ei voi väärentää tätä

Man-in-the-middle-hyökkääjän pitäisi siepata avaimenvaihto Firebase-palvelussa. Tarkemmin sanottuna hänen pitäisi:

  1. Korvata Firestore-asiakirjaan tallennetut aidot julkiset avaimet omillaan
  2. Muodostaa erilliset jaetut salaisuudet kummankin laitteen kanssa
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)
      

Man-in-the-middle-hyökkäyksen tunnistaminen toisistaan poikkeavien SAS-numeroiden avulla — muokattava lähde: docs/diagrams/mitm-detection.mmd

Tässä tapauksessa hyökkääjä laskee jaetun salaisuuden S_A laitteen A kanssa ja eri jaetun salaisuuden S_B laitteen B kanssa. Koska S_A ≠ S_B, laitteet laskevat eri varmistusnumerot.

Hyökkääjä ei voi saada numeroita täsmäämään, koska:

Käyttäjä näkee näytöissä eri numerot ja peruuttaa parituksen. Tässä vaiheessa hyökkäys on tullut näkyväksi.

Vaihe 5: Parituksen viimeistely

Vasta kun käyttäjä on tehnyt vahvistuksen molemmilla laitteilla paritus valmistuu:

  1. Yksi 64-merkkinen paritusavain (256 bittiä) johdetaan jaetusta salaisuudesta: SHA-256("pair:" + sharedSecret) → pairingKey
  2. Asiakirja-avain johdetaan muodossa SHA-256("doc:" + pairingKey) ja sitä käytetään Firestore-asiakirjan avaimena
  3. Salausavain johdetaan muodossa SHA-256("enc:" + pairingKey) ja se tarjoaa AES-256-GCM-avaimen salattua signalointia varten
  4. Molemmat laitteet tallentavat saman paritusavaimen ja siirtyvät tilan valintaan

Tästä eteenpäin kaikki myöhemmät yhteysyritykset (Firestore-signalointi ja WebRTC-yhteyden muodostaminen) salataan jaetulla AES-256-GCM-avaimella. Paritusavainta ei koskaan lähetetä taustajärjestelmään; asiakirjan tunnisteena käytetään vain sen SHA-256-tiivistettä.

Järjestelmäarkkitehtuuri

Seuraava kaavio näyttää paritukseen ja viestintään osallistuvat komponentit:

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

Järjestelmäarkkitehtuurin yleiskuva — muokattava lähde: docs/diagrams/pairing-architecture.mmd

Viestintäreitit yksityiskohtaisesti:

Vaihtoehto: koodin syöttäminen käsin

Jos Bluetooth ei ole käytettävissä (esimerkiksi vanhemmissa laitteissa), 4-merkkisen koodin voi myös kirjoittaa käsin. Käsinsyöttö käyttää samaa ECDH-avaimenvaihtoa ja samaa SAS-varmistusta kuin automaattinen paritus. Ainoa ero on, että käyttäjä lukee ja kirjoittaa koodin sen sijaan, että se löydettäisiin BLE:n kautta.

Koska ECDH-avaimenvaihto tapahtuu molemmissa tapauksissa Firebase-palvelun kautta, turvallisuus on sama. 4-merkkinen koodi on vain kohtaamispaikka; varsinainen salaus perustuu ECDH:sta johdettuun 256-bittiseen avaimeen.

Yhteenveto

Turvamekanismi Suojaa tältä
ECDH-avaimenvaihto (P-256) Avaimenvaihtoliikenteen salakuuntelu
Tilapäiset avainparit Eteenpäinsalattavuus — aiemmat paritukset pysyvät turvassa
Visuaalinen varmistusnumero (SAS) Man-in-the-middle-hyökkäys (MITM) avaimenvaihdon aikana
SHA-256-tiiviste asiakirja-avaimena Koodin poimiminen Firestoresta
AES-256-GCM-salaus Signalointidatan salakuuntelu
Vahvistus molemmilla puolilla Yksipuolinen paritus ilman käyttäjän tietoa
DTLS-SRTP (WebRTC) Äänen/videon salakuuntelu

Nämä kerrokset toimivat yhdessä: ECDH suojaa avaimenvaihtoa, varmistusnumero suojaa MITM-hyökkäyksiltä, AES-256-GCM suojaa signalointia ja WebRTC suojaa mediaa. Hyökkääjän pitäisi murtaa tämä ketju useasta kohdasta ilman, että laitteet tai vanhemmat huomaavat sitä.


Lisää artikkeleita