
Обратно спроектированный протокол BLE для CMF Watch Pro 2, документирующий структуру GATT, зашифрованные командные фреймы AES-128-CBC, процедуру аутентификации и синхронизацию данных о здоровье для разработки альтернативного сопутствующего приложения.
Неофициально. Этот документ описывает протокол Bluetooth Low Energy (BLE) часов CMF Watch Pro 2 (CMF by Nothing), восстановленный методом обратной разработки для альтернативного приложения-компаньона. Он не связан с Nothing/CMF и не одобрен ими. Используйте на свой страх и риск.
Все многобайтовые целые числа в заголовке кадра и кодах операций — big-endian. Целые числа
внутри полезной нагрузки команд — little-endian, если не указано иное (это отражает
прошивку устройства) — остерегайтесь исключений (GOALS_SET, GPS_PUSH, смещение/длина
массовой передачи — big-endian).
Каждое неочевидное утверждение ниже помечено способом его установления:
Если более поздний раздел исправляет более ранний, ранний текст сохраняется с указателем, а не удаляется — знание того, какие чтения были опробованы и опровергнуты, избавит следующего человека от того же обходного пути.
Тестовое устройство для всех захватов: CMF Watch Pro 2-5485, прошивка 1.0.0.73, серийный
номер CI04102520008192, MCU Actions ATS3089C (Cortex-M4), экран 466×360.
Телефон является GATT-клиентом; часы — периферийным устройством, рекламирующимся как
CMF Watch Pro 2-XXXX (4 шестнадцатеричных символа).
| Назначение | Сервис | Характеристика | Свойства |
|---|---|---|---|
| Запись команд | 0000fff0-0000-1000-8000-00805f9b34fb | 0000fff2-… | Write |
| Уведомления команд | 0000fff0-… | 0000fff1-… | Notify |
| Запись оболочки (AT) | — | 77d4ff01-2fe2-2334-0d35-9ccd078f529c | Write |
| Уведомления оболочки (AT) | — | 77d4ff02-… | Notify |
| Запись массовых данных | — | 02f00000-0000-0000-0000-00000000ffe1 | Write |
| Уведомления массовых данных | — | 02f00000-…ffe2 | Notify |
Включите уведомления, записав 01 00 в каждый CCCD (00002902-…). Канал команд
(fff1/fff2) передаёт кадрированный протокол ниже. Канал оболочки (77d4…) передаёт
обычный текст в стиле AT (например, AT GETSECRET; см. §14). Канал данных (02f0…)
передаёт большие бинарные блоки (циферблат, прошивка, AGPS), координируемые управляющими
кодами операций на канале команд.
UUID сервисов — часы рекламируют ~10 основных сервисов. Перечислены на реальном устройстве:
0xfff0 (команды), 0x180f (батарея), 0x180a (информация об устройстве), 0xefe7, 0xffd0,
02f00000-…ffe0 и 02f00000-…fe00 (данные), 77d4e67c-2fe2-2334-0d35-9ccd078f529c (оболочка /
сопряжение), e49a3001-f69a-11e8-8eb2-f2801f1b9fd1, f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.
⚠️ UUID сервиса оболочки —
77d4e67c-…, а не77d4ff00-…. В более ранних версиях этого документа предполагалось, что сервис разделяет префиксff00своих характеристик (77d4ff01/77d4ff02, §14) — это не так, по крайней мере на устройстве, на котором это проверялось (находка из freethinkel/fmc, см. §Источники). UUID характеристик не изменились. Не проверено, стабилен ли77d4e67cмежду устройствами — перечисляйте, а не зашивайте жёстко.
🌐 Примечание о Web Bluetooth. Chromium обнаруживает только те сервисы, которые страница перечислила в
optionalServices, даже для нефильтрованного вызоваgetPrimaryServices()— страница, перечисляющая 3 сервиса, видит 3, тогда какchrome://bluetooth-internals(собственный C++-слой Chrome, без ограничений) показывает все 10. Если вы пишете браузерный клиент, перечислите все UUID выше заранее, иначе сопряжение завершится ошибкой с сервисами, которые явно существуют. Web Bluetooth нет в Firefox/Safari; требуется жест пользователя + HTTPS/localhost.
✅ Целая реальная сессия прошла на единственном канале команд — во время 160-секундного захвата при интенсивном использовании не было трафика на каналах данных/прошивки или оболочки, кроме явной передачи OTA/циферблата.
0xF5)Каждое сообщение канала команд обёрнуто в один или несколько кадров с 11-байтовым заголовком:``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 0xF5 | chunkLen | cmd1 | chunkCount | chunkIndex | cmd2 | chunk bytes … | | 1 B | 2 B (BE) | 2 B BE | 2 B BE | 2 B BE | 2 B BE | chunkLen bytes | +------+-----------+--------+-------------+-------------+--------+-------------------+ __________________________ 11-byte header ____________________________/
- `cmd1`/`cmd2` вместе образуют **опкод** (см. §6). 🔎 подтверждено по фрейм-билдеру официального приложения (`C6117b.m30831g`).
- `chunkCount` = общее количество чанков для этой команды; `chunkIndex` — **с 1**.
- `chunkLen` = количество байт `chunk` в этом фрейме.
- Одна BLE-запись может фрагментироваться по MTU канала; приёмник буферизует сырые байты и заново извлекает полные фреймы. Большие полезные нагрузки разбиваются на несколько чанков (одинаковые `cmd1/cmd2`, возрастающий `chunkIndex`) и собираются по порядку.
### Соглашение об опкодах (✅ подтверждено на проводе)
- `cmd1 = 0xFFFF`: `cmd2` в диапазоне `0x80xx`/`0x90xx` = телефон→часы (запрос/установка); `0x00xx`/`0xa0xx` = часы→телефон (ответ). Пары сопоставляются по младшему байту (`0x9055`↔`0xa055`, `0x8051`↔`0x0051`).
- специфичный для функции `cmd1`: суффикс `cmd2` = `0x0001` **SET**, `0x0002` **GET**, `0x0003` **ACK**.
### Тело чанка
Для каждого чанка тело — это `payloadPiece ‖ CRC32_LE(payloadPiece)` (4-байтный CRC, little-endian, zlib/IEEE). Если команда **зашифрована** (см. §3), весь `payloadPiece ‖ CRC` затем шифруется AES-128-CBC/PKCS7, и этот шифротекст становится `chunk` фрейма.
**Особенность открытого текста:** для опкодов без шифрования часы *учитывают* 4-байтный CRC в `chunkLen`, но **не** передают его. Поэтому при декодировании фрейма с открытым текстом фактическая длина данных равна `chunkLen − 4`. (Зашифрованные фреймы несут CRC внутри шифротекста как обычно.)
Размер чанка (чтобы зашифрованные чанки ложились на границы AES-блоков), при `maxWrite = mtu − 3`:
- зашифрованный: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- открытый текст: `maxWrite − 11 − 4 − 1`
✅ Все наблюдаемые значения `chunkLen` зашифрованных фреймов были кратны 16 (выравнивание по блокам соблюдается).
---
## 3. Криптографические примитивы
- **AES-128-CBC** с дополнением **PKCS7** и **фиксированным IV** (из прошивки `CmfCharacteristic.AES_IV`):
`50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`.
- **CRC32** (zlib/IEEE), выдаётся как 4 байта little-endian.
- **SHA-256** по конкатенации частей.
Вывод ключа:```
authkey = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16] // persisted across sessions
sessionKey = SHA256( nonce ‖ authkey )[0..16] // per connection
secret = 16-байтовый секрет устройства (получается с часов через shell-команду
AT GETSECRET → GETSECRET:<32-hex>,OK).rnd1 = 16 случайных байт, выбранных телефоном; rnd2 = 16 случайных байт с часов.nonce = байты из ответа nonce часов.После установки ключа все кадры командного канала шифруются AES, кроме открытых опкодов, перечисленных в §5.
✅ Обе производные проверены: authkey, восстановленный с рутованного телефона из ntwatch.db, совпал со
значением, полученным из захваченных rnd1/rnd2/secret; sessionKey, воспроизведённый из захваченного nonce,
расшифровывает живые кадры.
Два пути входа используют один и тот же хвост nonce/confirm.