How BitChat Implements End‑to‑End Encrypted Group Chats: Architecture and Code Deep Dive
BitChat implements group‑wide encrypted broadcasts using AEAD encryption (ChaCha20‑Poly1305) combined with Ed25519 signatures, with all cryptographic operations centralized in GroupProtocol.swift.
End‑to‑end encrypted group chats in BitChat follow a broadcast model where every message is authenticated and encrypted before transmission. The protocol, as implemented in the permissionlesstech/bitchat repository, prioritizes forward secrecy through epoch‑based key rotation, strict replay protection via AAD binding, and non‑repudiable sender authentication through deterministic signing. This article examines the complete flow from message construction to verification, with direct references to the source implementation.
The Three‑Stage Encryption Pipeline
BitChat's group chat encryption operates in three distinct stages: message construction, cryptographic signing, and AEAD sealing. Each stage produces an intermediate structure that feeds into the next, culminating in a binary GroupMessageEnvelope ready for network transmission.
Stage 1: Message Construction and TLV Encoding
The inner payload uses a TLV (type‑length‑value) structure that encodes five critical fields. This binary format ensures compact, unambiguous serialization without schema versioning issues.
The TLV contains:
messageID— a unique UUID for deduplication and message orderingsenderSigningKey— the 32‑byte Ed25519 public key identifying the sendersenderNickname— human‑readable display name (plaintext within the encrypted envelope)timestampMs— millisecond‑precision Unix timestampcontent— the actual message text
These fields are assembled by the calling code before cryptographic processing begins. The GroupTLV.swift helper (referenced but not shown in the core protocol file) handles the binary encoding and decoding of these fields.
Stage 2: Ed25519 Signing with Deterministic Content
Before encryption, the message content is signed to prevent tampering and provide sender authentication. The signing content is deterministic — every peer can reconstruct the exact byte sequence to verify the signature.
As defined in GroupProtocol.swift, the GroupCrypto.messageSigningContent function (lines 29‑36) concatenates:
domain_string || groupID || epoch || messageID || timestampMs || content
The domain string is a protocol‑level constant that prevents cross‑protocol signature replay. The clear‑text group ID and epoch are included in the signed material, binding the signature to a specific group and time period.
The signing closure returns a 64‑byte Ed25519 signature:
let sign: (Data) -> Data = { data in
aliceSigningPrivateKey.signature(for: data)
}
This signature is appended to the TLV before encryption, creating a signed‑then‑encrypted structure that protects both confidentiality and authenticity.
Stage 3: ChaCha20‑Poly1305 AEAD Sealing
The complete TLV (including signature) is encrypted using GroupCrypto.sealMessage (lines 48‑94). This implements ChaCha20‑Poly1305 in AEAD mode with carefully constructed additional authenticated data (AAD).
The AAD is the concatenation of groupID || epoch — two 32‑byte values. This binding serves critical security functions:
- Replay protection: A message from epoch 5 cannot be replayed in epoch 6 because the AAD mismatch causes authentication failure
- Cross‑group isolation: Identical ciphertexts in different groups produce different authentication tags due to distinct
groupIDvalues - Epoch‑based forward secrecy: Compromise of a future epoch key does not retroactively expose past messages
The sealed output includes a 24‑byte nonce, variable‑length ciphertext, and 16‑byte Poly1305 authentication tag. These are packed into a GroupMessageEnvelope with type identifier 0x25.
// Sender: build and seal a group message
let sealed = try GroupCrypto.sealMessage(
content: "🚀 Release v2.0!",
messageID: UUID().uuidString,
senderNickname: "alice",
senderSigningKey: aliceSigningKey, // 32-byte Ed25519 public key
timestampMs: Date().millisecondsSince1970,
groupID: groupID, // 32-byte group identifier
epoch: currentEpoch,
key: epochKey, // 32-byte symmetric key for this epoch
sign: { data in aliceSigningPrivateKey.signature(for: data) }
)
Decryption and Verification Flow
Receivers process incoming 0x25 payloads through GroupCrypto.openMessage (lines 97‑168 in GroupProtocol.swift). The verification pipeline inverts the sender operations with strict validation at each step.
Step‑by‑Step Opening Process
- Envelope decoding (
GroupMessageEnvelope.decode): ExtractsgroupID,epoch,nonce, and ciphertext from the binary payload - AAD reconstruction: Recomputes
groupID || epochfrom envelope fields - AEAD opening: Decrypts using the stored epoch key; authentication failure aborts processing
- TLV parsing: Extracts sender signing key, signature, and plaintext fields
- Signature verification: Recomputes the deterministic signing content and validates the 64‑byte Ed25519 signature
// Receiver: decode and verify a group message
let envelope = GroupMessageEnvelope.decode(sealed)!
let plaintext = try GroupCrypto.openMessage(envelope, key: epochKey)
print("\(plaintext.senderNickname): \(plaintext.content)")
Trust and Membership Boundaries
The cryptographic primitive explicitly does not verify group membership. The openMessage function returns successfully for any message with valid signatures and epoch keys, regardless of whether the sender remains in the group. This is a deliberate architectural choice: roster validation is the caller's responsibility, typically handled by the group state manager in GroupStore.swift.
This separation allows the cryptographic layer to remain stateless and testable, while membership policy evolves independently.
Key Source Files and Functions
| Component | File Path | Responsibility |
|---|---|---|
| Core protocol | bitchat/Services/Groups/GroupProtocol.swift |
Envelope encoding, signing, encryption, verification |
| Envelope codec | GroupMessageEnvelope.encode() / .decode() |
Lines 351‑391; binary serialization of sealed messages |
| Signing content | GroupCrypto.messageSigningContent |
Lines 29‑36; deterministic byte sequence construction |
| Signature verification | GroupCrypto.verify |
Lines 39‑42; Ed25519 signature validation |
| Sealing (encryption) | GroupCrypto.sealMessage |
Lines 48‑94; ChaChaPoly AEAD with AAD binding |
| Opening (decryption) | GroupCrypto.openMessage |
Lines 97‑168; full decryption and verification pipeline |
| Epoch key storage | bitchat/Services/Groups/GroupStore.swift |
Key management, group creation, epoch rotation |
| Test coverage | bitchatTests/Services/GroupProtocolTests.swift |
Sealing/opening, signature validation, replay protection |
Security Properties Summary
BitChat's group chat design provides four cryptographic guarantees through specific mechanisms:
| Property | Mechanism |
|---|---|
| Confidentiality | ChaCha20‑Poly1305 encryption with 256‑bit epoch keys |
| Integrity | Poly1305 authentication tag; verification failure aborts processing |
| Authenticity | Ed25519 signatures over deterministic content including sender identity |
| Replay protection | Unique messageID plus AAD binding to groupID || epoch |
The epoch‑based key rotation (managed in GroupStore.swift) provides forward secrecy: compromise of the current epoch key exposes only messages from that epoch, not historical conversations.
Summary
- Group chats in BitChat use a broadcast encryption model where every message is signed with Ed25519 and encrypted with ChaCha20‑Poly1305
- The implementation lives entirely in
GroupProtocol.swift, with envelope encoding at lines 351‑391 and cryptographic operations spanning lines 29‑168 - AAD constructed as
groupID || epochprovides binding to specific groups and time periods, preventing cross‑group and cross‑epoch replay attacks - Deterministic signing content includes group metadata, enabling any peer to verify signatures without trusted infrastructure
- Membership verification is intentionally separated from cryptographic operations, handled by the group state manager rather than the core protocol
Frequently Asked Questions
What encryption algorithm does BitChat use for group chats?
BitChat uses ChaCha20‑Poly1305 in AEAD mode for encryption and authentication of message content, combined with Ed25519 for digital signatures. This construction appears in GroupProtocol.swift lines 48‑94 (sealMessage) and lines 97‑168 (openMessage). The ChaCha20 stream cipher provides constant‑time performance across device classes, while Poly1305 delivers fast, secure message authentication.
How does BitChat prevent replay attacks in group conversations?
Replay protection operates through dual mechanisms: the AEAD additional authenticated data (AAD) binds each ciphertext to a specific groupID and epoch, so replaying a message from epoch 5 into epoch 6 causes authentication failure. Additionally, the messageID field enables application‑layer deduplication. These protections are implemented in sealMessage and openMessage within GroupProtocol.swift.
Where are the epoch keys stored and rotated?
Epoch keys are managed by GroupStore.swift, which persists per‑group symmetric keys and handles epoch progression. The cryptographic layer in GroupProtocol.swift remains stateless — it receives the epoch key as a parameter and performs no key management itself. This separation allows GroupStore.swift to implement arbitrary rotation policies without modifying core cryptographic code.
Why doesn't the cryptographic layer verify group membership?
The openMessage function intentionally returns successfully for any cryptographically valid message, leaving roster validation to the caller. This design keeps the cryptographic primitive stateless and testable, while allowing flexible membership policies. The group state manager (typically in GroupStore.swift or calling code) checks whether the signing key belongs to a current member before displaying the message in the UI.
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 →