Skip to content

feat(cli): add check-key to diagnose a device's key setup - #57

Merged
Micahscope merged 4 commits into
mainfrom
key-diagnose-tool
Oct 13, 2026
Merged

Micahscope merged 4 commits into
mainfrom
key-diagnose-tool

Conversation

@Micahscope

Copy link
Copy Markdown
Contributor

Previously, nothing could tell you whether a device's key setup was right, or which part of it was wrong, if it was.

hubblenetwork check-key --device-id <id> --key <key> [--firmware PATH] compares three sources of evidence to find key mismatches. It attempts to decode BLE packets locally with the device's key. It confirms key type and succesfully decoded packets on the backend with the device id. Optionally, the tool can check a firmware file (hex, out, elf) or a built config file (.config, sdkconfig, autoconf.h) to see if the key size and encryption types are selected correctly.

Supporting changes:

  • cloud.get_device() and Organization.get_device(). The three identical device URL builders are merged into _device_endpoint().
  • Device parses encryption, counter_source, period_exponent and the most recent packet time.
  • A 404 now raises NotFoundError, a subclass of BackendError.
  • crypto: ctr_eid() is ported from hubble-device-sdk and pinned to its tools/ble-adv.py vectors. Also adds decrypt_at() and eax_eid_index().
  • ble.scan() takes an until callback to stop early.

@Micahscope

Copy link
Copy Markdown
Contributor Author

Rough draft; didn't do any human review other than basic testing.

Main idea: if your packets don't reach the backend, this tool will tell you what is wrong with your keys (incorrect size, incorrect encryption type, time out-of-sync).

Potential future nice-to-have is a satellite scan through a connected SDR as well, to check satellite packet keys

@buckleypaul

Copy link
Copy Markdown
Collaborator

I thought I had already done something similar, but only parts of this have an overlap - the key sweep logic. I asked Claude to diagnose the overlap:

The decrypt sweep in diagnose.AirScan tries each key size, counter mode and clock offset to see which one decrypts. main already has a sweep for this in detect.py (CtrCounterModeDetector, EaxExponentDetector, detect_eid_type). That leaves two sweeps to maintain. AirScan's is broader, so detect.py could move onto it.

Micahscope and others added 2 commits October 8, 2026 10:07
decrypt_eax derived key_0 once, at counter 0. The EID derivation takes
key_0 from the counter's high 16 bits, so every counter of 65536 or
more was compared against the wrong EIDs and never matched.

The pool reaches 65536 once the period is 2^10 s or more. At the
cloud's default exponent 15, that is pool slot 2, about 18 hours of
uptime. From then on, `ble scan --key` and `ble detect` reported a
correctly keyed AES-EAX device as undecodable.

key_0 is now derived for each 65536-counter block, as _generate_eid
does, and cached within the sweep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two things stopped the suite from running on Windows:

- TestReadmeStaysTrue and the source scans in test_terminal_compat read
  files with read_text() and no encoding, which is cp1252 on Windows.
  The README's UTF-8 failed to decode. They now read UTF-8 explicitly.
- test_terminal_compat imported pty at module level. pty needs termios,
  which Windows lacks, so the whole file failed to collect. The import
  now happens inside the one pty test, which skips where pty is missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Micahscope

Copy link
Copy Markdown
Contributor Author

I thought I had already done something similar, but only parts of this have an overlap - the key sweep logic. I asked Claude to diagnose the overlap:

The decrypt sweep in diagnose.AirScan tries each key size, counter mode and clock offset to see which one decrypts. main already has a sweep for this in detect.py (CtrCounterModeDetector, EaxExponentDetector, detect_eid_type). That leaves two sweeps to maintain. AirScan's is broader, so detect.py could move onto it.

New patch combines this tool, now ble validate, with ble check-time and ble detect into 1 tool.
Requires key input. Can provide more detail with dev id input. Can provide even more with firmware (.elf,.bin, etc, autoconf.h, etc) archive input

