Перш ніж 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 peer-to-peer (товста лінія): аудіо, відео й DataChannel передаються безпосередньо між пристроями — зашифровані за допомогою DTLS-SRTP. Жоден сервер не бачить цих даних.
- Firebase Firestore (суцільна лінія): дані сполучення (ключі ECDH) і сигналізація (SDP/ICE) проходять через Firestore — наскрізно зашифровані AES-256-GCM. Firebase не може розшифрувати ці дані.
- STUN-сервер: обидва пристрої визначають свою публічну IP-адресу, щоб можна було встановити пряме peer-to-peer-з’єднання.
- TURN-ретранслятор: якщо пряме з’єднання неможливе (наприклад, через мобільний інтернет), вибраний локальний сервер 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 — медіадані. Зловмиснику довелося б зламати цей ланцюг у кількох місцях, не привернувши уваги пристроїв або батьків.