Skip to content

Security: Jarod1230/MeshCore-Docs

Security

docs/security.md

Security Model & Cryptography

Source: src/Identity.h, src/MeshCore.h, platformio.ini, docs.meshcore.io/kiss_modem_protocol/

Overview

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.


Cryptographic Algorithms

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

Key Constants

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)

Node Identity

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:179

Key Management via CLI

get/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:33

Direct Message Encryption

Direct messages between two nodes use ECDH + AES-128:

  1. Both nodes exchange public keys via PAYLOAD_TYPE_ADVERT (0x04) packets
  2. Each node independently derives the same shared secret using X25519 ECDH via calcSharedSecret() (src/Identity.h:74)
  3. The shared secret is used as the AES-128 encryption key
  4. 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 / Channel Encryption

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

Node Advertisements

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 = 32 bytes, 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:121

Anonymous Requests

PAYLOAD_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:129

Creation 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:186

Path Privacy

Path 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→3bytes

The 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;
}

Access Control (ACL)

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/off

Default credentials (from FAQ):

  • Admin password: password
  • Guest password: hello

Security note: Change the default admin password after first setup.


Known Limitations

  • 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_IMPORT and ENABLE_PRIVATE_KEY_EXPORT are enabled by default in platformio.ini with the note "comment out for more secure firmware" (platformio.ini:32).

There aren't any published security advisories