diff --git a/src/configuration.h b/src/configuration.h
index 124c45029..148d1e95d 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -530,20 +530,67 @@ along with this program. If not, see .
// -----------------------------------------------------------------------------
// 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"
diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp
index b7f77fd07..d8f04a0c1 100644
--- a/src/mesh/PhoneAPI.cpp
+++ b/src/mesh/PhoneAPI.cpp
@@ -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 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
diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h
index bfc438cd8..695b41ae8 100644
--- a/src/mesh/PhoneAPI.h
+++ b/src/mesh/PhoneAPI.h
@@ -4,6 +4,7 @@
#include "concurrency/Lock.h"
#include "mesh-pb-constants.h"
#include "meshtastic/portnums.pb.h"
+#include
#include
#include
#include
@@ -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 isAdminAuthorized{false};
+ static std::atomic s_localAdminAuthorized;
#endif
private:
diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp
index 7aad3fbb5..cb623a208 100644
--- a/src/modules/AdminModule.cpp
+++ b/src/modules/AdminModule.cpp
@@ -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(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
}
}
diff --git a/src/security/EncryptedStorage.cpp b/src/security/EncryptedStorage.cpp
index 557df0e23..2133671ce 100644
--- a/src/security/EncryptedStorage.cpp
+++ b/src/security/EncryptedStorage.cpp
@@ -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
#include
-// 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
+// #include
+// #include // esp_fill_random()
+// #include // 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
diff --git a/src/security/EncryptedStorage.h b/src/security/EncryptedStorage.h
index 3d6ce4746..70f11eca0 100644
--- a/src/security/EncryptedStorage.h
+++ b/src/security/EncryptedStorage.h
@@ -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
diff --git a/variants/nrf52840/rak4631_lockdown/platformio.ini b/variants/nrf52840/rak4631_lockdown/platformio.ini
index e51dd234e..e545ca95b 100644
--- a/variants/nrf52840/rak4631_lockdown/platformio.ini
+++ b/variants/nrf52840/rak4631_lockdown/platformio.ini
@@ -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>