# How BitChat Implements End‑to‑End Encrypted Group Chats: Architecture and Code Deep Dive

> Discover how BitChat implements end-to-end encrypted group chats using AEAD encryption and Ed25519 signatures. Explore the architecture and code in our deep dive.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: architecture
- Published: 2026-08-07

---

**BitChat implements group‑wide encrypted broadcasts using AEAD encryption (ChaCha20‑Poly1305) combined with Ed25519 signatures, with all cryptographic operations centralized in [`GroupProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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](https://github.com/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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:

```swift
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`.

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/Groups/GroupStore.swift) | Key management, group creation, epoch rotation |
| Test coverage | [`bitchatTests/Services/GroupProtocolTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/GroupProtocol.swift).

### Where are the epoch keys stored and rotated?

Epoch keys are managed by **[`GroupStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GroupStore.swift)**, which persists per‑group symmetric keys and handles epoch progression. The cryptographic layer in [`GroupProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/GroupProtocol.swift) remains stateless — it receives the epoch key as a parameter and performs no key management itself. This separation allows [`GroupStore.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/GroupStore.swift) or calling code) checks whether the signing key belongs to a current member before displaying the message in the UI.