Skip to content

Commit c643ce0

Browse files
committed
Implement Ascon-128 AEAD encryption with per-packet key derivation
1 parent 86051da commit c643ce0

3 files changed

Lines changed: 362 additions & 3 deletions

File tree

src/MeshCore.h

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,37 @@
1212
#define CIPHER_KEY_SIZE 16
1313
#define CIPHER_BLOCK_SIZE 16
1414

15-
// V1
15+
// V1 (AES-ECB + HMAC) - Legacy encryption
1616
#define CIPHER_MAC_SIZE 2
1717
#define PATH_HASH_SIZE 1
1818

19+
// Ascon-128 AEAD encryption with per-packet key derivation
20+
//
21+
// Design goals:
22+
// 1. Minimize airtime (8 bytes overhead: 4-byte counter + 4-byte tag)
23+
// 2. Strong security through per-packet rekeying
24+
// 3. Simple try-decrypt fallback (no capability flags needed)
25+
//
26+
// Per-packet key derivation:
27+
// packet_key = HMAC-SHA256(shared_secret, counter)[0:16]
28+
//
29+
// This enables a short 4-byte tag because:
30+
// - Each message uses a unique derived key
31+
// - Attacker can't accumulate forgery attempts across messages
32+
// - At LoRa's 500ms/packet, brute forcing 2^32 attempts takes 68 years
33+
//
34+
// Counter: 4 bytes, initialized to random value at boot, increments per packet.
35+
// Random boot offset prevents counter reuse across reboots when RTC is unreliable.
36+
//
37+
// Backwards compatibility:
38+
// - Try Ascon decrypt first, fall back to legacy AES-ECB+HMAC on failure
39+
// - Old clients silently drop Ascon packets (tag check fails)
40+
#define ASCON_KEY_SIZE 16 // Ascon-128 uses 128-bit key
41+
#define ASCON_NONCE_SIZE 16 // Ascon-128 uses 128-bit nonce (internal)
42+
#define ASCON_COUNTER_SIZE 4 // Transmitted counter (random boot offset + sequence)
43+
#define ASCON_TAG_SIZE 4 // 32-bit tag (safe with per-packet rekey)
44+
#define ASCON_OVERHEAD 8 // Total overhead: counter + tag
45+
1946
#define MAX_PACKET_PAYLOAD 184
2047
#define MAX_PATH_SIZE 64
2148
#define MAX_TRANS_UNIT 255

src/Utils.cpp

Lines changed: 269 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,26 @@
22
#include <AES.h>
33
#include <SHA256.h>
44

5+
// LibAscon - lightweight AEAD cipher (CC0 license)
6+
// https://github.com/TheMatjaz/LibAscon
7+
extern "C" {
8+
#include <ascon.h>
9+
}
10+
511
#ifdef ARDUINO
612
#include <Arduino.h>
713
#endif
814

15+
#ifdef ESP32
16+
#include <esp_random.h>
17+
#endif
18+
19+
#ifdef NRF52_PLATFORM
20+
#include <nrf.h>
21+
#include <nrf_sdm.h>
22+
#include <nrf_soc.h>
23+
#endif
24+
925
namespace mesh {
1026

1127
uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) {
@@ -142,12 +158,263 @@ int Utils::parseTextParts(char* text, const char* parts[], int max_num, char sep
142158
*sp++ = 0; // replace the seperator with a null, and skip past it
143159
}
144160
}
145-
// if we hit the maximum parts, make sure LAST entry does NOT have separator
161+
// if we hit the maximum parts, make sure LAST entry does NOT have separator
146162
while (*sp && *sp != separator) sp++;
147163
if (*sp) {
148164
*sp = 0; // replace the separator with null
149165
}
150166
return num;
151167
}
152168

