Cracking the WHOOP 5.0 over Bluetooth | Alec Jude Wilson
Contents
Interactive<br>Play with the protocol in your browser<br>Wake a simulated band up, drag its heart rate, tilt it, and watch the real Bluetooth bytes change.
I own a WHOOP band. It reads my heart rate about a hundred times a second, tracks how I sleep, and decides every morning whether I’m “recovered.” All of that lives in WHOOP’s app and WHOOP’s cloud, behind a subscription. The data is about my own body and I can barely touch it.
Someone had already fixed this for the older band. John Middleton built my-whoop, an open-source, local-first client that reads a WHOOP 4.0 over Bluetooth and keeps everything on your own hardware: the app, the decoding, the storage, all of it.1 I have a 5.0. When I pointed his client at it, every channel went dark. Getting it talking again, and getting the biometric stream out, is the story. The 4.0 groundwork is John’s. The 5.0 work below is mine, built on top of his.
This is the technical writeup. If you’d rather poke at the protocol than read about it, I built an interactive playground where you can decode and build real 5.0 frames in the browser, take apart a biometric packet, and watch the checksums update as you type. The full spec lives there too.
A quick Bluetooth primer
Skip this if you’ve written BLE code before. If you haven’t, four words show up constantly below and they all map onto things you already know from the web.
A Bluetooth Low Energy device acts like a tiny server. It has no URLs. What it has instead are characteristics , which are closer to individual fields you can hit. Some you read. Some you write to. A few you can subscribe to, and those are called notify characteristics. Subscribe to one and the device pushes you a fresh value every time it has a new one, no polling involved, which is how a band feeds you a heart rate twice a second without you asking each time. Characteristics that belong together get bundled into a service with its own long UUID. The spec has a name for this whole scheme, GATT, and that’s genuinely the last time you need to think about that acronym.
The word that actually matters here is bond . Before a phone can touch most of a band’s characteristics, the two have to pair and then bond, which is roughly Bluetooth’s version of a TLS handshake with a “remember this device next time” step attached. And pairing has two strengths, which turns out to be the whole story of this post. The weak one is called “just-works”: it brings up an encrypted link without either side proving who it is, the way HTTPS behaves if you switch certificate checking off. Encrypted, but you can’t be sure who’s on the other end. The strong one is authenticated , and it also defends against someone wedged in the middle pretending to be one of the two devices. Every characteristic is tagged with how strong a bond you need before it will answer you, and that tag is the difference between the 4.0 and the 5.0.
The 4.0 was a pushover
WHOOP 4.0 exposes a custom service. You connect, you do one “just-works” write to its command characteristic, and iOS quietly brings up an encrypted link. After that the band streams: live heart rate, R-R intervals (the gaps between consecutive beats, which heart-rate variability is computed from), events, and a fourteen-day backlog of stored biometrics that the recovery and strain and sleep numbers are derived from. No passkey, no pairing dance. The whole “bonding trick” is a single write.
John’s client does all of that already: live heart rate on screen, history backfilling, the whole pipeline from band to local store. Then I pointed it at a 5.0 and every channel went dark.
The 5.0 wall
The 5.0’s custom service looks familiar at first. Same shape as the 4.0: one characteristic you write commands to, several you subscribe to for pushed data, all under a renamed UUID.
fd4b0001-cce1-4033-93ce-002d5875f58a (custom service)<br>fd4b0002 write → command channel<br>fd4b0003 notify → command responses<br>fd4b0004 notify → events<br>fd4b0005 notify → data / fragmented<br>fd4b0007 notify → new in 5.0
The familiarity ends the moment you touch it. Every attempt to subscribe or write comes straight back as a refusal from the Bluetooth stack itself, before the band’s own software ever sees the request:
subscribe fd4b0003 → Code 15 Encryption is insufficient<br>subscribe fd4b0004/5/7 → Code 5 Authentication is insufficient<br>write fd4b0002 → GATT Insufficient Authentication
The exact wording matters. “Encryption is insufficient” would mean an ordinary encrypted link is enough. “Authentication is insufficient” is stricter: it wants the authenticated pairing from the primer, the kind that’s protected against a man-in-the-middle, not just-works. The 4.0 handed over biometrics on a just-works link. The 5.0 demands the strong handshake before it will so much as acknowledge a subscription.
I spent a while trying to make a Mac complete that handshake, because a Mac is where my tooling lived. Dead end,...