How Private Chats Are End-to-End Encrypted in BitChat: Noise Protocol and XChaCha20-Poly1305 Implementation
BitChat encrypts private chats using a Noise IK handshake to establish a 32-byte shared secret, followed by XChaCha20-Poly1305 authenticated encryption for every message, ensuring only the two participants can read the plaintext.
BitChat, the open-source decentralized messaging framework maintained by permissionlesstech/bitchat, implements end-to-end encryption for direct messages through a rigorous cryptographic architecture. The system combines asymmetric key agreement via the Noise protocol with symmetric authenticated encryption to protect message confidentiality and integrity across untrusted mesh network relays.
Cryptographic Architecture Overview
BitChat’s private chat encryption relies on a hybrid cryptosystem that separates key exchange from data encryption. Each user maintains a long-term Curve25519 identity key pair stored within their PeerID, exchanged through the Nostr identity system. When two peers initiate a private conversation, they perform a Noise IK handshake to derive a unique session key, which then feeds into XChaCha20-Poly1305 for per-message encryption.
This design ensures that the underlying mesh network relays handle only opaque ciphertext. The encryption implementation spans several critical components in the codebase, from low-level cryptographic primitives in BitFoundation to protocol logic in NostrProtocol.swift.
Key Exchange and Session Establishment
Long-Term Identity Keys
Each BitChat user generates a persistent Curve25519 key pair using Curve25519.KeyAgreement.PrivateKey(). The public component is embedded in the user’s PeerID and distributed via Nostr metadata events. As referenced in BitFoundation/Tests/BitFoundationTests/TestHelpers.swift, these keys serve as the static identity credentials for all cryptographic handshakes.
The Noise IK Handshake
When a private chat session begins, the two peers execute a Noise IK handshake pattern implemented in bitchat/Nostr/NostrProtocol.swift. This handshake mixes the initiator’s ephemeral key with the responder’s long-term public key to produce a shared secret (NoiseKey) unique to that session.
The handshake follows the Noise protocol specification: ephemeral-static Diffie-Hellman operations generate a 32-byte pre-master secret, which undergoes key derivation to produce the final symmetric session key. This process occurs before any message content transmits across the network, establishing forward secrecy for the conversation.
Message Encryption and Decryption
XChaCha20-Poly1305 Compatibility Layer
BitChat implements authenticated encryption through XChaCha20Poly1305Compat, located in bitchat/Nostr/XChaCha20Poly1305Compat.swift. This wrapper extends Apple’s CryptoKit ChaChaPoly to support the extended 24-byte nonce variant required for random nonce safety.
Internally, the seal method runs HChaCha20 on the first 16 bytes of the 24-byte nonce to derive a 256-bit sub-key. The remaining 8 bytes, concatenated with four zero bytes, form the 12-byte nonce passed to the underlying ChaCha20-Poly1305 engine. This construction prevents nonce collision vulnerabilities while maintaining interoperability with standard cryptographic libraries.
Encrypting Private Messages
Every BitchatMessage marked with isPrivate = true undergoes encryption before transmission. In bitchat/Nostr/NostrProtocol.swift at line 619, the ChatTransportEventCoordinator invokes:
let sealedBox = try XChaCha20Poly1305Compat.seal(
plaintext: messageData,
key: sessionKey,
nonce24: randomNonce24)
This call produces a ciphertext and 16-byte authentication tag. The transport layer bundles these with the 24-byte nonce into the mesh packet, ensuring relays process only unidentifiable binary blobs.
Decrypting and Verification
Upon receipt, the recipient extracts the ciphertext, tag, and nonce from the packet. At line 656 in bitchat/Nostr/NostrProtocol.swift, the protocol handler executes:
let plaintext = try XChaCha20Poly1305Compat.open(
ciphertext: sealedBox.ciphertext,
tag: sealedBox.tag,
key: sessionKey,
nonce24: receivedNonce)
If the authentication tag verification fails—indicating tampering or key mismatch—the open method throws a decryption error and the message is discarded. Successful verification yields the original UTF-8 message content for display.
Storage, UI Indicators, and Key Lifecycle
Persisting Private Conversations
Decrypted messages are stored in ConversationStore via bitchat/App/PrivateConversationModels.swift. The storage layer segregates private conversations by peer ID, ensuring that plaintext never persists without the corresponding session context. The BitchatMessage struct maintains the isPrivate flag throughout the object lifecycle to enforce access controls.
UI Encryption Status
While the Noise handshake establishes the session key, bitchat/Views/ContentSheetViews.swift renders an "encrypted" caption to indicate the transitional state. Once key agreement completes and the first message decrypts successfully, the UI updates to show the verified lock status, providing users with immediate visual feedback about the conversation’s security posture.
Handling Key Rotation
When users rotate their long-term identity keys, BitFoundation/Sources/BitFoundation/PeerIDRotation.swift manages the cryptographic migration. The PeerIDRotation class recomputes rotation secrets and re-encrypts existing private conversation metadata under the new key material without exposing intermediate plaintext to disk or memory. This ensures continuity of chat history while maintaining the forward secrecy properties of the original session keys.
Summary
- BitChat implements end-to-end encryption for private chats using a Noise IK handshake followed by XChaCha20-Poly1305 authenticated encryption.
- Long-term Curve25519 identity keys are exchanged via Nostr and stored in the PeerID structure.
- The
XChaCha20Poly1305Compat.seal()andopen()methods inbitchat/Nostr/XChaCha20Poly1305Compat.swifthandle symmetric encryption using a 24-byte nonce and HChaCha20 subkey derivation. - Session establishment and message processing occur in
bitchat/Nostr/NostrProtocol.swift, specifically at lines 619 and 656 for encryption and decryption calls. - Key rotation is managed by
PeerIDRotation.swift, allowing users to change identity keys without losing encrypted conversation history.
Frequently Asked Questions
What cryptographic protocols does BitChat use for private chat encryption?
BitChat uses the Noise protocol framework (specifically the IK handshake pattern) for initial key exchange, combined with XChaCha20-Poly1305 for symmetric authenticated encryption. This pairing provides both authenticated key establishment and modern AEAD (Authenticated Encryption with Associated Data) security guarantees for message payloads.
How does BitChat establish shared secrets between two peers?
The two peers perform a Noise IK handshake using their long-term Curve25519 keys. The initiator generates an ephemeral key pair and performs Diffie-Hellman operations with the responder’s static public key. The resulting 32-byte shared secret undergoes HKDF key derivation to produce the session-specific symmetric key used for XChaCha20-Poly1305 operations.
What happens when a user rotates their identity key in BitChat?
The BitFoundation/Sources/BitFoundation/PeerIDRotation.swift module handles key rotation by recomputing rotation secrets under the new Curve25519 key pair. Existing private messages are cryptographically migrated to the new key material without decrypting and re-encrypting the plaintext, ensuring no temporary exposure of sensitive conversation content during the rotation process.
Does BitChat provide forward secrecy for private messages?
Yes, the Noise IK handshake provides forward secrecy by incorporating ephemeral keys into the session key derivation. Even if a long-term private key is compromised in the future, past session keys cannot be reconstructed because they depend on ephemeral Diffie-Hellman exchanges that are deleted immediately after the handshake completes.
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 →