Блог

Безопасное сопряжение в Baby Monitor Timmy

Как ECDH, число SAS и шифрование сигнального обмена работают вместе.

Прежде чем Baby Monitor Timmy начнёт передавать звук и видео, два устройства должны найти друг друга и установить доверие. Этот этап сопряжения — самый важный момент во всём процессе. Здесь я объясню, как Timmy выполняет сопряжение, какая криптография за этим стоит и почему находящийся рядом злоумышленник не сможет незаметно перехватить соединение.

Проблема: откуда устройство знает, с кем оно общается?

Когда два устройства подключаются впервые, главный вопрос такой: действительно ли устройство A общается с устройством B или между ними находится кто-то ещё? В криптографии это называется атакой «человек посередине» (MITM).

Timmy решает эту задачу с помощью обмена ключами Диффи — Хеллмана на эллиптических кривых (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 — медиапоток. Злоумышленнику пришлось бы взломать эту цепочку в нескольких местах так, чтобы ни устройства, ни родители этого не заметили.


Другие статьи