ble

package
v0.421.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 2, 2026 License: AGPL-3.0 Imports: 6 Imported by: 0

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

View Source
const AppleManufacturerID = 0x004C

AppleManufacturerID is Apple's Bluetooth SIG company identifier. See https://www.bluetooth.com/specifications/assigned-numbers/.

View Source
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

func DecodeEddystone(hexBlob string) (Eddystone, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL