Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 66 additions & 14 deletions src/Mesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,28 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint8_t secret[PUB_KEY_SIZE];
getPeerSharedSecret(secret, j);

// decrypt, checking MAC is valid
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
int macAndDataLen = pkt->payload_len - i;

// 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.
// Mask out route type bits — they are set after encryption and vary per hop.
uint8_t assoc[3] = { (uint8_t)(pkt->header & ~PH_ROUTE_MASK), dest_hash, src_hash };
int len;
bool decoded_aead = false;
if (getPeerFlags(j) & CONTACT_FLAG_AEAD) {
len = Utils::aeadDecrypt(secret, data, macAndData, macAndDataLen, assoc, 3, dest_hash, src_hash);
if (len > 0) decoded_aead = true;
else 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) decoded_aead = true;
}
}
if (len > 0) { // success!
if (decoded_aead) onPeerAeadDetected(j);
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
int k = 0;
uint8_t path_len = data[k++];
Expand All @@ -165,7 +183,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);
}
}
Expand Down Expand Up @@ -201,9 +219,16 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
uint8_t secret[PUB_KEY_SIZE];
self_id.calcSharedSecret(secret, sender);

// decrypt, checking MAC is valid
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
int macAndDataLen = pkt->payload_len - i;

// Try ECB first (Phase 1), then AEAD-4 fallback.
// Phase 2 MUST swap to AEAD-first (see peer message comment above).
int len = Utils::MACThenDecrypt(secret, data, macAndData, macAndDataLen);
if (len <= 0) {
uint8_t assoc[2] = { (uint8_t)(pkt->header & ~PH_ROUTE_MASK), dest_hash };
len = Utils::aeadDecrypt(secret, data, macAndData, macAndDataLen, assoc, 2, dest_hash, 0);
}
if (len > 0) { // success!
onAnonDataRecv(pkt, secret, sender, data, len);
pkt->markDoNotRetransmit();
Expand All @@ -227,9 +252,19 @@ 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
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i);
int macAndDataLen = pkt->payload_len - i;

