⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content

Conversation

@liquidraver
Copy link
Contributor

My pet project outgrew itself, so I thought I share it here, maybe someone will have use for the information and stuff I learned along the way in the future.

MeshCore v2 Encryption: ChaCha20-Poly1305 Implementation

Requirements

This implementation requires repeaters to forward PAYLOAD_VER_2 packets! (currently in dev everything is dropped above VER_1)

Decisions

I decided to use ChaCha20-Poly1305 because we are using software for decrypt anyway (ESP's can do hardware accelerated stuff, but I tried to be universal, chacha is 10x faster than the current AES-ECB+HMAC anyway, it was designed for "low-end" systems like our embedded variants)
I decided to use 12 byte nonce and 12 byte tag because it's the lowest that adheres to standards. (Nonce can be lowered to 8 to save some minor airtime)

Draft PR, if anyone have anything to teach me more I'd be glad to hear it.

TL;DR

ChaCha20-Poly1305 authenticated encryption, replacing AES-128-ECB scheme for enhanced security.
The implementation maintains backward compatibility with v1 packets while providing stronger cryptographic guarantees for "v2-capable" nodes, as minimal airtime increase as I could squeeze out, no handshakes no nothing "additional" packets.

This was a huge project, I worked on it in "one section at a time" to preserve context windows and try to understand everything I did along the way.
I'm not a crypto expert nor a coding guru, so read everything with this in mind.

I've tested everything I could on my daily driver companion and my repeater (the code is still running on them, I'm using our mesh with this right now, of course v2 encryptes talk just between my repeater's admin commands and my other test companion).

And for the masochist people, the full changelog, I "enchanced" my scribbles with claude:

What Changed

Core Cryptographic Changes

  1. New Encryption Algorithm: ChaCha20-Poly1305 AEAD cipher

    • Key size: 32 bytes (256-bit)
    • Nonce size: 12 bytes (96-bit)
    • Authentication tag: 12 bytes (96-bit, truncated from 16-byte Poly1305 output)
    • AAD (Additional Authenticated Data): Includes packet metadata (hashes, type, version) for integrity protection
  2. Payload Version System

    • PAYLOAD_VER_1: Legacy AES-128-ECB (unchanged, remains default)
    • PAYLOAD_VER_2: New ChaCha20-Poly1305 encryption
    • Version negotiation via advertisement flags (ADV_FEAT1_CHACHA_CAPABLE)
  3. Channel Tagging

    • Added CHANNEL_FLAG_V2 to GroupChannel.flags byte
    • Channels can be marked as v2-capable via companion app commands
    • Default channel creation remains v1 for backward compatibility
  4. Packet Types Migrated to v2

    • PAYLOAD_TYPE_REQ (request packets)
    • PAYLOAD_TYPE_RESPONSE (response packets)
    • PAYLOAD_TYPE_TXT_MSG (text messages)
    • PAYLOAD_TYPE_GRP_TXT (group text messages)
    • PAYLOAD_TYPE_GRP_DATA (group data messages)
    • PAYLOAD_TYPE_PATH (path return packets)
    • PAYLOAD_TYPE_ANON_REQ (anonymous request packets)
  5. Forwarding Logic Updated

    • Repeaters now forward PAYLOAD_VER_2 packets (previously dropped)
    • Old firmware will drop v2 packets (expected behavior)

Security Hardening (Additional Fixes)

This PR also includes critical security hardening fixes identified during code audit:

  1. Packet Parsing Hardening (Packet::readFrom())

    • Changed length parameter from uint8_t to uint16_t to prevent truncation
    • Added comprehensive bounds checks before reading header, transport codes, path, and payload
    • Prevents out-of-bounds reads on malformed packets
  2. ACK Packet Parsing

    • Added payload_len >= 4 validation before reading ack_crc
    • Applied to both flood and direct route ACK handlers
    • Prevents buffer overreads on truncated ACK packets
  3. TRACE Packet Parsing

    • Added minimum length check (payload_len >= 9) before reading trace fields
    • Added bounds validation before hash match operations
    • Prevents out-of-bounds access during path tracing
  4. Bridge Compatibility

    • RS232Bridge and ESPNowBridge now compatible with uint16_t length parameter
    • No truncation issues when handling 256-byte packets

Design Decisions in full

Why ChaCha20-Poly1305?

  1. Embedded Systems Optimized: ChaCha20 was specifically designed for software implementations on constrained devices, making it ideal for our embedded mesh nodes
  2. No Hardware Dependency: Unlike AES-GCM, ChaCha20-Poly1305 performs well in pure software, ensuring consistent performance across all platforms (ESP32, nRF52, RP2040, STM32)
  3. Side-Channel Resistance: ChaCha20's operations are naturally more resistant to timing side-channels compared to software AES implementations
  4. Proven Security: ChaCha20-Poly1305 is standardized (RFC 8439) and widely deployed (TLS 1.3, WireGuard)

Why 12-Byte (96-bit) Authentication Tag?

  1. Security vs Airtime Balance: 96-bit tags provide 2^-96 forgery probability (cryptographically secure) while minimizing airtime overhead
  2. Industry Standard: 96-bit tags are standard and widely accepted as secure
  3. Airtime Optimization: 12 bytes vs 16 bytes saves 4 bytes per packet, reducing transmission time on LoRa
  4. Library Support: The rweather/Crypto library supports tag truncation via CHACHA_TAG_SIZE

Why 12-Byte Nonce?

  1. Standard Size: 12-byte (96-bit) nonces are the standard for ChaCha20-Poly1305 (RFC 8439)
  2. Security: Provides 2^96 unique nonces, sufficient for the lifetime of any key
  3. Library Compatibility: Matches rweather/Crypto library expectations
  4. Nonce Generation: Uses hybrid strategy (random boot ID + counter + random salt) via Utils::getHardwareRandom()

AAD (Additional Authenticated Data) Implementation

AAD protects packet metadata from tampering:

  • What's included: dest_hash, src_hash (or channel_hash), payload_type, payload_version
  • Security benefit: Prevents attackers from modifying routing information or packet type without detection
  • Airtime impact: NONE - AAD is authenticated but not transmitted (computed from existing packet fields)
  • Compatibility: No breaking changes - AAD is computed from immutable packet header fields

How It Works

Encryption Flow (v2)

  1. Packet Creation: Node checks if peer/channel supports v2 (via peerSupportsCHACHA() or CHANNEL_FLAG_V2)
  2. Nonce Generation: Creates 12-byte nonce using Utils::getHardwareRandom() (platform-specific RNG)
  3. AAD Construction: Builds AAD from packet metadata (hashes, type, version)
  4. Encryption: Calls Utils::encryptCHACHA() which:
    • Sets ChaCha20 key and nonce
    • Adds AAD for authentication
    • Encrypts plaintext
    • Computes 12-byte Poly1305 tag
    • Returns: [nonce (12)] [ciphertext] [tag (12)]
  5. Packet Assembly: Prepends routing hashes and sets PAYLOAD_VER_2 in header

Decryption Flow (v2)

  1. Version Detection: Reads PAYLOAD_VER_2 from packet header
  2. AAD Reconstruction: Rebuilds AAD from packet metadata (same fields as encryption)
  3. Decryption: Calls Utils::decryptCHACHA() which:
    • Extracts nonce, ciphertext, and tag
    • Sets ChaCha20 key and nonce
    • Adds AAD
    • Decrypts ciphertext
    • Verifies tag using constant-time comparison (secure_compare())
    • Returns plaintext length on success, 0 on authentication failure
  4. Validation: If tag check fails, packet is silently dropped

Capability Negotiation

  • Advertisement: Nodes advertise v2 support via ADV_FEAT1_CHACHA_CAPABLE flag in PAYLOAD_TYPE_ADVERT packets
  • Peer Tracking: ContactInfo.supports_chacha and ClientInfo.supports_chacha track peer capabilities
  • Automatic Selection: Nodes automatically use v2 when both sender and receiver support it
  • Graceful Degradation: Falls back to v1 if peer doesn't advertise v2 support

Airtime Comparison (sorry if the table is shifted)

Packet Size Overhead

Message Length v1 (ECB) v2 (ChaCha, 12-byte tag) Difference
5 bytes 20 bytes 31 bytes +11 bytes
10 bytes 20 bytes 36 bytes +16 bytes
20 bytes 36 bytes 46 bytes +10 bytes
50 bytes 68 bytes 76 bytes +8 bytes
100 bytes 116 bytes 126 bytes +10 bytes

Airtime (BW62.5, SF8, CR8)

Message Length v1 Airtime v2 Airtime Increase
5 bytes 181.25 ms 230.40 ms +49.15 ms
10 bytes 181.25 ms 246.78 ms +65.54 ms
20 bytes 246.78 ms 279.55 ms +32.77 ms
50 bytes 377.86 ms 410.62 ms +32.77 ms
100 bytes 574.46 ms 607.23 ms +32.77 ms

Security Improvements

What v2 Provides

  1. Authenticated Encryption: Confidentiality + integrity in one operation
  2. Tamper Detection: AAD protects routing metadata from modification
  3. Constant-Time Verification: Tag comparison uses secure_compare() to prevent timing attacks
  4. Stronger Authentication: 96-bit tags vs 16-bit HMAC (2^96 vs 2^16 security level)
  5. Replay Protection: Same as v1 (hash-based duplicate detection), but stronger authentication makes replay detection more reliable

What v2 Doesn't Change

  1. Replay Protection: Still relies on in-memory hash cache (resets on reboot)
  2. ACK Encryption: PAYLOAD_TYPE_ACK packets remain unencrypted (by design, for low overhead)
  3. Key Management: Same shared secret derivation as v1
  4. Forwarding Behavior: Repeaters still forward packets (now including v2)

Backward Compatibility

  • v1 Packets: Continue to work unchanged
  • Old Firmware: Will drop v2 packets (expected)
  • Mixed Networks: v1 and v2 nodes can coexist (v1 nodes ignore v2 packets)
  • Channel Defaults: New channels default to v1 (must be explicitly upgraded to v2)

Resource Usage (CPU/RAM)

RAM Impact

  • Stack per operation: +200 bytes (temporary, ~350 bytes total vs ~146 bytes for v1)
  • Heap: No change (0 bytes - all stack-allocated)
  • Persistent RAM: No change (0 bytes)

CPU Impact

  • Performance: v2 is 10x faster than v1 (ChaCha20: ~3-4 cycles/byte vs AES-ECB: ~20-30 cycles/byte)
  • Busy repeater scenario (50 packets/sec): ~0.094% CPU usage (v2) vs ~0.94% CPU usage (v1)

Known Limitations / Future Work

  1. V2 channels' messages can only seen by V2 capable nodes

Breaking Changes

  • Old Repeaters: Will drop PAYLOAD_VER_2 packets

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant