BitChat Noise Protocol Integration: Session Establishment and Forward Secrecy Implementation

BitChat implements the Noise XX handshake pattern with Curve25519 static identity keys to establish mutually authenticated sessions, while providing forward secrecy through ephemeral pre-key envelopes and automatic session re-keying.

BitChat, developed in the permissionlesstech/bitchat repository, leverages the Noise Protocol Framework to secure peer-to-peer messaging channels without trusted third parties. The NoiseEncryptionService class in bitchat/Services/NoiseEncryptionService.swift serves as the primary façade for cryptographic operations, managing persistent identity keys, per-peer session state machines, and forward-secret courier delivery. This architecture combines static-key authentication with ephemeral key rotation to protect both real-time and asynchronous communications.

How BitChat Implements the Noise XX Handshake Pattern

The Noise Protocol Framework provides the foundation for BitChat's end-to-end encryption, utilizing specific handshake patterns tailored to different communication phases.

Static Identity Key Management

Every BitChat client maintains persistent cryptographic identities stored in the iOS Keychain. According to lines 54-86 in NoiseEncryptionService.swift, the initializer generates or loads a Curve25519 static identity key (used for Noise static key operations) and an Ed25519 signing key for message authentication.

These keys persist across app launches, enabling consistent peer identification while remaining protected by the operating system's secure enclave.

Session Initialization and Handshake Flow

The NoiseSessionManager coordinates per-peer session state. Lines 31-33 instantiate this manager with the static identity key and configuration timers. To establish a new session, the service calls initiateHandshake(with:) (lines 96-104), which generates raw Noise XX initiation bytes.

When the peer responds, processHandshakeMessageWithResult (lines 16-18) handles the incoming handshake bytes. Upon successful transcript completion, handleSessionEstablished (lines 23-30) derives a fingerprint from the remote static key and notifies registered authentication handlers.

Forward Secrecy Mechanisms in BitChat

Beyond the initial handshake, BitChat implements multiple layers of forward secrecy to protect historical messages even if long-term keys are compromised.

One-Way X-Pattern Envelopes for Courier Delivery

For store-and-forward messaging (courier mode), BitChat uses Noise X pattern envelopes encrypted to static keys. The sealCourierPayload and openCourierPayload methods (lines 46-62) create these envelopes. While this provides encryption, it lacks forward secrecy because the static key remains constant.

Pre-Key Bundles for True Forward Secrecy

To achieve true forward secrecy for asynchronous messaging, BitChat implements one-time pre-key envelopes. Each envelope encrypts to an ephemeral pre-key that is destroyed immediately after decryption.

The sealPrekeyPayload method (lines 92-106) constructs envelopes with a dedicated prekeyPrologue and X pattern, ensuring only the holder of the specific pre-key can decrypt the content:

// Assume we already have a PrekeyBundle from the remote peer
let prekey = remoteBundle.prekeys.first!

// Seal a payload to the ephemeral pre-key
let sealed = try noiseService.sealPrekeyPayload(payloadData,
                                                recipientPrekey: prekey)

// Transmit `sealed` via a courier (store-and-forward)

On receipt, openPrekeyPayload (lines 119-136) decrypts the message and marks the pre-key as consumed:

// On receipt, open it and consume the pre-key
let (plain, senderKey, consumed) = try noiseService.openPrekeyPayload(sealed,
                                                                     prekeyID: prekey.id)

print("Decrypted:", String(data: plain, encoding: .utf8)!)
print("Sender static key:", senderKey.hexString)
print("Pre-key was consumed:", consumed)

Automatic Session Re-Keying

BitChat maintains forward secrecy for active sessions through periodic re-keying. Lines 58-60 configure a rekeyTimer that fires every 60 seconds, triggering checkSessionsForRekey. When a session exceeds its key lifetime, initiateAutomaticRekey (lines 70-78) generates new handshake initiation bytes:

// The service runs a background timer (every 60 s) that calls:
noiseService.checkSessionsForRekey()   // internal call

// When a session needs rotation, the service emits:
noiseService.onRekeyHandshakeReady = { peer, initiation in
    // Send the new handshake initiation to the peer
}