// Try ECB first (Phase 1), then AEAD-4 fallback.
// Phase 2 MUST swap to AEAD-first (see peer message comment above).
// Note: group channels share a key, so nonce collisions across senders can leak
// P1 XOR P2 for colliding message pairs (no key recovery). Bounded risk, mainly
// worthwhile for public/hashtag channels where the PSK is already widely known.
int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, macAndDataLen);
if (len <= 0) {
uint8_t assoc[2] = { (uint8_t)(pkt->header & ~PH_ROUTE_MASK), channel_hash };
len = Utils::aeadDecrypt(channels[j].secret, data, macAndData, macAndDataLen, assoc, 2, channel_hash, 0);
}
if (len > 0) { // success!
onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len);
break;
Expand Down Expand Up @@ -432,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) {
if (path_len + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!!

Packet* packet = obtainNewPacket();
Expand Down Expand Up @@ -467,17 +502,26 @@ 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] = { (uint8_t)(packet->header & ~PH_ROUTE_MASK), 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;

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 hash_prefix = PATH_HASH_SIZE * 2; // dest_hash + src_hash
size_t max_overhead = aead_nonce ? (AEAD_NONCE_SIZE + AEAD_TAG_SIZE) : (CIPHER_MAC_SIZE + CIPHER_BLOCK_SIZE-1);
if (data_len + hash_prefix + max_overhead > MAX_PACKET_PAYLOAD) return NULL;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're working in unsigned and data_len is controlled by the caller, lengths close to max will cause this check's additions to wrap and potentially bypass the check.

Consider something like calculating:

size_t max_payload_data = MAX_PACKET_PAYLOAD - hash_prefix - max_overhead;                                                                                                                                      
if (data_len > max_payload_data) return NULL; 

} else {
return NULL; // invalid type
}
Expand All @@ -492,7 +536,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] = { (uint8_t)(packet->header & ~PH_ROUTE_MASK), 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;

Expand Down
9 changes: 6 additions & 3 deletions src/Mesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ class Mesh : public Dispatcher {
* \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; }
virtual void onPeerAeadDetected(int peer_idx) { }

/**
* \brief A (now decrypted) data packet has been received (by a known peer).
Expand Down Expand Up @@ -182,13 +185,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* 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(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, 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);
Expand Down
6 changes: 6 additions & 0 deletions src/MeshCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@
#define CIPHER_MAC_SIZE 2
#define PATH_HASH_SIZE 1

// AEAD-4 (ChaChaPoly) encryption
#define AEAD_TAG_SIZE 4
#define AEAD_NONCE_SIZE 2
#define CONTACT_FLAG_AEAD 0x02 // bit 1 of ContactInfo.flags (bit 0 = favourite)
#define FEAT1_AEAD_SUPPORT 0x0001 // bit 0 of feat1 uint16_t

#define MAX_PACKET_PAYLOAD 184
#define MAX_PATH_SIZE 64
#define MAX_TRANS_UNIT 255
Expand Down
115 changes: 115 additions & 0 deletions src/Utils.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "Utils.h"
#include <AES.h>
#include <SHA256.h>
#include <ChaChaPoly.h>

#ifdef ARDUINO
#include <Arduino.h>
Expand Down Expand Up @@ -87,6 +88,120 @@ int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uin
return 0; // invalid HMAC
}

/*
* AEAD-4: ChaCha20-Poly1305 authenticated encryption with 4-byte tag.
*
* Wire format (replaces ECB's [HMAC:2][ciphertext:N*16]):
* [nonce:2] [ciphertext:M] [tag:4] (M = exact plaintext length)
*
* Key derivation (per-message, eliminates nonce-reuse catastrophe):
* msg_key[32] = HMAC-SHA256(shared_secret[32], nonce_hi || nonce_lo || dest_hash || src_hash)
* Including hashes makes keys direction-dependent: Alice->Bob and Bob->Alice derive
* different keys even with the same nonce (for 255/256 peer pairs; the 1/256 where
* dest_hash == src_hash remains a residual risk inherent to 1-byte hashes).
*
* IV construction (12 bytes, from on-wire fields):
* iv[12] = { nonce_hi, nonce_lo, dest_hash, src_hash, 0, 0, 0, 0, 0, 0, 0, 0 }
*
* Associated data (authenticated but not encrypted):
* Peer msgs: header || dest_hash || src_hash
* Anon reqs: header || dest_hash
* Group msgs: header || channel_hash
*
* Nonce: 16-bit counter per peer, seeded from HW RNG on boot. With per-message
* key derivation, even a nonce collision (across reboots) only leaks P1 XOR P2
* for that message pair — no key recovery, no impact on other messages.
*
* Group channels: all members share the same key, so cross-sender nonce
* collisions are possible (~300 msgs for 50% chance with random nonces).
* Damage is bounded (message pair leak, no key recovery).
*/
int Utils::aeadEncrypt(const uint8_t* shared_secret,
uint8_t* dest,
const uint8_t* src, int src_len,
const uint8_t* assoc_data, int assoc_len,
uint16_t nonce_counter,
uint8_t dest_hash, uint8_t src_hash) {
if (src_len <= 0) return 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider a bit more defense here?

Suggested change
if (src_len <= 0) return 0;
if (src_len <= 0 || src_len > MAX_PACKET_PAYLOAD) return 0;
if (assoc_len < 0 || assoc_len > MAX_PACKET_PAYLOAD) return 0;


// Write nonce to output
dest[0] = (uint8_t)(nonce_counter >> 8);
dest[1] = (uint8_t)(nonce_counter & 0xFF);

// Derive per-message key: HMAC-SHA256(shared_secret, nonce || dest_hash || src_hash)
// Including hashes makes the key direction-dependent, preventing keystream reuse
// when Alice->Bob and Bob->Alice use the same nonce (255/256 peer pairs).
uint8_t msg_key[32];
{
uint8_t kdf_input[AEAD_NONCE_SIZE + 2] = { dest[0], dest[1], dest_hash, src_hash };
SHA256 sha;
sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
sha.update(kdf_input, sizeof(kdf_input));
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, msg_key, 32);
}

// Build 12-byte IV from on-wire fields
uint8_t iv[12];
iv[0] = dest[0]; // nonce_hi
iv[1] = dest[1]; // nonce_lo
iv[2] = dest_hash;
iv[3] = src_hash;
memset(&iv[4], 0, 8);

ChaChaPoly cipher;
cipher.setKey(msg_key, 32);
cipher.setIV(iv, 12);
cipher.addAuthData(assoc_data, assoc_len);
cipher.encrypt(dest + AEAD_NONCE_SIZE, src, src_len);
cipher.computeTag(dest + AEAD_NONCE_SIZE + src_len, AEAD_TAG_SIZE);
cipher.clear();
memset(msg_key, 0, 32);

return AEAD_NONCE_SIZE + src_len + AEAD_TAG_SIZE;
}

int Utils::aeadDecrypt(const uint8_t* shared_secret,
uint8_t* dest,
const uint8_t* src, int src_len,
const uint8_t* assoc_data, int assoc_len,
uint8_t dest_hash, uint8_t src_hash) {
// Minimum: nonce(2) + at least 1 byte ciphertext + tag(4)
if (src_len < AEAD_NONCE_SIZE + 1 + AEAD_TAG_SIZE) return 0;

int ct_len = src_len - AEAD_NONCE_SIZE - AEAD_TAG_SIZE;

// Derive per-message key: HMAC-SHA256(shared_secret, nonce || dest_hash || src_hash)
uint8_t msg_key[32];
{
uint8_t kdf_input[AEAD_NONCE_SIZE + 2] = { src[0], src[1], dest_hash, src_hash };
SHA256 sha;
sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
sha.update(kdf_input, sizeof(kdf_input));
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, msg_key, 32);
}

// Build 12-byte IV from on-wire fields
uint8_t iv[12];
iv[0] = src[0]; // nonce_hi
iv[1] = src[1]; // nonce_lo
iv[2] = dest_hash;
iv[3] = src_hash;
memset(&iv[4], 0, 8);

ChaChaPoly cipher;
cipher.setKey(msg_key, 32);
cipher.setIV(iv, 12);
cipher.addAuthData(assoc_data, assoc_len);
cipher.decrypt(dest, src + AEAD_NONCE_SIZE, ct_len);

bool valid = cipher.checkTag(src + AEAD_NONCE_SIZE + ct_len, AEAD_TAG_SIZE);
cipher.clear();
memset(msg_key, 0, 32);
if (!valid) memset(dest, 0, ct_len);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ECB path doesn't do this. Maybe it should?


return valid ? ct_len : 0;
}

static const char hex_chars[] = "0123456789ABCDEF";

void Utils::toHex(char* dest, const uint8_t* src, size_t len) {
Expand Down
23 changes: 23 additions & 0 deletions src/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,29 @@ class Utils {
*/
static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);

/**
* \brief Encrypt with ChaChaPoly AEAD. Derives per-message key via HMAC-SHA256(shared_secret, nonce || dest_hash || src_hash).
* Output: [nonce:2][ciphertext:src_len][tag:4]
* \returns total output length (AEAD_NONCE_SIZE + src_len + AEAD_TAG_SIZE), or 0 on failure
*/
static int aeadEncrypt(const uint8_t* shared_secret,
uint8_t* dest,
const uint8_t* src, int src_len,
const uint8_t* assoc_data, int assoc_len,
uint16_t nonce_counter,
uint8_t dest_hash, uint8_t src_hash);

/**
* \brief Decrypt with ChaChaPoly AEAD. Derives per-message key via HMAC-SHA256(shared_secret, nonce || dest_hash || src_hash).
* Input: [nonce:2][ciphertext:M][tag:4]
* \returns plaintext length, or 0 if tag verification fails
*/
static int aeadDecrypt(const uint8_t* shared_secret,
uint8_t* dest,
const uint8_t* src, int src_len,
const uint8_t* assoc_data, int assoc_len,
uint8_t dest_hash, uint8_t src_hash);

/**
* \brief converts 'src' bytes with given length to Hex representation, and null terminates.
*/
Expand Down
Loading