BLE RPA Address Handling Issue - Debugging & Resolution
Discover how BLE bonding and RPA work, investigate repeated pairing issues on Android, and identify the root cause across Android and ESP32/NimBLE. Learn how IRK exchange, RPA resolution, and identity-based bond storage ensure reliable reconnections.
BLE bonding and RPA handling in firmware for reliable device identification and connection management.
Published: Sept 28, 2026
BLE RPA & Identity: Behind Reliable Bonding
- The problem
BLE bonding can fail when temporary RPA addresses are not handled correctly.
- The solution
BLE bonding uses identity keys and addresses to recognise devices across connections.
- The result
Handle RPA resolution and store bonds using the peer’s identity address.
- The takeaway
Reliable BLE bonding requires proper identity-key exchange and RPA handling.
TABLE OF CONTENTS
WRITTEN BY
Introduction
We encountered a BLE bonding issue where some Android phones were repeatedly asked to pair with the device after reconnecting, even though the device had already been bonded.
The issue was first observed on newer Android devices, including the Samsung S25 and Google Pixel. After disconnecting and attempting to reconnect, the BLE connection could fail with CONNECTION_TERMINATED_BY_LOCAL_HOST, followed by another pairing prompt from Android. Interestingly, the same behaviour did not occur consistently across all devices or after every disconnect/reconnect cycle.
To identify the cause, we investigated the issue at both the Android application and ESP32 firmware levels. The investigation focused on BLE address types, Resolvable Private Addresses (RPA), identity addresses, stored bonding information, encryption state, and BLE disconnect reasons.
The investigation ultimately identified that the ESP32/NimBLE firmware was associating the Android device’s bonding information with a temporary RPA rather than its permanent BLE identity address. This caused the previously stored bond information to become unusable when the phone changed its RPA.
This article explains how BLE bonding and RPA work, how the issue was traced through Android logs and firmware behaviour, and how the bonding implementation was corrected in the ESP32 (NimBLE) firmware.
Problem Observed
The first connection and bonding always succeeded, the failure appeared only on the second and later reconnections.
Test sequence:
Scan and connect to the device from the phone.
Accept the pairing request and complete bonding.
Disconnect (app close, Bluetooth toggle, or moving out of range).
Reconnect to the same device.
Expected result at step 4: the phone and device re-encrypt the link using the stored keys, with no pairing prompt. Actual result: the link dropped with disconnect reason CONNECTION_TERMINATED_BY_LOCAL_HOST (HCI reason 0x16), and the phone asked to bond again.
| Phone used for testing | Behaviour on reconnection |
|---|---|
| Samsung S25 | Disconnect 0x16, pairing prompt every time |
| Google Pixel | Disconnect 0x16, pairing prompt every time |
| Older Android devices | Not seen immediately after disconnect/reconnect |
Expected result at step 4: the phone and device re-encrypt the link using the stored keys, with no pairing prompt. Actual result: the link dropped with disconnect reason CONNECTION_TERMINATED_BY_LOCAL_HOST (HCI reason 0x16), and the phone asked to bond again.
How BLE Bonding Works
Bonding is pairing plus storage: both sides save the security keys they exchanged, so the next connection is encrypted straight away without asking the user again.
Pairing creates the keys for one connection. Bonding saves those keys in non-volatile memory on both sides, so they survive a disconnect or power cycle. On the ESP32, NimBLE saves bonds in NVS, on the phone, Android saves them in its Bluetooth settings (the “Paired devices” list).
Pairing runs in three phases:
Feature exchange:
both sides share I/O capabilities (display, keyboard, none), whether they want bonding, and whether they support LE Secure Connections. This decides the method: Just Works, Passkey, or Numeric Comparison.
Key generation:
with LE Secure Connections, both sides run an ECDH key exchange and derive the LTK (Long Term Key). The user confirms with a passkey or comparison if the method needs it.
Key distribution:
over the now-encrypted link, each side sends its extra keys: the IRK (Identity Resolving Key) with its Identity Address, and optionally the CSRK for data signing.
| Key | Purpose | Used on reconnection for |
|---|---|---|
| LTK | Encrypts the link | Starting encryption without pairing again |
| IRK | Generates and resolves private addresses | Recognising the peer even when its address changes |
| Identity Address | The peer's permanent public or static address | The index under which the bond is stored |
| CSRK | Signs data on an unencrypted link | Rarely used |
The diagram shows the full lifecycle: keys are created and exchanged once, saved on both sides, and reused on the next connection. The critical step on reconnection is that the device must find the right bond for the incoming phone, which is where addresses and the IRK come in.
BLE Privacy and the Resolvable Private Address (RPA)
Modern phones never show their real Bluetooth address over the air, they use a Resolvable Private Address that changes regularly, and only bonded devices holding the phone’s IRK can map it back to the real identity.
Why Android and iOS use RPA
A fixed Bluetooth address is a tracking beacon. Anyone with a scanner in a shop, station or office could log the same address and follow a person over days. BLE Privacy (Bluetooth 4.2 onward) solves this by replacing the fixed address with a random-looking one that rotates, typically about every 15 minutes and on new connections. iOS has done this for years, and recent Android versions (as on the Samsung S25 and Pixel) use it by default for connections too. Older phones or older Android builds often kept one address for longer, which is why they did not show the fault.
BLE address types
| Type | Changes ? | Can be traced to the device ? |
|---|---|---|
| Public | Never (IEEE-assigned) | Yes, by anyone |
| Random static | Only at power cycle | Yes, by anyone |
| Resolvable private (RPA) | Every ~15 min / new connection | Only by holders of the IRK |
| Non-resolvable private | Frequently | No |
How an RPA is built
An RPA is 48 bits: a 24-bit random part (prand, top two bits set to 01) and a 24-bit hash. The phone computes the hash from its secret IRK:
How the device resolves the private address
When a connection arrives from an RPA, the device takes the prand part, runs the same AES function with every IRK it has stored, and compares the result with the hash part. A match reveals which bonded phone it is and its permanent identity address. This check can run in the host (NimBLE) or in the controller’s resolving list (LL Privacy), which does it in hardware.
The flow shows why the IRK matters: without it, or if the bond is filed under the RPA, every new RPA looks like a stranger and the device falls to the “pairing required” branch.
Debugging Approach we took to understand the root cause.
The CONNECTION_TERMINATED_BY_LOCAL_HOST reason was the first clue, we then compared what the phone thought it was bonded to with what the ESP32 had actually saved in NVS.
Mobile application side
Captured connection logs in the app and in nRF Connect across the pair → disconnect → reconnect loop.
Recorded the disconnect reason on each reconnect: status 22 (0x16), local host terminated, every time on the S25 and Pixel.
Checked the phone's bond state: after the first pairing the phone listed the device as bonded, so the phone side still held its keys.
Note that the phone's address seen by the device changed between connections, confirming the phone was using an RPA.
Firmware side (ESP32, NimBLE)
Logged every GAP event: connect, encryption change, disconnect, and repeat-pairing.
On connect, printed both addresses NimBLE reports for the peer: the over-the-air address (peer_ota_addr) and the identity address (peer_id_addr).
Dumped the bond store from NVS after each pairing to see which address each bond was saved under.
Logged the result of the encryption attempt on reconnect.
What the logs showed
| Check | Expected | Observed |
|---|---|---|
| Peer address on connect | RPA over the air, resolved to identity | RPA over the air, not resolved |
| Address used as bond key in NVS | Phone's identity (public) address | Phone's private address (RPA) |
| Peer address on reconnect | New RPA, resolved to same identity | New RPA, no matching bond |
| Encryption on reconnect | Success with stored LTK | Failed: key missing |
| Final result | Encrypted link | Phone drops link (0x16), asks to pair again |
The sequence shows the failure chain: the bond exists on both sides, but the ESP32 cannot find it because it is filed under an address the phone no longer uses.
Root Cause
RPA support was not implemented properly in the firmware: the identity keys were not exchanged and resolved, so the bond was saved under the phone’s temporary RPA and could never be found again once the phone rotated its address.
The chain of failure:
The firmware's Security Manager did not request or distribute the identity key (IRK + identity address) during pairing, so the ESP32 never held the phone's IRK.
Without the IRK, NimBLE could not resolve the phone's RPA to its identity address, and the peer's "identity" defaulted to the over-the-air RPA.
The bond (LTK and related keys) was written to NVS under that RPA.
The phone rotated its RPA. On reconnection the ESP32 looked up the new RPA, found no bond, and replied that the key was missing.
The phone's host terminated the link (0x16) and started pairing again. Each new pairing added another bond under another RPA, filling the bond store over time.
Older phones that kept the same address for longer did not rotate between the disconnect and the reconnect, so the stale RPA still matched and the bug stayed hidden. The newest Android devices rotate more aggressively, which exposed it at once.
Resolution
We enabled identity-key exchange in the NimBLE Security Manager, persisted bonds in NVS keyed by the peer’s identity address, and made every bond lookup use the resolved identity instead of the over-the-air RPA.
The fix had four parts:
Bond persistence
bonds are saved in NVS so they survive disconnects and device reboots.
Identity-key exchange
during pairing, both sides now distribute the identity key, so the phone sends its IRK and identity address to the ESP32.
Identity-based lookup
the bond is stored and searched by the phone's resolved identity address, never by the over-the-air RPA. If the phone deletes its bond and pairs again, the old bond for that identity is removed first.
Resolving list
the received IRK is added to the resolving list, and stored IRKs are reloaded from NVS at boot, so the phone is recognised even after a power cycle.
KEY_DIST_ID
IRK + identity addr
key = identity addr
resolving list
new RPA
identity addr
encrypt with LTK
The fixed flow: the IRK travels once at pairing, and from then on any RPA the phone uses resolves to the same identity and the same stored bond.
Verification
After the fix, the phones reconnected on the stored LTK with no pairing prompt and no 0x16 disconnect.
We repeated the original test loop and added the cases that force an address change:
| Test case | Result after fix |
|---|---|
| Pair, disconnect, reconnect immediately | Encrypted on stored LTK, no prompt |
| Reconnect after the phone rotates its RPA (wait > 15 min)l | RPA resolved to identity, no prompt |
| Toggle phone Bluetooth off/on, reconnect | No prompt |
| Power-cycle the ESP32, reconnect | Bond loaded from NVS, no prompt |
| Remove bond on phone only, reconnect | Repeat-pairing handled: old bond deleted, fresh pairing once |
In the firmware logs, peer_ota_addr now changed between connections while peer_id_addr stayed the phone’s identity address, and the NVS dump showed one bond per phone under that identity address.
Open item: confirm the retest on each phone model in the device list, including older devices, and record the results in this table.
Conclusion and Lessons Learned
The repeated re-bonding was a firmware privacy bug, not a phone bug: bonds must be stored and looked up by the peer’s identity address, which needs the IRK exchanged during pairing.
Treat every phone as a privacy device
Modern Android and iOS rotate RPAs, firmware must never use the over-the-air address as a bond key.
Always exchange identity keys
Both sides must share their IRK and identity address during pairing, or RPA resolution cannot work.
Log both addresses
Printing the over-the-air address and the resolved identity address on every connection makes this class of bug visible in minutes.
Read the disconnect reason
0x16 (local host terminated) after a reconnect usually means a key mismatch, not an RF problem.
Test with address rotation
Include a wait longer than the RPA timeout and a device power cycle in every bonding test plan, and test on the newest phones first.
BLE Communication, Bonding & RPA Handling
Understand how BLE bonding, identity addresses, and RPA resolution work together to maintain secure and reliable device connections across reconnections.
- Drafted using AI