Skip to content

Commit 131af5b

Browse files
committed
Allow persisting more session to flash
1 parent 2a8df87 commit 131af5b

10 files changed

Lines changed: 394 additions & 92 deletions

File tree

examples/companion_radio/DataStore.cpp

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -407,37 +407,105 @@ bool DataStore::saveNonces(DataStoreHost* host) {
407407

408408
void DataStore::loadSessionKeys(DataStoreHost* host) {
409409
File file = openRead(_getContactsChannelsFS(), "/sess_keys");
410-
if (file) {
411-
uint8_t rec[71]; // 4-byte pub_key prefix + 1 flags + 2 nonce + 32 session_key + 32 prev_session_key
412-
while (file.read(rec, 71) == 71) {
413-
uint16_t nonce;
414-
memcpy(&nonce, &rec[5], 2);
415-
host->onSessionKeyLoaded(rec, rec[4], nonce, &rec[7], &rec[39]);
410+
if (!file) return;
411+
while (true) {
412+
uint8_t rec[SESSION_KEY_RECORD_MIN_SIZE];
413+
if (file.read(rec, SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break;
414+
uint8_t flags = rec[4];
415+
uint16_t nonce;
416+
memcpy(&nonce, &rec[5], 2);
417+
uint8_t prev_key[SESSION_KEY_SIZE];
418+
if (flags & SESSION_FLAG_PREV_VALID) {
419+
if (file.read(prev_key, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break;
420+
} else {
421+
memset(prev_key, 0, SESSION_KEY_SIZE);
416422
}
417-
file.close();
423+
host->onSessionKeyLoaded(rec, flags, nonce, &rec[7], prev_key);
418424
}
425+
file.close();
419426
}
420427

421428
bool DataStore::saveSessionKeys(DataStoreHost* host) {
422-
File file = openWrite(_getContactsChannelsFS(), "/sess_keys");
423-
if (file) {
424-
uint8_t pub_key_prefix[4];
425-
uint8_t flags;
426-
uint16_t nonce;
427-
uint8_t session_key[32];
428-
uint8_t prev_session_key[32];
429-
for (int idx = 0; idx < MAX_SESSION_KEYS; idx++) {
430-
if (host->getSessionKeyForSave(idx, pub_key_prefix, &flags, &nonce, session_key, prev_session_key)) {
431-
file.write(pub_key_prefix, 4);
432-
file.write(&flags, 1);
433-
file.write((uint8_t*)&nonce, 2);
434-
file.write(session_key, 32);
435-
file.write(prev_session_key, 32);
429+
FILESYSTEM* fs = _getContactsChannelsFS();
430+
431+
// 1. Read old flash file into buffer (variable-length records)
432+
uint8_t old_buf[MAX_SESSION_KEYS_FLASH * SESSION_KEY_RECORD_SIZE];
433+
int old_len = 0;
434+
File rf = openRead(fs, "/sess_keys");
435+
if (rf) {
436+
while (true) {
437+
if (old_len + SESSION_KEY_RECORD_MIN_SIZE > (int)sizeof(old_buf)) break;
438+
if (rf.read(&old_buf[old_len], SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break;
439+
uint8_t flags = old_buf[old_len + 4];
440+
int rec_len = SESSION_KEY_RECORD_MIN_SIZE;
441+
if (flags & SESSION_FLAG_PREV_VALID) {
442+
if (old_len + SESSION_KEY_RECORD_SIZE > (int)sizeof(old_buf)) break;
443+
if (rf.read(&old_buf[old_len + SESSION_KEY_RECORD_MIN_SIZE], SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break;
444+
rec_len = SESSION_KEY_RECORD_SIZE;
436445
}
446+
old_len += rec_len;
447+
}
448+
rf.close();
449+
}
450+
451+
// 2. Write merged file
452+
File wf = openWrite(fs, "/sess_keys");
453+
if (!wf) return false;
454+
455+
// Write kept old records (variable-length)
456+
int pos = 0;
457+
while (pos + SESSION_KEY_RECORD_MIN_SIZE <= old_len) {
458+
uint8_t* rec = &old_buf[pos];
459+
uint8_t flags = rec[4];
460+
int rec_len = (flags & SESSION_FLAG_PREV_VALID) ? SESSION_KEY_RECORD_SIZE : SESSION_KEY_RECORD_MIN_SIZE;
461+
if (pos + rec_len > old_len) break;
462+
if (!host->isSessionKeyInRAM(rec) && !host->isSessionKeyRemoved(rec)) {
463+
wf.write(rec, rec_len);
464+
}
465+
pos += rec_len;
466+
}
467+
// Write current RAM entries (variable-length)
468+
for (int idx = 0; idx < MAX_SESSION_KEYS_RAM; idx++) {
469+
uint8_t pk[4]; uint8_t fl; uint16_t n; uint8_t sk[32]; uint8_t psk[32];
470+
if (!host->getSessionKeyForSave(idx, pk, &fl, &n, sk, psk)) continue;
471+
wf.write(pk, 4); wf.write(&fl, 1); wf.write((uint8_t*)&n, 2);
472+
wf.write(sk, 32);
473+
if (fl & SESSION_FLAG_PREV_VALID) {
474+
wf.write(psk, 32);
475+
}
476+
}
477+
wf.close();
478+
return true;
479+
}
480+
481+
bool DataStore::loadSessionKeyByPrefix(const uint8_t* prefix,
482+
uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) {
483+
File f = openRead(_getContactsChannelsFS(), "/sess_keys");
484+
if (!f) return false;
485+
while (true) {
486+
uint8_t rec[SESSION_KEY_RECORD_MIN_SIZE];
487+
if (f.read(rec, SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break;
488+
uint8_t rec_flags = rec[4];
489+
bool has_prev = (rec_flags & SESSION_FLAG_PREV_VALID);
490+
if (memcmp(rec, prefix, 4) == 0) {
491+
*flags = rec_flags;
492+
memcpy(nonce, &rec[5], 2);
493+
memcpy(session_key, &rec[7], SESSION_KEY_SIZE);
494+
if (has_prev) {
495+
if (f.read(prev_session_key, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break;
496+
} else {
497+
memset(prev_session_key, 0, SESSION_KEY_SIZE);
498+
}
499+
f.close();
500+
return true;
501+
}
502+
// Skip prev_key if present
503+
if (has_prev) {
504+
uint8_t skip[SESSION_KEY_SIZE];
505+
if (f.read(skip, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break;
437506
}
438-
file.close();
439-
return true;
440507
}
508+
f.close();
441509
return false;
442510
}
443511

examples/companion_radio/DataStore.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class DataStoreHost {
1717
const uint8_t* session_key, const uint8_t* prev_session_key) { return false; }
1818
virtual bool getSessionKeyForSave(int idx, uint8_t* pub_key_prefix, uint8_t* flags, uint16_t* nonce,
1919
uint8_t* session_key, uint8_t* prev_session_key) { return false; }
20+
virtual bool isSessionKeyInRAM(const uint8_t* pub_key_prefix) { return false; }
21+
virtual bool isSessionKeyRemoved(const uint8_t* pub_key_prefix) { return false; }
2022
};
2123

2224
class DataStore {
@@ -49,6 +51,8 @@ class DataStore {
4951
bool saveNonces(DataStoreHost* host);
5052
void loadSessionKeys(DataStoreHost* host);
5153
bool saveSessionKeys(DataStoreHost* host);
54+
bool loadSessionKeyByPrefix(const uint8_t* prefix,
55+
uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key);
5256
void migrateToSecondaryFS();
5357
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
5458
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);

examples/companion_radio/MyMesh.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2040,6 +2040,14 @@ void MyMesh::checkSerialInterface() {
20402040
}
20412041
}
20422042

2043+
bool MyMesh::isSessionKeyInRAM(const uint8_t* pub_key_prefix) {
2044+
return isSessionKeyInRAMPool(pub_key_prefix);
2045+
}
2046+
2047+
bool MyMesh::isSessionKeyRemoved(const uint8_t* pub_key_prefix) {
2048+
return isSessionKeyRemovedFromPool(pub_key_prefix);
2049+
}
2050+
20432051
void MyMesh::loop() {
20442052
BaseChatMesh::loop();
20452053

examples/companion_radio/MyMesh.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
164164
uint8_t* session_key, uint8_t* prev_session_key) override {
165165
return getSessionKeyEntry(idx, pub_key_prefix, flags, nonce, session_key, prev_session_key);
166166
}
167+
bool isSessionKeyInRAM(const uint8_t* pub_key_prefix) override;
168+
bool isSessionKeyRemoved(const uint8_t* pub_key_prefix) override;
167169

168170
void clearPendingReqs() {
169171
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
@@ -195,9 +197,16 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
195197
void saveChannels() { _store->saveChannels(this); }
196198
void saveContacts() { _store->saveContacts(this); }
197199
void saveNonces() { if (_store->saveNonces(this)) clearNonceDirty(); }
198-
void saveSessionKeys() { if (_store->saveSessionKeys(this)) clearSessionKeysDirty(); }
200+
void saveSessionKeys() { if (_store->saveSessionKeys(this)) { clearSessionKeysDirty(); clearSessionKeysRemoved(); } }
199201
void onSessionKeysUpdated() override { saveSessionKeys(); }
200202

203+
// Flash-backed session key overrides
204+
bool loadSessionKeyRecordFromFlash(const uint8_t* pub_key_prefix,
205+
uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) override {
206+
return _store->loadSessionKeyByPrefix(pub_key_prefix, flags, nonce, session_key, prev_session_key);
207+
}
208+
void mergeAndSaveSessionKeys() override { saveSessionKeys(); }
209+
201210
DataStore* _store;
202211
NodePrefs _prefs;
203212
uint32_t pending_login;

src/MeshCore.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@
3636
#define NONCE_INITIAL_MAX 50000 // max random nonce seed for new contacts
3737
#define SESSION_KEY_TIMEOUT_MS 180000 // 3 minutes per attempt
3838
#define SESSION_KEY_MAX_RETRIES 3 // attempts per negotiation round
39-
#define MAX_SESSION_KEYS 8 // max concurrent session key entries
39+
#define MAX_SESSION_KEYS_RAM 8 // max concurrent session key entries in RAM (LRU cache)
40+
#define MAX_SESSION_KEYS_FLASH 48 // max entries in flash file
41+
#define SESSION_KEY_RECORD_SIZE 71 // max bytes per record (with prev_key)
42+
#define SESSION_KEY_RECORD_MIN_SIZE 39 // min bytes per record: [pub_prefix:4][flags:1][nonce:2][key:32]
43+
#define SESSION_FLAG_PREV_VALID 0x01 // prev_session_key is valid for dual-decode
4044
#define SESSION_KEY_STALE_THRESHOLD 50 // sends without recv before fallback to static ECDH
4145
#define SESSION_KEY_ECB_THRESHOLD 100 // sends without recv before fallback to ECB
4246
#define SESSION_KEY_ABANDON_THRESHOLD 255 // sends without recv before clearing AEAD + session key

src/helpers/BaseChatMesh.cpp

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,7 @@ bool BaseChatMesh::removeContact(ContactInfo& contact) {
900900
}
901901
if (idx >= num_contacts) return false; // not found
902902

903-
session_keys.remove(contact.id.pub_key); // also remove session key if any
903+
removeSessionKey(contact.id.pub_key); // also remove session key if any
904904

905905
// remove from contacts array and parallel nonce tracking
906906
num_contacts--;
@@ -1015,6 +1015,41 @@ void BaseChatMesh::loop() {
10151015
}
10161016
}
10171017

1018+
// --- Session key flash-backed wrappers ---
1019+
1020+
SessionKeyEntry* BaseChatMesh::findSessionKey(const uint8_t* pub_key) {
1021+
auto entry = session_keys.findByPrefix(pub_key);
1022+
if (entry) return entry;
1023+
1024+
// Cache miss — try flash
1025+
uint8_t flags; uint16_t nonce;
1026+
uint8_t sk[SESSION_KEY_SIZE], psk[SESSION_KEY_SIZE];
1027+
if (!loadSessionKeyRecordFromFlash(pub_key, &flags, &nonce, sk, psk)) return nullptr;
1028+
1029+
// Save dirty evictee before overwriting
1030+
if (session_keys.isFull() && session_keys_dirty) {
1031+
mergeAndSaveSessionKeys();
1032+
}
1033+
session_keys.applyLoaded(pub_key, flags, nonce, sk, psk);
1034+
return session_keys.findByPrefix(pub_key);
1035+
}
1036+
1037+
SessionKeyEntry* BaseChatMesh::allocateSessionKey(const uint8_t* pub_key) {
1038+
// Check RAM and flash first
1039+
auto entry = findSessionKey(pub_key);
1040+
if (entry) return entry;
1041+
1042+
// Not found anywhere — save dirty evictee before allocating
1043+
if (session_keys.isFull() && session_keys_dirty) {
1044+
mergeAndSaveSessionKeys();
1045+
}
1046+
return session_keys.allocate(pub_key);
1047+
}
1048+
1049+
void BaseChatMesh::removeSessionKey(const uint8_t* pub_key) {
1050+
session_keys.remove(pub_key);
1051+
}
1052+
10181053
// --- Session key support (Phase 2 — initiator) ---
10191054

10201055
static bool canUseSessionKey(const SessionKeyEntry* entry) {
@@ -1030,7 +1065,7 @@ static bool canUseSessionKey(const SessionKeyEntry* entry) {
10301065
}
10311066

10321067
const uint8_t* BaseChatMesh::getEncryptionKeyFor(const ContactInfo& contact) {
1033-
auto entry = session_keys.findByPrefix(contact.id.pub_key);
1068+
auto entry = findSessionKey(contact.id.pub_key);
10341069
if (canUseSessionKey(entry)) {
10351070
return entry->session_key;
10361071
}
@@ -1039,7 +1074,7 @@ const uint8_t* BaseChatMesh::getEncryptionKeyFor(const ContactInfo& contact) {
10391074

10401075
uint16_t BaseChatMesh::getEncryptionNonceFor(const ContactInfo& contact) {
10411076
uint16_t nonce = 0;
1042-
auto entry = session_keys.findByPrefix(contact.id.pub_key);
1077+
auto entry = findSessionKey(contact.id.pub_key);
10431078
if (canUseSessionKey(entry)) {
10441079
++entry->nonce;
10451080
if (entry->sends_since_last_recv < 255) entry->sends_since_last_recv++;
@@ -1053,7 +1088,7 @@ uint16_t BaseChatMesh::getEncryptionNonceFor(const ContactInfo& contact) {
10531088
int idx = &contact - contacts;
10541089
if (idx >= 0 && idx < num_contacts)
10551090
contacts[idx].flags &= ~CONTACT_FLAG_AEAD;
1056-
session_keys.remove(contact.id.pub_key);
1091+
removeSessionKey(contact.id.pub_key);
10571092
onSessionKeysUpdated();
10581093
// nonce = 0 (ECB)
10591094
} else if (entry->sends_since_last_recv >= SESSION_KEY_ECB_THRESHOLD) {
@@ -1082,7 +1117,7 @@ bool BaseChatMesh::shouldInitiateSessionKey(const ContactInfo& contact) {
10821117
// Need a known path to send the request
10831118
if (contact.out_path_len < 0) return false;
10841119

1085-
auto entry = session_keys.findByPrefix(contact.id.pub_key);
1120+
auto entry = findSessionKey(contact.id.pub_key);
10861121

10871122
// Don't trigger if negotiation already in progress
10881123
if (entry && entry->state == SESSION_STATE_INIT_SENT) return false;
@@ -1118,7 +1153,7 @@ bool BaseChatMesh::shouldInitiateSessionKey(const ContactInfo& contact) {
11181153
}
11191154

11201155
bool BaseChatMesh::initiateSessionKeyNegotiation(const ContactInfo& contact) {
1121-
auto entry = session_keys.allocate(contact.id.pub_key);
1156+
auto entry = allocateSessionKey(contact.id.pub_key);
11221157
if (!entry) return false;
11231158

11241159
// Don't start a new negotiation if one is already pending
@@ -1154,7 +1189,7 @@ bool BaseChatMesh::handleSessionKeyResponse(ContactInfo& contact, const uint8_t*
11541189
if (len < 5 + PUB_KEY_SIZE) return false;
11551190
if (data[4] != RESP_TYPE_SESSION_KEY_ACCEPT) return false;
11561191

1157-
auto entry = session_keys.findByPrefix(contact.id.pub_key);
1192+
auto entry = findSessionKey(contact.id.pub_key);
11581193
if (!entry || entry->state != SESSION_STATE_INIT_SENT) return false;
11591194

11601195
const uint8_t* ephemeral_pub_B = &data[5];
@@ -1216,7 +1251,7 @@ uint8_t BaseChatMesh::handleIncomingSessionKeyInit(ContactInfo& from, const uint
12161251
memset(ephemeral_secret, 0, PUB_KEY_SIZE);
12171252

12181253
// 4. Store in pool (dual-decode: new key active, old key still valid)
1219-
auto entry = session_keys.allocate(from.id.pub_key);
1254+
auto entry = allocateSessionKey(from.id.pub_key);
12201255
if (!entry) return 0;
12211256

12221257
if (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE) {
@@ -1279,7 +1314,7 @@ void BaseChatMesh::checkSessionKeyTimeouts() {
12791314
const uint8_t* BaseChatMesh::getPeerSessionKey(int peer_idx) {
12801315
int i = matching_peer_indexes[peer_idx];
12811316
if (i >= 0 && i < num_contacts) {
1282-
auto entry = session_keys.findByPrefix(contacts[i].id.pub_key);
1317+
auto entry = findSessionKey(contacts[i].id.pub_key);
12831318
// Also try decode during INIT_SENT renegotiation (nonce > 1 means prior key exists)
12841319
if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE
12851320
|| (entry->state == SESSION_STATE_INIT_SENT && entry->nonce > 1)))
@@ -1291,7 +1326,7 @@ const uint8_t* BaseChatMesh::getPeerSessionKey(int peer_idx) {
12911326
const uint8_t* BaseChatMesh::getPeerPrevSessionKey(int peer_idx) {
12921327
int i = matching_peer_indexes[peer_idx];
12931328
if (i >= 0 && i < num_contacts) {
1294-
auto entry = session_keys.findByPrefix(contacts[i].id.pub_key);
1329+
auto entry = findSessionKey(contacts[i].id.pub_key);
12951330
if (entry && entry->state == SESSION_STATE_DUAL_DECODE)
12961331
return entry->prev_session_key;
12971332
}
@@ -1301,7 +1336,7 @@ const uint8_t* BaseChatMesh::getPeerPrevSessionKey(int peer_idx) {
13011336
void BaseChatMesh::onSessionKeyDecryptSuccess(int peer_idx) {
13021337
int i = matching_peer_indexes[peer_idx];
13031338
if (i >= 0 && i < num_contacts) {
1304-
auto entry = session_keys.findByPrefix(contacts[i].id.pub_key);
1339+
auto entry = findSessionKey(contacts[i].id.pub_key);
13051340
if (entry) {
13061341
bool changed = (entry->state == SESSION_STATE_DUAL_DECODE);
13071342
if (changed) {

src/helpers/BaseChatMesh.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,15 @@ class BaseChatMesh : public mesh::Mesh {
148148

149149
// Session key support (Phase 2 — initiator)
150150
virtual void onSessionKeysUpdated() { session_keys_dirty = true; } // called when session key pool changes; override to persist
151+
virtual bool loadSessionKeyRecordFromFlash(const uint8_t* pub_key_prefix,
152+
uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) { return false; }
153+
virtual void mergeAndSaveSessionKeys() {} // merge RAM + flash, write back
154+
155+
// Wrappers that add flash fallback on cache miss
156+
SessionKeyEntry* findSessionKey(const uint8_t* pub_key);
157+
SessionKeyEntry* allocateSessionKey(const uint8_t* pub_key);
158+
void removeSessionKey(const uint8_t* pub_key);
159+
151160
const uint8_t* getEncryptionKeyFor(const ContactInfo& contact);
152161
uint16_t getEncryptionNonceFor(const ContactInfo& contact);
153162
bool shouldInitiateSessionKey(const ContactInfo& contact);
@@ -164,6 +173,9 @@ class BaseChatMesh : public mesh::Mesh {
164173
int getSessionKeyCount() const { return session_keys.getCount(); }
165174
bool isSessionKeysDirty() const { return session_keys_dirty; }
166175
void clearSessionKeysDirty() { session_keys_dirty = false; }
176+
bool isSessionKeyInRAMPool(const uint8_t* pub_key_prefix) { return session_keys.hasPrefix(pub_key_prefix); }
177+
bool isSessionKeyRemovedFromPool(const uint8_t* pub_key_prefix) { return session_keys.isRemoved(pub_key_prefix); }
178+
void clearSessionKeysRemoved() { session_keys.clearRemoved(); }
167179

168180
// Mesh overrides
169181
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;

0 commit comments

Comments
 (0)