-
Notifications
You must be signed in to change notification settings - Fork 0
Security and Encryption
This firmware layers optional end-to-end encryption on top of stock CDP, designed around the different trust boundaries in a deployment rather than one blanket scheme. Encryption is off by default and opt-in per call site — a build/device with no encryption configured behaves exactly like stock CDP.
For the full step-by-step setup, see DEVELOPER_GUIDE.md's "End-to-End Encryption Setup (DuckCrypto)" section and docs/end-to-end-encryption-setup.md in the repository. This page summarizes the design.
src/security/DuckIdentity.h/.cpp — each device generates its own X25519 keypair on first boot (from the platform's hardware RNG) and persists it to flash. There is deliberately no backup mechanism: losing the key just means re-announcing a new identity.
-
begin()— generate or load the keypair. -
getPublicKey()/getPrivateKey()— 32-byte keys (the private key is never logged or transmitted). -
getDuid(duid)— derives this device's DUID as a truncated hash of its own public key (self-certifying for the device's own identity — see Known Limitations below). -
reset()— erase the stored identity so a new one is generated on nextbegin().
ChaCha20-Poly1305 (IETF, 96-bit nonce, 128-bit tag) via src/security/DuckCrypto.h/.cpp, chosen for safe constant-time behavior in software across boards without requiring hardware AES acceleration everywhere.
-
encryptWithPeer()/decryptFromPeer()— session mode: static-static X25519 ECDH between two long-term identities (Duck↔Duck, or Duck↔MeshBeacon Ops for downlink commands). -
sealToStatic()— sealed mode: fresh, one-time ephemeral X25519 keypair per call, ECDH against a pinned destination static public key (Duck→MeshBeacon Ops uplink). -
encryptWithGroupKey()/decryptWithGroupKey()— group mode: a single pre-shared symmetric key, for broadcast traffic readable by any Duck in the same deployment.
| Mode | Topic | Direction | Key material |
|---|---|---|---|
| Sealed uplink |
sealed_uplink (0x1C) |
Duck → MeshBeacon Ops, one-way | Ephemeral (sender) + MeshBeacon Ops's static public key |
| Encrypted command |
encrypted_cmd (0x1B) |
MeshBeacon Ops → Duck | MeshBeacon Ops's static keypair + the Duck's identity |
| Peer session |
encrypted_data (0x1E) |
Duck ↔ Duck | Both Ducks' long-term identities (needs prior key exchange) |
| Group broadcast |
group_broadcast (0x1F) |
Any Duck → any Duck with the same group key | Pre-shared symmetric key |
| Identity announce |
identity_announce (0x1D) |
Broadcast or directed | N/A — the public key itself is the payload |
In every mode, the CDP header (SDUID/DDUID/topic/hop count) stays in cleartext — routing and dedup never need to see the payload — but header fields are bound into the AEAD's additional authenticated data, so a relay can't splice a captured ciphertext onto a different sender, recipient, or topic.
Fixed per-message overhead (MAX_DATA_LENGTH = 229 bytes budget): plain sendData() adds 0 bytes; sendEncryptedData() adds 29 bytes; sendSealedData() adds 61 bytes (the extra 32 bytes is the ephemeral public key, since there's no prior session to reuse).
src/security/OpenDmsConfig.h/.cpp and src/security/MeshGroupConfig.h/.cpp pin, respectively, the deployment's MeshBeacon Ops static public key (not secret — only the matching private key on the server must stay confidential) and a pre-shared mesh group symmetric key (secret). Both:
- Default to an all-zero placeholder (
isConfigured()returnsfalseuntil a real value is provisioned). - Are stored in flash (LittleFS on nRF52, EEPROM on ESP32/other) with a CRC-8 integrity check so corrupted storage fails closed instead of trusting garbage.
- Are field-provisioned over USB serial rather than requiring a firmware reflash per deployment:
-
AT+OPENDMSKEY=<64 hex chars>/AT+OPENDMSKEY+RESET/AT+OPENDMSKEY? -
AT+MESHKEY=<64 hex chars>/AT+MESHKEY+RESET/AT+MESHKEY?
-
These commands are USB-serial only today — there is no BLE stack in this firmware, so provisioning requires physical possession of the device (a deliberate safety property; see Known Limitations for why remote/BLE provisioning is riskier).
Duck::setUplinkEncryptionEnabled(bool) / isUplinkEncryptionEnabled() is a runtime preference flag, seeded at build time by the DUCK_CRYPTO_DEFAULT_ENABLED macro (0/off unless a local_*_encrypted environment sets it to 1). Sketches check this flag themselves and branch between sendData() and sendSealedData()/sendEncryptedData() — the flag does not change sendData()'s own behavior. Encryption can only ever be turned on at runtime if the firmware was built with crypto support enabled (fail closed: no build-time support means no encryption, ever).
src/security/SecurityEventCounters.h maintains lightweight, RAM-only counters (encryptedCmdRejected, broadcastRejected, beaconRejected) incremented whenever an authentication/decrypt check fails. These have no effect on accept/reject decisions — they exist purely so a summary() string (safe to send in plaintext) can be uplinked periodically for operator visibility into rejected traffic.
-
TOFU impersonation window:
identity_announceis first-seen-wins with no signature. An attacker who broadcasts a forged announcement for a target DUID before the real device's genuine announcement is heard becomes permanently trusted for that DUID (later legitimate announcements are ignored by design). Requires physical radio proximity around a device's first boot/pairing — not a remote attack. -
DUID self-certification is one-sided: a DUID is derived from its own device's public key, but neither
learnPeerIdentity()(firmware) nor MeshBeacon Ops's TOFU logic re-derives and checks that hash when trusting an announcement claiming to be a given peer — so the "self-certifying" property only protects a device's own identity, not the validity of a peer's claimed identity. -
No signing key: only an X25519 (ECDH) keypair exists; X25519 cannot sign. There is currently no key material capable of authenticating an
identity_announcebeyond the TOFU race described above. - No forward secrecy: session and sealed-uplink-receive modes use long-term static-static ECDH (aside from the sender-side ephemeral key in sealed uplinks). A future compromise of a long-term private key would let an attacker who recorded past ciphertext retroactively decrypt historical traffic under that key.