# How BitChat Implements Noise Protocol Encryption in Its Mesh Network

> Discover how BitChat implements Noise Protocol encryption in its mesh network. Learn about its use of Curve25519, ChaCha20-Poly1305, and SHA-256 for secure communication.

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

---

**BitChat implements Noise Protocol encryption using the Noise XX handshake pattern with Curve25519 key exchange, ChaCha20-Poly1305 authenticated encryption, and SHA-256 fingerprints, orchestrated through the `NoiseEncryptionService` that sits between the BLE transport layer and chat logic.**

The **permissionlesstech/bitchat** repository provides a peer-to-peer mesh messaging application that secures all communications using the Noise Protocol framework. Understanding how Noise Protocol encryption is implemented within this Swift codebase reveals a sophisticated combination of static identity keys, per-peer session management, and periodic rekeying to maintain forward secrecy. The implementation centers on the `NoiseEncryptionService` class, which abstracts cryptographic operations away from the underlying Bluetooth Low Energy transport.

## Cryptographic Identity and Key Management

BitChat establishes persistent identities using two distinct key pairs that survive application restarts and form the root of trust for all mesh communications.

### Static Noise Keys

Each device maintains a **static Curve25519 key pair** (`noiseStaticKey`) that serves as its long-term identity within the mesh. In [`bitchat/Services/NoiseEncryptionService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NoiseEncryptionService.swift) (lines 45-66), the initializer loads this key from the iOS Keychain or generates a new pair if none exists. This static key is essential for the Noise XX handshake pattern, allowing peers to authenticate each other across session restarts.

### Ed25519 Signing Keys

Complementing the Noise key, the service maintains an **Ed25519 signing key** (`ed25519SigningKey`) for cryptographic binding (lines 58-61). This key signs announcements and binds the static Noise key to the user's identity, preventing man-in-the-middle attacks during the initial discovery phase. Both keys are persisted via the `KeychainManagerProtocol` and their public components are advertised in `AnnounceV2Packet` structures during BLE discovery.

## Session Lifecycle and the XX Handshake

The `NoiseEncryptionService` delegates per-peer state management to a `NoiseSessionManager`, which handles the cryptographic negotiation between devices.

### Lazy Session Creation

Sessions are instantiated on-demand rather than pre-allocated. When a peer first attempts to send a protected message, the `onHandshakeRequired` callback triggers (lines 86-92), signaling the transport layer to initiate the handshake process. This lazy approach conserves resources in large mesh networks where not all discovered peers require immediate encrypted channels.

### Handshake Execution and Rekeying

The Noise XX pattern executes a Diffie-Hellman exchange using **X25519** to derive a shared secret, then uses HKDF to generate symmetric encryption keys. The implementation leverages **CryptoKit's** `ChaCha20Poly1305` primitive for authenticated encryption.

Forward secrecy is enforced through aggressive rekeying. A `rekeyTimer` fires every 60 seconds (lines 80-82), triggering new handshakes that replace existing session keys. This ensures that compromising a device does not expose historical traffic.

## Message Encryption and Decryption Flow

Once a session is established, the service provides transparent encryption APIs that wrap payloads for BLE transmission.

### Sending Encrypted Messages

When transmitting data, the service checks for an active session. If none exists, it triggers the handshake callback. The encryption process follows this sequence:

```swift
func sendSecureMessage(to peerID: PeerID, payload: Data) throws {
    // Trigger handshake if needed
    noiseService.onHandshakeRequired?(peerID)

    // Encrypt using the established session
    let encrypted = try noiseService.encrypt(payload,
                                             for: peerID,
                                             payloadType: .privateFile)   // NoisePayloadType.privateFile = 0x20
    bleService.send(encrypted, to: peerID)
}

```

The `encrypt(_:for:payloadType:)` method applies **ChaCha20-Poly1305**, producing a ciphertext format compliant with the Noise specification: `[payload][type-byte][16-byte tag]`. As documented in [`PUSH-TO-TALK-DESIGN.md`](https://github.com/permissionlesstech/bitchat/blob/main/PUSH-TO-TALK-DESIGN.md) (lines 30-38), padding is applied to keep frames within a 256-byte bucket, ensuring compatibility with BLE MTU limitations.

### Receiving and Decrypting Messages

Inbound packets undergo reverse processing. The receiver looks up the session associated with the sender's peer ID and verifies the 16-byte authentication tag:

```swift
func handleIncoming(_ packet: BitchatPacket) {
    guard let decrypted = try? noiseService.decrypt(packet.payload,
                                                   from: packet.senderPeerID) else {
        // handle decryption failure – possibly a stale session
        return
    }
    // Process the clear-text payload
    processMessage(decrypted)
}

```

Decryption failures typically indicate stale sessions or replay attempts, prompting the transport layer to request a new handshake.

### Handling Session Events

The service exposes callbacks for transport layer coordination:

```swift
noiseService.sessionManager.onSessionEstablished = { [weak self] peerID, remoteKey, generation in
    self?.log("Noise session established with \(peerID)")
    // Optionally notify UI that the peer is now encrypted
    self?.updateEncryptionStatus(for: peerID, .noiseSecured)
}

```

These hooks allow the UI to display encryption status and enable message sending only after mutual authentication completes (lines 44-53).

## Courier Envelopes for Store-and-Forward

For offline messaging scenarios, BitChat implements **one-way Noise X encryption** via `CourierEnvelope` (defined in [`localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift), lines 17-33). Unlike bidirectional XX sessions, these envelopes encrypt data using only the recipient's static public key, allowing any mesh node to store and forward the message without holding session state. The envelope includes a discriminator byte to distinguish protocol versions (v1 vs v2).

