Skip to content

Commit c04942a

Browse files
committed
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
1 parent 259d50e commit c04942a

5 files changed

Lines changed: 75 additions & 31 deletions

File tree

src/Mesh.cpp

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,16 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
149149
uint8_t data[MAX_PACKET_PAYLOAD];
150150
int macAndDataLen = pkt->payload_len - i;
151151

152-
// Try ECB first (Phase 1: all senders use ECB), then AEAD-4 fallback.
153-
// IMPORTANT: Phase 2 MUST swap to AEAD-first. ECB-first has a 1/65536
154-
// false-positive rate on AEAD packets (nonce bytes matching truncated HMAC),
155-
// producing garbage plaintext. AEAD-first has only 1/2^32 false-positive on
156-
// ECB packets, which is negligible.
157-
int len = Utils::MACThenDecrypt(secret, data, macAndData, macAndDataLen);
158-
if (len <= 0) {
159-
uint8_t assoc[3] = { pkt->header, dest_hash, src_hash };
152+
// Try-both decode: AEAD-first for peers known to support it (avoids 1/65536
153+
// ECB false-positive on AEAD packets), ECB-first for unknown/legacy peers.
154+
uint8_t assoc[3] = { pkt->header, dest_hash, src_hash };
155+
int len;
156+
if (getPeerFlags(j) & CONTACT_FLAG_AEAD) {
160157
len = Utils::aeadDecrypt(secret, data, macAndData, macAndDataLen, assoc, 3, dest_hash, src_hash);
158+
if (len <= 0) len = Utils::MACThenDecrypt(secret, data, macAndData, macAndDataLen);
159+
} else {
160+
len = Utils::MACThenDecrypt(secret, data, macAndData, macAndDataLen);
161+
if (len <= 0) len = Utils::aeadDecrypt(secret, data, macAndData, macAndDataLen, assoc, 3, dest_hash, src_hash);
161162
}
162163
if (len > 0) { // success!
163164
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
@@ -172,7 +173,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
172173
if (onPeerPathRecv(pkt, j, secret, path, path_len, extra_type, extra, extra_len)) {
173174
if (pkt->isRouteFlood()) {
174175
// send a reciprocal return path to sender, but send DIRECTLY!
175-
mesh::Packet* rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, NULL, 0);
176+
mesh::Packet* rpath = createPathReturn(&src_hash, secret, pkt->path, pkt->path_len, 0, NULL, 0, getPeerNextAeadNonce(j));
176177
if (rpath) sendDirect(rpath, path, path_len, 500);
177178
}
178179
}
@@ -452,13 +453,13 @@ Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, siz
452453

453454
#define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE)
454455

455-
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) {
456+
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) {
456457
uint8_t dest_hash[PATH_HASH_SIZE];
457458
dest.copyHashTo(dest_hash);
458-
return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len);
459+
return createPathReturn(dest_hash, secret, path, path_len, extra_type, extra, extra_len, aead_nonce);
459460
}
460461

461-
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) {
462+
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) {
462463
uint8_t path_hash_size = (path_len >> 6) + 1;
463464
uint8_t path_hash_count = path_len & 63;
464465

@@ -490,17 +491,25 @@ Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret,
490491
getRNG()->random(&data[data_len], 4); data_len += 4;
491492
}
492493

493-
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
494+
if (aead_nonce) {
495+
uint8_t dh = packet->payload[0];
496+
uint8_t sh = packet->payload[1];
497+
uint8_t assoc[3] = { packet->header, dh, sh };
498+
len += Utils::aeadEncrypt(secret, &packet->payload[len], data, data_len, assoc, 3, aead_nonce, dh, sh);
499+
} else {
500+
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
501+
}
494502
}
495503

496504
packet->payload_len = len;
497505

498506
return packet;
499507
}
500508

