Skip to content
16 changes: 10 additions & 6 deletions Modules/mesh-meshcore-module/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@ Supported:

- Identity: an Ed25519 key pair created on first start and stored in the backend's data directory.
- Adverts: sent at start and every 12 hours, received adverts are verified and remembered as contacts.
- Channel messages (`GRP_TXT`): the "Public" channel is preconfigured, more channels can be added with
`mesh_set_channel()` (16-byte keys).
- Channel messages (`GRP_TXT`): the "Public" channel is preconfigured on a new node. It can be removed, and added again with
`mesh_set_channel()` and the name "Public" without a key. More channels can be added with `mesh_set_channel()`
(16-byte keys, or hashtag names without a key).
- Direct messages (`TXT_MSG`) to and from known contacts, sent as flood packets. Received messages are
acknowledged, and acknowledgements of sent messages mark them as delivered.

This node is a chat node: it does not repeat packets for others.

Radio presets: "Default" is EU_868 (869.618 MHz, 62.5 kHz, SF8, CR 4/8) US_915 (910.525 MHz, 62.5 kHz, SF7, CR 4/5) or
AU_915 (915.8 MHz, 250 kHz, SF10, CR 4/5).
"EU NL" is the Dutch community setting for EU_868: 869.618 MHz, 62.5 kHz, SF7, CR 4/5.
Other regions report `ERROR_NOT_SUPPORTED`.
Radio presets: "Default" is EU_868 (869.618 MHz, 62.5 kHz, SF8, CR 4/8), US_915 (910.525 MHz, 62.5 kHz, SF7, CR 4/5) or
AU_915 (915.8 MHz, 250 kHz, SF10, CR 4/5), and reports `ERROR_NOT_SUPPORTED` in other regions.
The other presets are the settings that the MeshCore community suggests per country, e.g. "Netherlands" and "Australia (Narrow)".
They set the frequency themselves. When the current region doesn't allow it, `mesh_get_preset_region()` returns the region of the country, which the app selects before the preset.
Presets also set the network parameters: the path hash size (bytes per hop, 1 to 3) and the default region scope. "Netherlands" and
"Netherlands (Limburg)" use 2 bytes and the scope "nl". Flooded packets with a scope are sent as transport flood packets, with the code that
MeshCore computes from the scope's key (SHA-256 of "#" and the name). Both can be changed with `mesh_set_network_settings()`.
A radio with `RADIO_MODULATION_ESPNOW` (no LoRa) uses MeshCore's ESP-NOW transport in every region, without presets:
unencrypted broadcast frames on Wi-Fi channel 1 at 20 dBm. It reaches other Tactility and MeshCore ESP-NOW nodes in
Wi-Fi range, not LoRa nodes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ constexpr size_t MESHCORE_CIPHER_KEY_SIZE = 16;
constexpr size_t MESHCORE_CIPHER_MAC_SIZE = 2;
constexpr size_t MESHCORE_CIPHER_BLOCK_SIZE = 16;

/**
* HMAC-SHA256 with a key of any length
* @return false when the MAC couldn't be computed, in which case @a mac is all zeros
*/
bool meshcore_hmac_sha256(const uint8_t* key, size_t key_length, const uint8_t* data, size_t length, uint8_t mac[32]);

void meshcore_sha256(uint8_t* hash, size_t hash_size, const uint8_t* data, size_t length);

/** SHA-256 over two concatenated inputs, truncated to @a hash_size */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ void meshcore_channel_from_key(const uint8_t key[16], MeshCoreChannel* channel);
/** Key of a hashtag channel (e.g. "#test"): the first 16 bytes of the SHA-256 of its name */
void meshcore_hashtag_channel_key(const char* name, uint8_t key[16]);

/** Key of a region scope: the key of the hashtag channel with the same name, which gets a "#" in front when it has none */
void meshcore_scope_key(const char* name, uint8_t key[16]);