## Peer Identification and Fingerprints

BitChat derives stable identifiers from cryptographic material to prevent spoofing. The **8-byte peer ID** that appears in every packet header represents the first 8 bytes of the SHA-256 hash of the peer's static Noise public key ([`docs/PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md), lines 31-33).

The `NoiseEncryptionService` caches these fingerprints in a `peerFingerprints` dictionary (lines 65-68) for O(1) lookup during packet processing. This design allows the transport layer to identify senders before completing the Noise handshake, routing packets efficiently while deferring authentication to the cryptographic layer.

## Summary

BitChat's Noise Protocol implementation delivers end-to-end encryption for mesh networks through:

- **Static Curve25519 keys** persisted in the iOS Keychain for persistent identity
- **Noise XX handshakes** providing mutual authentication and forward secrecy via 60-second rekeying
- **ChaCha20-Poly1305 authenticated encryption** via CryptoKit for all payload protection
- **Lazy session instantiation** triggered by `onHandshakeRequired` callbacks to conserve mesh resources
- **SHA-256 fingerprints** for compact peer identification without revealing public keys
- **CourierEnvelope structures** enabling encrypted store-and-forward messaging using one-way Noise X

## Frequently Asked Questions

### What Noise handshake pattern does BitChat use for peer-to-peer sessions?

BitChat uses the **Noise XX pattern** for bidirectional peer-to-peer sessions. This pattern provides mutual authentication by combining ephemeral Diffie-Hellman exchanges with static public keys, allowing both parties to verify each other's identities through the `NoiseSessionManager` before transmitting application data.

### How does BitChat maintain forward secrecy in its mesh network?

Forward secrecy is enforced through a **60-second rekey timer** (`rekeyTimer`) that triggers periodic re-handshakes. As implemented in [`NoiseEncryptionService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseEncryptionService.swift) (lines 80-82), this replaces symmetric session keys regularly, ensuring that compromising a device does not expose historical communications.

### What is the purpose of CourierEnvelope in the BitChat architecture?

The `CourierEnvelope` provides **one-way Noise X encryption** for store-and-forward scenarios. Defined in [`localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift), it allows any node to encrypt messages for a recipient using only that recipient's static public key, enabling offline message delivery without maintaining bidirectional session state.

### How are BitChat peer IDs derived from Noise keys?

Peer IDs are derived as the **first 8 bytes of the SHA-256 hash** of a device's static Noise public key ([`docs/PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/PEER-ID-ROTATION.md), lines 31-33). These fingerprints are cached in `peerFingerprints` (lines 65-68) and included in BLE announcements via `AnnounceV2Packet`, allowing nodes to identify peers before establishing encrypted sessions.