Wiki / Protocols

BLE Link Layer Encryption

How a BLE connection actually protects its payloads: AES-CCM under a session key derived at LL_ENC_REQ time, with a nonce built from a packet counter that never travels on the air.

The AES-CCM nonce, 13 bytes
packet counter (39 bits) implicit
direction (1 bit) 1 = central
IV (8 bytes) IVm || IVs
The IV halves are exchanged in the clear in LL_ENC_REQ and LL_ENC_RSP, so a sniffer has them. The counter is not transmitted at all: each end simply counts the encrypted packets it has sent in that direction. Nothing on the air states it, which is exactly why a device that loses count fails silently.

Link layer encryption is the layer that turns a BLE connection's payloads into ciphertext. It sits below L2CAP, so everything above it, ATT included, is protected without knowing anything about it, and a sniffer that catches an encrypted connection sees correct link-layer headers wrapped around bytes it cannot read.

What it is

The algorithm is AES-CCM: AES-128 in counter mode for confidentiality, with a CBC-MAC over the same data for integrity, producing a 4-byte MIC appended to every encrypted PDU. The parameters are fixed by the specification, so there is nothing to negotiate and nothing to downgrade at this layer.

Three things go into every encryption:

Input Size Where it comes from
Session key 16 bytes e(LTK, SKDm \|\| SKDs), computed independently by both ends
Nonce 13 bytes packet counter and direction bit (5 bytes), then IV (8 bytes)
Additional data 1 byte the LL header octet with the NESN, SN and MD bits masked out

The exchange that sets it up is short and entirely in the clear:

central   -> LL_ENC_REQ    Rand (8), EDIV (2), SKDm (8), IVm (4)
peripheral -> LL_ENC_RSP    SKDs (8), IVs (4)
peripheral -> LL_START_ENC_REQ
central   -> LL_START_ENC_RSP        encrypted, central counter 0
peripheral -> LL_START_ENC_RSP        encrypted, peripheral counter 0

Rand and EDIV identify which stored LTK to use, so on a reconnect between bonded devices they are non-zero and no pairing happens at all. On a first-time legacy pairing they are zero and the STK from the pairing plays the role of the LTK.

The packet counter deserves attention because it is the one input a sniffer never receives. Each direction has its own 39-bit counter, starting at zero when encryption starts and incrementing once per encrypted PDU sent in that direction. It is never transmitted. Both ends track it by counting, and the receiver rebuilds the nonce from its own count.

Why it matters

Two consequences fall out, and they run in opposite directions.

First, encryption at this layer is genuinely strong. Given only a capture, you do not read an encrypted connection without the LTK, and the LTK is not on the air under LE Secure Connections. This is why an over-the-air sniff of a modern device is often a dead end while an HCI snoop taken on the phone is not.

Second, the design has one brittle assumption: a nonce is never reused under one session key. Nothing in the protocol enforces it, nothing on the air records the counter, and the receiver has no way to notice a sender that has lost its place. A peripheral whose counter is reset by a wake path, a watchdog or a state-machine bug re-encrypts under nonces it has already spent, and CCM's confidentiality collapses into a two-time pad for those packets. The MIC does not save you: it authenticates each packet independently and is perfectly happy.

How to work with it

Wireshark dissects the setup, then tells you honestly that it cannot go further:

# The whole encryption setup, in clear
tshark -r cap.pcap -Y 'btle.control_opcode in {3 4 5 6}'

# Encrypted data PDUs: correct headers, unreadable payload
tshark -r cap.pcap -Y 'btle.data_header.llid == 2' -T fields -e frame.number -e btle.length

If you hold the LTK (from an HCI snoop of the bonding, from the phone's key store, or from a legacy pairing you cracked), crackle -i cap.pcap -l <ltk> -o dec.pcap will do the derivation and decrypt the file for you. Wireshark can also be handed the key in its Bluetooth protocol preferences.

Doing it by hand is not hard, and it is worth doing once:

session_key = e(ltk, skds + skdm)          # spec order, most significant first
nonce = counter.to_bytes(5, "little") | direction_bit_in_bit_39
nonce += ivm + ivs                          # 8 bytes, as they appeared on the wire

Pitfalls

  • Byte order is the whole game. SKD, IV, Rand and the confirm values are all little-endian on the wire and most significant octet first in the specification's formulas. Every failed reimplementation of this is a reversed buffer.
  • The counter is per direction, not per connection. Two independent counters, one for each end, both starting at zero. Mixing them decrypts nothing.
  • Empty PDUs are not encrypted. A zero-length data PDU carries no payload and therefore no MIC. Counting it as an encrypted packet shifts the counter and everything after it fails.
  • The MIC proves nothing about freshness. It is computed per packet under that packet's nonce. A replayed packet at the right counter value verifies; a genuine packet at the wrong counter does not.
  • LE Secure Connections does not change this layer. LESC changes how the LTK is agreed, not how the link is encrypted. The CCM construction, the nonce and the counters are identical.

Further reading