Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 19 additions & 31 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,31 @@ company with an on-call rota:
for anything that puts user data at risk.
- Credit in the release notes if you want it.

## What this package actually is

This repo is a pure-Dart library with zero runtime dependencies: bytes in,
decoded records/frames/commands out. It doesn't run on its own, ship an app,
talk to a network, or store anything — no database, no telemetry, no
Firebase. It's a dependency of [edge](https://github.com/OpenStrap/edge),
which is where the app, its distribution model, and its data-handling
questions live.

## What's in scope

- Anything that discloses a user's health data off their device.
- Anything that lets a third party read, write to, or hijack the Bluetooth
session with someone's band.
- Local data-at-rest problems: the database, exports, the App Group container,
widget snapshots.
- The optional companion worker in
[backend](https://github.com/OpenStrap/backend): auth, the import endpoints,
the opt-in telemetry and health-upload paths.
- Anything that causes the app to send data anywhere the user did not agree to.
- A decoder that misparses bytes in a way that's exploitable, not just wrong
(buffer overreads, crashes on malformed input, anything an attacker could
use by controlling bytes the band or a proxy sends).
- Anything in this package's command builders that could be used to send a
command to a band the caller didn't ask for.

## What's out of scope

- The band's own firmware. We don't ship it, can't patch it, and won't publish
attacks against it.
- WHOOP's own apps and services. Please report those to WHOOP.
- The fact that sideloaded builds are unsigned, or that a rooted/jailbroken
device can read app storage. Both are known properties of the distribution
model, documented in the README.
- App-level distribution and privacy questions (signing, sideloading, device
storage access, telemetry, health-data upload) — those belong to
[edge's SECURITY.md](https://github.com/OpenStrap/edge/blob/main/SECURITY.md)
and [PRIVACY.md](https://github.com/OpenStrap/edge/blob/main/PRIVACY.md),
not this repo.
- Metric accuracy. Wrong numbers are bugs — open a normal issue.

## Where your data actually is

Worth knowing before you go looking: OpenStrap computes and stores your health
data on-device, and there's no account or server holding it. Two qualifications,
so the boundary is exact:

- **Anonymous diagnostics** (Firebase crash/performance, never health data) are
**on by default in GitHub release builds** and absent from App Store / Play
Store builds. Switchable off in-app.
- **Health-data contribution** uploads the local database, but is opt-in, off by
default, and compiled out of store builds entirely.

Everything else the companion worker does — legacy import, an update pointer —
is optional and carries no health data. See [edge's PRIVACY.md](https://github.com/OpenStrap/edge/blob/main/PRIVACY.md).

That means the realistic attack surface is the phone, the Bluetooth link, and
the local database — not a cloud backend. Reports focused there are the most
useful.
25 changes: 10 additions & 15 deletions lib/src/gen5_records.dart
Original file line number Diff line number Diff line change
Expand Up @@ -719,14 +719,14 @@ class Gen5OpticalBlock {

/// LED A drive current in units of 10 µA. @ sharedMeta[1:3] u16.
/// [ledACurrentMicroamps] is the same number in µA.
int get ledACurrentRaw => _u16(sharedMetaRaw, 1);
int get ledACurrentRaw => _view(sharedMetaRaw).getUint16(1, Endian.little);
int get ledACurrentMicroamps => ledACurrentRaw * 10;

/// Which driver output LED B is wired to. @ sharedMeta[3].
int get ledBDriverConnection => sharedMetaRaw[3];

/// LED B drive current in units of 10 µA. @ sharedMeta[4:6] u16.
int get ledBCurrentRaw => _u16(sharedMetaRaw, 4);
int get ledBCurrentRaw => _view(sharedMetaRaw).getUint16(4, Endian.little);
int get ledBCurrentMicroamps => ledBCurrentRaw * 10;

/// Which PHYSICAL photodiode (1..4) is routed into the TIA 1 path for this
Expand All @@ -736,7 +736,8 @@ class Gen5OpticalBlock {

/// TIA 1's ADC full-scale range, in µA. @ channel0Meta[1:5] u32
/// (descriptor relative 7).
int get channel0AdcRange => _u32(channel0MetaRaw, 1);
int get channel0AdcRange =>
_view(channel0MetaRaw).getUint32(1, Endian.little);

/// TIA 1's offset-current setting, raw wire value: **signed i16, 10 nA/LSB
/// (0.01 µA/LSB)**. @ channel0Meta[5:7] = descriptor relative 11.
Expand All @@ -747,7 +748,8 @@ class Gen5OpticalBlock {
/// labels the unscaled nA value with a "µA" suffix — that is a logging bug,
/// not an alternative unit. [tia1OffsetCurrentNanoamps] is the same number
/// in nA.
int get tia1OffsetCurrentRaw => _i16(channel0MetaRaw, 5);
int get tia1OffsetCurrentRaw =>
_view(channel0MetaRaw).getInt16(5, Endian.little);
int get tia1OffsetCurrentNanoamps => tia1OffsetCurrentRaw * 10;

@Deprecated(
Expand All @@ -760,11 +762,13 @@ class Gen5OpticalBlock {
/// TIA 2's, same three fields at the same relative offsets (descriptor
/// relative 13/14/18).
int get channel1Source => channel1MetaRaw[0];
int get channel1AdcRange => _u32(channel1MetaRaw, 1);
int get channel1AdcRange =>
_view(channel1MetaRaw).getUint32(1, Endian.little);

/// TIA 2's offset-current setting — see [tia1OffsetCurrentRaw] for the unit
/// and the quantization; @ channel1Meta[5:7] = descriptor relative 18.
int get tia2OffsetCurrentRaw => _i16(channel1MetaRaw, 5);
int get tia2OffsetCurrentRaw =>
_view(channel1MetaRaw).getInt16(5, Endian.little);
int get tia2OffsetCurrentNanoamps => tia2OffsetCurrentRaw * 10;

@Deprecated(
Expand All @@ -775,15 +779,6 @@ class Gen5OpticalBlock {
int get channel1AdcOffset => tia2OffsetCurrentRaw & 0xFFFF;
}

int _u16(Uint8List b, int i) => b[i] | (b[i + 1] << 8);
int _i16(Uint8List b, int i) {
final v = _u16(b, i);
return v >= 0x8000 ? v - 0x10000 : v;
}

int _u32(Uint8List b, int i) =>
b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24);

/// The raw optical deep buffer. Layout is confirmed: the body starts at inner
/// 18, then 5 blocks of 422 bytes each; every block holds 50 sample-pair slots,
/// photodiode slot A at `block + 21` and slot B at `block + 221`, and the
Expand Down
Loading