Bitchat implements the Noise Protocol Framework for end-to-end encryption on the BLE mesh transport. All private payloads on the mesh — messages, delivery acknowledgments, read receipts — travel inside an established Noise session as typed ciphertext. Intermediate relay nodes see only opaqueDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/permissionlesstech/bitchat/llms.txt
Use this file to discover all available pages before exploring further.
noiseEncrypted packets and cannot read the content. Two Noise patterns are in use: Noise XX for live sessions between connected peers, and Noise X for offline courier envelope seals.
The XX Pattern
Bitchat uses the three-message Noise XX handshake pattern for all live peer-to-peer sessions:| Property | Provided by XX |
|---|---|
| Mutual authentication | ✅ Both parties prove possession of their static key |
| Identity hiding | ✅ Static keys are transmitted only after the initial ephemeral exchange |
| Key-compromise impersonation resistance | ✅ Compromising one peer’s static key does not allow impersonating that peer to others |
| Forward secrecy | ✅ Ephemeral keys are generated fresh for each handshake |
Core Components
The Noise implementation is structured around three collaborating classes, as documented inBRING_THE_NOISE.md:
NoiseEncryptionService is the main entry point for all Noise operations. It holds the device’s long-term Curve25519 static identity key (stored in the iOS/macOS Keychain) and delegates per-peer session state to NoiseSessionManager. NoiseSession holds the active handshake state and, once established, the split send/receive cipher states.
Handshake Flow
The three-message XX handshake proceeds as follows: Step 1 — Initiator sends ephemeral key (message 1):ee and es), and uses the resulting mixed key material to encrypt its static public key. The responder’s identity is now hidden inside ciphertext that only the initiator can decrypt.
Step 3 — Initiator sends encrypted static key (message 3):
se), and sends its own encrypted static key. After this message, both parties have mutually authenticated and derived split transport cipher keys from the full handshake transcript.
The handshake hash — a running SHA-256 digest of the entire transcript — is captured at the split point and stored for channel binding. This ties the resulting session to the specific handshake that produced it.
Session Management
Sessions are indexed by peer ID and managed with thread-safe concurrent dispatch:- 24 hours (
sessionTimeout: 86400) — sessions older than this trigger renegotiation - 1 billion messages (
maxMessagesPerSession: 1_000_000_000) — well within the ChaCha20-Poly1305 nonce limit of 2⁶⁴ − 1
NoiseSessionManager detects the collision (crossed message 1 packets) and coordinates a deterministic yield. One side wins the role of initiator; the other cancels its attempt and responds. An unauthenticated inbound message 1 moves the existing transport into a receive-only “quarantine” state while the replacement handshake proves the claimed identity, then restores or discards the quarantined transport on a bounded timer.
Rate limiting: The implementation enforces per-peer and global handshake rate limits:
- Max handshakes per peer: 10 per minute
- Max global handshakes: 30 per minute
- Max messages per peer: 100 per second
- Max global messages: 500 per second
Noise X for Courier Seals
When no transport can deliver a private message promptly, Bitchat seals it into a courier envelope for physical delivery. Courier envelopes use the Noise X pattern — a one-way, single-message construction:Cryptographic Primitives
| Component | Algorithm |
|---|---|
| Diffie-Hellman | X25519 (Curve25519) |
| AEAD cipher | ChaCha20-Poly1305 (ChaChaPoly in CryptoKit) |
| Hash function | SHA-256 |
| Key derivation | HKDF-SHA256 |
Noise_XX_25519_ChaChaPoly_SHA256. Bitchat uses Apple’s CryptoKit for all primitive operations; there are no third-party cryptographic dependencies on the Noise path.
Replay protection: NoiseCipherState implements a sliding-window replay protection scheme:
- Tracks received nonces in a 1,024-message window
- Rejects any nonce that falls below the window floor (too old) or has already been seen
- Handles out-of-order delivery within the window
noiseEncrypted and noiseHandshake packet types are padded (toward 256/512/1024/2048-byte buckets, PKCS#7 style). Frames requiring more than 255 bytes of padding to reach a bucket are emitted unpadded. All other packet types travel at their natural length.
Error Types
The Noise layer surfaces error conditions throughNoiseError:
NoiseSecurityError:
nonceExceeded fires when a session’s nonce counter approaches the 4-byte limit used by the transport framing. replayDetected is thrown when the sliding-window replay check rejects a duplicate or out-of-window nonce. In NoiseSecurityError, sessionExhausted covers the session approaching its message-count ceiling, and messageTooLarge enforces the 64 KiB Noise specification maximum (maxMessageSize = 65535).
Protocol Version Negotiation
Bitchat implements protocol version negotiation to ensure compatibility between different client versions. Upon connection, peers exchange supported versions before beginning the Noise handshake:- Version Hello — the connecting peer sends a
versionHello(0x20) message advertising the versions it supports - Version Ack — the remote peer responds with
versionAck(0x21), selecting the highest mutually supported version - Graceful fallback — peers that do not send version negotiation messages are assumed to support protocol v1, preserving backward compatibility
ProtocolVersion.supportedVersions. Peers that negotiate incompatible versions receive a rejection message and disconnect gracefully without disrupting sessions with other peers.