/**
* The code that identifies a packet as part of a region scope: the first 2 bytes of an HMAC-SHA256 over the payload type
* and the payload, keyed with the scope's key. 0x0000 and 0xFFFF are reserved, so they are moved to the nearest other code.
* @return false when the HMAC couldn't be computed, in which case @a code is not set
*/
bool meshcore_transport_code(const uint8_t key[16], const MeshCorePacket& packet, uint16_t* code);

/**
* Build the URI that MeshCore apps scan as a QR code to add a contact:
* meshcore://contact/add?name=<name>&public_key=<hex>&type=<type>
Expand Down
317 changes: 256 additions & 61 deletions Modules/mesh-meshcore-module/source/meshcore_backend.cpp

Large diffs are not rendered by default.

21 changes: 14 additions & 7 deletions Modules/mesh-meshcore-module/source/meshcore_crypto.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,8 @@ psa_key_id_t importKey(psa_key_type_t type, psa_algorithm_t algorithm, psa_key_u
}

void hmac(const uint8_t secret[MESHCORE_SECRET_SIZE], const uint8_t* data, size_t length, uint8_t mac[32]) {
const auto algorithm = PSA_ALG_HMAC(PSA_ALG_SHA_256);
const psa_key_id_t key = importKey(PSA_KEY_TYPE_HMAC, algorithm, PSA_KEY_USAGE_SIGN_MESSAGE, secret, MESHCORE_SECRET_SIZE);
size_t macLength = 0;
if (key == PSA_KEY_ID_NULL || psa_mac_compute(key, algorithm, data, length, mac, 32, &macLength) != PSA_SUCCESS) {
memset(mac, 0, 32);
}
psa_destroy_key(key);
// On failure the MAC is all zeros, which doesn't match any valid MAC
meshcore_hmac_sha256(secret, MESHCORE_SECRET_SIZE, data, length, mac);
}

/** AES-128-ECB over whole blocks */
Expand All @@ -58,6 +53,18 @@ bool aesEcb(const uint8_t secret[MESHCORE_SECRET_SIZE], bool encrypt, const uint

} // namespace

bool meshcore_hmac_sha256(const uint8_t* secret, size_t secret_length, const uint8_t* data, size_t length, uint8_t mac[32]) {
const auto algorithm = PSA_ALG_HMAC(PSA_ALG_SHA_256);
const psa_key_id_t key = importKey(PSA_KEY_TYPE_HMAC, algorithm, PSA_KEY_USAGE_SIGN_MESSAGE, secret, secret_length);
size_t macLength = 0;
const bool ok = key != PSA_KEY_ID_NULL && psa_mac_compute(key, algorithm, data, length, mac, 32, &macLength) == PSA_SUCCESS;
if (!ok) {
memset(mac, 0, 32);
}
psa_destroy_key(key);
return ok;
}

void meshcore_sha256(uint8_t* hash, size_t hash_size, const uint8_t* data, size_t length) {
meshcore_sha256_pair(hash, hash_size, data, length, nullptr, 0);
}
Expand Down
25 changes: 25 additions & 0 deletions Modules/mesh-meshcore-module/source/meshcore_protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#include <ed25519.h>

#include <mesh/mesh.h>

#include <cstdio>
#include <cstring>

Expand Down Expand Up @@ -60,6 +62,29 @@ void meshcore_hashtag_channel_key(const char* name, uint8_t key[16]) {
meshcore_sha256(key, 16, reinterpret_cast<const uint8_t*>(name), strlen(name));
}

void meshcore_scope_key(const char* name, uint8_t key[16]) {
char hashtag[MESH_NAME_MAX_LENGTH + 2];
snprintf(hashtag, sizeof(hashtag), "%s%s", name[0] == '#' ? "" : "#", name);
meshcore_hashtag_channel_key(hashtag, key);
}

bool meshcore_transport_code(const uint8_t key[16], const MeshCorePacket& packet, uint16_t* code) {
uint8_t message[1 + MESHCORE_MAX_PAYLOAD_SIZE];
message[0] = static_cast<uint8_t>(packet.payloadType);
memcpy(message + 1, packet.payload, packet.payloadLength);
uint8_t mac[32];
if (!meshcore_hmac_sha256(key, 16, message, 1 + packet.payloadLength, mac)) {
return false;
}
const uint16_t value = static_cast<uint16_t>(mac[0] | (mac[1] << 8));
if (value == 0x0000) {
*code = 0x0001;
} else {
*code = (value == 0xFFFF) ? 0xFFFE : value;
}
return true;
}

