Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ MeshCore runs simultaneously alongside Meshtastic and Reticulum. Use the protoco
- BLE: waits for GATT init (`connected` event) before issuing commands; includes nudge timeout for stuck `deviceQuery` on some devices. On **Windows**, **pair the MeshCore device in Settings → Bluetooth & devices** before connecting in the app; WinRT may need a bonded device for a stable Nordic UART session. On **Linux**, the app checks BlueZ pairing and may prompt for the PIN **before** Web Bluetooth completes when the radio is not bonded. A **second connect attempt** may run automatically after some transient GATT discovery or handshake timeouts (retry reuses the granted device without a new picker gesture).
- Serial: auto-reconnects on startup using a saved port signature so reconnect targets the same physical device when possible
- TCP: connects to MeshCore companion radio; default port **5000**, configurable per connection
- **MQTT (JSON v1):** The Connection tab MQTT card includes **Network Preset** buttons (order: **LetsMesh**, **MeshMapper**, **Colorado Mesh**, **Ripple Networks**, **Custom**). New installs default to **LetsMesh** (WebSocket on port 443, topic prefix `meshcore/test`; broker auth uses `@michaelhart/meshcore-decoder`'s `createAuthToken`; MQTT username `v1_<64-hex public key>`, password token with JWT `aud` matching the **MQTT server hostname**; optional **Packet logger** forwards RX packet summaries to the broker when enabled; see [docs/letsmesh-mqtt-auth.md](docs/letsmesh-mqtt-auth.md)). **MeshMapper** uses the same transport/topic shape (`meshcore/test`). **Colorado Mesh** is regional (Colorado residents only; WebSocket on port 443, topic prefix `meshcore/DEN`; confirm dialog on select; existing Colorado users get a one-time stay-or-switch prompt). IATA-scoped brokers require topic `meshcore/{IATA}` or `meshcore/test`. **Ripple Networks** (TLS on port 8883, topic prefix `meshcore`, shared credentials, insecure TLS confirm) and **Custom** remain available for other brokers.
- **MQTT (JSON v1):** The Connection tab MQTT card includes a **Network Preset** picker (order: **LetsMesh**, **MeshMapper**, **Colorado Mesh**, **Waev**, **Meshat.se**, **MeshCore.CA**, **EastMesh**, **Ripple Networks**, **Custom**). New installs default to **LetsMesh** (WebSocket on port 443, topic prefix `meshcore/test`; broker auth uses `@michaelhart/meshcore-decoder`'s `createAuthToken`; MQTT username `v1_<64-hex public key>`, password token with JWT `aud` matching the **MQTT server hostname**; optional **Packet logger** forwards RX packet summaries to the broker when enabled; see [docs/letsmesh-mqtt-auth.md](docs/letsmesh-mqtt-auth.md)). **LetsMesh**, **MeshMapper**, **Waev**, **Meshat.se**, **MeshCore.CA**, and **EastMesh** share that device-signing JWT flow (WebSocket path `/ws` for LetsMesh/MeshMapper, `/mqtt` for Waev/Meshat.se/MeshCore.CA/EastMesh; **MeshCore.CA** adds a Primary/Backup broker toggle). **Colorado Mesh** is regional (Colorado residents only; WebSocket on port 443, topic prefix `meshcore/DEN`; confirm dialog on select; existing Colorado users get a one-time stay-or-switch prompt). IATA-scoped brokers require topic `meshcore/{IATA}` or `meshcore/test`. **Ripple Networks** (TLS on port 8883, topic prefix `meshcore`, shared credentials, insecure TLS confirm) and **Custom** remain available for other brokers.

---

Expand Down Expand Up @@ -506,7 +506,7 @@ When both Meshtastic and MeshCore have different saved BLE peripherals, dual-rad

### MQTT

Enter your broker URL, topic, and optional credentials in the MQTT section of the Connection tab. When connected, the section collapses to a compact info card showing the server, client ID, and topic. You can send messages via MQTT without a radio when using **Meshtastic**, or **MeshCore** with brokers other than the public **LetsMesh** presets (Ripple / Custom still use the JSON v1 chat envelope for MQTT-only sends). **LetsMesh** public MQTT targets the **Analyzer** packet-logger model: optional RX summaries to `{topicPrefix}/meshcore/packets` when your radio is connected ([docs/letsmesh-mqtt-auth.md](docs/letsmesh-mqtt-auth.md)); MQTT-only channel chat to LetsMesh without a radio is not supported. **Meshtastic** uses the protobuf MQTT stack; **MeshCore** broker details are in [docs/meshcore-meshtastic-parity.md](docs/meshcore-meshtastic-parity.md). In **MeshCore** mode, **LetsMesh** / **Ripple Networks** / **Colorado Mesh** presets fill those fields for the corresponding public networks. **LetsMesh** uses the same contract as [meshcore-mqtt-broker](https://github.com/michaelhart/meshcore-mqtt-broker) with JWT `aud` matching the **regional broker hostname** you connect to (e.g. `mqtt-us-v1.letsmesh.net` / `mqtt-eu-v1.letsmesh.net`); mesh-client generates tokens from your imported MeshCore identity (`public_key` + `private_key` in config JSON). **Colorado Mesh** uses JWT auth with custom audience mapping for `meshcore_mqtt.coloradomesh.org`. Use **Custom** and paste credentials manually if your operator issued different rules.
Enter your broker URL, topic, and optional credentials in the MQTT section of the Connection tab. When connected, the section collapses to a compact info card showing the server, client ID, and topic. You can send messages via MQTT without a radio when using **Meshtastic**, or **MeshCore** with brokers other than the public **LetsMesh** presets (Ripple / Custom still use the JSON v1 chat envelope for MQTT-only sends). **LetsMesh** public MQTT targets the **Analyzer** packet-logger model: optional RX summaries to `{topicPrefix}/meshcore/packets` when your radio is connected ([docs/letsmesh-mqtt-auth.md](docs/letsmesh-mqtt-auth.md)); MQTT-only channel chat to LetsMesh without a radio is not supported. **Meshtastic** uses the protobuf MQTT stack; **MeshCore** broker details are in [docs/meshcore-meshtastic-parity.md](docs/meshcore-meshtastic-parity.md). In **MeshCore** mode, the **LetsMesh** / **MeshMapper** / **Colorado Mesh** / **Waev** / **Meshat.se** / **MeshCore.CA** / **EastMesh** / **Ripple Networks** presets fill those fields for the corresponding public networks. The device-signing presets (everything except Ripple / Custom) use the same contract as [meshcore-mqtt-broker](https://github.com/michaelhart/meshcore-mqtt-broker) with JWT `aud` matching the **broker hostname** you connect to (e.g. `mqtt-us-v1.letsmesh.net`, `mqtt.waev.app`, `mqtt1.meshcore.ca`); mesh-client generates tokens from your imported MeshCore identity (`public_key` + `private_key` in config JSON). **Custom** settings also use device signing when the configured server matches a known device-signing broker hostname (e.g. `mqtt.waev.app`); Custom is only non-device-signing for unmatched hosts. Use **Custom** and paste credentials manually if your operator issued different rules.

---

Expand Down
6 changes: 3 additions & 3 deletions docs/key-backup-and-crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Implementation: [`meshtasticDmKeyBackupStorage.ts`](../src/renderer/lib/meshtast

**Index privacy:** Same as Meshtastic — encrypted per-node archives hold private keys; the index JSON stores plaintext `nodeLabel`, `backedUpAt`, and full `publicKeyB64` (UI shows an 8-character hex prefix only).

The **active MQTT cache** is separate from per-node archives: LetsMesh JWT signing uses whichever identity was last connected or restored. Per-node archives retain full pairs without overwriting each other.
The **active MQTT cache** is separate from per-node archives: device-signing MeshCore MQTT (JWT) signing — LetsMesh, MeshMapper, Colorado Mesh, Waev, Meshat.se, MeshCore.CA, EastMesh — uses whichever identity was last connected or restored. Per-node archives retain full pairs without overwriting each other.

**How keys get into the active cache (not a per-node backup)**

Expand All @@ -88,12 +88,12 @@ The **active MQTT cache** is separate from per-node archives: LetsMesh JWT signi

1. Decrypt and validate both keys.
2. `importPrivateKey(privateKeyBytes)` on the connected radio.
3. Sync active MQTT cache with full pair; identity-updated event refreshes Connection tab LetsMesh username.
3. Sync active MQTT cache with full pair; identity-updated event refreshes the Connection tab MeshCore MQTT username.
4. Best-effort verification after reconnect delay.

Implementation: [`meshcoreKeyBackupStorage.ts`](../src/renderer/lib/meshcoreKeyBackupStorage.ts).

Details: [LetsMesh MQTT authentication](letsmesh-mqtt-auth.md).
Details: [MeshCore MQTT authentication](letsmesh-mqtt-auth.md).

---

Expand Down
19 changes: 14 additions & 5 deletions docs/letsmesh-mqtt-auth.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
# MeshCore MQTT authentication

This document describes the authentication contract used by many MeshCore MQTT brokers including **Colorado Mesh** and **LetsMesh**. mesh-client uses the same contract as [meshcore-mqtt-broker](https://github.com/michaelhart/meshcore-mqtt-broker): MQTT username `v1_<64-hex public key>` (uppercase) and a password produced by `@michaelhart/meshcore-decoder` `createAuthToken`.
This document describes the **device-signing** authentication contract shared by the public MeshCore MQTT presets: **LetsMesh** (US/EU), **MeshMapper**, **Colorado Mesh**, **Waev**, **Meshat.se**, **MeshCore.CA** (Primary/Backup), and **EastMesh**. mesh-client uses the same contract as [meshcore-mqtt-broker](https://github.com/michaelhart/meshcore-mqtt-broker): MQTT username `v1_<64-hex public key>` (uppercase) and a password produced by `@michaelhart/meshcore-decoder` `createAuthToken`. The broker allowlist and each broker's WebSocket path live in [`letsMeshJwt.ts`](../src/renderer/lib/letsMeshJwt.ts) (`DEVICE_SIGNING_HOST_WS_PATHS`).

## JWT audience (`aud`)

The broker validates that the token’s `aud` claim matches its configured `AUTH_EXPECTED_AUDIENCE` when that value is set.

For **LetsMesh public presets** (`mqtt-us-v1.letsmesh.net`, `mqtt-eu-v1.letsmesh.net`), mesh-client sets:
For **every device-signing preset**, mesh-client sets the JWT `aud` to the **same hostname as the MQTT connect server** (not a separate apex domain), via `letsMeshJwtAudience()`. Examples:

- **MQTT connect host/port**: the regional hostname and `443` (WebSocket TLS).
- **JWT `aud`**: the **same** regional hostname as the MQTT server (not a separate apex domain).
- **MQTT connect host/port**: the broker hostname (e.g. `mqtt-us-v1.letsmesh.net`, `mqtt.waev.app`, `mqtt1.meshcore.ca`) and `443` (WebSocket TLS).
- **JWT `aud`**: that exact connect hostname.

That aligns with common tooling such as [meshcoretomqtt](https://github.com/Cisien/meshcoretomqtt) (token `audience` matches the broker host). If your operator documents a different `aud`, use **Custom** MQTT and paste a manually generated token.

## WebSocket path (`wsPath`)

Device-signing brokers use one of two WebSocket paths, enforced on connect ([`letsMeshConnectionGuards.ts`](../src/renderer/lib/letsMeshConnectionGuards.ts)):

- **`/ws`** — LetsMesh (US/EU), MeshMapper, Colorado Mesh.
- **`/mqtt`** — Waev, Meshat.se, MeshCore.CA, EastMesh.

Selecting a preset (or a broker toggle) sets the correct path; a mismatched `wsPath` blocks Connect with a clear error and raises the amber deviation banner.

## WebSocket idle / keepalive

MeshCore MQTT over WSS uses **60s MQTT keepalive** (same order of magnitude as raw TCP). mqtt.js’s internal deadline is about **1.5× the keepalive**. The client sends **WebSocket `ping` frames** for proxy/LB idle paths, and periodically calls mqtt.js **`reschedulePing(true)`** so the internal keepalive timer resets when **PINGRESP** / **SUBACK** are not observed in time on the WebSocket path.
Device-signing presets default to a **30s MQTT keepalive**; the **LetsMesh Region (US/EU)** buttons raise it to **60s**. Both values are treated as valid (no deviation banner). mqtt.js’s internal deadline is about **1.5× the keepalive**. The client sends **WebSocket `ping` frames** for proxy/LB idle paths, and periodically calls mqtt.js **`reschedulePing(true)`** so the internal keepalive timer resets when **PINGRESP** / **SUBACK** are not observed in time on the WebSocket path.

## Debugging connection vs auth

Expand Down
Loading
Loading