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):
- Initiator → Responder: The initiator transmits its ephemeral public key (
e). - Responder → Initiator: The responder sends its ephemeral key (
e), performs theeeDiffie-Hellman operation, and transmits its static key (s) encrypted with theesDH result. - Initiator → Responder: The initiator sends its static key (
s) encrypted using theseDH 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 newNoiseHandshakeStatewithrole: .initiatorandpattern: .XX, returning the initial handshake bytes for transmission. - Handshake Processing: Incoming handshake data routes through
processHandshakeMessageWithResult(lines 813-846), which forwards bytes to the appropriateNoiseHandshakeState. Upon completion, the session marks itself as established and fires completion callbacks. - Data Transfer: Established sessions use
encrypt(_:for:)(lines 869-889) anddecrypt(_:from:)(lines 921-931) to AEAD-seal payloads using the derivedNoiseCipherState. - Automatic Rekeying: A background
rekeyTimerruns every minute to checksessionManager.getSessionsNeedingRekey()and triggersinitiateAutomaticRekey(lines 1069-1084) to establish new session keys. - Teardown: The
clearSession(for:)method removes specific sessions, whileclearEphemeralStateForPanic()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
sealCourierPayloadandopenCourierPayload(lines 446-474) to encrypt data to a known static key without forward secrecy. - Pre-key Envelopes: Implemented via
sealPrekeyPayloadandopenPrekeyPayload(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:
NoiseRateLimitergates handshake attempts and message traffic throughallowHandshakeandallowMessagechecks. - Message Size Validation:
NoiseSecurityValidator.validateMessageSizeenforces a 64 KiB payload limit to prevent denial-of-service. - Replay Protection:
NoiseCipherStateimplements 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, andNoiseSecurityErrorencode 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.swiftfeaturing the XX handshake, ChaCha20-Poly1305 encryption, and SHA-256 key derivation. - Session management through
NoiseSessionManagerhandles 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →