How BitChat Implements Forward Secrecy for Live Sessions

BitChat ensures forward secrecy by combining the Noise XX handshake protocol with automatic session re-keying every 60 seconds and one-time pre-key bundles for offline messages, ensuring compromised keys cannot decrypt past communications.

The open-source BitChat project (permissionlesstech/bitchat) implements robust forward secrecy protections that prevent attackers from decrypting historical messages even if long-term identity keys are compromised later. This cryptographic guarantee is enforced through a layered approach spanning live interactive sessions and asynchronous store-and-forward messaging.

Noise XX Handshake for Ephemeral Session Keys

At the foundation of BitChat's forward secrecy lies the Noise XX handshake pattern. When two peers establish a live session, the system initiates an ephemeral Diffie-Hellman exchange that generates fresh session keys never stored long-term.

In bitchat/Services/NoiseEncryptionService.swift, the initiateHandshake method creates a NoiseSessionManager instance with a static identity key, then executes the XX handshake through processHandshakeMessage. This design ensures that compromising a device's static identity key does not reveal traffic from past sessions, as the ephemeral keys used for derivation are destroyed immediately after the handshake completes.

Automatic Session Re-keying

To limit the window of exposure for any single session key, BitChat implements automatic re-keying that forces periodic handshakes during active conversations.

The service initializes a rekeyTimer (line ≈ 58) that fires every rekeyCheckInterval (default 60 seconds). On each tick, checkSessionsForRekey (line ≈ 70) identifies sessions exceeding their configured lifetime and triggers initiateAutomaticRekey (line ≈ 81). This method orchestrates a fresh XX handshake via onRekeyHandshakeReady, replacing the old session key while the conversation remains active. This continuous rotation ensures that even if a session key is later exposed, it only protects a minimal time window of traffic.

One-Time Pre-Key Bundles for Offline Security

For store-and-forward scenarios where peers are offline, BitChat maintains forward secrecy through one-time pre-key bundles. Each device gossips a batch of signed, single-use Curve25519 public keys. When sending to an offline peer, the sender calls sealPrekeyPayload (lines ≈ 92‑100) to encrypt the payload using the Noise X pattern, where the recipient's one-time pre-key acts as the static responder key.

After the recipient successfully decrypts the message via openPrekeyPayload, the pre-key is immediately marked as consumed and its private component permanently discarded (or automatically expired after a 48‑hour grace period). This mechanism guarantees that even a future compromise of the recipient's static identity key cannot decrypt already-consumed pre-key envelopes.

Non-Forward-Secure Fallbacks

The codebase explicitly distinguishes between forward-secret and non-forward-secret transport methods. The sealCourierPayload and openCourierPayload methods (lines ≈ 44‑46) implement a static-key X pattern for one-way messages when a peer is reachable but no interactive session exists. According to the source code, these courier envelopes are not forward-secret, and the service prefers established sessions or pre-key envelopes whenever possible.

Implementation Flow for Live Sessions

The complete forward secrecy workflow operates as follows:

  1. Session Creation – initiateHandshake establishes a Noise XX session via NoiseSessionManager, deriving symmetric keys from ephemeral DH exchanges.
  2. Periodic Rotation – The rekeyTimer triggers checkSessionsForRekey every minute, calling initiateAutomaticRekey to force fresh handshakes via onRekeyHandshakeReady.
  3. Active Encryption – While sessions are valid, encrypt(_:for:) and decrypt(_:from:) use the current session key. If re-keying is required, the service emits onHandshakeRequired to signal the UI.
  4. Offline Handling – For unreachable peers, currentPrekeyBundle retrieves fresh pre-keys, and sealPrekeyPayload creates forward-secret envelopes that are consumed permanently upon first decryption.

Code Examples

// 1. Start a live session (XX handshake)
let handshakeData = try noiseService.initiateHandshake(with: peerID)
// → sends handshakeData over BLE; remote calls processHandshakeMessage

// 2. Send a message using current session key
let encrypted = try noiseService.encrypt(messageData, for: peerID)

// 3. Automatic re-key triggers after ~60 seconds via rekeyTimer
// UI listens to onRekeyHandshakeReady to transmit new handshake

// 4. Send offline message with forward secrecy
let prekeyBundle = try noiseService.currentPrekeyBundle()!
let chosenPrekey = prekeyBundle.prekeys.randomElement()!
let sealed = try noiseService.sealPrekeyPayload(payload, recipientPrekey: chosenPrekey)
// → Recipient calls openPrekeyPayload once, then key is destroyed

Key Source Files

File Purpose
bitchat/Services/NoiseEncryptionService.swift Core service managing Noise sessions, automatic re-keying, and envelope sealing
localPackages/BitFoundation/Sources/BitFoundation/PrekeyBundle.swift Definition and signing of one-time pre-key bundles
localPackages/BitFoundation/Sources/BitFoundation/NoiseSessionManager.swift Low-level Noise protocol handling and session lifecycle

Summary

  • BitChat uses the Noise XX handshake to derive ephemeral session keys that provide forward secrecy for live conversations.
  • Automatic re-keying every 60 seconds limits the exposure window of any single session key through the rekeyTimer and initiateAutomaticRekey mechanisms.
  • One-time pre-key bundles enable forward secrecy for offline messages via sealPrekeyPayload, with keys permanently deleted after first use or after 48 hours.
  • Courier envelopes (sealCourierPayload) provide a fallback but explicitly sacrifice forward secrecy for connectivity.

Frequently Asked Questions

What is forward secrecy and why does BitChat use it?

Forward secrecy is a cryptographic property that ensures session keys are not compromised even if long-term private keys are exposed in the future. BitChat implements this to protect historical messages from retroactive decryption, ensuring that a stolen device or leaked identity key cannot be used to read past conversations.

How often does BitChat rotate session keys?

By default, BitChat checks for sessions needing rotation every 60 seconds via the rekeyCheckInterval timer. The checkSessionsForRekey method evaluates session lifetimes and triggers initiateAutomaticRekey to force a fresh Noise XX handshake when thresholds are exceeded.

What happens if a BitChat session key is compromised?

Due to automatic re-keying and the ephemeral nature of Noise XX handshakes, a compromised session key only exposes traffic from that specific short time window. Previous and subsequent sessions use independently derived keys that remain secure, as the ephemeral Diffie-Hellman exchange never persists long-term key material.

How does BitChat handle forward secrecy for offline messages?

When peers are offline, BitChat uses one-time pre-key bundles defined in PrekeyBundle.swift. The sender encrypts messages via sealPrekeyPayload using the Noise X pattern, and the recipient's private pre-key is destroyed immediately after openPrekeyPayload decrypts the message. This ensures offline messages have the same forward secrecy guarantees as live sessions.

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 →