Files
pyxis/lib/ble_interface/BLEIdentityManager.cpp
T
torlando-agent[bot] 70d4aa6be9 feat: graft pyxis onto upstream microReticulum 0.4.1
Repins microReticulum + microLXMF onto the upstream-0.4.1 graft and adapts
pyxis to the new src/microReticulum/ layout and 0.4.x APIs. The far-diverged
0.3.0 fork's Resource/Transport/Identity work is subsumed by upstream's
reimplementation; only the still-needed fixes ride on the pinned branches
(PKCS7/HMAC/X25519 crypto -- proven byte-identical to python RNS 1.3.1 --
Packet link-proof callback, Identity short-sig guard, and the bz2 layer +
decompress-on-receive in Resource::assemble()).

Consumer-side changes:
- platformio.ini: pin microReticulum @2f21fee (pyxis-fixes-on-0.4.1) and
  microLXMF @33760d0 (chore/microreticulum-0.4.1-layout); bump microStore
  ceea8f5 -> c5fb69d (0.4.x requires the new BasicFileStore::init API);
  -std=gnu++11 -> gnu++17 (upstream requires C++17).
- Namespace all microReticulum includes (angle + quote) to <microReticulum/...>
  for the relocated layout; shim-local Utilities/Stream.h|Print.h preserved.
- Interface::send_outgoing now returns bool: update TCP/BLE/SX1262/Auto
  overrides with correct success/failure returns.
- SDArchiveFileSystem::init(bool reformatOnFail=true) to match new microStore.
- Static Transport::get_path_table() -> path_table(); instance getter unchanged.
- Remove duplicate shim Cryptography/BZ2 (microReticulum provides it now; keep
  lib/libbz2 as the ESP32 bzlib provider).
- patch_littlefs_paths.py: normalize microStore's LittleFS adapter paths to a
  leading "/" -- ESP32 Arduino LittleFS rejects "./"-prefixed paths, which
  silently broke the path store (no peer paths learned, all messaging blocked).

Validated on T-Deck Plus: builds (RAM 27.5% / Flash 77.7%), boots stable
(no WDT/panic), and a full on-device LXMF e2e (DIRECT + OPPORTUNISTIC +
bz2-compressed-Resource receive) passes 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
2026-06-19 15:49:44 -04:00

494 lines
16 KiB
C++

