feat(firmware): MeshCore message RX — decrypt DMs addressed to us
- vendor orlp/ed25519 (lib/ed25519, MIT — the exact lib MeshCore uses) for Curve25519 ECDH key exchange; switch identity/advert signing to it (same seed -> same pubkey, so peers still recognise us; sigs are byte-identical) - store pubkeys from heard MeshCore adverts (contacts table) so a DM's 1-byte src_hash can be resolved to a full sender key - decode PAYLOAD_TYPE_TXT_MSG: ECDH shared secret, HMAC-SHA256 MAC check, AES-128-ECB decrypt, parse timestamp+text, drive the message modal - verified end-to-end: real MeshCore node -> "hello from HP Pro Desk" decoded Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8e5b4c97d7
commit
38c146c477
+122
-8
@@ -26,8 +26,12 @@
|
||||
#include "mbedtls/aes.h"
|
||||
#include "esp_mac.h"
|
||||
#include "esp_random.h"
|
||||
#include <Ed25519.h> // rweather/Crypto — MeshCore identity + advert signing
|
||||
#include <Preferences.h> // NVS-backed persistent Ed25519 key
|
||||
#include <AES.h> // rweather/Crypto — AES-128-ECB for MeshCore DM cipher
|
||||
#include <SHA256.h> // rweather/Crypto — HMAC-SHA256 for MeshCore packet MAC
|
||||
#include <Preferences.h> // NVS-backed persistent identity seed
|
||||
extern "C" {
|
||||
#include "ed_25519.h" // orlp/ed25519 (vendored, lib/ed25519) — identity, sign, ECDH
|
||||
}
|
||||
|
||||
// --- Heltec V3 SX1262 pin map (from the Heltec V3 schematic) ----------------
|
||||
static const int PIN_LORA_NSS = 8;
|
||||
@@ -321,7 +325,8 @@ static void sendMeshtasticText(const char* text) {
|
||||
// signed message = pub_key || timestamp || app_data (verified in MeshCore
|
||||
// Mesh.cpp onRecvPacket PAYLOAD_TYPE_ADVERT). app_data = flags(0x81 = chat
|
||||
// node + has-name) followed by the UTF-8 name.
|
||||
static uint8_t mcPrv[32]; // Ed25519 private key (seed)
|
||||
static uint8_t mcSeed[32]; // persistent 32-byte identity seed (NVS)
|
||||
static uint8_t mcPrv[64]; // orlp expanded private key, derived from seed
|
||||
static uint8_t mcPub[32]; // Ed25519 public key = MeshCore identity
|
||||
static uint32_t lastMcAdvertMs = 0;
|
||||
static const uint32_t MC_ADVERT_INTERVAL_MS = 30000;
|
||||
@@ -333,13 +338,15 @@ static uint32_t mcNow() { return MC_EPOCH_BASE + millis() / 1000; }
|
||||
static void initMeshCoreIdentity() {
|
||||
Preferences prefs;
|
||||
prefs.begin("meshcore", false);
|
||||
if (prefs.getBytes("prv", mcPrv, 32) != 32) {
|
||||
Ed25519::generatePrivateKey(mcPrv); // uses the Crypto lib CSPRNG
|
||||
prefs.putBytes("prv", mcPrv, 32);
|
||||
// NVS key "prv" holds the 32-byte seed (unchanged from the first identity so
|
||||
// peers that already added us keep recognising the same public key).
|
||||
if (prefs.getBytes("prv", mcSeed, 32) != 32) {
|
||||
for (int i = 0; i < 32; i++) mcSeed[i] = (uint8_t)esp_random(); // HW CSPRNG
|
||||
prefs.putBytes("prv", mcSeed, 32);
|
||||
Serial.println("MeshCore: generated new Ed25519 identity");
|
||||
}
|
||||
prefs.end();
|
||||
Ed25519::derivePublicKey(mcPub, mcPrv);
|
||||
ed25519_create_keypair(mcPub, mcPrv, mcSeed); // seed -> (pub, expanded prv)
|
||||
Serial.print("MeshCore identity pub: ");
|
||||
for (int i = 0; i < 32; i++) Serial.printf("%02x", mcPub[i]);
|
||||
Serial.println();
|
||||
@@ -358,7 +365,7 @@ static void sendMeshCoreAdvert() {
|
||||
memcpy(message + ml, app_data, ad); ml += ad;
|
||||
|
||||
uint8_t sig[64];
|
||||
Ed25519::sign(sig, mcPrv, mcPub, message, ml);
|
||||
ed25519_sign(sig, message, ml, mcPub, mcPrv);
|
||||
|
||||
uint8_t pkt[2 + 32 + 4 + 64 + 40]; size_t p = 0;
|
||||
pkt[p++] = 0x12; // route=flood(2) | type=advert(4)
|
||||
@@ -377,6 +384,108 @@ static void sendMeshCoreAdvert() {
|
||||
radio.startReceive();
|
||||
}
|
||||
|
||||
// --- MeshCore contacts (heard adverts) + DM receive ------------------------
|
||||
// To decrypt a direct message we need the SENDER's full public key. MeshCore
|
||||
// packets only carry a 1-byte src_hash (pubkey prefix), so we remember pubkeys
|
||||
// from the adverts we hear and match on that hash.
|
||||
struct McContact { uint8_t pub[32]; char name[24]; bool used; };
|
||||
static const int MC_MAX_CONTACTS = 8;
|
||||
static McContact mcContacts[MC_MAX_CONTACTS];
|
||||
static int mcContactRR = 0; // round-robin eviction cursor
|
||||
|
||||
static void mcStoreContact(const uint8_t* pub, const char* name) {
|
||||
for (int i = 0; i < MC_MAX_CONTACTS; i++) // update existing
|
||||
if (mcContacts[i].used && memcmp(mcContacts[i].pub, pub, 32) == 0) {
|
||||
strncpy(mcContacts[i].name, name, sizeof(mcContacts[i].name) - 1);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < MC_MAX_CONTACTS; i++) // fill a free slot
|
||||
if (!mcContacts[i].used) {
|
||||
mcContacts[i].used = true; memcpy(mcContacts[i].pub, pub, 32);
|
||||
strncpy(mcContacts[i].name, name, sizeof(mcContacts[i].name) - 1);
|
||||
return;
|
||||
}
|
||||
int i = mcContactRR; mcContactRR = (mcContactRR + 1) % MC_MAX_CONTACTS; // evict
|
||||
memcpy(mcContacts[i].pub, pub, 32);
|
||||
strncpy(mcContacts[i].name, name, sizeof(mcContacts[i].name) - 1);
|
||||
mcContacts[i].name[sizeof(mcContacts[i].name) - 1] = 0;
|
||||
}
|
||||
|
||||
// MeshCore Utils::MACThenDecrypt — verify a 2-byte truncated HMAC-SHA256 over
|
||||
// the ciphertext (key = 32-byte shared secret), then AES-128-ECB decrypt (key =
|
||||
// first 16 bytes). Returns plaintext length (multiple of 16) or 0 on bad MAC.
|
||||
static int mcMACThenDecrypt(const uint8_t* secret, uint8_t* dest,
|
||||
const uint8_t* src, int src_len) {
|
||||
if (src_len <= 2 || (src_len - 2) % 16 != 0) return 0;
|
||||
uint8_t hmac[2];
|
||||
SHA256 sha;
|
||||
sha.resetHMAC(secret, 32);
|
||||
sha.update(src + 2, src_len - 2);
|
||||
sha.finalizeHMAC(secret, 32, hmac, 2);
|
||||
if (memcmp(hmac, src, 2) != 0) return 0; // MAC mismatch
|
||||
AES128 aes; aes.setKey(secret, 16);
|
||||
int n = src_len - 2; const uint8_t* sp = src + 2; uint8_t* dp = dest;
|
||||
for (int o = 0; o < n; o += 16) { aes.decryptBlock(dp, sp); dp += 16; sp += 16; }
|
||||
return n;
|
||||
}
|
||||
|
||||
// Parse a received MeshCore packet: store adverts, decrypt DMs addressed to us.
|
||||
static void handleMeshCorePacket(const uint8_t* buf, size_t len) {
|
||||
if (len < 2) return;
|
||||
uint8_t header = buf[0];
|
||||
uint8_t route = header & 0x03;
|
||||
uint8_t ptype = (header >> 2) & 0x0F;
|
||||
size_t i = 1;
|
||||
if (route == 0x00 || route == 0x03) i += 4; // transport codes
|
||||
if (i >= len) return;
|
||||
uint8_t plb = buf[i++];
|
||||
uint8_t hsize = (plb >> 6) + 1, hcount = plb & 0x3F;
|
||||
i += (size_t)hsize * hcount; // skip path hashes
|
||||
if (i >= len) return;
|
||||
const uint8_t* payload = buf + i;
|
||||
int payload_len = (int)len - (int)i;
|
||||
|
||||
if (ptype == 0x04) { // ADVERT
|
||||
if (payload_len < 32 + 4 + 64 + 1) return;
|
||||
char name[24] = {0};
|
||||
const uint8_t* app = payload + 32 + 4 + 64;
|
||||
int app_len = payload_len - (32 + 4 + 64);
|
||||
if (app_len > 1) { int nl = app_len - 1; if (nl > 23) nl = 23; memcpy(name, app + 1, nl); }
|
||||
mcStoreContact(payload, name);
|
||||
strncpy(lastMsg, name[0] ? name : "advert", sizeof(lastMsg) - 1);
|
||||
lastMsg[sizeof(lastMsg) - 1] = 0; lastMsgNet = 'C'; lastMsgMs = millis();
|
||||
Serial.printf(" decoded[C](advert): %s\n", name);
|
||||
return;
|
||||
}
|
||||
if (ptype == 0x02) { // TXT_MSG (direct message)
|
||||
if (payload_len < 2 + 2 + 16) return;
|
||||
uint8_t dest_hash = payload[0], src_hash = payload[1];
|
||||
if (dest_hash != mcPub[0]) return; // not addressed to us
|
||||
const uint8_t* macData = payload + 2;
|
||||
int macLen = payload_len - 2;
|
||||
for (int c = 0; c < MC_MAX_CONTACTS; c++) {
|
||||
if (!mcContacts[c].used || mcContacts[c].pub[0] != src_hash) continue;
|
||||
uint8_t secret[32];
|
||||
ed25519_key_exchange(secret, mcContacts[c].pub, mcPrv); // ECDH
|
||||
uint8_t plain[192];
|
||||
int n = mcMACThenDecrypt(secret, plain, macData, macLen);
|
||||
if (n > 5) {
|
||||
plain[n < (int)sizeof(plain) ? n : (int)sizeof(plain) - 1] = 0;
|
||||
uint8_t txt_type = plain[4] & 0x03;
|
||||
const char* text = (const char*)&plain[5];
|
||||
if (txt_type == 0) { // TXT_TYPE_PLAIN
|
||||
strncpy(lastMsg, text, sizeof(lastMsg) - 1); lastMsg[sizeof(lastMsg) - 1] = 0;
|
||||
lastMsgNet = 'C'; lastMsgMs = millis();
|
||||
Serial.printf(" decoded[C](dm from %s): %s\n", mcContacts[c].name, text);
|
||||
triggerMessageModal("meshCore", mcContacts[c].name, text);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
Serial.printf(" [C] DM for us but no matching contact (src_hash=%02x)\n", src_hash);
|
||||
}
|
||||
}
|
||||
|
||||
static void drainPacket() {
|
||||
size_t len = radio.getPacketLength();
|
||||
uint8_t buf[256];
|
||||
@@ -407,6 +516,11 @@ static void drainPacket() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MeshCore: store heard adverts, decrypt direct messages addressed to us.
|
||||
if (CONFIGS[activeIdx].letter == 'C') {
|
||||
handleMeshCorePacket(buf, len);
|
||||
}
|
||||
}
|
||||
|
||||
// --- The trippy acid display ------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user