Initial commit

This commit is contained in:
Scott Powell 2025-01-13 14:07:48 +11:00
commit 6c7efdd0f6
59 changed files with 8604 additions and 0 deletions

29
src/Destination.cpp Normal file
View file

@ -0,0 +1,29 @@
#include "Destination.h"
#include "Utils.h"
#include <string.h>
namespace mesh {
Destination::Destination(const Identity& identity, const char* name) {
uint8_t name_hash[MAX_HASH_SIZE];
Utils::sha256(name_hash, MAX_HASH_SIZE, (const uint8_t *)name, strlen(name));
Utils::sha256(hash, MAX_HASH_SIZE, name_hash, MAX_HASH_SIZE, identity.pub_key, PUB_KEY_SIZE);
}
Destination::Destination(const char* name) {
uint8_t name_hash[MAX_HASH_SIZE];
Utils::sha256(name_hash, MAX_HASH_SIZE, (const uint8_t *)name, strlen(name));
Utils::sha256(hash, MAX_HASH_SIZE, name_hash, MAX_HASH_SIZE);
}
Destination::Destination() {
memset(hash, 0, MAX_HASH_SIZE);
}
bool Destination::matches(const uint8_t* other_hash) {
return memcmp(hash, other_hash, MAX_HASH_SIZE) == 0;
}
}

25
src/Destination.h Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <MeshCore.h>
#include <Identity.h>
namespace mesh {
/**
* \brief Represents an end-point in the mesh, identified by a truncated SHA256 hash. (of DEST_HASH_SIZE)
* The hash is either from just a 'name' (C-string), and these can be thought of as 'broadcast' addresses,
* or can be the hash of name + Identity.public_key
*/
class Destination {
public:
uint8_t hash[MAX_HASH_SIZE];
Destination(const Identity& identity, const char* name);
Destination(const char* name);
Destination(const uint8_t desthash[]) { memcpy(hash, desthash, MAX_HASH_SIZE); }
Destination();
bool matches(const uint8_t* other_hash);
};
}

161
src/Dispatcher.cpp Normal file
View file

@ -0,0 +1,161 @@
#include "Dispatcher.h"
namespace mesh {
void Dispatcher::begin() {
_radio->begin();
}
float Dispatcher::getAirtimeBudgetFactor() const {
return 5.0; // default, 16.6% (1/6th)
}
void Dispatcher::loop() {
if (outbound) { // waiting for outbound send to be completed
if (_radio->isSendComplete()) {
long t = _ms->getMillis() - outbound_start;
total_air_time += t; // keep track of how much air time we are using
//Serial.print(" airtime="); Serial.println(t);
// will need radio silence up to next_tx_time
next_tx_time = futureMillis(t * getAirtimeBudgetFactor());
_radio->onSendFinished();
onPacketSent(outbound);
outbound = NULL;
} else if (millisHasNowPassed(outbound_expiry)) {
MESH_DEBUG_PRINTLN("Dispatcher::loop(): WARNING: outbound packed send timed out!");
//Serial.println(" timed out");
_radio->onSendFinished();
releasePacket(outbound); // return to pool
outbound = NULL;
} else {
return; // can't do any more radio activity until send is complete or timed out
}
}
checkRecv();
checkSend();
}
void Dispatcher::onPacketSent(Packet* packet) {
releasePacket(packet); // default behaviour, return packet to pool
}
void Dispatcher::checkRecv() {
Packet* pkt;
{
uint8_t raw[MAX_TRANS_UNIT];
int len = _radio->recvRaw(raw, MAX_TRANS_UNIT);
if (len > 0) {
pkt = _mgr->allocNew();
if (pkt == NULL) {
MESH_DEBUG_PRINTLN("Dispatcher::checkRecv(): WARNING: received data, no unused packets available!");
} else {
int i = 0;
#ifdef NODE_ID
uint8_t sender_id = raw[i++];
if (sender_id == NODE_ID - 1 || sender_id == NODE_ID + 1) { // simulate that NODE_ID can only hear NODE_ID-1 or NODE_ID+1, eg. 3 can't hear 1
} else {
_mgr->free(pkt); // put back into pool
return;
}
#endif
//Serial.print("LoRa recv: len="); Serial.println(len);
pkt->header = raw[i++];
pkt->path_len = raw[i++];
if (pkt->path_len > MAX_PATH_SIZE || i + pkt->path_len > len) {
MESH_DEBUG_PRINTLN("Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", len);
_mgr->free(pkt); // put back into pool
pkt = NULL;
} else {
memcpy(pkt->path, &raw[i], pkt->path_len); i += pkt->path_len;
pkt->payload_len = len - i; // payload is remainder
memcpy(pkt->payload, &raw[i], pkt->payload_len);
}
}
} else {
pkt = NULL;
}
}
if (pkt) {
DispatcherAction action = onRecvPacket(pkt);
if (action == ACTION_RELEASE) {
_mgr->free(pkt);
} else if (action == ACTION_MANUAL_HOLD) {
// sub-class is wanting to manually hold Packet instance, and call releasePacket() at appropriate time
} else { // ACTION_RETRANSMIT*
uint8_t priority = (action >> 24) - 1;
uint32_t _delay = action & 0xFFFFFF;
_mgr->queueOutbound(pkt, priority, futureMillis(_delay));
}
}
}
void Dispatcher::checkSend() {
if (_mgr->getOutboundCount() == 0) return; // nothing waiting to send
if (!millisHasNowPassed(next_tx_time)) return; // still in 'radio silence' phase (from airtime budget setting)
if (_radio->isReceiving()) return; // check if radio is currently mid-receive
outbound = _mgr->getNextOutbound(_ms->getMillis());
if (outbound) {
int len = 0;
uint8_t raw[MAX_TRANS_UNIT];
#ifdef NODE_ID
raw[len++] = NODE_ID;
#endif
raw[len++] = outbound->header;
raw[len++] = outbound->path_len;
memcpy(&raw[len], outbound->path, outbound->path_len); len += outbound->path_len;
if (len + outbound->payload_len > MAX_TRANS_UNIT) {
MESH_DEBUG_PRINTLN("Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", len + outbound->payload_len);
_mgr->free(outbound);
outbound = NULL;
} else {
memcpy(&raw[len], outbound->payload, outbound->payload_len); len += outbound->payload_len;
uint32_t max_airtime = _radio->getEstAirtimeFor(len)*3/2;
outbound_start = _ms->getMillis();
_radio->startSendRaw(raw, len);
outbound_expiry = futureMillis(max_airtime);
//Serial.print("LoRa send: len="); Serial.print(len);
}
}
}
Packet* Dispatcher::obtainNewPacket() {
return _mgr->allocNew(); // TODO: zero out all fields
}
void Dispatcher::releasePacket(Packet* packet) {
_mgr->free(packet);
}
void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) {
if (packet->path_len > MAX_PATH_SIZE || packet->payload_len > MAX_PACKET_PAYLOAD) {
MESH_DEBUG_PRINTLN("Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", (uint32_t) packet->path_len, (uint32_t) packet->payload_len);
_mgr->free(packet);
} else {
_mgr->queueOutbound(packet, priority, futureMillis(delay_millis));
}
}
// Utility function -- handles the case where millis() wraps around back to zero
// 2's complement arithmetic will handle any unsigned subtraction up to HALF the word size (32-bits in this case)
bool Dispatcher::millisHasNowPassed(unsigned long timestamp) const {
return (long)(_ms->getMillis() - timestamp) > 0;
}
unsigned long Dispatcher::futureMillis(int millis_from_now) const {
return _ms->getMillis() + millis_from_now;
}
}

129
src/Dispatcher.h Normal file
View file

