Skip to content

Commit d1c79eb

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 2fd3867 commit d1c79eb

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
@@ -383,6 +383,36 @@ void DataStore::saveChannels(DataStoreHost* host) {
383383
}
384384
}
385385

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

388418
#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
@@ -863,6 +863,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
863863
next_ack_idx = 0;
864864
sign_data = NULL;
865865
dirty_contacts_expiry = 0;
866+
next_nonce_persist = 0;
866867
memset(advert_paths, 0, sizeof(advert_paths));
867868
memset(send_scope.key, 0, sizeof(send_scope.key));
868869
send_unscoped = false;
@@ -958,6 +959,14 @@ void MyMesh::begin(bool has_display) {
958959
resetContacts();
959960
_store->loadContacts(this);
960961
bootstrapRTCfromContacts();
962+
963+
// Load persisted AEAD nonces and apply boot bump if needed
964+
_store->loadNonces(this);
965+
bool dirty_reset = wasDirtyReset(board);
966+
finalizeNonceLoad(dirty_reset);
967+
if (dirty_reset) saveNonces(); // persist bumped nonces immediately
968+
next_nonce_persist = futureMillis(60000);
969+
961970
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
962971
_store->loadChannels(this);
963972

@@ -1454,6 +1463,7 @@ void MyMesh::handleCmdFrame(size_t len) {
14541463
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
14551464
saveContacts();
14561465
}
1466+
if (isNonceDirty()) saveNonces();
14571467
board.reboot();
14581468
} else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) {
14591469
uint8_t reply[11];
@@ -2152,6 +2162,7 @@ void MyMesh::checkCLIRescueCmd() {
21522162
}
21532163

21542164
} else if (strcmp(cli_command, "reboot") == 0) {
2165+
if (isNonceDirty()) saveNonces();
21552166
board.reboot(); // doesn't return
21562167
} else {
21572168
Serial.println(" Error: unknown command");
@@ -2203,6 +2214,14 @@ void MyMesh::loop() {
22032214
dirty_contacts_expiry = 0;
22042215
}
22052216

2217+
// periodic AEAD nonce persistence
2218+
if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) {
2219+
if (isNonceDirty()) {
2220+
saveNonces();
2221+
}
2222+
next_nonce_persist = futureMillis(60000);
2223+
}
2224+
22062225
#ifdef DISPLAY_CLASS
22072226
if (_ui) _ui->setHasConnection(_serial->isConnected());
22082227
#endif

examples/companion_radio/MyMesh.h

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

162164
void clearPendingReqs() {
163165
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
@@ -199,6 +201,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
199201
// helpers, short-cuts
200202
void saveChannels() { _store->saveChannels(this); }
201203
void saveContacts() { _store->saveContacts(this); }
204+
void saveNonces() { if (_store->saveNonces(this)) clearNonceDirty(); }
202205

203206
DataStore* _store;
204207
NodePrefs _prefs;
@@ -221,6 +224,7 @@ class MyMesh : public BaseChatMesh, public DataStoreHost {
221224
uint8_t *sign_data;
222225
uint32_t sign_data_len;
223226
unsigned long dirty_contacts_expiry;
227+
unsigned long next_nonce_persist;
224228

225229
TransportKey send_scope;
226230

examples/simple_repeater/MyMesh.cpp

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

@@ -643,7 +643,10 @@ void MyMesh::onPeerAeadDetected(int peer_idx) {
643643
auto c = acl.getClientByIdx(i);
644644
if (!(c->flags & CONTACT_FLAG_AEAD)) {
645645
c->flags |= CONTACT_FLAG_AEAD;
646-
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
646+
if (c->aead_nonce == 0) { // no persisted nonce — seed from RNG to avoid deterministic start
647+
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
648+
if (c->aead_nonce == 0) c->aead_nonce = 1;
649+
}
647650
}
648651
}
649652
}
@@ -691,11 +694,11 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
691694
if (packet->isRouteFlood()) {
692695
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
693696
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
694-
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, client->nextAeadNonce());
697+
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, acl.nextAeadNonceFor(*client));
695698
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
696699
} else {
697700
mesh::Packet *reply =
698-
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, client->nextAeadNonce());
701+
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, acl.nextAeadNonceFor(*client));
699702
if (reply) {
700703
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
701704
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
@@ -756,7 +759,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
756759
memcpy(temp, &timestamp, 4); // mostly an extra blob to help make packet_hash unique
757760
temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN
758761

759-
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, client->nextAeadNonce());
762+
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, acl.nextAeadNonceFor(*client));
760763
if (reply) {
761764
if (client->out_path_len == OUT_PATH_UNKNOWN) {
762765
sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize());
@@ -885,6 +888,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
885888
uptime_millis = 0;
886889
next_local_advert = next_flood_advert = 0;
887890
dirty_contacts_expiry = 0;
891+
next_nonce_persist = 0;
888892
set_radio_at = revert_radio_at = 0;
889893
_logging = false;
890894
region_load_active = false;
@@ -949,6 +953,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
949953
// load persisted prefs
950954
_cli.loadPrefs(_fs);
951955
acl.load(_fs, self_id);
956+
acl.setRNG(getRNG());
957+
acl.loadNonces();
958+
bool dirty_reset = wasDirtyReset(board);
959+
acl.finalizeNonceLoad(dirty_reset);
960+
if (dirty_reset) acl.saveNonces(); // persist bumped nonces immediately
961+
next_nonce_persist = futureMillis(60000);
952962
// TODO: key_store.begin();
953963
region_map.load(_fs);
954964

@@ -1320,6 +1330,12 @@ void MyMesh::loop() {
13201330
dirty_contacts_expiry = 0;
13211331
}
13221332

1333+
// persist dirty AEAD nonces
1334+
if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) {
1335+
if (acl.isNonceDirty()) { acl.saveNonces(); }
1336+
next_nonce_persist = futureMillis(60000);
1337+
}
1338+
13231339
// update uptime
13241340
uint32_t now = millis();
13251341
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
@@ -194,6 +195,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
194195
void savePrefs() override {
195196
_cli.savePrefs(_fs);
196197
}
198+
void onBeforeReboot() override {
199+
if (acl.isNonceDirty()) acl.saveNonces();
200+
}
197201

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

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

