How BitChat Implements the Noise Protocol for End-to-End Encrypted P2P Communication

BitChat implements a complete Noise Protocol Framework using the XX handshake pattern with Curve25519 elliptic curve Diffie-Hellman, ChaCha20-Poly1305 authenticated encryption, and SHA-256 key derivation to secure every peer-to-peer channel with mutual authentication and forward secrecy.

BitChat leverages a self-contained Swift implementation of the Noise Protocol Framework to secure all peer-to-peer (P2P) messaging without external cryptographic dependencies. The architecture centers on three core files—bitchat/Noise/NoiseProtocol.swift, bitchat/Services/NoiseEncryptionService.swift, and bitchat/Noise/NoiseSession.swift—which handle handshake negotiations, symmetric encryption, and session lifecycle management. This approach follows the official Noise specification while adding BitChat-specific features such as pre-key envelopes, courier envelopes, and automatic rekey timers.

Core Cryptographic Architecture

The implementation splits responsibilities across distinct layers to separate low-level cryptographic primitives from high-level session management.

Low-Level Primitives in NoiseProtocol.swift

The file bitchat/Noise/NoiseProtocol.swift contains the full cryptographic stack. NoiseProtocolName constructs the protocol identifier string Noise_XX_25519_ChaChaPoly_SHA256, defining the specific cipher suite used throughout the application.

NoiseCipherState (lines 33-88) handles symmetric encryption using ChaCha20-Poly1305, tracks nonces, and implements a sliding-window replay-protection mechanism (lines 165-188) to reject duplicate or out-of-order messages. NoiseSymmetricState manages the handshake transcript hash and key derivation during the initial negotiation.

NoiseHandshakeState (line 541) drives the complete handshake state machine for patterns including XX, IK, NK, and X. It executes Diffie-Hellman calculations, processes message writes and reads, and validates public keys according to the Noise specification.

High-Level Service Interface

NoiseEncryptionService in bitchat/Services/NoiseEncryptionService.swift (lines 12-110) provides the public API that the rest of the application uses. It loads static identity keys from the iOS Keychain, instantiates the NoiseSessionManager, and exposes methods for handshake initiation, encryption, decryption, and envelope creation.

Session State Management

NoiseSessionManager maintains a map of PeerID → NoiseSession, where each NoiseSession wraps a NoiseHandshakeState. This component handles session creation, lookup, rekeying, and teardown. When the app receives encrypted data, the session manager routes it to the appropriate cryptographic context based on the peer identifier.

The XX Handshake Pattern

BitChat uses the Noise XX pattern for its standard P2P communication, providing mutual authentication and forward secrecy through a three-message handshake.

Three-Message Handshake Flow

The handshake documented in NoiseProtocol.swift (lines 38-45) proceeds as follows:

  1. Initiator → Responder: Sends an ephemeral public key (e).
  2. Responder → Initiator: Sends an ephemeral public key (e), performs ephemeral-ephemeral DH (ee), encrypts its static public key (s), and sends ephemeral-static DH (es).
  3. Initiator → Responder: Sends its static public key (s) encrypted with the responder's static key (se DH).

Upon completion, both parties derive shared symmetric keys for the transport phase.

State Machine Implementation

The actual state machine lives inside NoiseHandshakeState.writeMessage and readMessage. These methods perform the necessary DH operations (ee, es, se), update the NoiseSymmetricState with handshake transcript hashes, and transition between handshake states until the session is established.

Session Lifecycle and Key Management

BitChat's session management ensures long-lived connections remain secure through automatic key rotation and persistent identity verification.

Identity Key Persistence

The service generates and stores two key pairs in the iOS Keychain:

  • A static Noise key (Curve25519) for Noise handshakes.
  • An Ed25519 signing key for message authentication.

The method generateAndSaveNoiseKey (lines 664-685) creates these keys if they don't exist, while generateAndSaveSigningKey handles the Ed25519 material.

Automatic Rekeying

A background rekeyTimer (lines 1069-1084) runs every minute to check sessionManager.getSessionsNeedingRekey(). When sessions exceed their key lifetime, the service triggers initiateAutomaticRekey to perform a new XX handshake transparently, ensuring forward secrecy is maintained over time.

Peer Fingerprinting

BitChat identifies peers using the SHA-256 hash of their static public key (remoteStaticKey.rawRepresentation.sha256Fingerprint()). The service maintains two dictionaries—peerFingerprints and fingerprintToPeerID—for fast bidirectional lookup (lines 1023-1043).

Asynchronous Messaging Envelopes

When peers are offline, BitChat supports store-and-forward messaging through one-way Noise envelopes that don't require bidirectional communication.

Courier Envelopes (X Pattern)