153-
}
169+
// ========== Ascon Encryption: Ascon-128 with Per-Packet Key Derivation ==========
170+
//
171+
// Security design:
172+
// 1. Per-packet key derivation: key = HMAC-SHA256(shared_secret, counter)[0:16]
173+
// This enables short 4-byte tags (safe because key changes every packet)
174+
// 2. Nonce: counter zero-padded to 16 bytes
175+
// 3. Tag: 4 bytes (2^32 forgery attempts, but key changes before exhaustion)
176+
//
177+
// Packet format: [counter:4][ciphertext:N][tag:4] = 8 bytes overhead
178+
179+
// Derive per-packet key from shared secret and counter
180+
// key = HMAC-SHA256(shared_secret, counter)[0:16]
181+
static void derivePacketKey(uint8_t* packet_key, const uint8_t* shared_secret, const uint8_t* counter) {
182+
SHA256 sha;
183+
uint8_t hmac_out[32];
184+
sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
185+
sha.update(counter, ASCON_COUNTER_SIZE);
186+
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac_out, 32);
187+
memcpy(packet_key, hmac_out, ASCON_KEY_SIZE);
188+
}
189+
190+
// Expand 4-byte counter to 16-byte nonce (zero-padded)
191+
static void expandNonce(uint8_t* nonce, const uint8_t* counter) {
192+
memset(nonce, 0, ASCON_NONCE_SIZE);
193+
memcpy(nonce + ASCON_NONCE_SIZE - ASCON_COUNTER_SIZE, counter, ASCON_COUNTER_SIZE);
194+
}
195+
196+
// Ascon Encrypt with Ascon-128 and per-packet key derivation
197+
// Returns: total length (counter + ciphertext + tag)
198+
int Utils::encryptAscon(const uint8_t* shared_secret, uint8_t* dest,
199+
const uint8_t* plaintext, int plaintext_len) {
200+
// Generate unique counter
201+
uint8_t counter[ASCON_COUNTER_SIZE];
202+
generateCounter(counter);
203+
204+
// Derive per-packet key
205+
uint8_t packet_key[ASCON_KEY_SIZE];
206+
derivePacketKey(packet_key, shared_secret, counter);
207+
208+
// Expand counter to full nonce
209+
uint8_t nonce[ASCON_NONCE_SIZE];
210+
expandNonce(nonce, counter);
211+
212+
// Write counter to output
213+
memcpy(dest, counter, ASCON_COUNTER_SIZE);
214+
215+
// Encrypt with Ascon-128 using LibAscon offline API
216+
// Output: ciphertext directly after counter, tag after ciphertext
217+
uint8_t* ciphertext_out = dest + ASCON_COUNTER_SIZE;
218+
uint8_t* tag_out = dest + ASCON_COUNTER_SIZE + plaintext_len;
219+
220+
ascon_aead128_encrypt(
221+
ciphertext_out, // ciphertext output
222+
tag_out, // tag output (truncated)
223+
packet_key, // 16-byte key
224+
nonce, // 16-byte nonce
225+
NULL, // no associated data
226+
plaintext, // plaintext input
227+
0, // assoc_data_len
228+
(size_t)plaintext_len, // plaintext_len
229+
ASCON_TAG_SIZE // tag_len (4 bytes - built-in truncation!)
230+
);
231+
232+
// Clear sensitive material
233+
memset(packet_key, 0, ASCON_KEY_SIZE);
234+
235+
return ASCON_COUNTER_SIZE + plaintext_len + ASCON_TAG_SIZE;
236+
}
237+
238+
// Ascon Decrypt with Ascon-128 and per-packet key derivation
239+
// Returns: plaintext length on success, 0 on authentication failure
240+
int Utils::decryptAscon(const uint8_t* shared_secret, uint8_t* dest,
241+
const uint8_t* src, int src_len) {
242+
// Validate minimum length
243+
if (src_len < ASCON_COUNTER_SIZE + ASCON_TAG_SIZE) {
244+
return 0;
245+
}
246+
247+
int ciphertext_len = src_len - ASCON_COUNTER_SIZE - ASCON_TAG_SIZE;
248+
249+
// Extract components
250+
const uint8_t* counter = src;
251+
const uint8_t* ciphertext = src + ASCON_COUNTER_SIZE;
252+
const uint8_t* tag = src + ASCON_COUNTER_SIZE + ciphertext_len;
253+
254+
// Derive per-packet key
255+
uint8_t packet_key[ASCON_KEY_SIZE];
256+
derivePacketKey(packet_key, shared_secret, counter);
257+
258+
// Expand counter to full nonce
259+
uint8_t nonce[ASCON_NONCE_SIZE];
260+
expandNonce(nonce, counter);
261+
262+
// Decrypt with Ascon-128 using LibAscon offline API
263+
// LibAscon supports truncated tags directly via expected_tag_len!
264+
bool valid = ascon_aead128_decrypt(
265+
dest, // plaintext output
266+
packet_key, // 16-byte key
267+
nonce, // 16-byte nonce
268+
NULL, // no associated data
269+
ciphertext, // ciphertext input
270+
tag, // expected tag (truncated)
271+
0, // assoc_data_len
272+
(size_t)ciphertext_len, // ciphertext_len
273+
ASCON_TAG_SIZE // expected_tag_len (4 bytes - built-in truncation!)
274+
);
275+
276+
// Clear sensitive material
277+
memset(packet_key, 0, ASCON_KEY_SIZE);
278+
279+
if (!valid) {
280+
// Authentication failed - zero output
281+
memset(dest, 0, ciphertext_len);
282+
return 0;
283+
}
284+
285+
return ciphertext_len;
286+
}
287+
288+
// Unified decryption: try Ascon first, fall back to legacy
289+
int Utils::decryptAuto(const uint8_t* shared_secret, uint8_t* dest,
290+
const uint8_t* src, int src_len, bool* was_ascon) {
291+
// Try Ascon first - this is the happy path for updated clients
292+
int len = decryptAscon(shared_secret, dest, src, src_len);
293+
if (len > 0) {
294+
if (was_ascon) *was_ascon = true;
295+
return len;
296+
}
297+
298+
// Fall back to legacy (AES-ECB + HMAC) for old clients
299+
len = MACThenDecrypt(shared_secret, dest, src, src_len);
300+
if (len > 0) {
301+
if (was_ascon) *was_ascon = false;
302+
}
303+
return len;
304+
}
305+
306+
// ========== Hardware RNG Implementation ==========
307+
308+
void Utils::getHardwareRandom(uint8_t* dest, size_t size) {
309+
#ifdef ESP32
310+
// ESP32: Use hardware TRNG (very fast, ~1μs)
311+
for (size_t i = 0; i < size; i += 4) {
312+
uint32_t rand_val = esp_random();
313+
size_t bytes_to_copy = (size - i) < 4 ? (size - i) : 4;
314+
memcpy(&dest[i], &rand_val, bytes_to_copy);
315+
}
316+
317+
#elif defined(NRF52_PLATFORM)
318+
// nRF52: Check if SoftDevice is enabled (BLE active)
319+
uint8_t sd_enabled = 0;
320+
sd_softdevice_is_enabled(&sd_enabled);
321+
322+
if (sd_enabled) {
323+
// SoftDevice is active - must use its API to avoid hardfault
324+
size_t remaining = size;
325+
size_t offset = 0;
326+
while (remaining > 0) {
327+
uint8_t available = 0;
328+
// Check how many random bytes are available
329+
sd_rand_application_bytes_available_get(&available);
330+
if (available > 0) {
331+
uint8_t to_get = (remaining < available) ? remaining : available;
332+
sd_rand_application_vector_get(dest + offset, to_get);
333+
offset += to_get;
334+
remaining -= to_get;
335+
}
336+
// If not enough bytes available, wait briefly for RNG to generate more
337+
if (remaining > 0 && available == 0) {
338+
delayMicroseconds(10);
339+
}
340+
}
341+
} else {
342+
// SoftDevice not active - safe to use RNG peripheral directly
343+
NRF_RNG->TASKS_START = 1;
344+
for (size_t i = 0; i < size; i++) {
345+
while (!NRF_RNG->EVENTS_VALRDY); // Wait for random byte
346+
dest[i] = NRF_RNG->VALUE;
347+
NRF_RNG->EVENTS_VALRDY = 0;
348+
}
349+
NRF_RNG->TASKS_STOP = 1; // Stop RNG to save power
350+
}
351+
352+
#elif defined(RP2040_PLATFORM)
353+
// RP2040: Use ROSC-based hardware random (fast, ~1μs)
354+
for (size_t i = 0; i < size; i += 4) {
355+
uint32_t rand_val = rp2040.hwrand32();
356+
size_t bytes_to_copy = (size - i) < 4 ? (size - i) : 4;
357+
memcpy(&dest[i], &rand_val, bytes_to_copy);
358+
}
359+
360+
#elif defined(STM32_PLATFORM)
361+
// STM32WL: Use hardware RNG peripheral
362+
// Most STM32 variants with LoRa (STM32WL) have hardware RNG
363+
#if defined(HAL_RNG_MODULE_ENABLED)
364+
extern RNG_HandleTypeDef hrng;
365+
for (size_t i = 0; i < size; i += 4) {
366+
uint32_t rand_val;
367+
HAL_RNG_GenerateRandomNumber(&hrng, &rand_val);
368+
size_t bytes_to_copy = (size - i) < 4 ? (size - i) : 4;
369+
memcpy(&dest[i], &rand_val, bytes_to_copy);
370+
}
371+
#else
372+
// No hardware RNG - fall back but mix with more entropy sources
373+
// WARNING: This path should be avoided for v2 encryption
374+
MESH_DEBUG_PRINTLN("WARNING: STM32 without HAL_RNG - using weak entropy!");
375+
static uint32_t stm32_entropy_pool = 0xDEADBEEF;
376+
for (size_t i = 0; i < size; i += 4) {
377+
// Mix multiple sources for better entropy
378+
stm32_entropy_pool ^= micros();
379+
stm32_entropy_pool ^= (millis() << 16);
380+
stm32_entropy_pool = (stm32_entropy_pool * 1103515245) + 12345; // LCG step
381+
uint32_t rand_val = stm32_entropy_pool ^ (i * 0x5DEECE66D);
382+
size_t bytes_to_copy = (size - i) < 4 ? (size - i) : 4;
383+
memcpy(&dest[i], &rand_val, bytes_to_copy);
384+
}
385+
#endif
386+
387+
#else
388+
// No secure RNG available - FAIL COMPILATION for v2 encryption safety
389+
#error "No hardware RNG available. Ascon-128 AEAD requires secure nonce generation. \
390+
Supported platforms: ESP32, nRF52, RP2040, STM32 with HAL_RNG."
391+
#endif
392+
}
393+
394+
// ========== Counter Generation for Ascon ==========
395+
396+
// Counter with random boot offset for Ascon nonce.
397+
// Since we derive a fresh key per packet (key = HMAC-SHA256(shared_secret, counter)),
398+
// the counter only needs to be unique, not unpredictable. However, if timestamp
399+
// replay protection can't be trusted (nodes rebooting to hardcoded dates), we need
400+
// the counter itself to be unlikely to repeat across reboots.
401+
//
402+
// Using random boot offset: even with weak RNG, the probability of collision is
403+
// low enough (1 in 2^32 with perfect RNG, worse with bad RNG but still helpful).
404+
static uint32_t s_ascon_counter = 0;
405+
static bool s_counter_initialized = false;
406+
407+
void Utils::initAsconCounter(RNG& rng) {
408+
if (!s_counter_initialized) {
409+
// Random starting point to avoid counter reuse across reboots
410+
rng.random((uint8_t*)&s_ascon_counter, sizeof(s_ascon_counter));
411+
s_counter_initialized = true;
412+
}
413+
}
414+
415+
void Utils::generateCounter(uint8_t* counter) {
416+
memcpy(counter, &s_ascon_counter, ASCON_COUNTER_SIZE);
417+
s_ascon_counter++;
418+
}
419+
420+
}

0 commit comments

Comments
 (0)