How BitChat Implements the Noise Protocol for End‑to‑End Encryption in Its BLE Mesh

BitChat uses the Noise Protocol Framework with the Noise_XX pattern and XChaCha20‑Poly1305 authenticated encryption to create secure, forward‑secret channels over its Bluetooth Low Energy mesh network.

The open‑source BitChat project (permissionlesstech/bitchat) implements end‑to‑end encryption by isolating all cryptographic operations in a dedicated SecureNoiseSession class. This design keeps the BLE transport layer agnostic to encryption details while guaranteeing confidentiality, integrity, and forward secrecy for mesh communications.

Core Architecture: The SecureNoiseSession Class

All Noise Protocol logic resides in bitchat/Noise/SecureNoiseSession.swift. This class wraps the handshake state machine, key derivation, and payload encryption/decryption in a single, reusable abstraction.

Session Initialization with X25519 Keys

When a BLE connection is established, the app creates a SecureNoiseSession instance configured for the Noise_XX pattern. This pattern requires no prior knowledge of peer static keys—ideal for opportunistic device discovery in mesh environments.

Key initialization parameters from the source:

  • Local static key pair: X25519.KeyPair generated via NostrIdentityBridge.swift utilities
  • Remote static key: X25519.PublicKey received through out‑of‑band mesh handshake
  • Pattern: .XX (mutual authentication with forward secrecy)
let localKeyPair = X25519.KeyPair()  // Generates fresh static key
let remotePublicKey = X25519.PublicKey(data: peerPubKeyBytes)
let noiseSession = SecureNoiseSession(
    localStaticKey: localKeyPair,
    remoteStaticKey: remotePublicKey,
    pattern: .XX
)

The Three‑Message Noise_XX Handshake

The performHandshake() method implements the canonical Noise_XX three‑message exchange defined in SecureNoiseSession.swift (lines 80–150).

Message Direction Payload Content Cryptographic Purpose
1 Initiator → Responder Empty payload + ephemeral public key Establishes initial shared secret
2 Responder → Initiator Ephemeral key + encrypted static key + MAC Mutual authentication begins
3 Initiator → Responder Encrypted static key payload Completes mutual authentication

Each message updates the internal chaining key and derives fresh symmetric keys for subsequent steps. The handshake completes with both parties possessing authenticated static keys and a shared secret.

noiseSession.performHandshake { result in
    switch result {
    case .success:
        print("Handshake completed – secure channel ready")
    case .failure(let error):
        print("Handshake failed: \(error)")
    }
}

Transport Key Derivation with HKDF

After handshake completion, SecureNoiseSession derives two unidirectional symmetric keys using HKDF (SecureNoiseSession.swift, lines 155–170):

  • txKey: For encrypting outgoing messages
  • rxKey: For decrypting incoming messages

The derivation uses the final chaining key and the domain separation string "bitchat noise transport" as the HKDF info parameter. This construction follows Noise Protocol specification requirements for key separation.

Payload Encryption: XChaCha20‑Poly1305

BitChat encrypts every BLE mesh packet with XChaCha20‑Poly1305, selected for its large 192‑bit nonce space that eliminates nonce‑reuse concerns in high‑throughput mesh scenarios.

The encryptMessage(_:) method (SecureNoiseSession.swift, lines 180–200) performs:

  1. Generates random 24‑byte nonce (XChaCha20 extended nonce)
  2. Encrypts plaintext with txKey
  3. Appends 16‑byte Poly1305 authentication tag
  4. Returns nonce || ciphertext || tag concatenation
let plaintext = Data("Hello BLE mesh!".utf8)
if let encrypted = try? noiseSession.encryptMessage(plaintext) {
    blePeripheral.writeValue(
        encrypted,
        for: characteristic,
        type: .withResponse
    )
}

Payload Decryption and Integrity Verification

On reception, decryptMessage(_:) (SecureNoiseSession.swift, lines 205–225):

  1. Extracts 24‑byte nonce from packet header
  2. Decrypts using rxKey
  3. Verifies Poly1305 MAC
  4. Returns plaintext or throws NoiseError.decryptionFailed

Failed MAC verification discards the packet and marks the session compromised, preventing oracle attacks.

func peripheral(
    _ peripheral: CBPeripheral,
    didUpdateValueFor characteristic: CBCharacteristic,
    error: Error?
) {
    guard let encrypted = characteristic.value else { return }
    do {
        let decrypted = try noiseSession.decryptMessage(encrypted)
        print("Received:", String(data: decrypted, encoding: .utf8)!)
    } catch {
        print("Decryption failed – packet rejected:", error)
    }
}

Key Rotation and Forward Secrecy

BitChat enforces periodic static key rotation via NostrIdentityBridge.swift (lines 70–95). This limits data encrypted under any single key, ensuring previously captured ciphertext remains secure even if current keys are compromised.

The rotation process:

  1. Generates new X25519.KeyPair
  2. Propagates new public key through mesh
  3. Re‑negotiates Noise handshake with all active peers
  4. Retires old keys after handshake completion

BLE Mesh Integration

The BLE transport layer treats SecureNoiseSession as an opaque encryption abstraction. CBPeripheral writes carry Noise‑encrypted payloads; delegate callbacks receive raw bytes for session decryption. This separation allows the mesh stack in BLEMeshManager to focus on routing and connection management while cryptographic guarantees remain enforced.

Per the project documentation in docs/BLE-ARCHITECTURE-V3.md, the Noise session sits at the transport layer boundary—below the message framing layer but above raw GATT operations.

Summary

  • Noise_XX pattern enables mutual authentication without prior key distribution, matching dynamic mesh discovery requirements
  • XChaCha20‑Poly1305 provides authenticated encryption with 192‑bit nonces, eliminating reuse risks
  • HKDF key derivation produces strictly separated txKey/rxKey pairs post‑handshake
  • SecureNoiseSession.swift centralizes all cryptographic state, isolating the BLE layer from algorithm details
  • Periodic key rotation in NostrIdentityBridge.swift maintains forward secrecy across long‑running mesh sessions

Frequently Asked Questions

Why does BitChat use Noise_XX instead of Noise_IK or Noise_XXfallback?

Noise_XX requires no pre‑shared or pre‑distributed static keys. In a BLE mesh where devices discover peers opportunistically, the initiator cannot know the responder's static key in advance. Noise_XX's three‑message pattern provides mutual authentication through in‑band static key transmission, fitting the decentralized mesh model where traditional PKI infrastructure is unavailable.

How does BitChat prevent nonce reuse when encrypting high volumes of mesh traffic?

XChaCha20‑Poly1305 uses 192‑bit (24‑byte) nonces randomly generated per message. This provides 2^192 possible nonce values—compared to 96‑bit in standard ChaCha20‑Poly1305—making accidental collision statistically impossible. Random nonce generation in encryptMessage(_:) removes the need for complex counter synchronization across mesh nodes.

What happens when a Noise decryption fails due to tampering or key mismatch?

The decryptMessage(_:) method throws NoiseError.decryptionFailed and the BLE layer discards the packet. The session is marked compromised, triggering rehandshake or connection termination depending on policy. This strict failure mode prevents padding oracle attacks and ensures no plaintext is ever returned from unverified ciphertext.

Where does BitChat generate and store the X25519 static keys used in Noise sessions?

Static key generation occurs in NostrIdentityBridge.swift using the project's Nostr identity infrastructure. Keys derive from the user's Nostr identity seed, enabling deterministic recovery across devices while maintaining cryptographic separation through domain‑specific derivation paths. Private keys remain in the iOS Secure Enclave when hardware support is available.

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 →