
CMF Watch Pro 2向けのリバースエンジニアリングされたBLEプロトコル。GATTレイアウト、AES-128-CBC暗号化コマンドフレーム、認証ハンドシェイク、および代替コンパニオンアプリ開発のためのヘルスデータ同期を文書化しています。
非公式。 このドキュメントは、CMF Watch Pro 2(CMF by Nothing)のBluetooth Low Energy(BLE)プロトコルを、代替コンパニオンアプリのためにリバースエンジニアリングによって再構築したものです。Nothing/CMFとは提携しておらず、その承認も受けていません。自己責任で使用してください。
フレームヘッダーとオペコード内のすべてのマルチバイト整数はビッグエンディアンです。コマンドペイロード内の整数は、特に明記されていない限りリトルエンディアンです(これはデバイスファームウェアを反映しています)— 例外(GOALS_SET、GPS_PUSH、バルク転送のオフセット/長さはビッグエンディアン)に注意してください。
以下に示す明白でない主張にはすべて、その確立方法を示すタグが付いています:
後のセクションで以前のセクションを修正する場合、以前のテキストは削除せず、ポインタ付きで保持されます — どの読み取りが試され、反駁されたかを知ることで、次の人が同じ回り道をしなくて済みます。
すべてのキャプチャのテストデバイス:CMF Watch Pro 2-5485、ファームウェア1.0.0.73、シリアルCI04102520008192、MCU Actions ATS3089C(Cortex-M4)、画面466×360。
電話がGATTクライアントで、ウォッチがペリフェラルであり、CMF Watch Pro 2-XXXX(4桁の16進数)としてアドバタイズします。
| 目的 | サービス | キャラクタリスティック | プロパティ |
|---|---|---|---|
| コマンド書き込み | 0000fff0-0000-1000-8000-00805f9b34fb | 0000fff2-… | Write |
| コマンド通知 | 0000fff0-… | 0000fff1-… | Notify |
| シェル書き込み(AT) | — | 77d4ff01-2fe2-2334-0d35-9ccd078f529c | Write |
| シェル通知(AT) | — | 77d4ff02-… | Notify |
| バルクデータ書き込み | — | 02f00000-0000-0000-0000-00000000ffe1 | Write |
| バルクデータ通知 | — | 02f00000-…ffe2 | Notify |
各CCCD(00002902-…)に01 00を書き込むことで通知を有効にします。コマンドチャネル(fff1/fff2)は以下のフレーム化されたプロトコルを運びます。シェルチャネル(77d4…)はプレーンなATスタイルのテキスト(例:AT GETSECRET、§14参照)を運びます。データチャネル(02f0…)は大きなバイナリブロブ(ウォッチフェイス、ファームウェア、AGPS)を運び、コマンドチャネル上の制御オペコードによって調整されます。
サービスUUID — ウォッチは約10個のプライマリサービスをアドバタイズします。実機で列挙されたもの:
0xfff0(コマンド)、0x180f(バッテリー)、0x180a(デバイス情報)、0xefe7、0xffd0、
02f00000-…ffe0および02f00000-…fe00(データ)、77d4e67c-2fe2-2334-0d35-9ccd078f529c(シェル/ペアリング)、e49a3001-f69a-11e8-8eb2-f2801f1b9fd1、f48a23c0-f69a-11e8-8eb2-f2801f1b9fd1。
⚠️ シェルサービスの UUIDは**
77d4e67c-…**であり、77d4ff00-…ではありません。このドキュメントの以前の改訂版では、サービスがそのキャラクタリスティック(77d4ff01/77d4ff02、§14)のff00プレフィックスを共有していると想定していました — 実際にはそうではありません。少なくともこの確認が行われたユニットでは(freethinkel/fmcからの発見、§Sources参照)。キャラクタリスティックUUIDは変更されていません。77d4e67cがユニット間で安定しているかどうかは未検証です — ハードコードするのではなく列挙してください。
🌐 Web Bluetoothに関する注意。 Chromiumは、ページが
optionalServicesにリストしたサービスしか発見しません。フィルタリングされていないgetPrimaryServices()呼び出しでも同様です — 3つのサービスをリストしたページは3つしか見えませんが、chrome://bluetooth-internals(Chrome自身のC++レイヤー、スコープなし)は10個すべてを表示します。ブラウザクライアントを書く場合は、上記のすべてのUUIDを事前にリストしてください。そうしないと、明らかに存在するサービスでペアリングが失敗します。Firefox/SafariにはWeb Bluetoothはありません。ユーザージェスチャーとHTTPS/localhostが必要です。
✅ 単一のコマンドチャネル上で実際のセッション全体が実行されました — 160秒間の高負荷キャプチャ中、明示的なOTA/ウォッチフェイス転送中を除いて、データ/ファームウェアまたはシェルチャネルにはトラフィックがありませんでした。
0xF5)すべてのコマンドチャネルメッセージは、1つ以上の11バイトヘッダーフレームにラップされます:``` +------+-----------+--------+-------------+-------------+--------+-------------------+ | 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` を合わせて **オペコード** とします(§6 参照)。🔎 公式アプリのフレームビルダー(`C6117b.m30831g`)に対して確認済みです。
- `chunkCount` = このコマンドの総チャンク数。`chunkIndex` は **1 始まり** です。
- `chunkLen` = このフレーム内の `chunk` のバイト数。
- 1 回の BLE 書き込みはリンク MTU によって断片化される場合があります。受信側は生バイトをバッファリングし、完全なフレームを再抽出します。大きなペイロードは複数のチャンク(同じ `cmd1/cmd2`、増加する `chunkIndex`)に分割され、順番に再組み立てされます。
### オペコードの規約(✅ ワイヤ上で確認済み)
- `cmd1 = 0xFFFF`:`cmd2` が `0x80xx`/`0x90xx` = スマホ→ウォッチ(要求/設定)。`0x00xx`/`0xa0xx` = ウォッチ→スマホ(応答)。ペアは下位バイトで対応します(`0x9055`↔`0xa055`、`0x8051`↔`0x0051`)。
- 機能固有の `cmd1`:`cmd2` のサフィックスは `0x0001` = **SET**、`0x0002` = **GET**、`0x0003` = **ACK**。
### チャンク本体
各チャンクについて、本体は `payloadPiece ‖ CRC32_LE(payloadPiece)`(4 バイト CRC、リトルエンディアン、zlib/IEEE)です。コマンドが**暗号化**されている場合(§3 参照)、`payloadPiece ‖ CRC` 全体が AES-128-CBC/PKCS7 で暗号化され、その暗号文がフレームの `chunk` になります。
**平文の癖:** 平文オペコードの場合、ウォッチは `chunkLen` に 4 バイトの CRC を*数えます*が、それを**送信しません**。したがって、平文フレームをデコードするとき、実際のデータ長は `chunkLen − 4` です。(暗号化フレームは通常どおり CRC を暗号文内に含みます。)
チャンクサイズ(暗号化チャンクが AES ブロック境界に揃うように)、`maxWrite = mtu − 3` とします:
- 暗号化:`floor((maxWrite − 11) / 16) * 16 − 4 − 1`
- 平文:`maxWrite − 11 − 4 − 1`
✅ 観測されたすべての暗号化フレームの `chunkLen` 値は 16 の倍数でした(ブロック整合が成立)。
---
## 3. 暗号プリミティブ
- **AES-128-CBC**、**PKCS7** パディング、**固定 IV**(ファームウェア `CmfCharacteristic.AES_IV` より):
`50 51 52 53 54 55 56 57 60 61 62 63 64 65 66 5A`。
- **CRC32**(zlib/IEEE)、4 バイトのリトルエンディアンとして出力。
- パーツの連結に対する **SHA-256**。
鍵導出:```
authkey = SHA256( rnd1 ‖ rnd2 ‖ secret )[0..16] // persisted across sessions
sessionKey = SHA256( nonce ‖ authkey )[0..16] // per connection
secret = 16バイトのデバイスシークレット(シェルコマンド
AT GETSECRET → GETSECRET:<32-hex>,OK でウォッチから取得可能)。rnd1 = 電話側が選択する16ランダムバイト、rnd2 = ウォッチからの16ランダムバイト。nonce = ウォッチのnonce応答からのバイト。キーが設定された後、§5に記載された平文オペコードを除き、すべてのコマンドチャネルフレームはAES暗号化されます。
✅ 両方の導出が検証済み:ルート化された電話のntwatch.dbから復元されたauthkeyが、
キャプチャしたrnd1/rnd2/secretから導出された値と一致しました。キャプチャしたnonceから再現されたsessionKeyは、ライブフレームを復号します。
2つのエントリパスが同じnonce/confirmテールを共有します。
phone → (shell) AT GETSECRET watch → (shell) GETSECRET:<32hex>,OK phone: rnd1 = random16 ; signed1 = SHA256(rnd1 ‖ secret) phone → AUTH_PAIR_REQUEST (plaintext) payload = rnd1(16) ‖ signed1(32) // 48 B watch → AUTH_PAIR_REPLY (plaintext) payload = rnd2(16) ‖ signed2(32) // 48 B phone verifies signed2 == SHA256(rnd2 ‖ secret) phone: authkey = SHA256(rnd1 ‖ rnd2 ‖ secret)[0..16] → set crypto key = authkey phone → AUTH_PHONE_NAME (encrypted) payload = 0xA5 ‖ model(UTF-8) // e.g. "CMF Watch Pro 2" watch → AUTH_WATCH_MAC (encrypted) phone → AUTH_NONCE_REQUEST (encrypted) payload = 0xA5 watch → AUTH_NONCE_REPLY (encrypted) payload = nonce phone: sessionKey = SHA256(nonce ‖ authkey)[0..16] → set crypto key = sessionKey phone → AUTHENTICATED_CONFIRM_REQUEST (encrypted) payload = 0xA5 watch → AUTHENTICATED_CONFIRM_REPLY (encrypted) → state = Initialized
`AUTH_FAILED (0xFFFF,0xA061)` または署名の不一致が発生した場合、認証は失敗します。
### 4.2 再接続(authkey が既知の場合)```
set crypto key = authkey (persisted)
phone → AUTH_PHONE_NAME (encrypted) payload = 0xA5 ‖ model
watch → AUTH_WATCH_MAC (encrypted)
phone → AUTH_NONCE_REQUEST (encrypted) payload = 0xA5
watch → AUTH_NONCE_REPLY (encrypted) payload = nonce
sessionKey = SHA256(nonce ‖ authkey)[0..16] → set crypto key = sessionKey
phone → AUTHENTICATED_CONFIRM_REQUEST (encrypted) payload = 0xA5
watch → AUTHENTICATED_CONFIRM_REPLY (encrypted) → Initialized
✅ 再接続順序(シェルトラフィックなし)は、実際のキャプチャでそのまま観測されました。
⚠️→✅ データクエリの前に
TIMEが必須です。Initialized後、ウォッチはセッション内でTIME (FFFF 8004)が送信されるまでBATTERY、SERIAL_NUMBER_GET、ACTIVITY_FETCH_*ハンドシェイクに 応答しません — それがない場合、未承諾のFIRMWARE_VERSION_RETだけが届き、他はすべてタイムアウトします。✅ ライブで確認済み(Pixel 8a):TIMEなしで3つのGETを送信 → ファームウェアのみ応答;TIMEを先に送信 → バッテリー と シリアル番号が応答し始めます。
推奨されるフェーズ2の順序:TIME → FIRMWARE_VERSION_GET → SERIAL_NUMBER_GET →
BATTERY (0xA5) → 設定プッシュ → ヘルス同期(§8)。
ほとんどの設定に独立した「読み取り」オペコードはありません。*_GET(cmd2 = 0x0002、ペイロード
0xA5)を送信すると、ウォッチは現在値を保持した SET オペコード(cmd2 = 0x0001)で応答します。
SET コマンドは cmd2 = 0x0003 と空のボディで確認応答されます。
フレームはキーが設定されると AES 暗号化されますが、以下を除く これらのオペコードは常に平文です:
AUTH_PAIR_REQUEST(FFFF 8047)、AUTH_PAIR_REPLY(FFFF 0048)DATA_CHUNK_WRITE_WATCHFACE(FFFF 9064)、DATA_CHUNK_WRITE_FIRMWARE(FFFF 9042)、
DATA_CHUNK_WRITE_AGPS(FFFF 905F)