Преди Baby Monitor Timmy да предаде аудио и видео, двете устройства трябва да се открият и да си имат доверие. Тази стъпка на сдвояване е най-важният момент в целия процес. Тук обяснявам как Timmy се сдвоява, каква криптография стои зад това и защо нападател наблизо не може незабелязано да поеме връзката.
Проблемът: Откъде устройството ми знае с кого говори?
Когато две устройства се свързват за първи път, основният въпрос е: наистина ли устройство A говори с устройство B, или някой стои по средата? В криптографията това се нарича атака „човек по средата“ (MITM).
Timmy решава това чрез обмен на ключове Elliptic Curve Diffie-Hellman (ECDH) през Firebase, комбиниран с визуална проверка от потребителя.
Следната диаграма показва целия процес на сдвояване с един поглед:
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
Пълна последователност на протокола за сдвояване — редактируем източник: docs/diagrams/pairing-sequence.mmd
Стъпка 1: Всяко устройство генерира двойка ключове
При отваряне на екрана за сдвояване всяко устройство генерира временна ECDH двойка ключове върху кривата P-256 (secp256r1):
- Частен ключ — остава само на устройството
- Публичен ключ — обменя се през Firebase
Ключовете се създават с криптографски сигурен генератор на случайни числа (Random.secure())
и са валидни само за този един опит за сдвояване. За всеки нов опит се генерират нови ключове.
Стъпка 2: Обмен на публични ключове през Firebase
За да могат две устройства да се открият, Timmy използва 4-символен код като място за среща. Този код може да бъде открит автоматично чрез Nearby Connections (Bluetooth Low Energy) или въведен ръчно. Той няма криптографска стойност; служи единствено двете устройства да открият един и същ документ във Firebase Firestore.
След като и двете устройства знаят кода, всяко записва публичния си ECDH ключ в споделен документ на Firestore. След това всяко устройство прочита публичния ключ на другото от този документ.
Важно: изпраща се само публичният ключ. Частният ключ никога не напуска устройството. Всеки, който наблюдава трафика към Firebase, вижда публични ключове, но не може да изчисли споделената тайна от тях. Това разчита на трудността на проблема с дискретния логаритъм върху елиптични криви (ECDLP).
Стъпка 3: Изчисляване на споделената тайна
След като и двете устройства са открили публичния ключ на другото, те независимо изчисляват една и съща споделена тайна:
sharedSecret = ECDH(myPrivateKey, remotePublicKey)
→ 32 bytes (identical on both devices)
Математиката на елиптичните криви гарантира, че и двете изчисления дават един и същ резултат, въпреки че всяко устройство знае само своя частен ключ и публичния ключ на другото.
Стъпка 4: Номерът за проверка (SAS)
От споделената тайна се извежда кратък низ за удостоверяване (SAS) — двуцифрено число, показано и на двете устройства:
hash = SHA-256("sas:" + sort(pubkeyA, pubkeyB) + sharedSecret)
number = (hash[0] × 256 + hash[1]) mod 100 → 00 to 99
И двете устройства показват един и същ номер — например, 42. Потребителят визуално сравнява дали числата на двата екрана съвпадат, след което потвърждава на всяко устройство поотделно.
Защо нападател не може да подправи това
Нападател „човек по средата“ би трябвало да прихване обмена на ключове във Firebase. По-конкретно, той би трябвало да:
- Замени истинските публични ключове, съхранени в документа на Firestore, със свои
- Установи отделни споделени тайни с всяко устройство
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)
Откриване на атака „човек по средата“ чрез несъвпадение на SAS — редактируем източник: docs/diagrams/mitm-detection.mmd
В този случай нападателят изчислява споделена тайна S_A с устройство A и различна
споделена тайна S_B с устройство B. Тъй като S_A ≠ S_B,
устройствата изчисляват различни номера за проверка.
Нападателят не може да направи числата еднакви, защото:
- Не знае частните ключове на устройствата
- SHA-256 не е обратима функция
- Вероятността за случайно съвпадение е само 1 на 100
Потребителят вижда различни числа на екраните и отменя сдвояването. В този момент атаката вече е видима.
Стъпка 5: Завършване на сдвояването
Едва след като потребителят е потвърдил проверката на двете устройства сдвояването завършва:
- Извежда се 64-символен ключ за сдвояване (256 бита) от споделената тайна:
SHA-256("pair:" + sharedSecret) → pairingKey - Ключът за документа се извежда като
SHA-256("doc:" + pairingKey)и служи като ключ за документа във Firestore - Ключът за криптиране се извежда като
SHA-256("enc:" + pairingKey)и предоставя AES-256-GCM ключа за криптирана сигнализация - И двете устройства съхраняват един и същ ключ за сдвояване и преминават към избор на режим
Оттук нататък всички следващи опити за свързване (сигнализация през Firestore, настройка на WebRTC) са криптирани със споделения AES-256-GCM ключ. Ключът за сдвояване никога не се изпраща към бекенда; само неговият SHA-256 хеш се използва като идентификатор на документа.
Архитектура на системата
Следната диаграма показва компонентите, участващи в сдвояването и комуникацията:
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
Преглед на архитектурата на системата — редактируем източник: docs/diagrams/pairing-architecture.mmd
Комуникационни пътища в детайли:
- WebRTC между равноправни устройства (дебела линия): Аудиото, видеото и DataChannel преминават директно между устройствата — криптирани с DTLS-SRTP. Никой сървър не вижда тези данни.
- Firebase Firestore (плътна линия): Данните за сдвояване (ECDH ключове) и сигнализацията (SDP/ICE) минават през Firestore — криптирани от край до край с AES-256-GCM. Firebase не може да декриптира данните.
- STUN сървър: И двете устройства откриват публичния си IP адрес, за да може да се установи директна peer-to-peer връзка.
- TURN реле: Ако директна връзка не е възможна (напр. при мобилни данни), избраният локален или Cloudflare TURN сървър препредава криптираните медийни данни. Краткосрочни идентификационни данни (24 ч.) се получават чрез Firebase Cloud Functions.
- Bluetooth LE (пунктирана линия): Nearby Connections открива автоматично близките устройства — предава се само кодът за среща, без ключов материал.
Резервен вариант: Ръчно въвеждане на код
Ако Bluetooth не е наличен (напр. при по-стари устройства), 4-символният код може да се въведе и ръчно. При ръчното въвеждане се използват същият ECDH обмен на ключове и същата SAS проверка както при автоматичното сдвояване. Единствената разлика е, че кодът се прочита и въвежда от потребителя, вместо да бъде открит чрез BLE.
Тъй като и в двата случая ECDH обменът на ключове се извършва през Firebase, сигурността е еднаква. 4-символният код е само място за среща; истинското криптиране се основава на 256-битовия ключ, изведен от ECDH.
Обобщение
| Механизъм за сигурност | Защитава срещу |
|---|---|
| ECDH обмен на ключове (P-256) | Подслушване на трафика при обмена на ключове |
| Временни двойки ключове | Пряка секретност — предишните сдвоявания остават защитени |
| Номер за визуална проверка (SAS) | Атака „човек по средата“ (MITM) при обмена на ключове |
| SHA-256 хеш като ключ за документа | Извличане на кода от Firestore |
| AES-256-GCM криптиране | Подслушване на данните за сигнализация |
| Потвърждение от двете страни | Едностранно сдвояване без знанието на потребителя |
| DTLS-SRTP (WebRTC) | Подслушване на аудио/видео |
Тези слоеве работят заедно: ECDH защитава обмена на ключове, номерът за проверка защитава от MITM, AES-256-GCM защитава сигнализацията, а WebRTC защитава медиите. Нападателят би трябвало да пробие тази верига на няколко места, без устройствата или родителите да забележат.