
Protocollo BLE reverse-engineered per il CMF Watch Pro 2, con documentazione del layout GATT, frame di comando crittografati AES-128-CBC, handshake di autenticazione e sincronizzazione dei dati sulla salute per lo sviluppo di app companion alternative.
Non ufficiale. Questo documento descrive il protocollo Bluetooth Low Energy (BLE) del CMF Watch Pro 2 (CMF by Nothing), ricostruito tramite reverse engineering per un'app companion alternativa. Non è affiliato né approvato da Nothing/CMF. Utilizzo a proprio rischio.
Tutti gli interi multi-byte nell'header del frame e negli opcode sono big-endian. Gli interi all'interno
dei payload dei comandi sono little-endian salvo diversa indicazione (questo rispecchia il firmware del dispositivo) —
attenzione alle eccezioni (GOALS_SET, GPS_PUSH, offset/lunghezza del trasferimento bulk sono big-endian).
Ogni affermazione non ovvia di seguito è etichettata con il metodo con cui è stata stabilita:
Dove una sezione successiva corregge una precedente, il testo precedente viene mantenuto con un riferimento piuttosto che eliminato — sapere quali letture sono state provate e confutate evita alla persona successiva lo stesso giro.
Dispositivo di test per tutte le catture: CMF Watch Pro 2-5485, fw 1.0.0.73, seriale CI04102520008192,
MCU Actions ATS3089C (Cortex-M4), schermo 466×360.
Il telefono è il client GATT; l'orologio è il periferico, che si pubblicizza come CMF Watch Pro 2-XXXX
(4 caratteri esadecimali).
| Scopo | Servizio | Caratteristica | Proprietà |
|---|---|---|---|
| Scrittura comandi | 0000fff0-0000-1000-8000-00805f9b34fb | 0000fff2-… | Write |
| Notifica comandi | 0000fff0-… | 0000fff1-… | Notify |
| Scrittura shell (AT) | — | 77d4ff01-2fe2-2334-0d35-9ccd078f529c | Write |
| Notifica shell (AT) | — | 77d4ff02-… | Notify |
| Scrittura dati bulk | — | 02f00000-0000-0000-0000-00000000ffe1 | Write |
| Notifica dati bulk | — | 02f00000-…ffe2 | Notify |
Abilita le notifiche scrivendo 01 00 su ogni CCCD (00002902-…). Il canale comandi
(fff1/fff2) trasporta il protocollo incapsulato descritto di seguito. Il canale shell (77d4…) trasporta
testo in stile AT semplice (es. AT GETSECRET; vedi §14). Il canale dati (02f0…) trasporta grandi blob
binari (watchface, firmware, AGPS), coordinati da opcode di controllo sul canale comandi.
UUID dei servizi — l'orologio pubblicizza ~10 servizi primari. Enumerati su un'unità reale:
0xfff0 (comandi), 0x180f (batteria), 0x180a (info dispositivo), 0xefe7, 0xffd0,
02f00000-…ffe0 e 02f00000-…fe00 (dati), 77d4e67c-2fe2-2334-0d35-9ccd078f529c (shell /
accoppiamento), e49a3001-f69a-11e8-8eb2-f2801f1b9fd1, f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.
⚠️ L'UUID del servizio shell è
77d4e67c-…, non77d4ff00-…. Revisioni precedenti di questo documento assumevano che il servizio condividesse il prefissoff00delle sue caratteristiche (77d4ff01/77d4ff02, §14) — non è così, almeno sull'unità su cui è stato verificato (risultato da freethinkel/fmc, vedi §Fonti). Gli UUID delle caratteristiche sono invariati. Non verificato se77d4e67csia stabile tra le unità — enumerare piuttosto che hardcodare.
🌐 Nota su Web Bluetooth. Chromium scopre solo i servizi che la pagina ha elencato in
optionalServices, anche per una chiamatagetPrimaryServices()non filtrata — una pagina che elenca 3 servizi ne vede 3, mentrechrome://bluetooth-internals(il livello C++ di Chrome, non limitato) mostra tutti e 10. Se scrivi un client browser, elenca tutti gli UUID sopra in anticipo o l'accoppiamento fallirà con servizi che esistono chiaramente. Nessun Web Bluetooth in Firefox/Safari; richiede un gesto dell'utente + HTTPS/localhost.
✅ Un'intera sessione reale è stata eseguita sul solo canale comandi — durante una cattura di 160 s di uso intensivo non c'è stato traffico sui canali dati/firmware o shell tranne durante un trasferimento esplicito OTA/watchface.
0xF5)Ogni messaggio sul canale comandi è incapsulato in uno o più frame con header di 11 byte:``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 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` insieme formano l'**opcode** (vedi §6). 🔎 confermato rispetto al frame builder
dell'app ufficiale (`C6117b.m30831g`).
- `chunkCount` = numero totale di chunk per questo comando; `chunkIndex` è **1-based**.
- `chunkLen` = numero di byte di `chunk` in questo frame.
- Una singola scrittura BLE può essere frammentata dall'MTU del link; il ricevitore bufferizza i byte
grezzi e ri-estrae i frame completi. I payload di grandi dimensioni vengono suddivisi in più chunk
(stesso `cmd1/cmd2`, `chunkIndex` crescente) e riassemblati in ordine.
### Convenzione opcode (✅ confermata sul wire)
- `cmd1 = 0xFFFF`: `cmd2` in `0x80xx`/`0x90xx` = telefono→orologio (richiesta/impostazione); `0x00xx`/`0xa0xx` =
orologio→telefono (risposta). Le coppie corrispondono tramite il byte basso (`0x9055`↔`0xa055`, `0x8051`↔`0x0051`).
- `cmd1` specifico della funzionalità: suffisso `cmd2` = `0x0001` **SET**, `0x0002` **GET**, `0x0003` **ACK**.
### Corpo del chunk
Per ogni chunk, il corpo è `payloadPiece ‖ CRC32_LE(payloadPiece)` (CRC a 4 byte, little-endian,
zlib/IEEE). Se il comando è **cifrato** (vedi §3), l'intero `payloadPiece ‖ CRC` viene poi
cifrato con AES-128-CBC/PKCS7 e quel ciphertext diventa il `chunk` del frame.
**Stranezza del plaintext:** per gli opcode in chiaro l'orologio *conta* il CRC a 4 byte in `chunkLen` ma
**non** lo trasmette. Quindi, decodificando un frame in chiaro, la lunghezza effettiva dei dati è `chunkLen − 4`.
(I frame cifrati trasportano il CRC all'interno del ciphertext come di consueto.)
Dimensionamento dei chunk (così i chunk cifrati cadono sui confini dei blocchi AES), con `maxWrite = mtu − 3`:
- cifrato: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- in chiaro: `maxWrite − 11 − 4 − 1`
✅ Tutti i valori `chunkLen` dei frame cifrati osservati erano multipli di 16 (l'allineamento dei blocchi è rispettato).
---
## 3. Primitive crittografiche
- **AES-128-CBC** con padding **PKCS7** e un **IV fisso** (dal firmware
`CmfCharacteristic.AES_IV`):
`50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`.
- **CRC32** (zlib/IEEE), emesso come 4 byte little-endian.
- **SHA-256** sulla concatenazione delle parti.
Derivazione della chiave:```
authkey = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16] // persisted across sessions
sessionKey = SHA256( nonce ‖ authkey )[0..16] // per connection
secret = segreto del dispositivo a 16 byte (ottenibile dall'orologio tramite il comando shell
AT GETSECRET → GETSECRET:<32-hex>,OK).rnd1 = 16 byte casuali scelti dal telefono; rnd2 = 16 byte casuali dall'orologio.nonce = byte dalla risposta nonce dell'orologio.Dopo che la chiave è impostata, tutti i frame del canale di comando sono crittografati AES tranne gli opcode in chiaro elencati in §5.