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>