Skip to content

Commit 4ddc3c3

Browse files
committed
Persist AEAD-4 nonces to flash across reboots
Prevent nonce reuse after reboots by persisting per-peer nonce counters to a dedicated /nonces (companion) or /s_nonces (server) file. On dirty reset (power-on, watchdog, brownout), nonces are bumped by NONCE_BOOT_BUMP (100) to cover any unpersisted messages. Clean wakes (deep sleep, software restart) load nonces as-is. - Add nonce persistence to BaseChatMesh (companion) and ClientACL (server) - Add wasDirtyReset() helper to ArduinoHelpers.h for platform-specific reset reason detection (ESP32/NRF52) - Add onBeforeReboot() callback to CommonCLI for pre-reboot nonce flush - Wire nonce persistence into all firmware variants: companion radio, repeater, room server, and sensor - Only clear dirty flag on successful file write
1 parent 50af9eb commit 4ddc3c3

18 files changed

Lines changed: 339 additions & 34 deletions

File tree

examples/companion_radio/DataStore.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,36 @@ void DataStore::saveChannels(DataStoreHost* host) {
375375
}
376376
}
377377

378+
void DataStore::loadNonces(DataStoreHost* host) {
379+
File file = openRead(_getContactsChannelsFS(), "/nonces");
380+
if (file) {
381+
uint8_t rec[6]; // 4-byte pub_key prefix + 2-byte nonce
382+
while (file.read(rec, 6) == 6) {
383+
uint16_t nonce;
384+
memcpy(&nonce, &rec[4], 2);
385+
host->onNonceLoaded(rec, nonce);
386+
}
387+
file.close();
388+
}
389+
}
390+
391+
bool DataStore::saveNonces(DataStoreHost* host) {
392+
File file = openWrite(_getContactsChannelsFS(), "/nonces");
393+
if (file) {
394+
int idx = 0;
395+
uint8_t pub_key_prefix[4];
396+
uint16_t nonce;
397+
while (host->getNonceForSave(idx, pub_key_prefix, &nonce)) {
398+
file.write(pub_key_prefix, 4);
399+
file.write((uint8_t*)&nonce, 2);
400+
idx++;
401+
}
402+
file.close();
403+
return true;
404+
}
405+
return false;
406+
}
407+
378408
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
379409

380410
#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE)

examples/companion_radio/DataStore.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ class DataStoreHost {
1111
virtual bool getContactForSave(uint32_t idx, ContactInfo& contact) =0;
1212
virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) =0;
1313
virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) =0;
14+
virtual bool onNonceLoaded(const uint8_t* pub_key_prefix, uint16_t nonce) { return false; }
15+
virtual bool getNonceForSave(int idx, uint8_t* pub_key_prefix, uint16_t* nonce) { return false; }
1416
};
1517

1618
class DataStore {
@@ -39,6 +41,8 @@ class DataStore {
3941
void saveContacts(DataStoreHost* host);
4042
void loadChannels(DataStoreHost* host);
4143
void saveChannels(DataStoreHost* host);
44+
void loadNonces(DataStoreHost* host);
45+
bool saveNonces(DataStoreHost* host);
4246
void migrateToSecondaryFS();
4347
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
4448
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);

examples/companion_radio/MyMesh.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
802802
next_ack_idx = 0;
803803
sign_data = NULL;
804804
dirty_contacts_expiry = 0;
805+
next_nonce_persist = 0;
805806
memset(advert_paths, 0, sizeof(advert_paths));
806807
memset(send_scope.key, 0, sizeof(send_scope.key));
807808

@@ -878,6 +879,14 @@ void MyMesh::begin(bool has_display) {
878879
resetContacts();
879880
_store->loadContacts(this);
880881
bootstrapRTCfromContacts();
882+
883+
// Load persisted AEAD nonces and apply boot bump if needed
884+
_store->loadNonces(this);
885+
bool dirty_reset = wasDirtyReset(board);
886+
finalizeNonceLoad(dirty_reset);
887+
if (dirty_reset) saveNonces(); // persist bumped nonces immediately
888+
next_nonce_persist = futureMillis(60000);
889+
881890
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
882891
_store->loadChannels(this);
883892

@@ -1325,6 +1334,7 @@ void MyMesh::handleCmdFrame(size_t len) {
13251334
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
13261335
saveContacts();
13271336
}
1337+
if (isNonceDirty()) saveNonces();
13281338
board.reboot();
13291339
} else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) {
13301340
uint8_t reply[11];
@@ -1976,6 +1986,7 @@ void MyMesh::checkCLIRescueCmd() {
19761986
}
19771987

19781988
} else if (strcmp(cli_command, "reboot") == 0) {
1989+
if (isNonceDirty()) saveNonces();
19791990
board.reboot(); // doesn't return
19801991
} else {
19811992
Serial.println(" Error: unknown command");
@@ -2027,6 +2038,14 @@ void MyMesh::loop() {
20272038
dirty_contacts_expiry = 0;
20282039
}
20292040

2041+
// periodic AEAD nonce persistence
2042+
if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) {
2043+
if (isNonceDirty()) {
2044+
saveNonces();
2045+
}
2046+
next_nonce_persist = futureMillis(60000);
2047+
}
2048+
20302049
#ifdef DISPLAY_CLASS
20312050
if (_ui) _ui->setHasConnection(_serial->isConnected());
20322051
#endif

