From 086a8d91b9f48709e8aae238860ed1dd7cb088da Mon Sep 17 00:00:00 2001 From: h2zero Date: Tue, 17 Mar 2026 15:03:06 -0600 Subject: [PATCH 1/3] Add bond conversion API between v1 and v2 bond data preserving old bonds --- .../NimBLE_Bond_Migration.ino | 68 +++++ src/NimBLEBondMigration.h | 273 ++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 examples/NimBLE_Bond_Migration/NimBLE_Bond_Migration.ino create mode 100644 src/NimBLEBondMigration.h diff --git a/examples/NimBLE_Bond_Migration/NimBLE_Bond_Migration.ino b/examples/NimBLE_Bond_Migration/NimBLE_Bond_Migration.ino new file mode 100644 index 000000000..aa391c7c0 --- /dev/null +++ b/examples/NimBLE_Bond_Migration/NimBLE_Bond_Migration.ino @@ -0,0 +1,68 @@ +/** + * NimBLE_Bond_Migration Demo: + * + * This example demonstrates running one-time bond-store migration before + * NimBLE initialization. Set NIMBLE_BOND_MIGRATION_MODE to select direction: + * 0 = off, 1 = migrate to current, 2 = migrate to v1. + */ + +#include +#include +#include + +namespace NimBLE_Bond_Migration = NimBLEBondMigration; + +#ifndef NIMBLE_BOND_MIGRATION_MODE +#define NIMBLE_BOND_MIGRATION_MODE 0 +#endif + +#if (NIMBLE_BOND_MIGRATION_MODE < 0) || (NIMBLE_BOND_MIGRATION_MODE > 2) +#error "NIMBLE_BOND_MIGRATION_MODE must be 0 (off), 1 (to current), or 2 (to v1)" +#endif + +void setup() { + Serial.begin(115200); + Serial.println("Starting NimBLE_Bond_Migration Demo"); + +#if NIMBLE_BOND_MIGRATION_MODE == 1 + { + esp_err_t rc = NimBLE_Bond_Migration::migrateBondStoreToCurrent(); + Serial.printf("NimBLE_Bond_Migration to current rc=%d\n", static_cast(rc)); + } +#elif NIMBLE_BOND_MIGRATION_MODE == 2 + { + esp_err_t rc = NimBLE_Bond_Migration::migrateBondStoreToV1(); + Serial.printf("NimBLE_Bond_Migration to v1 rc=%d\n", static_cast(rc)); + if (rc == ESP_OK) { + Serial.println("NimBLE_Bond_Migration completed to v1, upload v1 firmware to continue"); + while (true) { + delay(1000); + } + } else { + Serial.println("NimBLE_Bond_Migration to v1 failed"); + } + } +#endif + + NimBLEDevice::init("NimBLE"); + NimBLEDevice::setPower(3); /** +3db */ + + NimBLEDevice::setSecurityAuth(true, true, false); /** bonding, MITM, don't need BLE secure connections as we are using passkey pairing */ + NimBLEDevice::setSecurityPasskey(123456); + NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); /** Display only passkey */ + NimBLEServer* pServer = NimBLEDevice::createServer(); + NimBLEService* pService = pServer->createService("ABCD"); + NimBLECharacteristic* pNonSecureCharacteristic = pService->createCharacteristic("1234", NIMBLE_PROPERTY::READ); + NimBLECharacteristic* pSecureCharacteristic = + pService->createCharacteristic("1235", + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::READ_AUTHEN); + + pNonSecureCharacteristic->setValue("Hello Non Secure BLE"); + pSecureCharacteristic->setValue("Hello Secure BLE"); + + NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->addServiceUUID("ABCD"); + pAdvertising->start(); +} + +void loop() {} diff --git a/src/NimBLEBondMigration.h b/src/NimBLEBondMigration.h new file mode 100644 index 000000000..fbd0ab0b8 --- /dev/null +++ b/src/NimBLEBondMigration.h @@ -0,0 +1,273 @@ +#pragma once + +#if !defined(ESP_PLATFORM) +# error "NimBLEBondMigration.h currently requires ESP_PLATFORM (NVS backend)." +#endif + +#include +#include +#include + +#include "esp_err.h" +#include "esp_log.h" +#include "nvs.h" +#include "syscfg/syscfg.h" + +namespace NimBLEBondMigration { + +namespace detail { + +static constexpr const char* kLogTag = "NimBLEBondMigration"; +static constexpr const char* kBondNamespace = "nimble_bond"; +static constexpr const char* kOurSecPrefix = "our_sec"; +static constexpr const char* kPeerSecPrefix = "peer_sec"; +static constexpr size_t kNvsKeyMaxLen = 16; + +typedef struct { + uint8_t type; + uint8_t val[6]; +} ble_addr_t; + +struct BleStoreValueSecV1 { + ble_addr_t peer_addr; + + uint8_t key_size; + uint16_t ediv; + uint64_t rand_num; + uint8_t ltk[16]; + uint8_t ltk_present : 1; + + uint8_t irk[16]; + uint8_t irk_present : 1; + + uint8_t csrk[16]; + uint8_t csrk_present : 1; + + unsigned authenticated : 1; + uint8_t sc : 1; +}; + +struct BleStoreValueSecCurrent { + ble_addr_t peer_addr; + uint16_t bond_count; + + uint8_t key_size; + uint16_t ediv; + uint64_t rand_num; + uint8_t ltk[16]; + uint8_t ltk_present : 1; + + uint8_t irk[16]; + uint8_t irk_present : 1; + + uint8_t csrk[16]; + uint8_t csrk_present : 1; + uint32_t sign_counter; + + unsigned authenticated : 1; + uint8_t sc : 1; +}; + +struct MigrationStats { + uint16_t scanned; + uint16_t converted; + uint16_t alreadyTarget; + uint16_t skippedUnknownSize; +}; + +inline bool makeBondKey(char* out, size_t outLen, const char* prefix, uint16_t index) { + const int written = snprintf(out, outLen, "%s_%u", prefix, index); + return written > 0 && static_cast(written) < outLen; +} + +inline esp_err_t migrateEntryToCurrent(nvs_handle_t nvsHandle, + const char* key, + uint16_t index, + MigrationStats* stats) { + size_t blobSize = 0; + esp_err_t err = nvs_get_blob(nvsHandle, key, nullptr, &blobSize); + if (err == ESP_ERR_NVS_NOT_FOUND) { + return ESP_OK; + } + if (err != ESP_OK) { + return err; + } + + stats->scanned++; + + if (blobSize == sizeof(BleStoreValueSecCurrent)) { + stats->alreadyTarget++; + return ESP_OK; + } + + if (blobSize != sizeof(BleStoreValueSecV1)) { + stats->skippedUnknownSize++; + ESP_LOGW(kLogTag, + "Skipping key=%s due to unexpected size=%u", + key, + static_cast(blobSize)); + return ESP_OK; + } + + BleStoreValueSecV1 oldValue{}; + size_t readSize = sizeof(oldValue); + err = nvs_get_blob(nvsHandle, key, &oldValue, &readSize); + if (err != ESP_OK) { + return err; + } + + BleStoreValueSecCurrent newValue{}; + newValue.peer_addr = oldValue.peer_addr; + newValue.bond_count = index; + newValue.key_size = oldValue.key_size; + newValue.ediv = oldValue.ediv; + newValue.rand_num = oldValue.rand_num; + memcpy(newValue.ltk, oldValue.ltk, sizeof(newValue.ltk)); + newValue.ltk_present = oldValue.ltk_present; + memcpy(newValue.irk, oldValue.irk, sizeof(newValue.irk)); + newValue.irk_present = oldValue.irk_present; + memcpy(newValue.csrk, oldValue.csrk, sizeof(newValue.csrk)); + newValue.csrk_present = oldValue.csrk_present; + newValue.sign_counter = 0; + newValue.authenticated = oldValue.authenticated; + newValue.sc = oldValue.sc; + + err = nvs_set_blob(nvsHandle, key, &newValue, sizeof(newValue)); + if (err != ESP_OK) { + return err; + } + + stats->converted++; + return ESP_OK; +} + +inline esp_err_t migrateEntryToV1(nvs_handle_t nvsHandle, + const char* key, + MigrationStats* stats) { + size_t blobSize = 0; + esp_err_t err = nvs_get_blob(nvsHandle, key, nullptr, &blobSize); + if (err == ESP_ERR_NVS_NOT_FOUND) { + return ESP_OK; + } + if (err != ESP_OK) { + return err; + } + + stats->scanned++; + + if (blobSize == sizeof(BleStoreValueSecV1)) { + stats->alreadyTarget++; + return ESP_OK; + } + + if (blobSize != sizeof(BleStoreValueSecCurrent)) { + stats->skippedUnknownSize++; + ESP_LOGW(kLogTag, + "Skipping key=%s due to unexpected size=%u", + key, + static_cast(blobSize)); + return ESP_OK; + } + + BleStoreValueSecCurrent curValue{}; + size_t readSize = sizeof(curValue); + err = nvs_get_blob(nvsHandle, key, &curValue, &readSize); + if (err != ESP_OK) { + return err; + } + + BleStoreValueSecV1 oldValue{}; + oldValue.peer_addr = curValue.peer_addr; + oldValue.key_size = curValue.key_size; + oldValue.ediv = curValue.ediv; + oldValue.rand_num = curValue.rand_num; + memcpy(oldValue.ltk, curValue.ltk, sizeof(oldValue.ltk)); + oldValue.ltk_present = curValue.ltk_present; + memcpy(oldValue.irk, curValue.irk, sizeof(oldValue.irk)); + oldValue.irk_present = curValue.irk_present; + memcpy(oldValue.csrk, curValue.csrk, sizeof(oldValue.csrk)); + oldValue.csrk_present = curValue.csrk_present; + oldValue.authenticated = curValue.authenticated; + oldValue.sc = curValue.sc; + + err = nvs_set_blob(nvsHandle, key, &oldValue, sizeof(oldValue)); + if (err != ESP_OK) { + return err; + } + + stats->converted++; + return ESP_OK; +} + +inline esp_err_t migrateBondStore(bool toCurrent, uint16_t maxEntries) { + nvs_handle_t nvsHandle; + esp_err_t err = nvs_open(kBondNamespace, NVS_READWRITE, &nvsHandle); + if (err != ESP_OK) { + ESP_LOGE(kLogTag, + "Failed to open NVS namespace '%s', err=%d", + kBondNamespace, + static_cast(err)); + return err; + } + + MigrationStats stats{}; + char key[kNvsKeyMaxLen]{}; + + for (uint16_t i = 1; i <= maxEntries; ++i) { + if (!makeBondKey(key, sizeof(key), kOurSecPrefix, i)) { + nvs_close(nvsHandle); + return ESP_FAIL; + } + + err = toCurrent ? migrateEntryToCurrent(nvsHandle, key, i, &stats) + : migrateEntryToV1(nvsHandle, key, &stats); + if (err != ESP_OK) { + nvs_close(nvsHandle); + return err; + } + + if (!makeBondKey(key, sizeof(key), kPeerSecPrefix, i)) { + nvs_close(nvsHandle); + return ESP_FAIL; + } + + err = toCurrent ? migrateEntryToCurrent(nvsHandle, key, i, &stats) + : migrateEntryToV1(nvsHandle, key, &stats); + if (err != ESP_OK) { + nvs_close(nvsHandle); + return err; + } + } + + if (stats.converted > 0) { + err = nvs_commit(nvsHandle); + if (err != ESP_OK) { + nvs_close(nvsHandle); + return err; + } + } + + nvs_close(nvsHandle); + + ESP_LOGI(kLogTag, + "Bond migration %s: scanned=%u converted=%u already=%u skipped=%u", + toCurrent ? "to-current" : "to-v1", + stats.scanned, + stats.converted, + stats.alreadyTarget, + stats.skippedUnknownSize); + + return ESP_OK; +} + +} // namespace detail + +inline esp_err_t migrateBondStoreToCurrent(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { + return detail::migrateBondStore(true, maxEntries); +} + +inline esp_err_t migrateBondStoreToV1(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { + return detail::migrateBondStore(false, maxEntries); +} + +} // namespace NimBLEBondMigration From 1a97425311fecb47095780e08c2264c5f51ee749 Mon Sep 17 00:00:00 2001 From: h2zero Date: Wed, 18 Mar 2026 14:59:40 -0600 Subject: [PATCH 2/3] Add data dump --- src/NimBLEBondMigration.h | 207 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/src/NimBLEBondMigration.h b/src/NimBLEBondMigration.h index fbd0ab0b8..26652862f 100644 --- a/src/NimBLEBondMigration.h +++ b/src/NimBLEBondMigration.h @@ -7,6 +7,9 @@ #include #include #include +#include +#include +#include #include "esp_err.h" #include "esp_log.h" @@ -75,6 +78,107 @@ struct MigrationStats { uint16_t skippedUnknownSize; }; +inline void appendLine(std::string& out, const char* fmt, ...) { + char buf[256]; + va_list args; + va_start(args, fmt); + const int written = vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + if (written > 0) { + out.append(buf, strlen(buf)); + } +} + +inline void appendHex(std::string& out, const uint8_t* data, size_t len) { + static constexpr char hex[] = "0123456789ABCDEF"; + out.reserve(out.size() + (len * 2)); + for (size_t i = 0; i < len; ++i) { + const uint8_t value = data[i]; + out.push_back(hex[(value >> 4) & 0x0F]); + out.push_back(hex[value & 0x0F]); + } +} + +inline void appendAddr(std::string& out, const ble_addr_t& addr) { + appendLine(out, + "%02X:%02X:%02X:%02X:%02X:%02X(type=%u)", + addr.val[5], + addr.val[4], + addr.val[3], + addr.val[2], + addr.val[1], + addr.val[0], + addr.type); +} + +inline void appendV1Record(std::string& out, const BleStoreValueSecV1& sec) { + out += " peer_addr="; + appendAddr(out, sec.peer_addr); + out += "\n"; + appendLine(out, + " key_size=%u ediv=%u rand_num=%llu auth=%u sc=%u\n", + sec.key_size, + sec.ediv, + static_cast(sec.rand_num), + sec.authenticated, + sec.sc); + appendLine(out, + " ltk_present=%u irk_present=%u csrk_present=%u\n", + sec.ltk_present, + sec.irk_present, + sec.csrk_present); + if (sec.ltk_present) { + out += " ltk="; + appendHex(out, sec.ltk, sizeof(sec.ltk)); + out += "\n"; + } + if (sec.irk_present) { + out += " irk="; + appendHex(out, sec.irk, sizeof(sec.irk)); + out += "\n"; + } + if (sec.csrk_present) { + out += " csrk="; + appendHex(out, sec.csrk, sizeof(sec.csrk)); + out += "\n"; + } +} + +inline void appendCurrentRecord(std::string& out, const BleStoreValueSecCurrent& sec) { + out += " peer_addr="; + appendAddr(out, sec.peer_addr); + out += "\n"; + appendLine(out, + " bond_count=%u sign_counter=%u key_size=%u ediv=%u rand_num=%llu auth=%u sc=%u\n", + sec.bond_count, + sec.sign_counter, + sec.key_size, + sec.ediv, + static_cast(sec.rand_num), + sec.authenticated, + sec.sc); + appendLine(out, + " ltk_present=%u irk_present=%u csrk_present=%u\n", + sec.ltk_present, + sec.irk_present, + sec.csrk_present); + if (sec.ltk_present) { + out += " ltk="; + appendHex(out, sec.ltk, sizeof(sec.ltk)); + out += "\n"; + } + if (sec.irk_present) { + out += " irk="; + appendHex(out, sec.irk, sizeof(sec.irk)); + out += "\n"; + } + if (sec.csrk_present) { + out += " csrk="; + appendHex(out, sec.csrk, sizeof(sec.csrk)); + out += "\n"; + } +} + inline bool makeBondKey(char* out, size_t outLen, const char* prefix, uint16_t index) { const int written = snprintf(out, outLen, "%s_%u", prefix, index); return written > 0 && static_cast(written) < outLen; @@ -262,6 +366,109 @@ inline esp_err_t migrateBondStore(bool toCurrent, uint16_t maxEntries) { } // namespace detail +inline std::string dumpBondData(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { + std::string out; + out.reserve(2048); + + nvs_handle_t nvsHandle; + esp_err_t err = nvs_open(detail::kBondNamespace, NVS_READONLY, &nvsHandle); + if (err != ESP_OK) { + detail::appendLine(out, + "Failed to open NVS namespace '%s', err=%d\n", + detail::kBondNamespace, + static_cast(err)); + return out; + } + + out += "NimBLE bond dump\n"; + detail::appendLine(out, + "v1_size=%u current_size=%u max_entries=%u\n", + static_cast(sizeof(detail::BleStoreValueSecV1)), + static_cast(sizeof(detail::BleStoreValueSecCurrent)), + maxEntries); + + uint16_t foundCount = 0; + char key[detail::kNvsKeyMaxLen]{}; + + for (uint16_t i = 1; i <= maxEntries; ++i) { + const char* prefixes[] = {detail::kOurSecPrefix, detail::kPeerSecPrefix}; + const char* types[] = {"our", "peer"}; + + for (size_t j = 0; j < 2; ++j) { + if (!detail::makeBondKey(key, sizeof(key), prefixes[j], i)) { + out += "Key format error\n"; + continue; + } + + size_t blobSize = 0; + err = nvs_get_blob(nvsHandle, key, nullptr, &blobSize); + if (err == ESP_ERR_NVS_NOT_FOUND) { + continue; + } + if (err != ESP_OK) { + detail::appendLine(out, + "[%s:%u] key=%s read-size error err=%d\n", + types[j], + i, + key, + static_cast(err)); + continue; + } + + std::vector blob(blobSize); + size_t readSize = blobSize; + err = nvs_get_blob(nvsHandle, key, blob.data(), &readSize); + if (err != ESP_OK) { + detail::appendLine(out, + "[%s:%u] key=%s read error err=%d\n", + types[j], + i, + key, + static_cast(err)); + continue; + } + + foundCount++; + detail::appendLine(out, + "[%s:%u] key=%s size=%u ", + types[j], + i, + key, + static_cast(blobSize)); + + if (blobSize == sizeof(detail::BleStoreValueSecCurrent)) { + out += "format=current\n"; + detail::BleStoreValueSecCurrent sec{}; + memcpy(&sec, blob.data(), sizeof(sec)); + detail::appendCurrentRecord(out, sec); + } else if (blobSize == sizeof(detail::BleStoreValueSecV1)) { + out += "format=v1\n"; + detail::BleStoreValueSecV1 sec{}; + memcpy(&sec, blob.data(), sizeof(sec)); + detail::appendV1Record(out, sec); + } else { + out += "format=unknown\n"; + out += " raw="; + const size_t dumpLen = blobSize > 64 ? 64 : blobSize; + detail::appendHex(out, blob.data(), dumpLen); + if (blobSize > dumpLen) { + out += "..."; + } + out += "\n"; + } + } + } + + if (foundCount == 0) { + out += "No bond entries found\n"; + } else { + detail::appendLine(out, "Total entries found: %u\n", foundCount); + } + + nvs_close(nvsHandle); + return out; +} + inline esp_err_t migrateBondStoreToCurrent(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { return detail::migrateBondStore(true, maxEntries); } From c4a23a831df9ddc96a912c0d27825c92ad63d604 Mon Sep 17 00:00:00 2001 From: h2zero Date: Wed, 18 Mar 2026 17:40:51 -0600 Subject: [PATCH 3/3] Migrate local IRK value and update example. --- .../NimBLE_Bond_Migration.ino | 36 +-- src/NimBLEBondMigration.h | 254 +++++++++++++++--- 2 files changed, 231 insertions(+), 59 deletions(-) diff --git a/examples/NimBLE_Bond_Migration/NimBLE_Bond_Migration.ino b/examples/NimBLE_Bond_Migration/NimBLE_Bond_Migration.ino index aa391c7c0..f58296111 100644 --- a/examples/NimBLE_Bond_Migration/NimBLE_Bond_Migration.ino +++ b/examples/NimBLE_Bond_Migration/NimBLE_Bond_Migration.ino @@ -10,10 +10,8 @@ #include #include -namespace NimBLE_Bond_Migration = NimBLEBondMigration; - #ifndef NIMBLE_BOND_MIGRATION_MODE -#define NIMBLE_BOND_MIGRATION_MODE 0 +#define NIMBLE_BOND_MIGRATION_MODE 1 #endif #if (NIMBLE_BOND_MIGRATION_MODE < 0) || (NIMBLE_BOND_MIGRATION_MODE > 2) @@ -21,43 +19,37 @@ namespace NimBLE_Bond_Migration = NimBLEBondMigration; #endif void setup() { + using namespace NimBLEBondMigration; + Serial.begin(115200); Serial.println("Starting NimBLE_Bond_Migration Demo"); + bool success = false; #if NIMBLE_BOND_MIGRATION_MODE == 1 - { - esp_err_t rc = NimBLE_Bond_Migration::migrateBondStoreToCurrent(); - Serial.printf("NimBLE_Bond_Migration to current rc=%d\n", static_cast(rc)); - } + success = migrateBondStoreToCurrent(); + Serial.printf("NimBLE_Bond_Migration to current %s\n", success ? "success" : "failed"); #elif NIMBLE_BOND_MIGRATION_MODE == 2 - { - esp_err_t rc = NimBLE_Bond_Migration::migrateBondStoreToV1(); - Serial.printf("NimBLE_Bond_Migration to v1 rc=%d\n", static_cast(rc)); - if (rc == ESP_OK) { - Serial.println("NimBLE_Bond_Migration completed to v1, upload v1 firmware to continue"); - while (true) { - delay(1000); - } - } else { - Serial.println("NimBLE_Bond_Migration to v1 failed"); + success = migrateBondStoreToV1(); + Serial.printf("NimBLE_Bond_Migration to v1 %s\n", success ? "success" : "failed"); +#endif + + if (success) { + Serial.println("NimBLE_Bond_Migration completed, upload target firmware to continue"); + while (true) { + delay(1000); } } -#endif NimBLEDevice::init("NimBLE"); - NimBLEDevice::setPower(3); /** +3db */ - NimBLEDevice::setSecurityAuth(true, true, false); /** bonding, MITM, don't need BLE secure connections as we are using passkey pairing */ NimBLEDevice::setSecurityPasskey(123456); NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY); /** Display only passkey */ NimBLEServer* pServer = NimBLEDevice::createServer(); NimBLEService* pService = pServer->createService("ABCD"); - NimBLECharacteristic* pNonSecureCharacteristic = pService->createCharacteristic("1234", NIMBLE_PROPERTY::READ); NimBLECharacteristic* pSecureCharacteristic = pService->createCharacteristic("1235", NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::READ_AUTHEN); - pNonSecureCharacteristic->setValue("Hello Non Secure BLE"); pSecureCharacteristic->setValue("Hello Secure BLE"); NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); diff --git a/src/NimBLEBondMigration.h b/src/NimBLEBondMigration.h index 26652862f..37b725821 100644 --- a/src/NimBLEBondMigration.h +++ b/src/NimBLEBondMigration.h @@ -24,6 +24,7 @@ static constexpr const char* kLogTag = "NimBLEBondMigration"; static constexpr const char* kBondNamespace = "nimble_bond"; static constexpr const char* kOurSecPrefix = "our_sec"; static constexpr const char* kPeerSecPrefix = "peer_sec"; +static constexpr const char* kLocalIrkPrefix = "local_irk"; static constexpr size_t kNvsKeyMaxLen = 16; typedef struct { @@ -71,6 +72,20 @@ struct BleStoreValueSecCurrent { uint8_t sc : 1; }; +struct BleStoreValueLocalIrkV1 { + uint8_t irk[16]; +}; + +struct BleStoreValueLocalIrkCurrent { + ble_addr_t addr; + uint8_t irk[16]; +}; + +static constexpr uint8_t kDefaultLocalIrk[16] = { + 0xef, 0x8d, 0xe2, 0x16, 0x4f, 0xec, 0x43, 0x0d, + 0xbf, 0x5b, 0xdd, 0x34, 0xc0, 0x53, 0x1e, 0xb8, +}; + struct MigrationStats { uint16_t scanned; uint16_t converted; @@ -179,29 +194,45 @@ inline void appendCurrentRecord(std::string& out, const BleStoreValueSecCurrent& } } +inline void appendLocalIrkV1Record(std::string& out, const BleStoreValueLocalIrkV1& irkRecord) { + out += " irk="; + appendHex(out, irkRecord.irk, sizeof(irkRecord.irk)); + out += "\n"; +} + +inline void appendLocalIrkCurrentRecord(std::string& out, const BleStoreValueLocalIrkCurrent& irkRecord) { + out += " addr="; + appendAddr(out, irkRecord.addr); + out += "\n"; + out += " irk="; + appendHex(out, irkRecord.irk, sizeof(irkRecord.irk)); + out += "\n"; +} + inline bool makeBondKey(char* out, size_t outLen, const char* prefix, uint16_t index) { const int written = snprintf(out, outLen, "%s_%u", prefix, index); return written > 0 && static_cast(written) < outLen; } -inline esp_err_t migrateEntryToCurrent(nvs_handle_t nvsHandle, - const char* key, - uint16_t index, - MigrationStats* stats) { +inline bool migrateEntryToCurrent(nvs_handle_t nvsHandle, + const char* key, + uint16_t index, + MigrationStats* stats) { size_t blobSize = 0; esp_err_t err = nvs_get_blob(nvsHandle, key, nullptr, &blobSize); if (err == ESP_ERR_NVS_NOT_FOUND) { - return ESP_OK; + return true; } if (err != ESP_OK) { - return err; + ESP_LOGE(kLogTag, "nvs_get_blob size failed key=%s err=%d", key, static_cast(err)); + return false; } stats->scanned++; if (blobSize == sizeof(BleStoreValueSecCurrent)) { stats->alreadyTarget++; - return ESP_OK; + return true; } if (blobSize != sizeof(BleStoreValueSecV1)) { @@ -210,14 +241,15 @@ inline esp_err_t migrateEntryToCurrent(nvs_handle_t nvsHandle, "Skipping key=%s due to unexpected size=%u", key, static_cast(blobSize)); - return ESP_OK; + return true; } BleStoreValueSecV1 oldValue{}; size_t readSize = sizeof(oldValue); err = nvs_get_blob(nvsHandle, key, &oldValue, &readSize); if (err != ESP_OK) { - return err; + ESP_LOGE(kLogTag, "nvs_get_blob value failed key=%s err=%d", key, static_cast(err)); + return false; } BleStoreValueSecCurrent newValue{}; @@ -238,30 +270,32 @@ inline esp_err_t migrateEntryToCurrent(nvs_handle_t nvsHandle, err = nvs_set_blob(nvsHandle, key, &newValue, sizeof(newValue)); if (err != ESP_OK) { - return err; + ESP_LOGE(kLogTag, "nvs_set_blob failed key=%s err=%d", key, static_cast(err)); + return false; } stats->converted++; - return ESP_OK; + return true; } -inline esp_err_t migrateEntryToV1(nvs_handle_t nvsHandle, - const char* key, - MigrationStats* stats) { +inline bool migrateEntryToV1(nvs_handle_t nvsHandle, + const char* key, + MigrationStats* stats) { size_t blobSize = 0; esp_err_t err = nvs_get_blob(nvsHandle, key, nullptr, &blobSize); if (err == ESP_ERR_NVS_NOT_FOUND) { - return ESP_OK; + return true; } if (err != ESP_OK) { - return err; + ESP_LOGE(kLogTag, "nvs_get_blob size failed key=%s err=%d", key, static_cast(err)); + return false; } stats->scanned++; if (blobSize == sizeof(BleStoreValueSecV1)) { stats->alreadyTarget++; - return ESP_OK; + return true; } if (blobSize != sizeof(BleStoreValueSecCurrent)) { @@ -270,14 +304,15 @@ inline esp_err_t migrateEntryToV1(nvs_handle_t nvsHandle, "Skipping key=%s due to unexpected size=%u", key, static_cast(blobSize)); - return ESP_OK; + return true; } BleStoreValueSecCurrent curValue{}; size_t readSize = sizeof(curValue); err = nvs_get_blob(nvsHandle, key, &curValue, &readSize); if (err != ESP_OK) { - return err; + ESP_LOGE(kLogTag, "nvs_get_blob value failed key=%s err=%d", key, static_cast(err)); + return false; } BleStoreValueSecV1 oldValue{}; @@ -296,14 +331,77 @@ inline esp_err_t migrateEntryToV1(nvs_handle_t nvsHandle, err = nvs_set_blob(nvsHandle, key, &oldValue, sizeof(oldValue)); if (err != ESP_OK) { - return err; + ESP_LOGE(kLogTag, "nvs_set_blob failed key=%s err=%d", key, static_cast(err)); + return false; + } + + stats->converted++; + return true; +} + +inline bool migrateLocalIrkEntryToCurrent(nvs_handle_t nvsHandle, + const char* key, + MigrationStats* stats) { + size_t blobSize = 0; + esp_err_t err = nvs_get_blob(nvsHandle, key, nullptr, &blobSize); + if (err == ESP_ERR_NVS_NOT_FOUND) { + blobSize = 0; + } else if (err != ESP_OK) { + ESP_LOGE(kLogTag, "nvs_get_blob size failed key=%s err=%d", key, static_cast(err)); + return false; + } else { + stats->scanned++; + } + + if (blobSize != 0 && blobSize != sizeof(BleStoreValueLocalIrkCurrent) && + blobSize != sizeof(BleStoreValueLocalIrkV1)) { + stats->skippedUnknownSize++; + ESP_LOGW(kLogTag, + "Overwriting key=%s with default local_irk despite unexpected size=%u", + key, + static_cast(blobSize)); + } + + BleStoreValueLocalIrkCurrent newValue{}; + memcpy(newValue.irk, kDefaultLocalIrk, sizeof(newValue.irk)); + + err = nvs_set_blob(nvsHandle, key, &newValue, sizeof(newValue)); + if (err != ESP_OK) { + ESP_LOGE(kLogTag, "nvs_set_blob failed key=%s err=%d", key, static_cast(err)); + return false; } stats->converted++; - return ESP_OK; + return true; } -inline esp_err_t migrateBondStore(bool toCurrent, uint16_t maxEntries) { +inline bool migrateLocalIrkEntryToV1(nvs_handle_t nvsHandle, + const char* key, + MigrationStats* stats) { + size_t blobSize = 0; + esp_err_t err = nvs_get_blob(nvsHandle, key, nullptr, &blobSize); + if (err == ESP_ERR_NVS_NOT_FOUND) { + return true; + } + if (err != ESP_OK) { + ESP_LOGE(kLogTag, "nvs_get_blob size failed key=%s err=%d", key, static_cast(err)); + return false; + } + + stats->scanned++; + + // Rollback to v1 should not persist local IRK entries; erase if present. + err = nvs_erase_key(nvsHandle, key); + if (err != ESP_OK) { + ESP_LOGE(kLogTag, "nvs_erase_key failed key=%s err=%d", key, static_cast(err)); + return false; + } + + stats->converted++; + return true; +} + +inline bool migrateBondStore(bool toCurrent, uint16_t maxEntries) { nvs_handle_t nvsHandle; esp_err_t err = nvs_open(kBondNamespace, NVS_READWRITE, &nvsHandle); if (err != ESP_OK) { @@ -311,7 +409,7 @@ inline esp_err_t migrateBondStore(bool toCurrent, uint16_t maxEntries) { "Failed to open NVS namespace '%s', err=%d", kBondNamespace, static_cast(err)); - return err; + return false; } MigrationStats stats{}; @@ -320,34 +418,57 @@ inline esp_err_t migrateBondStore(bool toCurrent, uint16_t maxEntries) { for (uint16_t i = 1; i <= maxEntries; ++i) { if (!makeBondKey(key, sizeof(key), kOurSecPrefix, i)) { nvs_close(nvsHandle); - return ESP_FAIL; + return false; } - err = toCurrent ? migrateEntryToCurrent(nvsHandle, key, i, &stats) - : migrateEntryToV1(nvsHandle, key, &stats); - if (err != ESP_OK) { + if (!(toCurrent ? migrateEntryToCurrent(nvsHandle, key, i, &stats) + : migrateEntryToV1(nvsHandle, key, &stats))) { nvs_close(nvsHandle); - return err; + return false; } if (!makeBondKey(key, sizeof(key), kPeerSecPrefix, i)) { nvs_close(nvsHandle); - return ESP_FAIL; + return false; } - err = toCurrent ? migrateEntryToCurrent(nvsHandle, key, i, &stats) - : migrateEntryToV1(nvsHandle, key, &stats); - if (err != ESP_OK) { + if (!(toCurrent ? migrateEntryToCurrent(nvsHandle, key, i, &stats) + : migrateEntryToV1(nvsHandle, key, &stats))) { nvs_close(nvsHandle); - return err; + return false; + } + } + + if (toCurrent) { + if (!makeBondKey(key, sizeof(key), kLocalIrkPrefix, 1)) { + nvs_close(nvsHandle); + return false; + } + + if (!migrateLocalIrkEntryToCurrent(nvsHandle, key, &stats)) { + nvs_close(nvsHandle); + return false; + } + } else { + for (uint16_t i = 1; i <= maxEntries; ++i) { + if (!makeBondKey(key, sizeof(key), kLocalIrkPrefix, i)) { + nvs_close(nvsHandle); + return false; + } + + if (!migrateLocalIrkEntryToV1(nvsHandle, key, &stats)) { + nvs_close(nvsHandle); + return false; + } } } if (stats.converted > 0) { err = nvs_commit(nvsHandle); if (err != ESP_OK) { + ESP_LOGE(kLogTag, "nvs_commit failed err=%d", static_cast(err)); nvs_close(nvsHandle); - return err; + return false; } } @@ -361,7 +482,7 @@ inline esp_err_t migrateBondStore(bool toCurrent, uint16_t maxEntries) { stats.alreadyTarget, stats.skippedUnknownSize); - return ESP_OK; + return true; } } // namespace detail @@ -457,6 +578,65 @@ inline std::string dumpBondData(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_B out += "\n"; } } + + if (!detail::makeBondKey(key, sizeof(key), detail::kLocalIrkPrefix, i)) { + out += "Key format error\n"; + continue; + } + + size_t blobSize = 0; + err = nvs_get_blob(nvsHandle, key, nullptr, &blobSize); + if (err == ESP_ERR_NVS_NOT_FOUND) { + continue; + } + if (err != ESP_OK) { + detail::appendLine(out, + "[local_irk:%u] key=%s read-size error err=%d\n", + i, + key, + static_cast(err)); + continue; + } + + std::vector blob(blobSize); + size_t readSize = blobSize; + err = nvs_get_blob(nvsHandle, key, blob.data(), &readSize); + if (err != ESP_OK) { + detail::appendLine(out, + "[local_irk:%u] key=%s read error err=%d\n", + i, + key, + static_cast(err)); + continue; + } + + foundCount++; + detail::appendLine(out, + "[local_irk:%u] key=%s size=%u ", + i, + key, + static_cast(blobSize)); + + if (blobSize == sizeof(detail::BleStoreValueLocalIrkCurrent)) { + out += "format=local_irk_current\n"; + detail::BleStoreValueLocalIrkCurrent irkRecord{}; + memcpy(&irkRecord, blob.data(), sizeof(irkRecord)); + detail::appendLocalIrkCurrentRecord(out, irkRecord); + } else if (blobSize == sizeof(detail::BleStoreValueLocalIrkV1)) { + out += "format=local_irk_v1\n"; + detail::BleStoreValueLocalIrkV1 irkRecord{}; + memcpy(&irkRecord, blob.data(), sizeof(irkRecord)); + detail::appendLocalIrkV1Record(out, irkRecord); + } else { + out += "format=unknown\n"; + out += " raw="; + const size_t dumpLen = blobSize > 64 ? 64 : blobSize; + detail::appendHex(out, blob.data(), dumpLen); + if (blobSize > dumpLen) { + out += "..."; + } + out += "\n"; + } } if (foundCount == 0) { @@ -469,11 +649,11 @@ inline std::string dumpBondData(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_B return out; } -inline esp_err_t migrateBondStoreToCurrent(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { +inline bool migrateBondStoreToCurrent(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { return detail::migrateBondStore(true, maxEntries); } -inline esp_err_t migrateBondStoreToV1(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { +inline bool migrateBondStoreToV1(uint16_t maxEntries = MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { return detail::migrateBondStore(false, maxEntries); }