@ -0,0 +1,129 @@
#pragma once
#include <MeshCore.h>
#include <Identity.h>
#include <Packet.h>
#include <Utils.h>
#include <string.h>
namespace mesh {
/**
* \brief Abstraction of local/volatile clock with Millisecond granularity.
*/
class MillisecondClock {
public:
virtual unsigned long getMillis() = 0;
};
/**
* \brief Abstraction of this device's packet radio.
*/
class Radio {
public:
virtual void begin() { }
/**
* \brief polls for incoming raw packet.
* \param bytes destination to store incoming raw packet.
* \param sz maximum packet size allowed.
* \returns 0 if no incoming data, otherwise length of complete packet received.
*/
virtual int recvRaw(uint8_t* bytes, int sz) = 0;
/**
* \returns estimated transmit air-time needed for packet of 'len_bytes', in milliseconds.
*/
virtual uint32_t getEstAirtimeFor(int len_bytes) = 0;
/**
* \brief starts the raw packet send. (no wait)
* \param bytes the raw packet data
* \param len the length in bytes
*/
virtual void startSendRaw(const uint8_t* bytes, int len) = 0;
/**
* \returns true if the previous 'startSendRaw()' completed successfully.
*/
virtual bool isSendComplete() = 0;
/**
* \brief a hook for doing any necessary clean up after transmit.
*/
virtual void onSendFinished() = 0;
/**
* \returns true if the radio is currently mid-receive of a packet.
*/
virtual bool isReceiving() { return false; }
};
/**
* \brief An abstraction for managing instances of Packets (eg. in a static pool),
* and for managing the outbound packet queue.
*/
class PacketManager {
public:
virtual Packet* allocNew() = 0;
virtual void free(Packet* packet) = 0;
virtual void queueOutbound(Packet* packet, uint8_t priority, uint32_t scheduled_for) = 0;
virtual Packet* getNextOutbound(uint32_t now) = 0; // by priority
virtual int getOutboundCount() const = 0;
virtual int getFreeCount() const = 0;
virtual Packet* getOutboundByIdx(int i) = 0;
virtual Packet* removeOutboundByIdx(int i) = 0;
};
typedef uint32_t DispatcherAction;
#define ACTION_RELEASE (0)
#define ACTION_MANUAL_HOLD (1)
#define ACTION_RETRANSMIT(pri) (((uint32_t)1 + (pri))<<24)
#define ACTION_RETRANSMIT_DELAYED(pri, _delay) ((((uint32_t)1 + (pri))<<24) | (_delay))
/**
* \brief The low-level task that manages detecting incoming Packets, and the queueing
* and scheduling of outbound Packets.
*/
class Dispatcher {
Packet* outbound; // current outbound packet
unsigned long outbound_expiry, outbound_start, total_air_time;
unsigned long next_tx_time;
protected:
PacketManager* _mgr;
Radio* _radio;
MillisecondClock* _ms;
Dispatcher(Radio& radio, MillisecondClock& ms, PacketManager& mgr)
: _radio(&radio), _ms(&ms), _mgr(&mgr)
{
outbound = NULL; total_air_time = 0; next_tx_time = 0;
}
virtual DispatcherAction onRecvPacket(Packet* pkt) = 0;
virtual void onPacketSent(Packet* packet);
virtual float getAirtimeBudgetFactor() const;
public:
void begin();
void loop();
Packet* obtainNewPacket();
void releasePacket(Packet* packet);
void sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis=0);
unsigned long getTotalAirTime() const { return total_air_time; } // in milliseconds
// helper methods
bool millisHasNowPassed(unsigned long timestamp) const;
unsigned long futureMillis(int millis_from_now) const;
private:
void checkRecv();
void checkSend();
};
}

70
src/Identity.cpp Normal file
View file

@ -0,0 +1,70 @@
#include "Identity.h"
#include <string.h>
#define ED25519_NO_SEED 1
#include <ed_25519.h>
namespace mesh {
Identity::Identity() {
memset(pub_key, 0, sizeof(pub_key));
}
Identity::Identity(const char* pub_hex) {
Utils::fromHex(pub_key, PUB_KEY_SIZE, pub_hex);
}
bool Identity::verify(const uint8_t* sig, const uint8_t* message, int msg_len) const {
return ed25519_verify(sig, message, msg_len, pub_key);
}
bool Identity::readFrom(Stream& s) {
return (s.readBytes(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE);
}
bool Identity::writeTo(Stream& s) const {
return (s.write(pub_key, PUB_KEY_SIZE) == PUB_KEY_SIZE);
}
void Identity::printTo(Stream& s) const {
Utils::printHex(s, pub_key, PUB_KEY_SIZE);
}
LocalIdentity::LocalIdentity() {
memset(prv_key, 0, sizeof(prv_key));
}
LocalIdentity::LocalIdentity(const char* prv_hex, const char* pub_hex) : Identity(pub_hex) {
Utils::fromHex(prv_key, PRV_KEY_SIZE, prv_hex);
}
LocalIdentity::LocalIdentity(RNG* rng) {
uint8_t seed[SEED_SIZE];
rng->random(seed, SEED_SIZE);
ed25519_create_keypair(pub_key, prv_key, seed);
}
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);
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);
return success;
}
void LocalIdentity::printTo(Stream& s) const {
s.print("pub_key: "); Utils::printHex(s, pub_key, PUB_KEY_SIZE); s.println();
s.print("prv_key: "); Utils::printHex(s, prv_key, PRV_KEY_SIZE); s.println();
}
void LocalIdentity::sign(uint8_t* sig, const uint8_t* message, int msg_len) const {
ed25519_sign(sig, message, msg_len, pub_key, prv_key);
}
void LocalIdentity::calcSharedSecret(uint8_t* secret, const Identity& other) {
ed25519_key_exchange(secret, other.pub_key, prv_key);
}
}

75
src/Identity.h Normal file
View file

@ -0,0 +1,75 @@
#pragma once
#include <Utils.h>
#include <Stream.h>
namespace mesh {
/**
* \brief An identity in the mesh, with given Ed25519 public key, ie. a party whose signatures can be VERIFIED.
*/
class Identity {
public:
uint8_t pub_key[PUB_KEY_SIZE];
Identity();
Identity(const char* pub_hex);
Identity(const uint8_t* _pub) { memcpy(pub_key, _pub, PUB_KEY_SIZE); }
int copyHashTo(uint8_t* dest) const {
memcpy(dest, pub_key, PATH_HASH_SIZE); // hash is just prefix of pub_key
return PATH_HASH_SIZE;
}
bool isHashMatch(const uint8_t* hash) const {
return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0;
}
/**
* \brief Performs Ed25519 signature verification.
* \param sig IN - must be SIGNATURE_SIZE buffer.
* \param message IN - the original message which was signed.
* \param msg_len IN - the length in bytes of message.
* \returns true, if signature is valid.
*/
bool verify(const uint8_t* sig, const uint8_t* message, int msg_len) const;
bool matches(const Identity& other) const { return memcmp(pub_key, other.pub_key, PUB_KEY_SIZE) == 0; }
bool matches(const uint8_t* other_pubkey) const { return memcmp(pub_key, other_pubkey, PUB_KEY_SIZE) == 0; }
bool readFrom(Stream& s);
bool writeTo(Stream& s) const;
void printTo(Stream& s) const;
};
/**
* \brief An Identity generated on THIS device, ie. with public/private Ed25519 key pair being on this device.
*/
class LocalIdentity : public Identity {
uint8_t prv_key[PRV_KEY_SIZE];
public:
LocalIdentity();
LocalIdentity(const char* prv_hex, const char* pub_hex);
LocalIdentity(RNG* rng); // create new random
/**
* \brief Ed25519 digital signature.
* \param sig OUT - must be SIGNATURE_SIZE buffer.
* \param message IN - the raw message bytes to sign.
* \param msg_len IN - the length in bytes of message.
*/
void sign(uint8_t* sig, const uint8_t* message, int msg_len) const;
/**
* \brief the ECDH key exhange, with Ed25519 public key transposed to Ex25519.
* \param secret OUT - the 'shared secret' (must be PUB_KEY_SIZE bytes)
* \param other IN - the second party in the exchange.
*/
void calcSharedSecret(uint8_t* secret, const Identity& other);
bool readFrom(Stream& s);
bool writeTo(Stream& s) const;
void printTo(Stream& s) const;
};
}

392
src/Mesh.cpp Normal file
View file

@ -0,0 +1,392 @@
#include "Mesh.h"
//#include <Arduino.h>
namespace mesh {
void Mesh::begin() {
Dispatcher::begin();
}
void Mesh::loop() {
Dispatcher::loop();
}
bool Mesh::allowPacketForward(mesh::Packet* packet) {
return false; // by default, Transport NOT enabled
}
int Mesh::searchPeersByHash(const uint8_t* hash) {
return 0; // not found
}
int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int max_matches) {
return 0; // not found
}
DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
if (pkt->getPayloadVer() > PAYLOAD_VER_1) { // not supported in this firmware version
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): unsupported packet version");
return ACTION_RELEASE;
}
if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) {
if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) {
// remove our hash from 'path', then re-broadcast
pkt->path_len -= PATH_HASH_SIZE;
memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len);
return ACTION_RETRANSMIT(0); // Routed traffic is HIGHEST priority
}
return ACTION_RELEASE; // this node is NOT the next hop (OR this packet has already been forwarded), so discard.
}
DispatcherAction action = ACTION_RELEASE;
switch (pkt->getPayloadType()) {
case PAYLOAD_TYPE_ACK: {
int i = 0;
uint32_t ack_crc;
memcpy(&ack_crc, &pkt->payload[i], 4); i += 4;
if (i > pkt->payload_len) {
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): incomplete ACK packet");
} else {
onAckRecv(pkt, ack_crc);
action = routeRecvPacket(pkt);
}
break;
}
case PAYLOAD_TYPE_PATH:
case PAYLOAD_TYPE_REQ:
case PAYLOAD_TYPE_RESPONSE:
case PAYLOAD_TYPE_TXT_MSG: {
int i = 0;
uint8_t dest_hash = pkt->payload[i++];
uint8_t src_hash = pkt->payload[i++];
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
if (i + 2 >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): incomplete data packet");
} else {
if (self_id.isHashMatch(&dest_hash)) {
// scan contacts DB, for all matching hashes of 'src_hash' (max 4 matches supported ATM)
int num = searchPeersByHash(&src_hash);
// for each matching contact, try to decrypt data
for (int j = 0; j < num; j++) {
uint8_t secret[PUB_KEY_SIZE];
getPeerSharedSecret(secret, j);
// decrypt, checking MAC is valid
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
if (len > 0) { // success!
if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) {
int k = 0;
uint8_t path_len = data[k++];
uint8_t* path = &data[k]; k += path_len;
uint8_t extra_type = data[k++];
uint8_t* extra = &data[k];
uint8_t extra_len = len - k; // remainder of packet (may be padded with zeroes!)
onPeerPathRecv(pkt, j, path, path_len, extra_type, extra, extra_len);
} else {
onPeerDataRecv(pkt, pkt->getPayloadType(), j, data, len);
}
break;
}
}
}
action = routeRecvPacket(pkt);
}
break;
}
case PAYLOAD_TYPE_ANON_REQ: {
int i = 0;
uint8_t dest_hash = pkt->payload[i++];
uint8_t* sender_pub_key = &pkt->payload[i]; i += PUB_KEY_SIZE;
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
if (i + 2 >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): incomplete data packet");
} else {
if (self_id.isHashMatch(&dest_hash)) {
Identity sender(sender_pub_key);
uint8_t secret[PUB_KEY_SIZE];
self_id.calcSharedSecret(secret, sender);
// decrypt, checking MAC is valid
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(secret, data, macAndData, pkt->payload_len - i);
if (len > 0) { // success!
onAnonDataRecv(pkt, pkt->getPayloadType(), sender, data, len);
}
}
action = routeRecvPacket(pkt);
}
break;
}
case PAYLOAD_TYPE_GRP_DATA:
case PAYLOAD_TYPE_GRP_TXT: {
int i = 0;
uint8_t channel_hash = pkt->payload[i++];
uint8_t* macAndData = &pkt->payload[i]; // MAC + encrypted data
if (i + 2 >= pkt->payload_len) {
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): incomplete data packet");
} else {
// scan channels DB, for all matching hashes of 'channel_hash' (max 2 matches supported ATM)
GroupChannel channels[2];
int num = searchChannelsByHash(&channel_hash, channels, 2);
// for each matching channel, try to decrypt data
for (int j = 0; j < num; j++) {
// decrypt, checking MAC is valid
uint8_t data[MAX_PACKET_PAYLOAD];
int len = Utils::MACThenDecrypt(channels[j].secret, data, macAndData, pkt->payload_len - i);
if (len > 0) { // success!
onGroupDataRecv(pkt, pkt->getPayloadType(), channels[j], data, len);
break;
}
}
action = routeRecvPacket(pkt);
}
break;
}
case PAYLOAD_TYPE_ADVERT: {
int i = 0;
Identity id;
memcpy(id.pub_key, &pkt->payload[i], PUB_KEY_SIZE); i += PUB_KEY_SIZE;
uint32_t timestamp;
memcpy(&timestamp, &pkt->payload[i], 4); i += 4;
uint8_t* signature = &pkt->payload[i]; i += SIGNATURE_SIZE;
if (i > pkt->payload_len) {
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): incomplete advertisement packet");
} else {
uint8_t* app_data = &pkt->payload[i];
int app_data_len = pkt->payload_len - i;
if (app_data_len > MAX_ADVERT_DATA_SIZE) { app_data_len = MAX_ADVERT_DATA_SIZE; }
// check that signature is valid
bool is_ok;
{
uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE];
int msg_len = 0;
memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE;
memcpy(&message[msg_len], &timestamp, 4); msg_len += 4;
memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len;
is_ok = id.verify(signature, message, msg_len);
}
if (is_ok) {
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): valid advertisement received!");
onAdvertRecv(pkt, id, timestamp, app_data, app_data_len);
action = routeRecvPacket(pkt);
} else {
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): received advertisement with forged signature!");
}
}
break;
}
default:
MESH_DEBUG_PRINTLN("Mesh::onRecvPacket(): unknown payload type, header: %d", (int) pkt->header);
action = routeRecvPacket(pkt);
break;
}
return action;
}
DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
if (packet->isRouteFlood() && packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) {
// append this node's hash to 'path'
packet->path_len += self_id.copyHashTo(&packet->path[packet->path_len]);
// as this propagates outwards, give it lower and lower priority
return ACTION_RETRANSMIT(packet->path_len); // give priority to closer sources, than ones further away
}
return ACTION_RELEASE;
}
Packet* Mesh::createAdvert(const LocalIdentity& id, const uint8_t* app_data, size_t app_data_len) {
if (app_data_len > MAX_ADVERT_DATA_SIZE) return NULL;
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("Mesh::createAdvert(): error, packet pool empty");
return NULL;
}
packet->header = (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); // ROUTE_TYPE_* is set later
packet->path_len = 0;
int len = 0;
memcpy(&packet->payload[len], id.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE;
uint32_t emitted_timestamp = _rtc->getCurrentTime();
memcpy(&packet->payload[len], &emitted_timestamp, 4); len += 4;
uint8_t* signature = &packet->payload[len]; len += SIGNATURE_SIZE; // will fill this in later
memcpy(&packet->payload[len], app_data, app_data_len); len += app_data_len;
packet->payload_len = len;
{
uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE];
int msg_len = 0;
memcpy(&message[msg_len], id.pub_key, PUB_KEY_SIZE); msg_len += PUB_KEY_SIZE;
memcpy(&message[msg_len], &emitted_timestamp, 4); msg_len += 4;
memcpy(&message[msg_len], app_data, app_data_len); msg_len += app_data_len;
id.sign(signature, message, msg_len);
}
return packet;
}
#define MAX_COMBINED_PATH (MAX_PACKET_PAYLOAD - 2 - CIPHER_BLOCK_SIZE)
Packet* Mesh::createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) {
if (path_len + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!!
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("Mesh::createPathReturn(): error, packet pool empty");
return NULL;
}
packet->header = (PAYLOAD_TYPE_PATH << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->path_len = 0;
int len = 0;
len += dest.copyHashTo(&packet->payload[len]); // dest hash
len += self_id.copyHashTo(&packet->payload[len]); // src hash
{
int data_len = 0;
uint8_t data[MAX_PACKET_PAYLOAD];
data[data_len++] = path_len;
memcpy(&data[data_len], path, path_len); data_len += path_len;
if (extra_len > 0) {
data[data_len++] = extra_type;
memcpy(&data[data_len], extra, extra_len); data_len += extra_len;
} else {
// append a timestamp, or random blob (to make packet_hash unique)
data[data_len++] = 0xFF; // dummy payload type
getRNG()->random(&data[data_len], 4); data_len += 4;
}
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
}
packet->payload_len = len;
return packet;
}
Packet* Mesh::createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t data_len) {
if (type == PAYLOAD_TYPE_ANON_REQ) {
if (data_len + 1 + PUB_KEY_SIZE + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL;
} else if (type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE) {
if (data_len + 2 + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL;
} else {
return NULL; // invalid type
}
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("Mesh::createDatagram(): error, packet pool empty");
return NULL;
}
packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->path_len = 0;
int len = 0;
if (type == PAYLOAD_TYPE_ANON_REQ) {
len += dest.copyHashTo(&packet->payload[len]); // dest hash
memcpy(&packet->payload[len], self_id.pub_key, PUB_KEY_SIZE); len += PUB_KEY_SIZE; // sender pub_key
} else {
len += dest.copyHashTo(&packet->payload[len]); // dest hash
len += self_id.copyHashTo(&packet->payload[len]); // src hash
}
len += Utils::encryptThenMAC(secret, &packet->payload[len], data, data_len);
packet->payload_len = len;
return packet;
}
Packet* Mesh::createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len) {
if (!(type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA)) return NULL; // invalid type
if (data_len + 1 + CIPHER_BLOCK_SIZE-1 > MAX_PACKET_PAYLOAD) return NULL; // too long
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("Mesh::createGroupDatagram(): error, packet pool empty");
return NULL;
}
packet->header = (type << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->path_len = 0;
int len = 0;
memcpy(&packet->payload[len], channel.hash, PATH_HASH_SIZE); len += PATH_HASH_SIZE;
len += Utils::encryptThenMAC(channel.secret, &packet->payload[len], data, data_len);
packet->payload_len = len;
return packet;
}
Packet* Mesh::createAck(uint32_t ack_crc) {
Packet* packet = obtainNewPacket();
if (packet == NULL) {
MESH_DEBUG_PRINTLN("Mesh::createAck(): error, packet pool empty");
return NULL;
}
packet->header = (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT); // ROUTE_TYPE_* set later
packet->path_len = 0;
memcpy(packet->payload, &ack_crc, 4);
packet->payload_len = 4;
return packet;
}
void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) {
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_FLOOD;
allowPacketForward(packet); // mark this packet as already sent in case it is rebroadcast back to us
uint8_t pri;
if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) {
pri = 2;
} else if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) {
pri = 3; // de-prioritie these
} else {
pri = 1;
}
sendPacket(packet, pri, delay_millis);
}
void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis) {
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_DIRECT;
memcpy(packet->path, path, packet->path_len = path_len);
allowPacketForward(packet); // mark this packet as already sent in case it is rebroadcast back to us
sendPacket(packet, 0, delay_millis);
}
void Mesh::sendZeroHop(Packet* packet, uint32_t delay_millis) {
packet->header &= ~PH_ROUTE_MASK;
packet->header |= ROUTE_TYPE_DIRECT;
packet->path_len = 0; // path_len of zero means Zero Hop
allowPacketForward(packet); // mark this packet as already sent in case it is rebroadcast back to us
sendPacket(packet, 0, delay_millis);
}
}

151
src/Mesh.h Normal file
View file

@ -0,0 +1,151 @@
#pragma once
#include <Dispatcher.h>
namespace mesh {
/**
* An abstraction of the device's Realtime Clock.
*/
class RTCClock {
public:
/**
* \returns the current time. in UNIX epoch seconds.
*/
virtual uint32_t getCurrentTime() = 0;
/**
* \param time current time in UNIX epoch seconds.
*/
virtual void setCurrentTime(uint32_t time) = 0;
};
class GroupChannel {
public:
uint8_t hash[PATH_HASH_SIZE];
uint8_t secret[PUB_KEY_SIZE];
};
/**
* \brief The next layer in the basic Dispatcher task, Mesh recognises the particular Payload TYPES,
* and provides virtual methods for sub-classes on handling incoming, and also preparing outbound Packets.
*/
class Mesh : public Dispatcher {
RTCClock* _rtc;
RNG* _rng;
protected:
DispatcherAction onRecvPacket(Packet* pkt) override;
/**
* \brief Decide what to do with received packet, ie. discard, forward, or hold
*/
DispatcherAction routeRecvPacket(Packet* packet);
/**
* \brief Check whether this packet should be forwarded (re-transmitted) or not.
* Is sub-classes responsibility to make sure given packet is only transmitted ONCE (by this node)
*/
virtual bool allowPacketForward(Packet* packet);
/**
* \brief Perform search of local DB of peers/contacts.
* \returns Number of peers with matching hash
*/
virtual int searchPeersByHash(const uint8_t* hash);
/**
* \brief lookup the ECDH shared-secret between this node and peer by idx (calculate if necessary)
* \param dest_secret destination array to copy the secret (must be PUB_KEY_SIZE bytes)
* \param peer_idx index of peer, [0..n) where n is what searchPeersByHash() returned
*/
virtual void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { }
/**
* \brief A (now decrypted) data packet has been received (by a known peer).
* NOTE: these can be received multiple times (per sender/msg-id), via different routes
* \param type one of: PAYLOAD_TYPE_TXT_MSG, PAYLOAD_TYPE_REQ, PAYLOAD_TYPE_RESPONSE
* \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned
*/
virtual void onPeerDataRecv(Packet* packet, uint8_t type, int sender_idx, uint8_t* data, size_t len) { }
/**
* \brief A path TO peer (sender_idx) has been received. (also with optional 'extra' data encoded)
* NOTE: these can be received multiple times (per sender), via differen routes
* \param sender_idx index of peer, [0..n) where n is what searchPeersByHash() returned
*/
virtual void onPeerPathRecv(Packet* packet, int sender_idx, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { }
virtual int searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int max_matches);
/**
* \brief A new incoming Advertisement has been received.
* NOTE: these can be received multiple times (per id/timestamp), via different routes
*/
virtual void onAdvertRecv(Packet* packet, const Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) { }
/**
* \brief A (now decrypted) data packet has been received.
* NOTE: these can be received multiple times (per sender/contents), via different routes
* \param type one of: PAYLOAD_TYPE_ANON_REQ
* \param sender public key provided by sender
*/
virtual void onAnonDataRecv(Packet* packet, uint8_t type, const Identity& sender, uint8_t* data, size_t len) { }
/**
* \brief A path TO 'sender' has been received. (also with optional 'extra' data encoded)
* NOTE: these can be received multiple times (per sender), via differen routes
*/
virtual void onPathRecv(Packet* packet, Identity& sender, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { }
/**
* \brief An encrypted group data packet has been received.
* NOTE: the same payload can be received multiple times, via different routes
* \param type one of: PAYLOAD_TYPE_GRP_TXT, PAYLOAD_TYPE_GRP_DATA
* \param channel the matching GroupChannel
*/
virtual void onGroupDataRecv(Packet* packet, uint8_t type, const GroupChannel& channel, uint8_t* data, size_t len) { }
/**
* \brief A simple ACK packet has been received.
* NOTE: same ACK can be received multiple times, via different routes
*/
virtual void onAckRecv(Packet* packet, uint32_t ack_crc) { }
Mesh(Radio& radio, MillisecondClock& ms, RNG& rng, RTCClock& rtc, PacketManager& mgr)
: Dispatcher(radio, ms, mgr), _rng(&rng), _rtc(&rtc)
{
}
public:
void begin();
void loop();
LocalIdentity self_id;
RNG* getRNG() const { return _rng; }
RTCClock* getRTCClock() const { return _rtc; }
Packet* createAdvert(const LocalIdentity& id, const uint8_t* app_data=NULL, size_t app_data_len=0);
Packet* createDatagram(uint8_t type, const Identity& dest, const uint8_t* secret, const uint8_t* data, size_t len);
Packet* createGroupDatagram(uint8_t type, const GroupChannel& channel, const uint8_t* data, size_t data_len);
Packet* createAck(uint32_t ack_crc);
Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len);
/**
* \brief send a locally-generated Packet with flood routing
*/
void sendFlood(Packet* packet, uint32_t delay_millis=0);
/**
* \brief send a locally-generated Packet with Direct routing
*/
void sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uint32_t delay_millis=0);
/**
* \brief send a locally-generated Packet to just neigbor nodes (zero hops)
*/
void sendZeroHop(Packet* packet, uint32_t delay_millis=0);
};
}

