Documentation
¶
Overview ¶
Package ble decodes BLE advertisement payloads — currently just Apple Continuity manufacturer-data — into operator-facing structures. Pure offline parser; no transport, no hardware.
Wrap-vs-native judgement: the Apple Continuity format is a reverse-engineered public spec — furiousMAC's project, hexway's AppleJuice writeups, AppleBleee and AppleAir-style scanners all document the same TLV layout. The dissector is a short walker (~150 LoC of bytes-to-field) over a byte slice. Wrapping a FAP for this would require an SD-card install + a firmware-fork dependency for a pure parser. Native delivers host-side analysis (paste a captured Apple manufacturer-data hex from a forum post or a Wireshark capture and decode without a Flipper attached), inline test coverage against published vectors, and a table the operator can extend without rebuilding firmware.
What this package covers:
- Apple Continuity TLV walker (with separator-tolerant hex intake; auto-strip of optional 4C00 manufacturer prefix and full AD-structure prefix).
- Named action types per furiousMAC's catalog (0x02 iBeacon through 0x12 Find My).
- Per-type field decoding for the well-documented action types — Nearby Info, Nearby Action, Handoff, Tethering, Proximity Pairing, AirDrop, Magic Switch.
What this package does NOT cover (deliberately out of scope):
- Decryption of encrypted bodies (Handoff, ProximityPairing past the public prefix) — Apple's session keys are not publicly recoverable.
- Tag-name lookup for the 0xDF range or any vendor-private types beyond Apple's set.
- Round-trip re-encode — happy to add if a caller materialises.
Index ¶
Constants ¶
const AppleManufacturerID = 0x004C
AppleManufacturerID is Apple's Bluetooth SIG company identifier. See https://www.bluetooth.com/specifications/assigned-numbers/.
const EddystoneServiceUUID uint16 = 0xFEAA
EddystoneServiceUUID is the 16-bit UUID Google assigned to Eddystone. Service-data fields prefixed with this UUID carry Eddystone frames.
Variables ¶
This section is empty.
Functions ¶
func EncodeEddystone ¶ added in v0.387.0
func EncodeEddystone(r EddystoneEncodeRequest) ([]byte, error)
EncodeEddystone builds the raw bytes of an Eddystone frame — the inverse of DecodeEddystone. The four open frame types (UID / URL / TLM / EID) are supported, each round-trip-verified against the decoder.
Wrap-vs-native judgement ¶
Native, and the inverse of the existing decoder. The Eddystone frame layouts plus the URL scheme-prefix and TLD-expansion tables are public (Google's open Eddystone protocol specification); encoding is pure byte assembly + a prefix/expansion lookup — no crypto, no hardware. It produces the service-data payload an operator advertises from a beacon (e.g. a spoofed URL beacon for a phishing / proximity test); generation only, no BLE TX, so it is Low risk like the decoder. Correctness is verifiable two ways: round-trip against DecodeEddystone and the byte-exact examples in the Eddystone-URL spec.
Deliberately deferred ¶
eTLM (encrypted telemetry, TLM version 0x01) and EID derivation require the per-beacon identity key and AES, which the operator owns out-of-band; EID here only frames a caller-supplied token. The URL encoder abbreviates using the documented scheme/expansion tables and otherwise passes printable ASCII through; a byte the spec cannot represent is rejected rather than silently dropped.
Types ¶
type ActionTLV ¶
type ActionTLV struct {
// Type is the Action Type byte — 0x05 (AirDrop), 0x10 (Nearby
// Info), etc. Stored as an int rather than byte so JSON renders
// as a plain integer.
Type int `json:"type"`
// TypeHex is the operator-facing form of Type — uppercase,
// always 2 chars, no 0x prefix ("05", "10"). Matches every
// public reference's format.
TypeHex string `json:"type_hex"`
// Name is the canonical name for the action type, or
// "Unknown" when the type isn't in our catalog. The walker
// still records and returns unknown types so operators can
// flag novel signatures.
Name string `json:"name"`
// Length is the declared payload length in bytes (the L byte
// of the TLV).
Length int `json:"length"`
// Hex is the operator-facing hex rendering of Value — always
// uppercase, no separators.
Hex string `json:"hex"`
// Value is the raw payload bytes.
Value []byte `json:"-"`
// Fields carries documented-action-type field decodes, keyed
// by short snake_case names. Nil for unknown types or for
// types whose body we don't dissect further.
Fields map[string]any `json:"fields,omitempty"`
// DecodeWarning is non-empty when the per-type decoder hit a
// recoverable shape issue (e.g. payload shorter than the
// documented minimum for that action). The TLV is still
// returned so the operator can see the raw bytes; the warning
// flags that Fields may be partial or missing.
DecodeWarning string `json:"decode_warning,omitempty"`
}
ActionTLV is one decoded Continuity TLV entry. For documented action types Fields carries the parsed sub-fields by name; for unknown types Fields is nil and the operator has Hex.
type Continuity ¶
type Continuity struct {
// TLVs is the ordered list of action-type entries pulled from
// the payload. May be empty when the payload is empty after
// prefix-stripping (legal but unusual — an empty 0x004C
// manufacturer record).
TLVs []ActionTLV `json:"tlvs"`
// Count is len(TLVs) — surfaced for callers that consume the
// JSON directly without needing to compute it.
Count int `json:"count"`
// StrippedPrefix records what the parser stripped from the
// front of the input before walking ("none", "manufacturer",
// "ad_structure"). Useful to confirm the parser interpreted
// the input as the operator expected.
StrippedPrefix string `json:"stripped_prefix"`
}
Continuity is the top-level decode result.
func Decode ¶
func Decode(hexBlob string) (Continuity, error)
Decode parses a hex-encoded Apple Continuity payload. Three input shapes are accepted; the parser strips any prefix it recognises:
- Bare TLVs: 10 02 1B 00
- With manufacturer ID: 4C 00 10 02 1B 00
- Full AD structure: 06 FF 4C 00 10 02 1B 00
Separators (':' '-' '_' whitespace) are tolerated so callers can paste hex from Wireshark, btmon, etc., without preprocessing.
func DecodeBytes ¶
func DecodeBytes(b []byte) (Continuity, error)
DecodeBytes is the byte-slice variant of Decode for callers that already have raw bytes. Same prefix-detection and walking logic.
type Eddystone ¶ added in v0.210.0
type Eddystone struct {
// FrameType is the frame-type byte at offset 0 of the
// service-data payload.
FrameType int `json:"frame_type"`
// FrameTypeHex is the operator-facing hex form ("00", "10",
// "20", "30").
FrameTypeHex string `json:"frame_type_hex"`
// FrameName is the canonical name ("UID", "URL", "TLM",
// "EID") or "Unknown" for out-of-catalog types.
FrameName string `json:"frame_name"`
// Fields carries the decoded per-frame-type fields. Nil for
// unknown types or when DecodeWarning is set.
Fields map[string]any `json:"fields,omitempty"`
// Hex is the operator-facing hex rendering of the full
// service-data payload (including the frame-type byte).
Hex string `json:"hex"`
// DecodeWarning is non-empty when the payload is shorter than
// the documented minimum for its frame type. The frame type
// and raw hex are still surfaced.
DecodeWarning string `json:"decode_warning,omitempty"`
}
Eddystone is the top-level decode result.
func DecodeEddystone ¶ added in v0.210.0
DecodeEddystone parses a hex-encoded Eddystone service-data payload. Three input shapes are accepted; the parser strips any recognised prefix:
- Bare service data: 10 02 00 03 67 6F 6F (URL frame for "goo")
- With UUID prefix: AA FE 10 02 00 03 67 6F 6F
- Full AD structure: 0A 16 AA FE 10 02 00 03 67 6F 6F
(AD-type 0x16 = ServiceData16Bit; the UUID 0xFEAA is little-endian on the wire as AA FE.)
Separators (':' '-' '_' whitespace) are tolerated.
type EddystoneEncodeRequest ¶ added in v0.387.0
type EddystoneEncodeRequest struct {
Kind string `json:"kind"`
TxPower int `json:"tx_power_dbm,omitempty"`
URL string `json:"url,omitempty"`
Namespace string `json:"namespace,omitempty"`
Instance string `json:"instance,omitempty"`
BatteryMV int `json:"battery_mv,omitempty"`
TemperatureC float64 `json:"temperature_c,omitempty"`
AdvCount int `json:"adv_count,omitempty"`
Uptime100ms int `json:"uptime_100ms,omitempty"`
EphemeralID string `json:"ephemeral_id,omitempty"`
// Wrap controls the framing of the returned bytes:
// "" / "frame" — the bare Eddystone frame (starts with the
// frame-type byte); the default.
// "uuid" — prefixed with the 0xAA 0xFE service UUID.
// "ad" — the full BLE Service-Data AD structure
// (length, 0x16, 0xAA, 0xFE, frame) ready to drop
// into an advertising payload.
Wrap string `json:"wrap,omitempty"`
}
EddystoneEncodeRequest describes one Eddystone frame to build. Kind selects the frame type: "uid", "url", "tlm", or "eid". Only the fields relevant to that kind are read.
- uid: TxPower + Namespace (10-byte hex) + Instance (6-byte hex).
- url: TxPower + URL (the scheme prefix and TLD expansions are abbreviated to their Eddystone codes automatically).
- tlm: BatteryMV + TemperatureC + AdvCount + Uptime100ms (version 0x00, the unencrypted form).
- eid: TxPower + EphemeralID (8-byte hex; the operator supplies the rotating token — this builder does not derive it).
type EddystoneFrameType ¶ added in v0.210.0
type EddystoneFrameType byte
EddystoneFrameType enumerates the four documented frame types.
const ( // FrameUID — 16-byte beacon ID (10-byte namespace + 6-byte // instance). 18-byte total payload. FrameUID EddystoneFrameType = 0x00 // FrameURL — encoded URL up to ~17 bytes after the scheme // byte. Variable-length payload. FrameURL EddystoneFrameType = 0x10 // FrameTLM — telemetry: battery voltage, temperature, advert // count, time-since-boot. 14-byte payload. FrameTLM EddystoneFrameType = 0x20 // FrameEID — Ephemeral ID (rotating 8-byte token, requires // a server-side key to resolve). 10-byte payload. FrameEID EddystoneFrameType = 0x30 )
type GAPAdvertisement ¶ added in v0.217.0
type GAPAdvertisement struct {
// Records is the ordered list of decoded records.
Records []GAPRecord `json:"records"`
// Count is len(Records).
Count int `json:"count"`
// Warnings collects non-fatal observations (zero-length
// terminator hit, trailing bytes after final record, etc.).
Warnings []string `json:"warnings,omitempty"`
}
GAPAdvertisement is the top-level walker result.
func DecodeGAP ¶ added in v0.217.0
func DecodeGAP(hexBlob string) (GAPAdvertisement, error)
DecodeGAP parses a hex-encoded BLE GAP / EIR advertisement. Tolerates ':' / '-' / '_' / whitespace separators.
func DecodeGAPBytes ¶ added in v0.217.0
func DecodeGAPBytes(b []byte) (GAPAdvertisement, error)
DecodeGAPBytes is the byte-slice variant of DecodeGAP for callers that already have raw bytes.
type GAPRecord ¶ added in v0.217.0
type GAPRecord struct {
// Length is the declared record length byte (size of AD
// type + data; doesn't count the length byte itself).
Length int `json:"length"`
// ADType is the 1-byte AD type identifier.
ADType int `json:"ad_type"`
// ADTypeHex is the operator-facing form ("01", "FF").
ADTypeHex string `json:"ad_type_hex"`
// Name is the documented Bluetooth SIG name for the AD type,
// or "Unknown" when the type isn't in our table.
Name string `json:"name"`
// DataHex is the operator-facing hex rendering of the data
// portion (the bytes after the AD type, length = Length-1).
DataHex string `json:"data_hex"`
// Decoded is the per-AD-type structured field decode.
// Populated for the documented types we dissect (Flags,
// Service UUID lists, Local Name, TX Power, Service Data,
// Manufacturer Data, Appearance). nil for types we leave as
// raw hex.
Decoded map[string]any `json:"decoded,omitempty"`
}
GAPRecord is one decoded (length, AD type, data) entry.