How BitChat Manages Group Invites and Key Rotation in Private Messaging

BitChat implements creator-signed group state objects over the Noise protocol, where invitations and key rotations are distributed as signed payloads containing epoch counters and member lists, ensuring only the group creator can modify membership or encryption keys.

The permissionlesstech/bitchat repository implements a privacy-focused group messaging system built on the Noise protocol. At its core, group management relies on cryptographically signed state objects that control member admission and symmetric key rotation through an epoch-based versioning system.

Group Invitation Flow

Creator-Signed Invitations

Group invitations originate exclusively from the creator through ChatGroupCoordinator.inviteMember(nickname:). Located in bitchat/ViewModels/ChatGroupCoordinator.swift, this method validates the target peer exists in the current context, then constructs a signed group payload using signedStatePayload(for:group:key:). This payload encapsulates the current group epoch, the complete member list (including the new invitee), and a cryptographic signature generated from the creator’s static signing key.

The invite logic ensures that only the creator possesses the private key necessary to authorize new members, preventing any participant from unilaterally expanding the group.

Transport Over Noise

Once constructed, the invitation transmits as a groupInvite message (type 0x06 defined in bitchat/Protocols/BitchatProtocol.swift). The payload travels 1-to-1 over an established Noise session, ensuring confidentiality and integrity during transit. This design guarantees that only the intended recipient can decrypt and process the invitation, intercepting passive surveillance and man-in-the-middle attempts.

Key Rotation Mechanism

Epoch Increment and Key Generation

When membership changes—specifically when the creator removes a member—or when explicit rotation is requested, the system invokes GroupStore.rotateKey(groupID:members:) from bitchat/Services/Groups/GroupStore.swift. This function performs three critical operations:

  • Increments the group's epoch counter to invalidate previous states
  • Replaces the member list with the remaining participants
  • Generates a fresh symmetric key (newKey) for subsequent message encryption

The updated BitchatGroup object persists these changes, creating a clean cryptographic boundary between old and new group compositions.

Broadcasting Key Updates

Following a successful rotation, the coordinator sends a key-update message (type 0x07 in GroupProtocol.swift) to all remaining members. Like invitations, this update uses distributeState with the creator's signature attached, forcing participants to discard the previous key and adopt the fresh one. The coordinator rebuilds the signed payload using signedStatePayload(for:group:key:) with the rotated group state and new symmetric key before transmission.

Security Architecture

Creator-Authority Verification

Because every state transition—whether inviting members or rotating keys—requires the creator's signature, the protocol maintains strict creator authority. Peers validate incoming states by verifying the signature against the creator's public key before applying any membership or key changes. This prevents compromised members from forging invites or injecting unauthorized key material.

Replay Protection via Epochs

The epoch counter embedded in each group state functions as a monotonic sequence number. When GroupProtocol.handleIncomingState processes incoming messages, it rejects any state carrying an epoch lower than the current local value. This mechanism prevents replay attacks where adversaries might attempt to restore old group compositions or retired encryption keys.

Implementation Examples

The following Swift code illustrates the core invitation and rotation flows:

// Inviting a new member (ChatGroupCoordinator.swift)
func inviteMember(nickname rawNickname: String) -> CommandResult {
    guard let peer = context.peers[rawNickname] else {
        return .error(message: String(localized: "system.group.peer_not_found"))
    }
    // Build a signed group state that includes the new member
    guard let payload = signedStatePayload(for: currentGroup, key: currentKey) else {
        return .error(message: String(localized: "system.group.invite_failed"))
    }
    // Send the invite (type .invite) over the Noise session
    distributeState(payload, group: currentGroup, excluding: [], type: .invite)
    return .ok(message: String(localized: "system.group.invited"))
}
// Rotating the group key after member removal (ChatGroupCoordinator.swift)
guard let (rotated, newKey) = context.groupStore.rotateKey(
        groupID: group.groupID,
        members: remainingMembers) else {
    return .error(message: String(localized: "system.group.rotate_failed"))
}

// Broadcast the new key (type .keyUpdate) to all remaining members
let payload = signedStatePayload(for: rotated, key: newKey)!
distributeState(payload, group: rotated, excluding: [], type: .keyUpdate)
// Message type definitions (GroupProtocol.swift)
enum GroupMessageType: UInt8 {
    case invite    = 0x06   // Creator-signed group state (invite)
    case keyUpdate = 0x07   // Creator-signed group state (key rotation)
}

The command-line interface for these operations registers through bitchat/Services/CommandProcessor.swift, exposing /group invite and related commands to the user interface.

Summary

  • Creator-exclusive authority: Only the group creator can sign invitations and key rotations via signedStatePayload, enforced by signature verification in GroupProtocol.
  • Epoch-based versioning: The epoch counter in GroupStore.rotateKey provides atomic key transitions and replay protection.
  • Noise transport security: Both groupInvite (0x06) and keyUpdate (0x07) messages transmit over established Noise sessions, ensuring confidentiality.
  • Atomic membership changes: Key rotation automatically triggers when members are removed, ensuring forward secrecy for excluded participants.

Frequently Asked Questions

How are group invitations authenticated?

Invitations carry a cryptographic signature generated by the creator's static signing key within signedStatePayload(for:group:key:). Recipients verify this signature in GroupProtocol.handleIncomingState before accepting the member list and group key, ensuring only the legitimate creator can authorize new participants.

What happens when a member is removed from a BitChat group?

Removal triggers GroupStore.rotateKey(groupID:members:), which increments the group epoch, generates a fresh symmetric key, and updates the member list. The creator then broadcasts a keyUpdate message (type 0x07) to all remaining members, who immediately switch to the new key and invalidate the old one, preventing the removed member from decrypting future messages.

How does BitChat prevent replay attacks on group state?

Each group state includes a monotonic epoch counter. When processing incoming states, GroupProtocol.handleIncomingState rejects any message with an epoch equal to or lower than the currently stored value. This ensures outdated invitations or key states cannot be replayed to restore previous group configurations.

Can non-creator members invite others or rotate keys?

No. The protocol strictly reserves invitation and rotation capabilities to the creator. Because both operations require a valid signature from the creator's private key—which only ChatGroupCoordinator possesses in the signing context—non-creator members lack the cryptographic material necessary to generate valid groupInvite or keyUpdate payloads that peers would accept.

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 →