How Bluetooth Mesh protects every message — from friendship keys to AES-CCM double encryption

Bluetooth Mesh Security: Keys, Encryption & Obfuscation
How Bluetooth Mesh protects every message — from friendship keys to AES-CCM double encryption
⏱️ 12 min read
🔐 Security Focus
🔵 Mesh Profile Spec

What This Post Covers

Bluetooth Mesh is used in smart lighting, industrial automation, and building automation. Every single message in a mesh network travels through multiple nodes — and any node along the route could be compromised. So how does Mesh make sure your “turn off light” command cannot be intercepted, replayed, or faked?

This post breaks down the Bluetooth Mesh security model section by section — in plain language with diagrams. By the end, you’ll understand friendship security material, network/application keys, and the two-layer AES-CCM encryption system that protects every PDU.

🔑 Key Concepts

NetKey AppKey Friendship Security AES-CCM NetMIC / TransMIC Obfuscation Network Nonce IV Index EncryptionKey BeaconKey IdentityKey LPN / Friend Node

1. Friendship Security Material — When Two Nodes Get Close
LPN Friend Node Friendship Keys k2 function

In Bluetooth Mesh, a Low Power Node (LPN) — think a battery-powered sensor — cannot stay awake all the time. It pairs up with a nearby Friend Node (a mains-powered device) that stores messages on its behalf while it sleeps. This pairing is called a friendship.

A friendship is set up via two messages:

  • Friend Request — sent by the LPN, contains LPNCounter
  • Friend Offer — sent by the Friend node, contains FriendCounter

Once the friendship is established, those two nodes need their own private encryption keys so their messages cannot be read by other nodes in the same network (even though they share the same NetKey). These private keys are called friendship security material.

The keys are derived using the k2 function:

Input Value Source
NetKey Shared network key (same for all nodes in the subnet)
LPNAddress Source address in the Friend Request message
FriendAddress Source address in the Friend Offer message
LPNCounter Counter field in the Friend Request
FriendCounter Counter field in the Friend Offer

Output of k2(NetKey, 0x01 || LPNAddress || FriendAddress || LPNCounter || FriendCounter):

NID
EncryptionKey
PrivacyKey

These three output values — NID, EncryptionKey, and PrivacyKey — are used only between the LPN and its Friend node. All other nodes in the same mesh network continue to use the master security materials derived from the same NetKey.

This separation is important: it means other nodes cannot decrypt or tamper with messages exchanged during a friendship session, even though they know the NetKey.

2. What Gets Derived From a Network Key (NetKey)?
Network ID IdentityKey BeaconKey k1 / k3 functions

Every time you provision a new subnet in Bluetooth Mesh, you assign it a NetKey. From this single root key, the spec derives several secondary keys, each serving a different purpose. Here is what gets generated:

Derived Key Formula Purpose Public?
Network ID k3(NetKey) Short identifier for the network, used in beacons ✅ Yes
IdentityKey k1(NetKey, s1(“nkik”), “id128″||0x01) Used to generate node identity beacons (proxy) ❌ No
BeaconKey k1(NetKey, s1(“nkbk”), “id128″||0x01) Authenticates the Secure Network Beacon ❌ No

Each network key generates exactly one Network ID — so devices can identify which network a beacon belongs to without knowing the full NetKey. This is a nice trade-off between privacy and discoverability.

The IdentityKey and BeaconKey both use the k1 function but with different salt inputs ("nkik" vs "nkbk"), ensuring they are completely different despite starting from the same root.

3. Global Key Indexes — How Keys Are Organized
NetKey Index AppKey Index 4096 keys 12-bit index

Bluetooth Mesh supports large deployments — a shopping mall can have hundreds of devices, each needing different access controls. To manage this, the spec defines two global key lists maintained by the Configuration Client:

Key List Index Name Max Keys Index Range Special Note
Network Keys NetKey Index 4096 0x000 – 0xFFF Index 0x000 = Primary NetKey
Application Keys AppKey Index 4096 0x000 – 0xFFF Each AppKey is bound to one NetKey

The key index is a 12-bit value, which is why the maximum is 4096 (212). This allows different application groups — say lighting vs HVAC vs security — to have entirely separate application keys while sharing the same mesh network infrastructure.

