Source:
src/Identity.h,src/MeshCore.h,platformio.ini, docs.meshcore.io/kiss_modem_protocol/
MeshCore uses modern asymmetric cryptography to provide authentication and end-to-end encryption for direct messages, while group/channel messages use pre-shared symmetric keys.
| Function | Algorithm | Key Size | Source |
|---|---|---|---|
| Node identity | Ed25519 | 32-byte pub + 64-byte prv | src/MeshCore.h:7–8 |
| Digital signatures | Ed25519 | 64-byte signature | src/MeshCore.h:10 |
| Key exchange | X25519 (ECDH) | 32-byte shared secret | src/Identity.h:71 |
| Message encryption | AES-128 | 16-byte key | src/MeshCore.h:12 |
| Message authentication | HMAC-SHA256 (truncated) | 2 bytes in packet | src/MeshCore.h:16 |
| Channel key derivation | SHA-256 (first 16 bytes) | 16-byte channel key | docs.meshcore.io |
Source:
src/MeshCore.h:6–17
#define PUB_KEY_SIZE 32 // src/MeshCore.h:7 — Ed25519 public key bytes
#define PRV_KEY_SIZE 64 // src/MeshCore.h:8 — Ed25519 private key bytes
#define SEED_SIZE 32 // src/MeshCore.h:9 — Key generation seed
#define SIGNATURE_SIZE 64 // src/MeshCore.h:10 — Ed25519 signature bytes
#define CIPHER_KEY_SIZE 16 // src/MeshCore.h:12 — AES-128 key bytes
#define CIPHER_BLOCK_SIZE 16 // src/MeshCore.h:13 — AES block size bytes
// V1:
#define CIPHER_MAC_SIZE 2 // src/MeshCore.h:16 — Packet HMAC truncated to 2 bytes
#define PATH_HASH_SIZE 1 // src/MeshCore.h:17 — Default path hash size (V1)Each MeshCore node has a unique Ed25519 key pair stored in flash.
Identity class (src/Identity.h:11)
"An identity in the mesh, with given Ed25519 public key, ie. a party whose signatures can be VERIFIED."
// src/Identity.h:11–49
class Identity {
public:
uint8_t pub_key[PUB_KEY_SIZE]; // :13 — 32 bytes, Ed25519 public key
// Ed25519 signature verification:
bool verify(const uint8_t* sig, const uint8_t* message, int msg_len) const; // :41
};LocalIdentity class (src/Identity.h:54)
"An Identity generated on THIS device, ie. with public/private Ed25519 key pair being on this device."
// src/Identity.h:54–95
class LocalIdentity : public Identity {
uint8_t prv_key[PRV_KEY_SIZE]; // :55 — 64 bytes, never transmitted
// Ed25519 digital signature:
void sign(uint8_t* sig, const uint8_t* message, int msg_len) const; // :67
// ECDH key exchange (comment: "with Ed25519 public key transposed to Ex25519"):
void calcSharedSecret(uint8_t* secret, const Identity& other) const; // :74
void calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const; // :81
// Validate that a private key can be used for ECDH:
static bool validatePrivateKey(const uint8_t prv[64]); // :88
};The node's own identity is stored as Mesh::self_id (src/Mesh.h:179):
LocalIdentity self_id; // src/Mesh.h:179get/set prv.key # Set private key (64 hex chars) — serial-only access
get public.key # Display current public key
Private key import/export must be explicitly compiled in (platformio.ini:32–33):
-D ENABLE_PRIVATE_KEY_IMPORT=1 ; platformio.ini:32 — NOTE: comment out for more secure firmware
-D ENABLE_PRIVATE_KEY_EXPORT=1 ; platformio.ini:33Direct messages between two nodes use ECDH + AES-128:
- Both nodes exchange public keys via
PAYLOAD_TYPE_ADVERT(0x04) packets - Each node independently derives the same shared secret using X25519 ECDH via
calcSharedSecret()(src/Identity.h:74) - The shared secret is used as the AES-128 encryption key
- Each message is encrypted and authenticated with a 2-byte truncated HMAC-SHA256 (
CIPHER_MAC_SIZE = 2,src/MeshCore.h:16)
The ECDH key exchange comment in the source code explicitly states: "the ECDH key exchange, with Ed25519 public key transposed to Ex25519" (src/Identity.h:70–71).
This provides end-to-end encryption — intermediate repeater nodes cannot read message contents.
Group channels use pre-shared symmetric keys:
| Channel Type | Key Source |
|---|---|
| Public | Fixed 16-byte key shared by all MeshCore nodes |
Hashtag #name |
First 16 bytes of SHA256(channel_name) |
| Private | Randomly generated 16-byte secret, shared out-of-band |
The GroupChannel struct stores the channel identifier and secret (src/Mesh.h:7–11):
class GroupChannel {
public:
uint8_t hash[PATH_HASH_SIZE]; // src/Mesh.h:9 — routing identifier
uint8_t secret[PUB_KEY_SIZE]; // src/Mesh.h:10 — 32-byte key material
};Group messages use PAYLOAD_TYPE_GRP_TXT (0x05) or PAYLOAD_TYPE_GRP_DATA (0x06) (src/Packet.h:24–25).
Private channel secrets are shared via QR codes:
meshcore://channel/add?name=MyChannel&secret=8b3387e9c5cdea6ac9e5edbaa115cd72
Advertisements use PAYLOAD_TYPE_ADVERT (0x04) (src/Packet.h:23). They contain:
- Public key (32 bytes,
PUB_KEY_SIZE) — node's Ed25519 identity - Unix timestamp (4 bytes)
- Ed25519 signature (64 bytes,
SIGNATURE_SIZE) — proves key ownership - Optional data (up to
MAX_ADVERT_DATA_SIZE = 32bytes,src/MeshCore.h:11): GPS, feature flags, name
Other nodes call Identity::verify() (src/Identity.h:41) to verify the signature.
The onAdvertRecv() callback (src/Mesh.h:121) delivers verified advertisements to the application:
virtual void onAdvertRecv(Packet* packet, const Identity& id, uint32_t timestamp,
const uint8_t* app_data, size_t app_data_len) { } // src/Mesh.h:121PAYLOAD_TYPE_ANON_REQ (0x07) — comment: "generic request (prefixed with dest_hash, ephemeral pub_key, MAC)" (src/Packet.h:26).
Delivered to the application via (src/Mesh.h:129):
virtual void onAnonDataRecv(Packet* packet, const uint8_t* secret,
const Identity& sender, uint8_t* data, size_t len) { } // src/Mesh.h:129Creation via (src/Mesh.h:186):
Packet* createAnonDatagram(uint8_t type, const LocalIdentity& sender,
const Identity& dest, const uint8_t* secret,
const uint8_t* data, size_t data_len); // src/Mesh.h:186Path tracking uses truncated node hashes rather than full public keys:
- Default (
PATH_HASH_SIZE = 1,src/MeshCore.h:17): 1 byte per hop - Optional: 2- or 3-byte hashes via
set path.hash.mode 1|2
Path hash encoding (src/Packet.h:79):
uint8_t getPathHashSize() const { return (path_len >> 6) + 1; } // :79
// bits 6–7 of path_len: 0→1byte, 1→2bytes, 2→3bytesThe node hash is simply the public key prefix (src/Identity.h:19–22):
int copyHashTo(uint8_t* dest) const {
memcpy(dest, pub_key, PATH_HASH_SIZE); // :20 — hash is just prefix of pub_key
return PATH_HASH_SIZE;
}MeshCore implements a simple permission system for connected clients:
| Level | Code | Permissions |
|---|---|---|
| Guest | 0 |
Read-only with guest password |
| Read-only | 1 |
Read with user authentication |
| Read-write | 2 |
Read + send messages |
| Admin | 3 |
Full access including configuration |
setperm <pubkey> <0|1|2|3> # Assign permission to a public key
get acl # View current ACL (serial only)
get/set allow.read.only # Allow read-only clients: on/offDefault credentials (from FAQ):
- Admin password:
password - Guest password:
hello
Security note: Change the default admin password after first setup.
- 2-byte MAC (
CIPHER_MAC_SIZE = 2,src/MeshCore.h:16): The truncated HMAC provides limited collision resistance. This is a deliberate trade-off for LoRa's 255-byte MTU constraint. - Group channel forward secrecy: None. Security of group channels depends entirely on the secrecy of the shared key.
- Public channel: Uses a fixed key known to all MeshCore nodes — provides no confidentiality.
- Private key flags:
ENABLE_PRIVATE_KEY_IMPORTandENABLE_PRIVATE_KEY_EXPORTare enabled by default inplatformio.iniwith the note "comment out for more secure firmware" (platformio.ini:32).