What This Post Covers
When a Bluetooth Mesh node sends a message over the air, two critical security jobs happen at the network layer: first, sensitive header fields are obfuscated so a passive sniffer cannot track devices, and second, the receiver uses replay protection to discard messages that an attacker tries to re-send later. We will also look at how nodes announce their presence using Mesh Beacons.
This post is based on the Bluetooth Mesh Profile Specification (pages 117β120) and explains each concept using diagrams, BlueZ code snippets, and plain English.
Key Terms
Why Obfuscation Exists
Every Bluetooth Mesh network PDU carries a TTL (time-to-live), a sequence number (SEQ), and a source address (SRC) in its header. Without any protection, a passive sniffer sitting nearby can watch these fields and track which device sent which message β a simple privacy leak.
The spec’s answer is obfuscation: XOR those header fields with a pseudo-random pad derived from a Privacy Key, so an eavesdropper sees only scrambled bytes. Note that this is not encryption β it is designed only to stop passive tracking, not a determined attacker with the Privacy Key.
The Obfuscation Flow Step by Step
|
STEP 1
PrivacyRandom |
β |
STEP 2
Privacy Plaintext |
β |
STEP 3
PECB = e() |
β |
STEP 4
XOR β ObfuscatedData |
First 7 bytes of the already-encrypted part of the PDU:(EncDST || EncTransportPDU || NetMIC)[0β6] |
0x0000000000 || IV Index || PrivacyRandom(5 zero bytes + 4-byte IV Index + 7-byte PrivacyRandom = 16 bytes total) |
AES-128 block cipher using Privacy Key. Output = 16-byte PECB. Only first 6 bytes used. |
ObfuscatedData = (CTL||TTL||SEQ||SRC) β PECB[0β5]6 header bytes become scrambled. |
What the Network PDU Looks Like On-Air
| NID/IVI 1 byte |
ObfuscatedData 6 bytes (CTL+TTL+SEQ+SRC scrambled) |
EncDST 2 bytes |
EncTransportPDU variable |
NetMIC 4 or 8 bytes |
Green = obfuscated (scrambled header). Blue = encrypted payload. NID/IVI is never obfuscated.
How a Receiver Reverses Obfuscation
The receiver already knows the IV Index from the network and holds the Privacy Key. It just repeats the same steps (PrivacyRandom β Privacy Plaintext β PECB) and XORs the ObfuscatedData again to recover the original CTL, TTL, SEQ, and SRC. XOR is its own inverse.
Why the IV Index Is Part of This
If someone tries to break obfuscation by collecting lots of PDUs, they eventually succeed β but only until the IV Index changes. When the network increments the IV Index (a normal mesh event), every obfuscation attempt the attacker has built must restart from zero.
BlueZ Reference β Where Obfuscation Happens
In the BlueZ mesh stack, network layer obfuscation is implemented in mesh/net.c:
/* mesh/net.c β BlueZ mesh stack (simplified) */
/* Build the Privacy Plaintext: 5 zero bytes + IV Index + PrivacyRandom */
static void build_privacy_plaintext(uint32_t iv_index,
const uint8_t *privacy_random, /* 7 bytes */
uint8_t *out) /* 16 bytes */
{
memset(out, 0, 5); /* octets 0-4: zeros */
l_put_be32(iv_index, out + 5); /* octets 5-8: IV Index */
memcpy(out + 9, privacy_random, 7); /* octets 9-15: PrivRandom*/
}
/* Derive PECB using AES-128 with the Privacy Key */
static void derive_pecb(const uint8_t *privacy_key, /* 16 bytes */
const uint8_t *plaintext, /* 16 bytes */
uint8_t *pecb) /* 16 bytes out */
{
aes_ecb_one(privacy_key, plaintext, pecb);
}
/* XOR the 6-byte header with PECB[0..5] to produce ObfuscatedData */
static void obfuscate_header(const uint8_t *pecb,
uint8_t *hdr, /* CTL|TTL|SEQ(3)|SRC(2) = 6 bytes */
uint8_t *out)
{
int i;
for (i = 0; i < 6; i++)
out[i] = hdr[i] ^ pecb[i];
}
βΉοΈ aes_ecb_one() is BlueZ’s thin wrapper around the kernel’s AES-128 ECB. The real production code also handles the PrivacyRandom extraction from the encrypted payload bytes.
What Is a Replay Attack?
Imagine Node A turns on a smart light by sending a “Light ON” message. A nearby attacker silently records this valid, properly encrypted packet. Later, after Node A has turned the light off, the attacker re-transmits the exact same captured packet. The light turns on again β the attacker never broke any crypto, they just replayed an old message.
|
Node A
Sends “Light ON”
SEQ = 42 |
β
valid msg
|
Node B
Accepts SEQ=42
Stores max SEQ=42 |
β
replay!
|
Replay Attacker
Re-sends captured
SEQ = 42 packet |
β
to Node B
|
|
β οΈ Without replay protection, Node B cannot tell this is an old replayed packet β crypto checks still pass!
|
|||||
The Solution: Sequence Numbers + IV Index Tracking
Every element (a logical endpoint inside a node) increments its sequence number for every new message. The receiver maintains a Replay Protection List β a simple table of (source address β last accepted SEQ, last accepted IV Index) pairs.
| Incoming Message Condition | Action | Reason |
|---|---|---|
| SEQ > last stored SEQ and IV Index = stored IV | β Accept & update list | Fresh message from the same IV era |
| SEQ β€ last stored SEQ, same IV Index | β Discard (likely replay) | Old or duplicate packet |
| IV Index higher than stored IV | β Accept & reset stored SEQ | Network moved to a new IV era; SEQ counter restarts |
| IV Index lower than stored IV | β Discard | Stale from a previous IV era; definitely replayed |
Segmented Message Replay β The Tricky Case
A large message gets split into numbered segments, each carrying its own SEQ value. The spec says: store the highest SEQ seen for that source. When the attacker re-injects an old segment, the node notices its SEQ is below the stored maximum and drops it. The diagram below maps to Figure 3.43 in the spec.
| Node A β Node B | Node B Action | Attacker β Node B |
|---|---|---|
| Msg0 Unseg SEQ=10 | Store A β max_seq=10 | β |
| Msg1 Seg0/2 SEQ=11 Msg1 Seg1/2 SEQ=12 Msg1 Seg2/2 SEQ=13 |
Ack sent; max_seq=13 | β |
| Msg1 Seg1/2 retransmit SEQ=14 | Store A β max_seq=14 Save last Ack (SeqAuth=11, Ack=0,1,2) |
β |
| β | β Drop β SEQ=14 already seen, old replay | Replays Seg1/2 SEQ=14 |
| Msg1 Seg1/2 new SEQ=15 | Store A β max_seq=15; Ack | β |
BlueZ β Replay Protection List Lookup
In BlueZ mesh/net.c, replay protection is checked before any access-layer processing:
/* mesh/net.c β BlueZ mesh (simplified illustration) */
struct replay_cache_entry {
uint16_t src; /* source element address */
uint32_t seq; /* highest accepted sequence number */
uint32_t iv_index; /* IV Index for the above seq */
};
static bool replay_check(struct mesh_net *net,
uint16_t src,
uint32_t seq,
uint32_t iv_index)
{
struct replay_cache_entry *entry = find_replay_entry(net, src);
if (!entry) {
/* First message from this source β always accept */
add_replay_entry(net, src, seq, iv_index);
return true; /* not a replay */
}
/* Discard if IV Index is lower */
if (iv_index < entry->iv_index)
return false;
/* New IV Index: accept and reset */
if (iv_index > entry->iv_index) {
entry->iv_index = iv_index;
entry->seq = seq;
return true;
}
/* Same IV Index: accept only strictly higher SEQ */
if (seq <= entry->seq)
return false; /* replay detected β discard */
entry->seq = seq;
return true;
}
βΉοΈ In a real device with limited RAM, this list has a fixed maximum size. If it is full and cannot track a new source address, the spec requires discarding the message rather than risking a replay vulnerability.
What Are Mesh Beacons?
Before two Bluetooth Mesh nodes can talk securely, they must find each other and exchange trust. Mesh beacons are small BLE advertising packets that nodes and unprovisioned devices broadcast periodically for this purpose. They are sent as non-connectable, non-scannable undirected advertising events β the receiver just passively listens; no connection is formed.
Beacon Packet Structure
| Field | Size (octets) | Notes |
|---|---|---|
| Len | 1 | Standard BLE AD length byte |
| Type | 1 | AD Type = Β«Mesh BeaconΒ» |
| Beacon Type | 1 | 0x00 = Unprovisioned, 0x01 = Secure Network |
| Beacon Data | variable | Contents depend on Beacon Type |
The Two Beacon Types
Sent by a device that has not yet joined any mesh network. It is shouting “I am here, please provision me!” A Provisioner app (e.g., a phone running nRF Mesh) hears it and starts the provisioning handshake.
β’ Device UUID (16 bytes) β unique per device
β’ OOB Information (2 bytes) β what out-of-band auth methods are available
β’ URI Hash (4 bytes, optional) β hash of a URI with more device info
Sent by nodes that are already provisioned members of a mesh network. They use this beacon to keep the network healthy β advertising the current IV Index and key refresh state so other nodes stay in sync.
β’ Network ID or hash
β’ Current IV Index
β’ Key Refresh Flag & IV Update Flag
β’ Authentication value (MIC)
Beacon Endianness
All multi-byte numeric fields inside mesh beacons are transmitted in big-endian order β the most significant byte first. This is consistent throughout the mesh spec.
BlueZ β Sending an Unprovisioned Device Beacon
In BlueZ mesh/prov.c, an unprovisioned node starts advertising its beacon like this:
/* mesh/prov.c β BlueZ (simplified) */
#define MESH_AD_TYPE_BEACON 0x2B
/* Build the Unprovisioned Device Beacon payload */
static void build_unprovisioned_beacon(struct mesh_node *node,
uint8_t *buf,
size_t *len)
{
uint8_t *p = buf;
*p++ = 0x00; /* Beacon Type: Unprovisioned (0x00) */
/* Device UUID β 16 bytes, identifies this specific device */
memcpy(p, node->uuid, 16);
p += 16;
/* OOB Information β 2 bytes big-endian */
l_put_be16(node->oob_info, p);
p += 2;
/* URI Hash β 4 bytes, only present if the URI flag in OOB Info is set */
if (node->oob_info & OOB_URI_FLAG) {
l_put_be32(node->uri_hash, p);
p += 4;
}
*len = p - buf;
}
/* Kick off advertising with this beacon */
static void start_unprovisioned_beacon(struct mesh_node *node)
{
uint8_t buf[23];
size_t len;
build_unprovisioned_beacon(node, buf, &len);
mesh_send_beacon(MESH_AD_TYPE_BEACON, buf, len); /* BLE adv. non-connectable */
}
βΉοΈ mesh_send_beacon() ultimately calls BlueZ’s btd_adapter_set_adv_data() with a non-connectable, non-scannable advertising type (ADV_NONCONN_IND).
Quick Recap
| Concept | What It Does | Key Fields Involved |
|---|---|---|
| Network Obfuscation | Hides TTL, SEQ, SRC from passive sniffers using XOR + AES | PrivacyRandom, PECB, ObfuscatedData, Privacy Key, IV Index |
| Replay Protection | Discards re-sent old messages by tracking last accepted SEQ + IV | SEQ, IV Index, Replay Protection List |
| Mesh Beacons | Let devices announce themselves (unprovisioned) or maintain network health (provisioned) | Beacon Type, Device UUID, OOB Info, IV Index, Network ID |
Keep Learning Bluetooth Mesh
Explore more tutorials at EmbeddedPathashala β free embedded systems content for engineers.
