Skip to content

Commit c0e5000

Browse files
committed
Allow persisting more session to flash
1 parent d9859d1 commit c0e5000

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
@@ -419,37 +419,105 @@ bool DataStore::saveNonces(DataStoreHost* host) {
419419

420420
void DataStore::loadSessionKeys(DataStoreHost* host) {
421421
File file = openRead(_getContactsChannelsFS(), "/sess_keys");
422-
if (file) {
423-
uint8_t rec[71]; // 4-byte pub_key prefix + 1 flags + 2 nonce + 32 session_key + 32 prev_session_key
424-
while (file.read(rec, 71) == 71) {
425-
uint16_t nonce;
426-
memcpy(&nonce, &rec[5], 2);
427-
host->onSessionKeyLoaded(rec, rec[4], nonce, &rec[7], &rec[39]);
422+
if (!file) return;
423+
while (true) {
424+
uint8_t rec[SESSION_KEY_RECORD_MIN_SIZE];
425+
if (file.read(rec, SESSION_KEY_RECORD_MIN_SIZE) != SESSION_KEY_RECORD_MIN_SIZE) break;
426+
uint8_t flags = rec[4];
427+
uint16_t nonce;
428+
memcpy(&nonce, &rec[5], 2);
429+
uint8_t prev_key[SESSION_KEY_SIZE];
430+
if (flags & SESSION_FLAG_PREV_VALID) {
431+
if (file.read(prev_key, SESSION_KEY_SIZE) != SESSION_KEY_SIZE) break;
432+
} else {
433+
memset(prev_key, 0, SESSION_KEY_SIZE);
428434
}
429-
file.close();
435+
host->onSessionKeyLoaded(rec, flags, nonce, &rec[7], prev_key);
430436
}
437+
file.close();
431438
}
432439

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

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
@@ -2238,6 +2238,14 @@ void MyMesh::checkSerialInterface() {
22382238
}
22392239
}
22402240

2241+
bool MyMesh::isSessionKeyInRAM(const uint8_t* pub_key_prefix) {
2242+
return isSessionKeyInRAMPool(pub_key_prefix);
2243+
}
2244+
2245+
bool MyMesh::isSessionKeyRemoved(const uint8_t* pub_key_prefix) {
2246+
return isSessionKeyRemovedFromPool(pub_key_prefix);
2247+
}
2248+
22412249
void MyMesh::loop() {
22422250
BaseChatMesh::loop();
22432251

examples/companion_radio/MyMesh.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
169169
uint8_t* session_key, uint8_t* prev_session_key) override {
170170
return getSessionKeyEntry(idx, pub_key_prefix, flags, nonce, session_key, prev_session_key);
171171
}
172+
bool isSessionKeyInRAM(const uint8_t* pub_key_prefix) override;
173+
bool isSessionKeyRemoved(const uint8_t* pub_key_prefix) override;
172174

173175
void clearPendingReqs() {
174176
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
@@ -214,9 +216,16 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
214216
void saveChannels() { _store->saveChannels(this); }
215217
void saveContacts();
216218
void saveNonces() { if (_store->saveNonces(this)) clearNonceDirty(); }
217-
void saveSessionKeys() { if (_store->saveSessionKeys(this)) clearSessionKeysDirty(); }
219+
void saveSessionKeys() { if (_store->saveSessionKeys(this)) { clearSessionKeysDirty(); clearSessionKeysRemoved(); } }
218220
void onSessionKeysUpdated() override { saveSessionKeys(); }
219221

222+
// Flash-backed session key overrides
223+
bool loadSessionKeyRecordFromFlash(const uint8_t* pub_key_prefix,
224+
uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) override {
225+
return _store->loadSessionKeyByPrefix(pub_key_prefix, flags, nonce, session_key, prev_session_key);
226+
}
227+
void mergeAndSaveSessionKeys() override { saveSessionKeys(); }
228+
220229
DataStore* _store;
221230
NodePrefs _prefs;
222231
uint32_t pending_login;

src/MeshCore.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@
3737
#define NONCE_INITIAL_MAX 50000 // max random nonce seed for new contacts
3838
#define SESSION_KEY_TIMEOUT_MS 180000 // 3 minutes per attempt
3939
#define SESSION_KEY_MAX_RETRIES 3 // attempts per negotiation round
40-
#define MAX_SESSION_KEYS 8 // max concurrent session key entries
40+
#define MAX_SESSION_KEYS_RAM 8 // max concurrent session key entries in RAM (LRU cache)
41+
#define MAX_SESSION_KEYS_FLASH 48 // max entries in flash file
42+
#define SESSION_KEY_RECORD_SIZE 71 // max bytes per record (with prev_key)
43+
#define SESSION_KEY_RECORD_MIN_SIZE 39 // min bytes per record: [pub_prefix:4][flags:1][nonce:2][key:32]
44+
#define SESSION_FLAG_PREV_VALID 0x01 // prev_session_key is valid for dual-decode
4145
#define SESSION_KEY_STALE_THRESHOLD 50 // sends without recv before fallback to static ECDH
4246
#define SESSION_KEY_ECB_THRESHOLD 100 // sends without recv before fallback to ECB
4347
#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
@@ -991,7 +991,7 @@ bool BaseChatMesh::removeContact(ContactInfo& contact) {
991991
}
992992
if (idx >= num_contacts) return false; // not found
993993

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

996996
// remove from contacts array and parallel nonce tracking
997997
num_contacts--;
@@ -1106,6 +1106,41 @@ void BaseChatMesh::loop() {
11061106
}
11071107
}
11081108

1109+
// --- Session key flash-backed wrappers ---
1110+
1111+
SessionKeyEntry* BaseChatMesh::findSessionKey(const uint8_t* pub_key) {
1112+
auto entry = session_keys.findByPrefix(pub_key);
1113+
if (entry) return entry;
1114+
1115+
// Cache miss — try flash
1116+
uint8_t flags; uint16_t nonce;
1117+
uint8_t sk[SESSION_KEY_SIZE], psk[SESSION_KEY_SIZE];
1118+
if (!loadSessionKeyRecordFromFlash(pub_key, &flags, &nonce, sk, psk)) return nullptr;
1119+
1120+
// Save dirty evictee before overwriting
1121+
if (session_keys.isFull() && session_keys_dirty) {
1122+
mergeAndSaveSessionKeys();
1123+
}
1124+
session_keys.applyLoaded(pub_key, flags, nonce, sk, psk);
1125+
return session_keys.findByPrefix(pub_key);
1126+
}
1127+
1128+
SessionKeyEntry* BaseChatMesh::allocateSessionKey(const uint8_t* pub_key) {
1129+
// Check RAM and flash first
1130+
auto entry = findSessionKey(pub_key);
1131+
if (entry) return entry;
1132+
1133+
// Not found anywhere — save dirty evictee before allocating
1134+
if (session_keys.isFull() && session_keys_dirty) {
1135+
mergeAndSaveSessionKeys();
1136+
}
1137+
return session_keys.allocate(pub_key);
1138+
}
1139+
1140+
void BaseChatMesh::removeSessionKey(const uint8_t* pub_key) {
1141+
session_keys.remove(pub_key);
1142+
}
1143+
11091144
// --- Session key support (Phase 2 — initiator) ---
11101145

11111146
static bool canUseSessionKey(const SessionKeyEntry* entry) {
@@ -1121,7 +1156,7 @@ static bool canUseSessionKey(const SessionKeyEntry* entry) {
11211156
}
11221157

11231158
const uint8_t* BaseChatMesh::getEncryptionKeyFor(const ContactInfo& contact) {
1124-
auto entry = session_keys.findByPrefix(contact.id.pub_key);
1159+
auto entry = findSessionKey(contact.id.pub_key);
11251160
if (canUseSessionKey(entry)) {
11261161
return entry->session_key;
11271162
}
@@ -1130,7 +1165,7 @@ const uint8_t* BaseChatMesh::getEncryptionKeyFor(const ContactInfo& contact) {
11301165

11311166
uint16_t BaseChatMesh::getEncryptionNonceFor(const ContactInfo& contact) {
11321167
uint16_t nonce = 0;
1133-
auto entry = session_keys.findByPrefix(contact.id.pub_key);
1168+
auto entry = findSessionKey(contact.id.pub_key);
11341169
if (canUseSessionKey(entry)) {
11351170
++entry->nonce;
11361171
if (entry->sends_since_last_recv < 255) entry->sends_since_last_recv++;
@@ -1144,7 +1179,7 @@ uint16_t BaseChatMesh::getEncryptionNonceFor(const ContactInfo& contact) {
11441179
int idx = &contact - contacts;
11451180
if (idx >= 0 && idx < num_contacts)
11461181
contacts[idx].flags &= ~CONTACT_FLAG_AEAD;
1147-
session_keys.remove(contact.id.pub_key);
1182+
removeSessionKey(contact.id.pub_key);
11481183
onSessionKeysUpdated();
11491184
// nonce = 0 (ECB)
11501185
} else if (entry->sends_since_last_recv >= SESSION_KEY_ECB_THRESHOLD) {
@@ -1173,7 +1208,7 @@ bool BaseChatMesh::shouldInitiateSessionKey(const ContactInfo& contact) {
11731208
// Need a known path to send the request
11741209
if (contact.out_path_len < 0) return false;
11751210

1176-
auto entry = session_keys.findByPrefix(contact.id.pub_key);
1211+
auto entry = findSessionKey(contact.id.pub_key);
11771212

11781213
// Don't trigger if negotiation already in progress
11791214
if (entry && entry->state == SESSION_STATE_INIT_SENT) return false;
@@ -1209,7 +1244,7 @@ bool BaseChatMesh::shouldInitiateSessionKey(const ContactInfo& contact) {
12091244
}
12101245

12111246
bool BaseChatMesh::initiateSessionKeyNegotiation(const ContactInfo& contact) {
1212-
auto entry = session_keys.allocate(contact.id.pub_key);
1247+
auto entry = allocateSessionKey(contact.id.pub_key);
12131248
if (!entry) return false;
12141249

12151250
// Don't start a new negotiation if one is already pending
@@ -1245,7 +1280,7 @@ bool BaseChatMesh::handleSessionKeyResponse(ContactInfo& contact, const uint8_t*
12451280
if (len < 5 + PUB_KEY_SIZE) return false;
12461281
if (data[4] != RESP_TYPE_SESSION_KEY_ACCEPT) return false;
12471282

1248-
auto entry = session_keys.findByPrefix(contact.id.pub_key);
1283+
auto entry = findSessionKey(contact.id.pub_key);
12491284
if (!entry || entry->state != SESSION_STATE_INIT_SENT) return false;
12501285

12511286
const uint8_t* ephemeral_pub_B = &data[5];
@@ -1307,7 +1342,7 @@ uint8_t BaseChatMesh::handleIncomingSessionKeyInit(ContactInfo& from, const uint
13071342
memset(ephemeral_secret, 0, PUB_KEY_SIZE);
13081343

13091344
// 4. Store in pool (dual-decode: new key active, old key still valid)
1310-
auto entry = session_keys.allocate(from.id.pub_key);
1345+
auto entry = allocateSessionKey(from.id.pub_key);
13111346
if (!entry) return 0;
13121347

13131348
if (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE) {
@@ -1370,7 +1405,7 @@ void BaseChatMesh::checkSessionKeyTimeouts() {
13701405
const uint8_t* BaseChatMesh::getPeerSessionKey(int peer_idx) {
13711406
int i = matching_peer_indexes[peer_idx];
13721407
if (i >= 0 && i < num_contacts) {
1373-
auto entry = session_keys.findByPrefix(contacts[i].id.pub_key);
1408+
auto entry = findSessionKey(contacts[i].id.pub_key);
13741409
// Also try decode during INIT_SENT renegotiation (nonce > 1 means prior key exists)
13751410
if (entry && (entry->state == SESSION_STATE_ACTIVE || entry->state == SESSION_STATE_DUAL_DECODE
13761411
|| (entry->state == SESSION_STATE_INIT_SENT && entry->nonce > 1)))
@@ -1382,7 +1417,7 @@ const uint8_t* BaseChatMesh::getPeerSessionKey(int peer_idx) {
13821417
const uint8_t* BaseChatMesh::getPeerPrevSessionKey(int peer_idx) {
13831418
int i = matching_peer_indexes[peer_idx];
13841419
if (i >= 0 && i < num_contacts) {
1385-
auto entry = session_keys.findByPrefix(contacts[i].id.pub_key);
1420+
auto entry = findSessionKey(contacts[i].id.pub_key);
13861421
if (entry && entry->state == SESSION_STATE_DUAL_DECODE)
13871422
return entry->prev_session_key;
13881423
}
@@ -1392,7 +1427,7 @@ const uint8_t* BaseChatMesh::getPeerPrevSessionKey(int peer_idx) {
13921427
void BaseChatMesh::onSessionKeyDecryptSuccess(int peer_idx) {
13931428
int i = matching_peer_indexes[peer_idx];
13941429
if (i >= 0 && i < num_contacts) {
1395-
auto entry = session_keys.findByPrefix(contacts[i].id.pub_key);
1430+
auto entry = findSessionKey(contacts[i].id.pub_key);
13961431
if (entry) {
13971432
bool changed = (entry->state == SESSION_STATE_DUAL_DECODE);
13981433
if (changed) {

src/helpers/BaseChatMesh.h

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

160160
// Session key support (Phase 2 — initiator)
161161
virtual void onSessionKeysUpdated() { session_keys_dirty = true; } // called when session key pool changes; override to persist
162+
virtual bool loadSessionKeyRecordFromFlash(const uint8_t* pub_key_prefix,
163+
uint8_t* flags, uint16_t* nonce, uint8_t* session_key, uint8_t* prev_session_key) { return false; }
164+
virtual void mergeAndSaveSessionKeys() {} // merge RAM + flash, write back
165+
166+
// Wrappers that add flash fallback on cache miss
167+
SessionKeyEntry* findSessionKey(const uint8_t* pub_key);
168+
SessionKeyEntry* allocateSessionKey(const uint8_t* pub_key);
169+
void removeSessionKey(const uint8_t* pub_key);
170+
162171
const uint8_t* getEncryptionKeyFor(const ContactInfo& contact);
163172
uint16_t getEncryptionNonceFor(const ContactInfo& contact);
164173
bool shouldInitiateSessionKey(const ContactInfo& contact);
@@ -175,6 +184,9 @@ class BaseChatMesh : public mesh::Mesh {
175184
int getSessionKeyCount() const { return session_keys.getCount(); }
176185
bool isSessionKeysDirty() const { return session_keys_dirty; }
177186
void clearSessionKeysDirty() { session_keys_dirty = false; }
187+
bool isSessionKeyInRAMPool(const uint8_t* pub_key_prefix) { return session_keys.hasPrefix(pub_key_prefix); }
188+
bool isSessionKeyRemovedFromPool(const uint8_t* pub_key_prefix) { return session_keys.isRemoved(pub_key_prefix); }
189+
void clearSessionKeysRemoved() { session_keys.clearRemoved(); }
178190

179191
// Mesh overrides
180192
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)