From 994c8eb2442ab827eac864c96e30b6cb655c7f72 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:31:09 +0100 Subject: [PATCH] First raw implementation that seems to work --- examples/companion_radio/DataStore.cpp | 20 +- examples/simple_repeater/MyMesh.cpp | 43 +++- examples/simple_repeater/MyMesh.h | 3 +- examples/simple_room_server/MyMesh.cpp | 40 +++- examples/simple_room_server/MyMesh.h | 2 + examples/simple_sensor/SensorMesh.cpp | 45 +++- examples/simple_sensor/SensorMesh.h | 2 + src/Mesh.cpp | 274 +++++++++++++++++++++---- src/Mesh.h | 19 +- src/MeshCore.h | 7 +- src/Packet.cpp | 8 +- src/Packet.h | 2 +- src/Utils.cpp | 216 ++++++++++++++++++- src/Utils.h | 54 +++++ src/helpers/AdvertDataHelpers.h | 3 + src/helpers/BaseChatMesh.cpp | 69 +++++-- src/helpers/BaseChatMesh.h | 1 + src/helpers/ClientACL.h | 1 + src/helpers/CommonCLI.cpp | 3 + src/helpers/ContactInfo.h | 1 + src/helpers/esp32/ESPNOWRadio.cpp | 21 +- 21 files changed, 725 insertions(+), 109 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index f61f53aee..51e393f00 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -274,6 +274,9 @@ File file = openRead(_getContactsChannelsFS(), "/contacts3"); bool full = false; while (!full) { ContactInfo c; + memset(&c, 0, sizeof(c)); // Zero-initialize to prevent garbage in new fields + c.shared_secret_valid = false; + c.supports_chacha = false; uint8_t pub_key[32]; uint8_t unused; @@ -335,14 +338,18 @@ void DataStore::loadChannels(DataStoreHost* host) { uint8_t channel_idx = 0; while (!full) { ChannelDetails ch; - uint8_t unused[4]; + uint8_t flags_and_unused[4]; - bool success = (file.read(unused, 4) == 4); + bool success = (file.read(flags_and_unused, 4) == 4); success = success && (file.read((uint8_t *)ch.name, 32) == 32); success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32); if (!success) break; // EOF + // First byte stores channel flags (v2 support, etc), rest unused + // Old files have all zeros here, which means v1 (backward compatible) + ch.channel.flags = flags_and_unused[0]; + if (host->onChannelLoaded(channel_idx, ch)) { channel_idx++; } else { @@ -358,11 +365,14 @@ void DataStore::saveChannels(DataStoreHost* host) { if (file) { uint8_t channel_idx = 0; ChannelDetails ch; - uint8_t unused[4]; - memset(unused, 0, 4); + uint8_t flags_and_unused[4]; while (host->getChannelForSave(channel_idx, ch)) { - bool success = (file.write(unused, 4) == 4); + // First byte stores channel flags (v2 support, etc), rest unused + memset(flags_and_unused, 0, 4); + flags_and_unused[0] = ch.channel.flags; + + bool success = (file.write(flags_and_unused, 4) == 4); success = success && (file.write((uint8_t *)ch.name, 32) == 32); success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index d926148d6..335a3300b 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -530,16 +530,17 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m if (reply_len == 0) return; // invalid request + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, use_v2); if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else if (reply_path_len < 0) { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len, use_v2); if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len, use_v2); if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY); } } @@ -565,6 +566,16 @@ void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { } } +bool MyMesh::peerSupportsCHACHA(const mesh::Identity& dest) { + // Look up client by public key + ClientInfo* client = acl.getClient(dest.pub_key, PUB_KEY_SIZE); + if (client) { + return client->supports_chacha; + } + // Unknown peer - default to v1 (safe fallback) + return false; +} + static bool isShare(const mesh::Packet *packet) { if (packet->hasTransportCodes()) { return packet->transport_codes[0] == 0 && packet->transport_codes[1] == 0; // codes { 0, 0 } means 'send to nowhere' @@ -576,11 +587,19 @@ void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32 const uint8_t *app_data, size_t app_data_len) { mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl - // if this a zero hop advert (and not via 'Share'), add it to neighbours - if (packet->path_len == 0 && !isShare(packet)) { - AdvertDataParser parser(app_data, app_data_len); - if (parser.isValid() && parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters - putNeighbour(id, timestamp, packet->getSNR()); + AdvertDataParser parser(app_data, app_data_len); + if (parser.isValid()) { + // Update CHACHA capability for known clients (chat nodes that are in our ACL) + ClientInfo* client = acl.getClient(id.pub_key, PUB_KEY_SIZE); + if (client) { + client->supports_chacha = (parser.getFeat1() & ADV_FEAT1_CHACHA_CAPABLE) != 0; + } + + // if this a zero hop advert (and not via 'Share'), add it to neighbours + if (packet->path_len == 0 && !isShare(packet)) { + if (parser.getType() == ADV_TYPE_REPEATER) { // just keep neigbouring Repeaters + putNeighbour(id, timestamp, packet->getSNR()); + } } } } @@ -605,14 +624,15 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, client->last_timestamp = timestamp; client->last_activity = getRTCClock()->getCurrentTime(); + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); 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); + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, use_v2); if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { mesh::Packet *reply = - createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); + createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, use_v2); if (reply) { if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); @@ -673,7 +693,8 @@ 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); + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, use_v2); if (reply) { if (client->out_path_len < 0) { sendFlood(reply, CLI_REPLY_DELAY_MILLIS); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index f930ee7eb..2ab633184 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -162,7 +162,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; - void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len); + bool peerSupportsCHACHA(const mesh::Identity& dest) override; + void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) 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 onControlDataRecv(mesh::Packet* packet) override; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 60dd18407..0cd4cb0bb 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -71,7 +71,8 @@ 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); + bool use_v2 = client->supports_chacha; + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len, use_v2); if (reply) { if (client->out_path_len < 0) { sendFlood(reply); @@ -349,13 +350,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); // delay next push, give RESPONSE packet time to arrive first + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, 13); + PAYLOAD_TYPE_RESPONSE, reply_data, 13, use_v2); if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { - mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13); + mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13, use_v2); if (reply) { if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); @@ -387,6 +389,30 @@ void MyMesh::getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { } } +bool MyMesh::peerSupportsCHACHA(const mesh::Identity& dest) { + // Look up client by public key + ClientInfo* client = acl.getClient(dest.pub_key, PUB_KEY_SIZE); + if (client) { + return client->supports_chacha; + } + // Unknown peer - default to v1 (safe fallback) + return false; +} + +void MyMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32_t timestamp, + const uint8_t *app_data, size_t app_data_len) { + mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl + + // Update CHACHA capability for known clients + AdvertDataParser parser(app_data, app_data_len); + if (parser.isValid()) { + ClientInfo* client = acl.getClient(id.pub_key, PUB_KEY_SIZE); + if (client) { + client->supports_chacha = (parser.getFeat1() & ADV_FEAT1_CHACHA_CAPABLE) != 0; + } + } +} + void MyMesh::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]; @@ -480,7 +506,8 @@ 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); + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len, use_v2); if (reply) { if (client->out_path_len < 0) { sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY); @@ -534,13 +561,14 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, } else { int reply_len = handleRequest(client, sender_timestamp, &data[4], len - 4); if (reply_len > 0) { // valid command + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); 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); + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, use_v2); if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { - mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); + mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len, use_v2); if (reply) { if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index e7f1fee83..eb4eda2d3 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -148,6 +148,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override ; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; + bool peerSupportsCHACHA(const mesh::Identity& dest) override; + void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) 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/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 4995c55fc..cfe9a5498 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -256,7 +256,8 @@ 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); + bool use_v2 = c->supports_chacha; + auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len, use_v2); if (pkt) { if (c->out_path_len >= 0) { // we have an out_path, so send DIRECT sendDirect(pkt, c->out_path, c->out_path_len); @@ -464,13 +465,14 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con if (reply_len == 0) return; // invalid request + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); if (packet->isRouteFlood()) { // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, - PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, use_v2); if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len, use_v2); if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); } } @@ -496,6 +498,30 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { } } +bool SensorMesh::peerSupportsCHACHA(const mesh::Identity& dest) { + // Look up client by public key + ClientInfo* client = acl.getClient(dest.pub_key, PUB_KEY_SIZE); + if (client) { + return client->supports_chacha; + } + // Unknown peer - default to v1 (safe fallback) + return false; +} + +void SensorMesh::onAdvertRecv(mesh::Packet *packet, const mesh::Identity &id, uint32_t timestamp, + const uint8_t *app_data, size_t app_data_len) { + mesh::Mesh::onAdvertRecv(packet, id, timestamp, app_data, app_data_len); // chain to super impl + + // Update CHACHA capability for known clients + AdvertDataParser parser(app_data, app_data_len); + if (parser.isValid()) { + ClientInfo* client = acl.getClient(id.pub_key, PUB_KEY_SIZE); + if (client) { + client->supports_chacha = (parser.getFeat1() & ADV_FEAT1_CHACHA_CAPABLE) != 0; + } + } +} + void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash) { if (dest.out_path_len < 0) { mesh::Packet* ack = createAck(ack_hash); @@ -533,13 +559,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i from->last_timestamp = timestamp; from->last_activity = getRTCClock()->getCurrentTime(); + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); 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); + PAYLOAD_TYPE_RESPONSE, reply_data, reply_len, use_v2); if (path) sendFlood(path, SERVER_RESPONSE_DELAY); } else { - mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len); + mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len, use_v2); if (reply) { if (from->out_path_len >= 0) { // we have an out_path, so send DIRECT sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY); @@ -565,14 +592,15 @@ 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 + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); 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, use_v2); if (path) sendFlood(path, TXT_ACK_DELAY); } else { sendAckTo(*from, ack_hash); } } - } else if (flags == TXT_TYPE_CLI_DATA) { + } else if (flags == TXT_TYPE_CLI_DATA) { from->last_timestamp = sender_timestamp; from->last_activity = getRTCClock()->getCurrentTime(); @@ -594,7 +622,8 @@ 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); + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); + auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len, use_v2); if (reply) { if (from->out_path_len < 0) { sendFlood(reply, CLI_REPLY_DELAY_MILLIS); diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index c320eb447..829f875a6 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -123,6 +123,8 @@ class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override; int searchPeersByHash(const uint8_t* hash) override; void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override; + bool peerSupportsCHACHA(const mesh::Identity& dest) override; + void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) 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 onControlDataRecv(mesh::Packet* packet) override; diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 0548c9073..c643806a9 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -39,13 +39,18 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int } DispatcherAction Mesh::onRecvPacket(Packet* pkt) { - if (pkt->getPayloadVer() > PAYLOAD_VER_1) { // not supported in this firmware version + if (pkt->getPayloadVer() > PAYLOAD_VER_2) { // v1 and v2 supported MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unsupported packet version", getLogDateTime()); return ACTION_RELEASE; } if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->path_len < MAX_PATH_SIZE) { + // TRACE packet minimum: trace_tag(4) + auth_code(4) + flags(1) = 9 bytes + if (pkt->payload_len < 9) { + MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete TRACE packet", getLogDateTime()); + return ACTION_RELEASE; + } uint8_t i = 0; uint32_t trace_tag; memcpy(&trace_tag, &pkt->payload[i], 4); i += 4; @@ -56,9 +61,10 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t len = pkt->payload_len - i; uint8_t offset = pkt->path_len << path_sz; + uint8_t hash_size = 1 << path_sz; if (offset >= len) { // TRACE has reached end of given path onTraceRecv(pkt, trace_tag, auth_code, flags, pkt->path, &pkt->payload[i], len); - } else if (self_id.isHashMatch(&pkt->payload[i + offset], 1 << path_sz) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { + } else if (i + offset + hash_size <= pkt->payload_len && self_id.isHashMatch(&pkt->payload[i + offset], hash_size) && allowPacketForward(pkt) && !_tables->hasSeen(pkt)) { // append SNR (Not hash!) pkt->path[pkt->path_len++] = (int8_t) (pkt->getSNR()*4); @@ -80,10 +86,9 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) { // check for 'early received' ACK if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { - int i = 0; - uint32_t ack_crc; - memcpy(&ack_crc, &pkt->payload[i], 4); i += 4; - if (i <= pkt->payload_len) { + if (pkt->payload_len >= 4) { + uint32_t ack_crc; + memcpy(&ack_crc, &pkt->payload[0], 4); onAckRecv(pkt, ack_crc); } } @@ -115,12 +120,11 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { switch (pkt->getPayloadType()) { case PAYLOAD_TYPE_ACK: { - int i = 0; - uint32_t ack_crc; - memcpy(&ack_crc, &pkt->payload[i], 4); i += 4; - if (i > pkt->payload_len) { + if (pkt->payload_len < 4) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete ACK packet", getLogDateTime()); } else if (!_tables->hasSeen(pkt)) { + uint32_t ack_crc; + memcpy(&ack_crc, &pkt->payload[0], 4); onAckRecv(pkt, ack_crc); action = routeRecvPacket(pkt); } @@ -134,8 +138,12 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t dest_hash = pkt->payload[i++]; uint8_t src_hash = pkt->payload[i++]; - uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data - if (i + CIPHER_MAC_SIZE >= pkt->payload_len) { + uint8_t* encryptedData = &pkt->payload[i]; // encrypted data (v1: MAC + ciphertext, v2: nonce + ciphertext + tag) + bool is_v2 = (pkt->getPayloadVer() == PAYLOAD_VER_2); + + // Check minimum packet length based on version + int min_encrypted_len = is_v2 ? (CHACHA_NONCE_SIZE + CHACHA_TAG_SIZE) : CIPHER_MAC_SIZE; + if (i + min_encrypted_len >= pkt->payload_len) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); } else if (!_tables->hasSeen(pkt)) { // NOTE: this is a 'first packet wins' impl. When receiving from multiple paths, the first to arrive wins. @@ -151,9 +159,23 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t secret[PUB_KEY_SIZE]; getPeerSharedSecret(secret, j); - // decrypt, checking MAC is valid + // decrypt based on packet version uint8_t data[MAX_PACKET_PAYLOAD]; - int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); + int len; + if (is_v2) { + // ChaCha20-Poly1305 decryption with AAD + // AAD: dest_hash + src_hash + type + ver + uint8_t aad[4]; + aad[0] = dest_hash; + aad[1] = src_hash; + aad[2] = pkt->getPayloadType(); + aad[3] = PAYLOAD_VER_2; + len = Utils::decryptCHACHA(secret, data, encryptedData, pkt->payload_len - i, aad, sizeof(aad)); + } else { + // AES-ECB + HMAC decryption (v1) + len = Utils::MACThenDecrypt(secret, data, encryptedData, pkt->payload_len - i); + } + if (len > 0) { // success! if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { int k = 0; @@ -165,16 +187,23 @@ 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); + // Use same version as received packet for reciprocal path + mesh::Packet* rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, NULL, 0, is_v2); if (rpath) sendDirect(rpath, path, path_len, 500); } } } else { onPeerDataRecv(pkt, pkt->getPayloadType(), j, secret, data, len); } + // SECURITY: Clear sensitive data from stack before breaking + memset(data, 0, sizeof(data)); + memset(secret, 0, sizeof(secret)); found = true; break; } + // SECURITY: Clear buffers even if decryption failed + memset(data, 0, sizeof(data)); + memset(secret, 0, sizeof(secret)); } if (found) { pkt->markDoNotRetransmit(); // packet was for this node, so don't retransmit @@ -191,8 +220,12 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t dest_hash = pkt->payload[i++]; uint8_t* sender_pub_key = &pkt->payload[i]; i += PUB_KEY_SIZE; - uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data - if (i + 2 >= pkt->payload_len) { + uint8_t* encryptedData = &pkt->payload[i]; // encrypted data (v1: MAC + ciphertext, v2: nonce + ciphertext + tag) + bool is_v2 = (pkt->getPayloadVer() == PAYLOAD_VER_2); + + // Check minimum packet length based on version + int min_encrypted_len = is_v2 ? (CHACHA_NONCE_SIZE + CHACHA_TAG_SIZE) : (CIPHER_MAC_SIZE + 1); + if (i + min_encrypted_len > pkt->payload_len) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); } else if (!_tables->hasSeen(pkt)) { if (self_id.isHashMatch(&dest_hash)) { @@ -201,25 +234,46 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { uint8_t secret[PUB_KEY_SIZE]; self_id.calcSharedSecret(secret, sender); - // decrypt, checking MAC is valid + // decrypt based on packet version uint8_t data[MAX_PACKET_PAYLOAD]; - int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i); + int len; + if (is_v2) { + // ChaCha20-Poly1305 decryption with AAD + // AAD: dest_hash + sender_pub_key + type + ver + uint8_t aad[1 + PUB_KEY_SIZE + 2]; + aad[0] = dest_hash; + memcpy(&aad[1], sender_pub_key, PUB_KEY_SIZE); + aad[1 + PUB_KEY_SIZE] = pkt->getPayloadType(); + aad[1 + PUB_KEY_SIZE + 1] = PAYLOAD_VER_2; + len = Utils::decryptCHACHA(secret, data, encryptedData, pkt->payload_len - i, aad, sizeof(aad)); + } else { + // AES-ECB + HMAC decryption (v1) + len = Utils::MACThenDecrypt(secret, data, encryptedData, pkt->payload_len - i); + } + if (len > 0) { // success! onAnonDataRecv(pkt, secret, sender, data, len); pkt->markDoNotRetransmit(); } + // SECURITY: Clear sensitive data from stack + memset(data, 0, sizeof(data)); + memset(secret, 0, sizeof(secret)); } action = routeRecvPacket(pkt); } break; } - case PAYLOAD_TYPE_GRP_DATA: + case PAYLOAD_TYPE_GRP_DATA: case PAYLOAD_TYPE_GRP_TXT: { int i = 0; uint8_t channel_hash = pkt->payload[i++]; - uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data - if (i + 2 >= pkt->payload_len) { + uint8_t* encryptedData = &pkt->payload[i]; // encrypted data (v1: MAC + ciphertext, v2: nonce + ciphertext + tag) + bool is_v2 = (pkt->getPayloadVer() == PAYLOAD_VER_2); + + // Check minimum packet length based on version + int min_encrypted_len = is_v2 ? (CHACHA_NONCE_SIZE + CHACHA_TAG_SIZE) : 2; + if (i + min_encrypted_len >= pkt->payload_len) { MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): incomplete data packet", getLogDateTime()); } else if (!_tables->hasSeen(pkt)) { // scan channels DB, for all matching hashes of 'channel_hash' (max 4 matches supported ATM) @@ -227,13 +281,30 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { int num = searchChannelsByHash(&channel_hash, channels, 4); // for each matching channel, try to decrypt data for (int j = 0; j < num; j++) { - // decrypt, checking MAC is valid + // decrypt based on packet version uint8_t data[MAX_PACKET_PAYLOAD]; - int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i); + int len; + if (is_v2) { + // ChaCha20-Poly1305 decryption with AAD + // AAD: channel_hash + type + ver + uint8_t aad[3]; + aad[0] = channel_hash; + aad[1] = pkt->getPayloadType(); + aad[2] = PAYLOAD_VER_2; + len = Utils::decryptCHACHA(channels[j].secret, data, encryptedData, pkt->payload_len - i, aad, sizeof(aad)); + } else { + // AES-ECB + HMAC decryption (v1) + len = Utils::MACThenDecrypt(channels[j].secret, data, encryptedData, pkt->payload_len - i); + } + if (len > 0) { // success! onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len); + // SECURITY: Clear sensitive data from stack before breaking + memset(data, 0, sizeof(data)); break; } + // SECURITY: Clear buffer even if decryption failed + memset(data, 0, sizeof(data)); } action = routeRecvPacket(pkt); } @@ -430,23 +501,40 @@ Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, siz return packet; } -#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, bool use_v2) { 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, use_v2); } -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) { - if (path_len + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!! +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, bool use_v2) { + // Plaintext layout: [path_len (1)] [path (path_len)] [extra_type (1)] [extra (extra_len)] + // Or if no extra: [dummy_type (1)] [rand (4)] + const int plain_len = 1 + (int)path_len + ((extra_len > 0) ? (1 + (int)extra_len) : (1 + 4)); + + // Payload prefix: dest_hash (1) + src_hash (1) + const int prefix_len = 2; + + if (use_v2) { + // V2: prefix + nonce + ciphertext + tag + if (prefix_len + CHACHA_NONCE_SIZE + plain_len + CHACHA_TAG_SIZE > MAX_PACKET_PAYLOAD) return NULL; + } else { + // V1: prefix + MAC + padded ciphertext + const int padded_len = (plain_len + (CIPHER_BLOCK_SIZE - 1)) & ~(CIPHER_BLOCK_SIZE - 1); + if (prefix_len + CIPHER_MAC_SIZE + padded_len > MAX_PACKET_PAYLOAD) return NULL; + } Packet* packet = obtainNewPacket(); if (packet == NULL) { MESH_DEBUG_PRINTLN("%s Mesh::createPathReturn(): error, packet pool empty", getLogDateTime()); return NULL; } - packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later + + // Set header with version bits + packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT); + if (use_v2) { + packet->header |= (PAYLOAD_VER_2 << PH_VER_SHIFT); + } int len = 0; memcpy(&packet->payload[len], dest_hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; // dest hash @@ -467,7 +555,21 @@ 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 (use_v2) { + // ChaCha20-Poly1305 encryption with AAD + uint8_t nonce[CHACHA_NONCE_SIZE]; + Utils::generateSecureNonce(nonce); + // AAD: dest_hash + src_hash + type + ver + uint8_t aad[4]; + aad[0] = packet->payload[0]; // dest_hash + aad[1] = packet->payload[1]; // src_hash + aad[2] = PAYLOAD_TYPE_PATH; + aad[3] = PAYLOAD_VER_2; + len += Utils::encryptCHACHA(secret, &packet->payload[len], nonce, data, data_len, aad, sizeof(aad)); + } else { + // AES-ECB + HMAC (v1) + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + } } packet->payload_len = len; @@ -475,9 +577,18 @@ 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, bool use_v2) { 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; + // Payload prefix: dest_hash (1) + src_hash (1) + const int prefix_len = 2; + if (use_v2) { + // V2: prefix + nonce + ciphertext + tag + if (prefix_len + (int)data_len + CHACHA_NONCE_SIZE + CHACHA_TAG_SIZE > MAX_PACKET_PAYLOAD) return NULL; + } else { + // V1: prefix + MAC + padded ciphertext + const int padded_len = (((int)data_len) + (CIPHER_BLOCK_SIZE - 1)) & ~(CIPHER_BLOCK_SIZE - 1); + if (prefix_len + CIPHER_MAC_SIZE + padded_len > MAX_PACKET_PAYLOAD) return NULL; + } } else { return NULL; // invalid type } @@ -487,21 +598,47 @@ Packet* Mesh::createDatagram(uint8_t type, const Identity& dest, const uint8_t* MESH_DEBUG_PRINTLN("%s Mesh::createDatagram(): error, packet pool empty", getLogDateTime()); return NULL; } - packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later + + // Set header with version bits + packet->header = (type << PH_TYPE_SHIFT); + if (use_v2) { + packet->header |= (PAYLOAD_VER_2 << PH_VER_SHIFT); + } 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 (use_v2) { + // ChaCha20-Poly1305 encryption with AAD + uint8_t nonce[CHACHA_NONCE_SIZE]; + Utils::generateSecureNonce(nonce); + // AAD: dest_hash + src_hash + type + ver + uint8_t aad[4]; + aad[0] = packet->payload[0]; // dest_hash + aad[1] = packet->payload[1]; // src_hash + aad[2] = type; + aad[3] = PAYLOAD_VER_2; + len += Utils::encryptCHACHA(secret, &packet->payload[len], nonce, data, data_len, aad, sizeof(aad)); + } else { + // AES-ECB + HMAC (v1) + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + } packet->payload_len = len; return packet; } -Packet* Mesh::createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len) { +Packet* Mesh::createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len, bool use_v2) { if (type == PAYLOAD_TYPE_ANON_REQ) { - if (data_len + 1 + PUB_KEY_SIZE + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL; + if (use_v2) { + // V2: dest_hash + pub_key + nonce + ciphertext + tag + if (data_len + 1 + PUB_KEY_SIZE + CHACHA_NONCE_SIZE + CHACHA_TAG_SIZE > MAX_PACKET_PAYLOAD) return NULL; + } else { + // V1: dest_hash + pub_key + MAC + padded ciphertext + if (data_len + 1 + PUB_KEY_SIZE + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL; + } } else { return NULL; // invalid type } @@ -511,7 +648,12 @@ Packet* Mesh::createAnonDatagram(uint8_t type, const LocalIdentity& sender, cons MESH_DEBUG_PRINTLN("%s Mesh::createAnonDatagram(): error, packet pool empty", getLogDateTime()); return NULL; } - packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later + + // Set header with version bits + packet->header = (type << PH_TYPE_SHIFT); + if (use_v2) { + packet->header |= (PAYLOAD_VER_2 << PH_VER_SHIFT); + } int len = 0; if (type == PAYLOAD_TYPE_ANON_REQ) { @@ -520,7 +662,22 @@ Packet* Mesh::createAnonDatagram(uint8_t type, const LocalIdentity& sender, cons } else { // FUTURE: } - len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + + if (use_v2) { + // ChaCha20-Poly1305 encryption with AAD + uint8_t nonce[CHACHA_NONCE_SIZE]; + Utils::generateSecureNonce(nonce); + // AAD: dest_hash + sender_pub_key + type + ver + uint8_t aad[1 + PUB_KEY_SIZE + 2]; + aad[0] = packet->payload[0]; // dest_hash + memcpy(&aad[1], sender.pub_key, PUB_KEY_SIZE); + aad[1 + PUB_KEY_SIZE] = type; + aad[1 + PUB_KEY_SIZE + 1] = PAYLOAD_VER_2; + len += Utils::encryptCHACHA(secret, &packet->payload[len], nonce, data, data_len, aad, sizeof(aad)); + } else { + // AES-ECB + HMAC (v1) + len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len); + } packet->payload_len = len; @@ -529,18 +686,49 @@ Packet* Mesh::createAnonDatagram(uint8_t type, const LocalIdentity& sender, cons Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len) { if (!(type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA)) return NULL; // invalid type - if (data_len + 1 + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL; // too long + + bool use_v2 = (channel.flags & CHANNEL_FLAG_V2) != 0; + + // Payload prefix: channel_hash (PATH_HASH_SIZE, currently 1) + const int prefix_len = PATH_HASH_SIZE; + if (use_v2) { + // V2: prefix + nonce + ciphertext + tag + if (prefix_len + (int)data_len + CHACHA_NONCE_SIZE + CHACHA_TAG_SIZE > MAX_PACKET_PAYLOAD) return NULL; + } else { + // V1: prefix + MAC + padded ciphertext + const int padded_len = (((int)data_len) + (CIPHER_BLOCK_SIZE - 1)) & ~(CIPHER_BLOCK_SIZE - 1); + if (prefix_len + CIPHER_MAC_SIZE + padded_len > MAX_PACKET_PAYLOAD) return NULL; + } Packet* packet = obtainNewPacket(); if (packet == NULL) { MESH_DEBUG_PRINTLN("%s Mesh::createGroupDatagram(): error, packet pool empty", getLogDateTime()); return NULL; } - packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later + + // Set header with version bits + packet->header = (type << PH_TYPE_SHIFT); + if (use_v2) { + packet->header |= (PAYLOAD_VER_2 << PH_VER_SHIFT); + } int len = 0; memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE; - len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], data, data_len); + + if (use_v2) { + // ChaCha20-Poly1305 encryption with AAD + uint8_t nonce[CHACHA_NONCE_SIZE]; + Utils::generateSecureNonce(nonce); + // AAD: channel_hash + type + ver + uint8_t aad[3]; + aad[0] = channel.hash[0]; // channel_hash + aad[1] = type; + aad[2] = PAYLOAD_VER_2; + len += Utils::encryptCHACHA(channel.secret, &packet->payload[len], nonce, data, data_len, aad, sizeof(aad)); + } else { + // AES-ECB + HMAC (v1) + len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], data, data_len); + } packet->payload_len = len; diff --git a/src/Mesh.h b/src/Mesh.h index 00f7ed00f..bf1232ecc 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -4,10 +4,14 @@ namespace mesh { +// Channel flags +#define CHANNEL_FLAG_V2 0x01 // Use ChaCha20-Poly1305 (v2) encryption for this channel + class GroupChannel { public: uint8_t hash[PATH_HASH_SIZE]; uint8_t secret[PUB_KEY_SIZE]; + uint8_t flags; // CHANNEL_FLAG_* bits }; /** @@ -83,6 +87,13 @@ class Mesh : public Dispatcher { */ virtual void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { } + /** + * \brief Check if a peer supports ChaCha20-Poly1305 (v2) encryption. + * \param dest the Identity of the peer to check + * \returns true if peer has advertised CHACHA_CAPABLE flag + */ + virtual bool peerSupportsCHACHA(const Identity& dest) { return false; } + /** * \brief A (now decrypted) data packet has been received (by a known peer). * NOTE: these can be received multiple times (per sender/msg-id), via different routes @@ -182,13 +193,13 @@ class Mesh : public Dispatcher { 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* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len); + Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len, bool use_v2=false); + Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len, bool use_v2=false); Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len); Packet* createAck(uint32_t ack_crc); Packet* createMultiAck(uint32_t ack_crc, uint8_t 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, bool use_v2=false); + 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, bool use_v2=false); 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/MeshCore.h b/src/MeshCore.h index 718660d3b..974cf7891 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -12,10 +12,15 @@ #define CIPHER_KEY_SIZE 16 #define CIPHER_BLOCK_SIZE 16 -// V1 +// V1 (AES-ECB + HMAC) #define CIPHER_MAC_SIZE 2 #define PATH_HASH_SIZE 1 +// V2 (ChaCha20-Poly1305) +#define CHACHA_KEY_SIZE 32 +#define CHACHA_NONCE_SIZE 12 +#define CHACHA_TAG_SIZE 12 + #define MAX_PACKET_PAYLOAD 184 #define MAX_PATH_SIZE 64 #define MAX_TRANS_UNIT 255 diff --git a/src/Packet.cpp b/src/Packet.cpp index 2d54ca459..63e37ddb0 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -38,17 +38,21 @@ uint8_t Packet::writeTo(uint8_t dest[]) const { return i; } -bool Packet::readFrom(const uint8_t src[], uint8_t len) { - uint8_t i = 0; +bool Packet::readFrom(const uint8_t src[], uint16_t len) { + if (len == 0) return false; // minimum packet needs at least header + uint16_t i = 0; header = src[i++]; if (hasTransportCodes()) { + if (i + 4 > len) return false; // need 4 bytes for transport codes memcpy(&transport_codes[0], &src[i], 2); i += 2; memcpy(&transport_codes[1], &src[i], 2); i += 2; } else { transport_codes[0] = transport_codes[1] = 0; } + if (i >= len) return false; // need at least path_len byte path_len = src[i++]; if (path_len > sizeof(path)) return false; // bad encoding + if (i + path_len > len) return false; // path extends beyond buffer memcpy(path, &src[i], path_len); i += path_len; if (i >= len) return false; // bad encoding payload_len = len - i; diff --git a/src/Packet.h b/src/Packet.h index 42d73f416..badc5aad4 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -98,7 +98,7 @@ class Packet { * \param src (IN) buffer containing blob * \param len the packet length (as returned by writeTo()) */ - bool readFrom(const uint8_t src[], uint8_t len); + bool readFrom(const uint8_t src[], uint16_t len); }; } diff --git a/src/Utils.cpp b/src/Utils.cpp index 186c8720a..be7a088b8 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -1,11 +1,22 @@ #include "Utils.h" #include #include +#include #ifdef ARDUINO #include #endif +#ifdef ESP32 + #include +#endif + +#ifdef NRF52_PLATFORM + #include + #include + #include +#endif + namespace mesh { uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) { @@ -142,7 +153,7 @@ int Utils::parseTextParts(char* text, const char* parts[], int max_num, char sep *sp++ = 0; // replace the seperator with a null, and skip past it } } - // if we hit the maximum parts, make sure LAST entry does NOT have separator + // if we hit the maximum parts, make sure LAST entry does NOT have separator while (*sp && *sp != separator) sp++; if (*sp) { *sp = 0; // replace the separator with null @@ -150,4 +161,205 @@ int Utils::parseTextParts(char* text, const char* parts[], int max_num, char sep return num; } -} \ No newline at end of file +// ========== ChaCha20-Poly1305 Implementation ========== + +// Encrypt with ChaCha20-Poly1305 +// Returns: total length (nonce + ciphertext + tag) +int Utils::encryptCHACHA(const uint8_t* key, uint8_t* dest, const uint8_t* nonce, + const uint8_t* plaintext, int plaintext_len, + const uint8_t* aad, int aad_len) { + ChaChaPoly chacha; + + // Set key and nonce + chacha.setKey(key, CHACHA_KEY_SIZE); + chacha.setIV(nonce, CHACHA_NONCE_SIZE); + + // Add AAD if provided + if (aad && aad_len > 0) { + chacha.addAuthData(aad, aad_len); + } + + // Layout: [nonce || ciphertext || tag] + memcpy(dest, nonce, CHACHA_NONCE_SIZE); + + // Encrypt in-place + chacha.encrypt(dest + CHACHA_NONCE_SIZE, plaintext, plaintext_len); + + // Generate and append authentication tag + chacha.computeTag(dest + CHACHA_NONCE_SIZE + plaintext_len, CHACHA_TAG_SIZE); + + return CHACHA_NONCE_SIZE + plaintext_len + CHACHA_TAG_SIZE; +} + +// Decrypt with ChaCha20-Poly1305 +// Returns: plaintext length on success, 0 on authentication failure +int Utils::decryptCHACHA(const uint8_t* key, uint8_t* dest, + const uint8_t* src, int src_len, + const uint8_t* aad, int aad_len) { + // Validate minimum length + if (src_len < CHACHA_NONCE_SIZE + CHACHA_TAG_SIZE) { + return 0; + } + + int ciphertext_len = src_len - CHACHA_NONCE_SIZE - CHACHA_TAG_SIZE; + + // Extract components + const uint8_t* nonce = src; + const uint8_t* ciphertext = src + CHACHA_NONCE_SIZE; + const uint8_t* tag = src + CHACHA_NONCE_SIZE + ciphertext_len; + + ChaChaPoly chacha; + + // Set key and nonce + chacha.setKey(key, CHACHA_KEY_SIZE); + chacha.setIV(nonce, CHACHA_NONCE_SIZE); + + // Add AAD if provided + if (aad && aad_len > 0) { + chacha.addAuthData(aad, aad_len); + } + + // Decrypt (must be done before checkTag due to library requirements) + chacha.decrypt(dest, ciphertext, ciphertext_len); + + // Verify authentication tag (constant-time comparison) + if (!chacha.checkTag(tag, CHACHA_TAG_SIZE)) { + // SECURITY: Zero output buffer on auth failure to prevent use of + // unauthenticated data. Callers must check return value > 0. + memset(dest, 0, ciphertext_len); + return 0; + } + + return ciphertext_len; +} + +// ========== Hardware RNG Implementation ========== + +void Utils::getHardwareRandom(uint8_t* dest, size_t size) { +#ifdef ESP32 + // ESP32: Use hardware TRNG (very fast, ~1μs) + for (size_t i = 0; i < size; i += 4) { + uint32_t rand_val = esp_random(); + size_t bytes_to_copy = (size - i) < 4 ? (size - i) : 4; + memcpy(&dest[i], &rand_val, bytes_to_copy); + } + +#elif defined(NRF52_PLATFORM) + // nRF52: Check if SoftDevice is enabled (BLE active) + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + + if (sd_enabled) { + // SoftDevice is active - must use its API to avoid hardfault + size_t remaining = size; + size_t offset = 0; + while (remaining > 0) { + uint8_t available = 0; + // Check how many random bytes are available + sd_rand_application_bytes_available_get(&available); + if (available > 0) { + uint8_t to_get = (remaining < available) ? remaining : available; + sd_rand_application_vector_get(dest + offset, to_get); + offset += to_get; + remaining -= to_get; + } + // If not enough bytes available, wait briefly for RNG to generate more + if (remaining > 0 && available == 0) { + delayMicroseconds(10); + } + } + } else { + // SoftDevice not active - safe to use RNG peripheral directly + NRF_RNG->TASKS_START = 1; + for (size_t i = 0; i < size; i++) { + while (!NRF_RNG->EVENTS_VALRDY); // Wait for random byte + dest[i] = NRF_RNG->VALUE; + NRF_RNG->EVENTS_VALRDY = 0; + } + NRF_RNG->TASKS_STOP = 1; // Stop RNG to save power + } + +#elif defined(RP2040_PLATFORM) + // RP2040: Use ROSC-based hardware random (fast, ~1μs) + for (size_t i = 0; i < size; i += 4) { + uint32_t rand_val = rp2040.hwrand32(); + size_t bytes_to_copy = (size - i) < 4 ? (size - i) : 4; + memcpy(&dest[i], &rand_val, bytes_to_copy); + } + +#elif defined(STM32_PLATFORM) + // STM32WL: Use hardware RNG peripheral + // Most STM32 variants with LoRa (STM32WL) have hardware RNG + #if defined(HAL_RNG_MODULE_ENABLED) + extern RNG_HandleTypeDef hrng; + for (size_t i = 0; i < size; i += 4) { + uint32_t rand_val; + HAL_RNG_GenerateRandomNumber(&hrng, &rand_val); + size_t bytes_to_copy = (size - i) < 4 ? (size - i) : 4; + memcpy(&dest[i], &rand_val, bytes_to_copy); + } + #else + // No hardware RNG - fall back but mix with more entropy sources + // WARNING: This path should be avoided for v2 encryption + MESH_DEBUG_PRINTLN("WARNING: STM32 without HAL_RNG - using weak entropy!"); + static uint32_t stm32_entropy_pool = 0xDEADBEEF; + for (size_t i = 0; i < size; i += 4) { + // Mix multiple sources for better entropy + stm32_entropy_pool ^= micros(); + stm32_entropy_pool ^= (millis() << 16); + stm32_entropy_pool = (stm32_entropy_pool * 1103515245) + 12345; // LCG step + uint32_t rand_val = stm32_entropy_pool ^ (i * 0x5DEECE66D); + size_t bytes_to_copy = (size - i) < 4 ? (size - i) : 4; + memcpy(&dest[i], &rand_val, bytes_to_copy); + } + #endif + +#else + // No secure RNG available - FAIL COMPILATION for v2 encryption safety + #error "No hardware RNG available. ChaCha20-Poly1305 requires secure nonce generation. \ + Supported platforms: ESP32, nRF52, RP2040, STM32 with HAL_RNG." +#endif +} + +void Utils::getHighQualityRandom(RNG* rng, uint8_t* dest, size_t size) { + if (rng) { + // Use provided RNG (typically RadioNoiseListener for high entropy) + rng->random(dest, size); + } else { + // Fallback to hardware RNG if no RNG provided + getHardwareRandom(dest, size); + } +} + +// ========== Secure Nonce Generation ========== + +// Static state for hybrid nonce generation +static uint32_t s_nonce_boot_id = 0; +static uint32_t s_nonce_counter = 0; + +void Utils::generateSecureNonce(uint8_t* nonce) { + // Hybrid nonce format: [boot_id (4)] [counter (4)] [random (4)] + // This ensures uniqueness even if: + // - RNG has bias or periodicity + // - Many messages sent quickly + // - Device reboots (new boot_id) + + // Initialize boot_id once per boot with random value + if (s_nonce_boot_id == 0) { + getHardwareRandom((uint8_t*)&s_nonce_boot_id, 4); + // Ensure non-zero (extremely unlikely, but handle it) + if (s_nonce_boot_id == 0) s_nonce_boot_id = 1; + } + + // Copy boot_id (bytes 0-3) + memcpy(nonce, &s_nonce_boot_id, 4); + + // Copy and increment counter (bytes 4-7) + memcpy(nonce + 4, &s_nonce_counter, 4); + s_nonce_counter++; + + // Add random salt (bytes 8-11) for additional entropy + getHardwareRandom(nonce + 8, 4); +} + +} diff --git a/src/Utils.h b/src/Utils.h index 5736b8747..fcecdd1a2 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -54,6 +54,60 @@ class Utils { */ static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len); + /** + * \brief Encrypts with ChaCha20-Poly1305 AEAD cipher (v2 encryption). + * Layout: [nonce (12 bytes)] [ciphertext] [tag (12 bytes)] + * \param key 32-byte encryption key (shared secret) + * \param dest destination buffer for encrypted output + * \param nonce 12-byte nonce (must be unique per message) + * \param plaintext data to encrypt + * \param plaintext_len length of plaintext + * \param aad optional additional authenticated data (can be NULL) + * \param aad_len length of AAD (0 if no AAD) + * \returns total length of encrypted output (nonce + ciphertext + tag) + */ + static int encryptCHACHA(const uint8_t* key, uint8_t* dest, const uint8_t* nonce, + const uint8_t* plaintext, int plaintext_len, + const uint8_t* aad = nullptr, int aad_len = 0); + + /** + * \brief Decrypts with ChaCha20-Poly1305 AEAD cipher (v2 decryption). + * Expects layout: [nonce (12 bytes)] [ciphertext] [tag (12 bytes)] + * \param key 32-byte decryption key (shared secret) + * \param dest destination buffer for decrypted plaintext + * \param src encrypted data (nonce + ciphertext + tag) + * \param src_len length of encrypted data + * \param aad optional additional authenticated data (can be NULL) + * \param aad_len length of AAD (0 if no AAD) + * \returns plaintext length on success, 0 on authentication failure + */ + static int decryptCHACHA(const uint8_t* key, uint8_t* dest, + const uint8_t* src, int src_len, + const uint8_t* aad = nullptr, int aad_len = 0); + + /** + * \brief Get hardware random bytes from platform-specific TRNG. + * \param dest destination buffer + * \param size number of random bytes to generate + */ + static void getHardwareRandom(uint8_t* dest, size_t size); + + /** + * \brief Get high-quality random bytes, preferring provided RNG. + * \param rng optional RNG instance (e.g. RadioNoiseListener) + * \param dest destination buffer + * \param size number of random bytes to generate + */ + static void getHighQualityRandom(RNG* rng, uint8_t* dest, size_t size); + + /** + * \brief Generate a secure nonce for ChaCha20-Poly1305. + * Uses hybrid format: [boot_id (4)] [counter (4)] [random (4)] + * This ensures uniqueness even if RNG has bias/periodicity. + * \param nonce destination buffer (must be CHACHA_NONCE_SIZE bytes) + */ + static void generateSecureNonce(uint8_t* nonce); + /** * \brief converts 'src' bytes with given length to Hex representation, and null terminates. */ diff --git a/src/helpers/AdvertDataHelpers.h b/src/helpers/AdvertDataHelpers.h index abe14cbd0..2e2dcebe9 100644 --- a/src/helpers/AdvertDataHelpers.h +++ b/src/helpers/AdvertDataHelpers.h @@ -16,6 +16,9 @@ #define ADV_FEAT2_MASK 0x40 // FUTURE #define ADV_NAME_MASK 0x80 +// FEAT1 bit flags (encoded when ADV_FEAT1_MASK is set) +#define ADV_FEAT1_CHACHA_CAPABLE 0x0001 + class AdvertDataBuilder { uint8_t _type; bool _has_loc; diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index aebfc1b64..a0c5cc20c 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -21,6 +21,7 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) { uint8_t app_data_len; { AdvertDataBuilder builder(ADV_TYPE_CHAT, name); + builder.setFeat1(ADV_FEAT1_CHACHA_CAPABLE); // Advertise v2 encryption support app_data_len = builder.encodeTo(app_data); } @@ -32,6 +33,7 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl uint8_t app_data_len; { AdvertDataBuilder builder(ADV_TYPE_CHAT, name, lat, lon); + builder.setFeat1(ADV_FEAT1_CHACHA_CAPABLE); // Advertise v2 encryption support app_data_len = builder.encodeTo(app_data); } @@ -165,6 +167,8 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, } from->last_advert_timestamp = timestamp; from->lastmod = getRTCClock()->getCurrentTime(); + // Track ChaCha capability from advertisement FEAT1 flags + from->supports_chacha = (parser.getFeat1() & ADV_FEAT1_CHACHA_CAPABLE) != 0; onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know } @@ -188,6 +192,15 @@ void BaseChatMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { } } +bool BaseChatMesh::peerSupportsCHACHA(const mesh::Identity& dest) { + // Look up contact by public key + ContactInfo* contact = lookupContactByPubKey(dest.pub_key, PUB_KEY_SIZE); + if (contact) { + return contact->supports_chacha; + } + return false; // Unknown contact, default to v1 +} + 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) { @@ -214,8 +227,9 @@ 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 + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); 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, use_v2); if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { sendAckTo(from, ack_hash); @@ -226,7 +240,8 @@ 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); + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); + mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0, use_v2); if (path) sendFloodScoped(from, path); } } else if (flags == TXT_TYPE_SIGNED_PLAIN) { @@ -241,8 +256,9 @@ 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 + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); 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, use_v2); if (path) sendFloodScoped(from, path, TXT_ACK_DELAY); } else { sendAckTo(from, ack_hash); @@ -255,13 +271,14 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender memcpy(&sender_timestamp, data, 4); uint8_t reply_len = onContactRequest(from, sender_timestamp, &data[4], len - 4, temp_buf); if (reply_len > 0) { + bool use_v2 = (packet->getPayloadVer() == PAYLOAD_VER_2); 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, use_v2); 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, use_v2); if (reply) { if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); @@ -327,7 +344,8 @@ 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); + bool use_v2 = contact.supports_chacha; + mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0, use_v2); if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay } @@ -344,8 +362,9 @@ int BaseChatMesh::searchChannelsByHash(const uint8_t* hash, mesh::GroupChannel d #endif void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mesh::GroupChannel& channel, uint8_t* data, size_t len) { - uint8_t txt_type = data[4]; - if (type == PAYLOAD_TYPE_GRP_TXT && len > 5 && (txt_type >> 2) == 0) { // 0 = plain text msg + if (type == PAYLOAD_TYPE_GRP_TXT && len > 5) { + uint8_t txt_type = data[4]; + if ((txt_type >> 2) == 0) { // 0 = plain text msg uint32_t timestamp; memcpy(×tamp, data, 4); @@ -354,6 +373,7 @@ void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mes // notify UI of this new message onChannelMessageRecv(channel, packet, timestamp, (const char *) &data[5]); // let UI know + } } } @@ -376,7 +396,8 @@ 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); + bool use_v2 = peerSupportsCHACHA(recipient.id); + return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, len, use_v2); } int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout) { @@ -407,7 +428,8 @@ 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); + bool use_v2 = peerSupportsCHACHA(recipient.id); + auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len, use_v2); if (pkt == NULL) return MSG_SEND_FAILED; uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -429,9 +451,14 @@ bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& chan memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique temp[4] = 0; // TXT_TYPE_PLAIN - sprintf((char *) &temp[5], "%s: ", sender_name); // : - char *ep = strchr((char *) &temp[5], 0); - int prefix_len = ep - (char *) &temp[5]; + int n = snprintf((char *) &temp[5], sizeof(temp) - 5, "%s: ", sender_name); // : + if (n < 0) return false; + int prefix_len = strlen((char *) &temp[5]); + if (prefix_len > MAX_TEXT_LEN) { + prefix_len = MAX_TEXT_LEN; + temp[5 + prefix_len] = 0; + } + char *ep = (char *) &temp[5 + prefix_len]; if (text_len + prefix_len > MAX_TEXT_LEN) text_len = MAX_TEXT_LEN - prefix_len; memcpy(ep, text, text_len); @@ -496,7 +523,8 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, tlen = 4 + len; } - pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, tlen); + bool use_v2 = peerSupportsCHACHA(recipient.id); + pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, tlen, use_v2); } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -517,11 +545,13 @@ int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data, mesh::Packet* pkt; { uint8_t temp[MAX_PACKET_PAYLOAD]; + if (len > MAX_PACKET_PAYLOAD - 4) return MSG_SEND_FAILED; tag = getRTCClock()->getCurrentTimeUnique(); memcpy(temp, &tag, 4); // tag to match later (also extra blob to help make packet_hash unique) memcpy(&temp[4], data, len); - pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + len); + bool use_v2 = peerSupportsCHACHA(recipient.id); + pkt = createAnonDatagram(PAYLOAD_TYPE_ANON_REQ, self_id, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + len, use_v2); } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -548,7 +578,8 @@ 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); + bool use_v2 = peerSupportsCHACHA(recipient.id); + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len, use_v2); } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -575,7 +606,8 @@ 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)); + bool use_v2 = peerSupportsCHACHA(recipient.id); + pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp), use_v2); } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); @@ -698,7 +730,8 @@ 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); + bool use_v2 = peerSupportsCHACHA(contact->id); + auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->getSharedSecret(self_id), data, 9, use_v2); if (pkt) { sendDirect(pkt, contact->out_path, contact->out_path_len); } diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index fd391b980..741902150 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -125,6 +125,7 @@ class BaseChatMesh : public mesh::Mesh { 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; + bool peerSupportsCHACHA(const mesh::Identity& dest) 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/ClientACL.h b/src/helpers/ClientACL.h index 1b650edd2..36babe741 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -18,6 +18,7 @@ struct ClientInfo { uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) uint32_t last_activity; // by OUR clock (transient) + bool supports_chacha; // Peer advertised CHACHA_CAPABLE in FEAT1 (transient) union { struct { uint32_t sync_since; // sync messages SINCE this timestamp (by OUR clock) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2fc93006b..34c2fa332 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -183,12 +183,15 @@ void CommonCLI::savePrefs() { uint8_t CommonCLI::buildAdvertData(uint8_t node_type, uint8_t* app_data) { if (_prefs->advert_loc_policy == ADVERT_LOC_NONE) { AdvertDataBuilder builder(node_type, _prefs->node_name); + builder.setFeat1(ADV_FEAT1_CHACHA_CAPABLE); // Advertise v2 encryption support return builder.encodeTo(app_data); } else if (_prefs->advert_loc_policy == ADVERT_LOC_SHARE) { AdvertDataBuilder builder(node_type, _prefs->node_name, _sensors->node_lat, _sensors->node_lon); + builder.setFeat1(ADV_FEAT1_CHACHA_CAPABLE); // Advertise v2 encryption support return builder.encodeTo(app_data); } else { AdvertDataBuilder builder(node_type, _prefs->node_name, _prefs->node_lat, _prefs->node_lon); + builder.setFeat1(ADV_FEAT1_CHACHA_CAPABLE); // Advertise v2 encryption support return builder.encodeTo(app_data); } } diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index eff07741a..c6951d230 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -10,6 +10,7 @@ struct ContactInfo { uint8_t flags; int8_t out_path_len; mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated + bool supports_chacha; // Peer advertised CHACHA_CAPABLE in FEAT1 uint8_t out_path[MAX_PATH_SIZE]; uint32_t last_advert_timestamp; // by THEIR clock uint32_t lastmod; // by OUR clock diff --git a/src/helpers/esp32/ESPNOWRadio.cpp b/src/helpers/esp32/ESPNOWRadio.cpp index ced19f911..2ea3cc812 100644 --- a/src/helpers/esp32/ESPNOWRadio.cpp +++ b/src/helpers/esp32/ESPNOWRadio.cpp @@ -18,8 +18,13 @@ static void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) { static void OnDataRecv(const uint8_t *mac, const uint8_t *data, int len) { ESPNOW_DEBUG_PRINTLN("Recv: len = %d", len); + if (len <= 0) { + last_rx_len = 0; + return; + } + if (len > 255) len = 255; memcpy(rx_buf, data, len); - last_rx_len = len; + last_rx_len = (uint8_t)len; } void ESPNOWRadio::init() { @@ -100,12 +105,14 @@ float ESPNOWRadio::getLastSNR() const { return 0; } int ESPNOWRadio::recvRaw(uint8_t* bytes, int sz) { int len = last_rx_len; - if (last_rx_len > 0) { - memcpy(bytes, rx_buf, last_rx_len); - last_rx_len = 0; - n_recv++; - } - return len; + if (len <= 0 || sz <= 0) return 0; + + int copy_len = len; + if (copy_len > sz) copy_len = sz; + memcpy(bytes, rx_buf, copy_len); + last_rx_len = 0; + n_recv++; + return copy_len; } uint32_t ESPNOWRadio::getEstAirtimeFor(int len_bytes) {