46
src/MeshCore.h Normal file
View file

@ -0,0 +1,46 @@
#pragma once
#include <stdint.h>
#define MAX_HASH_SIZE 8
#define PUB_KEY_SIZE 32
#define PRV_KEY_SIZE 64
#define SEED_SIZE 32
#define SIGNATURE_SIZE 64
#define MAX_ADVERT_DATA_SIZE 32
#define CIPHER_KEY_SIZE 16
#define CIPHER_BLOCK_SIZE 16
// V1
#define CIPHER_MAC_SIZE 2
#define PATH_HASH_SIZE 1
#define MAX_PACKET_PAYLOAD 184
#define MAX_PATH_SIZE 64
#define MAX_TRANS_UNIT 255
#if MESH_DEBUG && ARDUINO
#include <Arduino.h>
#define MESH_DEBUG_PRINT(...) Serial.printf(__VA_ARGS__)
#define MESH_DEBUG_PRINTLN(F, ...) Serial.printf(F "\n", ##__VA_ARGS__)
#else
#define MESH_DEBUG_PRINT(...) {}
#define MESH_DEBUG_PRINTLN(...) {}
#endif
namespace mesh {
#define BD_STARTUP_NORMAL 0 // getStartupReason() codes
#define BD_STARTUP_RX_PACKET 1
class MainBoard {
public:
virtual uint16_t getBattMilliVolts() = 0;
virtual const char* getManufacturerName() const = 0;
virtual void onBeforeTransmit() { }
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
virtual uint8_t getStartupReason() const = 0;
};
}

16
src/MeshTables.h Normal file
View file

@ -0,0 +1,16 @@
#pragma once
#include <Mesh.h>
namespace mesh {
/**
* An abstraction of the data tables needed to be maintained, for the routing engine.
*/
class MeshTables {
public:
virtual bool hasForwarded(const uint8_t* packet_hash) const = 0;
virtual void setHasForwarded(const uint8_t* packet_hash) = 0;
};
}

23
src/Packet.cpp Normal file
View file

@ -0,0 +1,23 @@
#include "Packet.h"
#include <string.h>
#include <SHA256.h>
namespace mesh {
Packet::Packet() {
header = 0;
path_len = 0;
payload_len = 0;
}
void Packet::calculatePacketHash(uint8_t* hash) const {
SHA256 sha;
uint8_t t = getPayloadType();
sha.update(&t, 1);
sha.update(payload, payload_len);
sha.finalize(hash, MAX_HASH_SIZE);
}
}

73
src/Packet.h Normal file
View file

@ -0,0 +1,73 @@
#pragma once
#include <MeshCore.h>
namespace mesh {
// Packet::header values
#define PH_ROUTE_MASK 0x03 // 2-bits
#define PH_TYPE_SHIFT 2
#define PH_TYPE_MASK 0x0F // 4-bits
#define PH_VER_SHIFT 6
#define PH_VER_MASK 0x03 // 2-bits
#define ROUTE_TYPE_RESERVED1 0x00 // FUTURE
#define ROUTE_TYPE_FLOOD 0x01 // flood mode, needs 'path' to be built up (max 64 bytes)
#define ROUTE_TYPE_DIRECT 0x02 // direct route, 'path' is supplied
#define ROUTE_TYPE_RESERVED2 0x03 // FUTURE
#define PAYLOAD_TYPE_REQ 0x00 // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_RESPONSE 0x01 // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_TXT_MSG 0x02 // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
#define PAYLOAD_TYPE_ACK 0x03 // a simple ack
#define PAYLOAD_TYPE_ADVERT 0x04 // a node advertising its Identity
#define PAYLOAD_TYPE_GRP_TXT 0x05 // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
#define PAYLOAD_TYPE_GRP_DATA 0x06 // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
#define PAYLOAD_TYPE_ANON_REQ 0x07 // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
#define PAYLOAD_TYPE_PATH 0x08 // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
//...
#define PAYLOAD_TYPE_RESERVEDM 0x0F // FUTURE
#define PAYLOAD_VER_1 0x00 // 1-byte src/dest hashes, 2-byte MAC
#define PAYLOAD_VER_2 0x01 // FUTURE (eg. 2-byte hashes, 4-byte MAC ??)
#define PAYLOAD_VER_3 0x02 // FUTURE
#define PAYLOAD_VER_4 0x03 // FUTURE
/**
* \brief The fundamental transmission unit.
*/
class Packet {
public:
Packet();
uint8_t header;
uint16_t payload_len, path_len;
uint8_t path[MAX_PATH_SIZE];
uint8_t payload[MAX_PACKET_PAYLOAD];
/**
* \brief calculate the hash of payload + type
* \param dest_hash destination to store the hash (must be MAX_HASH_SIZE bytes)
*/
void calculatePacketHash(uint8_t* dest_hash) const;
/**
* \returns one of ROUTE_ values
*/
uint8_t getRouteType() const { return header & PH_ROUTE_MASK; }
bool isRouteFlood() const { return getRouteType() == ROUTE_TYPE_FLOOD; }
bool isRouteDirect() const { return getRouteType() == ROUTE_TYPE_DIRECT; }
/**
* \returns one of PAYLOAD_TYPE_ values
*/
uint8_t getPayloadType() const { return (header >> PH_TYPE_SHIFT) & PH_TYPE_MASK; }
/**
* \returns one of PAYLOAD_VER_ values
*/
uint8_t getPayloadVer() const { return (header >> PH_VER_SHIFT) & PH_VER_MASK; }
};
}

