Scope: what
meshtastic-sdkis and isn't responsible for protecting, where keys live, and which attack vectors the SDK design considers. Not a substitute for a formal security audit — pre-1.0 this is a working document for review.
- The bytes between the host (phone/desktop/server running the SDK) and the device.
- Cryptographic material the SDK handles in transit or persists: channel PSKs, the device's
session_passkey, PKI public keys exchanged via the device. - The SDK's persistence layer (
DeviceStorage) — what it stores, what callers must protect. - The transport adapters' attack surface (BLE pairing, TCP/HTTP endpoints, serial enumeration).
- The mesh-side wire crypto. Channel encryption (AES-CTR/AES-256) and end-to-end PKI DM encryption are performed by the firmware, not by the SDK. The SDK never holds private keys for PKI DMs and never decrypts mesh PKI traffic — the device decrypts and hands the SDK the cleartext over PhoneAPI. (See
references/meshtastic-firmware-behavior.mdlines 198-212.) - The host process's security posture. If the host process is compromised, all bets are off.
- Network-layer attacks against the underlying radio (RF jamming, replay over the air). The SDK is downstream of these.
- The MQTT broker and the firmware's MQTT side-channel —
meshtastic/mqtt-clientowns broker auth/TLS for direct broker use; PhoneAPI MQTT-proxy mode (protocol.md§14) is a transparent byte relay where credentials live on the device.
| Asset | Sensitivity | Where it lives in the SDK | Lifetime |
|---|---|---|---|
| Channel PSK | High — symmetric mesh-channel key | Channel proto inside ConfigBundle, persisted by DeviceStorage; passed in-memory through engine for decrypt |
Until channel rotated or storage cleared |
session_passkey |
Medium — 8-byte token gating admin RPCs for one connection | In-memory only inside CommandDispatcher while connected |
Per connection (not persisted) |
| Device PKI public key (own + peers) | Low — public material | NodeInfo.public_key; persisted by DeviceStorage |
Until node leaves NodeDB |
| Device PKI private key | Critical | Never present in SDK. Lives only on the device; SDK has no API to extract it. | n/a |
Inbound mesh-channel cleartext (MeshPacket.decoded) |
Varies (text/position/telemetry) | Engine memory, packets Flow, optionally storage |
Per packet; no persistence by default |
| Outbound mesh-channel cleartext (caller-supplied) | Caller's responsibility | Engine memory, outbound Channel |
Until handed to transport |
| Storage on disk (NodeDB, channels incl. PSKs, configs) | High (PSKs are key material) | :storage-sqldelight SQLite DB |
Until consumer deletes |
┌─ Mesh (RF, untrusted) ─┐
│ Other nodes, repeaters │
└────────────┬───────────┘
│ wire-crypto enforced by firmware (PSK + PKI)
┌────────────▼───────────┐
│ Local Meshtastic device│ ← trusted; holds private keys
└────────────┬───────────┘
│ PhoneAPI (BLE GATT / TCP / Serial / HTTP)
│ ── trust boundary ──
┌────────────▼───────────┐
│ Host process running │ ← trusted with everything below this line
│ meshtastic-sdk │
└────────────┬───────────┘
│ Storage write (DeviceStorage abstraction)
┌────────────▼───────────┐
│ Persistent storage │ ← consumer-controlled; SDK does NOT encrypt at rest
└────────────────────────┘
The SDK assumes the link between the host process and the device is untrusted but not actively MITM'd over BLE/serial — see "Per-transport posture" below.
| Transport | Confidentiality of PhoneAPI link | Notes |
|---|---|---|
| BLE GATT | None at PhoneAPI layer; relies on BLE pairing/bonding | Hosts SHOULD use bonded connections (Android: createBond(); iOS: standard CB pairing). Unpaired BLE traffic is sniffable from ~10 m. |
| TCP | None | TCP/4403 is plaintext. Do not expose to untrusted networks. Recommend SSH tunneling or a VPN if the radio is remote. |
| Serial (USB) | Physical access required | Treat physical access to the device or USB cable as a full compromise vector. |
Roadmap transports (transport-mqtt-proxy, transport-rpc, transport-http) carry their own posture rows in ./future/wasm-rpc-roadmap.md and are not part of the MVP threat model.
DeviceStorage implementations persist channel PSKs and the NodeDB. The SDK does not encrypt at rest. Consumers requiring at-rest protection must:
- Wrap their
StorageProviderwith platform-native encrypted storage:- Android:
EncryptedSharedPreferences/EncryptedFile(Jetpack Security). - iOS: Keychain-backed file or
NSFileProtectionCompleteflags viakotlinx-iofile backend. - JVM desktop: OS keystore + a KEK that decrypts the SQLDelight DB at startup.
- Android:
- Or, in headless server contexts, run on a filesystem with full-disk encryption.
:storage-sqldelight will optionally accept a SQLCipher driver in a follow-up artifact (:storage-sqldelight-cipher); pre-1.0 this is not in scope.
- Never persisted. Engine holds it in
CommandDispatcherfor the connection's lifetime. - Reset on every reconnect. A fresh
get_owner_requestafter handshake re-issues the value. - Single in-flight retry on
ADMIN_BAD_SESSION_KEY: the engine refreshes once, replays the original admin call once, then surfacesAdminResult.SessionKeyExpiredif still rejected. (Documented inerror-taxonomy.md.)
LogSink MAY receive sensitive material if abused. Rules enforced by the engine:
- Channel PSKs are NEVER logged at any level. Internal
toString()helpers redact them (Channel.psk = <16 bytes redacted>). session_passkeyis NEVER logged.- PKI public keys MAY be logged at
Debug(they're public). MeshPacket.decoded.payloadMAY be logged atVerboseonly. Production consumers should run withInfoor higher.- Frame-level byte dumps are gated behind
LogLevel.Verboseand the Builder opt-inBuilder.protocolLogging(level, redactor)(seeSPEC.md§3.1); off by default.
PSK redaction is a code-review invariant: reviewers inspect any new LogSink.log(...) call and reject interpolation of psk or session_passkey field names. Detekt's log-call inspection helps surface obvious cases, but there is no automated rule that proves redaction.
- Accidental on-disk leak of PSK to an untracked location. Mitigation: only
DeviceStoragewrites PSKs; engine never tee's into a file or log. Consumer'sLogSinkimpl is the only escape. - Stale NodeDB corrupting a swapped/factory-reset device's state. Mitigation:
recordOwnNodeNodeNum-mismatch atomic-clear (ADR-005); fresh handshake repopulates. - Replay/identity confusion across address changes. Mitigation:
TransportIdentityis a stable cache key; the NodeNum-mismatch rule handles physical-radio swaps behind the same address. - Unauthorized admin RPCs via leaked
session_passkey. Mitigation: passkey not persisted; rotates per connection. An attacker would need both the live transport socket AND access to engine memory. - Backpressure-induced silent data loss. Mitigation:
MeshEvent.PacketsDroppedis mandatory and observable. - Out-of-spec wire data crashing the engine. Mitigation:
WireCodecrejects malformed envelopes;MeshtasticException.Protocolsurfaces; engine continues. Property-based tests (Kotest) fuzz the codec.
- A compromised host process. If the host can read SDK memory, it can read PSKs, session passkeys, and cleartext mesh packets. This is true of any library.
- A compromised firmware. The SDK trusts what the device tells it. A malicious device can lie about NodeDB contents, claim arbitrary
session_passkeys, etc. - Physical access to the device. USB serial gives full PhoneAPI access. The device's own admin-key gating (firmware feature) is the appropriate mitigation; the SDK exposes it via
AdminApi. - Eavesdropping on TCP/4403. As stated above — host's responsibility.
- Side-channel attacks against AES on the device. Out of scope; firmware concern.
Use the org's coordinated-disclosure process. See /SECURITY.md at the repo root for the supported channels (private GitHub Security Advisory or the contacts listed there).
Vulnerabilities in the underlying firmware go to the meshtastic/firmware security process.
protocol.md§9 (channel encryption), §10 (PKI DMs), §13 (admin/session_passkey)references/meshtastic-firmware-behavior.md— confirms the device, not the SDK, decrypts PKI- ADR-005 —
recordOwnNoderebind contract error-taxonomy.md—SessionKeyExpiredretry behavior