From 9ce710ebd167df6b5e2f632096bd7de8fb539dec Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Fri, 13 Feb 2026 15:26:30 +0100 Subject: [PATCH] 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 --- examples/companion_radio/DataStore.cpp | 30 ++++++++++ examples/companion_radio/DataStore.h | 4 ++ examples/companion_radio/MyMesh.cpp | 19 +++++++ examples/companion_radio/MyMesh.h | 4 ++ examples/simple_repeater/MyMesh.cpp | 26 +++++++-- examples/simple_repeater/MyMesh.h | 4 ++ examples/simple_room_server/MyMesh.cpp | 28 +++++++-- examples/simple_room_server/MyMesh.h | 4 ++ examples/simple_sensor/SensorMesh.cpp | 30 +++++++--- examples/simple_sensor/SensorMesh.h | 4 ++ src/MeshCore.h | 4 ++ src/helpers/ArduinoHelpers.h | 15 +++++ src/helpers/BaseChatMesh.cpp | 78 +++++++++++++++++++++----- src/helpers/BaseChatMesh.h | 17 ++++++ src/helpers/ClientACL.cpp | 76 +++++++++++++++++++++++++ src/helpers/ClientACL.h | 24 +++++++- src/helpers/CommonCLI.cpp | 2 + src/helpers/CommonCLI.h | 4 ++ 18 files changed, 339 insertions(+), 34 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 0f3a0f9c5..2ab862f44 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -387,6 +387,36 @@ void DataStore::saveChannels(DataStoreHost* host) { } } +void DataStore::loadNonces(DataStoreHost* host) { + File file = openRead(_getContactsChannelsFS(), "/nonces"); + if (file) { + uint8_t rec[6]; // 4-byte pub_key prefix + 2-byte nonce + while (file.read(rec, 6) == 6) { + uint16_t nonce; + memcpy(&nonce, &rec[4], 2); + host->onNonceLoaded(rec, nonce); + } + file.close(); + } +} + +bool DataStore::saveNonces(DataStoreHost* host) { + File file = openWrite(_getContactsChannelsFS(), "/nonces"); + if (file) { + int idx = 0; + uint8_t pub_key_prefix[4]; + uint16_t nonce; + while (host->getNonceForSave(idx, pub_key_prefix, &nonce)) { + file.write(pub_key_prefix, 4); + file.write((uint8_t*)&nonce, 2); + idx++; + } + file.close(); + return true; + } + return false; +} + #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE) diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h index af5ee7af8..6efb66b95 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -11,6 +11,8 @@ public: virtual bool getContactForSave(uint32_t idx, ContactInfo& contact) =0; virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) =0; virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) =0; + virtual bool onNonceLoaded(const uint8_t* pub_key_prefix, uint16_t nonce) { return false; } + virtual bool getNonceForSave(int idx, uint8_t* pub_key_prefix, uint16_t* nonce) { return false; } }; class DataStore { @@ -39,6 +41,8 @@ public: void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL); void loadChannels(DataStoreHost* host); void saveChannels(DataStoreHost* host); + void loadNonces(DataStoreHost* host); + bool saveNonces(DataStoreHost* host); void migrateToSecondaryFS(); uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a78fd29a2..455507509 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -871,6 +871,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe next_ack_idx = 0; sign_data = NULL; dirty_contacts_expiry = 0; + next_nonce_persist = 0; memset(advert_paths, 0, sizeof(advert_paths)); memset(send_scope.key, 0, sizeof(send_scope.key)); send_unscoped = false; @@ -966,6 +967,14 @@ void MyMesh::begin(bool has_display) { resetContacts(); _store->loadContacts(this); bootstrapRTCfromContacts(); + + // Load persisted AEAD nonces and apply boot bump if needed + _store->loadNonces(this); + bool dirty_reset = wasDirtyReset(board); + finalizeNonceLoad(dirty_reset); + if (dirty_reset) saveNonces(); // persist bumped nonces immediately + next_nonce_persist = futureMillis(60000); + addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel _store->loadChannels(this); @@ -1462,6 +1471,7 @@ void MyMesh::handleCmdFrame(size_t len) { if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed? saveContacts(); } + if (isNonceDirty()) saveNonces(); board.reboot(); } else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) { uint8_t reply[11]; @@ -2184,6 +2194,7 @@ void MyMesh::checkCLIRescueCmd() { } } else if (strcmp(cli_command, "reboot") == 0) { + if (isNonceDirty()) saveNonces(); board.reboot(); // doesn't return } else { Serial.println(" Error: unknown command"); @@ -2235,6 +2246,14 @@ void MyMesh::loop() { dirty_contacts_expiry = 0; } + // periodic AEAD nonce persistence + if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) { + if (isNonceDirty()) { + saveNonces(); + } + next_nonce_persist = futureMillis(60000); + } + #ifdef DISPLAY_CLASS if (_ui) _ui->setHasConnection(_serial->isConnected()); #endif diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f4190f30a..53ac1ee51 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -159,6 +159,8 @@ protected: bool getContactForSave(uint32_t idx, ContactInfo& contact) override { return getContactByIdx(idx, contact); } bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) override { return setChannel(channel_idx, ch); } bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); } + bool onNonceLoaded(const uint8_t* pub_key_prefix, uint16_t nonce) override { return applyLoadedNonce(pub_key_prefix, nonce); } + bool getNonceForSave(int idx, uint8_t* pub_key_prefix, uint16_t* nonce) override { return getNonceEntry(idx, pub_key_prefix, nonce); } void clearPendingReqs() { pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0; @@ -203,6 +205,7 @@ private: // helpers, short-cuts void saveChannels() { _store->saveChannels(this); } void saveContacts(); + void saveNonces() { if (_store->saveNonces(this)) clearNonceDirty(); } DataStore* _store; NodePrefs _prefs; @@ -225,6 +228,7 @@ private: uint8_t *sign_data; uint32_t sign_data_len; unsigned long dirty_contacts_expiry; + unsigned long next_nonce_persist; TransportKey send_scope; diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 483def34b..2e0ec31f3 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -635,7 +635,7 @@ uint8_t MyMesh::getPeerFlags(int peer_idx) { uint16_t MyMesh::getPeerNextAeadNonce(int peer_idx) { int i = matching_peer_indexes[peer_idx]; if (i >= 0 && i < acl.getNumClients()) - return acl.getClientByIdx(i)->nextAeadNonce(); + return acl.nextAeadNonceFor(*acl.getClientByIdx(i)); return 0; } @@ -645,7 +645,10 @@ void MyMesh::onPeerAeadDetected(int peer_idx) { auto c = acl.getClientByIdx(i); if (!(c->flags & CONTACT_FLAG_AEAD)) { c->flags |= CONTACT_FLAG_AEAD; - getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce)); + if (c->aead_nonce == 0) { // no persisted nonce — seed from RNG to avoid deterministic start + getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce)); + if (c->aead_nonce == 0) c->aead_nonce = 1; + } } } } @@ -693,11 +696,11 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, client->nextAeadNonce()); + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, acl.nextAeadNonceFor(*client)); if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet *reply = - createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, client->nextAeadNonce()); + createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, acl.nextAeadNonceFor(*client)); if (reply) { if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT 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, memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique temp[4] = (TXT_TYPE_CLI_DATA << 2); // NOTE: legacy was: TXT_TYPE_PLAIN - auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, client->nextAeadNonce()); + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, acl.nextAeadNonceFor(*client)); if (reply) { if (client->out_path_len == OUT_PATH_UNKNOWN) { sendFloodReply(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); @@ -887,6 +890,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc uptime_millis = 0; next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; + next_nonce_persist = 0; set_radio_at = revert_radio_at = 0; _logging = false; region_load_active = false; @@ -956,6 +960,12 @@ void MyMesh::begin(FILESYSTEM *fs) { // load persisted prefs _cli.loadPrefs(_fs); acl.load(_fs, self_id); + acl.setRNG(getRNG()); + acl.loadNonces(); + bool dirty_reset = wasDirtyReset(board); + acl.finalizeNonceLoad(dirty_reset); + if (dirty_reset) acl.saveNonces(); // persist bumped nonces immediately + next_nonce_persist = futureMillis(60000); // TODO: key_store.begin(); region_map.load(_fs); @@ -1326,6 +1336,12 @@ void MyMesh::loop() { dirty_contacts_expiry = 0; } + // persist dirty AEAD nonces + if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) { + if (acl.isNonceDirty()) { acl.saveNonces(); } + next_nonce_persist = futureMillis(60000); + } + // update uptime uint32_t now = millis(); uptime_millis += now - last_millis; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index d8a6a3801..a83aad00d 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -103,6 +103,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { unsigned long pending_discover_until; bool region_load_active; unsigned long dirty_contacts_expiry; + unsigned long next_nonce_persist; #if MAX_NEIGHBOURS NeighbourInfo neighbours[MAX_NEIGHBOURS]; #endif @@ -197,6 +198,9 @@ public: void savePrefs() override { _cli.savePrefs(_fs); } + void onBeforeReboot() override { + if (acl.isNonceDirty()) acl.saveNonces(); + } void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 352dc8ae0..f478061e4 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -71,7 +71,7 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { mesh::Utils::sha256((uint8_t *)&client->extra.room.pending_ack, 4, reply_data, len, client->id.pub_key, PUB_KEY_SIZE); client->extra.room.push_post_timestamp = post.post_timestamp; - auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len, client->nextAeadNonce()); + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len, acl.nextAeadNonceFor(*client)); if (reply) { if (client->out_path_len == OUT_PATH_UNKNOWN) { unsigned long delay_millis = 0; @@ -424,7 +424,7 @@ uint8_t MyMesh::getPeerFlags(int peer_idx) { uint16_t MyMesh::getPeerNextAeadNonce(int peer_idx) { int i = matching_peer_indexes[peer_idx]; if (i >= 0 && i < acl.getNumClients()) - return acl.getClientByIdx(i)->nextAeadNonce(); + return acl.nextAeadNonceFor(*acl.getClientByIdx(i)); return 0; } @@ -434,7 +434,10 @@ void MyMesh::onPeerAeadDetected(int peer_idx) { auto c = acl.getClientByIdx(i); if (!(c->flags & CONTACT_FLAG_AEAD)) { c->flags |= CONTACT_FLAG_AEAD; - getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce)); + if (c->aead_nonce == 0) { // no persisted nonce — seed from RNG to avoid deterministic start + getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce)); + if (c->aead_nonce == 0) c->aead_nonce = 1; + } } } } @@ -532,7 +535,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, // mesh::Utils::sha256((uint8_t *)&expected_ack_crc, 4, temp, 5 + text_len, self_id.pub_key, // PUB_KEY_SIZE); - auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, client->nextAeadNonce()); + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, acl.nextAeadNonceFor(*client)); if (reply) { if (client->out_path_len == OUT_PATH_UNKNOWN) { 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, if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, client->nextAeadNonce()); + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, acl.nextAeadNonceFor(*client)); if (path) sendFloodReply(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { - mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, client->nextAeadNonce()); + mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, acl.nextAeadNonceFor(*client)); if (reply) { if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT 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 uptime_millis = 0; next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; + next_nonce_persist = 0; _logging = false; region_load_active = false; set_radio_at = revert_radio_at = 0; @@ -702,6 +706,12 @@ void MyMesh::begin(FILESYSTEM *fs) { acl.load(_fs, self_id); region_map.load(_fs); + acl.setRNG(getRNG()); + acl.loadNonces(); + bool dirty_reset = wasDirtyReset(board); + acl.finalizeNonceLoad(dirty_reset); + if (dirty_reset) acl.saveNonces(); // persist bumped nonces immediately + next_nonce_persist = futureMillis(60000); // establish default-scope { @@ -1048,6 +1058,12 @@ void MyMesh::loop() { dirty_contacts_expiry = 0; } + // persist dirty AEAD nonces + if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) { + if (acl.isNonceDirty()) { acl.saveNonces(); } + next_nonce_persist = futureMillis(60000); + } + // TODO: periodically check for OLD/inactive entries in known_clients[], and evict // update uptime diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 9844a867b..252804e6d 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -101,6 +101,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { ClientACL acl; CommonCLI _cli; unsigned long dirty_contacts_expiry; + unsigned long next_nonce_persist; uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long next_push; uint16_t _num_posted, _num_post_pushes; @@ -191,6 +192,9 @@ public: void savePrefs() override { _cli.savePrefs(_fs); } + void onBeforeReboot() override { + if (acl.isNonceDirty()) acl.saveNonces(); + } void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size); diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 867437392..c439067c5 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -256,7 +256,7 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) { mesh::Utils::sha256((uint8_t *)&t->expected_acks[t->attempt], 4, data, 5 + text_len, self_id.pub_key, PUB_KEY_SIZE); t->attempt++; - auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len, c->nextAeadNonce()); + auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len, acl.nextAeadNonceFor(*c)); if (pkt) { if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(pkt, c->out_path, c->out_path_len); @@ -510,7 +510,7 @@ uint8_t SensorMesh::getPeerFlags(int peer_idx) { uint16_t SensorMesh::getPeerNextAeadNonce(int peer_idx) { int i = matching_peer_indexes[peer_idx]; if (i >= 0 && i < acl.getNumClients()) - return acl.getClientByIdx(i)->nextAeadNonce(); + return acl.nextAeadNonceFor(*acl.getClientByIdx(i)); return 0; } @@ -520,7 +520,10 @@ void SensorMesh::onPeerAeadDetected(int peer_idx) { auto c = acl.getClientByIdx(i); if (!(c->flags & CONTACT_FLAG_AEAD)) { c->flags |= CONTACT_FLAG_AEAD; - getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce)); + if (c->aead_nonce == 0) { // no persisted nonce — seed from RNG to avoid deterministic start + getRNG()->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce)); + if (c->aead_nonce == 0) c->aead_nonce = 1; + } } } } @@ -565,10 +568,10 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, from->nextAeadNonce()); + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, acl.nextAeadNonceFor(*from)); if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len, from->nextAeadNonce()); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len, acl.nextAeadNonceFor(*from)); if (reply) { if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY); @@ -595,7 +598,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4, from->nextAeadNonce()); + PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4, acl.nextAeadNonceFor(*from)); if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize()); } else { sendAckTo(*from, ack_hash, packet->getPathHashSize()); @@ -623,7 +626,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique temp[4] = (TXT_TYPE_CLI_DATA << 2); - auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len, from->nextAeadNonce()); + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len, acl.nextAeadNonceFor(*from)); if (reply) { if (from->out_path_len == OUT_PATH_UNKNOWN) { sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); @@ -730,6 +733,7 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise { next_local_advert = next_flood_advert = 0; dirty_contacts_expiry = 0; + next_nonce_persist = 0; last_read_time = 0; num_alert_tasks = 0; set_radio_at = revert_radio_at = 0; @@ -773,6 +777,12 @@ void SensorMesh::begin(FILESYSTEM* fs) { acl.load(_fs, self_id); region_map.load(_fs); + acl.setRNG(getRNG()); + acl.loadNonces(); + bool dirty_reset = wasDirtyReset(board); + acl.finalizeNonceLoad(dirty_reset); + if (dirty_reset) acl.saveNonces(); // persist bumped nonces immediately + next_nonce_persist = futureMillis(60000); // establish default-scope { @@ -1002,4 +1012,10 @@ void SensorMesh::loop() { acl.save(_fs); dirty_contacts_expiry = 0; } + + // persist dirty AEAD nonces + if (next_nonce_persist && millisHasNowPassed(next_nonce_persist)) { + if (acl.isNonceDirty()) { acl.saveNonces(); } + next_nonce_persist = futureMillis(60000); + } } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 80d4612ca..524f2ff8b 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -60,6 +60,9 @@ public: const char* getNodeName() { return _prefs.node_name; } NodePrefs* getNodePrefs() { return &_prefs; } void savePrefs() override { _cli.savePrefs(_fs); } + void onBeforeReboot() override { + if (acl.isNonceDirty()) acl.saveNonces(); + } bool formatFileSystem() override; void sendSelfAdvertisement(int delay_millis, bool flood) override; void updateAdvertTimer() override; @@ -142,6 +145,7 @@ private: CommonCLI _cli; uint8_t reply_data[MAX_PACKET_PAYLOAD]; unsigned long dirty_contacts_expiry; + unsigned long next_nonce_persist; CayenneLPP telemetry; TransportKeyStore key_store; RegionMap region_map; diff --git a/src/MeshCore.h b/src/MeshCore.h index 30541075c..5279201cc 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -23,6 +23,10 @@ #define CONTACT_FLAG_AEAD 0x02 // bit 1 of ContactInfo.flags (bit 0 = favourite) #define FEAT1_AEAD_SUPPORT 0x0001 // bit 0 of feat1 uint16_t +// AEAD nonce persistence +#define NONCE_PERSIST_INTERVAL 50 // persist every N messages per peer +#define NONCE_BOOT_BUMP 100 // add this on load after dirty boot (must be >= 2 * PERSIST_INTERVAL) + #define MAX_PACKET_PAYLOAD 184 #define MAX_GROUP_DATA_LENGTH (MAX_PACKET_PAYLOAD - CIPHER_BLOCK_SIZE - 3) #define MAX_PATH_SIZE 64 diff --git a/src/helpers/ArduinoHelpers.h b/src/helpers/ArduinoHelpers.h index 97596daa3..9af0277e8 100644 --- a/src/helpers/ArduinoHelpers.h +++ b/src/helpers/ArduinoHelpers.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include class VolatileRTCClock : public mesh::RTCClock { @@ -33,3 +34,17 @@ public: } } }; + +// Returns true for dirty resets (power-on, watchdog, brownout, panic). +// Returns false for clean wakes (deep sleep, software restart). +inline bool wasDirtyReset(mesh::MainBoard& board) { +#if defined(ESP32) + esp_reset_reason_t rst = esp_reset_reason(); + return (rst != ESP_RST_DEEPSLEEP && rst != ESP_RST_SW); +#elif defined(NRF52_PLATFORM) + return !(board.getResetReason() & POWER_RESETREAS_SREQ_Msk); +#else + (void)board; + return true; +#endif +} diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 0b01f4afa..342b59081 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -9,6 +9,50 @@ #define TXT_ACK_DELAY 200 #endif +uint16_t BaseChatMesh::nextAeadNonceFor(const ContactInfo& contact) { + uint16_t nonce = contact.nextAeadNonce(); + if (nonce != 0) { + int idx = &contact - contacts; + if (idx >= 0 && idx < num_contacts && + (uint16_t)(contact.aead_nonce - nonce_at_last_persist[idx]) >= NONCE_PERSIST_INTERVAL) { + nonce_dirty = true; + } + } + return nonce; +} + +bool BaseChatMesh::applyLoadedNonce(const uint8_t* pub_key_prefix, uint16_t nonce) { + for (int i = 0; i < num_contacts; i++) { + if (memcmp(contacts[i].id.pub_key, pub_key_prefix, 4) == 0) { + contacts[i].aead_nonce = nonce; + return true; + } + } + return false; +} + +void BaseChatMesh::finalizeNonceLoad(bool needs_bump) { + for (int i = 0; i < num_contacts; i++) { + if (needs_bump) { + uint16_t old = contacts[i].aead_nonce; + contacts[i].aead_nonce += NONCE_BOOT_BUMP; + if (contacts[i].aead_nonce == 0) contacts[i].aead_nonce = 1; + if (contacts[i].aead_nonce < old) { + MESH_DEBUG_PRINTLN("AEAD nonce wrapped after boot bump for peer: %s", contacts[i].name); + } + } + nonce_at_last_persist[i] = contacts[i].aead_nonce; + } + nonce_dirty = false; +} + +bool BaseChatMesh::getNonceEntry(int idx, uint8_t* pub_key_prefix, uint16_t* nonce) { + if (idx >= num_contacts) return false; + memcpy(pub_key_prefix, contacts[idx].id.pub_key, 4); + *nonce = contacts[idx].aead_nonce; + return true; +} + void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { sendFlood(pkt, delay_millis); } @@ -118,6 +162,7 @@ void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identi ci.last_advert_timestamp = timestamp; ci.lastmod = getRTCClock()->getCurrentTime(); getRNG()->random((uint8_t*)&ci.aead_nonce, sizeof(ci.aead_nonce)); // seed AEAD nonce from HW RNG + if (ci.aead_nonce == 0) ci.aead_nonce = 1; if (parser.getFeat1() & FEAT1_AEAD_SUPPORT) { ci.flags |= CONTACT_FLAG_AEAD; } @@ -185,7 +230,8 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, return; } - populateContactFromAdvert(*from, id, parser, timestamp); + populateContactFromAdvert(*from, id, parser, timestamp); // seeds aead_nonce from RNG + nonce_at_last_persist[from - contacts] = from->aead_nonce; from->sync_since = 0; from->shared_secret_valid = false; } @@ -239,7 +285,7 @@ uint8_t BaseChatMesh::getPeerFlags(int peer_idx) { uint16_t BaseChatMesh::getPeerNextAeadNonce(int peer_idx) { int i = matching_peer_indexes[peer_idx]; if (i >= 0 && i < num_contacts) { - return contacts[i].nextAeadNonce(); + return nextAeadNonceFor(contacts[i]); } return 0; } @@ -275,7 +321,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 6, from.nextAeadNonce()); + PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 6, nextAeadNonceFor(from)); if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { sendAckTo(from, ack_hash, 6); @@ -286,7 +332,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra) - mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0, from.nextAeadNonce()); + mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0, nextAeadNonceFor(from)); if (path) sendFloodScoped(from, path); } } else if (flags == TXT_TYPE_SIGNED_PLAIN) { @@ -302,7 +348,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4, from.nextAeadNonce()); + PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4, nextAeadNonceFor(from)); if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { sendAckTo(from, (uint8_t *) &ack_hash); @@ -318,10 +364,10 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len, from.nextAeadNonce()); + PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len, nextAeadNonceFor(from)); if (path) sendFloodScoped(from, path, SERVER_RESPONSE_DELAY); } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len, from.nextAeadNonce()); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len, nextAeadNonceFor(from)); if (reply) { if (from.out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); @@ -387,7 +433,7 @@ void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len) { // NOTE: simplest impl is just to re-send a reciprocal return path to sender (DIRECTLY) // override this method in various firmwares, if there's a better strategy - mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0, contact.nextAeadNonce()); + mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0, nextAeadNonceFor(contact)); if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay } @@ -463,7 +509,7 @@ mesh::Packet* BaseChatMesh::composeMsgPacket(const ContactInfo& recipient, uint3 temp[len++] = attempt; // hide attempt number at tail end of payload } - return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, len, recipient.nextAeadNonce()); + return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, len, nextAeadNonceFor(recipient)); } int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout) { @@ -494,7 +540,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2); memcpy(&temp[5], text, text_len + 1); - auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len, recipient.nextAeadNonce()); + auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len, nextAeadNonceFor(recipient)); if (pkt == NULL) return MSG_SEND_FAILED; uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -666,7 +712,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_ memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique memcpy(&temp[4], req_data, data_len); - pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len, recipient.nextAeadNonce()); + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len, nextAeadNonceFor(recipient)); } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -693,7 +739,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u memset(&temp[5], 0, 4); // reserved (possibly for 'since' param) getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique - pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp), recipient.nextAeadNonce()); + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp), nextAeadNonceFor(recipient)); } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -816,7 +862,7 @@ void BaseChatMesh::checkConnections() { // calc expected ACK reply mesh::Utils::sha256((uint8_t *)&connections[i].expected_ack, 4, data, 9, self_id.pub_key, PUB_KEY_SIZE); - auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->getSharedSecret(self_id), data, 9, contact->nextAeadNonce()); + auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->getSharedSecret(self_id), data, 9, nextAeadNonceFor(*contact)); if (pkt) { sendDirect(pkt, contact->out_path, contact->out_path_len); } @@ -878,9 +924,12 @@ ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int pre bool BaseChatMesh::addContact(const ContactInfo& contact) { ContactInfo* dest = allocateContactSlot(contact.type == ADV_TYPE_NONE); if (dest) { + int idx = dest - contacts; *dest = contact; dest->shared_secret_valid = false; // mark shared_secret as needing calculation getRNG()->random((uint8_t*)&dest->aead_nonce, sizeof(dest->aead_nonce)); // always seed fresh from HW RNG + if (dest->aead_nonce == 0) dest->aead_nonce = 1; + nonce_at_last_persist[idx] = dest->aead_nonce; return true; // success } return false; @@ -893,10 +942,11 @@ bool BaseChatMesh::removeContact(ContactInfo& contact) { } if (idx >= num_contacts) return false; // not found - // remove from contacts array + // remove from contacts array and parallel nonce tracking num_contacts--; while (idx < num_contacts) { contacts[idx] = contacts[idx + 1]; + nonce_at_last_persist[idx] = nonce_at_last_persist[idx + 1]; idx++; } return true; // Success diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 9eeff5d6d..70bc09d7e 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -74,6 +74,10 @@ class BaseChatMesh : public mesh::Mesh { uint8_t temp_buf[MAX_TRANS_UNIT]; ConnectionInfo connections[MAX_CONNECTIONS]; + // Nonce persistence state (parallel to contacts[]) + uint16_t nonce_at_last_persist[MAX_CONTACTS]; + bool nonce_dirty; + mesh::Packet* composeMsgPacket(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char *text, uint32_t& expected_ack); void sendAckTo(const ContactInfo& dest, const uint8_t* ack_hash, uint8_t ack_len=4); @@ -90,6 +94,8 @@ protected: txt_send_timeout = 0; _pendingLoopback = NULL; memset(connections, 0, sizeof(connections)); + memset(nonce_at_last_persist, 0, sizeof(nonce_at_last_persist)); + nonce_dirty = false; } void bootstrapRTCfromContacts(); @@ -132,6 +138,17 @@ protected: virtual int getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { return 0; } // not implemented virtual bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], int len) { return false; } + // AEAD nonce persistence helpers + uint16_t nextAeadNonceFor(const ContactInfo& contact); // wraps nextAeadNonce() with dirty-check + bool applyLoadedNonce(const uint8_t* pub_key_prefix, uint16_t nonce); + void finalizeNonceLoad(bool needs_bump); + bool getNonceEntry(int idx, uint8_t* pub_key_prefix, uint16_t* nonce); + bool isNonceDirty() const { return nonce_dirty; } + void clearNonceDirty() { + for (int i = 0; i < num_contacts; i++) nonce_at_last_persist[i] = contacts[i].aead_nonce; + nonce_dirty = false; + } + // Mesh overrides void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override; int searchPeersByHash(const uint8_t* hash) override; diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 128238273..56b1a79c2 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -1,4 +1,5 @@ #include "ClientACL.h" +#include static File openWrite(FILESYSTEM* _fs, const char* filename) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) @@ -111,13 +112,87 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) { } else { c = oldest; // evict least active contact } + int idx = c - clients; memset(c, 0, sizeof(*c)); c->permissions = init_perms; c->id = id; c->out_path_len = OUT_PATH_UNKNOWN; + if (_rng) { + _rng->random((uint8_t*)&c->aead_nonce, sizeof(c->aead_nonce)); + if (c->aead_nonce == 0) c->aead_nonce = 1; + } + nonce_at_last_persist[idx] = c->aead_nonce; return c; } +uint16_t ClientACL::nextAeadNonceFor(const ClientInfo& client) { + uint16_t nonce = client.nextAeadNonce(); + if (nonce != 0) { + int idx = &client - clients; + if (idx >= 0 && idx < num_clients && + (uint16_t)(client.aead_nonce - nonce_at_last_persist[idx]) >= NONCE_PERSIST_INTERVAL) { + nonce_dirty = true; + } + } + return nonce; +} + +void ClientACL::loadNonces() { + if (!_fs) return; +#if defined(RP2040_PLATFORM) + File file = _fs->open("/s_nonces", "r"); +#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + File file = _fs->open("/s_nonces", FILE_O_READ); +#else + File file = _fs->open("/s_nonces", "r", false); +#endif + if (file) { + uint8_t rec[6]; // 4-byte pub_key prefix + 2-byte nonce + while (file.read(rec, 6) == 6) { + uint16_t nonce; + memcpy(&nonce, &rec[4], 2); + for (int i = 0; i < num_clients; i++) { + if (memcmp(clients[i].id.pub_key, rec, 4) == 0) { + clients[i].aead_nonce = nonce; + break; + } + } + } + file.close(); + } +} + +void ClientACL::saveNonces() { + if (!_fs) return; + File file = openWrite(_fs, "/s_nonces"); + if (file) { + for (int i = 0; i < num_clients; i++) { + file.write(clients[i].id.pub_key, 4); + file.write((uint8_t*)&clients[i].aead_nonce, 2); + nonce_at_last_persist[i] = clients[i].aead_nonce; + } + file.close(); + nonce_dirty = false; + } +} + +void ClientACL::finalizeNonceLoad(bool needs_bump) { + for (int i = 0; i < num_clients; i++) { + if (needs_bump) { + uint16_t old = clients[i].aead_nonce; + clients[i].aead_nonce += NONCE_BOOT_BUMP; + if (clients[i].aead_nonce == 0) clients[i].aead_nonce = 1; + if (clients[i].aead_nonce < old) { + MESH_DEBUG_PRINTLN("AEAD nonce wrapped after boot bump for client: %02x%02x%02x%02x", + clients[i].id.pub_key[0], clients[i].id.pub_key[1], + clients[i].id.pub_key[2], clients[i].id.pub_key[3]); + } + } + nonce_at_last_persist[i] = clients[i].aead_nonce; + } + nonce_dirty = false; +} + bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8_t* pubkey, int key_len, uint8_t perms) { ClientInfo* c; if ((perms & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST) { // guest role is not persisted in contacts @@ -128,6 +203,7 @@ bool ClientACL::applyPermissions(const mesh::LocalIdentity& self_id, const uint8 int i = c - clients; while (i < num_clients) { clients[i] = clients[i + 1]; + nonce_at_last_persist[i] = nonce_at_last_persist[i + 1]; i++; } } else { diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index 7e65c2ca3..31788d272 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -31,7 +31,7 @@ struct ClientInfo { uint8_t push_failures; } room; } extra; - + uint16_t nextAeadNonce() const { if (flags & CONTACT_FLAG_AEAD) { if (++aead_nonce == 0) ++aead_nonce; // skip 0 (means ECB) @@ -51,10 +51,18 @@ class ClientACL { ClientInfo clients[MAX_CLIENTS]; int num_clients; + // Nonce persistence state (parallel to clients[]) + uint16_t nonce_at_last_persist[MAX_CLIENTS]; + bool nonce_dirty; + mesh::RNG* _rng; + public: - ClientACL() { + ClientACL() { memset(clients, 0, sizeof(clients)); + memset(nonce_at_last_persist, 0, sizeof(nonce_at_last_persist)); num_clients = 0; + nonce_dirty = false; + _rng = NULL; } void load(FILESYSTEM* _fs, const mesh::LocalIdentity& self_id); void save(FILESYSTEM* _fs, bool (*filter)(ClientInfo*)=NULL); @@ -66,4 +74,16 @@ public: int getNumClients() const { return num_clients; } ClientInfo* getClientByIdx(int idx) { return &clients[idx]; } + + // AEAD nonce persistence + void setRNG(mesh::RNG* rng) { _rng = rng; } + uint16_t nextAeadNonceFor(const ClientInfo& client); + void loadNonces(); + void saveNonces(); + void finalizeNonceLoad(bool needs_bump); + bool isNonceDirty() const { return nonce_dirty; } + void clearNonceDirty() { + for (int i = 0; i < num_clients; i++) nonce_at_last_persist[i] = clients[i].aead_nonce; + nonce_dirty = false; + } }; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 72a825c50..e6add5ac4 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -225,10 +225,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re if (memcmp(command, "poweroff", 8) == 0 || memcmp(command, "shutdown", 8) == 0) { _board->powerOff(); // doesn't return } else if (memcmp(command, "reboot", 6) == 0) { + _callbacks->onBeforeReboot(); _board->reboot(); // doesn't return } else if (memcmp(command, "clkreboot", 9) == 0) { // Reset clock getRTCClock()->setCurrentTime(1715770351); // 15 May 2024, 8:50pm + _callbacks->onBeforeReboot(); _board->reboot(); // doesn't return } else if (memcmp(command, "advert.zerohop", 14) == 0 && (command[14] == 0 || command[14] == ' ')) { // send zerohop advert diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index f3abcf477..acd1b0fa7 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -114,6 +114,10 @@ public: virtual bool setRxBoostedGain(bool enable) { return false; // CommonCLI reports unsupported if not overridden by wrapper }; + + virtual void onBeforeReboot() { + // no op by default — override to flush nonces, etc. + }; }; class CommonCLI {