149
src/Utils.cpp Normal file
View file

@ -0,0 +1,149 @@
#include "Utils.h"
#include <AES.h>
#include <SHA256.h>
#ifdef ARDUINO
#include <Arduino.h>
#endif
namespace mesh {
uint32_t RNG::nextInt(uint32_t _min, uint32_t _max) {
uint32_t num;
random((uint8_t *) &num, sizeof(num));
return (num % (_max - _min)) + _min;
}
void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len) {
SHA256 sha;
sha.update(msg, msg_len);
sha.finalize(hash, hash_len);
}
void Utils::sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int frag1_len, const uint8_t* frag2, int frag2_len) {
SHA256 sha;
sha.update(frag1, frag1_len);
sha.update(frag2, frag2_len);
sha.finalize(hash, hash_len);
}
int Utils::decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
AES128 aes;
uint8_t* dp = dest;
const uint8_t* sp = src;
aes.setKey(shared_secret, CIPHER_KEY_SIZE);
while (sp - src < src_len) {
aes.decryptBlock(dp, sp);
dp += 16; sp += 16;
}
return sp - src; // will always be multiple of 16
}
int Utils::encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
AES128 aes;
uint8_t* dp = dest;
aes.setKey(shared_secret, CIPHER_KEY_SIZE);
while (src_len >= 16) {
aes.encryptBlock(dp, src);
dp += 16; src += 16; src_len -= 16;
}
if (src_len > 0) { // remaining partial block
uint8_t tmp[16];
memset(tmp, 0, 16);
memcpy(tmp, src, src_len);
aes.encryptBlock(dp, tmp);
dp += 16;
}
return dp - dest; // will always be multiple of 16
}
int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
int enc_len = encrypt(shared_secret, dest + CIPHER_MAC_SIZE, src, src_len);
SHA256 sha;
sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
sha.update(dest + CIPHER_MAC_SIZE, enc_len);
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, dest, CIPHER_MAC_SIZE);
return CIPHER_MAC_SIZE + enc_len;
}
int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes
uint8_t hmac[CIPHER_MAC_SIZE];
{
SHA256 sha;
sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
sha.update(src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE);
}
if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) {
return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
}
return 0; // invalid HMAC
}
static const char hex_chars[] = "0123456789ABCDEF";
void Utils::toHex(char* dest, const uint8_t* src, size_t len) {
while (len > 0) {
uint8_t b = *src++;
*dest++ = hex_chars[b >> 4];
*dest++ = hex_chars[b & 0x0F];
len--;
}
*dest = 0;
}
void Utils::printHex(Stream& s, const uint8_t* src, size_t len) {
while (len > 0) {
uint8_t b = *src++;
s.print(hex_chars[b >> 4]);
s.print(hex_chars[b & 0x0F]);
len--;
}
}
static uint8_t hexVal(char c) {
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= '0' && c <= '9') return c - '0';
return 0;
}
bool Utils::fromHex(uint8_t* dest, int dest_size, const char *src_hex) {
int len = strlen(src_hex);
if (len != dest_size*2) return false; // incorrect length
uint8_t* dp = dest;
while (dp - dest < dest_size) {
char ch = *src_hex++;
char cl = *src_hex++;
*dp++ = (hexVal(ch) << 4) | hexVal(cl);
}
return true;
}
int Utils::parseTextParts(char* text, const char* parts[], int max_num, char separator) {
int num = 0;
char* sp = text;
while (*sp && num < max_num) {
parts[num++] = sp;
while (*sp && *sp != separator) sp++;
if (*sp) {
*sp++ = 0; // replace the seperator with a null, and skip past it
}
}
// if we hit the maximum parts, make sure LAST entry does NOT have separator
while (*sp && *sp != separator) sp++;
if (*sp) {
*sp = 0; // replace the separator with null
}
return num;
}
}

81
src/Utils.h Normal file
View file

@ -0,0 +1,81 @@
#pragma once
#include <MeshCore.h>
#include <Stream.h>
#include <string.h>
namespace mesh {
class RNG {
public:
virtual void random(uint8_t* dest, size_t sz) = 0;
uint32_t nextInt(uint32_t _min, uint32_t _max);
};
class Utils {
public:
/**
* \brief calculates the SHA256 hash of 'msg', storing in 'hash' and truncating the hash to 'hash_len' bytes.
*/
static void sha256(uint8_t *hash, size_t hash_len, const uint8_t* msg, int msg_len);
/**
* \brief calculates the SHA256 hash of two fragments, 'frag1' and 'frag2' (in that order), storing in 'hash' and truncating.
*/
static void sha256(uint8_t *hash, size_t hash_len, const uint8_t* frag1, int frag1_len, const uint8_t* frag2, int frag2_len);
/**
* \brief Encrypts the 'src' bytes using AES128 cipher, using 'shared_secret' as key, with key length fixed at CIPHER_KEY_SIZE.
* Final block is padded with zero bytes before encrypt. Result stored in 'dest'.
* \returns The length in bytes put into 'dest'. (rounded up to block size)
*/
static int encrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);
/**
* \brief Decrypt the 'src' bytes using AES128 cipher, using 'shared_secret' as key, with key length fixed at CIPHER_KEY_SIZE.
* 'src_len' should be multiple of block size, as returned by 'encrypt()'.
* \returns The length in bytes put into 'dest'. (dest may contain trailing zero bytes in final block)
*/
static int decrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);
/**
* \brief encrypts bytes in src, then calculates MAC on ciphertext, inserting into leading bytes of 'dest'.
* \returns total length of bytes in 'dest' (MAC + ciphertext)
*/
static int encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);
/**
* \brief checks the MAC (in leading bytes of 'src'), then if valid, decrypts remaining bytes in src.
* \returns zero if MAC is invalid, otherwise the length of decrypted bytes in 'dest'
*/
static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);
/**
* \brief converts 'src' bytes with given length to Hex representation, and null terminates.
*/
static void toHex(char* dest, const uint8_t* src, size_t len);
/**
* \brief converts 'src_hex' hexadecimal string (should be null term) back to raw bytes, storing in 'dest'.
* \param dest_size must be exactly the expected size in bytes.
* \returns true if successful
*/
static bool fromHex(uint8_t* dest, int dest_size, const char *src_hex);
/**
* \brief Prints the hexadecimal representation of 'src' bytes of given length, to Stream 's'.
*/
static void printHex(Stream& s, const uint8_t* src, size_t len);
/**
* \brief parse 'text' into parts separated by 'separator' char.
* \param text the text to parse (note is MODIFIED!)
* \param parts destination array to store pointers to starts of parse parts
* \param max_num max elements to store in 'parts' array
* \param separator the separator character
* \returns the number of parts parsed (in 'parts')
*/
static int parseTextParts(char* text, const char* parts[], int max_num, char separator=',');
};
}

View file

@ -0,0 +1,27 @@
#pragma once
#include <Mesh.h>
#include <Arduino.h>
class VolatileRTCClock : public mesh::RTCClock {
long millis_offset;
public:
VolatileRTCClock() { millis_offset = 1715770351; } // 15 May 2024, 8:50pm
uint32_t getCurrentTime() override { return (millis()/1000 + millis_offset); }
void setCurrentTime(uint32_t time) override { millis_offset = time - millis()/1000; }
};
class ArduinoMillis : public mesh::MillisecondClock {
public:
unsigned long getMillis() override { return millis(); }
};
class StdRNG : public mesh::RNG {
public:
void begin(long seed) { randomSeed(seed); }
void random(uint8_t* dest, size_t sz) override {
for (int i = 0; i < sz; i++) {
dest[i] = (::random(0, 256) & 0xFF);
}
}
};

