How BitChat Encrypts and Routes Private Messages: End‑to‑End Security and Decentralized Delivery

BitChat encrypts private messages using X‑ChaCha20‑Poly1305 after establishing a Noise protocol session, then routes them through direct mesh connections, courier peers, or Nostr relays via the MessageRouter.

The permissionlesstech/bitchat repository implements a zero‑trust messaging layer where every private communication is cryptically sealed before leaving the device. This article examines the exact mechanisms in SecureNoiseSession.swift, MessageRouter.swift, and supporting files that enable confidential delivery across intermittent mesh networks.

End‑to‑End Encryption with Noise and X‑ChaCha20‑Poly1305

Establishing the Noise Session

Before any payload transmission, BitChat establishes a cryptographic Noise session between the two peers. In bitchat/Noise/SecureNoiseSession.swift, a Diffie‑Hellman exchange produces a shared secret that is converted into a symmetric key. This key remains opaque to intermediary nodes and is never transmitted over the wire.

Sealing Messages with X‑ChaCha20‑Poly1305

Once the session is active, every private message is encrypted with X‑ChaCha20‑Poly1305, a nonce‑extended authenticated encryption construction. The compatibility wrapper in bitchat/Nostr/XChaCha20Poly1305Compat.swift handles the sealing operation:

// Encryption (simplified)
let sealed = try XChaCha20Poly1305Compat.seal(
    plaintext: messageData,
    key: sharedKey,               // derived from Noise DH exchange
    nonce24: random24ByteNonce   // 24‑byte nonce; first 4 bytes are zero
)

The resulting ciphertext can only be decrypted by the holder of the recipient’s static Noise public key, ensuring end‑to‑end confidentiality even if the message traverses untrusted couriers or public relays.

Message Routing and Delivery Mechanisms

The MessageRouter Transport Logic

In bitchat/Services/MessageRouter.swift, the sendPrivate method evaluates four transport conditions to determine how to deliver the sealed payload:

  • Connected & secure session (transport.canDeliverSecurely): The router enqueues the message in the outbox, marks it as a secure transmission, and calls transport.sendPrivateMessage(). The outbox retains the copy until a delivery or read ACK confirms receipt.

  • Connected but no secure session: The router enqueues the message, sends it via the transport, and invokes attemptCourierDeposit(). The envelope remains sealed with the recipient’s static Noise key and is handed to nearby courier peers.

  • Reachable (mesh‑retention) but not connected: The router enqueues the message, sends via the reachable transport, and attempts a courier deposit if the transport cannot deliver promptly. This provides robustness when the peer is offline.

  • No reachable transport: The router enqueues the message only and unconditionally attempts a courier deposit. This guarantees eventual delivery via couriers or Nostr relays even when the mesh is completely unavailable.

Courier Fallback for Offline Delivery

When direct delivery is impossible, the MessageRouter invokes attemptCourierDeposit(messageID:for:). This system uses the recipient’s static Noise key to create a sealed envelope that is forwarded to favorite couriers (mutual favorites) or any verified peer listed in CourierDirectory. Couriers carry the opaque payload until they physically encounter the recipient or establish connectivity.

Nostr Relay Integration

If a bridge to Nostr relays is configured, the same sealed payload can be posted to a public relay via bridgeCourierDeposit() in bitchat/Nostr/NostrProtocol.swift. The router treats the relay as a store‑and‑forward intermediary; because the content is already encrypted with the recipient’s Noise key, the relay cannot read the message, preserving confidentiality while enabling asynchronous retrieval.

Read Receipts and Delivery Confirmation

Read receipts follow the identical encryption and routing path. When a user opens a chat, PrivateChatManager.markAsRead(from:) triggers MessageRouter.sendReadReceipt(), which encrypts the receipt using the same Noise‑derived symmetric key. The router attempts delivery over any reachable transport, falling back to retry logic if no route exists, ensuring the sender receives confirmation without exposing metadata.

Summary

  • Noise protocol sessions in SecureNoiseSession.swift establish ephemeral shared secrets for symmetric encryption
  • X‑ChaCha20‑Poly1305 provides authenticated encryption with extended nonces via XChaCha20Poly1305Compat.swift
  • The MessageRouter selects transport strategies dynamically: direct secure delivery, courier deposit, or Nostr relay bridging
  • Courier peers store sealed envelopes using the recipient’s static Noise public key, enabling offline message queueing without decryption capability
  • Read receipts receive identical encryption and routing treatment, maintaining metadata privacy throughout the confirmation flow

Frequently Asked Questions

What encryption algorithm does BitChat use for private messages?

BitChat uses X‑ChaCha20‑Poly1305 with 24‑byte nonces for authenticated encryption. The symmetric key is derived from a Noise protocol session established between peers, as implemented in SecureNoiseSession.swift and XChaCha20Poly1305Compat.swift.

How does BitChat deliver messages when the recipient is offline?

When the recipient is unreachable via direct mesh transport, the MessageRouter initiates a courier deposit via attemptCourierDeposit(). Couriers—mutual favorite peers or verified nodes from CourierDirectory—hold the sealed envelope until they physically encounter the recipient or connect to Nostr relays on their behalf.

What is the role of couriers in BitChat routing?

Couriers act as decentralized message carriers for offline peers. They receive encrypted envelopes they cannot decrypt (sealed with the recipient’s static Noise key) and attempt delivery through opportunistic connections or Nostr bridges, ensuring eventual delivery without compromising message confidentiality.

Are read receipts encrypted the same way as messages?

Yes. Read receipts follow the identical encryption and routing path. When PrivateChatManager.markAsRead is invoked, it calls MessageRouter.sendReadReceipt(), which encrypts the receipt using the same Noise‑derived key and routes it through available transports with automatic retry logic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →