emdashes begone (#10847)

This commit is contained in:
Tom
2026-07-01 19:01:27 -05:00
committed by GitHub
co-authored by GitHub
parent dee94e0758
commit 3becaf2d95
276 changed files with 1795 additions and 1793 deletions
+52 -52
View File
@@ -64,12 +64,12 @@ static constexpr size_t MAX_AUTH_SLOTS = 6;
// status produced for connection A (e.g. UNLOCKED with the active TTL,
// or UNLOCK_FAILED with a backoff) cannot be drained by connection B,
// which would otherwise learn that A just authenticated or just failed
// a real information leak across local clients.
// - a real information leak across local clients.
//
// File-scope rather than a per-PhoneAPI member because adding any
// non-trivial state directly to PhoneAPI broke USB-CDC enumeration on
// the current nRF52 framework; the auth-slot table next door uses the
// same workaround. Lifecycle is tied to the auth slot table both are
// same workaround. Lifecycle is tied to the auth slot table - both are
// keyed by PhoneAPI*, both are cleared together in clearAuthSlot_LH,
// and both share g_authSlotsMutex.
struct PendingStatusSlot {
@@ -78,7 +78,7 @@ struct PendingStatusSlot {
bool hasPending = false;
// True between a successful passphrase verify and the main-loop
// reloadFromDisk that follows. While set, the connection is NOT
// yet authorized and no UNLOCKED status has been emitted the
// yet authorized and no UNLOCKED status has been emitted - the
// client still sees LOCKED, and any admin op it tries is dropped
// by the existing unauth gates. Cleared either way by
// completePendingUnlocks once reload finishes.
@@ -107,7 +107,7 @@ static PendingStatusSlot *findOrAllocStatusSlot_LH(PhoneAPI *p)
// Mirror the auth-slot eviction policy: stale slots can be reused.
// A connection that lost its auth slot has nothing meaningful to be
// told via a pending status anyway. Never evict a slot mid-unlock
// (pendingUnlockAfterReload set) completing that flow on the
// (pendingUnlockAfterReload set) - completing that flow on the
// wrong PhoneAPI would authorize the wrong connection.
for (auto &s : g_statusSlots) {
if (!s.hasPending && !s.pendingUnlockAfterReload) {
@@ -143,7 +143,7 @@ static void buildStatus_LH(meshtastic_LockdownStatus &out, meshtastic_LockdownSt
memset(&out, 0, sizeof(out));
out.state = state;
// Collapse the specific token_* reasons to a generic "locked" over
// the wire full detail still goes to local logs. An unauth client
// the wire - full detail still goes to local logs. An unauth client
// does not need to know whether HMAC failed vs the boot count
// hit zero vs the file was the wrong size; all of those mean the
// same thing to the client ("locked, ask for passphrase") but
@@ -171,7 +171,7 @@ struct PhoneAuthSlot {
static PhoneAuthSlot g_authSlots[MAX_AUTH_SLOTS];
// Global auth epoch. Lock Now bumps it; per-slot `epoch` compared against
// this. Wraps at 2^32 revocations practically unreachable; on wrap the
// this. Wraps at 2^32 revocations - practically unreachable; on wrap the
// only behavioral effect is that any slot whose epoch happens to match the
// new low value would be treated as authorized again, which requires a
// pre-existing authorized slot to survive 2^32 lockNow events on the same
@@ -179,7 +179,7 @@ static PhoneAuthSlot g_authSlots[MAX_AUTH_SLOTS];
static uint32_t g_authEpoch = 1;
// Single mutex guarding g_authSlots and g_authEpoch. All readers and
// writers including const getters like getAdminAuthorized must take
// writers - including const getters like getAdminAuthorized - must take
// it. Granularity is fine because the critical sections are short (a
// fixed-size linear scan over 6 entries) and contention is dominated by
// getFromRadio's per-call redaction checks, which tolerate brief
@@ -191,7 +191,7 @@ static concurrency::Lock g_authSlotsMutex;
// evicts the first unauthorized slot found. Refuses to evict an authorized
// slot (those represent a live operator session and must outlive the table
// pressure of reconnect churn). Returns nullptr only if every slot is
// occupied by a different live, authorized PhoneAPI practically only
// occupied by a different live, authorized PhoneAPI - practically only
// reachable as a DoS via 7+ simultaneous authed connections, in which
// case fail-closed and log.
static PhoneAuthSlot *findOrAllocSlot_LH(PhoneAPI *p)
@@ -211,7 +211,7 @@ static PhoneAuthSlot *findOrAllocSlot_LH(PhoneAPI *p)
}
}
// Second pass: evict an unauthorized stale slot. Don't touch authorized
// ones those still represent an operator-authenticated session.
// ones - those still represent an operator-authenticated session.
for (auto &s : g_authSlots) {
if (!s.authorized) {
s.who = p;
@@ -312,7 +312,7 @@ void PhoneAPI::handleStartConfig()
#if defined(MESHTASTIC_EXCLUDE_FILES_MANIFEST)
// Skip the recursive FS walk. Used by platforms whose Zephyr LittleFS
// backend can't safely traverse a deep tree (e.g. nRF54L15) and platforms
// that don't support OTA browsing the manifest is only consumed by
// that don't support OTA browsing - the manifest is only consumed by
// companion apps for those flows.
releaseFilesManifest(filesManifest);
#else
@@ -444,12 +444,12 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
case meshtastic_ToRadio_packet_tag:
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!getAdminAuthorized()) {
// Allow admin messages addressed to this device passphrase delivery must get through.
// 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.
// Block everything else - unauthorized clients cannot inject mesh traffic.
// Require the packet to carry a decoded (not encrypted) payload so portnum is valid.
// Refuse to match when our own node number is still 0 (NodeDB
// not yet loaded happens during the locked-default boot path
// not yet loaded - happens during the locked-default boot path
// before reloadFromDisk). Otherwise a packet with to==0 would
// satisfy the equality and bypass the gate.
NodeNum ourNum = nodeDB->getNodeNum();
@@ -584,12 +584,12 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
fromRadioScratch.my_info = myNodeInfo;
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!getAdminAuthorized()) {
// device_id is a stable hardware identifier useful for an attacker
// device_id is a stable hardware identifier - useful for an attacker
// to fingerprint / correlate the device across observations. Strip it
// for unauthenticated clients. my_node_num is kept (it's broadcast
// on the mesh anyway). pio_env / min_app_version reveal the exact
// build flavour, useful only for picking which known-CVE to try.
// nodedb_count stays clients need it to decide whether to pull
// nodedb_count stays - clients need it to decide whether to pull
// the node DB after unlocking.
fromRadioScratch.my_info.device_id.size = 0;
memset(fromRadioScratch.my_info.device_id.bytes, 0, sizeof(fromRadioScratch.my_info.device_id.bytes));
@@ -657,7 +657,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
// position_flags, excluded_modules, optionsCount. None of it
// is needed to drive lockdown_auth, and most of it tells an
// attacker which CVE / behavior quirks to probe. Wipe the
// whole struct clients re-fetch once authenticated.
// whole struct - clients re-fetch once authenticated.
memset(&fromRadioScratch.metadata, 0, sizeof(fromRadioScratch.metadata));
}
#endif
@@ -668,7 +668,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
// Tell the client which modem presets are legal in each region so its UI
// can block illegal region+preset combinations. This is public RF /
// regulatory information (region and modem_preset are already in the
// unauthenticated LoRa whitelist below), so it is sent unconditionally
// unauthenticated LoRa whitelist below), so it is sent unconditionally -
// even an unauthorized/locked-down client can render a correct picker.
LOG_DEBUG("Send region preset map");
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_region_presets_tag;
@@ -683,7 +683,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
if (!getAdminAuthorized()) {
// Unauthenticated: emit a zero-initialized Channel. fromRadioScratch
// was memset(0) at the top of getFromRadio(), so leaving .channel
// untouched gives the client an empty entry no name, no PSK, no
// untouched gives the client an empty entry - no name, no PSK, no
// role. Advances the state machine normally so config_complete_id
// still fires.
} else
@@ -748,7 +748,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
// private knobs (ignore_incoming list, override_duty_cycle,
// override_frequency, sx126x_rx_boosted_gain, tx_power,
// ignore_mqtt, fem_lna_mode, config_ok_to_mqtt, ...) stay
// hidden they tell an attacker how the operator has tuned
// hidden - they tell an attacker how the operator has tuned
// the device but are not needed by an unauth client.
meshtastic_Config_LoRaConfig whitelist = {};
whitelist.use_preset = config.lora.use_preset;
@@ -775,7 +775,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
if (!getAdminAuthorized()) {
// Unauthenticated: emit an empty SecurityConfig (zero-init from
// the top-of-loop memset). No private_key, no admin_keys, no
// public_key nothing for an attacker to inspect.
// public_key - nothing for an attacker to inspect.
//
// Provisioning state (NEEDS_PROVISION vs LOCKED) is conveyed via
// the FromRadio.lockdown_status proto sent post-config; clients
@@ -906,7 +906,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) {
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!getAdminAuthorized()) {
// Unauthorized client: skip node DB and file manifest only send config complete
// Unauthorized client: skip node DB and file manifest - only send config complete
state = STATE_SEND_COMPLETE_ID;
} else
#endif
@@ -993,7 +993,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
} else if (mqttClientProxyMessageForPhone) {
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!getAdminAuthorized()) {
releaseMqttClientProxyPhonePacket(); // Discard unauthorized client
releaseMqttClientProxyPhonePacket(); // Discard - unauthorized client
} else
#endif
{
@@ -1004,7 +1004,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
} else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) {
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!getAdminAuthorized()) {
xmodemPacketForPhone = meshtastic_XModem_init_zero; // Discard unauthorized client
xmodemPacketForPhone = meshtastic_XModem_init_zero; // Discard - unauthorized client
} else
#endif
{
@@ -1015,7 +1015,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
} else if (hasPendingLockdownStatus()) {
concurrency::LockGuard guard(&g_authSlotsMutex);
// Look up our own slot only never another connection's. Re-check
// Look up our own slot only - never another connection's. Re-check
// hasPending under the lock since a concurrent drain on the same
// connection (unlikely but possible if multiple transport
// callbacks race against one PhoneAPI) may have grabbed it.
@@ -1033,7 +1033,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
} else if (packetForPhone) {
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
if (!getAdminAuthorized()) {
releasePhonePacket(); // Discard mesh traffic unauthorized client
releasePhonePacket(); // Discard mesh traffic - unauthorized client
} else
#endif
{
@@ -1044,7 +1044,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
releasePhonePacket();
}
} else if (replayPending()) {
// No live packet pending feed the phone one cached satellite-DB packet.
// No live packet pending - feed the phone one cached satellite-DB packet.
// popReplayPacket advances through positions->telemetry->environment->status,
// and flips replayPhase back to IDLE when everything has been drained.
meshtastic_MeshPacket replayPkt;
@@ -1222,7 +1222,7 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
pkt.hop_start = pkt.hop_limit;
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// Mark as if heard over the air, not internally generated iOS client filters
// Mark as if heard over the air, not internally generated - iOS client filters
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
@@ -1327,7 +1327,7 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
pkt.hop_start = pkt.hop_limit;
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// Mark as if heard over the air, not internally generated iOS client filters
// Mark as if heard over the air, not internally generated - iOS client filters
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
@@ -1391,7 +1391,7 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
pkt.hop_start = pkt.hop_limit;
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
// Mark as if heard over the air, not internally generated client filters
// Mark as if heard over the air, not internally generated - client filters
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
pkt.decoded.portnum = meshtastic_PortNum_NODE_STATUS_APP;
@@ -1483,7 +1483,7 @@ bool PhoneAPI::popReplayPacket(meshtastic_MeshPacket &out)
}
}
// Queue empty AND no more entries to feed it phase is exhausted.
// Queue empty AND no more entries to feed it - phase is exhausted.
advanceReplayPhase();
}
return false;
@@ -1609,7 +1609,7 @@ bool PhoneAPI::available()
hasPacket = !!packetForPhone;
if (hasPacket)
return true;
// Trailing replay drain feeds cached satellite-DB packets alongside
// Trailing replay drain - feeds cached satellite-DB packets alongside
// (lower priority than) live traffic.
return replayPending();
}
@@ -1666,7 +1666,7 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
// (b) Any other admin payload from an unauthorized connection:
// dropped here. The previous design relied on AdminModule
// to apply isLocalAdminAuthorized() during dispatch, but
// AdminModule runs on the Router task by then the
// AdminModule runs on the Router task - by then the
// PhoneAPI dispatching task has already exited and the
// per-connection auth context is unrecoverable. Putting
// the gate here closes that race and covers H6/H7 from the
@@ -1678,7 +1678,7 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &admin)) {
if (admin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag) {
handleLockdownAuthInline(admin.lockdown_auth);
// Wipe the decoded passphrase scratch the byte array in
// Wipe the decoded passphrase scratch - the byte array in
// p.decoded.payload.bytes is wiped by handleLockdownAuthInline.
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
@@ -1769,7 +1769,7 @@ bool PhoneAPI::getAdminAuthorized() const
{
// Runtime-toggle model: when lockdown is NOT active (a lockdown-capable
// build that hasn't been provisioned, or that was disabled), there is
// nothing to protect every connection is implicitly authorized, so
// nothing to protect - every connection is implicitly authorized, so
// all the `if (!getAdminAuthorized())` redaction gates throughout
// getFromRadio() / handleToRadio() become no-ops and the device serves
// config exactly like stock firmware. Only once provisioned (lockdown
@@ -1779,7 +1779,7 @@ bool PhoneAPI::getAdminAuthorized() const
return true;
#endif
concurrency::LockGuard g(&g_authSlotsMutex);
// const_cast is safe findOrAllocSlot_LH only mutates the slot table,
// const_cast is safe - findOrAllocSlot_LH only mutates the slot table,
// not the PhoneAPI itself, and the table key is just the pointer.
const auto *slot = findOrAllocSlot_LH(const_cast<PhoneAPI *>(this));
return slot && slot->authorized && slot->epoch == g_authEpoch;
@@ -1790,7 +1790,7 @@ void PhoneAPI::setAdminAuthorized(bool authorized)
concurrency::LockGuard g(&g_authSlotsMutex);
auto *slot = findOrAllocSlot_LH(this);
if (!slot)
return; // slot table full fail-closed
return; // slot table full - fail-closed
if (authorized) {
slot->epoch = g_authEpoch;
slot->authorized = true;
@@ -1813,7 +1813,7 @@ void PhoneAPI::completePendingUnlocks(bool reloadOk)
{
// Snapshot fields that we'll need outside the lock (we cannot call
// EncryptedStorage / setAdminAuthorized / unlockScreen while holding
// g_authSlotsMutex without risking re-entry setAdminAuthorized
// g_authSlotsMutex without risking re-entry - setAdminAuthorized
// itself takes the same lock).
constexpr size_t kMaxSnapshots = MAX_AUTH_SLOTS;
PhoneAPI *targets[kMaxSnapshots] = {};
@@ -1825,7 +1825,7 @@ void PhoneAPI::completePendingUnlocks(bool reloadOk)
continue;
if (targetCount < kMaxSnapshots)
targets[targetCount++] = s.who;
// Clear the pending flag either way failure path must not
// Clear the pending flag either way - failure path must not
// leave it set so a subsequent successful reload retries
// against the wrong PhoneAPI.
s.pendingUnlockAfterReload = false;
@@ -1841,13 +1841,13 @@ void PhoneAPI::completePendingUnlocks(bool reloadOk)
p->queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCKED, "", boots, until, 0);
}
// Screen-lock latch is cleared once any client successfully
// unlocks the operator has proven the passphrase. Matches the
// unlocks - the operator has proven the passphrase. Matches the
// re-verify path's behavior.
if (targetCount > 0)
meshtastic_security::unlockScreen();
LOG_INFO("Lockdown: post-reload completion: authorized %u connection(s)", (unsigned)targetCount);
} else {
// Storage corrupt emit LOCKED(storage_corrupt) to every slot
// Storage corrupt - emit LOCKED(storage_corrupt) to every slot
// that was awaiting the unlock. setAdminAuthorized is NOT called
// so the connection stays redacted and any set_config it sends
// is dropped at the existing unauth gates. Caller (main.cpp) has
@@ -1866,7 +1866,7 @@ void PhoneAPI::queueLockdownStatus(meshtastic_LockdownStatus_State state, const
concurrency::LockGuard guard(&g_authSlotsMutex);
auto *slot = findOrAllocStatusSlot_LH(this);
if (!slot)
return; // slot table exhausted fail-closed, no status delivered
return; // slot table exhausted - fail-closed, no status delivered
buildStatus_LH(slot->status, state, lock_reason, boots_remaining, valid_until_epoch, backoff_seconds);
slot->hasPending = true;
}
@@ -1914,14 +1914,14 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
ppVol[zi] = 0;
};
// Lock Now only honored from a connection that has already proven
// Lock Now - only honored from a connection that has already proven
// the passphrase. Unauthenticated clients used to be able to trigger
// a reboot, which was a trivial local-presence DoS (any BLE/USB
// attacker could brick-loop the device). Now lock_now requires
// prior auth on this connection.
if (la.lock_now) {
if (!getAdminAuthorized()) {
LOG_WARN("Lockdown: LOCK NOW from unauthorized connection denied");
LOG_WARN("Lockdown: LOCK NOW from unauthorized connection - denied");
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, 0);
zeroPassphrase();
return true;
@@ -1938,18 +1938,18 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
// Disable lockdown entirely. Requires the passphrase (must prove
// ownership before reverting at-rest encryption). We verify it here to
// load the DEK, then hand the heavy decrypt-revert work to the main
// loop via lockdownDisablePending exactly like the unlock reload
// loop via lockdownDisablePending - exactly like the unlock reload
// path, because decrypting + rewriting nodes.proto is too heavy for
// this transport-callback stack. APPROTECT is NOT reversed.
if (la.disable) {
if (la.passphrase.size < 1) {
LOG_WARN("Lockdown: disable with empty passphrase rejecting");
LOG_WARN("Lockdown: disable with empty passphrase - rejecting");
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, 0);
zeroPassphrase();
return true;
}
if (!EncryptedStorage::isLockdownActive()) {
// Already off nothing to do; report DISABLED so the client UI settles.
// Already off - nothing to do; report DISABLED so the client UI settles.
LOG_INFO("Lockdown: disable requested but lockdown is not active");
queueLockdownStatus(meshtastic_LockdownStatus_State_DISABLED, "", 0, 0, 0);
zeroPassphrase();
@@ -1972,13 +1972,13 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
return true;
}
// Empty-passphrase auth was previously a silent success clients
// Empty-passphrase auth was previously a silent success - clients
// got no feedback and the device looked the same as it would after
// an actual no-op. Emit UNLOCK_FAILED with no backoff so honest
// clients can detect their own bug and an attacker still learns
// nothing they wouldn't from any other failed attempt.
if (la.passphrase.size < 1) {
LOG_WARN("Lockdown: lockdown_auth with empty passphrase and lock_now=false rejecting");
LOG_WARN("Lockdown: lockdown_auth with empty passphrase and lock_now=false - rejecting");
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, 0);
zeroPassphrase();
return true;
@@ -2017,7 +2017,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
if (ok) {
needsReload = true;
// Mark this slot for the main-loop completion handler. Don't
// authorize or emit UNLOCKED yet `config` / `channelFile`
// authorize or emit UNLOCKED yet - `config` / `channelFile`
// / `nodeDatabase` still hold the locked-default placeholders
// installed by loadFromDisk()'s !isUnlocked() branch. If we
// flipped the connection to authorized here, the client could
@@ -2040,7 +2040,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
ok = EncryptedStorage::unlockWithPassphrase(la.passphrase.bytes, la.passphrase.size, boots, validUntilEpoch,
sessionMaxSeconds);
if (ok) {
// Storage was already unlocked no reload needed. Authorize
// Storage was already unlocked - no reload needed. Authorize
// and surface UNLOCKED to the client immediately.
setAdminAuthorized(true);
LOG_INFO("Lockdown: passphrase verified, this connection authorized");
@@ -2055,13 +2055,13 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCKED, "", EncryptedStorage::getBootsRemaining(),
EncryptedStorage::getValidUntilEpoch(), 0);
} else if (ok && needsReload) {
// Cold-unlock path: deliberately no status emission yet the
// Cold-unlock path: deliberately no status emission yet - the
// client keeps seeing LOCKED until completePendingUnlocks()
// runs after a successful reload.
} else {
uint32_t backoff = EncryptedStorage::getBackoffSecondsRemaining();
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, backoff);
// Don't log backoff seconds the client receives it in the
// Don't log backoff seconds - the client receives it in the
// UNLOCK_FAILED status anyway, and in non-DEBUG_MUTE builds the
// numeric value would otherwise spill onto a USB-attached
// attacker's serial terminal alongside other diagnostic noise.