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 ordering
  • senderSigningKey — the 32‑byte Ed25519 public key identifying the sender
  • senderNickname — human‑readable display name (plaintext within the encrypted envelope)
  • timestampMs — millisecond‑precision Unix timestamp
  • content — 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 groupID values
  • 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

  1. Envelope decoding (GroupMessageEnvelope.decode): Extracts groupID, epoch, nonce, and ciphertext from the binary payload
  2. AAD reconstruction: Recomputes groupID || epoch from envelope fields
  3. AEAD opening: Decrypts using the stored epoch key; authentication failure aborts processing
  4. TLV parsing: Extracts sender signing key, signature, and plaintext fields
  5. 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 || epoch provides 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:

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 →