fix: don't wipe admin keys when regenerating the keypair (#11088)

A client's "regenerate keys" action sends a blank SecurityConfig carrying
only the new private key rather than the config it read from the device,
so assigning it wholesale cleared admin_key, is_managed, serial_enabled,
debug_log_api_enabled, admin_channel_enabled and packet_signature_policy.
Losing the admin keys locks the owner out of remote admin with no recourse
but a physical connection to the node.

Detect that bare-rotation shape and swap in just the keypair, leaving the
rest of the security config intact. Deliberately clearing admin keys still
works through a SET that leaves the private key alone.

Fixes #11073
This commit is contained in:
Ben Meadors authored and GitHub committed 2026-07-20 07:19:29 -05:00
1 parent 290967f739
commit 5cf346311c
2 files changed
+103

No files matched your search

+27
View File
@@ -815,6 +815,23 @@ static void reconcileAccelerometerThread(bool wasOn, bool nowOn, bool otherFeatu
}
#endif
// A "regenerate keys" client sends a blank SecurityConfig holding only the new private key, rather than the
// config it read from us. Detect that shape - new private key, every other field at its proto default - so it
// isn't mistaken for "and clear everything else".
static bool isBareKeypairRotation(const meshtastic_Config_SecurityConfig &incoming,
const meshtastic_Config_SecurityConfig &current)
{
if (incoming.private_key.size != 32)
return false;
if (current.private_key.size == 32 && memcmp(incoming.private_key.bytes, current.private_key.bytes, 32) == 0)
return false;
return incoming.admin_key_count == 0 && !incoming.is_managed && !incoming.serial_enabled && !incoming.debug_log_api_enabled &&
!incoming.admin_channel_enabled &&
incoming.packet_signature_policy ==
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE;
}
void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
{
auto changes = SEGMENT_CONFIG;
@@ -1104,6 +1121,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
incoming.private_key = config.security.private_key;
incoming.public_key = config.security.public_key;
}
// Rotating the keypair must not drop the admin keys - that locks the owner out of remote admin with no
// recourse but a physical connection. Clearing admin keys still works via a SET that leaves the private
// key alone and sends an empty list.
if (isBareKeypairRotation(incoming, config.security)) {
LOG_INFO("Security set is a bare keypair rotation; preserving remaining security config");
meshtastic_Config_SecurityConfig rotated = config.security;
rotated.public_key = incoming.public_key; // usually empty; derived from the private key below
rotated.private_key = incoming.private_key;
incoming = rotated;
}
config.security = incoming;
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI)
// First provisioning (no key) generates one; a private key supplied without its public key derives it.