Browse Source

Allow persisting more session to flash

pull/1677/head
Wessel Nieboer 6 months ago
parent
commit
60eb092ab8
No known key found for this signature in database GPG Key ID: 929C8E45E33B5FD2
  1. 114
      examples/companion_radio/DataStore.cpp
  2. 4
      examples/companion_radio/DataStore.h
  3. 8
      examples/companion_radio/MyMesh.cpp
  4. 11
      examples/companion_radio/MyMesh.h
  5. 6
      src/MeshCore.h
  6. 57
      src/helpers/BaseChatMesh.cpp
  7. 12
      src/helpers/BaseChatMesh.h
  8. 192
      src/helpers/ClientACL.cpp
  9. 10
      src/helpers/ClientACL.h
  10. 72
      src/helpers/SessionKeyPool.h

114
examples/companion_radio/DataStore.cpp

@ -419,37 +419,105 @@ bool DataStore::saveNonces(DataStoreHost* host) {
void DataStore::loadSessionKeys(DataStoreHost* host) { void DataStore::loadSessionKeys(DataStoreHost* host) {
File file = openRead(_getContactsChannelsFS(), "/sess_keys"); File file = openRead(_getContactsChannelsFS(), "/sess_keys");
if (file) { if (!file) return;
uint8_t rec[71]; // 4-byte pub_key prefix + 1 flags + 2 nonce + 32 session_key + 32 prev_session_key while (true) {
while (file.read(rec, 71) == 71) { uint8_t rec[SESSION_KEY_RECORD_MIN_SIZE];
uint16_t nonce; if (file.read(rec, SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break;
memcpy(&nonce, &rec[5], 2); uint8_t flags = rec[4];
host->onSessionKeyLoaded(rec, rec[4], nonce, &rec[7], &rec[39]); 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) { bool DataStore::saveSessionKeys(DataStoreHost* host) {
File file = openWrite(_getContactsChannelsFS(), "/sess_keys"); FILESYSTEM* fs = _getContactsChannelsFS();
if (file) {
uint8_t pub_key_prefix[4]; // 1. Read old flash file into buffer (variable-length records)
uint8_t flags; uint8_t old_buf[MAX_SESSION_KEYS_FLASH * SESSION_KEY_RECORD_SIZE];
uint16_t nonce; int old_len = 0;
uint8_t session_key[32]; File rf = openRead(fs, "/sess_keys");
uint8_t prev_session_key[32]; if (rf) {
for (int idx = 0; idx < MAX_SESSION_KEYS; idx++) { while (true) {
if (host->getSessionKeyForSave(idx, pub_key_prefix, &flags, &nonce, session_key, prev_session_key)) { if (old_len + SESSION_KEY_RECORD_MIN_SIZE > (int)sizeof(old_buf)) break;
file.write(pub_key_prefix, 4); if (rf.read(&old_buf[old_len], SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break;
file.write(&flags, 1); uint8_t flags = old_buf[old_len + 4];
file.write((uint8_t*)&nonce, 2); int rec_len = SESSION_KEY_RECORD_MIN_SIZE;
file.write(session_key, 32); if (flags & SESSION_FLAG_PREV_VALID) {
file.write(prev_session_key, 32); 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; return false;
} }

4
examples/companion_radio/DataStore.h

@ -17,6 +17,8 @@ public:
const uint8_t* session_key, const uint8_t* prev_session_key) { return false; } 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, 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; } 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 { class DataStore {
@ -49,6 +51,8 @@ public:
bool saveNonces(DataStoreHost* host); bool saveNonces(DataStoreHost* host);
void loadSessionKeys(DataStoreHost* host); void loadSessionKeys(DataStoreHost* host);
bool saveSessionKeys(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(); void migrateToSecondaryFS();
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); 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); bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);

8
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() { void MyMesh::loop() {
BaseChatMesh::loop(); BaseChatMesh::loop();

11
examples/companion_radio/MyMesh.h

@ -169,6 +169,8 @@ protected:
uint8_t* session_key, uint8_t* prev_session_key) override { uint8_t* session_key, uint8_t* prev_session_key) override {
return getSessionKeyEntry(idx, pub_key_prefix, flags, nonce, session_key, prev_session_key); 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() { void clearPendingReqs() {
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0; pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
@ -214,9 +216,16 @@ private:
void saveChannels() { _store->saveChannels(this); } void saveChannels() { _store->saveChannels(this); }
void saveContacts(); void saveContacts();
void saveNonces() { if (_store->saveNonces(this)) clearNonceDirty(); } 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(); } 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; DataStore* _store;
NodePrefs _prefs; NodePrefs _prefs;
uint32_t pending_login; uint32_t pending_login;

6
src/MeshCore.h

@ -37,7 +37,11 @@
#define NONCE_INITIAL_MAX 50000 // max random nonce seed for new contacts #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_TIMEOUT_MS 180000 // 3 minutes per attempt
#define SESSION_KEY_MAX_RETRIES 3 // attempts per negotiation round #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_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_ECB_THRESHOLD 100 // sends without recv before fallback to ECB
#define SESSION_KEY_ABANDON_THRESHOLD 255 // sends without recv before clearing AEAD + session key #define SESSION_KEY_ABANDON_THRESHOLD 255 // sends without recv before clearing AEAD + session key

57
src/helpers/BaseChatMesh.cpp

@ -991,7 +991,7 @@ bool BaseChatMesh::removeContact(ContactInfo& contact) {
} }
if (idx >= num_contacts) return false; // not found 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 // remove from contacts array and parallel nonce tracking
num_contacts--; 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) --- // --- Session key support (Phase 2 — initiator) ---
static bool canUseSessionKey(const SessionKeyEntry* entry) { static bool canUseSessionKey(const SessionKeyEntry* entry) {
@ -1121,7 +1156,7 @@ static bool canUseSessionKey(const SessionKeyEntry* entry) {
} }
const uint8_t* BaseChatMesh::getEncryptionKeyFor(const ContactInfo& contact) { 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)) { if (canUseSessionKey(entry)) {
return entry->session_key; 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 BaseChatMesh::getEncryptionNonceFor(const ContactInfo& contact) {
uint16_t nonce = 0; uint16_t nonce = 0;
auto entry = session_keys.findByPrefix(contact.id.pub_key); auto entry = findSessionKey(contact.id.pub_key);
if (canUseSessionKey(entry)) { if (canUseSessionKey(entry)) {
++entry->nonce; ++entry->nonce;
if (entry->sends_since_last_recv < 255) entry->sends_since_last_recv++; 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; int idx = &contact - contacts;
if (idx >= 0 && idx < num_contacts) if (idx >= 0 && idx < num_contacts)
contacts[idx].flags &= ~CONTACT_FLAG_AEAD; contacts[idx].flags &= ~CONTACT_FLAG_AEAD;
session_keys.remove(contact.id.pub_key); removeSessionKey(contact.id.pub_key);
onSessionKeysUpdated(); onSessionKeysUpdated();
// nonce = 0 (ECB) // nonce = 0 (ECB)
} else if (entry->sends_since_last_recv >= SESSION_KEY_ECB_THRESHOLD) { } 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 // Need a known path to send the request
if (contact.out_path_len < 0) return false; 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 // Don't trigger if negotiation already in progress
if (entry && entry->state == SESSION_STATE_INIT_SENT) return false; 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) { 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; if (!entry) return false;
// Don't start a new negotiation if one is already pending // 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 (len < 5 + PUB_KEY_SIZE) return false;
if (data[4] != RESP_TYPE_SESSION_KEY_ACCEPT) 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; if (!entry || entry->state != SESSION_STATE_INIT_SENT) return false;
const uint8_t* ephemeral_pub_B = &data[5]; 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); memset(ephemeral_secret, 0, PUB_KEY_SIZE);
// 4. Store in pool (dual-decode: new key active, old key still valid) // 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) return 0;
if (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE) { 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) { const uint8_t* BaseChatMesh::getPeerSessionKey(int peer_idx) {
int i = matching_peer_indexes[peer_idx]; int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < num_contacts) { 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) // 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 if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE
|| (entry->state == SESSION_STATE_INIT_SENT && entry->nonce > 1))) || (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) { const uint8_t* BaseChatMesh::getPeerPrevSessionKey(int peer_idx) {
int i = matching_peer_indexes[peer_idx]; int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < num_contacts) { 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) if (entry && entry->state == SESSION_STATE_DUAL_DECODE)
return entry->prev_session_key; return entry->prev_session_key;
} }
@ -1392,7 +1427,7 @@ const uint8_t* BaseChatMesh::getPeerPrevSessionKey(int peer_idx) {
void BaseChatMesh::onSessionKeyDecryptSuccess(int peer_idx) { void BaseChatMesh::onSessionKeyDecryptSuccess(int peer_idx) {
int i = matching_peer_indexes[peer_idx]; int i = matching_peer_indexes[peer_idx];
if (i >= 0 && i < num_contacts) { 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) { if (entry) {
bool changed = (entry->state == SESSION_STATE_DUAL_DECODE); bool changed = (entry->state == SESSION_STATE_DUAL_DECODE);
if (changed) { if (changed) {

12
src/helpers/BaseChatMesh.h

@ -159,6 +159,15 @@ protected:
// Session key support (Phase 2 — initiator) // Session key support (Phase 2 — initiator)
virtual void onSessionKeysUpdated() { session_keys_dirty = true; } // called when session key pool changes; override to persist 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); const uint8_t* getEncryptionKeyFor(const ContactInfo& contact);
uint16_t getEncryptionNonceFor(const ContactInfo& contact); uint16_t getEncryptionNonceFor(const ContactInfo& contact);
bool shouldInitiateSessionKey(const ContactInfo& contact); bool shouldInitiateSessionKey(const ContactInfo& contact);
@ -175,6 +184,9 @@ protected:
int getSessionKeyCount() const { return session_keys.getCount(); } int getSessionKeyCount() const { return session_keys.getCount(); }
bool isSessionKeysDirty() const { return session_keys_dirty; } bool isSessionKeysDirty() const { return session_keys_dirty; }
void clearSessionKeysDirty() { session_keys_dirty = false; } 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 // 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; void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;

192
src/helpers/ClientACL.cpp

@ -214,7 +214,7 @@ bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8
c = getClient(pubkey, key_len); c = getClient(pubkey, key_len);
if (c == NULL) return false; // partial pubkey not found 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[] num_clients--; // delete from contacts[]
int i = c - clients; 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); memset(ephemeral_secret, 0, PUB_KEY_SIZE);
// 4. Store in pool (dual-decode: new key active, old key still valid) // 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) return 0;
if (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE) { 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) { 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)) { if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE)) {
return entry->session_key; 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) { 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) { if (entry && entry->state == SESSION_STATE_DUAL_DECODE) {
return entry->prev_session_key; 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) { 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) if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE)
&& entry->sends_since_last_recv < SESSION_KEY_STALE_THRESHOLD && entry->sends_since_last_recv < SESSION_KEY_STALE_THRESHOLD
&& entry->nonce < 65535) { && entry->nonce < 65535) {
@ -309,7 +309,7 @@ const uint8_t* ClientACL::getEncryptionKey(const ClientInfo& client) {
} }
uint16_t ClientACL::getEncryptionNonce(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) if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE)
&& entry->sends_since_last_recv < SESSION_KEY_STALE_THRESHOLD && entry->sends_since_last_recv < SESSION_KEY_STALE_THRESHOLD
&& entry->nonce < 65535) { && entry->nonce < 65535) {
@ -325,7 +325,7 @@ uint16_t ClientACL::getEncryptionNonce(const ClientInfo& client) {
int idx = &client - clients; int idx = &client - clients;
if (idx >= 0 && idx < num_clients) if (idx >= 0 && idx < num_clients)
clients[idx].flags &= ~CONTACT_FLAG_AEAD; clients[idx].flags &= ~CONTACT_FLAG_AEAD;
session_keys.remove(client.id.pub_key); removeSessionKey(client.id.pub_key);
saveSessionKeys(); saveSessionKeys();
return 0; // ECB return 0; // ECB
} }
@ -337,7 +337,7 @@ uint16_t ClientACL::getEncryptionNonce(const ClientInfo& client) {
} }
void ClientACL::onSessionConfirmed(const uint8_t* pub_key) { void ClientACL::onSessionConfirmed(const uint8_t* pub_key) {
auto entry = session_keys.findByPrefix(pub_key); auto entry = findSessionKey(pub_key);
if (entry) { if (entry) {
if (entry->state == SESSION_STATE_DUAL_DECODE) { if (entry->state == SESSION_STATE_DUAL_DECODE) {
memset(entry->prev_session_key, 0, SESSION_KEY_SIZE); 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; return c ? getEncryptionNonce(*c) : 0;
} }
void ClientACL::loadSessionKeys() { // --- Flash-backed session key wrappers ---
if (!_fs) return;
#if defined(RP2040_PLATFORM) static File openReadACL(FILESYSTEM* fs, const char* filename) {
File file = _fs->open("/s_sess_keys", "r"); #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) return fs->open(filename, FILE_O_READ);
File file = _fs->open("/s_sess_keys", FILE_O_READ); #elif defined(RP2040_PLATFORM)
return fs->open(filename, "r");
#else #else
File file = _fs->open("/s_sess_keys", "r", false); return fs->open(filename, "r", false);
#endif #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) { bool ClientACL::loadSessionKeyRecordFromFlash(const uint8_t* prefix,
uint8_t flags = rec[4]; uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) {
uint16_t nonce; if (!_fs) return false;
memcpy(&nonce, &rec[5], 2); File f = openReadACL(_fs, "/s_sess_keys");
session_keys.applyLoaded(rec, flags, nonce, &rec[7], &rec[39]); 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() { void ClientACL::saveSessionKeys() {
_session_keys_dirty = false;
if (!_fs) return; if (!_fs) return;
File file = openWrite(_fs, "/s_sess_keys");
if (file) { // 1. Read old flash file into buffer (variable-length records)
for (int i = 0; i < session_keys.getCount(); i++) { uint8_t old_buf[MAX_SESSION_KEYS_FLASH * SESSION_KEY_RECORD_SIZE];
uint8_t pub_key_prefix[4]; int old_len = 0;
uint8_t flags; File rf = openReadACL(_fs, "/s_sess_keys");
uint16_t nonce; if (rf) {
uint8_t session_key[SESSION_KEY_SIZE]; while (true) {
uint8_t prev_session_key[SESSION_KEY_SIZE]; if (old_len + SESSION_KEY_RECORD_MIN_SIZE > (int)sizeof(old_buf)) break;
if (session_keys.getEntryForSave(i, pub_key_prefix, &flags, &nonce, session_key, prev_session_key)) { if (rf.read(&old_buf[old_len], SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break;
file.write(pub_key_prefix, 4); uint8_t flags = old_buf[old_len + 4];
file.write(&flags, 1); int rec_len = SESSION_KEY_RECORD_MIN_SIZE;
file.write((uint8_t*)&nonce, 2); if (flags & SESSION_FLAG_PREV_VALID) {
file.write(session_key, SESSION_KEY_SIZE); if (old_len + SESSION_KEY_RECORD_SIZE > (int)sizeof(old_buf)) break;
file.write(prev_session_key, SESSION_KEY_SIZE); 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();
} }

10
src/helpers/ClientACL.h

@ -105,6 +105,16 @@ public:
void loadSessionKeys(); void loadSessionKeys();
void saveSessionKeys(); 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. // Peer-index forwarding helpers for server-side Mesh overrides.
// These resolve peer_idx → ClientInfo via matching_indexes[], then delegate // These resolve peer_idx → ClientInfo via matching_indexes[], then delegate
// to the corresponding method above. Eliminates repeated boilerplate in // to the corresponding method above. Eliminates repeated boilerplate in

72
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_DUAL_DECODE 2 // responder: new key active, old key still valid
#define SESSION_STATE_ACTIVE 3 // session key confirmed and in use #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 { struct SessionKeyEntry {
uint8_t peer_pub_prefix[4]; // first 4 bytes of peer's public key 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 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_prv[PRV_KEY_SIZE]; // initiator-only: ephemeral private key (zeroed after use)
uint8_t ephemeral_pub[PUB_KEY_SIZE]; // initiator-only: ephemeral public key uint8_t ephemeral_pub[PUB_KEY_SIZE]; // initiator-only: ephemeral public key
uint32_t last_used; // LRU counter (higher = more recent)
}; };
class SessionKeyPool { class SessionKeyPool {
SessionKeyEntry entries[MAX_SESSION_KEYS]; SessionKeyEntry entries[MAX_SESSION_KEYS_RAM];
int count; 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: public:
SessionKeyPool() : count(0) { SessionKeyPool() : count(0), lru_counter(0), removed_count(0) {
memset(entries, 0, sizeof(entries)); 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) { SessionKeyEntry* findByPrefix(const uint8_t* pub_key) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
if (memcmp(entries[i].peer_pub_prefix, pub_key, 4) == 0) { if (memcmp(entries[i].peer_pub_prefix, pub_key, 4) == 0) {
touch(&entries[i]);
return &entries[i]; return &entries[i];
} }
} }
return nullptr; 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) { SessionKeyEntry* allocate(const uint8_t* pub_key) {
// Check if already exists // Check if already exists
auto existing = findByPrefix(pub_key); auto existing = findByPrefix(pub_key);
if (existing) return existing; if (existing) return existing;
// Find free slot or evict oldest // Find free slot
if (count < MAX_SESSION_KEYS) { if (count < MAX_SESSION_KEYS_RAM) {
auto e = &entries[count++]; auto e = &entries[count++];
memset(e, 0, sizeof(*e)); memset(e, 0, sizeof(*e));
memcpy(e->peer_pub_prefix, pub_key, 4); memcpy(e->peer_pub_prefix, pub_key, 4);
touch(e);
return e; return e;
} }
// Pool full — evict the entry with state NONE, or the first one // Pool full — LRU eviction, skip INIT_SENT entries (ephemeral keys are RAM-only)
for (int i = 0; i < MAX_SESSION_KEYS; i++) { int evict_idx = -1;
if (entries[i].state == SESSION_STATE_NONE) { uint32_t min_used = 0xFFFFFFFF;
memset(&entries[i], 0, sizeof(entries[i])); for (int i = 0; i < MAX_SESSION_KEYS_RAM; i++) {
memcpy(entries[i].peer_pub_prefix, pub_key, 4); if (entries[i].state == SESSION_STATE_INIT_SENT) continue;
return &entries[i]; if (entries[i].last_used < min_used) {
min_used = entries[i].last_used;
evict_idx = i;
} }
} }
// All slots active — evict first entry if (evict_idx < 0) evict_idx = 0; // all INIT_SENT — shouldn't happen, fall back to [0]
memset(&entries[0], 0, sizeof(entries[0])); memset(&entries[evict_idx], 0, sizeof(entries[evict_idx]));
memcpy(entries[0].peer_pub_prefix, pub_key, 4); memcpy(entries[evict_idx].peer_pub_prefix, pub_key, 4);
return &entries[0]; touch(&entries[evict_idx]);
return &entries[evict_idx];
} }
void remove(const uint8_t* pub_key) { void remove(const uint8_t* pub_key) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
if (memcmp(entries[i].peer_pub_prefix, pub_key, 4) == 0) { 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 // Shift remaining entries down
count--; count--;
for (int j = i; j < count; j++) { 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; } int getCount() const { return count; }
SessionKeyEntry* getByIdx(int idx) { return (idx >= 0 && idx < count) ? &entries[idx] : nullptr; } 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] // 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 // 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, bool getEntryForSave(int idx, uint8_t* pub_key_prefix, uint8_t* flags, uint16_t* nonce,
uint8_t* session_key, uint8_t* prev_session_key) { uint8_t* session_key, uint8_t* prev_session_key) {
if (idx >= count) return false; if (idx >= count) return false;

Loading…
Cancel
Save