Skip to content

Commit 77f378d

Browse files
KomzpathebenternCopilotjp-bennett
authored
Reduce key duplication by enabling hardware RNG (#8803)
* Reduce key duplication by enabling hardware RNG * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Use micros() for worst case random seed for nrf52 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Minor cleanup, remove dead code and clarify comment * trunk * Add useRadioEntropy bool, default false. --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
1 parent 16dcafa commit 77f378d

File tree

8 files changed

+245
-16
lines changed

8 files changed

+245
-16
lines changed

src/mesh/CryptoEngine.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <memory>
55

66
#if !(MESHTASTIC_EXCLUDE_PKI)
7+
#include "HardwareRNG.h"
78
#include "NodeDB.h"
89
#include "aes-ccm.h"
910
#include "meshUtils.h"
@@ -26,6 +27,15 @@ void CryptoEngine::generateKeyPair(uint8_t *pubKey, uint8_t *privKey)
2627
{
2728
// Mix in any randomness we can, to make key generation stronger.
2829
CryptRNG.begin(optstr(APP_VERSION));
30+
31+
uint8_t hardwareEntropy[64] = {0};
32+
if (HardwareRNG::fill(hardwareEntropy, sizeof(hardwareEntropy), true)) {
33+
CryptRNG.stir(hardwareEntropy, sizeof(hardwareEntropy));
34+
} else {
35+
LOG_WARN("Hardware entropy unavailable, falling back to software RNG");
36+
}
37+
memset(hardwareEntropy, 0, sizeof(hardwareEntropy));
38+
2939
if (myNodeInfo.device_id.size == 16) {
3040
CryptRNG.stir(myNodeInfo.device_id.bytes, myNodeInfo.device_id.size);
3141
}

src/mesh/HardwareRNG.cpp

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#include "HardwareRNG.h"
2+
3+
#include <algorithm>
4+
#include <cstring>
5+
#include <random>
6+
7+
#include "configuration.h"
8+
9+
#if HAS_RADIO
10+
#include "RadioLibInterface.h"
11+
#endif
12+
13+
#if defined(ARCH_NRF52)
14+
#include <Adafruit_nRFCrypto.h>
15+
extern Adafruit_nRFCrypto nRFCrypto;
16+
#elif defined(ARCH_ESP32)
17+
#include <esp_system.h>
18+
#elif defined(ARCH_RP2040)
19+
#include <Arduino.h>
20+
#elif defined(ARCH_PORTDUINO)
21+
#include <random>
22+
#include <sys/random.h>
23+
#include <unistd.h>
24+
#endif
25+
26+
namespace HardwareRNG
27+
{
28+
29+
namespace
30+
{
31+
void fillWithRandomDevice(uint8_t *buffer, size_t length)
32+
{
33+
std::random_device rd;
34+
size_t offset = 0;
35+
while (offset < length) {
36+
uint32_t value = rd();
37+
size_t toCopy = std::min(length - offset, sizeof(value));
38+
memcpy(buffer + offset, &value, toCopy);
39+
offset += toCopy;
40+
}
41+
}
42+
43+
#if HAS_RADIO
44+
bool mixWithLoRaEntropy(uint8_t *buffer, size_t length)
45+
{
46+
// Only attempt to pull entropy from the modem if it is initialized and exposes the helper.
47+
// When the radio stack is disabled or has not yet been configured, we simply skip this step
48+
// and return false so callers know no extra mixing occurred.
49+
RadioLibInterface *radio = RadioLibInterface::instance;
50+
if (!radio) {
51+
LOG_ERROR("No radio instance available to provide entropy");
52+
return false;
53+
}
54+
55+
constexpr size_t chunkSize = 16;
56+
uint8_t scratch[chunkSize];
57+
size_t offset = 0;
58+
bool mixed = false;
59+
60+
while (offset < length) {
61+
size_t toCopy = std::min(length - offset, chunkSize);
62+
63+
// randomBytes() returns false if the modem does not support it or is not ready
64+
// (for instance, when the radio is powered down). We break immediately to avoid
65+
// blocking or returning partially-filled entropy and simply report failure.
66+
if (!radio->randomBytes(scratch, toCopy)) {
67+
break;
68+
}
69+
70+
for (size_t i = 0; i < toCopy; ++i) {
71+
buffer[offset + i] ^= scratch[i];
72+
}
73+
74+
mixed = true;
75+
offset += toCopy;
76+
}
77+
78+
// Avoid leaving the modem-sourced bytes sitting on the stack longer than needed.
79+
if (mixed) {
80+
memset(scratch, 0, sizeof(scratch));
81+
}
82+
83+
return mixed;
84+
}
85+
#endif
86+
} // namespace
87+
88+
bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
89+
{
90+
if (!buffer || length == 0) {
91+
return false;
92+
}
93+
94+
bool filled = false;
95+
96+
#if defined(ARCH_NRF52)
97+
// The Nordic SDK RNG provides cryptographic-quality randomness backed by hardware.
98+
nRFCrypto.begin();
99+
auto result = nRFCrypto.Random.generate(buffer, length);
100+
nRFCrypto.end();
101+
filled = result;
102+
#elif defined(ARCH_ESP32)
103+
// ESP32 exposes a true RNG via esp_fill_random().
104+
esp_fill_random(buffer, length);
105+
filled = true;
106+
#elif defined(ARCH_RP2040)
107+
// RP2040 has a hardware random number generator accessible through the Arduino core.
108+
size_t offset = 0;
109+
while (offset < length) {
110+
uint32_t value = rp2040.hwrand32();
111+
size_t toCopy = std::min(length - offset, sizeof(value));
112+
memcpy(buffer + offset, &value, toCopy);
113+
offset += toCopy;
114+
}
115+
filled = true;
116+
#elif defined(ARCH_PORTDUINO)
117+
// Prefer the host OS RNG first when running under Portduino.
118+
ssize_t generated = ::getrandom(buffer, length, 0);
119+
if (generated == static_cast<ssize_t>(length)) {
120+
filled = true;
121+
}
122+
123+
if (!filled) {
124+
fillWithRandomDevice(buffer, length);
125+
filled = true;
126+
}
127+
#endif
128+
129+
if (!filled) {
130+
// As a last resort, fall back to std::random_device. This should only be reached
131+
// if a platform-specific source was unavailable.
132+
fillWithRandomDevice(buffer, length);
133+
filled = true;
134+
}
135+
136+
#if HAS_RADIO
137+
if (useRadioEntropy) {
138+
// Best-effort: if the radio is active and can provide modem entropy, XOR it over the
139+
// buffer to improve overall quality. We consider the filling a success if either a
140+
// good platform RNG or the modem RNG provided data, so we return true as long as at
141+
// least one of those steps succeeded.
142+
filled = mixWithLoRaEntropy(buffer, length) || filled;
143+
}
144+
#endif
145+
146+
return filled;
147+
}
148+
149+
bool seed(uint32_t &seedOut)
150+
{
151+
uint32_t candidate = 0;
152+
if (!fill(reinterpret_cast<uint8_t *>(&candidate), sizeof(candidate), true)) {
153+
return false;
154+
}
155+
seedOut = candidate;
156+
return true;
157+
}
158+
159+
} // namespace HardwareRNG

src/mesh/HardwareRNG.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#pragma once
2+
3+
#include <cstddef>
4+
#include <cstdint>
5+
6+
namespace HardwareRNG
7+
{
8+
9+
/**
10+
* Fill the provided buffer with random bytes sourced from the most
11+
* appropriate hardware-backed RNG available on the current platform.
12+
*
13+
* @param buffer Destination buffer for random bytes
14+
* @param length Number of bytes to write
15+
* @param useRadioEntropy If true, attempt to mix radio entropy into the output as well.
16+
* @return true if the buffer was fully populated with entropy, false on failure
17+
*/
18+
bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy = false);
19+
20+
/**
21+
* Populate a 32-bit seed value with hardware-backed randomness where possible.
22+
*
23+
* @param seedOut Destination for the generated seed value
24+
* @return true if a seed was produced from a reliable entropy source
25+
*/
26+
bool seed(uint32_t &seedOut);
27+
28+
} // namespace HardwareRNG

src/mesh/RadioLibInterface.cpp

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,24 @@ bool RadioLibInterface::findInTxQueue(NodeNum from, PacketId id)
246246
return txQueue.find(from, id);
247247
}
248248

249+
bool RadioLibInterface::randomBytes(uint8_t *buffer, size_t length)
250+
{
251+
if (!buffer || length == 0 || !iface) {
252+
return false;
253+
}
254+
255+
// Older RadioLib versions only expose random(min, max), so fill the buffer byte-by-byte.
256+
for (size_t i = 0; i < length; ++i) {
257+
int32_t value = iface->random(0, 255);
258+
if (value < 0) {
259+
return false;
260+
}
261+
buffer[i] = static_cast<uint8_t>(value & 0xFF);
262+
}
263+
264+
return true;
265+
}
266+
249267
/** radio helper thread callback.
250268
We never immediately transmit after any operation (either Rx or Tx). Instead we should wait a random multiple of
251269
'slotTimes' (see definition in RadioInterface.h) taken from a contention window (CW) to lower the chance of collision.
@@ -587,4 +605,4 @@ bool RadioLibInterface::startSend(meshtastic_MeshPacket *txp)
587605

588606
return res == RADIOLIB_ERR_NONE;
589607
}
590-
}
608+
}

src/mesh/RadioLibInterface.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
172172
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
173173
virtual bool findInTxQueue(NodeNum from, PacketId id) override;
174174

175+
/**
176+
* Request randomness sourced from the LoRa modem, if supported by the active RadioLib interface.
177+
* @return true if len bytes were produced, false otherwise.
178+
*/
179+
bool randomBytes(uint8_t *buffer, size_t length);
180+
175181
private:
176182
/** if we have something waiting to send, start a short (random) timer so we can come check for collision before actually
177183
* doing the transmit */

src/platform/nrf52/main-nrf52.cpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <nrfx_wdt.h>
1818
#include <stdio.h>
1919
// #include <Adafruit_USBD_Device.h>
20+
#include "HardwareRNG.h"
2021
#include "NodeDB.h"
2122
#include "PowerMon.h"
2223
#include "error.h"
@@ -398,15 +399,14 @@ void nrf52Setup()
398399
#endif
399400

400401
// Init random seed
401-
union seedParts {
402-
uint32_t seed32;
403-
uint8_t seed8[4];
404-
} seed;
405-
nRFCrypto.begin();
406-
nRFCrypto.Random.generate(seed.seed8, sizeof(seed.seed8));
407-
LOG_DEBUG("Set random seed %u", seed.seed32);
408-
randomSeed(seed.seed32);
409-
nRFCrypto.end();
402+
uint32_t seed = 0;
403+
if (!HardwareRNG::seed(seed)) {
404+
LOG_WARN("Hardware RNG seed unavailable, using PRNG fallback");
405+
// Use a hardware timer value as a fallback seed for better entropy
406+
seed = micros();
407+
}
408+
LOG_DEBUG("Set random seed %u", seed);
409+
randomSeed(seed);
410410

411411
// Set up nrfx watchdog. Do not enable the watchdog yet (we do that
412412
// the first time through the main loop), so that other threads can

src/platform/portduino/PortduinoGlue.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "CryptoEngine.h"
2+
#include "HardwareRNG.h"
23
#include "PortduinoGPIO.h"
34
#include "SPIChip.h"
45
#include "mesh/RF95Interface.h"
@@ -233,7 +234,9 @@ void portduinoSetup()
233234
std::cout << "Running in simulated mode." << std::endl;
234235
portduino_config.MaxNodes = 200; // Default to 200 nodes
235236
// Set the random seed equal to TCPPort to have a different seed per instance
236-
randomSeed(TCPPort);
237+
uint32_t seed = TCPPort;
238+
HardwareRNG::seed(seed);
239+
randomSeed(seed);
237240
return;
238241
}
239242

@@ -512,7 +515,9 @@ void portduinoSetup()
512515
#endif
513516
printf("MAC ADDRESS: %02X:%02X:%02X:%02X:%02X:%02X\n", dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5]);
514517
// Rather important to set this, if not running simulated.
515-
randomSeed(time(NULL));
518+
uint32_t seed = static_cast<uint32_t>(time(NULL));
519+
HardwareRNG::seed(seed);
520+
randomSeed(seed);
516521

517522
std::string defaultGpioChipName = gpioChipName + std::to_string(portduino_config.lora_default_gpiochip);
518523

src/platform/rp2xx0/main-rp2xx0.cpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include "HardwareRNG.h"
12
#include "configuration.h"
23
#include "hardware/xosc.h"
34
#include <hardware/clocks.h>
@@ -98,10 +99,12 @@ void getMacAddr(uint8_t *dmac)
9899

99100
void rp2040Setup()
100101
{
101-
/* Sets a random seed to make sure we get different random numbers on each boot.
102-
Taken from CPU cycle counter and ROSC oscillator, so should be pretty random.
103-
*/
104-
randomSeed(rp2040.hwrand32());
102+
/* Sets a random seed to make sure we get different random numbers on each boot. */
103+
uint32_t seed = 0;
104+
if (!HardwareRNG::seed(seed)) {
105+
seed = rp2040.hwrand32();
106+
}
107+
randomSeed(seed);
105108

106109
#ifdef RP2040_SLOW_CLOCK
107110
uint f_pll_sys = frequency_count_khz(CLOCKS_FC0_SRC_VALUE_PLL_SYS_CLKSRC_PRIMARY);

0 commit comments

Comments
 (0)