DEV Community

Michael Muriithi
Michael Muriithi

Posted on

Building Censorship-Resistant Communication: Transports, Coercion Resistance, and Dead Man's Switches

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Wipes all encryption keys
  2. Overwrites the key slots with plausible-looking fake keys
  3. Fakes a normal app state
  4. 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();
}
Enter fullscreen mode Exit fullscreen mode

3. Dead Man's Switch

If you don't check in within a configurable time window (default: 48 hours), GhostWire automatically:

  1. Wipes all keys
  2. Notifies your emergency contacts via the mesh
  3. 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Data Destruction: Beyond Deletion

Standard "delete" just removes the file pointer. The data remains on disk. GhostWire uses secure deletion:

  1. Encrypted storage: All data at rest is encrypted. Deleting the key destroys the data.
  2. Key wiping: Overwrite key slots with random data. The encrypted data becomes permanent noise.
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

What Makes This Hard to Block

An adversary trying to stop GhostWire faces:

  1. 6 transports — blocking one doesn't stop the others
  2. Port hopping — no fixed ports to block
  3. Encrypted traffic — can't distinguish GhostWire from normal HTTPS
  4. obfs4 — traffic looks like random noise, not protocol data
  5. BLE/LoRa — no internet infrastructure needed
  6. No central servers — no single point to seize
  7. Duress resistance — device appears normal under coercion
  8. 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)