
Protocolo BLE com engenharia reversa para o CMF Watch Pro 2, documentando layout GATT, quadros de comando criptografados AES-128-CBC, handshake de autenticação e sincronização de dados de saúde para desenvolvimento de aplicativo companheiro alternativo.
Não oficial. Este documento descreve o protocolo Bluetooth Low Energy (BLE) do CMF Watch Pro 2 (CMF by Nothing), reconstruído por engenharia reversa para um aplicativo complementar alternativo. Não é afiliado nem endossado pela Nothing/CMF. Use por sua conta e risco.
Todos os inteiros multibyte no cabeçalho do quadro e opcodes são big-endian. Inteiros dentro
de payloads de comando são little-endian, salvo indicação em contrário (isso reflete o firmware do
dispositivo) — cuidado com as exceções (GOALS_SET, GPS_PUSH, offset/comprimento de transferência
em massa são big-endian).
Cada afirmação não óbvia abaixo é marcada com a forma como foi estabelecida:
Quando uma seção posterior corrige uma anterior, o texto anterior é mantido com um ponteiro em vez de ser excluído — saber quais leituras foram tentadas e refutadas poupa à próxima pessoa o mesmo desvio.
Dispositivo de teste para todas as capturas: CMF Watch Pro 2-5485, fw 1.0.0.73, serial CI04102520008192,
MCU Actions ATS3089C (Cortex-M4), tela 466×360.
O telefone é o cliente GATT; o relógio é o periférico, anunciando-se como CMF Watch Pro 2-XXXX
(4 caracteres hexadecimais).
| Finalidade | Serviço | Característica | Propriedades |
|---|---|---|---|
| Escrita de comando | 0000fff0-0000-1000-8000-00805f9b34fb | 0000fff2-… | Write |
| Notificação de comando | 0000fff0-… | 0000fff1-… | Notify |
| Escrita de shell (AT) | — | 77d4ff01-2fe2-2334-0d35-9ccd078f529c | Write |
| Notificação de shell (AT) | — | 77d4ff02-… | Notify |
| Escrita de dados em massa | — | 02f00000-0000-0000-0000-00000000ffe1 | Write |
| Notificação de dados em massa | — | 02f00000-…ffe2 | Notify |
Ative as notificações escrevendo 01 00 em cada CCCD (00002902-…). O canal de comando
(fff1/fff2) transporta o protocolo enquadrado abaixo. O canal de shell (77d4…) transporta
texto simples no estilo AT (ex.: AT GETSECRET; ver §14). O canal de dados (02f0…) transporta
grandes blobs binários (mostrador, firmware, AGPS), coordenados por opcodes de controle no canal de comando.
UUIDs de serviço — o relógio anuncia ~10 serviços primários. Enumerados em uma unidade real:
0xfff0 (comando), 0x180f (bateria), 0x180a (informações do dispositivo), 0xefe7, 0xffd0,
02f00000-…ffe0 e 02f00000-…fe00 (dados), 77d4e67c-2fe2-2334-0d35-9ccd078f529c (shell /
pareamento), e49a3001-f69a-11e8-8eb2-f2801f1b9fd1, f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1.
⚠️ O serviço de shell tem UUID
77d4e67c-…, não77d4ff00-…. Revisões anteriores deste documento presumiam que o serviço compartilhava o prefixoff00de suas características (77d4ff01/77d4ff02, §14) — não compartilha, pelo menos na unidade em que isso foi verificado (descoberta de freethinkel/fmc, ver §Fontes). Os UUIDs das características são inalterados. Não verificado se77d4e67cé estável entre unidades — enumere em vez de codificar.
🌐 Observação sobre Web Bluetooth. O Chromium apenas descobre serviços que a página listou em
optionalServices, mesmo para uma chamadagetPrimaryServices()sem filtro — uma página que lista 3 serviços vê 3, enquantochrome://bluetooth-internals(a própria camada C++ do Chrome, sem escopo) mostra todos os 10. Se você escrever um cliente de navegador, liste todos os UUIDs acima antecipadamente ou o pareamento falhará com serviços que claramente existem. Sem Web Bluetooth no Firefox/Safari; requer um gesto do usuário + HTTPS/localhost.
✅ Uma sessão real inteira rodou no único canal de comando — durante uma captura de 160 s de uso intenso, não houve tráfego nos canais de dados/firmware ou shell, exceto durante uma transferência explícita de OTA/mostrador.
0xF5)Toda mensagem do canal de comando é encapsulada em um ou mais quadros com cabeçalho 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 formam o **opcode** (ver §6). 🔎 confirmado no construtor de frames do app oficial (`C6117b.m30831g`).
- `chunkCount` = total de chunks para este comando; `chunkIndex` é **baseado em 1**.
- `chunkLen` = número de bytes de `chunk` neste frame.
- Uma única escrita BLE pode ser fragmentada pelo MTU do link; o receptor armazena os bytes brutos e re-extrai frames completos. Payloads grandes são divididos em múltiplos chunks (mesmo `cmd1/cmd2`, com `chunkIndex` crescente) e remontados em ordem.
### Convenção de opcode (✅ confirmado no wire)
- `cmd1 = 0xFFFF`: `cmd2` em `0x80xx`/`0x90xx` = telefone→relógio (requisição/definição); `0x00xx`/`0xa0xx` = relógio→telefone (resposta). Os pares correspondem pelo byte baixo (`0x9055`↔`0xa055`, `0x8051`↔`0x0051`).
- `cmd1` específico de funcionalidade: sufixo de `cmd2` = `0x0001` **SET**, `0x0002` **GET**, `0x0003` **ACK**.
### Corpo do chunk
Para cada chunk, o corpo é `payloadPiece ‖ CRC32_LE(payloadPiece)` (CRC de 4 bytes, little-endian, zlib/IEEE). Se o comando for **criptografado** (ver §3), todo o `payloadPiece ‖ CRC` é então criptografado com AES-128-CBC/PKCS7 e esse ciphertext torna-se o `chunk` do frame.
**Peculiaridade do plaintext:** para opcodes em plaintext, o relógio *conta* o CRC de 4 bytes em `chunkLen`, mas **não** o transmite. Portanto, ao decodificar um frame em plaintext, o comprimento real dos dados é `chunkLen − 4`. (Frames criptografados carregam o CRC dentro do ciphertext, como de costume.)
Dimensionamento do chunk (para que chunks criptografados caiam em limites de bloco AES), com `maxWrite = mtu − 3`:
- criptografado: `floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- plaintext: `maxWrite − 11 − 4 − 1`
✅ Todos os valores de `chunkLen` observados em frames criptografados eram múltiplos de 16 (o alinhamento de bloco é mantido).
---
## 3. Primitivas criptográficas
- **AES-128-CBC** com preenchimento **PKCS7** e um **IV fixo** (do 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 a concatenação das partes.
Derivação de chave:```
authkey = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16] // persisted across sessions
sessionKey = SHA256( nonce ‖ authkey )[0..16] // per connection
secret = segredo do dispositivo de 16 bytes (obtido do relógio via comando de shell
AT GETSECRET → GETSECRET:<32-hex>,OK).rnd1 = 16 bytes aleatórios escolhidos pelo telefone; rnd2 = 16 bytes aleatórios do relógio.nonce = bytes da resposta de nonce do relógio.Após a chave ser definida, todos os quadros do canal de comandos são criptografados com AES, exceto os opcodes de texto simples listados na §5.