Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion protobufs
6 changes: 5 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,7 @@ void setup()
nodeDB->hasWarned = true;
}
#endif
nodeDB->notifyPendingLicensedIdentityMigration();
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
if (inputBroker)
inputBroker->Init();
Expand Down Expand Up @@ -1206,7 +1207,7 @@ bool runASAP;
// TODO find better home than main.cpp
extern meshtastic_DeviceMetadata getDeviceMetadata()
{
meshtastic_DeviceMetadata deviceMetadata;
meshtastic_DeviceMetadata deviceMetadata = meshtastic_DeviceMetadata_init_zero;
strncpy(deviceMetadata.firmware_version, optstr(APP_VERSION), sizeof(deviceMetadata.firmware_version));
deviceMetadata.device_state_version = DEVICESTATE_CUR_VER;
deviceMetadata.canShutdown = pmu_found || HAS_CPU_SHUTDOWN;
Expand Down Expand Up @@ -1262,6 +1263,9 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()

#if !(MESHTASTIC_EXCLUDE_PKI)
deviceMetadata.hasPKC = true;
#endif
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
deviceMetadata.has_xeddsa = true;
#endif
return deviceMetadata;
}
Expand Down
14 changes: 8 additions & 6 deletions src/mesh/Channels.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,13 @@ bool Channels::ensureLicensedOperation()
}
auto &channelSettings = channel.settings;
if (strcasecmp(channelSettings.name, Channels::adminChannel) == 0) {
channel.role = meshtastic_Channel_Role_DISABLED;
channelSettings.psk.bytes[0] = 0;
channelSettings.psk.size = 0;
hasEncryptionOrAdmin = true;
channels.setChannel(channel);
if (channel.role != meshtastic_Channel_Role_DISABLED || channelSettings.psk.size > 0) {
channel.role = meshtastic_Channel_Role_DISABLED;
channelSettings.psk.bytes[0] = 0;
channelSettings.psk.size = 0;
hasEncryptionOrAdmin = true;
channels.setChannel(channel);
}

} else if (channelSettings.psk.size > 0) {
channelSettings.psk.bytes[0] = 0;
Expand Down Expand Up @@ -563,4 +565,4 @@ bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash)
int16_t Channels::setActiveByIndex(ChannelIndex channelIndex)
{
return setCrypto(channelIndex);
}
}
35 changes: 16 additions & 19 deletions src/mesh/FloodingRouter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
LOG_DEBUG("Repeated reliable tx");
// Check if it's still in the Tx queue, if not, we have to relay it again
if (!findInTxQueue(p->from, p->id)) {
reprocessPacket(p);
perhapsRebroadcast(p);
if (reprocessPacket(p))
perhapsRebroadcast(p);
}
} else {
perhapsCancelDupe(p);
Expand All @@ -68,14 +68,19 @@ bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p)
{
// isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages
if (isRebroadcaster() && iface && p->hop_limit > 0) {
// Re-authenticate before replacing the queued lower-hop copy so future callers remain safe.
if (passesRoutingAuthGate(p) != RoutingAuthVerdict::ACCEPT)
return true;

// If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to
// rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead.
uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining
if (iface->removePendingTXPacket(getFrom(p), p->id, dropThreshold)) {
LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id,
p->hop_limit, dropThreshold);

reprocessPacket(p);
if (!reprocessPacket(p))
return true;
perhapsRebroadcast(p);

rxDupe++;
Expand All @@ -87,32 +92,24 @@ bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p)
return false;
}

void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p)
bool FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p)
{
if (p->which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
auto decodedState = perhapsDecode(const_cast<meshtastic_MeshPacket *>(p));
if (decodedState != DecodeState::DECODE_SUCCESS && decodedState != DecodeState::DECODE_OPAQUE)
return false;
}

if (nodeDB)
nodeDB->updateFrom(*p);

#if !MESHTASTIC_EXCLUDE_TRACEROUTE
if (traceRouteModule && p->which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
// If we got a packet that is not decoded, try to decode it so we can check for traceroute.
auto decodedState = perhapsDecode(const_cast<meshtastic_MeshPacket *>(p));
if (decodedState == DecodeState::DECODE_SUCCESS) {
// parsing was successful, print for debugging
printPacket("reprocessPacket(DUP)", p);
} else {
// Fatal decoding error, we can't do anything with this packet
LOG_WARN(
"FloodingRouter::reprocessPacket: Fatal decode error (state=%d, id=0x%08x, from=%u), can't check for traceroute",
static_cast<int>(decodedState), p->id, getFrom(p));
return;
}
}

if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) {
traceRouteModule->processUpgradedPacket(*p);
}
#endif
return true;
}

bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p)
Expand Down
4 changes: 2 additions & 2 deletions src/mesh/FloodingRouter.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class FloodingRouter : public Router
bool perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p);

/* Call when we receive a packet that needs some reprocessing, but afterwards should be filtered */
void reprocessPacket(const meshtastic_MeshPacket *p);
bool reprocessPacket(const meshtastic_MeshPacket *p);

// Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of
// the same packet
Expand All @@ -75,4 +75,4 @@ class FloodingRouter : public Router

// Return true if we are a rebroadcaster
bool isRebroadcaster();
};
};
26 changes: 22 additions & 4 deletions src/mesh/NextHopRouter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@

NextHopRouter::NextHopRouter() {}

bool NextHopRouter::relayOpaquePacket(const meshtastic_MeshPacket *p)
{
// Opaque traffic is never admitted to PacketHistory, NodeDB, modules, phone, MQTT, or ACK
// handling. Relay only from the immutable outer routing header and let hop exhaustion bound it.
const auto mode = config.device.rebroadcast_mode;
if (!iface || isToUs(p) || isFromUs(p) || p->id == 0 || p->hop_limit == 0 || !isRebroadcaster() || owner.is_licensed ||
!IS_ONE_OF(mode, meshtastic_Config_DeviceConfig_RebroadcastMode_ALL,
meshtastic_Config_DeviceConfig_RebroadcastMode_ALL_SKIP_DECODING) ||
(p->next_hop != NO_NEXT_HOP_PREFERENCE && p->next_hop != nodeDB->getLastByteOfNodeNum(getNodeNum())))
return false;

meshtastic_MeshPacket *relay = packetPool.allocCopy(*p);
if (!relay)
return false;
relay->hop_limit--;
relay->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum());
return Router::send(relay) == ERRNO_OK;
}

PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions)
{
packet = p;
Expand Down Expand Up @@ -65,16 +84,15 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node);
// Check if it's still in the Tx queue, if not, we have to relay it again
if (!findInTxQueue(p->from, p->id)) {
reprocessPacket(p);
perhapsRebroadcast(p);
if (reprocessPacket(p))
perhapsRebroadcast(p);
}
} else {
bool isRepeated = getHopsAway(*p) == 0;
// If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again
if (isRepeated) {
if (!findInTxQueue(p->from, p->id)) {
reprocessPacket(p);
if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) {
if (reprocessPacket(p) && !perhapsRebroadcast(p) && isToUs(p) && p->want_ack) {
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/mesh/NextHopRouter.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class NextHopRouter : public FloodingRouter
* @return true to abandon the packet
*/
virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override;
bool relayOpaquePacket(const meshtastic_MeshPacket *p) override;

/**
* Look for packets we need to relay
Expand Down Expand Up @@ -209,4 +210,4 @@ class NextHopRouter : public FloodingRouter
/** Check if we should be rebroadcasting this packet if so, do so.
* @return true if we did rebroadcast */
bool perhapsRebroadcast(const meshtastic_MeshPacket *p) override;
};
};
56 changes: 49 additions & 7 deletions src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -860,7 +860,7 @@ void NodeDB::installDefaultNodeDatabase()
void NodeDB::installDefaultConfig(bool preserveKey = false)
{
uint8_t private_key_temp[32];
bool shouldPreserveKey = preserveKey && config.has_security && config.security.private_key.size > 0;
bool shouldPreserveKey = preserveKey && config.has_security && config.security.private_key.size == 32;
if (shouldPreserveKey) {
memcpy(private_key_temp, config.security.private_key.bytes, config.security.private_key.size);
}
Expand Down Expand Up @@ -1855,6 +1855,8 @@ bool NodeDB::enforceSatelliteCaps()
// them if they do); otherwise tracker/sensor/tak_tracker are role-protected.
static uint8_t warmProtectedCategory(const meshtastic_NodeInfoLite &n)
{
if (nodeInfoLiteHasXeddsaSigned(&n))
return static_cast<uint8_t>(WarmProtected::XeddsaSigner);
if (n.bitfield & (NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK |
NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK))
return static_cast<uint8_t>(WarmProtected::Flag);
Expand Down Expand Up @@ -2599,6 +2601,11 @@ void NodeDB::loadFromDisk()
moduleConfig.version = POSITION_TELEMETRY_OPTIN_VER;
saveToDisk(SEGMENT_MODULECONFIG);
}

if (channels.ensureLicensedOperation()) {
LOG_WARN("Licensed operation removed persisted channel encryption/admin access");
saveToDisk(SEGMENT_CHANNELS);
}
#if ARCH_PORTDUINO
// set any config overrides
if (portduino_config.has_configDisplayMode) {
Expand Down Expand Up @@ -3719,6 +3726,18 @@ bool NodeDB::copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
return false;
}

bool NodeDB::hasSeenXeddsaSigner(NodeNum n)
{
if (nodeInfoLiteHasXeddsaSigned(getMeshNode(n)))
return true;
#if WARM_NODE_COUNT > 0
uint8_t role = 0, prot = 0;
return warmStore.lookupMeta(n, role, prot) && prot == static_cast<uint8_t>(WarmProtected::XeddsaSigner);
#else
return false;
#endif
}

meshtastic_Config_DeviceConfig_Role NodeDB::getNodeRole(NodeNum n)
{
const meshtastic_NodeInfoLite *info = getMeshNode(n);
Expand Down Expand Up @@ -3811,6 +3830,8 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
lite->public_key.size = 32;
memcpy(lite->public_key.bytes, warm.public_key, 32);
}
if (warmProtOf(warm) == static_cast<uint8_t>(WarmProtected::XeddsaSigner))
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, true);
LOG_MIGRATION("Rehydrated node 0x%08x from warm tier (key=%d)", n, lite->public_key.size == 32);
}
#endif
Expand Down Expand Up @@ -3863,15 +3884,14 @@ bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_pub
bool NodeDB::generateCryptoKeyPair(const uint8_t *privateKey)
{
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
// Only generate keys for non-licensed users and if the LoRa region is set. The native simulator
// boots region-UNSET but still needs a keypair so PKI-encrypted DMs work between sim nodes, so
// allow keygen there regardless of region.
// Generate identity keys once a LoRa region is set. Licensed operation still needs the identity
// key for plaintext signatures, even though the key is never used for PKI encryption.
bool regionBlocksKeygen = config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET;
#if ARCH_PORTDUINO
if (portduino_config.lora_module == use_simradio)
regionBlocksKeygen = false;
#endif
if (owner.is_licensed || regionBlocksKeygen) {
if (regionBlocksKeygen) {
return false;
}

Expand Down Expand Up @@ -3921,15 +3941,30 @@ bool NodeDB::generateCryptoKeyPair(const uint8_t *privateKey)
LOG_DEBUG("Set DH private key for crypto operations");
crypto->setDHPrivateKey(config.security.private_key.bytes);

// Conditionally create new identity based on parameter
createNewIdentity();
if (createNewIdentity() && owner.is_licensed)
licensedIdentityMigrationPending = true;
}
return keygenSuccess;
#else
return false;
#endif
}

bool NodeDB::notifyPendingLicensedIdentityMigration()
{
if (!licensedIdentityMigrationPending || !service)
return false;
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
if (!notification)
return false;
notification->level = meshtastic_LogRecord_Level_WARNING;
notification->time = getValidTime(RTCQualityFromNet);
snprintf(notification->message, sizeof(notification->message), "%s", LICENSED_IDENTITY_MIGRATION_WARNING);
service->sendClientNotification(notification);
licensedIdentityMigrationPending = false;
return true;
}

bool NodeDB::createNewIdentity()
{
uint32_t oldNodeNum = getNodeNum();
Expand Down Expand Up @@ -4033,6 +4068,13 @@ bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location,
LOG_DEBUG("Restored channels");
}

if (owner.is_licensed && channels.ensureLicensedOperation()) {
restoreWhat |= SEGMENT_CHANNELS;
LOG_WARN("Licensed operation sanitized restored channel encryption/admin access");
}
if (restoreWhat & SEGMENT_CHANNELS)
channels.onConfigChanged();

success = saveToDisk(restoreWhat);
if (success) {
LOG_INFO("Restored preferences from backup");
Expand Down
10 changes: 10 additions & 0 deletions src/mesh/NodeDB.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ static const uint8_t LOW_ENTROPY_HASHES[][32] = {
0x37, 0x82, 0x8d, 0xb2, 0xcc, 0xd8, 0x97, 0x40, 0x9a, 0x5c, 0x8f, 0x40, 0x55, 0xcb, 0x4c, 0x3e}};
static const char LOW_ENTROPY_WARNING[] = "Compromised keys were detected and regenerated.";
#endif
static const char LICENSED_IDENTITY_MIGRATION_WARNING[] =
"Licensed signing generated a new identity key; this node identity changed.";
/*
DeviceState versions used to be defined in the .proto file but really only this function cares. So changed to a
#define here.
Expand Down Expand Up @@ -218,6 +220,7 @@ class NodeDB

bool keyIsLowEntropy = false;
bool hasWarned = false;
bool licensedIdentityMigrationPending = false;

/// don't do mesh based algorithm for node id assignment (initially)
/// instead just store in flash - possibly even in the initial alpha release do this hack
Expand Down Expand Up @@ -357,6 +360,10 @@ class NodeDB
/// tier. Returns false if we don't know a key for n.
bool copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out);

/// Whether this node has produced a verified XEdDSA signature, including while its
/// identity is resident only in the warm tier.
bool hasSeenXeddsaSigner(NodeNum n);

/// Resolve a node's device role - hot store (with user) first, then the role
/// cached in the warm tier, else CLIENT. Lets role-aware policy keep firing for
/// nodes that have aged out of the hot store.
Expand Down Expand Up @@ -471,6 +478,8 @@ class NodeDB
/// @param privateKey Optional 32-byte private key to use. If nullptr, generates new random keys.
bool generateCryptoKeyPair(const uint8_t *privateKey = nullptr);

bool notifyPendingLicensedIdentityMigration();

bool createNewIdentity();

bool backupPreferences(meshtastic_AdminMessage_BackupLocation location);
Expand Down Expand Up @@ -545,6 +554,7 @@ class NodeDB
// Grant the unit-test shim access to the private maintenance paths below
// (migration / cleanup / eviction) without relaxing production access.
friend class NodeDBTestShim;
friend class MockNodeDB;
#endif

/// purge db entries without user info
Expand Down
Loading