Most encryption tutorials stop at "use AES-256-GCM." But in a mesh network, the threat model is different. Keys rotate constantly. Messages arrive out of order. Some nodes are compromised. You need an AEAD cipher that's fast, portable, and plays well with key derivation.
GhostWire uses XChaCha20-Poly1305 as its primary cipher. Here's why, and how it works in practice.
Why Not AES-256-GCM?
AES is fast — on hardware that has AES-NI instructions. But GhostWire runs on ESP32, Raspberry Pi, old laptops, and phones. Not all of these have hardware AES acceleration.
ChaCha20-Poly1305:
- Constant-time on all platforms (no timing side-channels)
- Fast in software — often faster than AES on devices without AES-NI
- Simpler implementation — fewer ways to get it wrong
XChaCha20-Poly1305 extends this with a 192-bit nonce (vs 96-bit for standard ChaCha20-Poly1305). That's critical for mesh networks where you might encrypt millions of messages per key — a 96-bit nonce would exhaust after ~2^32 messages with the same key.
The Encryption Flow
Here's how a message gets encrypted in GhostWire:
use chacha20poly1305::{XChaCha20Poly1305, KeyInit, aead::Aead};
use chacha20poly1305::aead::OsRng;
use x25519_dalek::{EphemeralSecret, PublicKey};
// 1. Key exchange (X25519 ECDH)
let secret = EphemeralSecret::random_from_rng(OsRng);
let public = PublicKey::from(&secret);
// 2. Derive shared secret
let shared = secret.diffie_hellman(&peer_public);
// 3. Derive encryption key from shared secret
let key = hkdf_expand(shared.as_bytes(), b"ghostwire-v1");
// 4. Encrypt with XChaCha20-Poly1305
let cipher = XChaCha20Poly1305::new(key.into());
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let ciphertext = cipher.encrypt(&nonce, plaintext)?;
// 5. Send: nonce || ciphertext || tag
The nonce is random (not sequential) because messages might arrive out of order in a mesh network. With a 192-bit random nonce, the probability of collision is negligible even with millions of messages.
Forward Secrecy with the Ratchet
Static keys are a liability. If a node is compromised, all past messages are exposed. GhostWire implements a Double Ratchet pattern (inspired by Signal) for session keys:
Session Key = HKDF(previous_key, message_number)
Every message derives a new key from the previous one. Compromising the current key doesn't expose past messages — the previous keys are deleted after use.
struct RatchetState {
root_key: [u8; 32],
chain_key: [u8; 32],
message_number: u32,
}
impl RatchetState {
fn next_message_key(&mut self) -> [u8; 32] {
let key = hkdf_expand(&self.chain_key, &self.message_number.to_le_bytes());
self.chain_key = hkdf_expand(&self.chain_key, b"chain");
self.message_number += 1;
key
}
fn advance_chain(&mut self, new_chain_key: [u8; 32]) {
self.root_key = hkdf_expand(&self.root_key, &new_chain_key);
self.chain_key = new_chain_key;
}
}
This gives us forward secrecy (past messages are safe) and future secrecy (compromising one key doesn't expose future messages, if the ratchet step happens).
Post-Quantum Hybrid
XChaCha20-Poly1305 is quantum-safe (it's not based on factoring or discrete log). But the key exchange (X25519) isn't — a quantum computer could derive the shared secret.
GhostWire solves this with hybrid key exchange:
shared_secret = X25519(my_secret, peer_public) || ML-KEM-768(my_secret, peer_public)
Both classical and post-quantum shared secrets are concatenated and fed into HKDF. An attacker needs to break both to get the key. If ML-KEM-768 falls to quantum computing, X25519 still protects you (and vice versa).
fn hybrid_shared_secret(
x25519_secret: &EphemeralSecret,
x25519_public: &PublicKey,
mlkem_ciphertext: &[u8],
) -> [u8; 64] {
let classical = x25519_secret.diffie_hellman(x25519_public);
let pq = mlkem_decaps(mlkem_secret, mlkem_ciphertext);
let mut combined = [0u8; 64];
combined[..32].copy_from_slice(classical.as_bytes());
combined[32..].copy_from_slice(&pq);
combined
}
Performance on Constrained Devices
We benchmarked on an ESP32-S3 (240MHz, no AES-NI):
| Operation | XChaCha20-Poly1305 | AES-256-GCM |
|---|---|---|
| Encrypt 1KB | 12μs | 18μs |
| Encrypt 64KB | 0.8ms | 1.1ms |
| Key derivation | 45μs | 45μs |
ChaCha20 is ~33% faster on this platform. On x86 with AES-NI, AES wins — but GhostWire isn't targeting data centers.
What's Next
In the next article, we'll cover Sphinx onion routing — how GhostWire hides message metadata and sender identity using layered encryption.
GitHub: github.com/Phantomojo/GhostWire-secure-mesh-communication
Website: ghostwire.cc
Built from Nairobi, under RVC.
Top comments (0)