
Protocole BLE rétro-conçu pour la CMF Watch Pro 2, documentant la disposition GATT, les trames de commande chiffrées en AES-128-CBC, l'échange d'authentification et la synchronisation des données de santé pour le développement d'une application compagne alternative.
Non officiel. Ce document décrit le protocole Bluetooth Low Energy (BLE) de la CMF Watch Pro 2 (CMF by Nothing), reconstruit par rétro-ingénierie pour une application compagnon alternative. Il n'est ni affilié à, ni approuvé par Nothing/CMF. À utiliser à vos propres risques.
Tous les entiers multi-octets dans l'en-tête de trame et les opcodes sont en big-endian. Les entiers à l'intérieur
des charges utiles de commande sont en little-endian sauf indication contraire (cela reflète le firmware de l'appareil) —
attention aux exceptions (GOALS_SET, GPS_PUSH, décalage/longueur du transfert en masse sont en big-endian).
Chaque affirmation non évidente ci-dessous est étiquetée selon la manière dont elle a été établie :
Lorsqu'une section ultérieure corrige une section antérieure, le texte antérieur est conservé avec un renvoi plutôt que supprimé — savoir quelles lectures ont été essayées et réfutées évite à la personne suivante le même détour.
Appareil de test pour toutes les captures : CMF Watch Pro 2-5485, firmware 1.0.0.73, numéro de série CI04102520008192,
MCU Actions ATS3089C (Cortex-M4), écran 466×360.
Le téléphone est le client GATT ; la montre est le périphérique, diffusant sous le nom CMF Watch Pro 2-XXXX
(4 caractères hexadécimaux).
| Objectif | Service | Caractéristique | Propriétés |
|---|---|---|---|
| Écriture de commande | 0000fff0-0000-1000-8000-00805f9b34fb | 0000fff2-… | Write |
| Notification de commande | 0000fff0-… | 0000fff1-… | Notify |
| Écriture shell (AT) | — | 77d4ff01-2fe2-2334-0d35-9ccd078f529c | Write |
| Notification shell (AT) | — | 77d4ff02-… | Notify |
| Écriture de données en masse | — | 02f00000-0000-0000-0000-00000000ffe1 | Write |
| Notification de données en masse | — | 02f00000-…ffe2 | Notify |
Activez les notifications en écrivant 01 00 sur chaque CCCD (00002902-…). Le canal de commande
(fff1/fff2) transporte le protocole tramé ci-dessous. Le canal shell (77d4…) transporte du texte
brut de type AT (par ex. AT GETSECRET ; voir §14). Le canal de données (02f0…) transporte de gros blobs
binaires (cadran de montre, firmware, AGPS), coordonnés par des opcodes de contrôle sur le canal de commande.
UUID de services — la montre diffuse ~10 services principaux. Énumérés sur une unité réelle :
0xfff0 (commande), 0x180f (batterie), 0x180a (informations sur l'appareil), 0xefe7, 0xffd0,
02f00000-…ffe0 et 02f00000-…fe00 (données), 77d4e67c-2fe2-2334-0d35-9ccd078f529c (shell /
appairage), e49a3001-f69a-11e8-8eb2-f2801f1b9fd1, f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.
⚠️ L'UUID du service shell est
77d4e67c-…, et non77d4ff00-…. Les révisions antérieures de ce document supposaient que le service partageait le préfixeff00de ses caractéristiques (77d4ff01/77d4ff02, §14) — ce n'est pas le cas, du moins sur l'unité sur laquelle cela a été vérifié (constat issu de freethinkel/fmc, voir §Sources). Les UUID de caractéristiques sont inchangés. Non vérifié si77d4e67cest stable entre les unités — énumérez plutôt que de coder en dur.
🌐 Remarque Web Bluetooth. Chromium ne découvre jamais que les services que la page a répertoriés dans
optionalServices, même pour un appelgetPrimaryServices()non filtré — une page répertoriant 3 services en voit 3, tandis quechrome://bluetooth-internals(la couche C++ propre de Chrome, non scopée) affiche les 10. Si vous écrivez un client navigateur, listez chaque UUID ci-dessus à l'avance ou l'appairage échouera avec des services qui existent pourtant clairement. Pas de Web Bluetooth dans Firefox/Safari ; nécessite un geste utilisateur + HTTPS/localhost.
✅ Une session réelle complète s'est déroulée sur le seul canal de commande — lors d'une capture de 160 s en usage intensif, il n'y a eu aucun trafic sur les canaux de données/firmware ou shell, sauf lors d'un transfert explicite OTA/cadran de montre.
0xF5)Chaque message du canal de commande est encapsulé dans une ou plusieurs trames à en-tête de 11 octets :``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 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` forment ensemble l'**opcode** (voir §6). 🔎 confirmé par rapport au constructeur de trames
de l'application officielle (`C6117b.m30831g`).
- `chunkCount` = nombre total de morceaux pour cette commande ; `chunkIndex` est **basé sur 1**.
- `chunkLen` = nombre d'octets de `chunk` dans cette trame.
- Une seule écriture BLE peut être fragmentée par le MTU de la liaison ; le récepteur met en mémoire
tampon les octets bruts et ré-extrait les trames complètes. Les grandes charges utiles sont divisées
en plusieurs morceaux (même `cmd1/cmd2`, `chunkIndex` croissant) et réassemblées dans l'ordre.
### Convention d'opcode (✅ confirmé sur le fil)
- `cmd1 = 0xFFFF` : `cmd2` dans `0x80xx`/`0x90xx` = téléphone→montre (requête/définition) ; `0x00xx`/`0xa0xx` =
montre→téléphone (réponse). Les paires correspondent par l'octet de poids faible (`0x9055`↔`0xa055`, `0x8051`↔`0x0051`).
- `cmd1` spécifique à une fonctionnalité : suffixe `cmd2` = `0x0001` **SET**, `0x0002` **GET**, `0x0003` **ACK**.
### Corps du morceau
Pour chaque morceau, le corps est `payloadPiece ‖ CRC32_LE(payloadPiece)` (CRC 4 octets, petit-boutiste,
zlib/IEEE). Si la commande est **chiffrée** (voir §3), l'ensemble `payloadPiece ‖ CRC` est ensuite
chiffré en AES-128-CBC/PKCS7 et ce texte chiffré devient le `chunk` de la trame.
**Particularité du texte clair :** pour les opcodes en texte clair, la montre *compte* le CRC de 4 octets
dans `chunkLen` mais ne le **transmet** pas. Ainsi, lors du décodage d'une trame en texte clair, la longueur
réelle des données est `chunkLen − 4`. (Les trames chiffrées transportent le CRC à l'intérieur du texte
chiffré comme d'habitude.)
Dimensionnement des morceaux (pour que les morceaux chiffrés tombent sur les limites de blocs AES), avec
`maxWrite = mtu − 3` :
- chiffré : `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- texte clair : `maxWrite − 11 − 4 − 1`
✅ Toutes les valeurs `chunkLen` observées pour les trames chiffrées étaient des multiples de 16
(l'alignement des blocs est respecté).
---
## 3. Primitives cryptographiques
- **AES-128-CBC** avec remplissage **PKCS7** et un **IV fixe** (issu du firmware
`CmfCharacteristic.AES_IV`) :
`50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`.
- **CRC32** (zlib/IEEE), émis sous forme de 4 octets petit-boutistes.
- **SHA-256** sur la concaténation des parties.