Пред Baby Monitor Timmy да пренесува аудио и видео, двата уреди треба да се пронајдат и да си веруваат. Овој чекор на спарување е најкритичниот момент во целиот процес. Тука објаснувам како Timmy ги спарува уредите, која криптографија стои зад тоа и зошто напаѓач во близина не може незабележано да ја преземе врската.
Проблемот: Како мојот уред знае со кого разговара?
Кога два уреди се поврзуваат првпат, главното прашање е: дали Уред А навистина разговара со Уред Б или некој се наоѓа меѓу нив? Во криптографијата, тоа се нарекува напад „човек во средина“ (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 со Уред А и
различна заедничка тајна S_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 peer-to-peer (дебела линија): Аудиото, видеото и 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 ги штити медиумите. Напаѓачот би морал да го пробие овој синџир на неколку места, без уредите или родителите да забележат.