Encryption isn't enough. If an adversary can block your transport, seize your device, or coerce you into revealing keys, E2E encryption doesn't help.
GhostWire's approach to censorship resistance operates at three levels: transport diversity, coercion resistance, and data destruction.
Transport Diversity: If One Door Closes
GhostWire supports 6 transports, and automatically falls back between them:
Transport | Range | Speed | censorship resistance
─────────────────┼────────────┼──────────┼──────────────────────
Stealth TCP | Internet | High | Port hopping, obfs4
WebRTC | Internet | High | ICE/STUN/TURN
Bluetooth LE | ~100m | Low | Local, no internet
LoRa | ~5km | Very low | No infrastructure
obfs4 (Tor) | Internet | Medium | Pluggable transport
Reticulum | Variable | Variable | Packet-switched mesh
The key insight: an adversary would need to block all 6 transports simultaneously to stop communication.
Port Hopping
Standard TCP connections on known ports are easy to block. GhostWire uses port hopping — the listening port changes every 30 seconds based on a shared secret:
fn current_port(secret: &[u8; 32], timestamp: u64) -> u16 {
let epoch = timestamp / 30; // 30-second windows
let mut hasher = Sha256::new();
hasher.update(secret);
hasher.update(epoch.to_le_bytes());
let hash = hasher.finalize();
1024 + (u16::from_le_bytes([hash[0], hash[1]]) % 64512)
}
Both sides derive the same port from the shared secret and current time. An observer sees traffic on random ports — nothing to block without blocking the entire port range.
BLE Mesh
When the internet is completely cut, Bluetooth LE keeps working. GhostWire uses BLE advertising packets to discover nearby peers and establish connections. Range is limited (~100m), but in dense urban areas, messages can hop through multiple BLE nodes.
async fn ble_mesh_discovery(adapter: &Adapter) -> Vec<Peer> {
let mut peers = Vec::new();
let mut scan = adapter.scan().await?;
while let Some(event) = scan.next().await {
if let Ok(device) = event.into_device() {
if let Some(name) = device.name().await? {
if name.starts_with("GW-") {
let id = parse_peer_id(&name);
peers.push(Peer { id, transport: Transport::Ble });
}
}
}
}
peers
}
LoRa Fallback
For truly remote or disaster scenarios, LoRa provides multi-kilometer range with no infrastructure. The tradeoff is bandwidth — LoRa tops out at ~50 bytes/second. GhostWire compresses messages and uses store-and-forward for LoRa:
fn prepare_for_lora(message: &Message) -> Vec<LoRaPacket> {
let compressed = lz4_compress(&message.serialize());
compressed
.chunks(200) // LoRa max payload
.enumerate()
.map(|(i, chunk)| LoRaPacket {
seq: i as u16,
total: (compressed.len() / 200 + 1) as u16,
data: chunk.to_vec(),
checksum: crc32(chunk),
})
.collect()
}
Coercion Resistance: When They Have Your Device
The most sophisticated encryption is useless if someone puts a gun to your head and says "unlock your phone." GhostWire implements three coercion resistance mechanisms:
1. Duress PIN
Enter a specific PIN under duress, and GhostWire performs a cryptographic wipe — it doesn't just delete files, it overwrites the encryption keys with random data:
fn duress_wipe(device: &mut Device) {
// Overwrite all key material with random bytes
for key_slot in &mut device.key_slots {
let random: [u8; 32] = OsRng.gen();
key_slot.overwrite(&random);
}
// Overwrite the master key
let random_master: [u8; 64] = OsRng.gen();
device.master_key_overwrite(&random_master);
// Sync to disk
device.sync();
// The data is now permanently inaccessible
// Even forensic recovery can't retrieve the keys
}
The attacker sees a "working" app that happens to have no data. There's no visible difference between a正常使用 device and a wiped device.
2. Panic Mode
Shake the phone three times rapidly (or press volume up+down simultaneously) to trigger panic mode. This:
- Wipes all encryption keys
- Overwrites the key slots with plausible-looking fake keys
- Fakes a normal app state
- Starts logging all access attempts (for later evidence collection)
fn panic_mode(device: &mut Device) {
// Wipe real keys
duress_wipe(device);
// Plant fake keys that look real but decrypt to garbage
for key_slot in &mut device.key_slots {
let fake_key = generate_fake_key();
key_slot.write(&fake_key);
}
// Enable access logging
device.enable_access_log();
// App appears normal
device.show_normal_ui();
}
3. Dead Man's Switch
If you don't check in within a configurable time window (default: 48 hours), GhostWire automatically:
- Wipes all keys
- Notifies your emergency contacts via the mesh
- Publishes a signed "key revocation" message so contacts know your keys are compromised
struct DeadManSwitch {
interval: Duration, // How often to check in
last_checkin: Instant,
emergency_contacts: Vec<NodeId>,
enabled: bool,
}
impl DeadManSwitch {
fn check(&mut self, device: &mut Device) {
if !self.enabled { return; }
if self.last_checkin.elapsed() > self.interval {
// Time's up — execute
duress_wipe(device);
// Notify emergency contacts
for contact in &self.emergency_contacts {
let msg = EmergencyMessage::KeyRevocation {
revoked_by: device.my_id(),
timestamp: Utc::now(),
signature: device.sign(&device.my_id().to_bytes()),
};
device.send_urgent(*contact, msg);
}
}
}
fn checkin(&mut self) {
self.last_checkin = Instant::now();
}
}
Data Destruction: Beyond Deletion
Standard "delete" just removes the file pointer. The data remains on disk. GhostWire uses secure deletion:
- Encrypted storage: All data at rest is encrypted. Deleting the key destroys the data.
- Key wiping: Overwrite key slots with random data. The encrypted data becomes permanent noise.
- Forward secrecy: Past session keys are deleted after use. Even if current keys are compromised, past messages remain encrypted.
fn secure_delete(device: &mut Device, message_id: MessageId) {
// Step 1: Overwrite the message data with random bytes
if let Some(location) = device.get_storage_location(&message_id) {
let random_data: Vec<u8> = (0..location.size).map(|_| OsRng.gen()).collect();
device.write_storage(&location, &random_data);
}
// Step 2: Remove the index entry
device.remove_index(&message_id);
// Step 3: Sync to disk
device.sync();
// The data is now: (a) overwritten with random bytes,
// (b) unindexed, (c) encrypted with a deleted key
// Recovery is computationally infeasible
}
What Makes This Hard to Block
An adversary trying to stop GhostWire faces:
- 6 transports — blocking one doesn't stop the others
- Port hopping — no fixed ports to block
- Encrypted traffic — can't distinguish GhostWire from normal HTTPS
- obfs4 — traffic looks like random noise, not protocol data
- BLE/LoRa — no internet infrastructure needed
- No central servers — no single point to seize
- Duress resistance — device appears normal under coercion
- Dead man's switch — data destroys itself if you can't check in
To fully block GhostWire, you'd need to: jam all Bluetooth frequencies, block all LoRa frequencies, firewall all TCP ports, block all WebRTC traffic, and seize every device simultaneously. That's not a realistic threat model for most adversaries.
What's Next
GhostWire is open source and looking for contributors. If you care about censorship-resistant communication, we need help with:
- Transport layer testing (especially BLE and LoRa)
- Coercion resistance auditing
- Hardware integration (ESP32 firmware)
- Documentation
GitHub: github.com/Phantomojo/GhostWire-secure-mesh-communication
Website: ghostwire.cc
Built from Nairobi, under RVC.
Top comments (0)