From 5f1628ca1d6304e63c884525398a057bc2f73256 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Thu, 12 Feb 2026 01:34:33 +0100 Subject: [PATCH] Enable AEAD-4 sending to peers that advertise support Send ChaChaPoly-encrypted messages to peers with CONTACT_FLAG_AEAD set, and try AEAD decode first for those peers (avoiding 1/65536 ECB false-positive). Legacy peers continue to use ECB in both directions. - Add aead_nonce parameter to createDatagram/createPathReturn (default 0 = ECB) - Add getPeerFlags/getPeerNextAeadNonce virtual methods for decode-order selection - Add ContactInfo::nextAeadNonce() helper (returns nonce++ if AEAD, 0 otherwise) - Update all BaseChatMesh send paths to pass nonce for AEAD-capable peers - Adaptive decode order: AEAD-first for known AEAD peers, ECB-first for others --- src/Mesh.cpp | 49 ++++++++++++++++++++++++------------ src/Mesh.h | 8 +++--- src/helpers/BaseChatMesh.cpp | 38 ++++++++++++++++++++-------- src/helpers/BaseChatMesh.h | 2 ++ src/helpers/ContactInfo.h | 9 ++++++- 5 files changed, 75 insertions(+), 31 deletions(-) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 7a54dd21c..e670c808d 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -156,15 +156,16 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t data[MAX_PACKET_PAYLOAD]; int macAndDataLen = pkt->payload_len - i; - // Try ECB first (Phase 1: all senders use ECB), then AEAD-4 fallback. - // IMPORTANT: Phase 2 MUST swap to AEAD-first. ECB-first has a 1/65536 - // false-positive rate on AEAD packets (nonce bytes matching truncated HMAC), - // producing garbage plaintext. AEAD-first has only 1/2^32 false-positive on - // ECB packets, which is negligible. - int len = Utils::MACThenDecrypt(secret, data, macAndData, macAndDataLen); - if (len <= 0) { - uint8_t assoc[3] = { pkt->header, dest_hash, src_hash }; + // Try-both decode: AEAD-first for peers known to support it (avoids 1/65536 + // ECB false-positive on AEAD packets), ECB-first for unknown/legacy peers. + uint8_t assoc[3] = { pkt->header, dest_hash, src_hash }; + int len; + if (getPeerFlags(j) & CONTACT_FLAG_AEAD) { len = Utils::aeadDecrypt(secret, data, macAndData, macAndDataLen, assoc, 3, dest_hash, src_hash); + if (len <= 0) len = Utils::MACThenDecrypt(secret, data, macAndData, macAndDataLen); + } else { + len = Utils::MACThenDecrypt(secret, data, macAndData, macAndDataLen); + if (len <= 0) len = Utils::aeadDecrypt(secret, data, macAndData, macAndDataLen, assoc, 3, dest_hash, src_hash); } if (len > 0) { // success! if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { @@ -183,7 +184,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) { if (pkt->isRouteFlood()) { // send a reciprocal return path to sender, but send DIRECTLY! - mesh::Packet* rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, NULL, 0); + mesh::Packet* rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, NULL, 0, getPeerNextAeadNonce(j)); if (rpath) sendDirect(rpath, path, path_len, 500); } } @@ -466,13 +467,13 @@ Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, siz #define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE) -Packet* Mesh::createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) { +Packet* Mesh::createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len, uint16_t aead_nonce) { uint8_t dest_hash[PATH_HASH_SIZE]; dest.copyHashTo(dest_hash); - return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len); + return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len, aead_nonce); } -Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) { +Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len, uint16_t aead_nonce) { uint8_t path_hash_size = (path_len >> 6) + 1; uint8_t path_hash_count = path_len & 63; @@ -504,7 +505,14 @@ Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, getRNG()->random(&data[data_len], 4); data_len += 4; } - len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + if (aead_nonce) { + uint8_t dh = packet->payload[0]; + uint8_t sh = packet->payload[1]; + uint8_t assoc[3] = { packet->header, dh, sh }; + len += Utils::aeadEncrypt(secret, &packet->payload[len], data, data_len, assoc, 3, aead_nonce, dh, sh); + } else { + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + } } packet->payload_len = len; @@ -512,9 +520,10 @@ Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, return packet; } -Packet* Mesh::createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len) { +Packet* Mesh::createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len, uint16_t aead_nonce) { if (type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE) { - if (data_len + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL; + size_t max_overhead = aead_nonce ? (AEAD_NONCE_SIZE + AEAD_TAG_SIZE) : (CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE-1); + if (data_len + max_overhead > MAX_PACKET_PAYLOAD) return NULL; } else { return NULL; // invalid type } @@ -529,7 +538,15 @@ Packet* Mesh::createDatagram(uint8_t type, const Identity& dest, const uint8_t* int len = 0; len += dest.copyHashTo(&packet->payload[len]); // dest hash len += self_id.copyHashTo(&packet->payload[len]); // src hash - len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + + if (aead_nonce) { + uint8_t dest_hash = packet->payload[0]; + uint8_t src_hash = packet->payload[1]; + uint8_t assoc[3] = { packet->header, dest_hash, src_hash }; + len += Utils::aeadEncrypt(secret, &packet->payload[len], data, data_len, assoc, 3, aead_nonce, dest_hash, src_hash); + } else { + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + } packet->payload_len = len; diff --git a/src/Mesh.h b/src/Mesh.h index 49a299a6a..3e8245f94 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -83,6 +83,8 @@ protected: * \param peer_idx index of peer, [0..n) where n is what searchPeersByHash() returned */ virtual void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { } + virtual uint8_t getPeerFlags(int peer_idx) { return 0; } + virtual uint16_t getPeerNextAeadNonce(int peer_idx) { return 0; } /** * \brief A (now decrypted) data packet has been received (by a known peer). @@ -183,15 +185,15 @@ public: RTCClock* getRTCClock() const { return _rtc; } Packet* createAdvert(const LocalIdentity& id, const uint8_t* app_data=NULL, size_t app_data_len=0); - Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len); + Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len, uint16_t aead_nonce=0); Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len); Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len); Packet* createAck(const uint8_t* ack, uint8_t len); Packet* createAck(uint32_t ack_crc) { return createAck((uint8_t *) &ack_crc, 4); } Packet* createMultiAck(const uint8_t* ack, uint8_t len, uint8_t remaining); Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining) { return createMultiAck((uint8_t *)&ack_crc, 4, remaining); } - Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); - Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); + Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len, uint16_t aead_nonce=0); + Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len, uint16_t aead_nonce=0); Packet* createRawData(const uint8_t* data, size_t len); Packet* createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0); Packet* createControlData(const uint8_t* data, size_t len); diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index cd60e5c01..0b01f4afa 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -228,6 +228,22 @@ void BaseChatMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { } } +uint8_t BaseChatMesh::getPeerFlags(int peer_idx) { + int i = matching_peer_indexes[peer_idx]; + if (i >= 0 && i < num_contacts) { + return contacts[i].flags; + } + return 0; +} + +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 0; +} + void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) { int i = matching_peer_indexes[sender_idx]; if (i < 0 || i >= num_contacts) { @@ -259,7 +275,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); + PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 6, from.nextAeadNonce()); if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { sendAckTo(from, ack_hash, 6); @@ -270,7 +286,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); + mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0, from.nextAeadNonce()); if (path) sendFloodScoped(from, path); } } else if (flags == TXT_TYPE_SIGNED_PLAIN) { @@ -286,7 +302,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); + PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4, from.nextAeadNonce()); if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { sendAckTo(from, (uint8_t *) &ack_hash); @@ -302,10 +318,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); + PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len, from.nextAeadNonce()); if (path) sendFloodScoped(from, path, SERVER_RESPONSE_DELAY); } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len, from.nextAeadNonce()); 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); @@ -371,7 +387,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); + mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0, contact.nextAeadNonce()); if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay } @@ -447,7 +463,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); + return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, len, recipient.nextAeadNonce()); } int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout) { @@ -478,7 +494,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); + auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len, recipient.nextAeadNonce()); if (pkt == NULL) return MSG_SEND_FAILED; uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -650,7 +666,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); + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len, recipient.nextAeadNonce()); } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -677,7 +693,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)); + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp), recipient.nextAeadNonce()); } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -800,7 +816,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); + auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->getSharedSecret(self_id), data, 9, contact->nextAeadNonce()); if (pkt) { sendDirect(pkt, contact->out_path, contact->out_path_len); } diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index d98785470..9eeff5d6d 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -136,6 +136,8 @@ protected: 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; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; + uint8_t getPeerFlags(int peer_idx) override; + uint16_t getPeerNextAeadNonce(int peer_idx) override; void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override; bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index 2ba81080e..864329910 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -17,7 +17,14 @@ struct ContactInfo { uint32_t lastmod; // by OUR clock int32_t gps_lat, gps_lon; // 6 dec places uint32_t sync_since; - uint16_t aead_nonce; // per-peer AEAD nonce counter for DMs (not used for group messages), seeded from HW RNG + mutable uint16_t aead_nonce; // per-peer AEAD nonce counter for DMs (not used for group messages), seeded from HW RNG + + // Returns next AEAD nonce (post-increment) if peer supports AEAD, 0 otherwise. + // When 0, callers use ECB encryption. + uint16_t nextAeadNonce() const { + if (flags & CONTACT_FLAG_AEAD) return ++aead_nonce; + return 0; + } const uint8_t* getSharedSecret(const mesh::LocalIdentity& self_id) const { if (!shared_secret_valid) {