/**
* @file BLEIdentityManager.cpp
* @brief BLE-Reticulum Protocol v2.2 identity handshake manager implementation
*
* Uses fixed-size pools instead of STL containers to eliminate heap fragmentation.
*/
#include "BLEIdentityManager.h"
#include <microReticulum/Log.h>
namespace RNS { namespace BLE {
BLEIdentityManager::BLEIdentityManager() {
// Initialize all pools to empty state
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
_address_identity_pool[i].clear();
}
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
_handshakes_pool[i].clear();
}
}
void BLEIdentityManager::setLocalIdentity(const Bytes& identity_hash) {
if (identity_hash.size() >= Limits::IDENTITY_SIZE) {
_local_identity = Bytes(identity_hash.data(), Limits::IDENTITY_SIZE);
DEBUG("BLEIdentityManager: Local identity set: " + _local_identity.toHex().substr(0, 8) + "...");
} else {
ERROR("BLEIdentityManager: Invalid identity size: " + std::to_string(identity_hash.size()));
}
}
void BLEIdentityManager::setHandshakeCompleteCallback(HandshakeCompleteCallback callback) {
_handshake_complete_callback = callback;
}
void BLEIdentityManager::setHandshakeFailedCallback(HandshakeFailedCallback callback) {
_handshake_failed_callback = callback;
}
void BLEIdentityManager::setMacRotationCallback(MacRotationCallback callback) {
_mac_rotation_callback = callback;
}
//=============================================================================
// Handshake Operations
//=============================================================================
Bytes BLEIdentityManager::initiateHandshake(const Bytes& mac_address) {
if (!hasLocalIdentity()) {
ERROR("BLEIdentityManager: Cannot initiate handshake without local identity");
return Bytes();
}
if (mac_address.size() < Limits::MAC_SIZE) {
ERROR("BLEIdentityManager: Invalid MAC address size");
return Bytes();
}
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
// Create or update handshake session
HandshakeSession* session = getOrCreateSession(mac);
if (!session) {
WARNING("BLEIdentityManager: Handshake pool is full, cannot initiate");
return Bytes();
}
session->is_central = true;
session->state = HandshakeState::INITIATED;
session->started_at = Utilities::OS::time();
DEBUG("BLEIdentityManager: Initiating handshake as central with " +
BLEAddress(mac.data()).toString());
// Return our identity to be written to peer
return _local_identity;
}
bool BLEIdentityManager::processReceivedData(const Bytes& mac_address, const Bytes& data, bool is_central) {
if (mac_address.size() < Limits::MAC_SIZE) {
return false;
}
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
// Check if this looks like a handshake
if (!isHandshakeData(data, mac)) {
return false; // Regular data, not consumed
}
// This is a handshake - extract peer's identity
if (data.size() != Limits::IDENTITY_SIZE) {
// Should not happen given isHandshakeData check, but be safe
return false;
}
Bytes peer_identity(data.data(), Limits::IDENTITY_SIZE);
DEBUG("BLEIdentityManager: Received identity handshake from " +
BLEAddress(mac.data()).toString() + ": " +
peer_identity.toHex().substr(0, 8) + "...");
// Complete the handshake
completeHandshake(mac, peer_identity, is_central);
return true; // Handshake data consumed
}
bool BLEIdentityManager::isHandshakeData(const Bytes& data, const Bytes& mac_address) const {
// Handshake is detected if:
// 1. Data is exactly 16 bytes (identity size)
// 2. No existing identity mapping for this MAC
if (data.size() != Limits::IDENTITY_SIZE) {
return false;
}
if (mac_address.size() < Limits::MAC_SIZE) {
return false;
}
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
// Check if we already have identity for this MAC
const AddressIdentitySlot* slot = findAddressToIdentitySlot(mac);
if (slot) {
// Already have identity - this is regular data, not handshake
return false;
}
// No existing identity + 16 bytes = handshake
return true;
}
void BLEIdentityManager::completeHandshake(const Bytes& mac_address, const Bytes& peer_identity,
bool is_central) {
DEBUG("BLEIdentityManager::completeHandshake: Starting");
if (mac_address.size() < Limits::MAC_SIZE || peer_identity.size() != Limits::IDENTITY_SIZE) {
DEBUG("BLEIdentityManager::completeHandshake: Invalid sizes, returning");
return;
}
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
Bytes identity(peer_identity.data(), Limits::IDENTITY_SIZE);
DEBUG("BLEIdentityManager::completeHandshake: Created local copies");
// Check for MAC rotation: same identity from different MAC address
Bytes old_mac;
bool is_rotation = false;
AddressIdentitySlot* existing_slot = findIdentityToAddressSlot(identity);
if (existing_slot && existing_slot->mac_address != mac) {
// MAC rotation detected!
old_mac = existing_slot->mac_address;
is_rotation = true;
INFO("BLEIdentityManager: MAC rotation detected for identity " +
identity.toHex().substr(0, 8) + "...: " +
BLEAddress(old_mac.data()).toString() + " -> " +
BLEAddress(mac.data()).toString());
// Update the slot with new MAC (same identity)
existing_slot->mac_address = mac;
} else if (!existing_slot) {
// New mapping - add to pool
if (!setAddressIdentityMapping(mac, identity)) {
WARNING("BLEIdentityManager: Address-identity pool is full");
return;
}
}
DEBUG("BLEIdentityManager::completeHandshake: Stored mappings");
// Remove handshake session
removeHandshakeSession(mac);
DEBUG("BLEIdentityManager::completeHandshake: Removed handshake session");
DEBUG("BLEIdentityManager: Handshake complete with " +
BLEAddress(mac.data()).toString() +
" identity: " + identity.toHex().substr(0, 8) + "..." +
(is_central ? " (we are central)" : " (we are peripheral)"));
// Invoke MAC rotation callback if this was a rotation
if (is_rotation && _mac_rotation_callback) {
DEBUG("BLEIdentityManager::completeHandshake: Calling MAC rotation callback");
_mac_rotation_callback(old_mac, mac, identity);
DEBUG("BLEIdentityManager::completeHandshake: MAC rotation callback returned");
}
// Invoke handshake complete callback
if (_handshake_complete_callback) {
DEBUG("BLEIdentityManager::completeHandshake: Calling handshake complete callback");
_handshake_complete_callback(mac, identity, is_central);
DEBUG("BLEIdentityManager::completeHandshake: Callback returned");
} else {
DEBUG("BLEIdentityManager::completeHandshake: No callback set");
}
}
void BLEIdentityManager::checkTimeouts() {
double now = Utilities::OS::time();
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
HandshakeSession& session = _handshakes_pool[i];
if (!session.in_use) continue;
if (session.state != HandshakeState::COMPLETE) {
double age = now - session.started_at;
if (age > Timing::HANDSHAKE_TIMEOUT) {
Bytes mac = session.mac_address;
WARNING("BLEIdentityManager: Handshake timeout for " +
BLEAddress(mac.data()).toString());
if (_handshake_failed_callback) {
_handshake_failed_callback(mac, "Handshake timeout");
}
session.clear();
}
}
}
}
//=============================================================================
// Identity Mapping
//=============================================================================
Bytes BLEIdentityManager::getIdentityForMac(const Bytes& mac_address) const {
if (mac_address.size() < Limits::MAC_SIZE) {
return Bytes();
}
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
const AddressIdentitySlot* slot = findAddressToIdentitySlot(mac);
if (slot) {
return slot->identity;
}
return Bytes();
}
Bytes BLEIdentityManager::getMacForIdentity(const Bytes& identity) const {
if (identity.size() != Limits::IDENTITY_SIZE) {
return Bytes();
}
const AddressIdentitySlot* slot = findIdentityToAddressSlot(identity);
if (slot) {
return slot->mac_address;
}
return Bytes();
}
bool BLEIdentityManager::hasIdentity(const Bytes& mac_address) const {
if (mac_address.size() < Limits::MAC_SIZE) {
return false;
}
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
return findAddressToIdentitySlot(mac) != nullptr;
}
Bytes BLEIdentityManager::findIdentityByPrefix(const Bytes& prefix) const {
if (prefix.size() == 0 || prefix.size() > Limits::IDENTITY_SIZE) {
return Bytes();
}
// Search through all known identities for one that starts with this prefix
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
if (_address_identity_pool[i].in_use) {
const Bytes& identity = _address_identity_pool[i].identity;
if (identity.size() >= prefix.size() &&
memcmp(identity.data(), prefix.data(), prefix.size()) == 0) {
return identity;
}
}
}
return Bytes();
}
void BLEIdentityManager::updateMacForIdentity(const Bytes& identity, const Bytes& new_mac) {
if (identity.size() != Limits::IDENTITY_SIZE || new_mac.size() < Limits::MAC_SIZE) {
return;
}
Bytes mac(new_mac.data(), Limits::MAC_SIZE);
AddressIdentitySlot* slot = findIdentityToAddressSlot(identity);
if (!slot) {
return; // Unknown identity
}
// Update MAC address in the slot
slot->mac_address = mac;
DEBUG("BLEIdentityManager: Updated MAC for identity " +
identity.toHex().substr(0, 8) + "... to " +
BLEAddress(mac.data()).toString());
}
void BLEIdentityManager::removeMapping(const Bytes& mac_address) {
if (mac_address.size() < Limits::MAC_SIZE) {
return;
}
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
removeAddressIdentityMapping(mac);
DEBUG("BLEIdentityManager: Removed mapping for " +
BLEAddress(mac.data()).toString());
// Also clean up any pending handshake
removeHandshakeSession(mac);
}
void BLEIdentityManager::clearAllMappings() {
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
_address_identity_pool[i].clear();
}
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
_handshakes_pool[i].clear();
}
DEBUG("BLEIdentityManager: Cleared all identity mappings");
}
size_t BLEIdentityManager::knownPeerCount() const {
size_t count = 0;
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
if (_address_identity_pool[i].in_use) count++;
}
return count;
}
bool BLEIdentityManager::isHandshakeInProgress(const Bytes& mac_address) const {
if (mac_address.size() < Limits::MAC_SIZE) {
return false;
}
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
const HandshakeSession* session = findHandshakeSession(mac);
if (session) {
return session->state != HandshakeState::NONE &&
session->state != HandshakeState::COMPLETE;
}
return false;
}
//=============================================================================
// Pool Helper Methods - Address to Identity Mapping
//=============================================================================
BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findAddressToIdentitySlot(const Bytes& mac) {
if (mac.size() < Limits::MAC_SIZE) return nullptr;
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
if (_address_identity_pool[i].in_use &&
_address_identity_pool[i].mac_address == mac) {
return &_address_identity_pool[i];
}
}
return nullptr;
}
const BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findAddressToIdentitySlot(const Bytes& mac) const {
return const_cast<BLEIdentityManager*>(this)->findAddressToIdentitySlot(mac);
}
BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findIdentityToAddressSlot(const Bytes& identity) {
if (identity.size() != Limits::IDENTITY_SIZE) return nullptr;
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
if (_address_identity_pool[i].in_use &&
_address_identity_pool[i].identity == identity) {
return &_address_identity_pool[i];
}
}
return nullptr;
}
const BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findIdentityToAddressSlot(const Bytes& identity) const {
return const_cast<BLEIdentityManager*>(this)->findIdentityToAddressSlot(identity);
}
BLEIdentityManager::AddressIdentitySlot* BLEIdentityManager::findEmptyAddressIdentitySlot() {
for (size_t i = 0; i < ADDRESS_IDENTITY_POOL_SIZE; i++) {
if (!_address_identity_pool[i].in_use) {
return &_address_identity_pool[i];
}
}
return nullptr;
}
bool BLEIdentityManager::setAddressIdentityMapping(const Bytes& mac, const Bytes& identity) {
// Check if already exists for this MAC
AddressIdentitySlot* existing = findAddressToIdentitySlot(mac);
if (existing) {
existing->identity = identity;
return true;
}
// Check if already exists for this identity (MAC rotation case)
existing = findIdentityToAddressSlot(identity);
if (existing) {
existing->mac_address = mac;
return true;
}
// Find empty slot
AddressIdentitySlot* slot = findEmptyAddressIdentitySlot();
if (!slot) {
WARNING("BLEIdentityManager: Address-identity pool is full");
return false;
}
slot->in_use = true;
slot->mac_address = mac;
slot->identity = identity;
return true;
}
void BLEIdentityManager::removeAddressIdentityMapping(const Bytes& mac) {
AddressIdentitySlot* slot = findAddressToIdentitySlot(mac);
if (slot) {
slot->clear();
}
}
//=============================================================================
// Pool Helper Methods - Handshake Sessions
//=============================================================================
BLEIdentityManager::HandshakeSession* BLEIdentityManager::findHandshakeSession(const Bytes& mac) {
if (mac.size() < Limits::MAC_SIZE) return nullptr;
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
if (_handshakes_pool[i].in_use &&
_handshakes_pool[i].mac_address == mac) {
return &_handshakes_pool[i];
}
}
return nullptr;
}
const BLEIdentityManager::HandshakeSession* BLEIdentityManager::findHandshakeSession(const Bytes& mac) const {
return const_cast<BLEIdentityManager*>(this)->findHandshakeSession(mac);
}
BLEIdentityManager::HandshakeSession* BLEIdentityManager::findEmptyHandshakeSlot() {
for (size_t i = 0; i < HANDSHAKE_POOL_SIZE; i++) {
if (!_handshakes_pool[i].in_use) {
return &_handshakes_pool[i];
}
}
return nullptr;
}
BLEIdentityManager::HandshakeSession* BLEIdentityManager::getOrCreateSession(const Bytes& mac_address) {
Bytes mac(mac_address.data(), Limits::MAC_SIZE);
// Check if session already exists
HandshakeSession* existing = findHandshakeSession(mac);
if (existing) {
return existing;
}
// Find empty slot
HandshakeSession* slot = findEmptyHandshakeSlot();
if (!slot) {
return nullptr; // Pool is full
}
// Create new session
slot->in_use = true;
slot->mac_address = mac;
slot->state = HandshakeState::NONE;
slot->started_at = Utilities::OS::time();
return slot;
}
void BLEIdentityManager::removeHandshakeSession(const Bytes& mac) {
HandshakeSession* session = findHandshakeSession(mac);
if (session) {
session->clear();
}
}
}} // namespace RNS::BLE