View file

@ -0,0 +1,16 @@
#pragma once
#include <RadioLib.h>
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
class CustomSX1262 : public SX1262 {
public:
CustomSX1262(Module *mod) : SX1262(mod) { }
bool isReceiving() {
uint16_t irq = getIrqStatus();
bool hasPreamble = (irq & SX126X_IRQ_HEADER_VALID);
return hasPreamble;
}
};

View file

@ -0,0 +1,12 @@
#pragma once
#include "CustomSX1262.h"
#include "RadioLibWrappers.h"
class CustomSX1262Wrapper : public RadioLibWrapper {
public:
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceiving() override { return ((CustomSX1262 *)_radio)->isReceiving(); }
float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); }
};

View file

@ -0,0 +1,16 @@
#pragma once
#include <RadioLib.h>
#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received
class CustomSX1268 : public SX1268 {
public:
CustomSX1268(Module *mod) : SX1268(mod) { }
bool isReceiving() {
uint16_t irq = getIrqStatus();
bool hasPreamble = (irq & SX126X_IRQ_HEADER_VALID);
return hasPreamble;
}
};

View file

@ -0,0 +1,12 @@
#pragma once
#include "CustomSX1268.h"
#include "RadioLibWrappers.h"
class CustomSX1268Wrapper : public RadioLibWrapper {
public:
CustomSX1268Wrapper(CustomSX1268& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool isReceiving() override { return ((CustomSX1268 *)_radio)->isReceiving(); }
float getLastRSSI() const override { return ((CustomSX1268 *)_radio)->getRSSI(); }
float getLastSNR() const override { return ((CustomSX1268 *)_radio)->getSNR(); }
};

48
src/helpers/ESP32Board.h Normal file
View file

@ -0,0 +1,48 @@
#pragma once
#include <MeshCore.h>
#include <Arduino.h>
#if defined(ESP_PLATFORM)
#include <rom/rtc.h>
#include <sys/time.h>
class ESP32Board : public mesh::MainBoard { // abstract class
public:
void begin() {
// for future use, sub-classes SHOULD call this from their begin()
}
void reboot() override {
esp_restart();
}
};
class ESP32RTCClock : public mesh::RTCClock {
public:
ESP32RTCClock() { }
void begin() {
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_POWERON) {
// start with some date/time in the recent past
struct timeval tv;
tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm
tv.tv_usec = 0;
settimeofday(&tv, NULL);
}
}
uint32_t getCurrentTime() override {
time_t _now;
time(&_now);
return _now;
}
void setCurrentTime(uint32_t time) override {
struct timeval tv;
tv.tv_sec = time;
tv.tv_usec = 0;
settimeofday(&tv, NULL);
}
};
#endif

View file

@ -0,0 +1,91 @@
#pragma once
#include "ESP32Board.h"
#include <Arduino.h>
// LoRa radio module pins for Heltec V3
#define P_LORA_DIO_1 14
#define P_LORA_NSS 8
#define P_LORA_RESET RADIOLIB_NC
#define P_LORA_BUSY 13
#define P_LORA_SCLK 9
#define P_LORA_MISO 11
#define P_LORA_MOSI 10
// built-ins
#define PIN_VBAT_READ 1
#define PIN_ADC_CTRL 37
#define PIN_ADC_CTRL_ACTIVE LOW
#define PIN_ADC_CTRL_INACTIVE HIGH
#define PIN_LED_BUILTIN 35
#include <driver/rtc_io.h>
class HeltecV3Board : public ESP32Board {
uint8_t startup_reason;
public:
void begin() {
startup_reason = BD_STARTUP_NORMAL;
ESP32Board::begin();
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_DEEPSLEEP) {
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
if (wakeup_source & (1 << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep)
startup_reason = BD_STARTUP_RX_PACKET;
}
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
}
// battery read support
pinMode(PIN_VBAT_READ, INPUT);
adcAttachPin(PIN_VBAT_READ);
analogReadResolution(10);
pinMode(PIN_ADC_CTRL, OUTPUT);
}
uint8_t getStartupReason() const { return startup_reason; }
void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1) {
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
if (pin_wake_btn < 0) {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
} else {
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
}
if (secs > 0) {
esp_sleep_enable_timer_wakeup(secs * 1000000);
}
// Finally set ESP32 into sleep
esp_deep_sleep_start(); // CPU halts here and never returns!
}
uint16_t getBattMilliVolts() override {
digitalWrite(PIN_ADC_CTRL, PIN_ADC_CTRL_ACTIVE);
uint32_t raw = 0;
for (int i = 0; i < 8; i++) {
raw += analogRead(PIN_VBAT_READ);
}
raw = raw / 8;
digitalWrite(PIN_ADC_CTRL, PIN_ADC_CTRL_INACTIVE);
return (5.2 * (3.3 / 1024.0) * raw) * 1000;
}
const char* getManufacturerName() const override {
return "Heltec V3";
}
};

View file

@ -0,0 +1,27 @@
#include "IdentityStore.h"
bool IdentityStore::load(const char *name, mesh::LocalIdentity& id) {
bool loaded = false;
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
if (_fs->exists(filename)) {
File file = _fs->open(filename);
if (file) {
loaded = id.readFrom(file);
file.close();
}
}
return loaded;
}
bool IdentityStore::save(const char *name, const mesh::LocalIdentity& id) {
char filename[40];
sprintf(filename, "%s/%s.id", _dir, name);
File file = _fs->open(filename, "w", true);
if (file) {
id.writeTo(file);
file.close();
return true;
}
return false;
}

View file

@ -0,0 +1,15 @@
#pragma once
#include <FS.h>
#include <Identity.h>
class IdentityStore {
fs::FS* _fs;
const char* _dir;
public:
IdentityStore(fs::FS& fs, const char* dir): _fs(&fs), _dir(dir) { }
void begin() { _fs->mkdir(_dir); }
bool load(const char *name, mesh::LocalIdentity& id);
bool save(const char *name, const mesh::LocalIdentity& id);
};

View file

@ -0,0 +1,93 @@
#define RADIOLIB_STATIC_ONLY 1
#include "RadioLibWrappers.h"
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
#define STATE_TX_DONE 4
#define STATE_INT_READY 16
static volatile uint8_t state = STATE_IDLE;
// this function is called when a complete packet
// is transmitted by the module
static
#if defined(ESP8266) || defined(ESP32)
ICACHE_RAM_ATTR
#endif
void setFlag(void) {
// we sent a packet, set the flag
state |= STATE_INT_READY;
}
void RadioLibWrapper::begin() {
_radio->setPacketReceivedAction(setFlag); // this is also SentComplete interrupt
state = STATE_IDLE;
if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep)
setFlag(); // LoRa packet is already received
}
}
int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) {
if (state & STATE_INT_READY) {
int len = _radio->getPacketLength();
if (len > 0) {
if (len > sz) { len = sz; }
int err = _radio->readData(bytes, len);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData()");
} else {
// Serial.print(" readData() -> "); Serial.println(len);
}
n_recv++;
}
state = STATE_IDLE; // need another startReceive()
return len;
}
if (state != STATE_RX) {
int err = _radio->startReceive();
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceive()");
}
state = STATE_RX;
}
return 0;
}
uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) {
return _radio->getTimeOnAir(len_bytes) / 1000;
}
void RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) {
state = STATE_TX_WAIT;
_board->onBeforeTransmit();
int err = _radio->startTransmit((uint8_t *) bytes, len);
if (err != RADIOLIB_ERR_NONE) {
MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startTransmit()");
}
}
bool RadioLibWrapper::isSendComplete() {
if (state & STATE_INT_READY) {
state = STATE_IDLE;
n_sent++;
return true;
}
return false;
}
void RadioLibWrapper::onSendFinished() {
_radio->finishTransmit();
_board->onAfterTransmit();
state = STATE_IDLE;
}
float RadioLibWrapper::getLastRSSI() const {
return _radio->getRSSI();
}
float RadioLibWrapper::getLastSNR() const {
return _radio->getSNR();
}

