diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 35b80bfa..a6d30bbf 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -419,37 +419,105 @@ bool DataStore::saveNonces(DataStoreHost* host) { void DataStore::loadSessionKeys(DataStoreHost* host) { File file = openRead(_getContactsChannelsFS(), "/sess_keys"); - if (file) { - uint8_t rec[71]; // 4-byte pub_key prefix + 1 flags + 2 nonce + 32 session_key + 32 prev_session_key - while (file.read(rec, 71) == 71) { - uint16_t nonce; - memcpy(&nonce, &rec[5], 2); - host->onSessionKeyLoaded(rec, rec[4], nonce, &rec[7], &rec[39]); + if (!file) return; + while (true) { + uint8_t rec[SESSION_KEY_RECORD_MIN_SIZE]; + if (file.read(rec, SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break; + uint8_t flags = rec[4]; + uint16_t nonce; + memcpy(&nonce, &rec[5], 2); + uint8_t prev_key[SESSION_KEY_SIZE]; + if (flags & SESSION_FLAG_PREV_VALID) { + if (file.read(prev_key, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break; + } else { + memset(prev_key, 0, SESSION_KEY_SIZE); } - file.close(); + host->onSessionKeyLoaded(rec, flags, nonce, &rec[7], prev_key); } + file.close(); } bool DataStore::saveSessionKeys(DataStoreHost* host) { - File file = openWrite(_getContactsChannelsFS(), "/sess_keys"); - if (file) { - uint8_t pub_key_prefix[4]; - uint8_t flags; - uint16_t nonce; - uint8_t session_key[32]; - uint8_t prev_session_key[32]; - for (int idx = 0; idx < MAX_SESSION_KEYS; idx++) { - if (host->getSessionKeyForSave(idx, pub_key_prefix, &flags, &nonce, session_key, prev_session_key)) { - file.write(pub_key_prefix, 4); - file.write(&flags, 1); - file.write((uint8_t*)&nonce, 2); - file.write(session_key, 32); - file.write(prev_session_key, 32); + FILESYSTEM* fs = _getContactsChannelsFS(); + + // 1. Read old flash file into buffer (variable-length records) + uint8_t old_buf[MAX_SESSION_KEYS_FLASH * SESSION_KEY_RECORD_SIZE]; + int old_len = 0; + File rf = openRead(fs, "/sess_keys"); + if (rf) { + while (true) { + if (old_len + SESSION_KEY_RECORD_MIN_SIZE > (int)sizeof(old_buf)) break; + if (rf.read(&old_buf[old_len], SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break; + uint8_t flags = old_buf[old_len + 4]; + int rec_len = SESSION_KEY_RECORD_MIN_SIZE; + if (flags & SESSION_FLAG_PREV_VALID) { + if (old_len + SESSION_KEY_RECORD_SIZE > (int)sizeof(old_buf)) break; + if (rf.read(&old_buf[old_len + SESSION_KEY_RECORD_MIN_SIZE], SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break; + rec_len = SESSION_KEY_RECORD_SIZE; } + old_len += rec_len; + } + rf.close(); + } + + // 2. Write merged file + File wf = openWrite(fs, "/sess_keys"); + if (!wf) return false; + + // Write kept old records (variable-length) + int pos = 0; + while (pos + SESSION_KEY_RECORD_MIN_SIZE <= old_len) { + uint8_t* rec = &old_buf[pos]; + uint8_t flags = rec[4]; + int rec_len = (flags & SESSION_FLAG_PREV_VALID) ? SESSION_KEY_RECORD_SIZE : SESSION_KEY_RECORD_MIN_SIZE; + if (pos + rec_len > old_len) break; + if (!host->isSessionKeyInRAM(rec) && !host->isSessionKeyRemoved(rec)) { + wf.write(rec, rec_len); + } + pos += rec_len; + } + // Write current RAM entries (variable-length) + for (int idx = 0; idx < MAX_SESSION_KEYS_RAM; idx++) { + uint8_t pk[4]; uint8_t fl; uint16_t n; uint8_t sk[32]; uint8_t psk[32]; + if (!host->getSessionKeyForSave(idx, pk, &fl, &n, sk, psk)) continue; + wf.write(pk, 4); wf.write(&fl, 1); wf.write((uint8_t*)&n, 2); + wf.write(sk, 32); + if (fl & SESSION_FLAG_PREV_VALID) { + wf.write(psk, 32); + } + } + wf.close(); + return true; +} + +bool DataStore::loadSessionKeyByPrefix(const uint8_t* prefix, + uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) { + File f = openRead(_getContactsChannelsFS(), "/sess_keys"); + if (!f) return false; + while (true) { + uint8_t rec[SESSION_KEY_RECORD_MIN_SIZE]; + if (f.read(rec, SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break; + uint8_t rec_flags = rec[4]; + bool has_prev = (rec_flags & SESSION_FLAG_PREV_VALID); + if (memcmp(rec, prefix, 4) == 0) { + *flags = rec_flags; + memcpy(nonce, &rec[5], 2); + memcpy(session_key, &rec[7], SESSION_KEY_SIZE); + if (has_prev) { + if (f.read(prev_session_key, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break; + } else { + memset(prev_session_key, 0, SESSION_KEY_SIZE); + } + f.close(); + return true; + } + // Skip prev_key if present + if (has_prev) { + uint8_t skip[SESSION_KEY_SIZE]; + if (f.read(skip, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break; } - file.close(); - return true; } + f.close(); return false; } diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h index 3584cb34..360aa0cb 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -17,6 +17,8 @@ public: const uint8_t* session_key, const uint8_t* prev_session_key) { return false; } virtual bool getSessionKeyForSave(int idx, uint8_t* pub_key_prefix, uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) { return false; } + virtual bool isSessionKeyInRAM(const uint8_t* pub_key_prefix) { return false; } + virtual bool isSessionKeyRemoved(const uint8_t* pub_key_prefix) { return false; } }; class DataStore { @@ -49,6 +51,8 @@ public: bool saveNonces(DataStoreHost* host); void loadSessionKeys(DataStoreHost* host); bool saveSessionKeys(DataStoreHost* host); + bool loadSessionKeyByPrefix(const uint8_t* prefix, + uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key); void migrateToSecondaryFS(); uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a07779ff..b6501681 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2248,6 +2248,14 @@ void MyMesh::checkSerialInterface() { } } +bool MyMesh::isSessionKeyInRAM(const uint8_t* pub_key_prefix) { + return isSessionKeyInRAMPool(pub_key_prefix); +} + +bool MyMesh::isSessionKeyRemoved(const uint8_t* pub_key_prefix) { + return isSessionKeyRemovedFromPool(pub_key_prefix); +} + void MyMesh::loop() { BaseChatMesh::loop(); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index fae98d22..427b4d4c 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -169,6 +169,8 @@ protected: uint8_t* session_key, uint8_t* prev_session_key) override { return getSessionKeyEntry(idx, pub_key_prefix, flags, nonce, session_key, prev_session_key); } + bool isSessionKeyInRAM(const uint8_t* pub_key_prefix) override; + bool isSessionKeyRemoved(const uint8_t* pub_key_prefix) override; void clearPendingReqs() { pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0; @@ -214,9 +216,16 @@ private: void saveChannels() { _store->saveChannels(this); } void saveContacts(); void saveNonces() { if (_store->saveNonces(this)) clearNonceDirty(); } - void saveSessionKeys() { if (_store->saveSessionKeys(this)) clearSessionKeysDirty(); } + void saveSessionKeys() { if (_store->saveSessionKeys(this)) { clearSessionKeysDirty(); clearSessionKeysRemoved(); } } void onSessionKeysUpdated() override { saveSessionKeys(); } + // Flash-backed session key overrides + bool loadSessionKeyRecordFromFlash(const uint8_t* pub_key_prefix, + uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) override { + return _store->loadSessionKeyByPrefix(pub_key_prefix, flags, nonce, session_key, prev_session_key); + } + void mergeAndSaveSessionKeys() override { saveSessionKeys(); } + DataStore* _store; NodePrefs _prefs; uint32_t pending_login; diff --git a/src/MeshCore.h b/src/MeshCore.h index aae7ca82..089acaaf 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -37,7 +37,11 @@ #define NONCE_INITIAL_MAX 50000 // max random nonce seed for new contacts #define SESSION_KEY_TIMEOUT_MS 180000 // 3 minutes per attempt #define SESSION_KEY_MAX_RETRIES 3 // attempts per negotiation round -#define MAX_SESSION_KEYS 8 // max concurrent session key entries +#define MAX_SESSION_KEYS_RAM 8 // max concurrent session key entries in RAM (LRU cache) +#define MAX_SESSION_KEYS_FLASH 48 // max entries in flash file +#define SESSION_KEY_RECORD_SIZE 71 // max bytes per record (with prev_key) +#define SESSION_KEY_RECORD_MIN_SIZE 39 // min bytes per record: [pub_prefix:4][flags:1][nonce:2][key:32] +#define SESSION_FLAG_PREV_VALID 0x01 // prev_session_key is valid for dual-decode #define SESSION_KEY_STALE_THRESHOLD 50 // sends without recv before fallback to static ECDH #define SESSION_KEY_ECB_THRESHOLD 100 // sends without recv before fallback to ECB #define SESSION_KEY_ABANDON_THRESHOLD 255 // sends without recv before clearing AEAD + session key diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 8f0bd79a..02dc9116 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -991,7 +991,7 @@ bool BaseChatMesh::removeContact(ContactInfo& contact) { } if (idx >= num_contacts) return false; // not found - session_keys.remove(contact.id.pub_key); // also remove session key if any + removeSessionKey(contact.id.pub_key); // also remove session key if any // remove from contacts array and parallel nonce tracking num_contacts--; @@ -1106,6 +1106,41 @@ void BaseChatMesh::loop() { } } +// --- Session key flash-backed wrappers --- + +SessionKeyEntry* BaseChatMesh::findSessionKey(const uint8_t* pub_key) { + auto entry = session_keys.findByPrefix(pub_key); + if (entry) return entry; + + // Cache miss — try flash + uint8_t flags; uint16_t nonce; + uint8_t sk[SESSION_KEY_SIZE], psk[SESSION_KEY_SIZE]; + if (!loadSessionKeyRecordFromFlash(pub_key, &flags, &nonce, sk, psk)) return nullptr; + + // Save dirty evictee before overwriting + if (session_keys.isFull() && session_keys_dirty) { + mergeAndSaveSessionKeys(); + } + session_keys.applyLoaded(pub_key, flags, nonce, sk, psk); + return session_keys.findByPrefix(pub_key); +} + +SessionKeyEntry* BaseChatMesh::allocateSessionKey(const uint8_t* pub_key) { + // Check RAM and flash first + auto entry = findSessionKey(pub_key); + if (entry) return entry; + + // Not found anywhere — save dirty evictee before allocating + if (session_keys.isFull() && session_keys_dirty) { + mergeAndSaveSessionKeys(); + } + return session_keys.allocate(pub_key); +} + +void BaseChatMesh::removeSessionKey(const uint8_t* pub_key) { + session_keys.remove(pub_key); +} + // --- Session key support (Phase 2 — initiator) --- static bool canUseSessionKey(const SessionKeyEntry* entry) { @@ -1121,7 +1156,7 @@ static bool canUseSessionKey(const SessionKeyEntry* entry) { } const uint8_t* BaseChatMesh::getEncryptionKeyFor(const ContactInfo& contact) { - auto entry = session_keys.findByPrefix(contact.id.pub_key); + auto entry = findSessionKey(contact.id.pub_key); if (canUseSessionKey(entry)) { return entry->session_key; } @@ -1130,7 +1165,7 @@ const uint8_t* BaseChatMesh::getEncryptionKeyFor(const ContactInfo& contact) { uint16_t BaseChatMesh::getEncryptionNonceFor(const ContactInfo& contact) { uint16_t nonce = 0; - auto entry = session_keys.findByPrefix(contact.id.pub_key); + auto entry = findSessionKey(contact.id.pub_key); if (canUseSessionKey(entry)) { ++entry->nonce; if (entry->sends_since_last_recv < 255) entry->sends_since_last_recv++; @@ -1144,7 +1179,7 @@ uint16_t BaseChatMesh::getEncryptionNonceFor(const ContactInfo& contact) { int idx = &contact - contacts; if (idx >= 0 && idx < num_contacts) contacts[idx].flags &= ~CONTACT_FLAG_AEAD; - session_keys.remove(contact.id.pub_key); + removeSessionKey(contact.id.pub_key); onSessionKeysUpdated(); // nonce = 0 (ECB) } else if (entry->sends_since_last_recv >= SESSION_KEY_ECB_THRESHOLD) { @@ -1173,7 +1208,7 @@ bool BaseChatMesh::shouldInitiateSessionKey(const ContactInfo& contact) { // Need a known path to send the request if (contact.out_path_len < 0) return false; - auto entry = session_keys.findByPrefix(contact.id.pub_key); + auto entry = findSessionKey(contact.id.pub_key); // Don't trigger if negotiation already in progress if (entry && entry->state == SESSION_STATE_INIT_SENT) return false; @@ -1209,7 +1244,7 @@ bool BaseChatMesh::shouldInitiateSessionKey(const ContactInfo& contact) { } bool BaseChatMesh::initiateSessionKeyNegotiation(const ContactInfo& contact) { - auto entry = session_keys.allocate(contact.id.pub_key); + auto entry = allocateSessionKey(contact.id.pub_key); if (!entry) return false; // Don't start a new negotiation if one is already pending @@ -1245,7 +1280,7 @@ bool BaseChatMesh::handleSessionKeyResponse(ContactInfo& contact, const uint8_t* if (len < 5 + PUB_KEY_SIZE) return false; if (data[4] != RESP_TYPE_SESSION_KEY_ACCEPT) return false; - auto entry = session_keys.findByPrefix(contact.id.pub_key); + auto entry = findSessionKey(contact.id.pub_key); if (!entry || entry->state != SESSION_STATE_INIT_SENT) return false; const uint8_t* ephemeral_pub_B = &data[5]; @@ -1307,7 +1342,7 @@ uint8_t BaseChatMesh::handleIncomingSessionKeyInit(ContactInfo& from, const uint memset(ephemeral_secret, 0, PUB_KEY_SIZE); // 4. Store in pool (dual-decode: new key active, old key still valid) - auto entry = session_keys.allocate(from.id.pub_key); + auto entry = allocateSessionKey(from.id.pub_key); if (!entry) return 0; if (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE) { @@ -1370,7 +1405,7 @@ void BaseChatMesh::checkSessionKeyTimeouts() { const uint8_t* BaseChatMesh::getPeerSessionKey(int peer_idx) { int i = matching_peer_indexes[peer_idx]; if (i >= 0 && i < num_contacts) { - auto entry = session_keys.findByPrefix(contacts[i].id.pub_key); + auto entry = findSessionKey(contacts[i].id.pub_key); // Also try decode during INIT_SENT renegotiation (nonce > 1 means prior key exists) if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE || (entry->state == SESSION_STATE_INIT_SENT && entry->nonce > 1))) @@ -1382,7 +1417,7 @@ const uint8_t* BaseChatMesh::getPeerSessionKey(int peer_idx) { const uint8_t* BaseChatMesh::getPeerPrevSessionKey(int peer_idx) { int i = matching_peer_indexes[peer_idx]; if (i >= 0 && i < num_contacts) { - auto entry = session_keys.findByPrefix(contacts[i].id.pub_key); + auto entry = findSessionKey(contacts[i].id.pub_key); if (entry && entry->state == SESSION_STATE_DUAL_DECODE) return entry->prev_session_key; } @@ -1392,7 +1427,7 @@ const uint8_t* BaseChatMesh::getPeerPrevSessionKey(int peer_idx) { void BaseChatMesh::onSessionKeyDecryptSuccess(int peer_idx) { int i = matching_peer_indexes[peer_idx]; if (i >= 0 && i < num_contacts) { - auto entry = session_keys.findByPrefix(contacts[i].id.pub_key); + auto entry = findSessionKey(contacts[i].id.pub_key); if (entry) { bool changed = (entry->state == SESSION_STATE_DUAL_DECODE); if (changed) { diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 18e5ed1d..92481f0c 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -159,6 +159,15 @@ protected: // Session key support (Phase 2 — initiator) virtual void onSessionKeysUpdated() { session_keys_dirty = true; } // called when session key pool changes; override to persist + virtual bool loadSessionKeyRecordFromFlash(const uint8_t* pub_key_prefix, + uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) { return false; } + virtual void mergeAndSaveSessionKeys() {} // merge RAM + flash, write back + + // Wrappers that add flash fallback on cache miss + SessionKeyEntry* findSessionKey(const uint8_t* pub_key); + SessionKeyEntry* allocateSessionKey(const uint8_t* pub_key); + void removeSessionKey(const uint8_t* pub_key); + const uint8_t* getEncryptionKeyFor(const ContactInfo& contact); uint16_t getEncryptionNonceFor(const ContactInfo& contact); bool shouldInitiateSessionKey(const ContactInfo& contact); @@ -175,6 +184,9 @@ protected: int getSessionKeyCount() const { return session_keys.getCount(); } bool isSessionKeysDirty() const { return session_keys_dirty; } void clearSessionKeysDirty() { session_keys_dirty = false; } + bool isSessionKeyInRAMPool(const uint8_t* pub_key_prefix) { return session_keys.hasPrefix(pub_key_prefix); } + bool isSessionKeyRemovedFromPool(const uint8_t* pub_key_prefix) { return session_keys.isRemoved(pub_key_prefix); } + void clearSessionKeysRemoved() { session_keys.clearRemoved(); } // Mesh overrides void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override; diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 1abfa9fa..f3cda385 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -214,7 +214,7 @@ bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8 c = getClient(pubkey, key_len); if (c == NULL) return false; // partial pubkey not found - session_keys.remove(c->id.pub_key); // also remove session key if any + removeSessionKey(c->id.pub_key); // also remove session key if any num_clients--; // delete from contacts[] int i = c - clients; @@ -262,7 +262,7 @@ int ClientACL::handleSessionKeyInit(const ClientInfo* client, const uint8_t* eph memset(ephemeral_secret, 0, PUB_KEY_SIZE); // 4. Store in pool (dual-decode: new key active, old key still valid) - auto entry = session_keys.allocate(client->id.pub_key); + auto entry = allocateSessionKey(client->id.pub_key); if (!entry) return 0; if (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE) { @@ -283,7 +283,7 @@ int ClientACL::handleSessionKeyInit(const ClientInfo* client, const uint8_t* eph } const uint8_t* ClientACL::getSessionKey(const uint8_t* pub_key) { - auto entry = session_keys.findByPrefix(pub_key); + auto entry = findSessionKey(pub_key); if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE)) { return entry->session_key; } @@ -291,7 +291,7 @@ const uint8_t* ClientACL::getSessionKey(const uint8_t* pub_key) { } const uint8_t* ClientACL::getPrevSessionKey(const uint8_t* pub_key) { - auto entry = session_keys.findByPrefix(pub_key); + auto entry = findSessionKey(pub_key); if (entry && entry->state == SESSION_STATE_DUAL_DECODE) { return entry->prev_session_key; } @@ -299,7 +299,7 @@ const uint8_t* ClientACL::getPrevSessionKey(const uint8_t* pub_key) { } const uint8_t* ClientACL::getEncryptionKey(const ClientInfo& client) { - auto entry = session_keys.findByPrefix(client.id.pub_key); + auto entry = findSessionKey(client.id.pub_key); if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE) && entry->sends_since_last_recv < SESSION_KEY_STALE_THRESHOLD && entry->nonce < 65535) { @@ -309,7 +309,7 @@ const uint8_t* ClientACL::getEncryptionKey(const ClientInfo& client) { } uint16_t ClientACL::getEncryptionNonce(const ClientInfo& client) { - auto entry = session_keys.findByPrefix(client.id.pub_key); + auto entry = findSessionKey(client.id.pub_key); if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE) && entry->sends_since_last_recv < SESSION_KEY_STALE_THRESHOLD && entry->nonce < 65535) { @@ -325,7 +325,7 @@ uint16_t ClientACL::getEncryptionNonce(const ClientInfo& client) { int idx = &client - clients; if (idx >= 0 && idx < num_clients) clients[idx].flags &= ~CONTACT_FLAG_AEAD; - session_keys.remove(client.id.pub_key); + removeSessionKey(client.id.pub_key); saveSessionKeys(); return 0; // ECB } @@ -337,7 +337,7 @@ uint16_t ClientACL::getEncryptionNonce(const ClientInfo& client) { } void ClientACL::onSessionConfirmed(const uint8_t* pub_key) { - auto entry = session_keys.findByPrefix(pub_key); + auto entry = findSessionKey(pub_key); if (entry) { if (entry->state == SESSION_STATE_DUAL_DECODE) { memset(entry->prev_session_key, 0, SESSION_KEY_SIZE); @@ -386,46 +386,160 @@ uint16_t ClientACL::peerEncryptionNonce(int peer_idx, const int* matching_indexe return c ? getEncryptionNonce(*c) : 0; } -void ClientACL::loadSessionKeys() { - if (!_fs) return; -#if defined(RP2040_PLATFORM) - File file = _fs->open("/s_sess_keys", "r"); -#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - File file = _fs->open("/s_sess_keys", FILE_O_READ); +// --- Flash-backed session key wrappers --- + +static File openReadACL(FILESYSTEM* fs, const char* filename) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + return fs->open(filename, FILE_O_READ); +#elif defined(RP2040_PLATFORM) + return fs->open(filename, "r"); #else - File file = _fs->open("/s_sess_keys", "r", false); + return fs->open(filename, "r", false); #endif - if (file) { - uint8_t rec[71]; // [pub_prefix:4][flags:1][nonce:2][session_key:32][prev_session_key:32] - while (file.read(rec, 71) == 71) { - uint8_t flags = rec[4]; - uint16_t nonce; - memcpy(&nonce, &rec[5], 2); - session_keys.applyLoaded(rec, flags, nonce, &rec[7], &rec[39]); +} + +bool ClientACL::loadSessionKeyRecordFromFlash(const uint8_t* prefix, + uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) { + if (!_fs) return false; + File f = openReadACL(_fs, "/s_sess_keys"); + if (!f) return false; + while (true) { + uint8_t rec[SESSION_KEY_RECORD_MIN_SIZE]; + if (f.read(rec, SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break; + uint8_t rec_flags = rec[4]; + bool has_prev = (rec_flags & SESSION_FLAG_PREV_VALID); + if (memcmp(rec, prefix, 4) == 0) { + *flags = rec_flags; + memcpy(nonce, &rec[5], 2); + memcpy(session_key, &rec[7], SESSION_KEY_SIZE); + if (has_prev) { + if (f.read(prev_session_key, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break; + } else { + memset(prev_session_key, 0, SESSION_KEY_SIZE); + } + f.close(); + return true; } - file.close(); + // Skip prev_key if present + if (has_prev) { + uint8_t skip[SESSION_KEY_SIZE]; + if (f.read(skip, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break; + } + } + f.close(); + return false; +} + +SessionKeyEntry* ClientACL::findSessionKey(const uint8_t* pub_key) { + auto entry = session_keys.findByPrefix(pub_key); + if (entry) return entry; + + // Cache miss — try flash + uint8_t flags; uint16_t nonce; + uint8_t sk[SESSION_KEY_SIZE], psk[SESSION_KEY_SIZE]; + if (!loadSessionKeyRecordFromFlash(pub_key, &flags, &nonce, sk, psk)) return nullptr; + + // Save dirty evictee before overwriting + if (session_keys.isFull() && _session_keys_dirty) { + saveSessionKeys(); + } + session_keys.applyLoaded(pub_key, flags, nonce, sk, psk); + return session_keys.findByPrefix(pub_key); +} + +SessionKeyEntry* ClientACL::allocateSessionKey(const uint8_t* pub_key) { + auto entry = findSessionKey(pub_key); + if (entry) return entry; + + // Not found anywhere — save dirty evictee before allocating + if (session_keys.isFull() && _session_keys_dirty) { + saveSessionKeys(); + } + return session_keys.allocate(pub_key); +} + +void ClientACL::removeSessionKey(const uint8_t* pub_key) { + session_keys.remove(pub_key); +} + +void ClientACL::loadSessionKeys() { + if (!_fs) return; + File file = openReadACL(_fs, "/s_sess_keys"); + if (!file) return; + while (true) { + uint8_t rec[SESSION_KEY_RECORD_MIN_SIZE]; + if (file.read(rec, SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break; + uint8_t flags = rec[4]; + uint16_t nonce; + memcpy(&nonce, &rec[5], 2); + uint8_t prev_key[SESSION_KEY_SIZE]; + if (flags & SESSION_FLAG_PREV_VALID) { + if (file.read(prev_key, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break; + } else { + memset(prev_key, 0, SESSION_KEY_SIZE); + } + session_keys.applyLoaded(rec, flags, nonce, &rec[7], prev_key); } + file.close(); } void ClientACL::saveSessionKeys() { - _session_keys_dirty = false; if (!_fs) return; - File file = openWrite(_fs, "/s_sess_keys"); - if (file) { - for (int i = 0; i < session_keys.getCount(); i++) { - uint8_t pub_key_prefix[4]; - uint8_t flags; - uint16_t nonce; - uint8_t session_key[SESSION_KEY_SIZE]; - uint8_t prev_session_key[SESSION_KEY_SIZE]; - if (session_keys.getEntryForSave(i, pub_key_prefix, &flags, &nonce, session_key, prev_session_key)) { - file.write(pub_key_prefix, 4); - file.write(&flags, 1); - file.write((uint8_t*)&nonce, 2); - file.write(session_key, SESSION_KEY_SIZE); - file.write(prev_session_key, SESSION_KEY_SIZE); + + // 1. Read old flash file into buffer (variable-length records) + uint8_t old_buf[MAX_SESSION_KEYS_FLASH * SESSION_KEY_RECORD_SIZE]; + int old_len = 0; + File rf = openReadACL(_fs, "/s_sess_keys"); + if (rf) { + while (true) { + if (old_len + SESSION_KEY_RECORD_MIN_SIZE > (int)sizeof(old_buf)) break; + if (rf.read(&old_buf[old_len], SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break; + uint8_t flags = old_buf[old_len + 4]; + int rec_len = SESSION_KEY_RECORD_MIN_SIZE; + if (flags & SESSION_FLAG_PREV_VALID) { + if (old_len + SESSION_KEY_RECORD_SIZE > (int)sizeof(old_buf)) break; + if (rf.read(&old_buf[old_len + SESSION_KEY_RECORD_MIN_SIZE], SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break; + rec_len = SESSION_KEY_RECORD_SIZE; } + old_len += rec_len; } - file.close(); + rf.close(); } + + // 2. Write merged file + File wf = openWrite(_fs, "/s_sess_keys"); + if (!wf) return; + + // Write kept old records (variable-length) + int pos = 0; + while (pos + SESSION_KEY_RECORD_MIN_SIZE <= old_len) { + uint8_t* rec = &old_buf[pos]; + uint8_t flags = rec[4]; + int rec_len = (flags & SESSION_FLAG_PREV_VALID) ? SESSION_KEY_RECORD_SIZE : SESSION_KEY_RECORD_MIN_SIZE; + if (pos + rec_len > old_len) break; + if (!session_keys.hasPrefix(rec) && !session_keys.isRemoved(rec)) { + wf.write(rec, rec_len); + } + pos += rec_len; + } + // Write current RAM entries (variable-length) + for (int i = 0; i < session_keys.getCount(); i++) { + uint8_t pub_key_prefix[4]; + uint8_t flags; + uint16_t nonce; + uint8_t session_key[SESSION_KEY_SIZE]; + uint8_t prev_session_key[SESSION_KEY_SIZE]; + if (session_keys.getEntryForSave(i, pub_key_prefix, &flags, &nonce, session_key, prev_session_key)) { + wf.write(pub_key_prefix, 4); + wf.write(&flags, 1); + wf.write((uint8_t*)&nonce, 2); + wf.write(session_key, SESSION_KEY_SIZE); + if (flags & SESSION_FLAG_PREV_VALID) { + wf.write(prev_session_key, SESSION_KEY_SIZE); + } + } + } + wf.close(); + _session_keys_dirty = false; + session_keys.clearRemoved(); } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 455c7cce..b47e1833 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -105,6 +105,16 @@ public: void loadSessionKeys(); void saveSessionKeys(); +private: + // Flash-backed session key wrappers + SessionKeyEntry* findSessionKey(const uint8_t* pub_key); + SessionKeyEntry* allocateSessionKey(const uint8_t* pub_key); + void removeSessionKey(const uint8_t* pub_key); + bool loadSessionKeyRecordFromFlash(const uint8_t* pub_key_prefix, + uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key); + +public: + // Peer-index forwarding helpers for server-side Mesh overrides. // These resolve peer_idx → ClientInfo via matching_indexes[], then delegate // to the corresponding method above. Eliminates repeated boilerplate in diff --git a/src/helpers/SessionKeyPool.h b/src/helpers/SessionKeyPool.h index b2c168a6..d1f100fc 100644 --- a/src/helpers/SessionKeyPool.h +++ b/src/helpers/SessionKeyPool.h @@ -8,7 +8,6 @@ #define SESSION_STATE_DUAL_DECODE 2 // responder: new key active, old key still valid #define SESSION_STATE_ACTIVE 3 // session key confirmed and in use -#define SESSION_FLAG_PREV_VALID 0x01 // prev_session_key is valid for dual-decode struct SessionKeyEntry { uint8_t peer_pub_prefix[4]; // first 4 bytes of peer's public key @@ -21,55 +20,85 @@ struct SessionKeyEntry { unsigned long timeout_at; // millis timestamp for INIT timeout uint8_t ephemeral_prv[PRV_KEY_SIZE]; // initiator-only: ephemeral private key (zeroed after use) uint8_t ephemeral_pub[PUB_KEY_SIZE]; // initiator-only: ephemeral public key + uint32_t last_used; // LRU counter (higher = more recent) }; class SessionKeyPool { - SessionKeyEntry entries[MAX_SESSION_KEYS]; + SessionKeyEntry entries[MAX_SESSION_KEYS_RAM]; int count; + uint32_t lru_counter; + + // Track prefixes removed since last save, so merge-save doesn't resurrect them + uint8_t removed_prefixes[MAX_SESSION_KEYS_RAM][4]; + int removed_count; + + void touch(SessionKeyEntry* entry) { + entry->last_used = ++lru_counter; + } public: - SessionKeyPool() : count(0) { + SessionKeyPool() : count(0), lru_counter(0), removed_count(0) { memset(entries, 0, sizeof(entries)); + memset(removed_prefixes, 0, sizeof(removed_prefixes)); } + bool isFull() const { return count >= MAX_SESSION_KEYS_RAM; } + SessionKeyEntry* findByPrefix(const uint8_t* pub_key) { for (int i = 0; i < count; i++) { if (memcmp(entries[i].peer_pub_prefix, pub_key, 4) == 0) { + touch(&entries[i]); return &entries[i]; } } return nullptr; } + // Lookup without updating LRU — use during save/merge to avoid perturbing eviction order + bool hasPrefix(const uint8_t* pub_key) const { + for (int i = 0; i < count; i++) { + if (memcmp(entries[i].peer_pub_prefix, pub_key, 4) == 0) return true; + } + return false; + } + SessionKeyEntry* allocate(const uint8_t* pub_key) { // Check if already exists auto existing = findByPrefix(pub_key); if (existing) return existing; - // Find free slot or evict oldest - if (count < MAX_SESSION_KEYS) { + // Find free slot + if (count < MAX_SESSION_KEYS_RAM) { auto e = &entries[count++]; memset(e, 0, sizeof(*e)); memcpy(e->peer_pub_prefix, pub_key, 4); + touch(e); return e; } - // Pool full — evict the entry with state NONE, or the first one - for (int i = 0; i < MAX_SESSION_KEYS; i++) { - if (entries[i].state == SESSION_STATE_NONE) { - memset(&entries[i], 0, sizeof(entries[i])); - memcpy(entries[i].peer_pub_prefix, pub_key, 4); - return &entries[i]; + // Pool full — LRU eviction, skip INIT_SENT entries (ephemeral keys are RAM-only) + int evict_idx = -1; + uint32_t min_used = 0xFFFFFFFF; + for (int i = 0; i < MAX_SESSION_KEYS_RAM; i++) { + if (entries[i].state == SESSION_STATE_INIT_SENT) continue; + if (entries[i].last_used < min_used) { + min_used = entries[i].last_used; + evict_idx = i; } } - // All slots active — evict first entry - memset(&entries[0], 0, sizeof(entries[0])); - memcpy(entries[0].peer_pub_prefix, pub_key, 4); - return &entries[0]; + if (evict_idx < 0) evict_idx = 0; // all INIT_SENT — shouldn't happen, fall back to [0] + memset(&entries[evict_idx], 0, sizeof(entries[evict_idx])); + memcpy(entries[evict_idx].peer_pub_prefix, pub_key, 4); + touch(&entries[evict_idx]); + return &entries[evict_idx]; } void remove(const uint8_t* pub_key) { for (int i = 0; i < count; i++) { if (memcmp(entries[i].peer_pub_prefix, pub_key, 4) == 0) { + // Track removed prefix for merge-save + if (removed_count < MAX_SESSION_KEYS_RAM) { + memcpy(removed_prefixes[removed_count++], entries[i].peer_pub_prefix, 4); + } // Shift remaining entries down count--; for (int j = i; j < count; j++) { @@ -81,11 +110,20 @@ public: } } + bool isRemoved(const uint8_t* pub_key_prefix) const { + for (int i = 0; i < removed_count; i++) { + if (memcmp(removed_prefixes[i], pub_key_prefix, 4) == 0) return true; + } + return false; + } + + void clearRemoved() { removed_count = 0; } + int getCount() const { return count; } SessionKeyEntry* getByIdx(int idx) { return (idx >= 0 && idx < count) ? &entries[idx] : nullptr; } - // Persistence helpers — 71-byte records: [pub_prefix:4][flags:1][nonce:2][session_key:32][prev_session_key:32] - // Returns false when idx is past end + // Persistence helpers — variable-length records: [pub_prefix:4][flags:1][nonce:2][session_key:32][prev_session_key:32 if flags & PREV_VALID] + // Returns false when idx is past end or entry is not persistable bool getEntryForSave(int idx, uint8_t* pub_key_prefix, uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) { if (idx >= count) return false;