Блог

Сигурно сдвояване в Baby Monitor Timmy

Как ECDH, SAS номерът и криптираната сигнализация работят заедно.

Преди 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):

Ключовете се създават с криптографски сигурен генератор на случайни числа (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. По-конкретно, той би трябвало да:

  1. Замени истинските публични ключове, съхранени в документа на Firestore, със свои
  2. Установи отделни споделени тайни с всяко устройство
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, устройствата изчисляват различни номера за проверка.

Нападателят не може да направи числата еднакви, защото:

Потребителят вижда различни числа на екраните и отменя сдвояването. В този момент атаката вече е видима.

Стъпка 5: Завършване на сдвояването

Едва след като потребителят е потвърдил проверката на двете устройства сдвояването завършва:

  1. Извежда се 64-символен ключ за сдвояване (256 бита) от споделената тайна: SHA-256("pair:" + sharedSecret) → pairingKey
  2. Ключът за документа се извежда като SHA-256("doc:" + pairingKey) и служи като ключ за документа във Firestore
  3. Ключът за криптиране се извежда като SHA-256("enc:" + pairingKey) и предоставя AES-256-GCM ключа за криптирана сигнализация
  4. И двете устройства съхраняват един и същ ключ за сдвояване и преминават към избор на режим

Оттук нататък всички следващи опити за свързване (сигнализация през 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

Комуникационни пътища в детайли:

Резервен вариант: Ръчно въвеждане на код

Ако 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 защитава медиите. Нападателят би трябвало да пробие тази верига на няколко места, без устройствата или родителите да забележат.


Още статии