
Protocolo BLE de ingeniería inversa para el CMF Watch Pro 2, documentando el diseño GATT, tramas de comandos cifradas con AES-128-CBC, handshake de autenticación y sincronización de datos de salud para el desarrollo de aplicaciones complementarias alternativas.
No oficial. Este documento describe el protocolo Bluetooth Low Energy (BLE) del CMF Watch Pro 2 (CMF by Nothing), reconstruido mediante ingeniería inversa para una aplicación complementaria alternativa. No está afiliado ni respaldado por Nothing/CMF. Úselo bajo su propio riesgo.
Todos los enteros de múltiples bytes en el encabezado de trama y los opcodes son big-endian. Los enteros dentro
de las cargas útiles de comandos son little-endian salvo que se indique lo contrario (esto refleja el firmware del dispositivo) —
cuidado con las excepciones (GOALS_SET, GPS_PUSH, el desplazamiento/longitud de la transferencia masiva son big-endian).
Cada afirmación no obvia a continuación está etiquetada con cómo se estableció:
Cuando una sección posterior corrige una anterior, el texto anterior se conserva con una referencia en lugar de eliminarse — saber qué lecturas se probaron y se refutaron le ahorra a la siguiente persona el mismo desvío.
Dispositivo de prueba para todas las capturas: CMF Watch Pro 2-5485, fw 1.0.0.73, serie CI04102520008192,
MCU Actions ATS3089C (Cortex-M4), pantalla 466×360.
El teléfono es el cliente GATT; el reloj es el periférico, que se anuncia como CMF Watch Pro 2-XXXX
(4 caracteres hexadecimales).
| Propósito | Servicio | Característica | Propiedades |
|---|---|---|---|
| Escritura de comandos | 0000fff0-0000-1000-8000-00805f9b34fb | 0000fff2-… | Write |
| Notificación de comandos | 0000fff0-… | 0000fff1-… | Notify |
| Escritura de shell (AT) | — | 77d4ff01-2fe2-2334-0d35-9ccd078f529c | Write |
| Notificación de shell (AT) | — | 77d4ff02-… | Notify |
| Escritura de datos masivos | — | 02f00000-0000-0000-0000-00000000ffe1 | Write |
| Notificación de datos masivos | — | 02f00000-…ffe2 | Notify |
Habilite las notificaciones escribiendo 01 00 en cada CCCD (00002902-…). El canal de comandos
(fff1/fff2) transporta el protocolo enmarcado a continuación. El canal de shell (77d4…) transporta texto
plano estilo AT (p. ej., AT GETSECRET; consulte §14). El canal de datos (02f0…) transporta blobs
binarios grandes (esfera de reloj, firmware, AGPS), coordinados por opcodes de control en el canal de comandos.
UUID de servicios — el reloj anuncia ~10 servicios primarios. Enumerados en una unidad real:
0xfff0 (comandos), 0x180f (batería), 0x180a (información del dispositivo), 0xefe7, 0xffd0,
02f00000-…ffe0 y 02f00000-…fe00 (datos), 77d4e67c-2fe2-2334-0d35-9ccd078f529c (shell /
emparejamiento), e49a3001-f69a-11e8-8eb2-f2801f1b9fd1, f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.
⚠️ El servicio de shell tiene el UUID
77d4e67c-…, no77d4ff00-…. Revisiones anteriores de este documento asumían que el servicio compartía el prefijoff00de sus características (77d4ff01/77d4ff02, §14) — no es así, al menos en la unidad en la que se verificó (hallazgo de freethinkel/fmc, consulte §Fuentes). Los UUID de las características no han cambiado. No se ha verificado si77d4e67ces estable entre unidades — enumere en lugar de codificar.
🌐 Nota sobre Web Bluetooth. Chromium solo descubre servicios que la página haya listado en
optionalServices, incluso para una llamada sin filtro agetPrimaryServices()— una página que liste 3 servicios verá 3, mientras quechrome://bluetooth-internals(la propia capa C++ de Chrome, sin ámbito) muestra los 10. Si escribe un cliente de navegador, liste todos los UUID anteriores de antemano o el emparejamiento fallará con servicios que existen claramente. No hay Web Bluetooth en Firefox/Safari; requiere un gesto del usuario + HTTPS/localhost.
✅ Una sesión real completa se ejecutó en el único canal de comandos — durante una captura de 160 s de uso intensivo no hubo tráfico en los canales de datos/firmware o shell excepto durante una transferencia explícita de OTA/esfera de reloj.
0xF5)Cada mensaje del canal de comandos está envuelto en una o más tramas con encabezado de 11 bytes:``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 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` juntos forman el **opcode** (ver §6). 🔎 confirmado contra el constructor de tramas
de la app oficial (`C6117b.m30831g`).
- `chunkCount` = total de fragmentos para este comando; `chunkIndex` tiene base **1**.
- `chunkLen` = número de bytes de `chunk` en esta trama.
- Una sola escritura BLE puede fragmentarse según el MTU del enlace; el receptor almacena los bytes
crudos y vuelve a extraer tramas completas. Las cargas útiles grandes se dividen en varios
fragmentos (mismo `cmd1/cmd2`, `chunkIndex` creciente) y se reensamblan en orden.
### Convención de opcodes (✅ confirmado en el cable)
- `cmd1 = 0xFFFF`: `cmd2` en `0x80xx`/`0x90xx` = teléfono→reloj (solicitud/ajuste); `0x00xx`/`0xa0xx` =
reloj→teléfono (respuesta). Los pares coinciden por el byte bajo (`0x9055`↔`0xa055`, `0x8051`↔`0x0051`).
- `cmd1` específico de función: el sufijo de `cmd2` = `0x0001` **SET**, `0x0002` **GET**, `0x0003` **ACK**.
### Cuerpo del fragmento
Para cada fragmento, el cuerpo es `payloadPiece ‖ CRC32_LE(payloadPiece)` (CRC de 4 bytes, little-endian,
zlib/IEEE). Si el comando está **cifrado** (ver §3), todo `payloadPiece ‖ CRC` se cifra luego con
AES-128-CBC/PKCS7 y ese texto cifrado se convierte en el `chunk` de la trama.
**Particularidad del texto plano:** para opcodes en texto plano, el reloj *cuenta* el CRC de 4 bytes en
`chunkLen` pero **no** lo transmite. Así que al decodificar una trama en texto plano, la longitud real
de los datos es `chunkLen − 4`. (Las tramas cifradas llevan el CRC dentro del texto cifrado como es normal).
Tamaño de fragmento (para que los fragmentos cifrados caigan en los límites de bloque AES), con
`maxWrite = mtu − 3`:
- cifrado: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- texto plano: `maxWrite − 11 − 4 − 1`
✅ Todos los valores de `chunkLen` observados en tramas cifradas fueron múltiplos de 16 (el alineamiento
de bloques se cumple).
---
## 3. Primitivas criptográficas
- **AES-128-CBC** con relleno **PKCS7** y un **IV fijo** (del firmware
`CmfCharacteristic.AES_IV`):
`50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`.
- **CRC32** (zlib/IEEE), emitido como 4 bytes little-endian.
- **SHA-256** sobre la concatenación de las partes.
Derivación de claves:```
authkey = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16] // persisted across sessions
sessionKey = SHA256( nonce ‖ authkey )[0..16] // per connection
secret = secreto del dispositivo de 16 bytes (obtenible desde el reloj mediante el comando de shell
AT GETSECRET → GETSECRET:<32-hex>,OK).rnd1 = 16 bytes aleatorios elegidos por el teléfono; rnd2 = 16 bytes aleatorios del reloj.nonce = bytes de la respuesta de nonce del reloj.