buckleypaul and others added 2 commits October 13, 2026 11:52
Adds synchronous cloud decryption, so nearby packets can be attributed
to devices without holding their keys.

- cloud.decode_packets: one POST /org/{id}/packets/decode call of up to
  25 advertisements. A 403 names the missing read-packets scope.
- Organization.decode_packets: takes any number of AES-CTR/AES-EAX
  packets, dedupes identical advertisements, batches them, and returns
  results aligned to the input (None where the cloud could not decrypt).
  Packets outside the endpoint's 72h/2h window are answered None locally,
  since one invalid item fails the whole batch. Also adds get_device().
- AesEaxPacket.raw keeps the service data byte for byte, as the endpoint
  needs it.
- errors: HTTP 403/404/429 map to PermissionDeniedError, NotFoundError
  and RateLimitError; raise_for_response clips oversized error bodies.
- ble decode: scans and streams cloud-decrypted rows, flushing every 25
  packets or 2s and remembering each distinct advertisement's answer.
- CLI cleanup: one _org_or_fail() helper for building the Organization,
  and the decode table printer reuses _StreamingTablePrinter.

tests/fake_packets.py builds correctly encrypted CTR and EAX
advertisements for the new tests.
Previously, `ble validate` stopped at the first step that failed and
reported almost every key problem as "Unable to decrypt packet with
given device key". Nothing could tell you which part of a key setup
was wrong.

`ble validate [--key <key>] [--device-id <id>] [--firmware PATH]`
compares up to four sources of evidence and names the mismatch, such
as a key registered UNIX_TIME but flashed DEVICE_UPTIME, a 32-byte key
in a build that reads only 16 bytes, or a key registered to another
device.

- --key decodes BLE packets locally, trying both key sizes, both
  counter modes, a year of clock skew and every AES-EAX period. It
  reports the device's clock, including one that was never set.
- --device-id checks the registered key type, and sends the packets
  heard to the cloud decode endpoint, which says whose they are. This
  works with no gateway nearby, and the cloud stores what it decodes.
  With --key, only this key's packets are sent, so an undecoded packet
  means the registered key is not this --key. A token without the
  read-packets scope falls back to whether the backend decoded a packet
  from the device in the last 10 minutes.
- --firmware reads the key size and counter source from the flashed
  image (hex, ihex, bin, elf) or a build's config (.config, sdkconfig,
  autoconf.h), and looks for the key's bytes in the image.

Pass --key, --device-id, or both. Existing `ble validate --key K
--device-id D` invocations still work. --device-id alone needs no key:
the cloud picks the device out of everything heard. --key alone needs
no credentials, and covers what `ble check-time` did.

This change removes `ble detect` and `ble check-time` in favor of
`ble validate`. Both removed tools print a usage error and point to
`ble validate`.

Supporting changes:

- validate.py holds the diagnosis, decoupled from the CLI like
  detect.py.
- org.decode_packets() is Organization.decode_packets() for a caller
  that already resolved the environment, so validate does not spend two
  requests constructing an Organization.
- Device parses encryption, counter_source, period_exponent and the
  most recent packet time.
- crypto: ctr_eid() is ported from hubble-device-sdk and pinned to its
  tools/ble-adv.py vectors. Also adds decrypt_at() and eax_eid_index().
  decrypt() parses a packet once per sweep, not once per counter.
  tests/fake_packets.py now derives its CTR EIDs from it.
- ble.scan() takes an `until` callback to stop early.

Signed-off-by: Micah Morefield <micah@hubble.com>
Co-authored-by: Paul Buckley <paul@hubble.com>
@Micahscope

Copy link
Copy Markdown
Contributor Author

Combined with improvements in #58 (comment)

-- Added ble decode
-- Update ble validate to not require a device key; you can also provide just a device ID (+credentials)

@Micahscope
Micahscope merged commit 428c5fd into main Oct 13, 2026
2 checks passed
@Micahscope
Micahscope deleted the key-diagnose-tool branch October 13, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants