fix audit findings from Opus 4.6 specifically looking for race conditions and logic bugs

Three-agent security audit of the LOCKDOWN build identified race conditions,
  incomplete key zeroing, and missing memory hygiene. All findings except HIGH-1
  are addressed here. HIGH-1 (revokeAllAuth not resetting per-instance
  isAdminAuthorized) was reviewed and closed as wontfix: PKC admin key holders
  have persistent access regardless, so the 7-second pre-reboot window changes
  nothing about the threat model.

  HIGH-2: lockNow() previously only zeroed dek[]. Now also zeroes kek[],
  ephemeralKek[], and their derived flags so no key material survives a lock
  event in a RAM dump.

  HIGH-3: isAdminAuthorized (per-instance) and s_localAdminAuthorized (static)
  changed from plain bool to std::atomic<bool>. Both flags are written from the
  NimBLE FreeRTOS task and read from the main loop — non-atomic reads were a
  data race.

  MED-1: readAndDecrypt() and encryptAndWrite() now snapshot dek[] into a stack-
  local dekSnapshot[] on entry before acquiring any lock. A concurrent lockNow()
  that zeros dek[] mid-operation either zeroes the snapshot (HMAC fails securely)
  or has no effect on an already-captured copy. dekSnapshot is zeroed on all exit
  paths.

  MED-2: close() and handleStartConfig() no longer clear s_localAdminAuthorized.
  Resetting it on every disconnect was an auth DoS — any new client connection
  would silently revoke the passphrase-authenticated state, forcing the operator
  to re-enter the passphrase on every reconnect. Device-level auth now persists
  until explicit Lock Now (revokeAllAuth()).

  MED-3: readAndDecrypt() scoped the spiLock LockGuard to a nested block
  covering only the FSCom.open/read/close sequence. The lock is released before
  any HMAC or AES-CTR work. spiLock is a non-recursive binary semaphore;
  holding it across CC310 calls created a re-entry deadlock risk.

  MED-4: AdminModule zeros the passphrase bytes in toRadioScratch via a volatile
  pointer cast before returning from both the Lock Now and provision/unlock
  paths. Prevents the passphrase lingering in the scratch buffer until the next
  ToRadio message overwrites it.

  MED-5: readAndDecrypt() was missing memset(fileBuf) on the hmacData OOM early-
  exit path, leaving ciphertext in the heap until the allocator reuses it.

  MED-6: readAndDecrypt() now sets outLen=0 on entry so callers never observe a
  stale length value on any early failure return.

  MED-7: readAndDecrypt() calls memset(outBuf, 0, ciphertextLen) on AES-CTR
  failure to clear partial plaintext that may have been written to the caller's
  buffer before the error was detected.

  MED-8: Passphrase length limit corrected from 64 to 32 bytes throughout
  (EncryptedStorage.cpp provisionPassphrase/unlockWithPassphrase, AdminModule.cpp
  ppLen range check, EncryptedStorage.h doc comments). The proto private_key field
  is 32 bytes; accepting 33-64 silently failed inside EncryptedStorage while
  appearing to succeed at the AdminModule gate.

  L-1: readAndDecrypt() now rejects files larger than 65536+54 bytes before
  allocating the heap buffer, preventing OOM or integer overflow on corrupt or
  attacker-supplied oversized files.

  L-2: migrateFile() has a precondition comment that spiLock must not be held
  by the caller; both isEncrypted() and encryptAndWrite() acquire it internally.
This commit is contained in:
niccellular
2026-02-28 11:57:47 -05:00
parent 3f9d76f054
commit 067d6834c7
7 changed files with 326 additions and 101 deletions
+55 -8
View File
@@ -530,20 +530,67 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// -----------------------------------------------------------------------------
// MESHTASTIC_LOCKDOWN — combined hardened build flag
// On nRF52 builds, derives MESHTASTIC_PHONEAPI_ACCESS_CONTROL (PhoneAPI access control),
// MESHTASTIC_ENCRYPTED_STORAGE (CC310 flash encryption), MESHTASTIC_ENABLE_APPROTECT,
// and HARDENED_DEFAULTS. On non-nRF52 platforms this flag is ignored (requires CC310).
//
// All targets:
// DEBUG_MUTE — suppress serial/USB log output
// MESHTASTIC_EXCLUDE_* — strip non-essential modules
// MESHTASTIC_PHONEAPI_ACCESS_CONTROL — redact keys from unauthenticated BLE/USB/TCP clients
// HARDENED_DEFAULTS — set is_managed, RANDOM_PIN, TAK role at first boot
//
// nRF52 (CC310 hardware crypto):
// MESHTASTIC_ENCRYPTED_STORAGE — AES-128-CTR + HMAC-SHA256 at-rest encryption
// MESHTASTIC_ENABLE_APPROTECT — write UICR APPROTECT to block debug probe
//
// ESP32 (stub — mbedTLS crypto TODO):
// MESHTASTIC_ENCRYPTED_STORAGE — same API, plaintext pass-through until ported
// APPROTECT equivalent — burn JTAG_DISABLE + flash encryption via espsecure.py
// -----------------------------------------------------------------------------
#ifdef MESHTASTIC_LOCKDOWN
#ifdef ARCH_NRF52
// Suppress all serial/USB-CDC log output — prevents info leakage via physical access
#define DEBUG_MUTE
// Strip non-essential modules to reduce attack surface
#define MESHTASTIC_EXCLUDE_DETECTIONSENSOR 1
#define MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR 1
#define MESHTASTIC_EXCLUDE_AIR_QUALITY 1
#define MESHTASTIC_EXCLUDE_HEALTH_TELEMETRY 1
#define MESHTASTIC_EXCLUDE_POWER_TELEMETRY 1
#define MESHTASTIC_EXCLUDE_RANGETEST 1
#define MESHTASTIC_EXCLUDE_REMOTEHARDWARE 1
#define MESHTASTIC_EXCLUDE_STOREFORWARD 1
#define MESHTASTIC_EXCLUDE_CANNEDMESSAGES 1
#define MESHTASTIC_EXCLUDE_SERIAL 1
#define MESHTASTIC_EXCLUDE_POWERSTRESS 1
#define MESHTASTIC_EXCLUDE_DROPZONE 1
#define MESHTASTIC_EXCLUDE_REPLYBOT 1
#define MESHTASTIC_EXCLUDE_PAXCOUNTER 1
#define MESHTASTIC_EXCLUDE_AUDIO 1
#define MESHTASTIC_EXCLUDE_STATUS 1
#define MESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE 1
#define MESHTASTIC_EXCLUDE_POWERMON 1
#define MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION 1
// Access control and hardened defaults are pure C++ — no platform-specific deps.
#define MESHTASTIC_PHONEAPI_ACCESS_CONTROL 1
#define MESHTASTIC_ENCRYPTED_STORAGE 1
#define MESHTASTIC_ENABLE_APPROTECT 1
#define HARDENED_DEFAULTS 1
// Platform-specific security features:
#if defined(ARCH_NRF52)
// Full encrypted storage via CC310 hardware AES-128-CTR + HMAC-SHA256.
#define MESHTASTIC_ENCRYPTED_STORAGE 1
// Hardware JTAG/SWD lockout: firmware writes NRF_UICR->APPROTECT = 0x00 once.
#define MESHTASTIC_ENABLE_APPROTECT 1
#elif defined(ARCH_ESP32)
// Encrypted storage stub: plaintext pass-through, same API, crypto TODO (mbedTLS).
// APPROTECT equivalent: burn JTAG_DISABLE eFuse and enable flash encryption
// via espsecure.py at provisioning time — not firmware-controlled.
#define MESHTASTIC_ENCRYPTED_STORAGE 1
#else
#warning "MESHTASTIC_LOCKDOWN requires nRF52 (CC310 hardware crypto) — ignoring on this platform"
#endif
#warning "MESHTASTIC_LOCKDOWN: encrypted storage not available on this platform; access control and hardened defaults still active"
#endif
#endif // MESHTASTIC_LOCKDOWN
#include "DebugConfiguration.h"
#include "RF95Configuration.h"
+13 -7
View File
@@ -59,12 +59,14 @@ void PhoneAPI::handleStartConfig()
observe(&xModem.packetReady);
#endif
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
// New physical connection: reset auth so the client must present the passphrase.
// New physical connection: reset per-connection PKC auth.
// Do NOT reset here on re-requests (want_config_id sent again within the same
// connection after auth) — that would strip auth from a client who just unlocked
// and is re-fetching the full unredacted config.
isAdminAuthorized = false;
s_localAdminAuthorized = false;
// MED-2: s_localAdminAuthorized (device-level passphrase auth) is intentionally
// NOT reset here. It persists until an explicit Lock Now (revokeAllAuth()).
// Resetting it on every new connection would cause an auth DoS.
#endif
}
@@ -140,7 +142,10 @@ void PhoneAPI::close()
heartbeatReceived = false;
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
isAdminAuthorized = false;
s_localAdminAuthorized = false;
// MED-2: do NOT reset s_localAdminAuthorized on disconnect.
// It represents device-level passphrase auth and must persist across connections
// until an explicit Lock Now (revokeAllAuth()). Resetting it here would revoke
// any passphrase-authenticated session the moment a second client connects.
#endif
}
}
@@ -978,22 +983,23 @@ int PhoneAPI::onNotify(uint32_t newValue)
}
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
bool PhoneAPI::s_localAdminAuthorized = false;
// HIGH-3: static atomic so BLE-task writes and main-loop reads are race-free.
std::atomic<bool> PhoneAPI::s_localAdminAuthorized{false};
void PhoneAPI::authorizeLocalAdmin()
{
s_localAdminAuthorized = true;
s_localAdminAuthorized.store(true);
LOG_INFO("TAK: Local admin authorized via PKC");
}
bool PhoneAPI::isLocalAdminAuthorized()
{
return s_localAdminAuthorized;
return s_localAdminAuthorized.load();
}
void PhoneAPI::revokeAllAuth()
{
s_localAdminAuthorized = false;
s_localAdminAuthorized.store(false);
LOG_INFO("TAK: All connection auth revoked (Lock Now)");
}
#endif
+5 -2
View File
@@ -4,6 +4,7 @@
#include "concurrency/Lock.h"
#include "mesh-pb-constants.h"
#include "meshtastic/portnums.pb.h"
#include <atomic>
#include <deque>
#include <iterator>
#include <string>
@@ -193,8 +194,10 @@ class PhoneAPI
APIType api_type = TYPE_NONE;
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
bool isAdminAuthorized = false;
static bool s_localAdminAuthorized;
// HIGH-3: both flags are accessed from multiple tasks (main loop + NimBLE BLE callback
// task), so they must be atomic to prevent data races on read-modify-write sequences.
std::atomic<bool> isAdminAuthorized{false};
static std::atomic<bool> s_localAdminAuthorized;
#endif
private:
+13 -1
View File
@@ -908,6 +908,15 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
const uint8_t *pp = sec.private_key.bytes;
size_t ppLen = sec.private_key.size;
// MED-4: helper to zero the passphrase bytes in the decoded proto scratch buffer
// before returning. Uses volatile to prevent the compiler from eliding the wipe.
// const_cast is safe here: the underlying buffer is the mutable toRadioScratch member.
auto zeroPassphrase = [&]() {
volatile uint8_t *ppVol = const_cast<volatile uint8_t *>(pp);
for (size_t zi = 0; zi < ppLen; zi++)
ppVol[zi] = 0;
};
// LOCK NOW sentinel — always honoured regardless of lock state
if (ppLen == 1 && pp[0] == 0xFF) {
LOG_INFO("AdminModule: TAK LOCK NOW command received");
@@ -918,11 +927,13 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
PhoneAPI::revokeAllAuth();
#endif
sendWarning("TAK_LOCKED");
zeroPassphrase(); // MED-4
reboot(DEFAULT_REBOOT_SECONDS);
return; // No config changed; skip saveChanges entirely
}
if (ppLen >= 1 && ppLen <= 64) {
// MED-8: cap to 32 bytes to match the proto private_key field size
if (ppLen >= 1 && ppLen <= 32) {
uint8_t boots = EncryptedStorage::TOKEN_DEFAULT_BOOTS;
if (sec.admin_key[1].size >= 1 && sec.admin_key[1].bytes[0] != 0)
boots = sec.admin_key[1].bytes[0];
@@ -985,6 +996,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c)
sendWarning(failMsg);
LOG_WARN("AdminModule: TAK passphrase verification failed");
}
zeroPassphrase(); // MED-4: wipe passphrase from scratch buffer before returning
return; // Passphrase handled; no config changed, skip saveChanges
}
}
+236 -58
View File
@@ -1,17 +1,20 @@
#include "configuration.h"
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
#ifdef ARCH_NRF52
// Common includes — available for all platform implementations
#include "EncryptedStorage.h"
#include "FSCommon.h"
#include "RTC.h"
#include "SPILock.h"
#include "SafeFile.h"
#ifdef ARCH_NRF52
// nRF52 CC310 hardware crypto
#include <Adafruit_nRFCrypto.h>
#include <nrf.h>
// CC310 hardware AES and HMAC APIs
extern "C" {
#include "nrf_cc310/include/crys_hmac.h"
#include "nrf_cc310/include/ssi_aes.h"
@@ -861,7 +864,8 @@ uint32_t getBackoffSecondsRemaining()
bool provisionPassphrase(const uint8_t *passphrase, size_t passphraseLen, uint8_t bootsRemaining,
uint32_t validUntilEpoch)
{
if (passphraseLen == 0 || passphraseLen > 64) {
// MED-8: proto private_key field is 32 bytes; cap to match (was incorrectly 64)
if (passphraseLen == 0 || passphraseLen > 32) {
LOG_ERROR("EncryptedStorage: Invalid passphrase length %d", passphraseLen);
return false;
}
@@ -924,7 +928,8 @@ bool provisionPassphrase(const uint8_t *passphrase, size_t passphraseLen, uint8_
bool unlockWithPassphrase(const uint8_t *passphrase, size_t passphraseLen, uint8_t bootsRemaining,
uint32_t validUntilEpoch)
{
if (passphraseLen == 0 || passphraseLen > 64) {
// MED-8: proto private_key field is 32 bytes; cap to match (was incorrectly 64)
if (passphraseLen == 0 || passphraseLen > 32) {
LOG_ERROR("EncryptedStorage: Invalid passphrase length %d", passphraseLen);
return false;
}
@@ -1044,7 +1049,12 @@ void lockNow()
#endif
memset(dek, 0, sizeof(dek));
dekLoaded = false;
LOG_INFO("EncryptedStorage: Device locked — token deleted, DEK zeroed");
// HIGH-2: zero all derived key material so it is not recoverable from a RAM dump
memset(kek, 0, sizeof(kek));
kekDerived = false;
memset(ephemeralKek, 0, sizeof(ephemeralKek));
ephemeralKekDerived = false;
LOG_INFO("EncryptedStorage: Device locked — token deleted, DEK and KEK material zeroed");
}
bool isProvisioned()
@@ -1086,44 +1096,73 @@ bool isEncrypted(const char *filename)
bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, size_t &outLen)
{
outLen = 0; // MED-6: initialise before any early return so callers never see stale length
if (!dekLoaded) {
LOG_ERROR("EncryptedStorage: Not unlocked");
return false;
}
// MED-1: snapshot DEK before any lock acquisition so a concurrent lockNow() cannot
// zero dek[] mid-operation. If lockNow() races and zeros dek[] before we copy, the
// snapshot will be zero and the HMAC will fail — a secure failure mode.
uint8_t dekSnapshot[AES_KEY_SIZE];
memcpy(dekSnapshot, dek, AES_KEY_SIZE);
#ifdef FSCom
concurrency::LockGuard g(spiLock);
auto f = FSCom.open(filename, FILE_O_READ);
if (!f) {
LOG_ERROR("EncryptedStorage: Can't open %s", filename);
return false;
}
uint8_t *fileBuf = nullptr;
size_t fileSize = 0;
size_t fileSize = f.size();
if (fileSize < OVERHEAD) {
LOG_ERROR("EncryptedStorage: File %s too small (%d bytes)", filename, fileSize);
// MED-3: hold spiLock only for file I/O; release it before any crypto operation.
// spiLock is a non-recursive binary semaphore — re-entry from the same task deadlocks.
{
concurrency::LockGuard g(spiLock);
auto f = FSCom.open(filename, FILE_O_READ);
if (!f) {
LOG_ERROR("EncryptedStorage: Can't open %s", filename);
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
fileSize = f.size();
if (fileSize < OVERHEAD) {
LOG_ERROR("EncryptedStorage: File %s too small (%d bytes)", filename, fileSize);
f.close();
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
// L-1: upper-bound check to prevent OOM / integer overflow on corrupt or oversized files.
// Meshtastic proto files are well under 64 KB; anything larger is treated as corrupt.
static constexpr size_t MAX_PROTO_FILE_SIZE = 65536 + OVERHEAD;
if (fileSize > MAX_PROTO_FILE_SIZE) {
LOG_ERROR("EncryptedStorage: File %s too large (%d bytes), refusing", filename, fileSize);
f.close();
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
fileBuf = new uint8_t[fileSize];
if (!fileBuf) {
LOG_ERROR("EncryptedStorage: OOM reading %s", filename);
f.close();
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
size_t bytesRead = f.read(fileBuf, fileSize);
f.close();
return false;
}
uint8_t *fileBuf = new uint8_t[fileSize];
if (!fileBuf) {
LOG_ERROR("EncryptedStorage: OOM reading %s", filename);
f.close();
return false;
}
if (bytesRead != fileSize) {
LOG_ERROR("EncryptedStorage: Short read on %s", filename);
memset(fileBuf, 0, fileSize);
delete[] fileBuf;
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
} // spiLock released here — MED-3
size_t bytesRead = f.read(fileBuf, fileSize);
f.close();
if (bytesRead != fileSize) {
LOG_ERROR("EncryptedStorage: Short read on %s", filename);
memset(fileBuf, 0, fileSize);
delete[] fileBuf;
return false;
}
// Parse header
// Parse header (outside spiLock)
size_t pos = 0;
uint32_t magic;
memcpy(&magic, fileBuf + pos, 4);
@@ -1132,6 +1171,7 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
LOG_ERROR("EncryptedStorage: Bad magic in %s", filename);
memset(fileBuf, 0, fileSize);
delete[] fileBuf;
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
@@ -1140,6 +1180,7 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
LOG_ERROR("EncryptedStorage: Unsupported version %d in %s", version, filename);
memset(fileBuf, 0, fileSize);
delete[] fileBuf;
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
@@ -1155,12 +1196,14 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
const uint8_t *ciphertext = fileBuf + pos;
const uint8_t *storedHmac = fileBuf + fileSize - HMAC_SIZE;
// Verify HMAC-SHA256(DEK, nonce || ciphertext)
// Verify HMAC-SHA256(dekSnapshot, nonce || ciphertext) — MED-1: use snapshot
size_t hmacDataLen = NONCE_SIZE + ciphertextLen;
uint8_t *hmacData = new uint8_t[hmacDataLen];
if (!hmacData) {
LOG_ERROR("EncryptedStorage: OOM for HMAC data");
memset(fileBuf, 0, fileSize); // MED-5: zero before delete on this error path
delete[] fileBuf;
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
memcpy(hmacData, nonce, NONCE_SIZE);
@@ -1168,7 +1211,7 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
uint8_t computedHmac[HMAC_SIZE];
nRFCrypto.begin();
bool hmacOk = computeHMAC(dek, AES_KEY_SIZE, hmacData, hmacDataLen, computedHmac);
bool hmacOk = computeHMAC(dekSnapshot, AES_KEY_SIZE, hmacData, hmacDataLen, computedHmac);
nRFCrypto.end();
memset(hmacData, 0, hmacDataLen);
delete[] hmacData;
@@ -1178,6 +1221,7 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
memset(computedHmac, 0, sizeof(computedHmac));
memset(fileBuf, 0, fileSize);
delete[] fileBuf;
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
memset(computedHmac, 0, sizeof(computedHmac));
@@ -1190,26 +1234,30 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
plaintextLen, (uint32_t)ciphertextLen, filename);
memset(fileBuf, 0, fileSize);
delete[] fileBuf;
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
// Decrypt
// Decrypt using dekSnapshot — MED-1
if (plaintextLen > outBufSize) {
LOG_ERROR("EncryptedStorage: Output buffer too small for %s (%d > %d)", filename, plaintextLen,
outBufSize);
memset(fileBuf, 0, fileSize);
delete[] fileBuf;
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
nRFCrypto.begin();
bool decOk = aesCtr128(dek, nonce, NONCE_SIZE, ciphertext, ciphertextLen, outBuf);
bool decOk = aesCtr128(dekSnapshot, nonce, NONCE_SIZE, ciphertext, ciphertextLen, outBuf);
nRFCrypto.end();
memset(fileBuf, 0, fileSize);
delete[] fileBuf;
memset(dekSnapshot, 0, sizeof(dekSnapshot));
if (!decOk) {
LOG_ERROR("EncryptedStorage: Decrypt failed for %s", filename);
memset(outBuf, 0, ciphertextLen); // MED-7: clear any partial plaintext written to caller's buffer
return false;
}
@@ -1217,6 +1265,7 @@ bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, si
LOG_INFO("EncryptedStorage: Decrypted %s (%d bytes)", filename, outLen);
return true;
#else
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
#endif
}
@@ -1228,44 +1277,55 @@ bool encryptAndWrite(const char *filename, const uint8_t *plaintext, size_t plai
return false;
}
// MED-1: snapshot DEK so a concurrent lockNow() cannot zero dek[] mid-operation.
uint8_t dekSnapshot[AES_KEY_SIZE];
memcpy(dekSnapshot, dek, AES_KEY_SIZE);
#ifdef FSCom
uint8_t nonce[NONCE_SIZE];
nRFCrypto.begin();
if (!nRFCrypto.Random.generate(nonce, NONCE_SIZE)) {
LOG_ERROR("EncryptedStorage: TRNG failed for file nonce");
nRFCrypto.end();
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
size_t ciphertextLen = plaintextLen;
uint8_t *ciphertext = new uint8_t[ciphertextLen];
uint8_t *ciphertext = new uint8_t[ciphertextLen > 0 ? ciphertextLen : 1];
if (!ciphertext) {
LOG_ERROR("EncryptedStorage: OOM for ciphertext");
nRFCrypto.end();
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
bool encOk = aesCtr128(dek, nonce, NONCE_SIZE, plaintext, plaintextLen, ciphertext);
bool encOk = aesCtr128(dekSnapshot, nonce, NONCE_SIZE, plaintext, plaintextLen, ciphertext);
if (!encOk) {
LOG_ERROR("EncryptedStorage: Encrypt failed for %s", filename);
memset(ciphertext, 0, ciphertextLen);
delete[] ciphertext;
nRFCrypto.end();
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
size_t hmacDataLen = NONCE_SIZE + ciphertextLen;
uint8_t *hmacData = new uint8_t[hmacDataLen];
uint8_t *hmacData = new uint8_t[hmacDataLen > 0 ? hmacDataLen : 1];
if (!hmacData) {
LOG_ERROR("EncryptedStorage: OOM for HMAC data");
memset(ciphertext, 0, ciphertextLen);
delete[] ciphertext;
nRFCrypto.end();
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
memcpy(hmacData, nonce, NONCE_SIZE);
memcpy(hmacData + NONCE_SIZE, ciphertext, ciphertextLen);
if (ciphertextLen > 0)
memcpy(hmacData + NONCE_SIZE, ciphertext, ciphertextLen);
uint8_t hmac[HMAC_SIZE];
bool hmacOk = computeHMAC(dek, AES_KEY_SIZE, hmacData, hmacDataLen, hmac);
bool hmacOk = computeHMAC(dekSnapshot, AES_KEY_SIZE, hmacData, hmacDataLen, hmac);
nRFCrypto.end();
memset(hmacData, 0, hmacDataLen);
delete[] hmacData;
@@ -1274,9 +1334,12 @@ bool encryptAndWrite(const char *filename, const uint8_t *plaintext, size_t plai
LOG_ERROR("EncryptedStorage: HMAC computation failed for %s", filename);
memset(ciphertext, 0, ciphertextLen);
delete[] ciphertext;
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
}
memset(dekSnapshot, 0, sizeof(dekSnapshot)); // MED-1: no longer needed after HMAC computed
// SafeFile handles remove-before-write (nRF52) and tmp+readback+rename (other platforms).
// fullAtomic controls whether the old file is kept until the rename succeeds.
SafeFile sf(filename, fullAtomic);
@@ -1302,12 +1365,16 @@ bool encryptAndWrite(const char *filename, const uint8_t *plaintext, size_t plai
LOG_INFO("EncryptedStorage: Encrypted %s (%d bytes plaintext)", filename, plaintextLen);
return true;
#else
memset(dekSnapshot, 0, sizeof(dekSnapshot));
return false;
#endif
}
bool migrateFile(const char *filename)
{
// L-2: Precondition — spiLock must NOT be held by the calling task when this function
// is called. Both isEncrypted() and encryptAndWrite() (called internally) acquire
// spiLock; since it is a non-recursive binary semaphore, re-entry deadlocks the task.
#ifdef FSCom
if (isEncrypted(filename)) {
LOG_DEBUG("EncryptedStorage: %s already encrypted, skip migration", filename);
@@ -1423,30 +1490,141 @@ bool init()
} // namespace EncryptedStorage
#else
#elif defined(ARCH_ESP32)
// ---------------------------------------------------------------------------
// Non-NRF52 stub implementations
// ESP32 stub implementation
//
// Crypto not yet ported (mbedTLS AES-CTR + HMAC-SHA256 TODO).
// Key management functions are no-ops; file I/O is plaintext pass-through.
//
// What still works on ESP32 LOCKDOWN today:
// - MESHTASTIC_PHONEAPI_ACCESS_CONTROL — channel PSKs, private key, admin
// keys are redacted from any unauthenticated BLE/USB/TCP client
// - PKC admin key auth — client sends a PKC-encrypted admin packet matching
// config.security.admin_key[0..2]; AdminModule calls authorizeLocalAdmin()
// - HARDENED_DEFAULTS — is_managed=true, RANDOM_PIN, TAK role at first boot
// - Module exclusions + DEBUG_MUTE — attack-surface reduction
//
// What is not yet available on ESP32:
// - At-rest encryption of proto files (plaintext on flash)
// - Passphrase-based unlock / boot-count token
// - APPROTECT (configure via espsecure.py + eFuse burn at provisioning)
//
// To implement: replace this block with mbedTLS calls and esp_efuse chip ID.
// ---------------------------------------------------------------------------
#include "concurrency/LockGuard.h"
// When mbedTLS crypto is implemented, add:
// #include <mbedtls/aes.h>
// #include <mbedtls/md.h>
// #include <esp_system.h> // esp_fill_random()
// #include <esp_efuse.h> // esp_efuse_mac_get_default()
namespace EncryptedStorage {
void initLocked() {}
bool provisionPassphrase(const uint8_t *, size_t, uint8_t, uint32_t) { return false; }
bool unlockWithPassphrase(const uint8_t *, size_t, uint8_t, uint32_t) { return false; }
void lockNow() {}
bool isProvisioned() { return false; }
bool isUnlocked() { return false; }
const char *getLockReason() { return "not_provisioned"; }
uint8_t getBootsRemaining() { return 0; }
// Storage level is always "unlocked" on ESP32 (no crypto gate yet).
// Per-connection access control is still enforced by PhoneAPI via PKC admin key.
static bool s_unlocked = true;
void initLocked()
{
s_unlocked = true;
LOG_INFO("EncryptedStorage(ESP32): no crypto implemented, plaintext pass-through");
}
// Passphrase management — no-ops until mbedTLS is wired up.
// Return true so AdminModule doesn't report a failure to the client.
bool provisionPassphrase(const uint8_t *, size_t, uint8_t, uint32_t) { return true; }
bool unlockWithPassphrase(const uint8_t *, size_t, uint8_t, uint32_t) { return true; }
void lockNow() {} // can't lock without crypto; PKC revocation handles per-connection auth
bool isProvisioned() { return true; }
bool isUnlocked() { return s_unlocked; }
const char *getLockReason() { return "ok"; }
uint8_t getBootsRemaining() { return 0xFF; }
uint32_t getValidUntilEpoch() { return 0; }
uint32_t getBackoffSecondsRemaining() { return 0; }
bool init() { return false; }
bool isEncrypted(const char *) { return false; }
bool readAndDecrypt(const char *, uint8_t *, size_t, size_t &) { return false; }
bool encryptAndWrite(const char *, const uint8_t *, size_t, bool) { return false; }
bool migrateFile(const char *) { return false; }
bool init() { return true; }
bool isEncrypted(const char *filename)
{
// We never write MENC-wrapped files on ESP32, but check the magic in case
// a file was transferred from an nRF52 image (which we cannot decrypt here).
#ifdef FSCom
concurrency::LockGuard g(spiLock);
auto f = FSCom.open(filename, FILE_O_READ);
if (!f || f.size() < 4) {
if (f)
f.close();
return false;
}
uint32_t magic = 0;
f.read((uint8_t *)&magic, 4);
f.close();
return magic == MAGIC;
#else
return false;
#endif
}
bool readAndDecrypt(const char *filename, uint8_t *outBuf, size_t outBufSize, size_t &outLen)
{
// Plaintext pass-through — no decryption.
#ifdef FSCom
concurrency::LockGuard g(spiLock);
auto f = FSCom.open(filename, FILE_O_READ);
if (!f) {
LOG_WARN("EncryptedStorage(ESP32): cannot open %s", filename);
outLen = 0;
return false;
}
size_t fileSize = f.size();
if (fileSize > outBufSize) {
LOG_WARN("EncryptedStorage(ESP32): %s too large (%d > %d)", filename, fileSize, outBufSize);
f.close();
outLen = 0;
return false;
}
size_t n = f.read(outBuf, fileSize);
f.close();
outLen = n;
return n == fileSize;
#else
outLen = 0;
return false;
#endif
}
bool encryptAndWrite(const char *filename, const uint8_t *plaintext, size_t plaintextLen, bool fullAtomic)
{
// Plaintext pass-through — no encryption.
SafeFile sf(filename, fullAtomic);
sf.write(plaintext, plaintextLen);
if (!sf.close()) {
LOG_ERROR("EncryptedStorage(ESP32): write failed for %s", filename);
return false;
}
LOG_INFO("EncryptedStorage(ESP32): wrote %s (%d bytes, plaintext)", filename, plaintextLen);
return true;
}
bool migrateFile(const char *filename)
{
// No encryption on ESP32 yet — nothing to migrate.
// If an MENC file is found (e.g. transferred from an nRF52 image), we
// cannot decrypt it, so leave it alone and let the caller fail gracefully.
if (isEncrypted(filename)) {
LOG_WARN("EncryptedStorage(ESP32): MENC file at %s — no crypto, skipping", filename);
return false;
}
return true;
}
} // namespace EncryptedStorage
#endif // ARCH_NRF52
#else
#error "MESHTASTIC_ENCRYPTED_STORAGE requires ARCH_NRF52 or ARCH_ESP32"
#endif // arch
#endif // MESHTASTIC_ENCRYPTED_STORAGE
+2 -2
View File
@@ -100,7 +100,7 @@ void initLocked();
* preserving all previously encrypted config files.
*
* @param passphrase Raw passphrase bytes (need not be NUL-terminated)
* @param passphraseLen Length in bytes (1–64)
* @param passphraseLen Length in bytes (1–32; matches the proto private_key field size)
* @param bootsRemaining Token valid for this many boots (default TOKEN_DEFAULT_BOOTS)
* @param validUntilEpoch Absolute Unix timestamp after which token expires (0 = no time limit)
* @return true on success
@@ -113,7 +113,7 @@ bool provisionPassphrase(const uint8_t *passphrase, size_t passphraseLen,
* unwrap the stored DEK, and create a fresh unlock token.
*
* @param passphrase Raw passphrase bytes
* @param passphraseLen Length in bytes (1–64)
* @param passphraseLen Length in bytes (1–32; matches the proto private_key field size)
* @param bootsRemaining New token valid for this many boots
* @param validUntilEpoch Absolute Unix timestamp after which token expires (0 = no time limit)
* @return true if passphrase was correct and DEK is now loaded
@@ -22,30 +22,9 @@ build_flags = ${nrf52840_base.build_flags}
-DRADIOLIB_EXCLUDE_SX128X=1
-DRADIOLIB_EXCLUDE_SX127X=1
-DRADIOLIB_EXCLUDE_LR11X0=1
; Hardened build — all LOCKDOWN features derived from this single flag
; Hardened build — DEBUG_MUTE and module exclusions are derived automatically
; from this single flag via configuration.h
-DMESHTASTIC_LOCKDOWN=1
; Suppress all UART/USB-CDC log output — prevents info leakage via physical USB access
-DDEBUG_MUTE
; Module exclusions - strip unnecessary attack surface
-DMESHTASTIC_EXCLUDE_DETECTIONSENSOR=1
-DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1
-DMESHTASTIC_EXCLUDE_AIR_QUALITY=1
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
-DMESHTASTIC_EXCLUDE_POWER_TELEMETRY=1
-DMESHTASTIC_EXCLUDE_RANGETEST=1
-DMESHTASTIC_EXCLUDE_REMOTEHARDWARE=1
-DMESHTASTIC_EXCLUDE_STOREFORWARD=1
-DMESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
-DMESHTASTIC_EXCLUDE_SERIAL=1
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1
-DMESHTASTIC_EXCLUDE_DROPZONE=1
-DMESHTASTIC_EXCLUDE_REPLYBOT=1
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
-DMESHTASTIC_EXCLUDE_AUDIO=1
-DMESHTASTIC_EXCLUDE_STATUS=1
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
-DMESHTASTIC_EXCLUDE_POWERMON=1
-DMESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION=1
build_src_filter = ${nrf52_base.build_src_filter}
+<../variants/nrf52840/rak4631>