mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-17 17:11:30 +10:00
Add rak4631_lockdown hardened build variant
Introduces a new RAK4631 build variant (-DMESHTASTIC_LOCKDOWN=1) with encrypted flash storage, BLE/USB config access control, and hardened defaults. New files: - variants/nrf52840/rak4631_lockdown/ — PlatformIO env, variant header - src/security/EncryptedStorage.h/.cpp — AES-128-CTR + HMAC-SHA256 encrypted proto storage using CC310 hardware, passphrase-gated DEK, TTL/boot-count unlock token, backoff on failed attempts - src/security/APProtect.h/.cpp — UICR APPROTECT (SWD lockout) Modified files: - configuration.h — MESHTASTIC_LOCKDOWN flag derives PHONEAPI_ACCESS_CONTROL, ENCRYPTED_STORAGE, ENABLE_APPROTECT, HARDENED_DEFAULTS (nRF52 only) - NodeDB.cpp/.h — encrypted loadProto/saveProto, locked-boot early return, migration of plaintext files to MENC, hardened defaults, saveToDiskNoRetry guard prevents FSCom.format() when storage locked - PhoneAPI.cpp/.h — per-connection auth reset, redacts channel PSKs and security keys for unauthorized clients, revokeAllAuth() for Lock Now, post-config TAK_LOCKED/TAK_NEEDS_PROVISION notification - AdminModule.cpp — passphrase delivery via set_config(security), provision/unlock/re-verify flow, Lock Now sentinel (0xFF), PKC admin auth callback to PhoneAPI - PowerFSM.cpp — guard serialEnter() BLE disable on nRF52
This commit is contained in:
+169
-14
@@ -3,6 +3,9 @@
|
||||
#include "GPS.h"
|
||||
#endif
|
||||
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
#include "security/EncryptedStorage.h"
|
||||
#endif
|
||||
#include "Channels.h"
|
||||
#include "Default.h"
|
||||
#include "FSCommon.h"
|
||||
@@ -54,6 +57,14 @@ void PhoneAPI::handleStartConfig()
|
||||
observe(&service->fromNumChanged);
|
||||
#ifdef FSCom
|
||||
observe(&xModem.packetReady);
|
||||
#endif
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
// New physical connection: reset auth so the client must present the passphrase.
|
||||
// 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;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -127,6 +138,10 @@ void PhoneAPI::close()
|
||||
config_state = 0;
|
||||
pauseBluetoothLogging = false;
|
||||
heartbeatReceived = false;
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
isAdminAuthorized = false;
|
||||
s_localAdminAuthorized = false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +170,21 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
if (pb_decode_from_bytes(buf, bufLength, &meshtastic_ToRadio_msg, &toRadioScratch)) {
|
||||
switch (toRadioScratch.which_payload_variant) {
|
||||
case meshtastic_ToRadio_packet_tag:
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
// Allow admin messages addressed to this device — passphrase delivery must get through.
|
||||
// AdminModule handles its own is_managed gate for those.
|
||||
// Block everything else — unauthorized clients cannot inject mesh traffic.
|
||||
// Require the packet to carry a decoded (not encrypted) payload so portnum is valid.
|
||||
bool isLocalAdmin = toRadioScratch.packet.which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||
toRadioScratch.packet.decoded.portnum == meshtastic_PortNum_ADMIN_APP &&
|
||||
toRadioScratch.packet.to == nodeDB->getNodeNum();
|
||||
if (!isLocalAdmin) {
|
||||
LOG_INFO("TAK: Dropping non-admin ToRadio packet from unauthorized client");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return handleToRadioPacket(toRadioScratch.packet);
|
||||
case meshtastic_ToRadio_want_config_id_tag:
|
||||
config_nonce = toRadioScratch.want_config_id;
|
||||
@@ -166,6 +196,12 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
close();
|
||||
break;
|
||||
case meshtastic_ToRadio_xmodemPacket_tag:
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
LOG_INFO("TAK: Dropping xmodem packet from unauthorized client");
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
LOG_INFO("Got xmodem packet");
|
||||
#ifdef FSCom
|
||||
xModem.handlePacket(toRadioScratch.xmodemPacket);
|
||||
@@ -286,8 +322,15 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
}
|
||||
if (config_nonce == SPECIAL_NONCE_ONLY_NODES) {
|
||||
// If client only wants node info, jump directly to sending nodes
|
||||
state = STATE_SEND_OTHER_NODEINFOS;
|
||||
onNowHasData(0);
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
state = STATE_SEND_COMPLETE_ID; // Unauthorized: skip node DB
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
state = STATE_SEND_OTHER_NODEINFOS;
|
||||
onNowHasData(0);
|
||||
}
|
||||
} else {
|
||||
state = STATE_SEND_METADATA;
|
||||
}
|
||||
@@ -303,7 +346,17 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
|
||||
case STATE_SEND_CHANNELS:
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_channel_tag;
|
||||
fromRadioScratch.channel = channels.getByIndex(config_state);
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
// Send channel structure but zero out PSK to prevent key extraction
|
||||
fromRadioScratch.channel = channels.getByIndex(config_state);
|
||||
memset(fromRadioScratch.channel.settings.psk.bytes, 0, sizeof(fromRadioScratch.channel.settings.psk.bytes));
|
||||
fromRadioScratch.channel.settings.psk.size = 0;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
fromRadioScratch.channel = channels.getByIndex(config_state);
|
||||
}
|
||||
config_state++;
|
||||
// Advance when we have sent all of our Channels
|
||||
if (config_state >= MAX_NUM_CHANNELS) {
|
||||
@@ -336,6 +389,13 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
LOG_DEBUG("Send config: network");
|
||||
fromRadioScratch.config.which_payload_variant = meshtastic_Config_network_tag;
|
||||
fromRadioScratch.config.payload_variant.network = config.network;
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
// Redact wifi PSK
|
||||
memset(fromRadioScratch.config.payload_variant.network.wifi_psk, 0,
|
||||
sizeof(fromRadioScratch.config.payload_variant.network.wifi_psk));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case meshtastic_Config_display_tag:
|
||||
LOG_DEBUG("Send config: display");
|
||||
@@ -356,6 +416,30 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
LOG_DEBUG("Send config: security");
|
||||
fromRadioScratch.config.which_payload_variant = meshtastic_Config_security_tag;
|
||||
fromRadioScratch.config.payload_variant.security = config.security;
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
// Redact private key
|
||||
memset(fromRadioScratch.config.payload_variant.security.private_key.bytes, 0,
|
||||
sizeof(fromRadioScratch.config.payload_variant.security.private_key.bytes));
|
||||
fromRadioScratch.config.payload_variant.security.private_key.size = 0;
|
||||
// Redact admin keys
|
||||
for (int i = 0; i < 3; i++) {
|
||||
memset(fromRadioScratch.config.payload_variant.security.admin_key[i].bytes, 0,
|
||||
sizeof(fromRadioScratch.config.payload_variant.security.admin_key[i].bytes));
|
||||
fromRadioScratch.config.payload_variant.security.admin_key[i].size = 0;
|
||||
}
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
// Signal provisioning state via admin_key[0] so the client knows
|
||||
// whether to show "Set passphrase" vs "Enter passphrase":
|
||||
// admin_key[0].size == 0 → not provisioned (first time setup)
|
||||
// admin_key[0].size == 1, bytes[0] == 0x01 → provisioned, locked
|
||||
if (EncryptedStorage::isProvisioned()) {
|
||||
fromRadioScratch.config.payload_variant.security.admin_key[0].bytes[0] = 0x01;
|
||||
fromRadioScratch.config.payload_variant.security.admin_key[0].size = 1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
case meshtastic_Config_sessionkey_tag:
|
||||
LOG_DEBUG("Send config: sessionkey");
|
||||
@@ -454,6 +538,12 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
config_state++;
|
||||
// Advance when we have sent all of our ModuleConfig objects
|
||||
if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) {
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
// Unauthorized client: skip node DB and file manifest — only send config complete
|
||||
state = STATE_SEND_COMPLETE_ID;
|
||||
} else
|
||||
#endif
|
||||
// Handle special nonce behaviors:
|
||||
// - SPECIAL_NONCE_ONLY_CONFIG: Skip node info, go directly to file manifest
|
||||
// - SPECIAL_NONCE_ONLY_NODES: After sending nodes, skip to complete
|
||||
@@ -544,24 +634,46 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.queueStatus = *queueStatusPacketForPhone;
|
||||
releaseQueueStatusPhonePacket();
|
||||
} else if (mqttClientProxyMessageForPhone) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag;
|
||||
fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone;
|
||||
releaseMqttClientProxyPhonePacket();
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
releaseMqttClientProxyPhonePacket(); // Discard — unauthorized client
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag;
|
||||
fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone;
|
||||
releaseMqttClientProxyPhonePacket();
|
||||
}
|
||||
} else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) {
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag;
|
||||
fromRadioScratch.xmodemPacket = xmodemPacketForPhone;
|
||||
xmodemPacketForPhone = meshtastic_XModem_init_zero;
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
xmodemPacketForPhone = meshtastic_XModem_init_zero; // Discard — unauthorized client
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag;
|
||||
fromRadioScratch.xmodemPacket = xmodemPacketForPhone;
|
||||
xmodemPacketForPhone = meshtastic_XModem_init_zero;
|
||||
}
|
||||
} else if (clientNotification) {
|
||||
// Always deliver clientNotification — required for TAK_UNLOCKED/TAK_LOCKED to reach client
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_clientNotification_tag;
|
||||
fromRadioScratch.clientNotification = *clientNotification;
|
||||
releaseClientNotification();
|
||||
} else if (packetForPhone) {
|
||||
printPacket("phone downloaded packet", packetForPhone);
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
releasePhonePacket(); // Discard mesh traffic — unauthorized client
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
printPacket("phone downloaded packet", packetForPhone);
|
||||
|
||||
// Encapsulate as a FromRadio packet
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = *packetForPhone;
|
||||
releasePhonePacket();
|
||||
// Encapsulate as a FromRadio packet
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_packet_tag;
|
||||
fromRadioScratch.packet = *packetForPhone;
|
||||
releasePhonePacket();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -604,6 +716,28 @@ void PhoneAPI::sendConfigComplete()
|
||||
service->api_state = service->STATE_ETH;
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
// Immediately after config_complete_id, notify the client if it hasn't authenticated.
|
||||
// Gate on client auth state (not storage lock state) — storage may be auto-unlocked via
|
||||
// boot token, but each new BLE/USB connection must still present the passphrase.
|
||||
if (!isAdminAuthorized && !s_localAdminAuthorized) {
|
||||
char msg[48];
|
||||
if (!EncryptedStorage::isProvisioned()) {
|
||||
strncpy(msg, "TAK_NEEDS_PROVISION", sizeof(msg));
|
||||
} else {
|
||||
// Provisioned: client must authenticate. Use lock reason if storage is hard-locked,
|
||||
// otherwise signal that passphrase auth is required for this connection.
|
||||
if (!EncryptedStorage::isUnlocked()) {
|
||||
snprintf(msg, sizeof(msg), "TAK_LOCKED:%s", EncryptedStorage::getLockReason());
|
||||
} else {
|
||||
strncpy(msg, "TAK_LOCKED:needs_auth", sizeof(msg));
|
||||
}
|
||||
}
|
||||
sendNotification(meshtastic_LogRecord_Level_WARNING, 0, msg);
|
||||
LOG_INFO("PhoneAPI: sent %s to client", msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Allow subclasses to know we've entered steady-state so they can lower power consumption
|
||||
onConfigComplete();
|
||||
|
||||
@@ -842,3 +976,24 @@ int PhoneAPI::onNotify(uint32_t newValue)
|
||||
|
||||
return timeout ? -1 : 0; // If we timed out, MeshService should stop iterating through observers as we just removed one
|
||||
}
|
||||
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
bool PhoneAPI::s_localAdminAuthorized = false;
|
||||
|
||||
void PhoneAPI::authorizeLocalAdmin()
|
||||
{
|
||||
s_localAdminAuthorized = true;
|
||||
LOG_INFO("TAK: Local admin authorized via PKC");
|
||||
}
|
||||
|
||||
bool PhoneAPI::isLocalAdminAuthorized()
|
||||
{
|
||||
return s_localAdminAuthorized;
|
||||
}
|
||||
|
||||
void PhoneAPI::revokeAllAuth()
|
||||
{
|
||||
s_localAdminAuthorized = false;
|
||||
LOG_INFO("TAK: All connection auth revoked (Lock Now)");
|
||||
}
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user