Browse Source

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
pull/1677/head
Wessel Nieboer 6 months ago
parent
commit
9ce710ebd1
No known key found for this signature in database GPG Key ID: 929C8E45E33B5FD2
  1. 30
      examples/companion_radio/DataStore.cpp
  2. 4
      examples/companion_radio/DataStore.h
  3. 19
      examples/companion_radio/MyMesh.cpp
  4. 4
      examples/companion_radio/MyMesh.h
  5. 26
      examples/simple_repeater/MyMesh.cpp
  6. 4
      examples/simple_repeater/MyMesh.h
  7. 28
      examples/simple_room_server/MyMesh.cpp
  8. 4
      examples/simple_room_server/MyMesh.h
  9. 30
      examples/simple_sensor/SensorMesh.cpp
  10. 4
      examples/simple_sensor/SensorMesh.h
  11. 4
      src/MeshCore.h
  12. 15
      src/helpers/ArduinoHelpers.h
  13. 78
      src/helpers/BaseChatMesh.cpp
  14. 17
      src/helpers/BaseChatMesh.h
  15. 76
      src/helpers/ClientACL.cpp
  16. 24
      src/helpers/ClientACL.h
  17. 2
      src/helpers/CommonCLI.cpp
  18. 4
      src/helpers/CommonCLI.h

30
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)

4
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);

19
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

4
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;

26
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, &timestamp, 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;

4
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);

28
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

4
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);

30
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, &timestamp, 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);
}
}

4
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;

4
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

15
src/helpers/ArduinoHelpers.h

@ -1,6 +1,7 @@
#pragma once
#include <Mesh.h>
#include <MeshCore.h>
#include <Arduino.h>
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
}

78
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

17
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;

76
src/helpers/ClientACL.cpp

@ -1,4 +1,5 @@
#include "ClientACL.h"
#include <MeshCore.h>
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 {

24
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;
}
};

2
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

4
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 {

Loading…
Cancel
Save