Appearance
Network key, channel names, PSKs, and channelHash
LowMesh encrypts the application tail (after the dynamic plaintext prefix: base + optional region + fragment + routing blocks) using AES-256 keys (psk32) in AES-256-CCM-4 mode (4-byte MIC), while those prefix bytes stay outside the ciphertext. That includes the default community domain channelHash == 0x0000, which uses the normative ROM key defaultCommunityPsk32 (see Channels & security). The base header still includes channelHash (LE uint16_t) so receivers can select the right key material — see Packet layout.
Asymmetric layer: Devices hold long-term Ed25519 keys in NVM; 1:1 Direct Messages use X25519 ECDH plus HKDF to derive a per-peer psk32 for AES-CCM-4 (see §5).
There is no numeric channelId on the wire. A receiver learns which crypto domain applies by matching channelHash against:
0x0000:defaultCommunityPsk32(fixed ROM key — all devices share it), and/or- Named channels: locally configured
(channelNameUtf8, psk32)pairs (SHA256 recipe in Channels & security), and/or - Pairwise DM sessions: cached
channelHashvalues derived from an X25519 ECDH shared secret (see Pairwise DM session below).
1) Network Master Key (NMK) — optional convenience root
| Property | Value |
|---|---|
| Size | 32 bytes |
| Purpose | Optional root secret for a deployment; can be used to derive initial per-channel PSKs during provisioning (USB/BLE/QR). |
| Storage | Device NVM in wrapped form (device unique key) or secure element. |
Deriving a channel PSK from NMK (informative)
If you want “one QR for the network” but still separate crypto domains per channel name:
psk32 = HKDF-SHA256(
ikm = NMK,
salt = "lowmesh/psk/v1" || deploymentId,
info = uint16_le(N) || channelNameUtf8
)This yields a stable psk32 for a given channelNameUtf8, which then yields a stable channelHash via the normative SHA256 recipe in Channels & security.
Why keep explicit PSKs anyway? Meshtastic-style deployments often want some channels to be standalone secrets (QR contains only that channel’s PSK), independent of the umbrella NMK. Firmware MAY store explicit psk32 per name without using HKDF.
2) Explicit per-channel PSKs in NVM (MeshCore™-like)
In NVM, a channel is stored at minimum as:
| Field | Size | Notes |
|---|---|---|
channelNameUtf8 | 1–63 bytes | UTF-8, no BOM |
psk | 32 bytes | AES-256 key for that channel |
The channelHash on the wire is always computed from exactly the same channelNameUtf8 + psk bytes you intend (see normative definition), then truncated to 16 bits for transmission. If two different local entries collide to the same 16-bit hash, that is a deployment error (rotate name/PSK).
3) Admin / operator channel key
Admin mesh traffic uses the fixed name lowmesh/admin and a dedicated adminChannelPsk (32 bytes). See Mesh admin & operator channel.
4) What gets encrypted
| Region | All channelHash values (including 0x0000) |
|---|---|
| Base + optional Region + optional Fragment (AAD prefix; excludes routing block) | Plain on the wire; copied into CCM AAD with ttlRaw zero-masked at offset 0x04 |
Routing block (if ttlRaw & 0x80) | Plain on the wire (excluded from CCM AAD) |
Inner bytes for payloadType | AES-CCM ciphertext + 4-byte MIC under the resolved psk32 |
Wrong PSK / wrong name / unknown peer yields CCM verification failure or meaningless plaintext; receivers MUST only accept plaintext after decrypt when channelHash resolves to defaultCommunityPsk32, a named channel, a cached DM session, or another documented key source. Unknown hashes follow DeviceRole::relayCommunityTraffic for relay behavior — see Embedded storage.
Key rotation
- Distribute a new
psk32(or newchannelNameUtf8, if you treat names as disposable) out-of-band. - Nodes MAY accept both
(name,psk)variants during an overlap window (implementation-defined). - Bump any visible epoch fields in higher-layer payloads when those exist.
Relationship to USB provisioning
NETWORK_KEY_SET / channel provisioning commands (see USB & BLE hex API) SHOULD write channelNameUtf8 + psk32 pairs (not a wire channelId). Never echo full keys in logs.
5) Pairwise DM session (X25519 + HKDF) — normative
LowMesh adds 1:1 Direct Message (DM) mode on top of AES-256-CCM-4. The bulk payload is still AES-CCM-4; the per-conversation key is HKDF-derived from an ECDH shared secret, not used raw and not from a human channel name.
Trust states (UI / policy)
Each DM peer contact SHOULD record a trustState:
| Value | Name | Meaning |
|---|---|---|
0 | UNVERIFIED | Peer key seen on-air but not yet accepted by the user |
1 | TOFU | Trust-on-first-use: first observed key was accepted locally |
2 | VERIFIED | Key confirmed out-of-band (QR, physical pairing, etc.) |
Policy MAY restrict sensitive DM traffic to TOFU or VERIFIED peers.
Pairwise encrypted DM encrypts traffic to a discovered key; encryption alone does not prove human/device identity. QR or short fingerprint comparison is the recommended verification flow.
Identity prerequisite (Ed25519)
Each device has a long-term Ed25519 keypair in NVM — see Embedded storage. Peers MUST learn the counterparty’s 32-byte Ed25519 public key (e.g. from NODE_ADVERT / NODE_INFO_* — see Node identity) before they can open a DM session.
Session establishment
To send a DM to peer P:
- Let
skEd_local/pkEd_localbe the local Ed25519 keypair, andpkEd_peerbeP’s known Ed25519 public key (32 bytes). - Transpose the local Ed25519 key material to an X25519 scalar
skX_localusing the standard Ed25519→X25519 mapping (same as libsodiumcrypto_sign_ed25519_pk_to_curve25519/..._sk_to_curve25519family, or equivalent in mbedTLS). - Map
pkEd_peerto an X25519 u-coordinate public keypkX_peer(same standard mapping). - Compute the 32-byte ECDH output (reject an all-zero shared secret):
x25519SharedSecret[0..31] = X25519( skX_local, pkX_peer )Sort the two 32-byte X25519 public keys lexicographically into
pkLowandpkHigh.Derive the DM AES key:
salt = SHA256(
UTF8("LowMesh/DM/salt/1") ||
pkLow[0..31] ||
pkHigh[0..31]
)
dmKey = HKDF-SHA256(
IKM = x25519SharedSecret[0..31],
salt = salt,
info = UTF8("LowMesh/DM/AES-256-CCM/key/1"),
L = 32
)- Use
dmKeyaspsk32for AES-256-CCM for this DM conversation. Do not usex25519SharedSecretdirectly as the AES key.
Wire channelHash for DMs
dmHashDigest[0..31] = SHA256(
UTF8("LowMesh/DM/channelHash/1") ||
dmKey[0..31]
)
channelHashDM_onWire = uint16_le( dmHashDigest[0] | (dmHashDigest[1] << 8) )Implementation constraints (ESP32-S3, nRF52840)
Ed25519↔X25519 conversion, SHA-256, and X25519 ECDH SHOULD use optimized, vetted libraries (mbedTLS, TweetNaCl, PSA Crypto, etc.). Curve math can be CPU-heavy relative to LoRa symbol times — implementations SHOULD avoid blocking radio ISR / driver paths (defer to a worker task, precompute where possible, and cap concurrent ECDH work).
Security note
This design binds confidentiality to long-term identity keys and per-peer ECDH. It does not by itself provide forward secrecy unless you add ephemeral keys or a ratchet in a higher layer.