From a2df37356f6fcce0dc66e3674112fa8da368f61f Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Mon, 3 Nov 2025 11:21:20 -0800 Subject: [PATCH 1/7] Support working with ed25519 seeds in addition to raw keypairs. Our ed25519 library uses a representation of its key pair that is largely incompatible with modern implementations, which mostly work with the original 32-byte seed; Peters' impentation represents the private key as the clamped sha512 of the seed. This change: - preserves the original seed when generating keys - adds CLI commands to obtain the seed via `get prv.seed`, under the same conditions as `get prv.key` is allowed - adds support for `set prv.key` to supply a seed, in which case the keypair will be re-generated from it. This is mostly to enable external key management using modern libraries, but could also be of use on devices where we don't have a trustworthy entropy source. I split Identity::writeTo(uint8_t*,size_t) into explicit forms for the thing being written; the original implementation wrote a different thing depending on the length, which would be ambiguous between pubkey and seed and cumbersome if it tried to return all three in one long buffer. Identity::readFrom() did not have that ambiguity problem because keys can't be set from pubkey alone, though it might be preferable to split readFrom() up as well and not use magic length values. --- src/Identity.cpp | 36 ++++++++++++++++++++++++++---------- src/Identity.h | 15 ++++++++++++++- src/helpers/CommonCLI.cpp | 18 +++++++++++++++++- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/Identity.cpp b/src/Identity.cpp index 8329892830..56cf7bfdbf 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -43,7 +43,6 @@ LocalIdentity::LocalIdentity(const char* prv_hex, const char* pub_hex) : Identit } LocalIdentity::LocalIdentity(RNG* rng) { - uint8_t seed[SEED_SIZE]; rng->random(seed, SEED_SIZE); ed25519_create_keypair(pub_key, prv_key, seed); } @@ -51,12 +50,17 @@ LocalIdentity::LocalIdentity(RNG* rng) { bool LocalIdentity::readFrom(Stream& s) { bool success = (s.readBytes(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE); success = success && (s.readBytes(prv_key, PRV_KEY_SIZE) == PRV_KEY_SIZE); + memset(seed, 0, SEED_SIZE); + if (success) { + s.readBytes(seed, SEED_SIZE); + } return success; } bool LocalIdentity::writeTo(Stream& s) const { bool success = (s.write(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE); success = success && (s.write(prv_key, PRV_KEY_SIZE) == PRV_KEY_SIZE); + success = success && (s.write(seed, SEED_SIZE) == SEED_SIZE); return success; } @@ -65,26 +69,38 @@ void LocalIdentity::printTo(Stream& s) const { s.print("prv_key: "); Utils::printHex(s, prv_key, PRV_KEY_SIZE); s.println(); } -size_t LocalIdentity::writeTo(uint8_t* dest, size_t max_len) { +size_t LocalIdentity::writePubkeyTo(uint8_t* dest, size_t max_len) { + if (max_len < PUB_KEY_SIZE) return 0; // not big enough + memcpy(dest, pub_key, PUB_KEY_SIZE); + return PUB_KEY_SIZE; +} + +size_t LocalIdentity::writePrvkeyTo(uint8_t* dest, size_t max_len) { if (max_len < PRV_KEY_SIZE) return 0; // not big enough + memcpy(dest, prv_key, PRV_KEY_SIZE); + return PRV_KEY_SIZE; +} - if (max_len < PRV_KEY_SIZE + PUB_KEY_SIZE) { // only room for prv_key - memcpy(dest, prv_key, PRV_KEY_SIZE); - return PRV_KEY_SIZE; - } - memcpy(dest, prv_key, PRV_KEY_SIZE); // otherwise can fit prv + pub keys - memcpy(&dest[PRV_KEY_SIZE], pub_key, PUB_KEY_SIZE); - return PRV_KEY_SIZE + PUB_KEY_SIZE; +size_t LocalIdentity::writeSeedTo(uint8_t* dest, size_t max_len) { + if (max_len < SEED_SIZE) return 0; // not big enough + memcpy(dest, seed, SEED_SIZE); + return SEED_SIZE; } void LocalIdentity::readFrom(const uint8_t* src, size_t len) { if (len == PRV_KEY_SIZE + PUB_KEY_SIZE) { // has prv + pub keys memcpy(prv_key, src, PRV_KEY_SIZE); memcpy(pub_key, &src[PRV_KEY_SIZE], PUB_KEY_SIZE); + memset(seed, 0, SEED_SIZE); } else if (len == PRV_KEY_SIZE) { memcpy(prv_key, src, PRV_KEY_SIZE); // now need to re-calculate the pub_key ed25519_derive_pub(pub_key, prv_key); + memset(seed, 0, SEED_SIZE); + } else if (len == SEED_SIZE) { + memcpy(seed, src, SEED_SIZE); + // re-generate the keypair from the given seed + ed25519_create_keypair(pub_key, prv_key, seed); } } @@ -96,4 +112,4 @@ void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_k ed25519_key_exchange(secret, other_pub_key, prv_key); } -} \ No newline at end of file +} diff --git a/src/Identity.h b/src/Identity.h index 42fb9d9aed..885055fbcf 100644 --- a/src/Identity.h +++ b/src/Identity.h @@ -46,6 +46,7 @@ class Identity { */ class LocalIdentity : public Identity { uint8_t prv_key[PRV_KEY_SIZE]; + uint8_t seed[SEED_SIZE]; public: LocalIdentity(); LocalIdentity(const char* prv_hex, const char* pub_hex); @@ -76,7 +77,19 @@ class LocalIdentity : public Identity { bool readFrom(Stream& s); bool writeTo(Stream& s) const; void printTo(Stream& s) const; - size_t writeTo(uint8_t* dest, size_t max_len); + size_t writePubkeyTo(uint8_t* dest, size_t max_len); + size_t writePrvkeyTo(uint8_t* dest, size_t max_len); + size_t writeSeedTo(uint8_t* dest, size_t max_len); + /** + * \brief Set the Ed25519 keypair. + * \param src IN - the source for the key(s) or seed + * \param len IN - length of the input; if equal to SEED_SIZE, src is + * assumed to be a new seed, from which new private and public keys + * will be generated. If equal to PRV_KEY_SIZE, the corresponding + * public key will be re-generated. If equal to PRV_KEY_SIZE+ + * PUB_KEY_SIZE, no key regen is needed. The seed can only later + * be obtained via the `get prv.seed` CLI if SEED_SIZE is used. + */ void readFrom(const uint8_t* src, size_t len); }; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 88327aa89e..aa00771600 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -268,9 +268,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %s", _prefs->guest_password); } else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only uint8_t prv_key[PRV_KEY_SIZE]; - int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE); + int len = _callbacks->getSelfId().writePrvkeyTo(prv_key, PRV_KEY_SIZE); mesh::Utils::toHex(tmp, prv_key, len); sprintf(reply, "> %s", tmp); + } else if (sender_timestamp == 0 && memcmp(config, "prv.seed", 8) == 0) { // from serial command line only + uint8_t seed[SEED_SIZE]; + int len = _callbacks->getSelfId().writeSeedTo(seed, SEED_SIZE); + mesh::Utils::toHex(tmp, seed, len); + sprintf(reply, "> %s", tmp); } else if (memcmp(config, "name", 4) == 0) { sprintf(reply, "> %s", _prefs->node_name); } else if (memcmp(config, "repeat", 6) == 0) { @@ -393,6 +398,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { strcpy(reply, "Error, invalid key"); } + } else if (sender_timestamp == 0 && memcmp(config, "prv.seed ", 9) == 0) { // from serial command line only + uint8_t seed[SEED_SIZE]; + bool success = mesh::Utils::fromHex(seed, SEED_SIZE, &config[9]); + if (success) { + mesh::LocalIdentity new_id; + new_id.readFrom(seed, SEED_SIZE); + _callbacks->saveIdentity(new_id); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, invalid seed"); + } } else if (memcmp(config, "name ", 5) == 0) { StrHelper::strncpy(_prefs->node_name, &config[5], sizeof(_prefs->node_name)); savePrefs(); From ecd0cfc1c133aad93e65257f002151591f6bcfd9 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 24 Apr 2026 15:57:34 +1200 Subject: [PATCH 2/7] update discord links --- README.md | 2 +- docs/faq.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ebad1f6f59..f8b9e5e083 100644 --- a/README.md +++ b/README.md @@ -117,4 +117,4 @@ There are a number of fairly major features in the pipeline, with no particular - Report bugs and request features on the [GitHub Issues](https://github.com/ripplebiz/MeshCore/issues) page. - Find additional guides and components on [my site](https://buymeacoffee.com/ripplebiz). -- Join [MeshCore Discord](https://discord.gg/BMwCtwHj5V) to chat with the developers and get help from the community. +- Join [MeshCore Discord](https://meshcore.gg) to chat with the developers and get help from the community. diff --git a/docs/faq.md b/docs/faq.md index 9fe1534a1a..3edc0a6953 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -194,7 +194,7 @@ Recently, as of October 2025, many regions have moved to the "narrow" setting, a After extensive testing, many regions have switched or about to switch over to BW62.5 and SF7, 8, or 9. Narrower bandwidth setting and lower SF setting allow MeshCore's radio signals to fit between interference in the ISM band, provide for a lower noise floor, better SNR, and faster transmissions. -If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the [#meshcore-app](https://discord.com/channels/1343693475589263471/1391681655911088241) channel on the [MeshCore Discord server ](https://discord.gg/cYtQNYCCRK) to let Liam Cottle know. +If you have consensus from your community in your region to update your region's preset recommendation, please post your update request on the [#meshcore-app](https://discord.com/channels/1343693475589263471/1391681655911088241) channel on the [MeshCore Discord server ](https://meshcore.gg) to let Liam Cottle know. @@ -526,7 +526,7 @@ The third character is the capital letter 'O', not zero `0` - Firmware repo: https://github.com/meshcore-dev/MeshCore ### 5.8. Q: How can I support MeshCore? -**A:** Provide your honest feedback on GitHub and on [MeshCore Discord server](https://discord.gg/BMwCtwHj5V). Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at . +**A:** Provide your honest feedback on GitHub and on [MeshCore Discord server](https://meshcore.gg). Spread the word of MeshCore to your friends and communities; help them get started with MeshCore. Support Scott's MeshCore development at . Support Liam Cottle's smartphone client development by unlocking the server administration wait gate with in-app purchase From 68360157ecee382e6221d593724e473dd47cede5 Mon Sep 17 00:00:00 2001 From: OhYou-0 Date: Fri, 24 Apr 2026 10:16:19 -0700 Subject: [PATCH 3/7] Add LilyGo T-ETH Elite board support --- variants/lilygo_teth_elite/TETHEliteBoard.h | 10 +++ variants/lilygo_teth_elite/platformio.ini | 99 +++++++++++++++++++++ variants/lilygo_teth_elite/target.cpp | 43 +++++++++ variants/lilygo_teth_elite/target.h | 20 +++++ 4 files changed, 172 insertions(+) create mode 100644 variants/lilygo_teth_elite/TETHEliteBoard.h create mode 100644 variants/lilygo_teth_elite/platformio.ini create mode 100644 variants/lilygo_teth_elite/target.cpp create mode 100644 variants/lilygo_teth_elite/target.h diff --git a/variants/lilygo_teth_elite/TETHEliteBoard.h b/variants/lilygo_teth_elite/TETHEliteBoard.h new file mode 100644 index 0000000000..15eb9533ef --- /dev/null +++ b/variants/lilygo_teth_elite/TETHEliteBoard.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class TETHEliteBoard : public ESP32Board { +public: + const char* getManufacturerName() const override { + return "LilyGO T-ETH Elite"; + } +}; diff --git a/variants/lilygo_teth_elite/platformio.ini b/variants/lilygo_teth_elite/platformio.ini new file mode 100644 index 0000000000..97728f8b4c --- /dev/null +++ b/variants/lilygo_teth_elite/platformio.ini @@ -0,0 +1,99 @@ +[LilyGo_TETH_Elite_sx1262] +extends = esp32_base +board = esp32s3box +board_build.partitions = default_16MB.csv +board_upload.flash_size = 16MB +build_flags = + ${esp32_base.build_flags} + -I variants/lilygo_teth_elite + -D BOARD_HAS_PSRAM + -D LILYGO_TETH_ELITE + -D LILYGO_T_ETH_ELITE_ESP32S3 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D P_LORA_DIO_1=8 + -D P_LORA_NSS=40 + -D P_LORA_RESET=46 + -D P_LORA_BUSY=16 + -D P_LORA_SCLK=10 + -D P_LORA_MISO=9 + -D P_LORA_MOSI=11 + -D P_LORA_TX_LED=38 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D USE_SX1262 + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=8 + -D SX126X_RX_BOOSTED_GAIN=1 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/lilygo_teth_elite> +lib_deps = + ${esp32_base.lib_deps} + +[env:LilyGo_TETH_Elite_sx1262_repeater] +extends = LilyGo_TETH_Elite_sx1262 +build_flags = + ${LilyGo_TETH_Elite_sx1262.build_flags} + -D ADVERT_NAME='"T-ETH Elite Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TETH_Elite_sx1262.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_TETH_Elite_sx1262.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_TETH_Elite_sx1262_room_server] +extends = LilyGo_TETH_Elite_sx1262 +build_flags = + ${LilyGo_TETH_Elite_sx1262.build_flags} + -D ADVERT_NAME='"T-ETH Elite Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TETH_Elite_sx1262.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_TETH_Elite_sx1262.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_TETH_Elite_sx1262_companion_radio_usb] +extends = LilyGo_TETH_Elite_sx1262 +build_flags = + ${LilyGo_TETH_Elite_sx1262.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TETH_Elite_sx1262.build_src_filter} + +<../examples/companion_radio/*.cpp> +lib_deps = + ${LilyGo_TETH_Elite_sx1262.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_TETH_Elite_sx1262_companion_radio_ble] +extends = LilyGo_TETH_Elite_sx1262 +build_flags = + ${LilyGo_TETH_Elite_sx1262.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TETH_Elite_sx1262.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = + ${LilyGo_TETH_Elite_sx1262.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/lilygo_teth_elite/target.cpp b/variants/lilygo_teth_elite/target.cpp new file mode 100644 index 0000000000..4dc377d620 --- /dev/null +++ b/variants/lilygo_teth_elite/target.cpp @@ -0,0 +1,43 @@ +#include +#include "target.h" + +TETHEliteBoard board; + +static SPIClass spi(HSPI); +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + + return radio.std_init(&spi); +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/lilygo_teth_elite/target.h b/variants/lilygo_teth_elite/target.h new file mode 100644 index 0000000000..a842186cf6 --- /dev/null +++ b/variants/lilygo_teth_elite/target.h @@ -0,0 +1,20 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include "TETHEliteBoard.h" + +extern TETHEliteBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); From 528bf3f61e8909c4b16601e9835afb2df8feb94a Mon Sep 17 00:00:00 2001 From: Liam Cottle Date: Sun, 26 Apr 2026 00:24:40 +1200 Subject: [PATCH 4/7] add FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..262a9ee4be --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: meshcore-dev From 34db93150a4be32dd5780b1f59da8a2104a0bbb7 Mon Sep 17 00:00:00 2001 From: uncle lit <43320854+LitBomb@users.noreply.github.com> Date: Sun, 26 Apr 2026 18:35:02 -0700 Subject: [PATCH 5/7] Removed links to outdated resources and links Removed links to outdated resources and links --- docs/faq.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 3edc0a6953..c5866fb5f9 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -111,7 +111,6 @@ Anyone is able to build anything they like on top of MeshCore without paying any - MeshCore Firmware on GitHub: [https://github.com/meshcore-dev/MeshCore](https://github.com/meshcore-dev/MeshCore) - MeshCore Companion Web App: [https://app.meshcore.nz](https://app.meshcore.nz) - MeshCore Map: [https://map.meshcore.io](https://map.meshcore.io) -- Andy Kirby's [MeshCore Intro Video](https://www.youtube.com/watch?v=t1qne8uJBAc) - Liam Cottle's [MeshCore Technical Presentation](https://www.youtube.com/watch?v=OwmkVkZQTf4) You need LoRa hardware devices to run MeshCore firmware as clients or server (repeater and room server). @@ -404,9 +403,6 @@ Another way to download map tiles is to use this Python script to get the tiles There is also a modified script that adds additional error handling and parallel downloads: -UK map tiles are available separately from Andy Kirby on his discord server: - - ### 4.8. Q: Where do the map tiles go? Once you have the tiles downloaded, copy the `\tiles` folder to the root of your T-Deck's SD card. @@ -563,10 +559,6 @@ pio run -e RAK_4631_Repeater ``` then you'll find `firmware.zip` in `.pio/build/RAK_4631_Repeater` -Andy also has a video on how to build using VS Code: -*How to build and flash Meshcore repeater firmware | Heltec V3* - *(Link referenced in the Discord post)* - ### 5.10. Q: Are there other MeshCore related open source projects? **A:** [Liam Cottle](https://liamcottle.net)'s MeshCore web client and MeshCore Javascript library are open source under MIT license. From b948369d71c6b83ff01f3207cd6437b1b2ba8003 Mon Sep 17 00:00:00 2001 From: Keith Tweed Date: Sun, 26 Apr 2026 19:51:33 -0600 Subject: [PATCH 6/7] Update script link in FAQ 4.7 --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 3edc0a6953..18f7ce3555 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -402,7 +402,7 @@ Another way to download map tiles is to use this Python script to get the tiles There is also a modified script that adds additional error handling and parallel downloads: - + UK map tiles are available separately from Andy Kirby on his discord server: From eb332843ef95c746372efa2f9686d9e2ddba42c2 Mon Sep 17 00:00:00 2001 From: Devin Carraway Date: Tue, 28 Apr 2026 22:17:06 +1000 Subject: [PATCH 7/7] Allow set prv.seed remotely --- src/helpers/CommonCLI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 5851ef12e9..56ce0999c6 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -535,7 +535,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep } else { strcpy(reply, "Error, bad key"); } - } else if (sender_timestamp == 0 && memcmp(config, "prv.seed ", 9) == 0) { // from serial command line only + } else if (memcmp(config, "prv.seed ", 9) == 0) { uint8_t seed[SEED_SIZE]; bool success = mesh::Utils::fromHex(seed, SEED_SIZE, &config[9]); if (success) {