How BitChat Implements End-to-End Encryption Using the Noise Protocol

BitChat implements end-to-end encryption using a self-contained Noise Protocol Framework that employs the XX handshake pattern with Curve25519 ephemeral keys, ChaCha20-Poly1305 authenticated encryption, and SHA-256 key derivation, managed through a session-based architecture that supports mutual authentication, automatic rekeying, and store-and-forward messaging.

BitChat, an open-source peer-to-peer messaging application maintained by permissionlesstech, secures all communications through a comprehensive implementation of the Noise Protocol Framework. The architecture combines low-level cryptographic primitives with high-level session management to provide forward-secret, mutually-authenticated channels over Bluetooth Low Energy (BLE) and other transports.

Core Cryptographic Components

The implementation spans three primary Swift files that separate concerns between raw cryptography, session state, and application-facing APIs.

NoiseProtocol.swift

The file bitchat/Noise/NoiseProtocol.swift contains the complete cryptographic implementation, including the NoiseCipherState class (lines 33-88) which handles ChaCha20-Poly1305 encryption with nonce tracking and sliding-window replay protection. This component also defines NoiseSymmetricState for key derivation during handshakes and NoiseHandshakeState (line 541) to drive the full handshake pattern logic.

NoiseEncryptionService.swift

Located at bitchat/Services/NoiseEncryptionService.swift, this class provides the public API used by the rest of the application. It manages the device's static identity keys, creates and maintains the session manager, and exposes methods including initiateHandshake, encrypt, decrypt, and envelope helpers (lines 12-110).

NoiseSession.swift

The bitchat/Noise/NoiseSession.swift file defines NoiseSessionManager, which maintains a mapping of PeerID to NoiseSession objects. Each session wraps a NoiseHandshakeState and provides lifecycle management for re-keying, recovery, and rate-limiting enforcement.

The XX Handshake Pattern

BitChat uses the Noise XX pattern for mutual authentication, which proceeds through three distinct message exchanges documented in the file header of NoiseProtocol.swift (lines 38-45):

  1. Initiator → Responder: The initiator transmits its ephemeral public key (e).
  2. Responder → Initiator: The responder sends its ephemeral key (e), performs the ee Diffie-Hellman operation, and transmits its static key (s) encrypted with the es DH result.
  3. Initiator → Responder: The initiator sends its static key (s) encrypted using the se DH result.

The state machine inside NoiseHandshakeState.writeMessage and readMessage performs the necessary DH calculations and updates the symmetric state accordingly. The protocol identifier string follows the standard format: Noise_XX_25519_ChaChaPoly_SHA256.

Session Lifecycle Management

The NoiseEncryptionService orchestrates a complete lifecycle for encrypted sessions:

  • Creation: During initialization (lines 54-63), the service loads or generates the device's static Curve25519 key pair and Ed25519 signing key from the keychain, then instantiates a NoiseSessionManager.
  • Handshake Initiation: The initiateHandshake(with:) method (lines 998-1018) requests the session manager to create a new NoiseHandshakeState with role: .initiator and pattern: .XX, returning the initial handshake bytes for transmission.
  • Handshake Processing: Incoming handshake data routes through processHandshakeMessageWithResult (lines 813-846), which forwards bytes to the appropriate NoiseHandshakeState. Upon completion, the session marks itself as established and fires completion callbacks.
  • Data Transfer: Established sessions use encrypt(_:for:) (lines 869-889) and decrypt(_:from:) (lines 921-931) to AEAD-seal payloads using the derived NoiseCipherState.
  • Automatic Rekeying: A background rekeyTimer runs every minute to check sessionManager.getSessionsNeedingRekey() and triggers initiateAutomaticRekey (lines 1069-1084) to establish new session keys.
  • Teardown: The clearSession(for:) method removes specific sessions, while clearEphemeralStateForPanic() wipes all session state during security events.

Identity Keys and Fingerprinting

BitChat persists long-term identity keys in the iOS keychain through helper methods like generateAndSaveNoiseKey and generateAndSaveSigningKey (lines 664-685). Each peer is identified by a fingerprint calculated as the SHA-256 hash of its static public key using remoteStaticKey.rawRepresentation.sha256Fingerprint().

The NoiseEncryptionService maintains bidirectional lookup dictionaries (peerFingerprints and fingerprintToPeerID) for fast peer identification during handshake processing (lines 1023-1043).

Store-and-Forward Envelopes