This automatic rotation ensures that session keys are refreshed without user intervention, limiting the window of exposure if keys are compromised.

Complete Session Establishment Workflow

Combining these components, a typical secure messaging flow involves identity verification, handshake completion, and encrypted payload transmission:

let noiseService = NoiseEncryptionService(keychain: myKeychain)

// 1️⃣ Initiate the handshake (raw XX initiation bytes)
let initData = try noiseService.initiateHandshake(with: peerID)

// Send `initData` over BLE to the peer…

// 2️⃣ Process the peer's response (handled by the BLE layer)
let response = try noiseService.processHandshakeMessage(with: peerID, message: incomingData)

// Once the handshake completes, the session is ready (lines 81-88 guard encryption):
if noiseService.hasEstablishedSession(with: peerID) {
    // 3️⃣ Encrypt a payload
    let ciphertext = try noiseService.encrypt(messageData, for: peerID)
    // send `ciphertext`
}

Key Source Files and Architecture

File Purpose
bitchat/Services/NoiseEncryptionService.swift Core high-level API for Noise XX handshake, session management, encryption/decryption, forward-secret pre-key envelopes, and automatic re-keying.
bitchat/Services/NoiseSessionManager.swift Low-level state machine tracking per-peer sessions, handling handshake transcripts, and performing re-key operations.
bitchat/Services/NoiseHandshakeState.swift Wraps the generic Noise protocol library; creates initiator/responder states for XX and X patterns.
bitchat/Services/NoiseRateLimiter.swift Rate-limits handshake and message attempts to mitigate DoS attacks.
bitchatTests/Services/NoiseEncryptionServiceTests.swift Unit tests validating handshake flow, session replacement, re-key, and forward-secret envelope handling.

Summary

  • BitChat uses Noise XX for mutual authentication during session establishment, binding Curve25519 static identity keys to each session.
  • Forward secrecy is achieved through two mechanisms: one-time pre-key envelopes that are consumed after use (X pattern with ephemeral keys), and automatic re-keying timers that rotate session keys every 60 seconds.
  • Static identity persistence uses Curve25519 and Ed25519 key pairs stored in the iOS Keychain, initialized in NoiseEncryptionService lines 54-86.
  • Asynchronous messaging relies on sealPrekeyPayload and openPrekeyPayload to encrypt messages to ephemeral pre-keys, preventing decryption by future key compromises.
  • Session lifecycle is managed by NoiseSessionManager, with handshake initiation at lines 96-104 and processing at lines 16-18 of the main service file.

Frequently Asked Questions

How does BitChat achieve forward secrecy for offline messages?

BitChat implements forward secrecy for store-and-forward messaging using pre-key envelopes. The sealPrekeyPayload method encrypts messages to a one-time ephemeral pre-key from the recipient's bundle. When openPrekeyPayload decrypts the message, it immediately consumes and deletes the pre-key, ensuring that even if the recipient's long-term static key is later compromised, the historical message cannot be decrypted.

What is the difference between courier envelopes and pre-key envelopes in BitChat?

Courier envelopes use the Noise X pattern with static keys (sealCourierPayload / openCourierPayload, lines 46-62), providing encryption but no forward secrecy since the static key persists. Pre-key envelopes (lines 92-106 and 119-136) use ephemeral pre-keys that are destroyed after use, providing true forward secrecy for asynchronous delivery where the recipient is offline.

How often does BitChat rotate session keys?

BitChat runs a background timer every 60 seconds (configured at lines 58-60) that calls checkSessionsForRekey. If a session has exceeded its key lifetime, initiateAutomaticRekey (lines 70-78) triggers a new Noise handshake to establish fresh session keys, limiting the window of vulnerability for active conversations.

Where are the static identity keys stored in BitChat?

The Curve25519 static identity key and Ed25519 signing key are persisted in the iOS Keychain during NoiseEncryptionService initialization (lines 54-86). This provides hardware-backed security and ensures that identity keys survive app restarts while remaining inaccessible to other applications or unauthorized users.

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 →