Skip to content

Commit 051b20a

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 f6a3bb6 commit 051b20a

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
@@ -387,6 +387,36 @@ void DataStore::saveChannels(DataStoreHost* host) {
387387
}
388388
}
389389

390+
void DataStore::loadNonces(DataStoreHost* host) {
391+
File file = openRead(_getContactsChannelsFS(), "/nonces");
392+
if (file) {
393+
uint8_t rec[6]; // 4-byte pub_key prefix + 2-byte nonce
394+
while (file.read(rec, 6) == 6) {
395+
uint16_t nonce;
396+
memcpy(&nonce, &rec[4], 2);
397+
host->onNonceLoaded(rec, nonce);
398+
}
399+
file.close();
400+
}
401+
}
402+
403+
bool DataStore::saveNonces(DataStoreHost* host) {
404+
File file = openWrite(_getContactsChannelsFS(), "/nonces");
405+
if (file) {
406+
int idx = 0;
407+
uint8_t pub_key_prefix[4];
408+
uint16_t nonce;
409+
while (host->getNonceForSave(idx, pub_key_prefix, &nonce)) {
410+
file.write(pub_key_prefix, 4);
411+
file.write((uint8_t*)&nonce, 2);
412+
idx++;
413+
}
414+
file.close();
415+
return true;
416+
}
417+
return false;
418+
}
419+
390420
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
391421

392422
#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, bool (*filter)(const ContactInfo& c) = NULL);
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
@@ -866,6 +866,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
866866
next_ack_idx = 0;
867867
sign_data = NULL;
868868
dirty_contacts_expiry = 0;
869+
next_nonce_persist = 0;
869870
memset(advert_paths, 0, sizeof(advert_paths));
870871
memset(send_scope.key, 0, sizeof(send_scope.key));
871872
send_unscoped = false;
@@ -961,6 +962,14 @@ void MyMesh::begin(bool has_display) {
961962
resetContacts();
962963
_store->loadContacts(this);
963964
bootstrapRTCfromContacts();
965+
966+
// Load persisted AEAD nonces and apply boot bump if needed
967+
_store->loadNonces(this);
968+
bool dirty_reset = wasDirtyReset(board);
969+
finalizeNonceLoad(dirty_reset);
970+
if (dirty_reset) saveNonces(); // persist bumped nonces immediately
971+
next_nonce_persist = futureMillis(60000);
972+
964973
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
965974
_store->loadChannels(this);
966975

@@ -1457,6 +1466,7 @@ void MyMesh::handleCmdFrame(size_t len) {
14571466
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
14581467
saveContacts();
14591468
}
1469+
if (isNonceDirty()) saveNonces();
14601470
board.reboot();
14611471
} else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) {
14621472
uint8_t reply[11];
@@ -2174,6 +2184,7 @@ void MyMesh::checkCLIRescueCmd() {
21742184
}
21752185

21762186
} else if (strcmp(cli_command, "reboot") == 0) {
2187+
if (isNonceDirty()) saveNonces();
21772188
board.reboot(); // doesn't return
21782189
} else {
21792190
Serial.println(" Error: unknown command");
@@ -2225,6 +2236,14 @@ void MyMesh::loop() {
22252236
dirty_contacts_expiry = 0;
22262237
}
22272238

2239+
// periodic AEAD nonce persistence
2240+
if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) {
2241+
if (isNonceDirty()) {
2242+
saveNonces();
2243+
}
2244+
next_nonce_persist = futureMillis(60000);
2245+
}
2246+
22282247
#ifdef DISPLAY_CLASS
22292248
if (_ui) _ui->setHasConnection(_serial->isConnected());
22302249
#endif

examples/companion_radio/MyMesh.h

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

