# How BitChat Implements Offline Message Sealing Using Noise X

> Discover how BitChat uses Noise X for offline message sealing. Learn about payload encryption and anonymous routing techniques for secure communication.

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

---

**BitChat implements offline message sealing by encrypting payloads with a one-way Noise X pattern to the recipient’s static public key, then wrapping the ciphertext in a courier envelope tagged with a rotating HMAC value that allows anonymous routing without revealing the recipient’s identity.**

BitChat is an open-source peer-to-peer messaging protocol built for censorship-resistant mesh networks. The `permissionlesstech/bitchat` repository solves the offline delivery problem—where the recipient is disconnected—by using the **Noise X** cryptographic pattern to create sealed envelopes that any intermediary node can store and forward without reading the content or identifying the intended recipient.

## The Three-Step Sealing Process

Offline message sealing in BitChat follows a strict three-phase pipeline: derive an anonymous recipient tag, encrypt the payload using Noise X, and package the result into a `CourierEnvelope` for transport.

### 1. Deriving the Anonymous Recipient Tag

To enable routing without exposing the recipient’s static public key, BitChat generates a **rotating recipient tag**. This 16-byte identifier is computed as an HMAC-SHA-256 over the recipient’s static Noise public key and the current UTC day, truncated to 16 bytes.

In [`bitchat/localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/localPackages/BitFoundation/Sources/BitFoundation/CourierEnvelope.swift), the `recipientTag(noiseStaticKey:epochDay:)` static method implements this derivation:

```swift
public static func recipientTag(noiseStaticKey: Data, epochDay: UInt32) -> Data {
    var message = tagContext
    withUnsafeBytes(of: epochDay.bigEndian) { message.append(contentsOf: $0) }
    let mac = HMAC<SHA256>.authenticationCode(for: message,
                                               using: SymmetricKey(data: noiseStaticKey))
    return Data(mac).prefix(tagLength)            // ← 16-byte tag
}

```

The tag rotates daily via the `epochDay` parameter, preventing correlation attacks where an observer might link envelopes across time periods. When the recipient comes online, they regenerate candidate tags for the current day using their own static key to locate envelopes destined for them.

### 2. Encrypting with the Noise X Pattern

The actual payload encryption uses the **one-way Noise X** pattern: the sender encrypts directly to the recipient’s long-term static public key without performing a full handshake. This creates a "seal" that only the holder of the corresponding private key can open.

The implementation resides in [`bitchat/Services/NoiseEncryptionService.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NoiseEncryptionService.swift). The `seal(_:to:)` method derives a shared secret between the sender’s ephemeral or static key and the recipient’s static key, then encrypts using ChaChaPoly:

```swift
func seal(_ plaintext: Data, to recipientStaticKey: Data) throws -> Data {
    // Derive a shared secret from the recipient’s static public key and our
    // long-term static private key, then encrypt with ChaChaPoly.
    let shared = try deriveSharedSecret(with: recipientStaticKey)
    return try ChaChaPoly.seal(plaintext, using: shared).combined
}

```

When the envelope specifies a pre-key (for forward secrecy), the same routine applies but uses the one-time pre-key as the static key target, ensuring that even key compromise does not expose past messages.

### 3. Assembling the Courier Envelope

The sealed ciphertext, recipient tag, and metadata are assembled into a `CourierEnvelope` structure. This TLV-encoded envelope contains the expiry timestamp, copy budget, optional pre-key identifier, and the encrypted payload.

The construction flow in the outbox logic looks like this:

```swift
let tag = CourierEnvelope.recipientTag(noiseStaticKey: recipientStaticKey,
                                       epochDay: CourierEnvelope.epochDay(for: Date()))
let sealed = try noiseService.seal(messageData, to: recipientStaticKey)

let envelope = CourierEnvelope(recipientTag: tag,
                              expiry: UInt64(Date().addingTimeInterval(24*60*60).timeIntervalSince1970 * 1000),
                              ciphertext: sealed,
                              copies: 1,               // carry-only budget
                              prekeyID: nil)           // static-key (Noise X) envelope

```

The envelope is then stored in the `MessageOutboxStore` and disseminated via available transports—whether Bluetooth Low Energy mesh, Nostr relays, or direct peer connections. Intermediary nodes route based solely on the 16-byte tag, which reveals neither the sender nor the recipient’s identity.

## Forward Secrecy Enhancements

While basic Noise X provides confidentiality to the static key, BitChat supports **forward-secret offline sealing** through pre-keys defined in [`bitchat/localPackages/BitFoundation/Sources/BitFoundation/PrekeyBundle.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/localPackages/BitFoundation/Sources/BitFoundation/PrekeyBundle.swift). When a pre-key is used (indicated by a non-nil `prekeyID` in the envelope), the seal targets the one-time pre-key rather than the long-term static key. This ensures that even if the recipient’s static key is compromised later, previously sealed offline messages remain secure because the pre-keys have been deleted after use.

## Summary

- **Anonymous Routing**: The 16-byte recipient tag is derived from HMAC-SHA-256 of the static key and current day, rotating daily to prevent correlation.
- **One-Way Encryption**: The Noise X pattern in `NoiseEncryptionService` seals payloads directly to the recipient’s static public key using ChaChaPoly.
- **Envelope Structure**: `CourierEnvelope` packs the ciphertext, rotating tag, expiry, and copy budget into a format readable only by the intended recipient.
- **Transport Agnostic**: Sealed envelopes flow through `MessageOutboxStore` to any available mesh peer, enabling store-and-forward without trust.
- **Forward Secrecy**: Optional pre-key targeting (via `PrekeyBundle`) upgrades static-key seals to forward-secret seals.

## Frequently Asked Questions

### What is the Noise X pattern in BitChat?

The Noise X pattern in BitChat is a one-way cryptographic seal where a sender encrypts a message directly to a recipient’s long-term static public key without performing a bidirectional handshake. This allows the sender to encrypt without requiring an online recipient, creating a "sealed envelope" that only the private key holder can open.

### How does the recipient tag protect privacy?

The recipient tag protects privacy by serving as a routing identifier that reveals neither the recipient’s public key nor their identity. Because it is an HMAC truncation derived from the static key and the current day’s timestamp, only the intended recipient—who possesses the static key—can compute the correct tag to locate their messages. Daily rotation prevents long-term tracking of envelope flows.

### Can offline messages be decrypted by forwarding nodes?

No. Forwarding nodes possess only the `CourierEnvelope` containing the encrypted ciphertext and the routing tag. The ciphertext is sealed using the Noise X pattern, which requires the recipient’s private static key (or the private pre-key) to derive the ChaChaPoly decryption key. Intermediaries cannot decrypt the payload or identify the sender or recipient beyond the anonymous 16-byte tag.

### What provides forward secrecy in BitChat's offline sealing?

Forward secrecy is achieved when the sender targets a **one-time pre-key** instead of the static key. According to the logic in [`PrekeyBundle.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PrekeyBundle.swift) and the envelope construction, if the `prekeyID` field is set, the seal uses the ephemeral pre-key as the target. Since the recipient deletes pre-keys after use, compromise of the long-term static key cannot decrypt past messages sealed to now-deleted pre-keys.