4. Message Security — The Double Lock System
AES-CCM NetMIC TransMIC Two-Layer Encryption

This is the heart of Bluetooth Mesh security. Every message is protected by two separate encryption layers — think of it like a letter inside a sealed envelope, and the envelope also inside a locked box. Even if someone breaks the outer lock, the inner seal still protects the content.

🔵 Upper Transport Layer — AppKey / DevKey Encryption
Encrypts the Access Payload → adds TransMIC
⬇ Output becomes Transport PDU ⬇
🌐 Network Layer — EncryptionKey (from NetKey) Encryption
Encrypts DST + Transport PDU → adds NetMIC
⬇ Network Header obfuscated ⬇
📡 Transmitted Message: NID | Obfuscated Header | Encrypted Payload | NetMIC

How MIC sizes work

MIC stands for Message Integrity Check — it tells the receiver whether the message was tampered with. Every Bluetooth Mesh message must have a minimum of 64 bits of total MIC. This 64-bit budget is split between the two layers depending on the message type:

Message Type NetMIC Size TransMIC Size Total Auth Bits
Control Message 64-bit None 64 bits
Access Message (unsegmented) 32-bit 32-bit 64 bits
Access Message (segmented) 32-bit 32 or 64-bit 64–96 bits

Control messages are not authenticated at the upper transport layer because they don’t carry application data — they are internal mesh operations like heartbeats or segment acknowledgments. Access messages carry real payload and get the full double-layer treatment.

5. Upper Transport Layer Encryption — Protecting the Payload
AppKey DevKey Application Nonce Label UUID

The upper transport layer encrypts the access payload — the actual content of your message (e.g., “set light brightness to 80%”). It uses AES-CCM, the same algorithm used in BLE link-layer encryption, just applied at a higher layer.

Which key is used?

  • If the message is application data (e.g., sensor reading to a group of lights): use AppKey with an Application Nonce
  • If the message is a configuration message (e.g., provisioner configuring a node): use DevKey with a Device Nonce

Destination Type Encryption Formula
Virtual Address (Label UUID) AES-CCM(AppKey, AppNonce, Payload, LabelUUID)
Unicast / Group Address AES-CCM(AppKey, AppNonce, Payload)
Unicast (config, DevKey) AES-CCM(DevKey, DevNonce, Payload)

What makes a Nonce unique?

The nonce includes the sequence number and the source address of the sender. This guarantees no two nodes can ever produce the same nonce — even if they send the same payload at the same time. The IV Index extends the nonce space even further, preventing nonce exhaustion over the lifetime of the device.

⚡ Important: The access payload is encrypted once at the source and stays encrypted as it hops through relay nodes. Relay nodes don’t re-encrypt or re-decrypt the application payload — they only handle the outer network layer. This keeps latency low and processing simple on relay nodes.

The output of this layer is:

Upper Transport PDU = EncAccessPayload || TransMIC

6. Network Layer Encryption — The Outer Envelope
EncryptionKey Network Nonce EncDST NetMIC

Once the upper transport layer has encrypted the payload, the network layer takes over and encrypts the destination address (DST) and the entire Transport PDU together. This is the “outer envelope” of the message.

The key used here is not the NetKey directly — it’s the EncryptionKey derived from the NetKey (using the k2 function for master security, or the friendship-specific k2 for LPN/Friend pairs).

EncDST || EncTransportPDU, NetMIC
    = AES-CCM(EncryptionKey, NetworkNonce, DST || TransportPDU)

After network layer encryption:
NID Obfuscated (CTL | TTL | SEQ | SRC) EncDST EncTransportPDU NetMIC
clear obfuscated encrypted MIC

Notice that the destination address itself is encrypted. This means a passive observer watching the air cannot even tell who a message is addressed to — a nice privacy property.

7. Network Header Obfuscation — Hiding Traffic Patterns
CTL TTL SEQ SRC PrivacyKey

After encryption is complete, there is still one problem: the network header fields — CTL (control bit), TTL (time-to-live), SEQ (sequence number), and SRC (source address) — are not encrypted. A passive eavesdropper could watch traffic and say “node at address 0x0005 is sending a lot of messages — must be the main controller.”

