# How Private Chats Are End-to-End Encrypted in BitChat: Noise Protocol and XChaCha20-Poly1305 Implementation

> Discover how BitChat secures private chats with Noise IK handshake and XChaCha20-Poly1305 encryption. Learn how end-to-end encryption ensures your messages are unreadable to others.

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

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) at **line 619**, the `ChatTransportEventCoordinator` invokes:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift), the protocol handler executes:

```swift
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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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()` and `open()` methods in [`bitchat/Nostr/XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/XChaCha20Poly1305Compat.swift) handle symmetric encryption using a 24-byte nonce and HChaCha20 subkey derivation.
- Session establishment and message processing occur in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift), specifically at lines 619 and 656 for encryption and decryption calls.
- **Key rotation** is managed by [`PeerIDRotation.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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.