
Reverse-engineertes BLE-Protokoll für die CMF Watch Pro 2, das das GATT-Layout, AES-128-CBC-verschlüsselte Befehlsframes, den Authentifizierungs-Handshake und die Synchronisierung von Gesundheitsdaten für die Entwicklung alternativer Companion-Apps dokumentiert.
Inoffiziell. Dieses Dokument beschreibt das Bluetooth-Low-Energy-Protokoll (BLE) der CMF Watch Pro 2 (CMF by Nothing), rekonstruiert durch Reverse Engineering für eine alternative Begleit-App. Es steht in keiner Verbindung zu Nothing/CMF und wird von diesen nicht unterstützt. Nutzung auf eigene Gefahr.
Alle Mehrbyte-Ganzzahlen im Frame-Header und in den Opcodes sind Big-Endian. Ganzzahlen innerhalb
von Befehls-Payloads sind Little-Endian, sofern nicht anders angegeben (dies spiegelt die Geräte-Firmware wider) —
achten Sie auf die Ausnahmen (GOALS_SET, GPS_PUSH, Bulk-Transfer-Offset/-Länge sind Big-Endian).
Jede nicht offensichtliche Behauptung unten ist mit der Art ihrer Ermittlung gekennzeichnet:
Wo ein späterer Abschnitt einen früheren korrigiert, wird der frühere Text mit einem Verweis beibehalten statt gelöscht — zu wissen, welche Messwerte versucht und widerlegt wurden, erspart der nächsten Person denselben Umweg.
Testgerät für alle Mitschnitte: CMF Watch Pro 2-5485, Firmware 1.0.0.73, Seriennummer CI04102520008192,
MCU Actions ATS3089C (Cortex-M4), Bildschirm 466×360.
Das Telefon ist der GATT-Client; die Uhr ist das Peripheriegerät und wirbt als CMF Watch Pro 2-XXXX
(4 Hex-Zeichen).
| Zweck | Dienst | Charakteristik | Eigenschaften |
|---|---|---|---|
| Befehls-Schreiben | 0000fff0-0000-1000-8000-00805f9b34fb | 0000fff2-… | Write |
| Befehls-Benachrichtigung | 0000fff0-… | 0000fff1-… | Notify |
| Shell-Schreiben (AT) | — | 77d4ff01-2fe2-2334-0d35-9ccd078f529c | Write |
| Shell-Benachrichtigung (AT) | — | 77d4ff02-… | Notify |
| Bulk-Datenschreiben | — | 02f00000-0000-0000-0000-00000000ffe1 | Write |
| Bulk-Datenbenachrichtigung | — | 02f00000-…ffe2 | Notify |
Aktivieren Sie Benachrichtigungen durch Schreiben von 01 00 in jedes CCCD (00002902-…). Der Befehlskanal
(fff1/fff2) überträgt das unten beschriebene gerahmte Protokoll. Der Shell-Kanal (77d4…) überträgt
einfachen AT-Stil-Text (z. B. AT GETSECRET; siehe §14). Der Datenkanal (02f0…) überträgt große Binär-
Blobs (Watchface, Firmware, AGPS), koordiniert durch Steuer-Opcodes auf dem Befehlskanal.
Dienst-UUIDs — die Uhr bewirbt ~10 primäre Dienste. Auf einem echten Gerät aufgelistet:
0xfff0 (Befehl), 0x180f (Akku), 0x180a (Geräteinformationen), 0xefe7, 0xffd0,
02f00000-…ffe0 und 02f00000-…fe00 (Daten), 77d4e67c-2fe2-2334-0d35-9ccd078f529c (Shell/
Pairing), e49a3001-f69a-11e8-8eb2-f2801f1b9fd1, f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.
⚠️ Die Shell-Dienst-UUID ist
77d4e67c-…, nicht77d4ff00-…. Frühere Revisionen dieses Dokuments gingen davon aus, dass der Dienst dasff00-Präfix seiner Charakteristiken teilt (77d4ff01/77d4ff02, §14) — das tut er nicht, zumindest nicht auf dem Gerät, auf dem dies geprüft wurde (Erkenntnis aus freethinkel/fmc, siehe §Quellen). Die Charakteristik-UUIDs sind unverändert. Nicht verifiziert, ob77d4e67cüber Geräte hinweg stabil ist — aufzählen statt fest codieren.
🌐 Web-Bluetooth-Hinweis. Chromium entdeckt nur Dienste, die die Seite in
optionalServicesaufgelistet hat, selbst bei einem ungefiltertengetPrimaryServices()-Aufruf — eine Seite, die 3 Dienste auflistet, sieht 3, währendchrome://bluetooth-internals(Chromiums eigene C++-Ebene, unbegrenzt) alle 10 zeigt. Wenn Sie einen Browser-Client schreiben, listen Sie jede oben genannte UUID im Voraus auf, sonst schlägt das Pairing fehl mit Diensten, die offensichtlich existieren. Kein Web Bluetooth in Firefox/Safari; benötigt eine Benutzergeste + HTTPS/localhost.
✅ Eine vollständige reale Sitzung lief auf dem einzigen Befehlskanal — während eines 160 s langen Mitschnitts mit starker Nutzung gab es keinen Datenverkehr auf den Daten-/Firmware- oder Shell-Kanälen, außer während einer expliziten OTA-/Watchface- Übertragung.
0xF5)Jede Befehlskanal-Nachricht ist in einen oder mehrere Frames mit 11-Byte-Header verpackt:``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 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` bilden zusammen den **Opcode** (siehe §6). 🔎 bestätigt gegen den Frame-Builder der offiziellen App (`C6117b.m30831g`).
- `chunkCount` = Gesamtzahl der Chunks für diesen Befehl; `chunkIndex` ist **1-basiert**.
- `chunkLen` = Anzahl der Bytes von `chunk` in diesem Frame.
- Ein einzelner BLE-Write kann durch die Link-MTU fragmentiert werden; der Empfänger puffert die Rohbytes und extrahiert die vollständigen Frames erneut. Große Payloads werden in mehrere Chunks aufgeteilt (gleiches `cmd1/cmd2`, aufsteigender `chunkIndex`) und in der Reihenfolge wieder zusammengesetzt.
### Opcode-Konvention (✅ im Datenverkehr bestätigt)
- `cmd1 = 0xFFFF`: `cmd2` in `0x80xx`/`0x90xx` = Telefon→Uhr (Anfrage/Setzen); `0x00xx`/`0xa0xx` = Uhr→Telefon (Antwort). Paare stimmen im niederwertigen Byte überein (`0x9055`↔`0xa055`, `0x8051`↔`0x0051`).
- funktionsspezifisches `cmd1`: `cmd2`-Suffix = `0x0001` **SET**, `0x0002` **GET**, `0x0003` **ACK**.
### Chunk-Body
Für jeden Chunk ist der Body `payloadPiece ‖ CRC32_LE(payloadPiece)` (4-Byte-CRC, Little-Endian, zlib/IEEE). Wenn der Befehl **verschlüsselt** ist (siehe §3), wird das gesamte `payloadPiece ‖ CRC` anschließend mit AES-128-CBC/PKCS7 verschlüsselt, und dieser Chiffretext wird zum Frame-`chunk`.
**Klartext-Eigenheit:** Bei Klartext-Opcodes *zählt* die Uhr die 4-Byte-CRC in `chunkLen`, überträgt sie aber **nicht**. Beim Dekodieren eines Klartext-Frames beträgt die tatsächliche Datenlänge also `chunkLen − 4`. (Verschlüsselte Frames tragen die CRC wie üblich innerhalb des Chiffretexts.)
Chunk-Größe (damit verschlüsselte Chunks auf AES-Blockgrenzen landen), mit `maxWrite = mtu − 3`:
- verschlüsselt: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- Klartext: `maxWrite − 11 − 4 − 1`
✅ Alle beobachteten `chunkLen`-Werte verschlüsselter Frames waren Vielfache von 16 (Blockausrichtung gilt).
---
## 3. Kryptografische Grundbausteine
- **AES-128-CBC** mit **PKCS7**-Padding und einem **festen IV** (aus der Firmware `CmfCharacteristic.AES_IV`):
`50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`.
- **CRC32** (zlib/IEEE), ausgegeben als 4 Little-Endian-Bytes.
- **SHA-256** über die Verkettung der Teile.
Schlüsselableitung:```
authkey = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16] // persisted across sessions
sessionKey = SHA256( nonce ‖ authkey )[0..16] // per connection
secret = 16-Byte-Gerätegeheimnis (vom Gerät über den Shell-Befehl abrufbar
AT GETSECRET → GETSECRET:<32-hex>,OK).rnd1 = 16 vom Telefon gewählte Zufallsbytes; rnd2 = 16 Zufallsbytes vom Gerät.nonce = Bytes aus der Nonce-Antwort des Geräts.Nachdem der Schlüssel gesetzt ist, sind alle Befehls-Kanal-Frames AES-verschlüsselt, außer den Klartext- Opcodes, die in §5 aufgeführt sind.