# How Noise Session Establishment (XX Handshake) Works in BitChat

> Learn how BitChat uses the Noise XX handshake to establish secure, authenticated, end-to-end encrypted sessions between peers without needing static public keys. Understand the XX handshake.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-03

---

**BitChat uses the Noise XX handshake pattern to establish mutually authenticated, end-to-end encrypted sessions between peers without requiring prior knowledge of each other's static public keys.**

The Noise XX pattern provides one of the most versatile handshake configurations in the Noise Protocol Framework, offering **mutual authentication** while allowing either party to initiate communication. In BitChat, this pattern is implemented as `Noise_XX_25519_ChaChaPoly_SHA256` across the `bitchat/Noise` package, with the static key pair managed by `NoiseEncryptionService` and session state orchestrated through `NoiseSession` and `NoiseSessionManager`.

## The Three-Message XX Handshake Flow

The Noise XX pattern exchanges exactly three messages to establish a secure transport channel. Unlike simpler patterns, neither peer needs the other's static public key before starting the handshake.

### Step 1: Initiator Sends Ephemeral Public Key

The initiator begins by generating a fresh ephemeral Curve25519 key pair and transmitting only the public key `e`.

```swift
// NoiseEncryptionService.swift - initiator starts outbound session
let noise = NoiseEncryptionService(keychain: myKeychain)
let session = try noise.createOutboundSession(peerPublicKey: peerNoiseKey)

// First handshake message contains ephemeral public key only
let msg1 = try session.writeMessage(data: Data()) // 32-byte `e`

```

In [`bitchat/Noise/NoiseSession.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseSession.swift), the session records this as the initiator's first handshake record and marks itself as **not a responder** (meaning it will not send the first message in subsequent exchanges).

### Step 2: Responder Replies with Ephemeral and Encrypted Static Key

Upon receiving `e`, the responder performs three critical operations:

1. Generates its own ephemeral key pair (`e′`)
2. Computes Diffie-Hellman shared secrets: `DH(e, e′)` and `DH(e, s′)` where `s′` is the responder's static secret key
3. Encrypts its static public key using these secrets and transmits `[e′ || enc(s′)]`

This step **proves possession of the responder's long-term static key** without revealing it in plaintext. The `NoiseSession` validates the payload structure and stores the responder's static public key for later authentication.

### Step 3: Initiator Sends Encrypted Static Key

The initiator now possesses `e′` and the authenticated `s′`. It completes mutual authentication by:

1. Computing `DH(e′, s)` and `DH(e, s)` where `s` is the initiator's static secret key
2. Encrypting its static public key using these DH results
3. Sending `enc(s)` to the responder

After processing this message, both parties have completed **mutual static key authentication** and derive identical transport encryption and MAC keys from the full chain of DH results.

| Message | Sender | Contents | Cryptographic Purpose |
|---------|--------|----------|----------------------|
| 1 | Initiator | `e` (32 bytes) | Esthemeral-only, no static key commitment |
| 2 | Responder | `e′ \|\| enc(s′)` | Proves responder identity via encrypted static key |
| 3 | Initiator | `enc(s)` | Proves initiator identity, completes key agreement |

## Key Implementation Files and Responsibilities

### NoiseProtocol.swift

[`bitchat/Noise/NoiseProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseProtocol.swift) defines the XX pattern constants and protocol properties. It declares the pattern as "most versatile, provides mutual authentication" and establishes the `Noise_XX_25519_ChaChaPoly_SHA256` configuration used throughout BitChat.

### NoiseSession.swift

[`bitchat/Noise/NoiseSession.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseSession.swift) contains the **core state machine** driving the three-message exchange. Critical implementation details include:

- Tracking whether the local party is a **responder** (determines who sends message 1)
- The explicit comment: *"For XX pattern, we don't need remote static key upfront"*—highlighting the pattern's flexibility
- Managing the transition from handshake mode to transport mode upon completion

### NoiseSessionManager.swift

[`bitchat/Noise/NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Noise/NoiseSessionManager.swift) handles **session lifecycle and security boundaries**:

- Secure token hand-off between handshake and transport phases
- Preventing premature leakage of prepared message-1
- Monitoring inbound responder state while awaiting the third message
- Coordinating automatic re-keying after successful handshake completion

### NoiseEncryptionService.swift