View file

@ -0,0 +1,42 @@
#pragma once
#include <Mesh.h>
#include <RadioLib.h>
class RadioLibWrapper : public mesh::Radio {
protected:
PhysicalLayer* _radio;
mesh::MainBoard* _board;
uint32_t n_recv, n_sent;
public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; }
void begin() override;
int recvRaw(uint8_t* bytes, int sz) override;
uint32_t getEstAirtimeFor(int len_bytes) override;
void startSendRaw(const uint8_t* bytes, int len) override;
bool isSendComplete() override;
void onSendFinished() override;
uint32_t getPacketsRecv() const { return n_recv; }
uint32_t getPacketsSent() const { return n_sent; }
virtual float getLastRSSI() const;
virtual float getLastSNR() const;
};
/**
* \brief an RNG impl using the noise from the LoRa radio as entropy.
* NOTE: this is VERY SLOW! Use only for things like creating new LocalIdentity
*/
class RadioNoiseListener : public mesh::RNG {
PhysicalLayer* _radio;
public:
RadioNoiseListener(PhysicalLayer& radio): _radio(&radio) { }
void random(uint8_t* dest, size_t sz) override {
for (int i = 0; i < sz; i++) {
dest[i] = _radio->randomByte() ^ (::random(0, 256) & 0xFF);
}
}
};

View file

@ -0,0 +1,56 @@
#pragma once
#include <MeshTables.h>
#ifdef ESP32
#include <FS.h>
#endif
#define MAX_PACKET_HASHES 64
class SimpleMeshTables : public mesh::MeshTables {
uint8_t _fwd_hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE];
int _next_fwd_idx;
int lookupHashIndex(const uint8_t* hash) const {
const uint8_t* sp = _fwd_hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) return i;
}
return -1;
}
public:
SimpleMeshTables() {
memset(_fwd_hashes, 0, sizeof(_fwd_hashes));
_next_fwd_idx = 0;
}
#ifdef ESP32
void restoreFrom(File f) {
f.read(_fwd_hashes, sizeof(_fwd_hashes));
f.read((uint8_t *) &_next_fwd_idx, sizeof(_next_fwd_idx));
}
void saveTo(File f) {
f.write(_fwd_hashes, sizeof(_fwd_hashes));
f.write((const uint8_t *) &_next_fwd_idx, sizeof(_next_fwd_idx));
}
#endif
bool hasForwarded(const uint8_t* packet_hash) const override {
int i = lookupHashIndex(packet_hash);
return i >= 0;
}
void setHasForwarded(const uint8_t* packet_hash) override {
int i = lookupHashIndex(packet_hash);
if (i >= 0) {
// already in table
} else {
memcpy(&_fwd_hashes[_next_fwd_idx*MAX_HASH_SIZE], packet_hash, MAX_HASH_SIZE);
_next_fwd_idx = (_next_fwd_idx + 1) % MAX_PACKET_HASHES; // cyclic table
}
}
};

View file

@ -0,0 +1,33 @@
#pragma once
#include <Packet.h>
#include <string.h>
#define MAX_PACKET_HASHES 64
class SimpleSeenTable {
uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE];
int _next_idx;
public:
SimpleSeenTable() {
memset(_hashes, 0, sizeof(_hashes));
_next_idx = 0;
}
bool hasSeenPacket(const mesh::Packet* packet) {
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
const uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) return true;
}
memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE);
_next_idx = (_next_idx + 1) % MAX_PACKET_HASHES; // cyclic table
return false;
}
};

View file

@ -0,0 +1,97 @@
#include "StaticPoolPacketManager.h"
PacketQueue::PacketQueue(int max_entries) {
_table = new mesh::Packet*[max_entries];
_pri_table = new uint8_t[max_entries];
_schedule_table = new uint32_t[max_entries];
_size = max_entries;
_num = 0;
}
mesh::Packet* PacketQueue::get(uint32_t now) {
uint8_t min_pri = 0xFF;
int best_idx = -1;
for (int j = 0; j < _num; j++) {
if (_schedule_table[j] > now) continue; // scheduled for future... ignore for now
if (_pri_table[j] < min_pri) { // select most important priority amongst non-future entries
min_pri = _pri_table[j];
best_idx = j;
}
}
if (best_idx < 0) return NULL; // empty, or all items are still in the future
mesh::Packet* top = _table[best_idx];
int i = best_idx;
_num--;
while (i < _num) {
_table[i] = _table[i+1];
_pri_table[i] = _pri_table[i+1];
_schedule_table[i] = _schedule_table[i+1];
i++;
}
return top;
}
mesh::Packet* PacketQueue::removeByIdx(int i) {
if (i >= _num) return NULL; // invalid index
mesh::Packet* item = _table[i];
_num--;
while (i < _num) {
_table[i] = _table[i+1];
_pri_table[i] = _pri_table[i+1];
_schedule_table[i] = _schedule_table[i+1];
i++;
}
return item;
}
void PacketQueue::add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
if (_num == _size) {
// TODO: log "FATAL: queue is full!"
return;
}
_table[_num] = packet;
_pri_table[_num] = priority;
_schedule_table[_num] = scheduled_for;
_num++;
}
StaticPoolPacketManager::StaticPoolPacketManager(int pool_size): unused(pool_size), send_queue(pool_size) {
// load up our unusued Packet pool
for (int i = 0; i < pool_size; i++) {
unused.add(new mesh::Packet(), 0, 0);
}
}
mesh::Packet* StaticPoolPacketManager::allocNew() {
return unused.removeByIdx(0); // just get first one (returns NULL if empty)
}
void StaticPoolPacketManager::free(mesh::Packet* packet) {
unused.add(packet, 0, 0);
}
void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) {
send_queue.add(packet, priority, scheduled_for);
}
mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) {
//send_queue.sort(); // sort by scheduled_for/priority first
return send_queue.get(now);
}
int StaticPoolPacketManager::getOutboundCount() const {
return send_queue.count();
}
int StaticPoolPacketManager::getFreeCount() const {
return unused.count();
}
mesh::Packet* StaticPoolPacketManager::getOutboundByIdx(int i) {
return send_queue.itemAt(i);
}
mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) {
return send_queue.removeByIdx(i);
}

View file

@ -0,0 +1,34 @@
#pragma once
#include <Dispatcher.h>
class PacketQueue {
mesh::Packet** _table;
uint8_t* _pri_table;
uint32_t* _schedule_table;
int _size, _num;
public:
PacketQueue(int max_entries);
mesh::Packet* get(uint32_t now);
void add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for);
int count() const { return _num; }
mesh::Packet* itemAt(int i) const { return _table[i]; }
mesh::Packet* removeByIdx(int i);
};
class StaticPoolPacketManager : public mesh::PacketManager {
PacketQueue unused, send_queue;
public:
StaticPoolPacketManager(int pool_size);
mesh::Packet* allocNew() override;
void free(mesh::Packet* packet) override;
void queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) override;
mesh::Packet* getNextOutbound(uint32_t now) override;
int getOutboundCount() const override;
int getFreeCount() const override;
mesh::Packet* getOutboundByIdx(int i) override;
mesh::Packet* removeOutboundByIdx(int i) override;
};