examples/companion_radio/MyMesh.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
154154
bool getContactForSave(uint32_t idx, ContactInfo& contact) override { return getContactByIdx(idx, contact); }
155155
bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) override { return setChannel(channel_idx, ch); }
156156
bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); }
157+
bool onNonceLoaded(const uint8_t* pub_key_prefix, uint16_t nonce) override { return applyLoadedNonce(pub_key_prefix, nonce); }
158+
bool getNonceForSave(int idx, uint8_t* pub_key_prefix, uint16_t* nonce) override { return getNonceEntry(idx, pub_key_prefix, nonce); }
157159

158160
void clearPendingReqs() {
159161
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
@@ -184,6 +186,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
184186
// helpers, short-cuts
185187
void saveChannels() { _store->saveChannels(this); }
186188
void saveContacts() { _store->saveContacts(this); }
189+
void saveNonces() { if (_store->saveNonces(this)) clearNonceDirty(); }
187190

188191
DataStore* _store;
189192
NodePrefs _prefs;
@@ -205,6 +208,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
205208
uint8_t *sign_data;
206209
uint32_t sign_data_len;
207210
unsigned long dirty_contacts_expiry;
211+
unsigned long next_nonce_persist;
208212

209213
TransportKey send_scope;
210214

examples/simple_repeater/MyMesh.cpp

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ uint8_t MyMesh::getPeerFlags(int peer_idx) {
589589
uint16_t MyMesh::getPeerNextAeadNonce(int peer_idx) {
590590
int i = matching_peer_indexes[peer_idx];
591591
if (i >= 0 && i < acl.getNumClients())
592-
return acl.getClientByIdx(i)->nextAeadNonce();
592+
return acl.nextAeadNonceFor(*acl.getClientByIdx(i));
593593
return 0;
594594
}
595595

@@ -599,7 +599,10 @@ void MyMesh::onPeerAeadDetected(int peer_idx) {
599599
auto c = acl.getClientByIdx(i);
600600
if (!(c->flags & CONTACT_FLAG_AEAD)) {
601601
c->flags |= CONTACT_FLAG_AEAD;
602-
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
602+
if (c->aead_nonce == 0) { // no persisted nonce — seed from RNG to avoid deterministic start
603+
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
604+
if (c->aead_nonce == 0) c->aead_nonce = 1;
605+
}
603606
}
604607
}
605608
}
@@ -647,11 +650,11 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
647650
if (packet->isRouteFlood()) {
648651
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
649652
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
650-
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, client->nextAeadNonce());
653+
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, acl.nextAeadNonceFor(*client));
651654
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
652655
} else {
653656
mesh::Packet *reply =
654-
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, client->nextAeadNonce());
657+
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, acl.nextAeadNonceFor(*client));
655658
if (reply) {
656659
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
657660
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
@@ -712,7 +715,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
712715
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
713716
temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN
714717

715-
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, client->nextAeadNonce());
718+
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, acl.nextAeadNonceFor(*client));
716719
if (reply) {
717720
if (client->out_path_len == OUT_PATH_UNKNOWN) {
718721
sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
@@ -839,6 +842,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
839842
uptime_millis = 0;
840843
next_local_advert = next_flood_advert = 0;
841844
dirty_contacts_expiry = 0;
845+
next_nonce_persist = 0;
842846
set_radio_at = revert_radio_at = 0;
843847
_logging = false;
844848
region_load_active = false;
@@ -893,6 +897,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
893897
// load persisted prefs
894898
_cli.loadPrefs(_fs);
895899
acl.load(_fs, self_id);
900+
acl.setRNG(getRNG());
901+
acl.loadNonces();
902+
bool dirty_reset = wasDirtyReset(board);
903+
acl.finalizeNonceLoad(dirty_reset);
904+
if (dirty_reset) acl.saveNonces(); // persist bumped nonces immediately
905+
next_nonce_persist = futureMillis(60000);
896906
// TODO: key_store.begin();
897907
region_map.load(_fs);
898908

@@ -1304,6 +1314,12 @@ void MyMesh::loop() {
13041314
dirty_contacts_expiry = 0;
13051315
}
13061316

1317+
// persist dirty AEAD nonces
1318+
if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) {
1319+
if (acl.isNonceDirty()) { acl.saveNonces(); }
1320+
next_nonce_persist = futureMillis(60000);
1321+
}
1322+
13071323
// update uptime
13081324
uint32_t now = millis();
13091325
uptime_millis += now - last_millis;

examples/simple_repeater/MyMesh.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
102102
unsigned long pending_discover_until;
103103
bool region_load_active;
104104
unsigned long dirty_contacts_expiry;
105+
unsigned long next_nonce_persist;
105106
#if MAX_NEIGHBOURS
106107
NeighbourInfo neighbours[MAX_NEIGHBOURS];
107108
#endif
@@ -191,6 +192,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
191192
void savePrefs() override {
192193
_cli.savePrefs(_fs);
193194
}
195+
void onBeforeReboot() override {
196+
if (acl.isNonceDirty()) acl.saveNonces();
197+
}
194198

195199
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
196200
bool formatFileSystem() override;

examples/simple_room_server/MyMesh.cpp

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) {
7171
mesh::Utils::sha256((uint8_t *)&client->extra.room.pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE);
7272
client->extra.room.push_post_timestamp = post.post_timestamp;
7373

74-
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len, client->nextAeadNonce());
74+
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len, acl.nextAeadNonceFor(*client));
7575
if (reply) {
7676
if (client->out_path_len == OUT_PATH_UNKNOWN) {
7777
unsigned long delay_millis = 0;
@@ -399,7 +399,7 @@ uint8_t MyMesh::getPeerFlags(int peer_idx) {
399399
uint16_t MyMesh::getPeerNextAeadNonce(int peer_idx) {
400400
int i = matching_peer_indexes[peer_idx];
401401
if (i >= 0 && i < acl.getNumClients())
402-
return acl.getClientByIdx(i)->nextAeadNonce();
402+
return acl.nextAeadNonceFor(*acl.getClientByIdx(i));
403403
return 0;
404404
}
405405

@@ -409,7 +409,10 @@ void MyMesh::onPeerAeadDetected(int peer_idx) {
409409
auto c = acl.getClientByIdx(i);
410410
if (!(c->flags & CONTACT_FLAG_AEAD)) {
411411
c->flags |= CONTACT_FLAG_AEAD;
412-
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
412+
if (c->aead_nonce == 0) { // no persisted nonce — seed from RNG to avoid deterministic start
413+
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
414+
if (c->aead_nonce == 0) c->aead_nonce = 1;
415+
}
413416
}
414417
}
415418
}
@@ -507,7 +510,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
507510
// mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key,
508511
// PUB_KEY_SIZE);
509512

510-
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, client->nextAeadNonce());
513+
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, acl.nextAeadNonceFor(*client));
511514
if (reply) {
512515
if (client->out_path_len == OUT_PATH_UNKNOWN) {
513516
sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
@@ -564,10 +567,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
564567
if (packet->isRouteFlood()) {
565568
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
566569
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
567-
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, client->nextAeadNonce());
570+
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, acl.nextAeadNonceFor(*client));
568571
if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
569572
} else {
570-
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, client->nextAeadNonce());
573+
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, acl.nextAeadNonceFor(*client));
571574
if (reply) {
572575
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
573576
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
@@ -619,6 +622,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
619622
uptime_millis = 0;
620623
next_local_advert = next_flood_advert = 0;
621624
dirty_contacts_expiry = 0;
625+
next_nonce_persist = 0;
622626
_logging = false;
623627
set_radio_at = revert_radio_at = 0;
624628

@@ -665,6 +669,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
665669
_cli.loadPrefs(_fs);
666670

667671
acl.load(_fs, self_id);
672+
acl.setRNG(getRNG());
673+
acl.loadNonces();
674+
bool dirty_reset = wasDirtyReset(board);
675+
acl.finalizeNonceLoad(dirty_reset);
676+
if (dirty_reset) acl.saveNonces(); // persist bumped nonces immediately
677+
next_nonce_persist = futureMillis(60000);
668678

669679
radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
670680
radio_set_tx_power(_prefs.tx_power_dbm);
@@ -912,6 +922,12 @@ void MyMesh::loop() {
912922
dirty_contacts_expiry = 0;
913923
}
914924

925+
// persist dirty AEAD nonces
926+
if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) {
927+
if (acl.isNonceDirty()) { acl.saveNonces(); }
928+
next_nonce_persist = futureMillis(60000);
929+
}
930+
915931
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
916932

917933
// update uptime

examples/simple_room_server/MyMesh.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
9797
ClientACL acl;
9898
CommonCLI _cli;
9999
unsigned long dirty_contacts_expiry;
100+
unsigned long next_nonce_persist;
100101
uint8_t reply_data[MAX_PACKET_PAYLOAD];
101102
unsigned long next_push;
102103
uint16_t _num_posted, _num_post_pushes;
@@ -177,6 +178,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
177178
void savePrefs() override {
178179
_cli.savePrefs(_fs);
179180
}
181+
void onBeforeReboot() override {
182+
if (acl.isNonceDirty()) acl.saveNonces();
183+
}
180184

181185
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) override;
182186
bool formatFileSystem() override;

0 commit comments

Comments
 (0)