Files
MeshCore/src/helpers/FleetCommand.h
T

128 lines
6.3 KiB
C++

#pragma once
#include <stddef.h>
#include <stdint.h>
#include "../Identity.h"
#include "FleetControlConfig.h"
namespace mesh {
// Signed, private-channel fleet commands. The channel key supplies privacy;
// only the publisher's Ed25519 signature authorizes a command. Receivers store
// the public identity and never need the publisher's private identity.
class FleetCommand {
public:
static constexpr uint16_t DataType = 0xFF01;
static constexpr size_t KeySize = 16;
static constexpr size_t TargetSize = 16;
static constexpr size_t SignatureSize = 64;
static constexpr size_t HeaderSize = 29;
static constexpr size_t MinHeaderSize = 14;
static constexpr size_t MaxPayloadLength = 165;
static constexpr size_t FragmentHeaderSize = 11;
static constexpr size_t FragmentDataLength = MaxPayloadLength - FragmentHeaderSize;
static constexpr size_t MaxEnvelopeLength = 2 * FragmentDataLength;
static constexpr size_t SinglePacketMaxCommandLength = MaxPayloadLength - MinHeaderSize - SignatureSize;
static constexpr size_t MaxCommandLength = MaxEnvelopeLength - MinHeaderSize - SignatureSize;
static constexpr size_t FixedTargetMaxCommandLength = MaxEnvelopeLength - HeaderSize - SignatureSize;
static constexpr size_t MaxTargetBytes = 86;
static constexpr size_t MaxTargetCount = 17;
static constexpr uint8_t RegionRecordType = 0x20;
static constexpr uint8_t HomeRecordType = 0x21;
static constexpr uint8_t GeoRecordType = 0x22;
static constexpr size_t GeoRecordLength = 13;
static constexpr size_t MaxRegionNameLength = 30;
static constexpr uint32_t MinRadiusMeters = 1;
static constexpr uint32_t MaxRadiusMeters = 20050000;
static constexpr uint32_t MinEpoch = 1735689600UL; // 2025-01-01 UTC
static constexpr uint32_t MaxLifetime = 600;
static constexpr uint32_t MaxClockLead = 60;
struct Decoded {
uint32_t sequence;
uint32_t expires;
bool broadcast;
char command[MaxCommandLength + 1];
};
struct Targets {
uint8_t length;
uint8_t count;
// Each record is a length/type byte (4, 6, or 16) followed by that many
// bytes. Prefixes are raw public-key bytes; complete keys are SHA-256/128.
// Region/home records are type 0x20/0x21, name length, then canonical name.
// GPS records are type 0x22, signed latitude/longitude E6, radius metres LE32.
uint8_t data[MaxTargetBytes];
};
enum class RegionTarget { Configured, Home };
// Names are length-delimited and case-sensitive, with one leading '#' alias
// removed by the encoder. The role matches against its existing RegionMap.
// This bounded, read-only callback must not execute or mutate any state.
using RegionMatcher = bool (*)(void* context, RegionTarget target,
const char* name, size_t length);
using GeoMatcher = bool (*)(void* context, int32_t latitude_e6,
int32_t longitude_e6, uint32_t radius_meters);
// Optional runtime work gate. Called only after all cheap envelope,
// targeting and time checks, immediately before Ed25519 verification.
using SignatureGate = bool (*)(void* context);
struct Fragment {
uint32_t sequence;
uint16_t total_length;
uint8_t index;
const uint8_t* data;
size_t length;
};
static bool privateKeyAllowed(const uint8_t* key);
// "all" becomes the reserved zero target; otherwise require one complete
// public key and derive the first 16 bytes of SHA-256, not a short node ID.
static bool parseTarget(const char* text, uint8_t* target);
// Parse one bounded token containing semicolon-separated public keys and named
// regions. Bare key-length tokens never fall back to region names; explicit
// region:/home: selectors permit ambiguous names. GPS circles use
// gps:latitude,longitude:radius-km. "all" is accepted alone.
static bool parseTargets(const char* text, size_t length, Targets& targets);
static bool commandAllowed(const char* command);
static bool withinRadius(int32_t center_latitude_e6, int32_t center_longitude_e6,
uint32_t radius_meters, int32_t local_latitude_e6,
int32_t local_longitude_e6);
// Return the exact envelope size, or zero on invalid input. The signature
// binds the domain, private channel key, complete header, and command bytes.
static size_t encode(const LocalIdentity& publisher, const uint8_t* key,
uint32_t sequence, uint32_t expires,
const uint8_t* target, const char* command,
uint8_t* output, size_t capacity);
// Broadcast and prefix/list requests use FMC2. Zero records imply all;
// a single complete key retains FMC1 encoding. Targets and command share
// the two-packet envelope budget, so lists reduce the command allowance.
static size_t encode(const LocalIdentity& publisher, const uint8_t* key,
uint32_t sequence, uint32_t expires,
const Targets& targets, const char* command,
uint8_t* output, size_t capacity);
// Accept an exact envelope only: group framing owns padding validation.
// On success the caller must durably reject an old/repeated sequence before
// any mutation, and defer execution until the receive stack has unwound.
static bool decode(const Identity& publisher, const uint8_t* key,
const uint8_t* payload, size_t length, uint32_t now,
const uint8_t* self_public_key, Decoded& output,
RegionMatcher matcher = nullptr, void* context = nullptr,
GeoMatcher geo_matcher = nullptr, void* geo_context = nullptr,
SignatureGate signature_gate = nullptr, void* signature_context = nullptr);
// Envelopes <= MaxPayloadLength travel unchanged. Longer envelopes use
// exactly two FMP1 frames, each bounded by the group payload limit.
static size_t fragment(const uint8_t* envelope, size_t length, uint8_t index,
uint8_t* output, size_t capacity);
// Fragment metadata is untrusted. The receiver must assemble both parts,
// verify the entire envelope once, require its signed sequence to match,
// and durably reserve replay state before executing any command.
static bool parseFragment(const uint8_t* payload, size_t length, Fragment& output);
};
} // namespace mesh