When peers are offline, BitChat supports asynchronous messaging through two one-way envelope types implemented using the Noise X pattern:

  • Courier Envelopes: Use sealCourierPayload and openCourierPayload (lines 446-474) to encrypt data to a known static key without forward secrecy.
  • Pre-key Envelopes: Implemented via sealPrekeyPayload and openPrekeyPayload (lines 992-1020), these provide forward secrecy by encrypting to a one-time pre-key that is consumed after decryption.

Both envelope types use NoiseHandshakeState with pattern .X and include domain-separated prologues (bitchat-courier-v1 or bitchat-prekey-v1) to prevent cross-protocol attacks.

Security Features and Validation

The implementation includes multiple defensive layers:

  • Rate Limiting: NoiseRateLimiter gates handshake attempts and message traffic through allowHandshake and allowMessage checks.
  • Message Size Validation: NoiseSecurityValidator.validateMessageSize enforces a 64 KiB payload limit to prevent denial-of-service.
  • Replay Protection: NoiseCipherState implements a sliding-window nonce tracking mechanism (lines 165-188) to reject duplicate or out-of-order messages.
  • Structured Error Handling: Detailed enums including NoiseEncryptionError, NoiseSessionError, and NoiseSecurityError encode specific failure modes for debugging and user feedback.

Implementation Examples

Initializing the Encryption Service

let keychain = KeychainManager()               // Conforms to KeychainManagerProtocol
let noiseService = NoiseEncryptionService(keychain: keychain)
// Loads or generates static Noise and Ed25519 keys (lines 45-86)

Initiating a Secure Handshake

do {
    let handshakeData = try noiseService.initiateHandshake(with: peerID)
    transport.send(handshakeData)               // BLEService or other transport
} catch {
    // Handle handshakeRequired, rate-limit, or other errors
}

Encrypting Application Data

do {
    let encrypted = try noiseService.encrypt(messageData, for: peerID)
    transport.send(encrypted)
} catch NoiseEncryptionError.handshakeRequired {
    // Trigger a new handshake before retrying
}

Decrypting Incoming Packets

do {
    let plaintext = try noiseService.decrypt(packetData, from: peerID)
    // Pass plaintext to higher-level message parser
} catch {
    // May indicate tampering, replay, or missing session
}

Sending Offline Messages

let courierPayload = try noiseService.sealCourierPayload(
    data, 
    recipientStaticKey: otherPeerStaticKey
)
// Creates one-way envelope using X pattern (lines 446-456)

Summary

  • BitChat implements a complete Noise Protocol stack in NoiseProtocol.swift featuring the XX handshake, ChaCha20-Poly1305 encryption, and SHA-256 key derivation.
  • Session management through NoiseSessionManager handles per-peer state, automatic rekeying every minute, and secure teardown.
  • Mutual authentication derives from the XX pattern's three-phase handshake that exchanges and validates static public keys.
  • Store-and-forward messaging uses Courier and Pre-key envelopes with domain-separated prologues to support offline communication.
  • Security controls include rate limiting, 64 KiB message size validation, sliding-window replay protection, and comprehensive error handling.

Frequently Asked Questions

What Noise handshake pattern does BitChat use for peer authentication?

BitChat uses the Noise XX pattern implemented in NoiseHandshakeState (line 541 of NoiseProtocol.swift). This pattern provides mutual authentication and forward secrecy through a three-message exchange where both parties transmit ephemeral keys first, followed by encrypted static keys.

How does BitChat encrypt messages after the initial handshake completes?

After handshake completion, BitChat uses the derived keys to initialize a NoiseCipherState object (lines 33-88). The NoiseEncryptionService then routes calls to encrypt(_:for:) and decrypt(_:from:) through the session manager, which applies ChaCha20-Poly1305 AEAD encryption with automatic nonce tracking and replay protection via a sliding window mechanism.

What are Courier and Pre-key envelopes in BitChat?

Courier envelopes (sealCourierPayload, lines 446-474) allow one-way encryption to a known static key without forward secrecy for basic store-and-forward scenarios. Pre-key envelopes (sealPrekeyPayload, lines 992-1020) provide forward secrecy by encrypting to a one-time pre-key that is consumed after decryption. Both use the Noise X pattern with domain-specific prologues to prevent cross-protocol attacks.

How does BitChat prevent replay attacks and ensure message freshness?

Replay protection is built into NoiseCipherState through a sliding-window nonce tracking system (lines 165-188) that rejects duplicate or out-of-order messages. Additionally, the NoiseRateLimiter class gates incoming handshakes and message traffic to prevent flooding attacks, while NoiseSecurityValidator enforces strict 64 KiB message size limits.

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 →