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):
- Yksi yksityinen avain — pysyy vain laitteessa
- Yksi julkinen avain — vaihdetaan Firebase-palvelun kautta
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:
- Korvata Firestore-asiakirjaan tallennetut aidot julkiset avaimet omillaan
- 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:
- Hän ei tiedä laitteiden yksityisiä avaimia
- SHA-256-tiivistettä ei voi palauttaa alkuperäiseen muotoonsa
- Satunnaisen täsmäyksen todennäköisyys on vain 1 / 100
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:
- Yksi 64-merkkinen paritusavain (256 bittiä) johdetaan jaetusta salaisuudesta:
SHA-256("pair:" + sharedSecret) → pairingKey - Asiakirja-avain johdetaan muodossa
SHA-256("doc:" + pairingKey)ja sitä käytetään Firestore-asiakirjan avaimena - Salausavain johdetaan muodossa
SHA-256("enc:" + pairingKey)ja se tarjoaa AES-256-GCM-avaimen salattua signalointia varten - 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:
- WebRTC-vertaisverkko (paksu viiva): Ääni, video ja DataChannel kulkevat suoraan laitteiden välillä — DTLS-SRTP-salattuina. Mikään palvelin ei näe näitä tietoja.
- Firebase Firestore (yhtenäinen viiva): Paritustiedot (ECDH-avaimet) ja signalointi (SDP/ICE) kulkevat Firestoren kautta AES-256-GCM:llä päästä päähän salattuina. Firebase ei voi purkaa tietojen salausta.
- STUN-palvelin: Molemmat laitteet selvittävät julkisen IP-osoitteensa, jotta suora vertaisyhteys voidaan muodostaa.
- TURN-välityspalvelin: Jos suora yhteys ei ole mahdollinen (esimerkiksi mobiilidatalla), valittu paikallinen tai Cloudflare TURN -palvelin välittää salatun median. Lyhytaikaiset tunnistetiedot (24 h) haetaan Firebase Cloud Functions -palvelun kautta.
- Bluetooth LE (katkoviiva): Nearby Connections löytää lähellä olevat laitteet automaattisesti — vain kohtaamiskoodi välitetään, ei avainmateriaalia.
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ä.