501-
Packet* Mesh::createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len) {
509+
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) {
502510
if (type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE) {
503-
if (data_len + CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL;
511+
size_t max_overhead = aead_nonce ? (AEAD_NONCE_SIZE + AEAD_TAG_SIZE) : (CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE-1);
512+
if (data_len + max_overhead > MAX_PACKET_PAYLOAD) return NULL;
504513
} else {
505514
return NULL; // invalid type
506515
}
@@ -515,7 +524,15 @@ Packet* Mesh::createDatagram(uint8_t type, const Identity& dest, const uint8_t*
515524
int len = 0;
516525
len += dest.copyHashTo(&packet->payload[len]); // dest hash
517526
len += self_id.copyHashTo(&packet->payload[len]); // src hash
518-
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
527+
528+
if (aead_nonce) {
529+
uint8_t dest_hash = packet->payload[0];
530+
uint8_t src_hash = packet->payload[1];
531+
uint8_t assoc[3] = { packet->header, dest_hash, src_hash };
532+
len += Utils::aeadEncrypt(secret, &packet->payload[len], data, data_len, assoc, 3, aead_nonce, dest_hash, src_hash);
533+
} else {
534+
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
535+
}
519536

520537
packet->payload_len = len;
521538

src/Mesh.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ class Mesh : public Dispatcher {
8282
* \param peer_idx index of peer, [0..n) where n is what searchPeersByHash() returned
8383
*/
8484
virtual void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { }
85+
virtual uint8_t getPeerFlags(int peer_idx) { return 0; }
86+
virtual uint16_t getPeerNextAeadNonce(int peer_idx) { return 0; }
8587

8688
/**
8789
* \brief A (now decrypted) data packet has been received (by a known peer).
@@ -182,13 +184,13 @@ class Mesh : public Dispatcher {
182184
RTCClock* getRTCClock() const { return _rtc; }
183185

184186
Packet* createAdvert(const LocalIdentity& id, const uint8_t* app_data=NULL, size_t app_data_len=0);
185-
Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len);
187+
Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len, uint16_t aead_nonce=0);
186188
Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len);
187189
Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
188190
Packet* createAck(uint32_t ack_crc);
189191
Packet* createMultiAck(uint32_t ack_crc, uint8_t remaining);
190-
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);
191-
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);
192+
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);
193+
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);
192194
Packet* createRawData(const uint8_t* data, size_t len);
193195
Packet* createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0);
194196
Packet* createControlData(const uint8_t* data, size_t len);

src/helpers/BaseChatMesh.cpp

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,22 @@ void BaseChatMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) {
208208
}
209209
}
210210

211+
uint8_t BaseChatMesh::getPeerFlags(int peer_idx) {
212+
int i = matching_peer_indexes[peer_idx];
213+
if (i >= 0 && i < num_contacts) {
214+
return contacts[i].flags;
215+
}
216+
return 0;
217+
}
218+
219+
uint16_t BaseChatMesh::getPeerNextAeadNonce(int peer_idx) {
220+
int i = matching_peer_indexes[peer_idx];
221+
if (i >= 0 && i < num_contacts) {
222+
return contacts[i].nextAeadNonce();
223+
}
224+
return 0;
225+
}
226+
211227
void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) {
212228
int i = matching_peer_indexes[sender_idx];
213229
if (i < 0 || i >= num_contacts) {
@@ -235,7 +251,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
235251
if (packet->isRouteFlood()) {
236252
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
237253
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
238-
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
254+
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4, from.nextAeadNonce());
239255
if (path) sendFloodScoped(from, path, TXT_ACK_DELAY);
240256
} else {
241257
sendAckTo(from, ack_hash);
@@ -246,7 +262,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
246262

247263
if (packet->isRouteFlood()) {
248264
// let this sender know path TO here, so they can use sendDirect() (NOTE: no ACK as extra)
249-
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0);
265+
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len, 0, NULL, 0, from.nextAeadNonce());
250266
if (path) sendFloodScoped(from, path);
251267
}
252268
} else if (flags == TXT_TYPE_SIGNED_PLAIN) {
@@ -262,7 +278,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
262278
if (packet->isRouteFlood()) {
263279
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK
264280
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
265-
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4);
281+
PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4, from.nextAeadNonce());
266282
if (path) sendFloodScoped(from, path, TXT_ACK_DELAY);
267283
} else {
268284
sendAckTo(from, ack_hash);
@@ -278,10 +294,10 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
278294
if (packet->isRouteFlood()) {
279295
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
280296
mesh::Packet* path = createPathReturn(from.id, secret, packet->path, packet->path_len,
281-
PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len);
297+
PAYLOAD_TYPE_RESPONSE, temp_buf, reply_len, from.nextAeadNonce());
282298
if (path) sendFloodScoped(from, path, SERVER_RESPONSE_DELAY);
283299
} else {
284-
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len);
300+
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len, from.nextAeadNonce());
285301
if (reply) {
286302
if (from.out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT
287303
sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY);
@@ -347,7 +363,7 @@ void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
347363
void BaseChatMesh::handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len) {
348364
// NOTE: simplest impl is just to re-send a reciprocal return path to sender (DIRECTLY)
349365
// override this method in various firmwares, if there's a better strategy
350-
mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0);
366+
mesh::Packet* rpath = createPathReturn(contact.id, contact.getSharedSecret(self_id), path, path_len, 0, NULL, 0, contact.nextAeadNonce());
351367
if (rpath) sendDirect(rpath, contact.out_path, contact.out_path_len, 3000); // 3 second delay
352368
}
353369

@@ -396,7 +412,7 @@ mesh::Packet* BaseChatMesh::composeMsgPacket(const ContactInfo& recipient, uint3
396412
temp[len++] = attempt; // hide attempt number at tail end of payload
397413
}
398414

399-
return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, len);
415+
return createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, len, recipient.nextAeadNonce());
400416
}
401417

402418
int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint8_t attempt, const char* text, uint32_t& expected_ack, uint32_t& est_timeout) {
@@ -427,7 +443,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest
427443
temp[4] = (attempt & 3) | (TXT_TYPE_CLI_DATA << 2);
428444
memcpy(&temp[5], text, text_len + 1);
429445

430-
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len);
446+
auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, recipient.id, recipient.getSharedSecret(self_id), temp, 5 + text_len, recipient.nextAeadNonce());
431447
if (pkt == NULL) return MSG_SEND_FAILED;
432448

433449
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
@@ -568,7 +584,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_
568584
memcpy(temp, &tag, 4); // mostly an extra blob to help make packet_hash unique
569585
memcpy(&temp[4], req_data, data_len);
570586

571-
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len);
587+
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, 4 + data_len, recipient.nextAeadNonce());
572588
}
573589
if (pkt) {
574590
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
@@ -595,7 +611,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u
595611
memset(&temp[5], 0, 4); // reserved (possibly for 'since' param)
596612
getRNG()->random(&temp[9], 4); // random blob to help make packet-hash unique
597613

598-
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp));
614+
pkt = createDatagram(PAYLOAD_TYPE_REQ, recipient.id, recipient.getSharedSecret(self_id), temp, sizeof(temp), recipient.nextAeadNonce());
599615
}
600616
if (pkt) {
601617
uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength());
@@ -718,7 +734,7 @@ void BaseChatMesh::checkConnections() {
718734
// calc expected ACK reply
719735
mesh::Utils::sha256((uint8_t *)&connections[i].expected_ack, 4, data, 9, self_id.pub_key, PUB_KEY_SIZE);
720736

721-
auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->getSharedSecret(self_id), data, 9);
737+
auto pkt = createDatagram(PAYLOAD_TYPE_REQ, contact->id, contact->getSharedSecret(self_id), data, 9, contact->nextAeadNonce());
722738
if (pkt) {
723739
sendDirect(pkt, contact->out_path, contact->out_path_len);
724740
}

src/helpers/BaseChatMesh.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ class BaseChatMesh : public mesh::Mesh {
126126
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;
127127
int searchPeersByHash(const uint8_t* hash) override;
128128
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
129+
uint8_t getPeerFlags(int peer_idx) override;
130+
uint16_t getPeerNextAeadNonce(int peer_idx) override;
129131
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
130132
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;
131133
void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override;

src/helpers/ContactInfo.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,14 @@ struct ContactInfo {
1717
uint32_t lastmod; // by OUR clock
1818
int32_t gps_lat, gps_lon; // 6 dec places
1919
uint32_t sync_since;
20-
uint16_t aead_nonce; // per-peer AEAD nonce counter for DMs (not used for group messages), seeded from HW RNG
20+
mutable uint16_t aead_nonce; // per-peer AEAD nonce counter for DMs (not used for group messages), seeded from HW RNG
21+
22+
// Returns next AEAD nonce (post-increment) if peer supports AEAD, 0 otherwise.
23+
// When 0, callers use ECB encryption.
24+
uint16_t nextAeadNonce() const {
25+
if (flags & CONTACT_FLAG_AEAD) return ++aead_nonce;
26+
return 0;
27+
}
2128

2229
const uint8_t* getSharedSecret(const mesh::LocalIdentity& self_id) const {
2330
if (!shared_secret_valid) {

0 commit comments

Comments
 (0)