void meshcore_shared_secret(const MeshCoreIdentity& identity, const uint8_t peerPublicKey[MESHCORE_PUBLIC_KEY_SIZE], uint8_t secret[MESHCORE_SECRET_SIZE]) {
ed25519_key_exchange(secret, peerPublicKey, identity.privateKey);
}
Expand Down
98 changes: 98 additions & 0 deletions Modules/mesh-meshcore-module/tests/source/protocol_test.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "doctest.h"

#include <mesh_meshcore/meshcore_backend.h>
#include <mesh_meshcore/meshcore_packet.h>
#include <mesh_meshcore/meshcore_protocol.h>

Expand Down Expand Up @@ -159,3 +160,100 @@ TEST_CASE("MeshCore contact URIs match the format of the MeshCore app") {
// Too small for the URI
CHECK_FALSE(meshcore_build_contact_uri("Example Contact", key, 1, uri, 60));
}

TEST_CASE("a MeshCore preset needs the region that allows its frequency") {
const auto presetIndex = [](const char* name) {
for (size_t i = 0; meshcore_backend_api.presets[i] != nullptr; i++) {
if (strcmp(meshcore_backend_api.presets[i], name) == 0) {
return i;
}
}
FAIL("unknown preset");
return size_t(0);
};
RadioRegion region = RADIO_REGION_UNSET;

CHECK_EQ(meshcore_backend_api.get_preset_region(presetIndex("Netherlands"), RADIO_REGION_US_915, &region), ERROR_NONE);
CHECK_EQ(region, RADIO_REGION_EU_868);
CHECK_EQ(meshcore_backend_api.get_preset_region(presetIndex("Australia"), RADIO_REGION_EU_868, &region), ERROR_NONE);
CHECK_EQ(region, RADIO_REGION_AU_915);
CHECK_EQ(meshcore_backend_api.get_preset_region(presetIndex("USA"), RADIO_REGION_US_915, &region), ERROR_NONE);
CHECK_EQ(region, RADIO_REGION_US_915);
CHECK_EQ(meshcore_backend_api.get_preset_region(presetIndex("EU 433MHz (Narrow)"), RADIO_REGION_EU_868, &region), ERROR_NONE);
CHECK_EQ(region, RADIO_REGION_EU_433);
CHECK_EQ(meshcore_backend_api.get_preset_region(presetIndex("Default"), RADIO_REGION_JP_920, &region), ERROR_NONE);
CHECK_EQ(region, RADIO_REGION_JP_920);
}

TEST_CASE("a region scope limits a packet with a code that other MeshCore nodes compute the same way") {
// The key and codes were computed with an independent HMAC-SHA256
uint8_t key[16];
meshcore_scope_key("nl", key);
const uint8_t expected[16] = { 0xa8, 0x28, 0x9f, 0x73, 0x66, 0x5c, 0x28, 0x05, 0x7d, 0x4f, 0x38, 0x15, 0xa6, 0x00, 0x29, 0x21 };
CHECK_EQ(memcmp(key, expected, sizeof(key)), 0);
uint8_t hashtagKey[16];
meshcore_scope_key("#nl", hashtagKey);
CHECK_EQ(memcmp(key, hashtagKey, sizeof(key)), 0);

MeshCorePacket packet;
packet.payloadType = MeshCorePayload::TextMessage;
packet.payloadLength = 5;
memcpy(packet.payload, "hello", 5);
uint16_t code = 0;
REQUIRE(meshcore_transport_code(key, packet, &code));
CHECK_EQ(code, 0xEEC8);

packet.payloadType = MeshCorePayload::Advert;
packet.payloadLength = 0;
REQUIRE(meshcore_transport_code(key, packet, &code));
CHECK_EQ(code, 0xF329);
}

