{"slug": "ble-rpa-address-handling-issue-debugging-resolution", "title": "BLE RPA Address Handling Issue - Debugging & Resolution", "summary": "An engineering team traced a BLE bonding failure in which newer Android phones, including the Samsung S25 and Google Pixel, were repeatedly prompted to pair after reconnecting, with connections dropping with HCI reason 0x16 (CONNECTION_TERMINATED_BY_LOCAL_HOST). The root cause was in the ESP32/NimBLE firmware, which stored the phone's bond against a temporary Resolvable Private Address instead of its permanent identity address, making the stored bond unusable once the phone rotated its RPA. The team corrected the bonding implementation to resolve RPAs via the exchanged IRK and index bonds by identity address, restoring encrypted reconnections without re-pairing.", "body_md": "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.\n\n*This article was written with the assistance of AI and reviewed by our engineering team.*\n\n*BLE bonding and RPA handling in firmware for reliable device identification and connection management.*\n\nWe 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.\n\nThe 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.\n\nTo 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.\n\nThe 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.\n\nThis 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.\n\nThe first connection and bonding always succeeded, the failure appeared only on the second and later reconnections.\n\n**Test sequence:**\n\nExpected 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.\n\n| Phone used for testing | Behaviour on reconnection | \n|---|---|\n| Samsung S25 | Disconnect 0x16, pairing prompt every time | \n| Google Pixel | Disconnect 0x16, pairing prompt every time | \n| Older Android devices | Not seen immediately after disconnect/reconnect | \n\nBonding 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.\n\n**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).\n\nPairing runs in three phases:\n\n| Key | Purpose | Used on reconnection for | \n|---|---|---|\n| LTK | Encrypts the link | Starting encryption without pairing again | \n| IRK | Generates and resolves private addresses | Recognising the peer even when its address changes | \n| Identity Address | The peer's permanent public or static address | The index under which the bond is stored | \n| CSRK | Signs data on an unencrypted link | Rarely used | \n\nThe lifecycle between the Android phone and the ESP32 device:\n\nThe steps show 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.\n\nModern 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.\n\nA 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.\n\n| Type | Changes? | Can be traced to the device? | \n|---|---|---|\n| Public | Never (IEEE-assigned) | Yes, by anyone | \n| Random static | Only at power cycle | Yes, by anyone | \n| Resolvable private (RPA) | Every ~15 min / new connection | Only by holders of the IRK | \n| Non-resolvable private | Frequently | No | \n\nAn 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:\n\n```\ninput  = 0^104 || prand            (104 zero bits concatenated with the 24-bit prand)\noutput = AES-128(IRK, input)       (encrypt using the Identity Resolving Key)\nhash   = output mod 2^24           (extract 24 bits)\nRPA    = prand and hash concatenated (48 bits total)\n```\n\nWhen 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.\n\nThe 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.\n\nThe `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.\n\n`peer_ota_addr`) and the identity address (` peer_id_addr`).\n| Check | Expected | Observed | \n|---|---|---|\n| Peer address on connect | RPA over the air, resolved to identity | RPA over the air, not resolved | \n| Address used as bond key in NVS | Phone's identity (public) address | Phone's private address (RPA) | \n| Peer address on reconnect | New RPA, resolved to same identity | New RPA, no matching bond | \n| Encryption on reconnect | Success with stored LTK | Failed: key missing | \n| Final result | Encrypted link | Phone drops link (0x16), asks to pair again | \n\nWhat happened between the Android phone and the faulty ESP32 firmware:\n\nThe 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.\n\nRPA 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.\n\nThe chain of failure:\n\nOlder 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.\n\nWe 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.\n\nThe fix had four parts:\n\n```\nPairing with KEY_DIST_ID\n  → Receive phone's IRK + identity addr\n  → Save LTK + IRK in NVS (key = identity addr)\n  → Add IRK to resolving list\n  → Reconnect from new RPA\n  → RPA resolved to identity addr\n  → Bond found, encrypt with LTK\n```\n\nThe 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.\n\nAfter the fix, the phones reconnected on the stored LTK with no pairing prompt and no 0x16 disconnect.\n\nWe repeated the original test loop and added the cases that force an address change:\n\n| Test case | Result after fix | \n|---|---|\n| Pair, disconnect, reconnect immediately | Encrypted on stored LTK, no prompt | \n| Reconnect after the phone rotates its RPA (wait > 15 min) | RPA resolved to identity, no prompt | \n| Toggle phone Bluetooth off/on, reconnect | No prompt | \n| Power-cycle the ESP32, reconnect | Bond loaded from NVS, no prompt | \n| Remove bond on phone only, reconnect | Repeat-pairing handled: old bond deleted, fresh pairing once | \n\nIn 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.\n\nThe 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.\n\nModern Android and iOS rotate RPAs, firmware must never use the over-the-air address as a bond key.\n\nBoth sides must share their IRK and identity address during pairing, or RPA resolution cannot work.\n\nPrinting the over-the-air address and the resolved identity address on every connection makes this class of bug visible in minutes.\n\n0x16 (local host terminated) after a reconnect usually means a key mismatch, not an RF problem.\n\nInclude a wait longer than the RPA timeout and a device power cycle in every bonding test plan, and test on the newest phones first.\n\n*Written by Jagan S., System Software Engineer, Krishworks Technology Innovations. Originally published at [krishworkstech.com](https://krishworkstech.com/ble-rpa-address-handling-issue-debugging-resolution/).*", "url": "https://wpnews.pro/news/ble-rpa-address-handling-issue-debugging-resolution", "canonical_source": "https://dev.to/krishworks_technologies_3/ble-rpa-address-handling-issue-debugging-resolution-137l", "published_at": "2026-10-30 06:05:46+00:00", "updated_at": "2026-10-30 06:19:33.836320+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["ESP32", "NimBLE", "Android", "Samsung", "Google Pixel"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/ble-rpa-address-handling-issue-debugging-resolution", "markdown": "https://wpnews.pro/news/ble-rpa-address-handling-issue-debugging-resolution.md", "text": "https://wpnews.pro/news/ble-rpa-address-handling-issue-debugging-resolution.txt", "jsonld": "https://wpnews.pro/news/ble-rpa-address-handling-issue-debugging-resolution.jsonld"}}