Files
pyxis/lib/ble_interface/BLEIdentityManager.cpp
torlando-tech ac6ceca9f8 Initial commit: standalone Pyxis T-Deck firmware
Split T-Deck firmware from microReticulum examples/lxmf_tdeck/ into its
own repo. microReticulum is consumed as a git submodule dependency pinned
to feat/t-deck. All include paths updated from relative symlinks to bare
includes resolved via library build flags.

Both tdeck (NimBLE) and tdeck-bluedroid environments compile successfully.
Licensed under AGPLv3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 19:48:33 -05: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 "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