Courier envelopes use the Noise X pattern (initiator-only) to encrypt data to a known static public key without performing a handshake. This provides confidentiality but no forward secrecy. The implementation in sealCourierPayload and openCourierPayload (lines 446-474) uses a prologue string bitchat-courier-v1 to domain-separate these messages from standard handshakes.

Pre-Key Envelopes

Pre-key envelopes also use the Noise X pattern but incorporate one-time pre-keys to achieve forward secrecy. The sender encrypts to the recipient's static key and a one-time ephemeral pre-key; once decrypted, the pre-key is consumed and cannot be reused. Implementation resides in sealPrekeyPayload and openPrekeyPayload (lines 992-1020) with the prologue bitchat-prekey-v1.

Security Controls

Multiple defense mechanisms protect against common attacks on encrypted messaging systems.

Replay Protection

NoiseCipherState implements a sliding-window nonce tracking system (lines 165-188) that rejects any message with a nonce below the current window or duplicate to previously received nonces. This prevents replay attacks where an adversary resends captured ciphertext.

Rate Limiting and Validation

NoiseRateLimiter gates handshake attempts and message traffic through allowHandshake and allowMessage checks. NoiseSecurityValidator.validateMessageSize enforces a 64 KiB maximum payload size to prevent denial-of-service via memory exhaustion.

Implementation Examples

Initializing the Encryption Service

let keychain = KeychainManager()               // Conforms to KeychainManagerProtocol
let noiseService = NoiseEncryptionService(keychain: keychain)

This loads or generates the static Noise key and Ed25519 signing key (lines 45-86), making the service ready for P2P communication.

Initiating a Handshake

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

The method calls sessionManager.initiateHandshake (lines 997-1018), constructing a NoiseHandshakeState with role .initiator and pattern .XX, and returns the raw handshake bytes for transport.

Encrypting and Decrypting Messages

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

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

Encryption validates session existence (line 882) before delegating to sessionManager.encrypt. Decryption uses sessionManager.decryptWithSessionGeneration (lines 931-964) to handle both established sessions and handshake messages.

Sending Store-and-Forward Messages

// Courier envelope (no forward secrecy)
let courierPayload = try noiseService.sealCourierPayload(data, recipientStaticKey: otherPeerStaticKey)

// Pre-key envelope (with forward secrecy)
let prekeyPayload = try noiseService.sealPrekeyPayload(data, recipientStaticKey: otherPeerStaticKey, prekey: oneTimePrekey)

These methods create one-way envelopes using the X pattern with appropriate prologue strings to distinguish envelope types.

Summary

  • BitChat implements the complete Noise_XX_25519_ChaChaPoly_SHA256 protocol in NoiseProtocol.swift, providing mutual authentication and forward secrecy for P2P channels.
  • NoiseEncryptionService abstracts keychain access, session management, and cryptographic operations behind a clean Swift API used by the transport layer.
  • Automatic rekeying and sliding-window replay protection ensure long-lived sessions remain secure against key compromise and message replay.
  • Courier and Pre-key envelopes enable encrypted store-and-forward messaging when peers are offline, with Pre-key envelopes providing forward secrecy through one-time keys.
  • All cryptographic state is managed through NoiseSessionManager and NoiseSession, which track per-peer handshake states and enforce rate limits and message size validation.

Frequently Asked Questions

What Noise pattern does BitChat use for standard P2P communication?

BitChat uses the Noise XX pattern for standard peer-to-peer channels. This three-message handshake provides mutual authentication (both parties learn each other's static public keys) and forward secrecy (ephemeral keys are deleted after the handshake). The implementation resides in NoiseHandshakeState within bitchat/Noise/NoiseProtocol.swift.

How does BitChat provide forward secrecy in store-and-forward messaging?

For asynchronous messaging, BitChat offers two envelope types. Courier envelopes use the X pattern without forward secrecy, encrypting directly to a static key. Pre-key envelopes also use the X pattern but include a one-time ephemeral pre-key; once the recipient decrypts the message, the pre-key is consumed and deleted, ensuring the encrypted data cannot be decrypted later even if the static key is compromised.

What happens when a Noise session requires rekeying?

A background timer (rekeyTimer) checks for sessions needing rotation every minute. When triggered, NoiseEncryptionService calls initiateAutomaticRekey to perform a new XX handshake transparently between peers. This derives new symmetric keys without user intervention, maintaining forward secrecy over long-lived connections.

How are peer identities verified in BitChat's Noise implementation?

BitChat generates a fingerprint for each peer by taking the SHA-256 hash of their static Curve25519 public key. These fingerprints are stored in bidirectional lookup dictionaries (peerFingerprints and fingerprintToPeerID) within the encryption service. During the XX handshake, the static public keys are transmitted encrypted and authenticated, allowing each peer to verify the other's identity against previously stored fingerprints or out-of-band verification.

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 →