163165
void clearPendingReqs() {
164166
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
@@ -203,6 +205,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
203205
// helpers, short-cuts
204206
void saveChannels() { _store->saveChannels(this); }
205207
void saveContacts();
208+
void saveNonces() { if (_store->saveNonces(this)) clearNonceDirty(); }
206209

207210
DataStore* _store;
208211
NodePrefs _prefs;
@@ -225,6 +228,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
225228
uint8_t *sign_data;
226229
uint32_t sign_data_len;
227230
unsigned long dirty_contacts_expiry;
231+
unsigned long next_nonce_persist;
228232

229233
TransportKey send_scope;
230234

examples/simple_repeater/MyMesh.cpp

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ uint8_t MyMesh::getPeerFlags(int peer_idx) {
635635
uint16_t MyMesh::getPeerNextAeadNonce(int peer_idx) {
636636
int i = matching_peer_indexes[peer_idx];
637637
if (i >= 0 && i < acl.getNumClients())
638-
return acl.getClientByIdx(i)->nextAeadNonce();
638+
return acl.nextAeadNonceFor(*acl.getClientByIdx(i));
639639
return 0;
640640
}
641641

@@ -645,7 +645,10 @@ void MyMesh::onPeerAeadDetected(int peer_idx) {
645645
auto c = acl.getClientByIdx(i);
646646
if (!(c->flags & CONTACT_FLAG_AEAD)) {
647647
c->flags |= CONTACT_FLAG_AEAD;
648-
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
648+
if (c->aead_nonce == 0) { // no persisted nonce — seed from RNG to avoid deterministic start
649+
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
650+
if (c->aead_nonce == 0) c->aead_nonce = 1;
651+
}
649652
}
650653
}
651654
}
@@ -693,11 +696,11 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
693696
if (packet->isRouteFlood()) {
694697
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
695698
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
696-
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, client->nextAeadNonce());
699+
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, acl.nextAeadNonceFor(*client));
697700
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
698701
} else {
699702
mesh::Packet *reply =
700-
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, client->nextAeadNonce());
703+
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, acl.nextAeadNonceFor(*client));
701704
if (reply) {
702705
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
703706
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
@@ -758,7 +761,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
758761
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
759762
temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN
760763

761-
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, client->nextAeadNonce());
764+
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, acl.nextAeadNonceFor(*client));
762765
if (reply) {
763766
if (client->out_path_len == OUT_PATH_UNKNOWN) {
764767
sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
@@ -887,6 +890,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
887890
uptime_millis = 0;
888891
next_local_advert = next_flood_advert = 0;
889892
dirty_contacts_expiry = 0;
893+
next_nonce_persist = 0;
890894
set_radio_at = revert_radio_at = 0;
891895
_logging = false;
892896
region_load_active = false;
@@ -956,6 +960,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
956960
// load persisted prefs
957961
_cli.loadPrefs(_fs);
958962
acl.load(_fs, self_id);
963+
acl.setRNG(getRNG());
964+
acl.loadNonces();
965+
bool dirty_reset = wasDirtyReset(board);
966+
acl.finalizeNonceLoad(dirty_reset);
967+
if (dirty_reset) acl.saveNonces(); // persist bumped nonces immediately
968+
next_nonce_persist = futureMillis(60000);
959969
// TODO: key_store.begin();
960970
region_map.load(_fs);
961971

@@ -1326,6 +1336,12 @@ void MyMesh::loop() {
13261336
dirty_contacts_expiry = 0;
13271337
}
13281338

1339+
// persist dirty AEAD nonces
1340+
if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) {
1341+
if (acl.isNonceDirty()) { acl.saveNonces(); }
1342+
next_nonce_persist = futureMillis(60000);
1343+
}
1344+
13291345
// update uptime
13301346
uint32_t now = millis();
13311347
uptime_millis += now - last_millis;

examples/simple_repeater/MyMesh.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
103103
unsigned long pending_discover_until;
104104
bool region_load_active;
105105
unsigned long dirty_contacts_expiry;
106+
unsigned long next_nonce_persist;
106107
#if MAX_NEIGHBOURS
107108
NeighbourInfo neighbours[MAX_NEIGHBOURS];
108109
#endif
@@ -197,6 +198,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
197198
void savePrefs() override {
198199
_cli.savePrefs(_fs);
199200
}
201+
void onBeforeReboot() override {
202+
if (acl.isNonceDirty()) acl.saveNonces();
203+
}
200204

201205
void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
202206

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;
@@ -424,7 +424,7 @@ uint8_t MyMesh::getPeerFlags(int peer_idx) {
424424
uint16_t MyMesh::getPeerNextAeadNonce(int peer_idx) {
425425
int i = matching_peer_indexes[peer_idx];
426426
if (i >= 0 && i < acl.getNumClients())
427-
return acl.getClientByIdx(i)->nextAeadNonce();
427+
return acl.nextAeadNonceFor(*acl.getClientByIdx(i));
428428
return 0;
429429
}
430430

@@ -434,7 +434,10 @@ void MyMesh::onPeerAeadDetected(int peer_idx) {
434434
auto c = acl.getClientByIdx(i);
435435
if (!(c->flags & CONTACT_FLAG_AEAD)) {
436436
c->flags |= CONTACT_FLAG_AEAD;
437-
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
437+
if (c->aead_nonce == 0) { // no persisted nonce — seed from RNG to avoid deterministic start
438+
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
439+
if (c->aead_nonce == 0) c->aead_nonce = 1;
440+
}
438441
}
439442
}
440443
}
@@ -532,7 +535,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
532535
// mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key,
533536
// PUB_KEY_SIZE);
534537

535-
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, client->nextAeadNonce());
538+
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, acl.nextAeadNonceFor(*client));
536539
if (reply) {
537540
if (client->out_path_len == OUT_PATH_UNKNOWN) {
538541
sendFloodReply(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
@@ -589,10 +592,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
589592
if (packet->isRouteFlood()) {
590593
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
591594
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
592-
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, client->nextAeadNonce());
595+
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, acl.nextAeadNonceFor(*client));
593596
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
594597
} else {
595-
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, client->nextAeadNonce());
598+
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, acl.nextAeadNonceFor(*client));
596599
if (reply) {
597600
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
598601
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
@@ -647,6 +650,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
647650
uptime_millis = 0;
648651
next_local_advert = next_flood_advert = 0;
649652
dirty_contacts_expiry = 0;
653+
next_nonce_persist = 0;
650654
_logging = false;
651655
region_load_active = false;
652656
set_radio_at = revert_radio_at = 0;
@@ -702,6 +706,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
702706

703707
acl.load(_fs, self_id);
704708
region_map.load(_fs);
709+
acl.setRNG(getRNG());
710+
acl.loadNonces();
711+
bool dirty_reset = wasDirtyReset(board);
712+
acl.finalizeNonceLoad(dirty_reset);
713+
if (dirty_reset) acl.saveNonces(); // persist bumped nonces immediately
714+
next_nonce_persist = futureMillis(60000);
705715

706716
// establish default-scope
707717
{
@@ -1048,6 +1058,12 @@ void MyMesh::loop() {
10481058
dirty_contacts_expiry = 0;
10491059
}
10501060

1061+
// persist dirty AEAD nonces
1062+
if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) {
1063+
if (acl.isNonceDirty()) { acl.saveNonces(); }
1064+
next_nonce_persist = futureMillis(60000);
1065+
}
1066+
10511067
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
10521068

10531069
// update uptime

examples/simple_room_server/MyMesh.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
101101
ClientACL acl;
102102
CommonCLI _cli;
103103
unsigned long dirty_contacts_expiry;
104+
unsigned long next_nonce_persist;
104105
uint8_t reply_data[MAX_PACKET_PAYLOAD];
105106
unsigned long next_push;
106107
uint16_t _num_posted, _num_post_pushes;
@@ -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 sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
196200

0 commit comments

Comments
 (0)