[`bitchat/Services/NoiseEncryptionService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NoiseEncryptionService.swift) provides the **application-facing API**:

- Stores the local static Curve25519 key pair in the system keychain
- Generates ephemeral keys for each new session
- Performs all Diffie-Hellman operations
- Guarantees transport messages cannot be confused with handshake transcripts
- Exposes `encrypt()` and `decrypt()` methods after handshake completion

## Complete Session Establishment Example

```swift
import Foundation

// MARK: - Full XX Handshake Demonstration

// Initialize service with persistent static keypair
let noiseService = NoiseEncryptionService(keychain: keychain)

// Create outbound session as initiator
var initiatorSession = try noiseService.createOutboundSession(
    peerPublicKey: responderStaticPubKey
)

// === MESSAGE 1: Initiator → Responder ===
let message1 = try initiatorSession.writeMessage(data: Data())
// Transmits 32-byte ephemeral public key `e`

// (Network transmission to responder...)

// === Responder processes message 1 and creates reply ===
var responderSession = try noiseService.createInboundSession(
    receivedData: message1
)
let message2 = try responderSession.writeMessage(data: Data())
// Transmits `e′ || enc(s′)` — 32 bytes ephemeral + encrypted static

// (Network transmission to initiator...)

// === MESSAGE 3: Initiator processes responder, sends final authentication ===
_ = try initiatorSession.readMessage(message2)
let message3 = try initiatorSession.writeMessage(data: Data())
// Transmits `enc(s)` — encrypted initiator static key

// (Network transmission to responder...)

// === Handshake complete for responder ===
_ = try responderSession.readMessage(message3)

// Both sessions now in TRANSPORT mode — mutually authenticated
assert(initiatorSession.isHandshakeComplete)
assert(responderSession.isHandshakeComplete)

// Transport encryption active
let plaintext = "Hello, BitChat!".data(using: .utf8)!
let encrypted = try initiatorSession.encrypt(payload: plaintext)
let decrypted = try responderSession.decrypt(packet: encrypted)
// decrypted == plaintext

```

## Transport Mode and Message Protection

Once the XX handshake completes, `NoiseEncryptionService` transitions both sessions to **transport mode**. All subsequent communications—voice calls, media transfers, and chat messages—use the derived cipher keys:

- **Encryption**: ChaCha20-Poly1305 authenticated encryption with 16-byte Noise tags
- **Padding**: Optional message padding via [`MessagePadding.swift`](https://github.com/permissionlesstech/bitchat/blob/main/MessagePadding.swift) to obscure payload sizes
- **Re-keying**: Automatic key rotation triggered by `NoiseSessionManager` after handshake completion

The transport layer guarantees **confidentiality** (only the authenticated peer can decrypt), **integrity** (any modification is detected), and **identity binding** (both parties have cryptographically proven static key ownership).

## Summary

- **Noise XX handshake** in BitChat uses three messages to achieve mutual authentication without pre-shared static keys.
- **Message 1** (`e`): Initiator sends ephemeral public key only—no static key commitment required.
- **Message 2** (`e′ || enc(s′)`): Responder proves identity by encrypting its static key using combined DH secrets.
- **Message 3** (`enc(s)`): Initiator proves identity, completing the handshake and enabling transport key derivation.
- **Core files**: [`NoiseProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseProtocol.swift) (constants), [`NoiseSession.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseSession.swift) (state machine), [`NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseSessionManager.swift) (lifecycle), and [`NoiseEncryptionService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseEncryptionService.swift) (application API).

## Frequently Asked Questions

### What makes the XX pattern different from simpler Noise patterns like NK or IK?

The **XX pattern** provides mutual authentication without requiring either party to know the other's static public key beforehand. Patterns like **NK** (sender anonymous, recipient known) or **IK** (immediate static key transmission) sacrifice this flexibility—XX enables truly permissionless connection establishment where either peer can initiate and both authenticate dynamically.

### How does BitChat prevent handshake message confusion with transport messages?

`NoiseEncryptionService` maintains strict **state separation** enforced by `NoiseSession`. Handshake messages propagate through `writeMessage()` and `readMessage()` methods, while transport messages use `encrypt()` and `decrypt()`—these APIs are mutually exclusive based on `isHandshakeComplete` state. The underlying `CipherState` objects are only initialized after successful handshake finalization.

### Where is the static private key stored in BitChat's implementation?

The static Curve25519 key pair is persisted in the **system keychain** by `NoiseEncryptionService`, not in application memory or UserDefaults. The private key is accessed only through the service's internal methods for DH operations, never exposed to higher-level application code. This aligns with iOS security best practices and protects against extraction attacks.

### Can the XX handshake be used for forward secrecy?

Yes—**ephemeral keys** (`e` and `e′`) are generated fresh for each session and discarded after use. Even if the static private keys are compromised later, recorded handshake transcripts cannot be decrypted because the ephemeral secrets are unrecoverable. This provides **future secrecy for past sessions** combined with **mutual authentication** of long-term identities.