krishworkstech.com

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 Address Handling

BLE RPA & Identity: Behind Reliable Bonding

BLE bonding can fail when temporary RPA addresses are not handled correctly.

BLE bonding uses identity keys and addresses to recognise devices across connections.

Handle RPA resolution and store bonds using the peer’s identity address.

Reliable BLE bonding requires proper identity-key exchange and RPA handling.

TABLE OF CONTENTS

WRITTEN BY

Siddhant D. Kadam
Jagan S.
System Software Engineer
Krishworks Technology Innovations
01

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.

BLE RPA Address Handling Introduction
02

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.

03

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
Android phone ESP32 device Connect Pairing Request (IO caps, bonding) Pairing Response Phase 2: ECDH + passkey, both derive the LTK Encrypt link with LTK Distribute IRK + Identity Address Distribute IRK + Identity Address Save bond in NVS key = peer identity address Save bond in Android paired-devices store Disconnect Reconnect (new RPA) Resolve RPA with IRK find bond, load LTK Encrypt with stored LTK Encrypted, no pairing prompt Android phone ESP32 device

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.

04

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:

prand (24 bits) 0^104 (104 zero bits) concatenate concatenate 0^104 || prand IRK (Identity Resolving Key) uses encrypt AES-128 output result mod 2^24 extract 24 hash (24 bits) concatenate concatenate RPA (48 bits total)

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.

Incoming connection from RPA Split RPA into prand + hash For each stored IRK: compute ah(IRK, prand) Computed hash = received hash? Yes No, all IRKs tried Peer resolved to Identity Address Unknown peer Look up bond by identity address No bond found: pairing required Load LTK and encrypt link

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.

05

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
Android phone ESP32 (faulty) Reconnect from new RPA-2 Search NVS for RPA-2 bond saved under RPA-1 Start encryption (LTK) Key missing Phone treats bond as broken Disconnect 0x16 local host terminated New pairing request Android phone ESP32 (faulty)

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.

06

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.

07

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.

Pairing with KEY_DIST_ID Receive phone's IRK + identity addr Save LTK + IRK in NVS key = identity addr Add IRK to resolving list Reconnect from new RPA RPA resolved to identity addr Bond found, encrypt with LTK
Pairing with
KEY_DIST_ID
Receive phone's
IRK + identity addr
Save LTK + IRK in NVS
key = identity addr
Add IRK to
resolving list
Reconnect from
new RPA
RPA resolved to
identity addr
Bond found,
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.

08

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.

09

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.

Siddhant D. Kadam
Jagan S.
System Software Engineer
Krishworks Technology Innovations
SHARE AT
grok-1
chatgpt-6
gemini-icon-logo
perplexity-color-1-

Explore more Articles

Scroll to Top
  • Schematic design
  • PCB and schematic source files
  • Assembling drawing files
  • Providing prototype/sample and production PCB service
  • Testing and validation of designed hardware
  • HIPAA
  • Azure Key
  • Management
  • ES, Checksum,
  • MD5sum
  • AWS
  • Azure
  • GCP
  • DigitalOcean
  • Kotlin
  • Python
  • Tensorflow
  • Computer Vision
  • ECG
  • SPO2
  • Heart Rate
  • Glucometer
  • Blood Pressure
  • UX UI Process
  • Figma and FigJam
  • Adobe Suite
  • Selenium Java
  • Postman
  • Swagger
  • Jmeter
  • SQL
  • Java Scripter
  • Test ng
  • Extents Reports
  • Flutter
  • Java
  • Kotlin
  • Swift
  • Dart
  • React JS
  • Python
  • NodeJS
  • Django
  • HTML, CSS, JS
RDBMS
  • PostgreSQL
  • Oracle
  • MySQL
  • MariaDB
No SQL Based
  • MongoDB
  • GCP
  • FirestoreDB
  • DynamoDB
  • Azure
  • CosmosDB
  • AWS