@@ -432,7 +432,10 @@ void MyMesh::onPeerAeadDetected(int peer_idx) {
432432
auto c = acl.getClientByIdx(i);
433433
if (!(c->flags & CONTACT_FLAG_AEAD)) {
434434
c->flags |= CONTACT_FLAG_AEAD;
435-
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
435+
if (c->aead_nonce == 0) { // no persisted nonce — seed from RNG to avoid deterministic start
436+
getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce));
437+
if (c->aead_nonce == 0) c->aead_nonce = 1;
438+
}
436439
}
437440
}
438441
}
@@ -530,7 +533,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
530533
// mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key,
531534
// PUB_KEY_SIZE);
532535

533-
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, client->nextAeadNonce());
536+
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, acl.nextAeadNonceFor(*client));
534537
if (reply) {
535538
if (client->out_path_len == OUT_PATH_UNKNOWN) {
536539
sendFloodReply(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize());
@@ -587,10 +590,10 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
587590
if (packet->isRouteFlood()) {
588591
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
589592
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
590-
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, client->nextAeadNonce());
593+
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, acl.nextAeadNonceFor(*client));
591594
if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize());
592595
} else {
593-
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, client->nextAeadNonce());
596+
mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, acl.nextAeadNonceFor(*client));
594597
if (reply) {
595598
if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
596599
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
@@ -645,6 +648,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
645648
uptime_millis = 0;
646649
next_local_advert = next_flood_advert = 0;
647650
dirty_contacts_expiry = 0;
651+
next_nonce_persist = 0;
648652
_logging = false;
649653
region_load_active = false;
650654
set_radio_at = revert_radio_at = 0;
@@ -695,6 +699,12 @@ void MyMesh::begin(FILESYSTEM *fs) {
695699

696700
acl.load(_fs, self_id);
697701
region_map.load(_fs);
702+
acl.setRNG(getRNG());
703+
acl.loadNonces();
704+
bool dirty_reset = wasDirtyReset(board);
705+
acl.finalizeNonceLoad(dirty_reset);
706+
if (dirty_reset) acl.saveNonces(); // persist bumped nonces immediately
707+
next_nonce_persist = futureMillis(60000);
698708

699709
// establish default-scope
700710
{
@@ -1040,6 +1050,12 @@ void MyMesh::loop() {
10401050
dirty_contacts_expiry = 0;
10411051
}
10421052

1053+
// persist dirty AEAD nonces
1054+
if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) {
1055+
if (acl.isNonceDirty()) { acl.saveNonces(); }
1056+
next_nonce_persist = futureMillis(60000);
1057+
}
1058+
10431059
// TODO: periodically check for OLD/inactive entries in known_clients[], and evict
10441060

10451061
// 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;
@@ -188,6 +189,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks {
188189
void savePrefs() override {
189190
_cli.savePrefs(_fs);
190191
}
192+
void onBeforeReboot() override {
193+
if (acl.isNonceDirty()) acl.saveNonces();
194+
}
191195

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

0 commit comments

Comments
 (0)