Decode the pod's key-exchange frames, and retire A-3
A-3 said the v2 needs no encryption: the pod completes the handshake and streams buttons in cleartext, and we have never sent a key. That is true for about fifty seconds. Captured on the tablet mid-ride and decoded here. The pod sends a `0xff` `03 00` frame carrying a **compressed 33-byte P-256 point** in protobuf field 1, a constant `0x02030000` in field 2, and 40 or 60 unexplained bytes in field 3. It sends one shortly after connecting and another **242 ms after the last button frame** — and in the same breath a `0xff` `05 00` status frame flips a flag 0 → 1 and a value 15 → 900. We answer neither, and from that moment the paddle byte of the button bitmask is frozen at `ff` while the D-pad keeps reporting in cleartext. So §2.3's open question — whether the daily Zwift unlock is needed on this path — is answered: it is, and this is the mechanism. The note that our unencrypted sessions "ran past three minutes" no longer holds either. Also recorded: this is **not** the Play handshake of §3.5. That is `RideOn 01 02` plus a raw 64-byte key on Sync RX; ours is a compressed 33-byte key inside protobuf on the async channel, with two fields the Play write-up has no room for. Implementing that spec verbatim would be implementing a different device. And the reason no responder is written yet: every frame we have is device → app. Nothing shows what a working client writes back, which is exactly what a responder must send, and five frames of one direction will not yield it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+34
-1
@@ -239,7 +239,40 @@ open-ended and not obviously safe. **Virtual shifting is therefore emulated app-
|
||||
> **three minutes** with button edges still arriving, so this path does not appear to hit
|
||||
> that timeout — but a long ride is the real test.
|
||||
|
||||
**Encryption — not required.** ✅ **A-3 holds for the v2.** The pod completed the handshake
|
||||
### 2.3.3 The `0xff` key-exchange frames — decoded 2026-08-27 ⚠ **A-3 is dead**
|
||||
|
||||
Captured on the tablet during a real ride. The `−` pod connected, streamed cleartext
|
||||
buttons for **fifty seconds**, and then stopped reporting its paddles while the link stayed
|
||||
up and battery frames continued. Two frames bracket the moment:
|
||||
|
||||
| Frame | Meaning |
|
||||
|-------|---------|
|
||||
| `ff 03 00` + protobuf | **key offer.** field 1 = 33 bytes, a *compressed* P-256 point (`02`/`03` prefix, fresh each session); field 2 = varint `0x02030000`, constant; field 3 = 40 or 60 bytes, contents unknown |
|
||||
| `ff 05 00` + protobuf | **status.** field 93 nests: `.1` = device id ASCII, `.2` = flag **0 → 1**, `.3` = **15 → 900**, `.4` = 8, `.5` = 9, `.6` = a small counter |
|
||||
|
||||
The pod sent a key offer at +4 s, a **second** key offer 242 ms after the last button frame,
|
||||
and the status flag flipped `0 → 1` with `.3` going `15 → 900` in the same breath. We never
|
||||
answered either offer.
|
||||
|
||||
**So A-3 does not hold.** The claim was that the pod "streams button and battery events in
|
||||
cleartext and the app has never sent a key" — true, but only for about a minute. The earlier
|
||||
note that "our unencrypted sessions ran past three minutes" and the open question of whether
|
||||
the daily unlock is needed on this path are both answered: it is, and this is the mechanism.
|
||||
|
||||
> **This is not the Play handshake of §3.5.** That one is `RideOn 01 02` + a *raw 64-byte*
|
||||
> key written to Sync RX, answered by `RideOn 00 09` + 64 bytes. Our v2 offers a
|
||||
> **compressed 33-byte** key inside protobuf on the **async** channel under type `0xff`, with
|
||||
> two fields the Play description has no room for. Implementing Makinolo's spec verbatim
|
||||
> would be implementing a different device's protocol.
|
||||
|
||||
> **The missing half.** Every frame above is device → app. Nothing in any capture shows what
|
||||
> a *working* client writes back, and that is precisely what a responder has to send. It
|
||||
> cannot be inferred from these five frames. Getting it means capturing both directions of a
|
||||
> real unlock — Android's Bluetooth HCI snoop log against the official Zwift app — which is
|
||||
> the next action, not more guessing.
|
||||
|
||||
**Encryption — was thought not required.** ⚠ **Superseded by §2.3.3; A-3 held only for the
|
||||
first minute of a session.** The pod completed the handshake
|
||||
and streamed button and battery events in cleartext, and the app has never sent a key. The
|
||||
ECDH P-256 → HKDF → AES-256-CCM path documented below is therefore *not* needed, and no
|
||||
crypto crate has been added.
|
||||
|
||||
Reference in New Issue
Block a user