Clamp position precision on public / known-keys (#10665)

* Clamp position precision on public / known-keys (Compliance)

* Fix review comments: bounds check, all-channel precision clamp, disabled channel guard

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Refactor position precision comments for clarity and update channel configuration handling in AdminModule

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Ben Meadors
2026-06-14 18:53:18 -05:00
committed by GitHub
co-authored by GitHub copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Copilot Autofix powered by AI
parent 8a0c7592cc
commit d878c81ce8
7 changed files with 180 additions and 1 deletions
+36
View File
@@ -404,6 +404,42 @@ bool Channels::isDefaultChannel(ChannelIndex chIndex)
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::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
+6
View File
@@ -86,6 +86,9 @@ class Channels
// Returns true if the channel has the default name and PSK
bool isDefaultChannel(ChannelIndex chIndex);
// Returns true if this channel's effective key is publicly decryptable (open or well-known/default PSK).
bool usesPublicKey(ChannelIndex chIndex);
// Returns true if we can be reached via a channel with the default settings given a region and modem preset
bool hasDefaultChannel();
@@ -144,6 +147,9 @@ extern Channels channels;
static const uint8_t defaultpsk[] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59,
0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01};
/// True if a getKey()-resolved key offers no privacy: length 0 (off) or the public defaultpsk family. Pure; for tests.
bool cryptoKeyIsPublic(const CryptoKey &key);
static const uint8_t eventpsk[] = {0x38, 0x4b, 0xbc, 0xc0, 0x1d, 0xc0, 0x22, 0xd1, 0x81, 0xbf, 0x36,
0xb8, 0x61, 0x21, 0xe1, 0xfb, 0x96, 0xb7, 0x2e, 0x55, 0xbf, 0x74,
0x22, 0x7e, 0x9d, 0x6a, 0xfb, 0x48, 0xd6, 0x4c, 0xb1, 0xa1};
+10 -1
View File
@@ -16,7 +16,16 @@ uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel)
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex)
{
return getPositionPrecisionForChannel(channels.getByIndex(channelIndex));
const meshtastic_Channel &ch = channels.getByIndex(channelIndex);
if (ch.role == meshtastic_Channel_Role_DISABLED)
return 0;
uint32_t precision = getPositionPrecisionForChannel(ch);
// Never send a precise position on a publicly-decryptable channel (key check is gated on > ceiling).
if (precision > MAX_POSITION_PRECISION_PUBLIC_KEY && channels.usesPublicKey(channelIndex)) {
precision = MAX_POSITION_PRECISION_PUBLIC_KEY;
}
return precision;
}
static int32_t truncateCoordinate(int32_t coordinate, uint32_t precision)
+9
View File
@@ -4,7 +4,16 @@
#include "meshtastic/mesh.pb.h"
#include <stdint.h>
// Max precision on a publicly-decryptable channel. CCPA "precise geolocation" = within a ~564m (1,850ft) radius.
// Precision is bit-truncation of latitude_i/longitude_i: the latitude cell stays ~constant in meters worldwide
// (~700m at 15 bits), while only the longitude cell varies — widest at the equator, narrowing toward the poles.
// 15 also matches the MQTT map-report public precision ceiling.
#define MAX_POSITION_PRECISION_PUBLIC_KEY 15
// Configured precision as-is; does NOT apply the public-key clamp -- use the channelIndex overload for the on-wire value.
uint32_t getPositionPrecisionForChannel(const meshtastic_Channel &channel);
// Configured precision, clamped to MAX_POSITION_PRECISION_PUBLIC_KEY when the channel's effective key is publicly decryptable.
uint32_t getPositionPrecisionForChannel(uint8_t channelIndex);
void applyPositionPrecision(meshtastic_Position &position, uint32_t precision);
bool applyPositionPrecision(meshtastic_MeshPacket &packet, uint32_t precision);
+20
View File
@@ -2,6 +2,7 @@
#include "Channels.h"
#include "MeshService.h"
#include "NodeDB.h"
#include "PositionPrecision.h"
#include "PowerFSM.h"
#include "RTC.h"
#include "SPILock.h"
@@ -1157,7 +1158,26 @@ void AdminModule::handleSetChannel(const meshtastic_Channel &cc)
if (channels.ensureLicensedOperation()) {
sendWarning(licensedModeMessage);
}
// Refresh derived state (primaryIndex in particular) BEFORE the precision clamp below. usesPublicKey()
// resolves a secondary channel's key against the primary, so it must see the post-update primaryIndex;
// running the clamp first could evaluate secondaries against the previous primary and skip the clamp/warning.
channels.onConfigChanged(); // tell the radios about this change
// Persist the public-key precision clamp for all channels that may be affected (e.g. secondaries
// that inherit a now-public primary key) and warn the client once if anything was coarsened.
bool clamped = false;
for (uint8_t i = 0; i < channels.getNumChannels(); i++) {
meshtastic_Channel &ch = channels.getByIndex(i);
if (ch.role == meshtastic_Channel_Role_DISABLED || !ch.settings.has_module_settings)
continue;
uint32_t allowed = getPositionPrecisionForChannel(i);
if (allowed != ch.settings.module_settings.position_precision) {
ch.settings.module_settings.position_precision = allowed;
clamped = true;
}
}
if (clamped)
sendWarning(publicChannelPrecisionMessage);
saveChanges(SEGMENT_CHANNELS, false);
}
+3
View File
@@ -88,6 +88,9 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
static constexpr const char *licensedModeMessage =
"Licensed mode activated, removing admin channel and encryption from all channels";
static constexpr const char *publicChannelPrecisionMessage =
"Precise position is not allowed on a public (open / known-key) channel; reduced to coarse precision";
extern AdminModule *adminModule;
void disableBluetooth();