mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 23:04:49 -05:00
* first pass tests * more tests * Fix two crafted-admin-packet crashes found by the E5 fuzzer Both are reachable from an authorized admin (local from==0, admin channel, or PKC) - remote DoS: 1. SIGFPE in LoRa config validation. A set_config LoRaConfig with use_preset=false and bandwidth=0 makes freqSlotWidth 0, so numFreqSlots is 0 and `hash(name) % numFreqSlots` (RadioInterface.cpp) divides by zero. Guard the modulo; the existing channel_num check then rejects/ clamps the config. 2. Stack overflow in Channels::getKey. A SECONDARY channel at the primary slot with an empty PSK recursed into getKey(primaryIndex) forever. Skip the primary-key borrow when chIndex == primaryIndex. Re-enable the E5 admin fuzzer to hit both triggers again (use_preset both ways incl. bandwidth 0, plus the set_channel tag) as regression guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Correct fuzz-test invariants after the crash fixes - E5 admin fuzz: node eviction under a filling NodeDB is legitimate, so assert only the bounded-count invariant, not that a specific seed node survives 6000 mutating ops. - TMM blitz: scope off the nodeinfo direct-response send path (it needs a fully-wired MeshService/phone queue the fixture doesn't provide; the deterministic directResponse tests cover it). The crafted-nodenum rate/unknown/position cache stress is unchanged. clod helped too * realistic tests * test: dedup fuzz RNG into shared test/support/DeterministicRng.h The four in-tree fuzz suites (test_fuzz_decode, test_fuzz_packets, test_hop_scaling, test_traffic_management) each carried a byte-identical copy of the seeded 64-bit LCG (rngSeed/rngNext/rngByte/rngRange). Hoist it into one shared header so there is a single generator to reason about and no risk of the copies drifting. static inline keeps per-suite state per translation unit and avoids -Wunused-function for suites that don't use every helper. Also corrects a stale comment in test_traffic_management (the blitz's nodeinfo direct-response path is intentionally left off). No behavioral change: same constants, same per-suite seeds. clod helped too * test: fuzz uncovered ProtobufModule handlers and the MQTT downlink ingress Extend the in-tree fuzz coverage to packet sources that previously had none: - test_fuzz_packets E8/E9/E10: drive PositionModule, DeviceTelemetryModule and NeighborInfoModule at handleReceivedProtobuf directly (via using-shims, bypassing the ProtobufModule reply/send path so no router is needed). The fixture already stands up nodeDB/service/channels, and nodeStatus/powerStatus are auto-initialized in main.cpp, so no new globals are required. Adds a shared fuzzRxHeader() helper for crafting adversarial RX packet headers. - test_fuzz_decode: add meshtastic_KeyVerification to the decode table. The KeyVerification and StoreForward handler paths are documented as decode-level only, with the concrete reason each is intrinsic (private-state gating / PSRAM + self-pointer wiring), not a fixture gap. - test_mqtt: test_receiveFuzzServiceEnvelope blitzes the non-RF broker-push ingress (onReceiveProto) two ways - raw garbage bytes that must fail envelope decode cleanly, and a well-formed ServiceEnvelope wrapping a crafted inner MeshPacket over crafted channel_id/gateway_id - exercising the channel match, isFromUs, XEdDSA receive policy and perhapsDecode chain. Adds a deliverRaw() passthrough to MQTTUnitTest. All under the coverage env (ASan/LSan). No firmware/src changes. Full sweep GREEN 27/27, 544 cases. clod helped too * Harden LoRa/channel config against crafted admin messages; consolidate test helpers Production (review findings on the hot-fuzz crash fixes): - Clamp bandwidth at the source (clampBandwidthKHz) in checkOrClampConfigLora and applyModemConfig so numFreqSlots can never be 0 for any consumer; a bandwidth-0 set_config previously passed validation and re-armed the SIGFPE on the next applyModemConfig. - Guard applyModemConfig's hash % numFreqSlots (the validator's sibling modulo was fixed earlier but this one was still unguarded). - Enforce the primary-channel invariant in Channels::onConfigChanged: a config demoting every slot now re-promotes the stale SECONDARY slot (keeping its key) or restores the default channel if the slot is DISABLED, instead of leaving every getPrimaryIndex() reader on a non-primary slot. The getKey recursion guard stays as defense-in-depth. Tests: - New test/support/MockMeshService.h and AdminModuleTestShim.h replace four byte-identical mocks and three divergent admin shims (test_mqtt's capturing mock is genuinely different and stays). - DeterministicRng.h: add rngFill() (replaces 14 hand-rolled fill loops) and rngEdgeNodeNum() (unifies the three NodeNum boundary pools). - Extract fuzzChannelSettings() shared by the set_channel case and fuzzBeacon. - fuzzBeacon: the un-terminated branch now fills the whole buffer with non-NUL bytes so the strnlen bound is actually stressed (~50% of iterations, not ~4%). - E6 beacon fuzz: replace the TEST_ASSERT_TRUE(true) tautology with real invariants (handler never consumes; offers land in lastReceivedOffer keyed to the sender). - Trim the seven over-long comment blocks flagged against the 1-2 line rule; the FINDINGS trailer moves to this commit message (see production notes). Full native suite GREEN 27/27 under the coverage (ASan/LSan) env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Clamp UTF-8 char length in the emote walkers; add test_fuzz_emotes A TEXT_MESSAGE payload is opaque protobuf bytes, so PB_VALIDATE_UTF8 never screens it - invalid UTF-8 and truncated multi-byte lead bytes reach the emote/width render path verbatim. EmoteRenderer's walkers advanced by utf8CharLen(lead) without clamping to the bytes actually remaining, so a truncated lead (e.g. a lone 0xF0, which claims 4 bytes) near the end of the buffer made getUtf8ChunkWidth's memcpy read past the string. ASan confirms a heap-buffer-overflow READ from measureStringWithEmotes. Add utf8CharLenClamped() and use it at every walk site (width measure, truncation cut-loop, and the draw-path text-run/chunk builders); the one already-guarded site (matchAtIgnoringModifiers) is unchanged. New test/test_fuzz_emotes drives measureStringWithEmotes and truncateToWidth over adversarial byte strings (biased to embed/end in truncated multi-byte leads) in exact-sized heap buffers so any over-read is a hard ASan fault. Its headless display uses a synthetic font (firstChar 0, fontData centered in a large buffer) so the stock OLEDDisplay::getStringWidth - which indexes the font jump table with a signed char and over-reads for any byte >= 0x80 - does not mask the finding. native-suite-count bumped 27 -> 28. Full native suite GREEN 28/28 under the coverage (ASan/LSan) env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep emote width measurement in-bounds for non-ASCII bytes OLEDDisplay::getStringWidth (the utf8=false path EmoteRenderer uses on default builds) indexes the font jump table by (c - firstChar) with a signed char and no bounds check, so any byte outside printable ASCII - high bytes from UTF-8 text, but also a stray control byte like 0x0A - reads outside the font array. On-device this reads adjacent flash and returns a garbage width; under ASan the test_fuzz_emotes fuzzer flags it as a global-buffer-overflow, and it made the non-ASCII width measurement meaningless either way. The OLED driver is a pinned upstream dependency, so guard it firmware-side in EmoteRenderer's getStringWidth helper: measure a sanitized copy where any byte outside [0x20, 0x7E] counts as a '?' placeholder. Printable ASCII is unchanged and the UA/RU lookup path is untouched. test_fuzz_emotes now drives a real ArialMT font instead of the synthetic in-bounds font it needed before this fix, so the suite exercises the true production width path (utf8CharLen clamp + this sanitizer) end to end. The same fuzzer tripped the global-buffer-overflow before this change. Full native suite GREEN 28/28 under the coverage (ASan/LSan) env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
551 lines
20 KiB
C++
551 lines
20 KiB
C++
#include "Channels.h"
|
|
|
|
#include "CryptoEngine.h"
|
|
#include "Default.h"
|
|
#include "DisplayFormatters.h"
|
|
#include "NodeDB.h"
|
|
#include "RadioInterface.h"
|
|
#include "configuration.h"
|
|
|
|
#include <assert.h>
|
|
|
|
#if !MESHTASTIC_EXCLUDE_MQTT
|
|
#include "mqtt/MQTT.h"
|
|
#endif
|
|
|
|
Channels channels;
|
|
|
|
const char *Channels::adminChannel = "admin";
|
|
const char *Channels::gpioChannel = "gpio";
|
|
const char *Channels::serialChannel = "serial";
|
|
#if !MESHTASTIC_EXCLUDE_MQTT
|
|
const char *Channels::mqttChannel = "mqtt";
|
|
#endif
|
|
|
|
meshtastic_Channel dummyChannel = {.index = -1};
|
|
|
|
uint8_t xorHash(const uint8_t *p, size_t len)
|
|
{
|
|
uint8_t code = 0;
|
|
for (size_t i = 0; i < len; i++)
|
|
code ^= p[i];
|
|
return code;
|
|
}
|
|
|
|
/** Given a channel number, return the (0 to 255) hash for that channel.
|
|
* The hash is just an xor of the channel name followed by the channel PSK being used for encryption
|
|
* If no suitable channel could be found, return -1
|
|
*/
|
|
int16_t Channels::generateHash(ChannelIndex channelNum)
|
|
{
|
|
auto k = getKey(channelNum);
|
|
if (k.length < 0)
|
|
return -1; // invalid
|
|
else {
|
|
const char *name = getName(channelNum);
|
|
uint8_t h = xorHash((const uint8_t *)name, strlen(name));
|
|
|
|
h ^= xorHash(k.bytes, k.length);
|
|
|
|
return h;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate a channel, fixing any errors as needed
|
|
*/
|
|
meshtastic_Channel &Channels::fixupChannel(ChannelIndex chIndex)
|
|
{
|
|
meshtastic_Channel &ch = getByIndex(chIndex);
|
|
|
|
ch.index = chIndex; // Preinit the index so it be ready to share with the phone (we'll never change it later)
|
|
|
|
if (!ch.has_settings) {
|
|
// No settings! Must disable and skip
|
|
ch.role = meshtastic_Channel_Role_DISABLED;
|
|
memset(&ch.settings, 0, sizeof(ch.settings));
|
|
ch.has_settings = true;
|
|
} else {
|
|
meshtastic_ChannelSettings &meshtastic_channelSettings = ch.settings;
|
|
|
|
// Convert the old string "Default" to our new short representation
|
|
if (strcmp(meshtastic_channelSettings.name, "Default") == 0)
|
|
*meshtastic_channelSettings.name = '\0';
|
|
}
|
|
|
|
hashes[chIndex] = generateHash(chIndex);
|
|
|
|
return ch;
|
|
}
|
|
|
|
void Channels::initDefaultLoraConfig()
|
|
{
|
|
meshtastic_Config_LoRaConfig &loraConfig = config.lora;
|
|
|
|
loraConfig.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; // Default to Long Range & Fast
|
|
loraConfig.use_preset = true;
|
|
loraConfig.tx_power = 0; // default
|
|
loraConfig.channel_num = 0;
|
|
|
|
#ifdef USERPREFS_LORACONFIG_TX_POWER
|
|
loraConfig.tx_power = USERPREFS_LORACONFIG_TX_POWER;
|
|
#endif
|
|
#ifdef USERPREFS_LORACONFIG_MODEM_PRESET
|
|
loraConfig.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET;
|
|
#endif
|
|
#ifdef USERPREFS_LORACONFIG_CHANNEL_NUM
|
|
loraConfig.channel_num = USERPREFS_LORACONFIG_CHANNEL_NUM;
|
|
#endif
|
|
|
|
// Apply any hardcoded USERPREFS overrides for custom modem config (e.g. region-locked boards)
|
|
#ifdef USERPREFS_LORACONFIG_USE_PRESET
|
|
loraConfig.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
|
|
#endif
|
|
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
|
|
loraConfig.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
|
|
#endif
|
|
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
|
|
loraConfig.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
|
|
#endif
|
|
#ifdef USERPREFS_LORACONFIG_CODING_RATE
|
|
loraConfig.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
|
|
#endif
|
|
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
|
|
loraConfig.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
|
|
#endif
|
|
}
|
|
|
|
bool Channels::ensureLicensedOperation()
|
|
{
|
|
if (!owner.is_licensed) {
|
|
return false;
|
|
}
|
|
bool hasEncryptionOrAdmin = false;
|
|
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
|
auto channel = channels.getByIndex(i);
|
|
if (!channel.has_settings) {
|
|
continue;
|
|
}
|
|
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);
|
|
|
|
} else if (channelSettings.psk.size > 0) {
|
|
channelSettings.psk.bytes[0] = 0;
|
|
channelSettings.psk.size = 0;
|
|
hasEncryptionOrAdmin = true;
|
|
channels.setChannel(channel);
|
|
}
|
|
}
|
|
return hasEncryptionOrAdmin;
|
|
}
|
|
|
|
/**
|
|
* Write a default channel to the specified channel index
|
|
*/
|
|
void Channels::initDefaultChannel(ChannelIndex chIndex)
|
|
{
|
|
meshtastic_Channel &ch = getByIndex(chIndex);
|
|
meshtastic_ChannelSettings &channelSettings = ch.settings;
|
|
|
|
uint8_t defaultpskIndex = 1;
|
|
channelSettings.psk.bytes[0] = defaultpskIndex;
|
|
channelSettings.psk.size = 1;
|
|
strncpy(channelSettings.name, "", sizeof(channelSettings.name));
|
|
channelSettings.module_settings.position_precision = 13; // default to sending location on the primary channel
|
|
channelSettings.has_module_settings = true;
|
|
|
|
ch.has_settings = true;
|
|
ch.role = chIndex == 0 ? meshtastic_Channel_Role_PRIMARY : meshtastic_Channel_Role_SECONDARY;
|
|
|
|
switch (chIndex) {
|
|
case 0:
|
|
#ifdef USERPREFS_CHANNEL_0_PSK
|
|
static const uint8_t defaultpsk0[] = USERPREFS_CHANNEL_0_PSK;
|
|
memcpy(channelSettings.psk.bytes, defaultpsk0, sizeof(defaultpsk0));
|
|
channelSettings.psk.size = sizeof(defaultpsk0);
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_0_NAME
|
|
strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_0_NAME);
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_0_PRECISION
|
|
channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_0_PRECISION;
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_0_UPLINK_ENABLED
|
|
channelSettings.uplink_enabled = USERPREFS_CHANNEL_0_UPLINK_ENABLED;
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_0_DOWNLINK_ENABLED
|
|
channelSettings.downlink_enabled = USERPREFS_CHANNEL_0_DOWNLINK_ENABLED;
|
|
#endif
|
|
break;
|
|
case 1:
|
|
#ifdef USERPREFS_CHANNEL_1_PSK
|
|
static const uint8_t defaultpsk1[] = USERPREFS_CHANNEL_1_PSK;
|
|
memcpy(channelSettings.psk.bytes, defaultpsk1, sizeof(defaultpsk1));
|
|
channelSettings.psk.size = sizeof(defaultpsk1);
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_1_NAME
|
|
strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_1_NAME);
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_1_PRECISION
|
|
channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_1_PRECISION;
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_1_UPLINK_ENABLED
|
|
channelSettings.uplink_enabled = USERPREFS_CHANNEL_1_UPLINK_ENABLED;
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_1_DOWNLINK_ENABLED
|
|
channelSettings.downlink_enabled = USERPREFS_CHANNEL_1_DOWNLINK_ENABLED;
|
|
#endif
|
|
break;
|
|
case 2:
|
|
#ifdef USERPREFS_CHANNEL_2_PSK
|
|
static const uint8_t defaultpsk2[] = USERPREFS_CHANNEL_2_PSK;
|
|
memcpy(channelSettings.psk.bytes, defaultpsk2, sizeof(defaultpsk2));
|
|
channelSettings.psk.size = sizeof(defaultpsk2);
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_2_NAME
|
|
strcpy(channelSettings.name, (const char *)USERPREFS_CHANNEL_2_NAME);
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_2_PRECISION
|
|
channelSettings.module_settings.position_precision = USERPREFS_CHANNEL_2_PRECISION;
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_2_UPLINK_ENABLED
|
|
channelSettings.uplink_enabled = USERPREFS_CHANNEL_2_UPLINK_ENABLED;
|
|
#endif
|
|
#ifdef USERPREFS_CHANNEL_2_DOWNLINK_ENABLED
|
|
channelSettings.downlink_enabled = USERPREFS_CHANNEL_2_DOWNLINK_ENABLED;
|
|
#endif
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
CryptoKey Channels::getKey(ChannelIndex chIndex)
|
|
{
|
|
meshtastic_Channel &ch = getByIndex(chIndex);
|
|
const meshtastic_ChannelSettings &channelSettings = ch.settings;
|
|
|
|
CryptoKey k;
|
|
memset(k.bytes, 0, sizeof(k.bytes)); // In case the user provided a short key, we want to pad the rest with zeros
|
|
|
|
if (!ch.has_settings || ch.role == meshtastic_Channel_Role_DISABLED) {
|
|
k.length = -1; // invalid
|
|
} else {
|
|
memcpy(k.bytes, channelSettings.psk.bytes, channelSettings.psk.size);
|
|
k.length = channelSettings.psk.size;
|
|
if (k.length == 0) {
|
|
// A secondary with no PSK borrows the primary's key; the chIndex != primaryIndex check
|
|
// prevents infinite recursion if the primary slot itself is marked SECONDARY
|
|
if (ch.role == meshtastic_Channel_Role_SECONDARY && chIndex != primaryIndex) {
|
|
LOG_DEBUG("Unset PSK for secondary channel %s. use primary key", ch.settings.name);
|
|
k = getKey(primaryIndex);
|
|
} else {
|
|
LOG_WARN("User disabled encryption");
|
|
}
|
|
} else if (k.length == 1) {
|
|
// Convert the short single byte variants of psk into variant that can be used more generally
|
|
|
|
uint8_t pskIndex = k.bytes[0];
|
|
LOG_DEBUG("Expand short PSK #%d", pskIndex);
|
|
if (pskIndex == 0)
|
|
k.length = 0; // Turn off encryption
|
|
else {
|
|
memcpy(k.bytes, defaultpsk, sizeof(defaultpsk));
|
|
k.length = sizeof(defaultpsk);
|
|
// Bump up the last byte of PSK as needed
|
|
uint8_t *last = k.bytes + sizeof(defaultpsk) - 1;
|
|
*last = *last + pskIndex - 1; // index of 1 means no change vs defaultPSK
|
|
}
|
|
} else if (k.length < 16) {
|
|
// Error! The user specified only the first few bits of an AES128 key. So by convention we just pad the rest of the
|
|
// key with zeros
|
|
LOG_WARN("User provided a too short AES128 key - padding");
|
|
k.length = 16;
|
|
} else if (k.length < 32 && k.length != 16) {
|
|
// Error! The user specified only the first few bits of an AES256 key. So by convention we just pad the rest of the
|
|
// key with zeros
|
|
LOG_WARN("User provided a too short AES256 key - padding");
|
|
k.length = 32;
|
|
}
|
|
}
|
|
|
|
return k;
|
|
}
|
|
|
|
/** Given a channel index, change to use the crypto key specified by that index
|
|
*/
|
|
int16_t Channels::setCrypto(ChannelIndex chIndex)
|
|
{
|
|
CryptoKey k = getKey(chIndex);
|
|
|
|
if (k.length < 0)
|
|
return -1;
|
|
else {
|
|
// Tell our crypto engine about the psk
|
|
crypto->setKey(k);
|
|
return getHash(chIndex);
|
|
}
|
|
}
|
|
|
|
void Channels::initDefaults()
|
|
{
|
|
channelFile.channels_count = MAX_NUM_CHANNELS;
|
|
for (int i = 0; i < channelFile.channels_count; i++)
|
|
fixupChannel(i);
|
|
initDefaultLoraConfig();
|
|
|
|
#ifdef USERPREFS_CHANNELS_TO_WRITE
|
|
for (int i = 0; i < USERPREFS_CHANNELS_TO_WRITE; i++) {
|
|
initDefaultChannel(i);
|
|
}
|
|
#else
|
|
initDefaultChannel(0);
|
|
#endif
|
|
}
|
|
|
|
void Channels::onConfigChanged()
|
|
{
|
|
// Make sure the phone hasn't mucked anything up
|
|
bool hasPrimary = false;
|
|
for (int i = 0; i < channelFile.channels_count; i++) {
|
|
const meshtastic_Channel &ch = fixupChannel(i);
|
|
|
|
if (ch.role == meshtastic_Channel_Role_PRIMARY) {
|
|
primaryIndex = i;
|
|
hasPrimary = true;
|
|
}
|
|
}
|
|
// Enforce the invariant that primaryIndex references a PRIMARY channel: a malformed config can
|
|
// demote every slot, which would leave all getPrimaryIndex() readers on a stale non-primary slot
|
|
if (!hasPrimary) {
|
|
meshtastic_Channel &stale = getByIndex(primaryIndex);
|
|
if (stale.role == meshtastic_Channel_Role_SECONDARY) {
|
|
stale.role = meshtastic_Channel_Role_PRIMARY; // keep the slot's key material
|
|
} else {
|
|
// Promoting a DISABLED (zeroed) slot would make a plaintext primary; restore the default
|
|
primaryIndex = 0;
|
|
initDefaultChannel(0);
|
|
}
|
|
LOG_WARN("Config has no PRIMARY channel, restored one at slot %u", primaryIndex);
|
|
fixupChannel(primaryIndex);
|
|
}
|
|
#if !MESHTASTIC_EXCLUDE_MQTT
|
|
if (channels.anyMqttEnabled() && mqtt && !mqtt->isEnabled()) {
|
|
LOG_DEBUG("MQTT is enabled on at least one channel, so set MQTT thread to run immediately");
|
|
mqtt->start();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
meshtastic_Channel &Channels::getByIndex(ChannelIndex chIndex)
|
|
{
|
|
// remove this assert cause malformed packets can make our firmware reboot here.
|
|
if (chIndex < channelFile.channels_count) { // This should be equal to MAX_NUM_CHANNELS
|
|
meshtastic_Channel *ch = channelFile.channels + chIndex;
|
|
return *ch;
|
|
} else {
|
|
LOG_ERROR("Invalid channel index %d > %d, malformed packet received?", chIndex, channelFile.channels_count);
|
|
return dummyChannel;
|
|
}
|
|
}
|
|
|
|
meshtastic_Channel &Channels::getByName(const char *chName)
|
|
{
|
|
for (ChannelIndex i = 0; i < getNumChannels(); i++) {
|
|
if (strcasecmp(getGlobalId(i), chName) == 0) {
|
|
return channelFile.channels[i];
|
|
}
|
|
}
|
|
|
|
return getByIndex(getPrimaryIndex());
|
|
}
|
|
|
|
void Channels::setChannel(const meshtastic_Channel &c)
|
|
{
|
|
meshtastic_Channel &old = getByIndex(c.index);
|
|
|
|
// if this is the new primary, demote any existing roles
|
|
if (c.role == meshtastic_Channel_Role_PRIMARY)
|
|
for (int i = 0; i < getNumChannels(); i++)
|
|
if (channelFile.channels[i].role == meshtastic_Channel_Role_PRIMARY)
|
|
channelFile.channels[i].role = meshtastic_Channel_Role_SECONDARY;
|
|
|
|
old = c; // slam in the new settings/role
|
|
}
|
|
|
|
bool Channels::anyMqttEnabled()
|
|
{
|
|
#if USERPREFS_EVENT_MODE && !MESHTASTIC_EXCLUDE_MQTT
|
|
// Don't publish messages on the public MQTT broker if we are in event mode
|
|
if (mqtt && mqtt->isUsingDefaultServer() && mqtt->isUsingDefaultRootTopic()) {
|
|
return false;
|
|
}
|
|
#endif
|
|
for (int i = 0; i < getNumChannels(); i++)
|
|
if (channelFile.channels[i].role != meshtastic_Channel_Role_DISABLED && channelFile.channels[i].has_settings &&
|
|
(channelFile.channels[i].settings.downlink_enabled || channelFile.channels[i].settings.uplink_enabled))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
const char *Channels::getName(size_t chIndex)
|
|
{
|
|
// Convert the short "" representation for Default into a usable string
|
|
const meshtastic_ChannelSettings &channelSettings = getByIndex(chIndex).settings;
|
|
const char *channelName = channelSettings.name;
|
|
if (!*channelName) { // emptystring
|
|
// Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case
|
|
// the app effed up and forgot to set channelSettings.name
|
|
if (config.lora.use_preset) {
|
|
channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
|
|
} else {
|
|
channelName = "Custom";
|
|
}
|
|
}
|
|
|
|
return channelName;
|
|
}
|
|
|
|
bool Channels::isDefaultChannel(ChannelIndex chIndex)
|
|
{
|
|
const auto &ch = getByIndex(chIndex);
|
|
if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) {
|
|
const char *name = getName(chIndex);
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset);
|
|
// Check if the name is the default derived from the modem preset
|
|
if (strcmp(name, presetName) == 0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool cryptoKeyIsPublic(const CryptoKey &key)
|
|
{
|
|
if (key.length == 0)
|
|
return true; // encryption disabled
|
|
// Match the defaultpsk family ignoring its last byte (getKey() bumps only that byte per 1-byte index).
|
|
if (key.length == (int)sizeof(defaultpsk) && memcmp(key.bytes, defaultpsk, sizeof(defaultpsk) - 1) == 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
bool Channels::usesPublicKey(ChannelIndex chIndex)
|
|
{
|
|
const meshtastic_Channel &ch = getByIndex(chIndex);
|
|
if (!ch.has_settings || ch.role == meshtastic_Channel_Role_DISABLED)
|
|
return false;
|
|
|
|
const auto &psk = ch.settings.psk;
|
|
if (psk.size == 0) {
|
|
// Secondary channels inherit the primary key when unset; primary size==0 means encryption disabled.
|
|
if (ch.role == meshtastic_Channel_Role_SECONDARY) {
|
|
// Guard against malformed configs with no PRIMARY channel (primaryIndex could point back to us).
|
|
if (primaryIndex == chIndex)
|
|
return true; // fail closed: treat as public
|
|
return usesPublicKey(primaryIndex);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (psk.size == 1) {
|
|
// Short PSK aliases: 0 disables encryption; 1..255 are the public defaultpsk family.
|
|
return true;
|
|
}
|
|
|
|
return (psk.size == sizeof(defaultpsk) && memcmp(psk.bytes, defaultpsk, sizeof(defaultpsk) - 1) == 0);
|
|
}
|
|
|
|
bool Channels::isWellKnownChannel(ChannelIndex chIndex)
|
|
{
|
|
const auto &ch = getByIndex(chIndex);
|
|
// Absent (unencrypted) or single-byte PSK - all the well-known key indexes
|
|
if (ch.settings.psk.size > 1)
|
|
return false;
|
|
|
|
const char *name = getName(chIndex);
|
|
for (int p = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; p <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; p++) {
|
|
const char *presetName =
|
|
DisplayFormatters::getModemPresetDisplayName(static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(p), false, true);
|
|
// Presets without a display name fall through to "Invalid" - never a match
|
|
if (strcmp(presetName, "Invalid") != 0 && strcmp(name, presetName) == 0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Channels::hasDefaultChannel()
|
|
{
|
|
// If we don't use a preset or the default frequency slot, or we override the frequency, we don't have a default channel
|
|
if (!config.lora.use_preset || !RadioInterface::uses_default_frequency_slot || config.lora.override_frequency)
|
|
return false;
|
|
// Check if any of the channels are using the default name and PSK
|
|
for (size_t i = 0; i < getNumChannels(); i++) {
|
|
if (isDefaultChannel(i))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Given a channel hash setup crypto for decoding that channel (or the primary channel if that channel is unsecured)
|
|
*
|
|
* This method is called before decoding inbound packets
|
|
*
|
|
* @return false if the channel hash or channel is invalid
|
|
*/
|
|
bool Channels::decryptForHash(ChannelIndex chIndex, ChannelHash channelHash)
|
|
{
|
|
if (chIndex > getNumChannels() || getHash(chIndex) != channelHash) {
|
|
// LOG_DEBUG("Skip channel %d (hash %x) due to invalid hash/index, want=%x", chIndex, getHash(chIndex),
|
|
// channelHash);
|
|
return false;
|
|
} else {
|
|
LOG_DEBUG("Use channel %d (hash 0x%x)", chIndex, channelHash);
|
|
setCrypto(chIndex);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash)
|
|
{
|
|
// Iterate all known presets
|
|
for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX;
|
|
++preset) {
|
|
const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false,
|
|
config.lora.use_preset);
|
|
if (!name)
|
|
continue;
|
|
if (strcmp(name, "Invalid") == 0)
|
|
continue; // skip invalid placeholder
|
|
uint8_t h = xorHash((const uint8_t *)name, strlen(name));
|
|
// Expand default PSK alias 1 to actual bytes and xor into hash
|
|
uint8_t tmp = h ^ xorHash(defaultpsk, sizeof(defaultpsk));
|
|
if (tmp == channelHash) {
|
|
// Set crypto to defaultpsk and report success
|
|
CryptoKey k;
|
|
memcpy(k.bytes, defaultpsk, sizeof(defaultpsk));
|
|
k.length = sizeof(defaultpsk);
|
|
crypto->setKey(k);
|
|
LOG_INFO("Matched default preset '%s' for hash 0x%x; set default PSK", name, channelHash);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Given a channel index setup crypto for encoding that channel (or the primary channel if that channel is unsecured)
|
|
*
|
|
* This method is called before encoding outbound packets
|
|
*
|
|
* @return the (0 to 255) hash for that channel - if no suitable channel could be found, return -1
|
|
*/
|
|
int16_t Channels::setActiveByIndex(ChannelIndex channelIndex)
|
|
{
|
|
return setCrypto(channelIndex);
|
|
} |