"""Execute the bounded MT codec with real Crypto AES and an independent oracle. No PlatformIO, hardware or downloaded sources are used. Set MESHCORE_CRYPTO_DIR to an existing rweather/Crypto checkout, or use the cached PlatformIO dependency. The Python cryptography package independently generates encrypted wire vectors. """ import os from pathlib import Path import shutil import struct import subprocess import sys import tempfile import unittest ROOT = Path(__file__).resolve().parents[1] DEFAULT_KEY = bytes.fromhex("d4f1bb3a20290759f0bcffabcf4e6901") FROM, PACKET_ID = 0x11223344, 0x89ABCDEF def varint(value): encoded = bytearray() while value >= 128: encoded.append((value & 127) | 128) value >>= 7 encoded.append(value) return bytes(encoded) def data_text(text): return b"\x08\x01\x12" + varint(len(text)) + text def data_nodeinfo(long_name, short_name, sender): # Independent handwritten schema oracle: User fields 1/2/3/7/9 only. user_id = ("!%08x" % sender).encode("ascii") user = b"\x0a" + varint(len(user_id)) + user_id if long_name: user += b"\x12" + varint(len(long_name)) + long_name user += b"\x1a" + varint(len(short_name)) + short_name user += b"\x38\x01\x48\x01" # CLIENT_MUTE=1, is_unmessagable=true. return b"\x08\x04\x12" + varint(len(user)) + user def reference_frame(data, name="LongFast", key=DEFAULT_KEY, *, to=0xFFFFFFFF, sender=FROM, packet_id=PACKET_ID, flags=0x63): from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes channel_hash = 0 for byte in name.encode() + key: channel_hash ^= byte header = struct.pack(" #include #include #include #include #include using namespace mesh::mt; struct Golden { uint8_t key[32]; size_t key_len; const char* name; const char* text; size_t text_len; uint8_t frame[255]; size_t frame_len; }; static const Golden golden[] = { @GOLDEN@ }; // Stock 2.7.26 Router::perhapsEncode marks Data.bitfield present even when // config_ok_to_mqtt and want_response are both false: field 9 is 0x48,0x00. static const Golden stock_zero_bitfield[] = { @STOCK_ZERO_BITFIELD@ }; struct NodeInfoGolden { uint8_t key[32]; size_t key_len; const char* name; const char* long_name; size_t long_name_len; const char* short_name; size_t short_name_len; uint32_t from, id; uint8_t hops; uint8_t frame[255]; size_t frame_len; }; static const NodeInfoGolden node_info_golden[] = { @NODEINFO_GOLDEN@ }; struct Malformed { const char* label; uint8_t frame[255]; size_t length; }; static const Malformed malformed[] = { @MALFORMED@ }; struct Modem { Region region; Preset preset; bool valid; float frequency, bandwidth; uint8_t sf, cr; }; static const Modem modems[] = { @MODEMS@ }; static void presets() { Preset preset = Preset::LongFast; for (const auto* name : {"ShortTurbo", "ShortFast", "ShortSlow", "MediumTurbo", "MediumFast", "MediumSlow", "LongTurbo", "LongFast", "LongMod", "LongSlow"}) { assert(parsePreset(name, preset)); assert(!strcmp(presetName(preset), name)); } assert(parsePreset("LongModerate", preset) && preset == Preset::LongModerate); assert(parsePreset("longfast", preset) && preset == Preset::LongFast); assert(parsePreset("shortturbo", preset) && preset == Preset::ShortTurbo); assert(parsePreset("LoNgTuRbO", preset) && preset == Preset::LongTurbo); assert(parsePreset("LONGMODERATE", preset) && preset == Preset::LongModerate); for (const auto* invalid : {"", "Long-Fast", "LongUltra", "mt-LongFast", "LongFast "}) { preset = Preset::LongFast; assert(!parsePreset(invalid, preset) && preset == Preset::LongFast); } assert(!parsePreset(nullptr, preset)); assert(presetName(static_cast(255)) == nullptr); for (const auto& test : modems) { mesh::RadioProfileParams params; params.freq = 123; const auto original = params; assert(modemConfig(test.region, test.preset, params) == test.valid); if (!test.valid) { assert(params == original); continue; } assert(std::fabs(params.freq - test.frequency) < 0.0001f); assert(params.bw == test.bandwidth && params.sf == test.sf && params.cr == test.cr); assert(params.preamble == 16 && params.protocol == mesh::RadioProfileProtocol::Meshtext); assert(modemConfig(test.region, test.preset, params, 1)); const float start = test.region == Region::US ? 902 : test.region == Region::EU868 ? 869.4f : test.region == Region::EU433 ? 433 : 915; const uint32_t width = test.region == Region::US ? 26000 : test.region == Region::EU868 ? 250 : test.region == Region::EU433 ? 1000 : 13000; const uint32_t slots = width / static_cast(test.bandwidth); assert(std::fabs(params.freq - (start + test.bandwidth / 2000)) < 0.0001f); assert(modemConfig(test.region, test.preset, params, slots)); const auto last = params; assert(!modemConfig(test.region, test.preset, params, slots + 1) && params == last); assert(!modemConfig(test.region, test.preset, params, 0xffffffffU) && params == last); } mesh::RadioProfileParams params; assert(!modemConfig(Region::Unset, Preset::LongFast, params)); assert(!modemConfig(static_cast(255), Preset::LongFast, params)); assert(!modemConfig(Region::US, static_cast(255), params)); puts("PASS presets"); } static void frames() { for (const auto& test : golden) { uint8_t frame[MaxFrameSize + 2]; memset(frame, 0xa5, sizeof(frame)); const size_t length = encodeText(0x11223344, 0x89abcdef, 3, test.name, test.key, test.key_len, test.text, test.text_len, frame); assert(length == test.frame_len && !memcmp(frame, test.frame, length)); assert(frame[MaxFrameSize] == 0xa5 && frame[MaxFrameSize + 1] == 0xa5); DecodedText decoded; assert(decodeText(test.frame, test.frame_len, test.name, test.key, test.key_len, 0x99887766, decoded)); assert(decoded.from == 0x11223344 && decoded.id == 0x89abcdef); assert(decoded.hop_limit == 3 && decoded.hop_start == 3 && decoded.hops == 0); assert(decoded.text_len == test.text_len && !memcmp(decoded.text, test.text, test.text_len)); assert(decoded.text[decoded.text_len] == 0); memcpy(frame, test.frame, test.frame_len); frame[12] = (7 << 5) | 2; assert(decodeText(frame, length, test.name, test.key, test.key_len, 0x99887766, decoded)); assert(decoded.hops == 5 && decoded.hop_start == 7 && decoded.hop_limit == 2); frame[12] = 3; // older hop-start-less packets remain readable assert(decodeText(frame, length, test.name, test.key, test.key_len, 0x99887766, decoded)); assert(decoded.hops == 0); assert(!decodeText(frame, length, test.name, test.key, test.key_len, 0x11223344, decoded)); frame[13] ^= 1; assert(!decodeText(frame, length, test.name, test.key, test.key_len, 0x99887766, decoded)); } assert(!memcmp(DefaultPsk, golden[0].key, 16)); puts("PASS frames"); } static void malformed_frames() { for (const auto& test : malformed) { DecodedText decoded; decoded.id = 123; strcpy(decoded.text, "unchanged"); decoded.text_len = 9; if (decodeText(test.frame, test.length, "LongFast", DefaultPsk, 16, 0x99887766, decoded)) { fprintf(stderr, "accepted malformed vector: %s\n", test.label); assert(false); } assert(decoded.id == 123 && decoded.text_len == 9 && !strcmp(decoded.text, "unchanged")); } puts("PASS malformed"); } static void node_info_frames() { static_assert(MaxLongNameSize == 24, "stock long-name limit"); static_assert(MaxShortNameSize == 4, "stock short-name max_size:5 includes NUL"); for (const auto& test : node_info_golden) { uint8_t frame[MaxFrameSize + 2]; memset(frame, 0xa5, sizeof(frame)); const size_t length = encodeNodeInfo(test.from, test.id, test.hops, test.name, test.key, test.key_len, test.long_name, test.long_name_len, test.short_name, test.short_name_len, frame); // Full independent encrypted-frame equality also proves exact standard User // fields, matching nonce/header, and absence of Data.want_response/keys/MAC. assert(length == test.frame_len && !memcmp(frame, test.frame, length)); assert(frame[length] == 0xa5 && frame[MaxFrameSize] == 0xa5 && frame[MaxFrameSize + 1] == 0xa5); assert((frame[12] & 0x18) == 0); // Neither want_ack nor via_mqtt. assert((frame[12] & 7) == test.hops && (frame[12] >> 5) == test.hops); assert(frame[14] == 0 && frame[15] == 0); DecodedText decoded; decoded.id = 123; strcpy(decoded.text, "unchanged"); assert(!decodeText(frame, length, test.name, test.key, test.key_len, 0x99887766, decoded)); assert(decoded.id == 123 && !strcmp(decoded.text, "unchanged")); // NodeInfo RX still ignored. } puts("PASS nodeinfo"); } static void node_info_bounds() { uint8_t frame[MaxFrameSize]; memset(frame, 0xa5, sizeof(frame)); const auto invalid = [&](uint32_t from, uint32_t id, uint8_t hops, const char* name, const uint8_t* key, size_t key_len, const char* long_name, size_t long_len, const char* short_name, size_t short_len) { assert(!encodeNodeInfo(from, id, hops, name, key, key_len, long_name, long_len, short_name, short_len, frame)); for (uint8_t byte : frame) assert(byte == 0xa5); // Rejection writes no partial packet. }; char long_name[MaxLongNameSize + 1]; memset(long_name, 'L', sizeof(long_name)); char short_name[MaxShortNameSize + 1]; memset(short_name, 'S', sizeof(short_name)); for (uint32_t from : {0U, 1U, 2U, 3U, 0xffffffffU}) invalid(from, 1, 7, "LongFast", DefaultPsk, 16, "long", 4, "shrt", 4); invalid(4, 0, 7, "LongFast", DefaultPsk, 16, "long", 4, "shrt", 4); invalid(4, 1, 8, "LongFast", DefaultPsk, 16, "long", 4, "shrt", 4); for (const auto* name : {static_cast(nullptr), "", "123456789012"}) invalid(4, 1, 7, name, DefaultPsk, 16, "long", 4, "shrt", 4); for (size_t key_len : {size_t(0), size_t(1), size_t(15), size_t(17), size_t(31), size_t(33)}) invalid(4, 1, 7, "LongFast", DefaultPsk, key_len, "long", 4, "shrt", 4); invalid(4, 1, 7, "LongFast", nullptr, 16, "long", 4, "shrt", 4); invalid(4, 1, 7, "LongFast", DefaultPsk, 16, nullptr, 0, "shrt", 4); invalid(4, 1, 7, "LongFast", DefaultPsk, 16, "long", 4, nullptr, 4); invalid(4, 1, 7, "LongFast", DefaultPsk, 16, "long", 4, "", 0); for (size_t size : {MaxLongNameSize + 1, size_t(-1)}) invalid(4, 1, 7, "LongFast", DefaultPsk, 16, long_name, size, "shrt", 4); for (size_t size : {MaxShortNameSize + 1, size_t(-1)}) invalid(4, 1, 7, "LongFast", DefaultPsk, 16, "long", 4, short_name, size); const char* bad_names[] = {"\0", "\xc0\xaf", "\xe0\x80\x80", "\xed\xa0\x80", "\xf0\x80\x80\x80", "\xf4\x90\x80\x80", "\x80", "\xc2", "\xe2\x82"}; const size_t bad_lengths[] = {1, 2, 3, 3, 4, 4, 1, 1, 2}; for (size_t i = 0; i < sizeof(bad_lengths) / sizeof(bad_lengths[0]); ++i) { invalid(4, 1, 7, "LongFast", DefaultPsk, 16, bad_names[i], bad_lengths[i], "shrt", 4); invalid(4, 1, 7, "LongFast", DefaultPsk, 16, "long", 4, bad_names[i], bad_lengths[i]); } const char embedded_nul[] = {'a', 0, 'b'}; invalid(4, 1, 7, "LongFast", DefaultPsk, 16, embedded_nul, 3, "shrt", 4); invalid(4, 1, 7, "LongFast", DefaultPsk, 16, "long", 4, embedded_nul, 3); assert(!encodeNodeInfo(4, 1, 7, "LongFast", DefaultPsk, 16, "long", 4, "shrt", 4, nullptr)); // Exact-length byte arrays need no trailing NUL; the codec never scans them. assert(encodeNodeInfo(4, 1, 7, "LongFast", DefaultPsk, 16, long_name, MaxLongNameSize, short_name, MaxShortNameSize, frame)); assert(encodeNodeInfo(4, 1, 0, "LongFast", DefaultPsk, 16, "", 0, "a", 1, frame)); // A single non-ASCII scalar can occupy the complete four-byte stock field. assert(encodeNodeInfo(4, 1, 3, "LongFast", DefaultPsk, 16, "\xc3\xa9", 2, "\xf0\x9f\x9a\x80", 4, frame)); puts("PASS nodeinfo_bounds"); } static void argument_bounds() { uint8_t frame[MaxFrameSize] = {}; char text[MaxTextSize + 2]; memset(text, 'x', sizeof(text)); assert(encodeText(1, 1, 7, "LongFast", DefaultPsk, 16, text, MaxTextSize, frame)); for (size_t length : {size_t(0), MaxTextSize + 1, size_t(0xffffffffU)}) assert(!encodeText(1, 1, 3, "LongFast", DefaultPsk, 16, text, length, frame)); assert(!encodeText(0, 1, 3, "LongFast", DefaultPsk, 16, text, 1, frame)); assert(!encodeText(0xffffffffU, 1, 3, "LongFast", DefaultPsk, 16, text, 1, frame)); assert(!encodeText(1, 0, 3, "LongFast", DefaultPsk, 16, text, 1, frame)); assert(!encodeText(1, 1, 8, "LongFast", DefaultPsk, 16, text, 1, frame)); assert(encodeText(1, 1, 3, "12345678901", DefaultPsk, 16, text, 1, frame)); for (const auto* name : {static_cast(nullptr), "", "123456789012", "01234567890123456789012345678901xx"}) assert(!encodeText(1, 1, 3, name, DefaultPsk, 16, text, 1, frame)); for (size_t key_length : {size_t(0), size_t(1), size_t(15), size_t(17), size_t(31), size_t(33)}) assert(!encodeText(1, 1, 3, "LongFast", DefaultPsk, key_length, text, 1, frame)); assert(!encodeText(1, 1, 3, "LongFast", nullptr, 16, text, 1, frame)); assert(!encodeText(1, 1, 3, "LongFast", DefaultPsk, 16, nullptr, 1, frame)); assert(!encodeText(1, 1, 3, "LongFast", DefaultPsk, 16, text, 1, nullptr)); const char* invalid_utf8[] = {"\0", "\xc0\xaf", "\xe0\x80\x80", "\xed\xa0\x80", "\xf0\x80\x80\x80", "\xf4\x90\x80\x80", "\x80", "\xc2", "\xe2\x82"}; const size_t lengths[] = {1, 2, 3, 3, 4, 4, 1, 1, 2}; for (size_t i = 0; i < sizeof(lengths) / sizeof(lengths[0]); ++i) assert(!encodeText(1, 1, 3, "LongFast", DefaultPsk, 16, invalid_utf8[i], lengths[i], frame)); const char nul_text[] = {'a', 0, 'b'}; assert(!encodeText(1, 1, 3, "LongFast", DefaultPsk, 16, nul_text, 3, frame)); DecodedText decoded; assert(!decodeText(nullptr, 50, "LongFast", DefaultPsk, 16, 0, decoded)); assert(!decodeText(frame, 0, "LongFast", DefaultPsk, 16, 0, decoded)); assert(!decodeText(frame, 256, "LongFast", DefaultPsk, 16, 0, decoded)); assert(!decodeText(frame, 50, "LongFast", nullptr, 16, 0, decoded)); assert(!decodeText(frame, 50, nullptr, DefaultPsk, 16, 0, decoded)); puts("PASS bounds"); } static void extensions() { const uint8_t frame[] = @EXTENSION_FRAME@; DecodedText decoded; assert(decodeText(frame, sizeof(frame), "LongFast", DefaultPsk, 16, 0x99887766, decoded)); assert(decoded.text_len == 1 && !strcmp(decoded.text, "x")); // Header ACK/request flags do not make this passive decoder transmit. const uint8_t requests[] = @REQUEST_FRAME@; assert(decodeText(requests, sizeof(requests), "LongFast", DefaultPsk, 16, 0x99887766, decoded)); puts("PASS extensions"); } static void stock_zero_bitfield_frames() { for (const auto& test : stock_zero_bitfield) { DecodedText decoded; assert(decodeText(test.frame, test.frame_len, test.name, test.key, test.key_len, 0x99887766, decoded)); assert(decoded.from == 0x11223344 && decoded.id == 0x89abcdef); assert(decoded.hop_limit == 7 && decoded.hop_start == 7 && decoded.hops == 0); assert((test.frame[12] & 0x18) == 0); // Stock SDK requested neither ACK nor MQTT. assert(decoded.text_len == test.text_len && !memcmp(decoded.text, test.text, test.text_len)); assert(decoded.text[decoded.text_len] == 0); } puts("PASS stock_zero_bitfield"); } static void dedup() { SeenPackets seen; assert(!seen.seen(1, 1)); assert(seen.remember(1, 1)); assert(seen.seen(1, 1)); assert(!seen.remember(1, 1)); assert(seen.remember(2, 1)); // same ID from another sender is distinct assert(!seen.remember(0, 1) && !seen.remember(1, 0) && !seen.remember(0xffffffffU, 1)); seen.clear(); assert(!seen.seen(1, 1)); for (uint32_t i = 1; i <= SeenPackets::Capacity; ++i) assert(seen.remember(1, i)); assert(seen.seen(1, 1)); assert(seen.remember(1, 33)); assert(!seen.seen(1, 1)); // Invalid data never poisons dedup; the subsequent valid frame is accepted. seen.clear(); DecodedText decoded; for (const auto& test : malformed) if (decodeText(test.frame, test.length, "LongFast", DefaultPsk, 16, 0x99887766, decoded)) seen.remember(decoded.from, decoded.id); assert(!seen.seen(0x11223344, 0x89abcdef)); assert(decodeText(golden[0].frame, golden[0].frame_len, "LongFast", DefaultPsk, 16, 0x99887766, decoded)); assert(seen.remember(decoded.from, decoded.id)); assert(!seen.remember(decoded.from, decoded.id)); // Successful own TX uses the same pair cache to suppress over-air echoes. seen.clear(); assert(seen.remember(0x11223344, 0x89abcdef)); assert(seen.seen(0x11223344, 0x89abcdef)); puts("PASS dedup"); } int main(int argc, char** argv) { assert(argc == 2); if (!strcmp(argv[1], "presets")) presets(); else if (!strcmp(argv[1], "frames")) frames(); else if (!strcmp(argv[1], "malformed")) malformed_frames(); else if (!strcmp(argv[1], "nodeinfo")) node_info_frames(); else if (!strcmp(argv[1], "nodeinfo_bounds")) node_info_bounds(); else if (!strcmp(argv[1], "bounds")) argument_bounds(); else if (!strcmp(argv[1], "extensions")) extensions(); else if (!strcmp(argv[1], "stock_zero_bitfield")) stock_zero_bitfield_frames(); else if (!strcmp(argv[1], "dedup")) dedup(); else assert(false); } ''' def vectors(): inputs = [(DEFAULT_KEY, "LongFast", b"hello"), (bytes(range(32)), "LongTurbo", "caf\u00e9 \U0001f6f0\ufe0f".encode()), (bytes(range(16)), "LongMod", bytes(range(33, 127))), (DEFAULT_KEY, "LongFast", b"a" * 127), (DEFAULT_KEY, "LongFast", b"b" * 128), (bytes(range(32)), "LongTurbo", b"c" * 228), (DEFAULT_KEY, "LongFast", "\u0080\u07ff\u0800\ud7ff\ue000\uffff\U00010000\U0010ffff".encode())] golden = [] for key, name, text in inputs: frame = reference_frame(data_text(text), name, key) golden.append("{" + array(key) + "," + str(len(key)) + ',"' + name + '",' + literal(text) + "," + str(len(text)) + "," + array(frame) + "," + str(len(frame)) + "}") stock_zero_bitfield = [] for key, name in [(bytes(range(16)), "ShortTurbo"), (bytes(range(32)), "LongFast")]: text = "stock field9 zero caf\u00e9".encode() # Independently encrypted exact stock Data shape, not our encodeText: # TEXT_MESSAGE_APP + UTF-8 payload + OPTIONAL uint32 bitfield=0. data = data_text(text) + b"\x48\x00" frame = reference_frame(data, name, key, flags=0xE7) stock_zero_bitfield.append("{" + array(key) + "," + str(len(key)) + ',"' + name + '",' + literal(text) + "," + str(len(text)) + "," + array(frame) + "," + str(len(frame)) + "}") node_inputs = [(DEFAULT_KEY, "LongFast", b"MeshCore", b"CORE", FROM, 7), (bytes(range(32)), "LongTurbo", ("\u00e9" * 12).encode(), "\U0001f680".encode(), 0xABCDEF12, 0), (bytes(range(16)), "LongMod", b"L" * 24, "\u00e9\u00e9".encode(), 4, 3), (bytes(range(32)), "private", b"", b"a", 0xFFFFFFFE, 1)] node_inputs += [(DEFAULT_KEY, "LongFast", "\u6d4b\u8bd5".encode(), "\u6d4bA".encode(), FROM, hops) for hops in (2, 4, 5, 6)] node_info = [] for offset, (key, name, long_name, short_name, sender, hops) in enumerate(node_inputs): packet_id = PACKET_ID + offset frame = reference_frame(data_nodeinfo(long_name, short_name, sender), name, key, sender=sender, packet_id=packet_id, flags=hops | (hops << 5)) node_info.append("{" + array(key) + "," + str(len(key)) + ',"' + name + '",' + literal(long_name) + "," + str(len(long_name)) + "," + literal(short_name) + "," + str(len(short_name)) + "," + str(sender) + "U," + str(packet_id) + "U," + str(hops) + "," + array(frame) + "," + str(len(frame)) + "}") bad_data = { "empty Data": b"", "missing port": b"\x12\x01x", "missing text": b"\x08\x01", "empty text": b"\x08\x01\x12\x00", "other app": b"\x08\x02\x12\x01x", "wrong port wire": b"\x0a\x01\x01\x12\x01x", "wrong payload wire": b"\x08\x01\x10\x01", "truncated length": b"\x08\x01\x12\x80", "oversized length": b"\x08\x01\x12\xff\x01x", "field zero": b"\x00" + data_text(b"x"), "unknown group wire": data_text(b"x") + b"\x53", "overflow varint": b"\xff" * 9 + b"\x02", "noncanonical tag": b"\x88\x00\x01\x12\x01x", "duplicate port": data_text(b"x") + b"\x08\x01", "duplicate text": data_text(b"x") + b"\x12\x01y", "UTF8 surrogate": data_text(b"\xed\xa0\x80"), "embedded NUL": data_text(b"x\x00y"), "proxied DM dest": data_text(b"x") + b"\x25" + struct.pack("