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+
925namespace mesh {
1026
1127uint32_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