From 894feb58583c89cf1b9be72dbf2684d7f6d08f95 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:19:26 +1100 Subject: [PATCH] MeshCore v1.18 batch 1: decrypt block check, skip empty channel slots, repeater serial buffer, un-scoped flood (v12), text type 3 relay and repeater --- examples/companion_radio/MyMesh.cpp | 18 +++++++++++++----- examples/companion_radio/MyMesh.h | 1 + examples/simple_repeater/MyMesh.cpp | 2 +- examples/simple_repeater/main.cpp | 12 ++++++++++-- src/Utils.cpp | 7 +++++-- src/Utils.h | 2 +- src/helpers/BaseChatMesh.cpp | 10 ++++++++-- src/helpers/BaseChatMesh.h | 2 +- src/helpers/TxtDataHelpers.h | 1 + 9 files changed, 41 insertions(+), 14 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index f6f7987d..fa9ba349 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -761,7 +761,10 @@ void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pk // Scope priority: BLE app send_scope → per-channel scope → device default → unscoped TransportKey scope; - if (!send_scope.isNull()) { + if (send_unscoped) { + // BLE app has explicitly requested un-scoped via CMD 54 (ver 12+) -- null key + memset(scope.key, 0, sizeof(scope.key)); + } else if (!send_scope.isNull()) { // BLE app has set a scope via CMD 54 — use it (highest priority) memcpy(scope.key, send_scope.key, sizeof(scope.key)); } else { @@ -1237,7 +1240,7 @@ bool MyMesh::uiSendCliCommand(uint32_t contact_idx, const char* command) { uint32_t timestamp = getRTCClock()->getCurrentTimeUnique(); uint32_t est_timeout; - int result = sendCommandData(*recipient, timestamp, 0, command, est_timeout); + int result = sendCommandData(*recipient, timestamp, 0, TXT_TYPE_CLI_DATA, command, est_timeout); if (result == MSG_SEND_FAILED) { MESH_DEBUG_PRINTLN("UI: CLI command send failed to %s: %s", recipient->name, command); return false; @@ -1684,6 +1687,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _rxlog_head = 0; _rxlog_count = 0; memset(send_scope.key, 0, sizeof(send_scope.key)); + send_unscoped = false; memset(_sent_track, 0, sizeof(_sent_track)); _sent_track_idx = 0; _admin_contact_idx = -1; @@ -2046,16 +2050,16 @@ void MyMesh::handleCmdFrame(size_t len) { uint8_t *pub_key_prefix = &cmd_frame[i]; i += 6; ContactInfo *recipient = lookupContactByPubKey(pub_key_prefix, 6); - if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA)) { + if (recipient && (txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND)) { char *text = (char *)&cmd_frame[i]; int tlen = len - i; uint32_t est_timeout; text[tlen] = 0; // ensure null int result; uint32_t expected_ack; - if (txt_type == TXT_TYPE_CLI_DATA) { + if (txt_type == TXT_TYPE_CLI_DATA || txt_type == TXT_TYPE_CLI_COMMAND) { msg_timestamp = getRTCClock()->getCurrentTimeUnique(); // Use node's RTC instead of app timestamp to avoid tripping replay protection - result = sendCommandData(*recipient, msg_timestamp, attempt, text, est_timeout); + result = sendCommandData(*recipient, msg_timestamp, attempt, txt_type, text, est_timeout); expected_ack = 0; // no Ack expected } else { result = sendMessage(*recipient, msg_timestamp, attempt, text, expected_ack, est_timeout); @@ -2896,6 +2900,10 @@ void MyMesh::handleCmdFrame(size_t len) { memset(send_scope.key, 0, sizeof(send_scope.key)); // set scope to null Serial.println("[CMD54] Per-channel scope cleared"); } + send_unscoped = false; + writeOKFrame(); + } else if (cmd_frame[0] == CMD_SET_FLOOD_SCOPE_KEY && len >= 2 && cmd_frame[1] == 1) { // ver 12+ + send_unscoped = true; writeOKFrame(); } else if (cmd_frame[0] == CMD_SET_DEFAULT_FLOOD_SCOPE && len >= 1) { // v1.15+ — set device-wide default flood scope (name[31] + key[16]) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 5abe02d3..502b97df 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -387,6 +387,7 @@ private: unsigned long _nextContactSaveDue; // ESP32 chunked save: next scheduled flush (0 = unscheduled) TransportKey send_scope; + bool send_unscoped; // force un-scoped flood (instead of using send_scope) // --- Region scope resolution for incoming channel messages (display only) --- // A received scoped flood/direct packet carries a one-way transport code. We match diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index eaf1962f..23c39240 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -674,7 +674,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, memcpy(&sender_timestamp, data, 4); // timestamp (by sender's RTC clock - which could be wrong) uint8_t flags = (data[4] >> 2); // message attempt number, and other flags - if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA)) { + if (!(flags == TXT_TYPE_PLAIN || flags == TXT_TYPE_CLI_DATA || flags == TXT_TYPE_CLI_COMMAND)) { MESH_DEBUG_PRINTLN("onPeerDataRecv: unsupported text type received: flags=%02x", (uint32_t)flags); } else if (sender_timestamp >= client->last_timestamp) { // prevent replay attacks bool is_retry = (sender_timestamp == client->last_timestamp); diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 7f82f625..4c2ab2d9 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -169,6 +169,13 @@ void setup() { void loop() { int len = strlen(command); + // `command` must stay NUL-terminated within its bounds. If it ever isn't, + // strlen() above can return >= sizeof(command) and the loop below would then + // index past the buffer, so clamp defensively. + if (len >= (int)sizeof(command)) { + command[0] = 0; + len = 0; + } while (Serial.available() && len < sizeof(command)-1) { char c = Serial.read(); if (c != '\n') { @@ -178,8 +185,9 @@ void loop() { } if (c == '\r') break; } - if (len == sizeof(command)-1) { // command buffer full - command[sizeof(command)-1] = '\r'; + if (len == sizeof(command)-1) { // buffer full: treat as a completed line + command[sizeof(command)-2] = '\r'; // place end-of-line marker inside the buffer + command[sizeof(command)-1] = 0; // keep the buffer NUL-terminated } if (len > 0 && command[len - 1] == '\r') { // received complete line diff --git a/src/Utils.cpp b/src/Utils.cpp index 186c8720..a70d4a70 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -74,15 +74,18 @@ int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uin int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) { if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes + int enc_len = src_len - CIPHER_MAC_SIZE; + if (enc_len % CIPHER_BLOCK_SIZE != 0) return 0; // reject non-block-aligned ciphertext + uint8_t hmac[CIPHER_MAC_SIZE]; { SHA256 sha; sha.resetHMAC(shared_secret, PUB_KEY_SIZE); - sha.update(src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); + sha.update(src + CIPHER_MAC_SIZE, enc_len); sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE); } if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) { - return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE); + return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, enc_len); } return 0; // invalid HMAC } diff --git a/src/Utils.h b/src/Utils.h index 5736b874..9997fe3c 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -50,7 +50,7 @@ public: /** * \brief checks the MAC (in leading bytes of 'src'), then if valid, decrypts remaining bytes in src. - * \returns zero if MAC is invalid, otherwise the length of decrypted bytes in 'dest' + * \returns zero if MAC is invalid or ciphertext is not block-aligned, otherwise the length of decrypted bytes in 'dest' */ static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len); diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index e7465040..51b1530b 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -344,6 +344,12 @@ void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8 int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel dest[], int max_matches) { int n = 0; for (int i = 0; i < MAX_GROUP_CHANNELS && n < max_matches; i++) { + // Skip empty/unconfigured slots. An empty slot has an all-zero secret and + // therefore matches null-key group traffic (a node transmitting with an + // unset PSK): the zero-key MAC validates against the empty slot and the + // foreign message is delivered as if it belonged to that channel. Any node + // with a free channel slot would otherwise act as a null-key sink. + if (channels[i].name[0] == 0) continue; if (channels[i].channel.hash[0] == hash[0]) { dest[n++] = channels[i].channel; } @@ -407,13 +413,13 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, return rc; } -int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout) { +int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout) { int text_len = strlen(text); if (text_len > MAX_TEXT_LEN) return MSG_SEND_FAILED; uint8_t temp[5+MAX_TEXT_LEN+1]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique - temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2); + temp[4] = (attempt & 3) | (txt_type << 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); diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index ed3784c5..b7acc756 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -162,7 +162,7 @@ public: mesh::Packet* createSelfAdvert(const char* name); mesh::Packet* createSelfAdvert(const char* name, double lat, double lon); int sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout); - int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& est_timeout); + int sendCommandData(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, uint8_t txt_type, const char* text, uint32_t& est_timeout); bool sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len); int sendLogin(const ContactInfo& recipient, const char* password, uint32_t& est_timeout); int sendAnonReq(const ContactInfo& recipient, const uint8_t* data, uint8_t len, uint32_t& tag, uint32_t& est_timeout); diff --git a/src/helpers/TxtDataHelpers.h b/src/helpers/TxtDataHelpers.h index 6ab84d39..3c42478e 100644 --- a/src/helpers/TxtDataHelpers.h +++ b/src/helpers/TxtDataHelpers.h @@ -6,6 +6,7 @@ #define TXT_TYPE_PLAIN 0 // a plain text message #define TXT_TYPE_CLI_DATA 1 // a CLI command #define TXT_TYPE_SIGNED_PLAIN 2 // plain text, signed by sender +#define TXT_TYPE_CLI_COMMAND 3 // a CLI command (explictly) class StrHelper { public: