Skip to content
Closed
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
11 changes: 11 additions & 0 deletions src/mesh/Channels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -459,4 +459,15 @@ bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash)
int16_t Channels::setActiveByIndex(ChannelIndex channelIndex)
{
return setCrypto(channelIndex);
}

bool Channels::isAeadEnabled(ChannelIndex chIndex)
{
#if !(MESHTASTIC_EXCLUDE_PKI)
meshtastic_Channel &ch = getByIndex(chIndex);
return ch.has_settings && ch.settings.aead_enabled;
#else
(void)chIndex;
return false;
#endif
}
4 changes: 4 additions & 0 deletions src/mesh/Channels.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ class Channels

int16_t getHash(ChannelIndex i) { return hashes[i]; }

/** Returns true if the specified channel has AEAD (AES-CCM) encryption enabled.
* Always returns false when PKI is excluded from the build. */
bool isAeadEnabled(ChannelIndex chIndex);

private:
/** Given a channel index, change to use the crypto key specified by that index
*
Expand Down
26 changes: 26 additions & 0 deletions src/mesh/CryptoEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,32 @@ bool CryptoEngine::setDHPublicKey(uint8_t *pubKey)
return true;
}

bool CryptoEngine::encryptPacketAead(uint32_t fromNode, uint64_t packetId, size_t numBytes, const uint8_t *bytesIn,
uint8_t *bytesOut)
{
if (key.length <= 0)
return false;
initNonce(fromNode, packetId);
aes_ccm_ae(key.bytes, key.length, nonce, MESHTASTIC_CHANNEL_AEAD_OVERHEAD, bytesIn, numBytes, nullptr, 0, bytesOut,
bytesOut + numBytes);
return true;
}

size_t CryptoEngine::decryptPacketAead(uint32_t fromNode, uint64_t packetId, size_t rawSize, const uint8_t *bytesIn,
uint8_t *bytesOut)
{
if (key.length <= 0 || rawSize <= MESHTASTIC_CHANNEL_AEAD_OVERHEAD)
return 0;
size_t cryptLen = rawSize - MESHTASTIC_CHANNEL_AEAD_OVERHEAD;
const uint8_t *auth = bytesIn + cryptLen;
initNonce(fromNode, packetId);
if (!aes_ccm_ad(key.bytes, key.length, nonce, MESHTASTIC_CHANNEL_AEAD_OVERHEAD, bytesIn, cryptLen, nullptr, 0, auth,
bytesOut)) {
return 0;
}
return cryptLen;
}

#endif
concurrency::Lock *cryptLock;

Expand Down
20 changes: 20 additions & 0 deletions src/mesh/CryptoEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ class CryptoEngine
virtual void encryptPacket(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes);
virtual void decrypt(uint32_t fromNode, uint64_t packetId, size_t numBytes, uint8_t *bytes);
virtual void encryptAESCtr(CryptoKey key, uint8_t *nonce, size_t numBytes, uint8_t *bytes);

#if !(MESHTASTIC_EXCLUDE_PKI)
/** Bytes appended by AES-CCM authenticated encryption on channel packets (8-byte tag). */
#define MESHTASTIC_CHANNEL_AEAD_OVERHEAD 8

/**
* Encrypt a channel packet with AES-CCM (authenticated encryption).
* Writes ciphertext to bytesOut and appends an 8-byte auth tag.
* @return true on success.
*/
bool encryptPacketAead(uint32_t fromNode, uint64_t packetId, size_t numBytes, const uint8_t *bytesIn, uint8_t *bytesOut);

/**
* Decrypt a channel packet with AES-CCM (authenticated decryption).
* Reads ciphertext+tag from bytesIn, writes plaintext to bytesOut.
* @return plaintext size on success, 0 on authentication failure.
*/
size_t decryptPacketAead(uint32_t fromNode, uint64_t packetId, size_t rawSize, const uint8_t *bytesIn, uint8_t *bytesOut);
#endif

#ifndef PIO_UNIT_TESTING
protected:
#endif
Expand Down
38 changes: 29 additions & 9 deletions src/mesh/Router.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -471,16 +471,26 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
if (channels.decryptForHash(chIndex, p->channel)) {
// we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a
// fresh copy for each decrypt attempt.
memcpy(bytes, p->encrypted.bytes, rawSize);
// Try to decrypt the packet if we can
crypto->decrypt(p->from, p->id, rawSize, bytes);
size_t decryptedSize = rawSize;
#if !(MESHTASTIC_EXCLUDE_PKI)
if (channels.isAeadEnabled(chIndex)) {
// AEAD channel: decrypt with authentication, no fallback
decryptedSize = crypto->decryptPacketAead(p->from, p->id, rawSize, p->encrypted.bytes, bytes);
if (decryptedSize == 0)
continue; // Tag mismatch — try next channel
} else
#endif
{
memcpy(bytes, p->encrypted.bytes, rawSize);
crypto->decrypt(p->from, p->id, rawSize, bytes);
}

// printBytes("plaintext", bytes, p->encrypted.size);

// Take those raw bytes and convert them back into a well structured protobuf we can understand
meshtastic_Data decodedtmp;
memset(&decodedtmp, 0, sizeof(decodedtmp));
if (!pb_decode_from_bytes(bytes, rawSize, &meshtastic_Data_msg, &decodedtmp)) {
if (!pb_decode_from_bytes(bytes, decryptedSize, &meshtastic_Data_msg, &decodedtmp)) {
LOG_ERROR("Invalid protobufs in received mesh packet id=0x%08x (bad psk?)!", p->id);
} else if (decodedtmp.portnum == meshtastic_PortNum_UNKNOWN_APP) {
LOG_ERROR("Invalid portnum (bad psk?)!");
Expand Down Expand Up @@ -597,13 +607,18 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
}
} */

if (numbytes + MESHTASTIC_HEADER_LENGTH > MAX_LORA_PAYLOAD_LEN)
return meshtastic_Routing_Error_TOO_LARGE;

// printBytes("plaintext", bytes, numbytes);

ChannelIndex chIndex = p->channel; // keep as a local because we are about to change it

#if !(MESHTASTIC_EXCLUDE_PKI)
size_t channelAeadOverhead = channels.isAeadEnabled(chIndex) ? MESHTASTIC_CHANNEL_AEAD_OVERHEAD : 0;
#else
size_t channelAeadOverhead = 0;
#endif
if (numbytes + MESHTASTIC_HEADER_LENGTH + channelAeadOverhead > MAX_LORA_PAYLOAD_LEN)
return meshtastic_Routing_Error_TOO_LARGE;

#if !(MESHTASTIC_EXCLUDE_PKI)
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
// We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node
Expand Down Expand Up @@ -656,8 +671,13 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
// No suitable channel could be found for
return meshtastic_Routing_Error_NO_CHANNEL;
}
crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes);
memcpy(p->encrypted.bytes, bytes, numbytes);
if (channelAeadOverhead > 0) {
crypto->encryptPacketAead(getFrom(p), p->id, numbytes, bytes, p->encrypted.bytes);
numbytes += MESHTASTIC_CHANNEL_AEAD_OVERHEAD;
} else {
crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes);
memcpy(p->encrypted.bytes, bytes, numbytes);
}
}
#else
if (p->pki_encrypted == true) {
Expand Down
15 changes: 10 additions & 5 deletions src/mesh/generated/meshtastic/channel.pb.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ typedef struct _meshtastic_ChannelSettings {
/* Per-channel module settings. */
bool has_module_settings;
meshtastic_ModuleSettings module_settings;
/* If true, packets on this channel use AES-CCM authenticated encryption
instead of plain AES-CTR. All nodes on the channel must agree on this setting. */
bool aead_enabled;
} meshtastic_ChannelSettings;

/* A pair of a channel number, mode and the (sharable) settings for that channel */
Expand Down Expand Up @@ -128,10 +131,10 @@ extern "C" {


/* Initializer values for message structs */
#define meshtastic_ChannelSettings_init_default {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_default}
#define meshtastic_ChannelSettings_init_default {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_default, 0}
#define meshtastic_ModuleSettings_init_default {0, 0}
#define meshtastic_Channel_init_default {0, false, meshtastic_ChannelSettings_init_default, _meshtastic_Channel_Role_MIN}
#define meshtastic_ChannelSettings_init_zero {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_zero}
#define meshtastic_ChannelSettings_init_zero {0, {0, {0}}, "", 0, 0, 0, false, meshtastic_ModuleSettings_init_zero, 0}
#define meshtastic_ModuleSettings_init_zero {0, 0}
#define meshtastic_Channel_init_zero {0, false, meshtastic_ChannelSettings_init_zero, _meshtastic_Channel_Role_MIN}

Expand All @@ -145,6 +148,7 @@ extern "C" {
#define meshtastic_ChannelSettings_uplink_enabled_tag 5
#define meshtastic_ChannelSettings_downlink_enabled_tag 6
#define meshtastic_ChannelSettings_module_settings_tag 7
#define meshtastic_ChannelSettings_aead_enabled_tag 8
#define meshtastic_Channel_index_tag 1
#define meshtastic_Channel_settings_tag 2
#define meshtastic_Channel_role_tag 3
Expand All @@ -157,7 +161,8 @@ X(a, STATIC, SINGULAR, STRING, name, 3) \
X(a, STATIC, SINGULAR, FIXED32, id, 4) \
X(a, STATIC, SINGULAR, BOOL, uplink_enabled, 5) \
X(a, STATIC, SINGULAR, BOOL, downlink_enabled, 6) \
X(a, STATIC, OPTIONAL, MESSAGE, module_settings, 7)
X(a, STATIC, OPTIONAL, MESSAGE, module_settings, 7) \
X(a, STATIC, SINGULAR, BOOL, aead_enabled, 8)
#define meshtastic_ChannelSettings_CALLBACK NULL
#define meshtastic_ChannelSettings_DEFAULT NULL
#define meshtastic_ChannelSettings_module_settings_MSGTYPE meshtastic_ModuleSettings
Expand Down Expand Up @@ -187,8 +192,8 @@ extern const pb_msgdesc_t meshtastic_Channel_msg;

/* Maximum encoded size of messages (where known) */
#define MESHTASTIC_MESHTASTIC_CHANNEL_PB_H_MAX_SIZE meshtastic_Channel_size
#define meshtastic_ChannelSettings_size 72
#define meshtastic_Channel_size 87
#define meshtastic_ChannelSettings_size 74
#define meshtastic_Channel_size 89
#define meshtastic_ModuleSettings_size 8

#ifdef __cplusplus
Expand Down
38 changes: 38 additions & 0 deletions test/test_crypto/test_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,43 @@ void test_AES_CTR(void)
TEST_ASSERT_EQUAL_MEMORY(expected, plain, 16);
}

void test_channel_AEAD(void)
{
// Set a channel key (AES-256)
CryptoKey k;
k.length = 32;
HexToBytes(k.bytes, "603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4");
crypto->setKey(k);

uint32_t fromNode = 0x1234;
uint64_t packetId = 0xDEADBEEF;
const char *plaintext = "Hello mesh!";
size_t plainLen = strlen(plaintext);

uint8_t encrypted[128] __attribute__((__aligned__));
uint8_t decrypted[128] __attribute__((__aligned__));

// Encrypt
TEST_ASSERT(crypto->encryptPacketAead(fromNode, packetId, plainLen, (const uint8_t *)plaintext, encrypted));
size_t cipherLen = plainLen + MESHTASTIC_CHANNEL_AEAD_OVERHEAD;

// Decrypt round-trip
size_t result = crypto->decryptPacketAead(fromNode, packetId, cipherLen, encrypted, decrypted);
TEST_ASSERT_EQUAL(plainLen, result);
TEST_ASSERT_EQUAL_MEMORY(plaintext, decrypted, plainLen);

// Tamper with ciphertext — should fail authentication
encrypted[0] ^= 0xFF;
result = crypto->decryptPacketAead(fromNode, packetId, cipherLen, encrypted, decrypted);
TEST_ASSERT_EQUAL(0, result);
encrypted[0] ^= 0xFF; // restore

// Tamper with auth tag — should fail authentication
encrypted[plainLen] ^= 0xFF;
result = crypto->decryptPacketAead(fromNode, packetId, cipherLen, encrypted, decrypted);
TEST_ASSERT_EQUAL(0, result);
}

void setup()
{
// NOTE!!! Wait for >2 secs
Expand All @@ -192,6 +229,7 @@ void setup()
RUN_TEST(test_DH25519);
RUN_TEST(test_AES_CTR);
RUN_TEST(test_PKC);
RUN_TEST(test_channel_AEAD);
exit(UNITY_END()); // stop unit testing
}

Expand Down