To prevent this kind of traffic analysis, these header fields are obfuscated using the PrivacyKey. Obfuscation is not the same as encryption — it’s a one-way XOR trick that makes the values look random without actually making them secret from someone who has the key.

⚠️ Limitation: Obfuscation is designed to stop passive, simple eavesdroppers. A determined attacker who can analyse many packets could still detect patterns and eventually recover the source address or sequence number. Obfuscation is a privacy tool, not a security guarantee.

The obfuscation happens after both the TransMIC and NetMIC have already been calculated — so it does not affect integrity checking. The receiver simply un-obfuscates first, then verifies the MICs.

8. Where This Happens in BlueZ — Code Perspective
BlueZ mesh mesh-crypto.c net.c transport.c

BlueZ has a full Bluetooth Mesh implementation under mesh/. Here are the key source files and what they map to from this post:

BlueZ File Responsibility
mesh/mesh-crypto.c k1, k2, k3 key derivation functions, AES-CCM encrypt/decrypt
mesh/net.c Network layer: EncDST, EncTransportPDU, NetMIC, obfuscation
mesh/transport.c Upper transport layer: EncAccessPayload, TransMIC
mesh/friend.c Friendship establishment, friendship security material derivation
mesh/keyring.c NetKey / AppKey storage and global key index management

Here is how BlueZ calls mesh_crypto_aes_ccm_encrypt from the network layer (simplified from mesh/net.c):

/*
 * Network layer encryption (mesh/net.c - simplified)
 * Encrypts: DST (2 bytes) + TransportPDU
 * Key: EncryptionKey derived from NetKey via k2
 * Nonce: Network nonce (13 bytes), built from SEQ, SRC, IV Index
 */

static bool network_encrypt(struct mesh_net *net,
                             const uint8_t *enc_key,   /* EncryptionKey */
                             const uint8_t *nonce,     /* Network Nonce */
                             uint8_t *pdu,             /* DST || TransportPDU */
                             uint16_t pdu_len,
                             uint8_t *out,             /* EncDST || EncTransportPDU */
                             uint8_t *net_mic,         /* NetMIC output */
                             uint8_t mic_size)         /* 4 or 8 bytes */
{
    return mesh_crypto_aes_ccm_encrypt(enc_key, nonce,
                                        NULL, 0,          /* no AAD */
                                        pdu, pdu_len,
                                        out,
                                        net_mic, mic_size);
}

And here is a simplified view of how the upper transport layer encrypts the access payload using an AppKey:

/*
 * Upper transport layer encryption (mesh/transport.c - simplified)
 * Encrypts the access payload using AppKey
 * Output: EncAccessPayload + TransMIC
 */

static bool upper_transport_encrypt(const uint8_t *app_key,   /* AppKey */
                                     const uint8_t *app_nonce, /* Application Nonce */
                                     const uint8_t *payload,   /* Access Payload */
                                     uint16_t payload_len,
                                     uint8_t *out,
                                     uint8_t *trans_mic,
                                     uint8_t mic_size)         /* 4 bytes (unseg) */
{
    return mesh_crypto_aes_ccm_encrypt(app_key, app_nonce,
                                        NULL, 0,
                                        payload, payload_len,
                                        out,
                                        trans_mic, mic_size);
}

The nonce for each layer is built differently — the network nonce includes NID, CTL, TTL, SEQ, SRC and IV Index; the application nonce includes SZMIC, SEQ, SRC, DST and IV Index. This means even if the same payload is sent twice, the nonce changes because the sequence number increments.

9. Everything in One View — The Security Stack
Layer What Gets Protected Key Used Output
Upper Transport Access Payload (your data) AppKey or DevKey EncAccessPayload + TransMIC
Network DST address + Transport PDU EncryptionKey (from NetKey) EncDST + EncTransportPDU + NetMIC
Obfuscation CTL, TTL, SEQ, SRC (header) PrivacyKey (from NetKey) Obfuscated header bytes
Friendship LPN ↔ Friend PDUs only k2(NetKey, addresses, counters) Separate NID + EncKey + PrivKey

Next Up in the Bluetooth Classic & Mesh Series

We’ll cover Mesh provisioning — how a new device joins the network and gets its NetKey securely.

🏠 Back to EmbeddedPathashala 📘 Full Bluetooth Series

Leave a Reply

Your email address will not be published. Required fields are marked *