diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index fbf6dbc..f3772e0 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -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.