TEST_CASE("a MeshCore preset only gives a radio profile in a region that allows its frequency") {
size_t netherlands = 0;
while (strcmp(meshcore_backend_api.presets[netherlands], "Netherlands") != 0) {
netherlands++;
}
MeshRadioProfile profile {};
CHECK_EQ(meshcore_backend_api.get_radio_profile(nullptr, RADIO_REGION_US_915, RADIO_MODULATION_LORA, netherlands, &profile), ERROR_NOT_SUPPORTED);
CHECK_EQ(meshcore_backend_api.get_radio_profile(nullptr, RADIO_REGION_EU_868, RADIO_MODULATION_LORA, netherlands, &profile), ERROR_NONE);
CHECK_EQ(profile.frequency, 869618000);
}

TEST_CASE("HMAC-SHA256 matches the RFC 4231 test vector") {
const uint8_t key[] = { 'J', 'e', 'f', 'e' };
const char* message = "what do ya want for nothing?";
const uint8_t expected[32] = {
0x5b, 0xdc, 0xc1, 0x46, 0xbf, 0x60, 0x75, 0x4e, 0x6a, 0x04, 0x24, 0x26, 0x08, 0x95, 0x75, 0xc7,
0x5a, 0x00, 0x3f, 0x08, 0x9d, 0x27, 0x39, 0x83, 0x9d, 0xec, 0x58, 0xb9, 0x64, 0xec, 0x38, 0x43,
};
uint8_t mac[32];
REQUIRE(meshcore_hmac_sha256(key, sizeof(key), reinterpret_cast<const uint8_t*>(message), strlen(message), mac));
CHECK_EQ(memcmp(mac, expected, sizeof(mac)), 0);
}

TEST_CASE("every MeshCore preset can be used in the region that it selects") {
for (size_t preset = 0; meshcore_backend_api.presets[preset] != nullptr; preset++) {
CAPTURE(std::string(meshcore_backend_api.presets[preset]));
// Whatever region the radio has, a preset ends up in a region that gives it a profile that lies in the region's bands
for (int current = RADIO_REGION_EU_433; current < RADIO_REGION_COUNT; current++) {
RadioRegion region = RADIO_REGION_UNSET;
if (meshcore_backend_api.get_preset_region(preset, static_cast<RadioRegion>(current), &region) != ERROR_NONE) {
FAIL("no region allows the preset");
}
MeshRadioProfile profile {};
if (meshcore_backend_api.get_radio_profile(nullptr, region, RADIO_MODULATION_LORA, preset, &profile) != ERROR_NONE) {
// The default preset only exists in some regions
CHECK_EQ(preset, 0);
continue;
}
const RadioRegionBand* bands = nullptr;
const size_t count = radio_get_region_bands(region, &bands);
bool fits = false;
for (size_t i = 0; i < count; i++) {
fits |= profile.frequency - profile.bandwidth / 2 >= bands[i].frequency_min && profile.frequency + profile.bandwidth / 2 <= bands[i].frequency_max;
}
CHECK(fits);
}
}
}
4 changes: 2 additions & 2 deletions Modules/mesh-meshtastic-module/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

Supported:

- The LongFast preset (SF11, 250 kHz, CR 4/5, sync word 0x2B), in the frequency slot that the primary channel's
- The LongFast, LongModerate, LongSlow, MediumFast, MediumSlow, ShortFast, ShortSlow and ShortTurbo modem presets (sync word 0x2B), in the frequency slot that the primary channel's
name selects within the region's band, like Meshtastic does.
- Channels: the primary channel is preconfigured as "LongFast" with the default key. More channels can be added with
`mesh_set_channel()`, with a 1-byte PSK index, or a 16 or 32-byte key.
Expand All @@ -16,5 +16,5 @@ Supported:
The node number is random and stored in the backend's data directory. The node doesn't rebroadcast packets for
others (like the CLIENT_MUTE role).

Not implemented: public key (PKI) encryption of direct messages, other modem presets, positions and telemetry.
Not implemented: public key (PKI) encryption of direct messages, positions and telemetry.
Direct messages are encrypted with the primary channel's key, which current firmware still accepts.
Loading
Loading