# BitChat Private Message Envelope Format: XChaCha20-Poly1305 Over Nostr Explained

> Understand the BitChat private message envelope format. Discover how XChaCha20-Poly1305 encryption is used over Nostr events for secure private messaging.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: api-reference
- Published: 2026-08-02

---

**TLDR:** BitChat encrypts private messages using XChaCha20-Poly1305 and transports them as kind-1059 Nostr events with a `v2:` prefixed payload containing a 24-byte nonce, ciphertext, and 16-byte Poly1305 tag, all Base64-URL encoded and incompatible with standard NIP-44 or NIP-59 formats.

BitChat delivers end-to-end encrypted messaging over the Nostr protocol using a proprietary envelope specification built on XChaCha20-Poly1305 AEAD. This approach, implemented in the `permissionlesstech/bitchat` open-source repository, generates unique symmetric keys via HKDF and encapsulates message data in a custom binary format wrapped within Nostr kind-1059 events.

## The Kind-1059 Nostr Event Container

BitChat private messages are transmitted as **kind-1059** Nostr events, a proprietary event type distinct from standard encrypted direct messages. According to the implementation in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift), the `sendPrivateMessage(to:payload:)` method constructs these events by placing the encrypted envelope in the `content` field and tagging the recipient in the event tags.

The resulting JSON structure follows this schema:

```json
{
  "kind": 1059,
  "pubkey": "<sender-nostr-pubkey>",
  "created_at": 1723456789,
  "content": "v2:<Base64Url-encoded-nonce-cipher-tag>",
  "tags": [["p", "<recipient-nostr-pubkey>"]]
}

```

Only BitChat clients recognize this envelope format; other Nostr implementations treat the payload as opaque data.

## XChaCha20-Poly1305 Envelope Components

The BitChat envelope consists of five distinct cryptographic components assembled in [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift). This construction deviates from Nostr's native NIP-44 encryption to provide extended nonce capabilities.

### Version Prefix

Every private message envelope begins with the ASCII string `v2:` prepended to the Base64-URL encoded payload. This prefix signals to BitChat clients that the message requires XChaCha20-Poly1305 decryption rather than standard Nostr encryption schemes.

### 24-Byte Nonce Structure

BitChat generates a cryptographically random **24-byte nonce** for each message. Internally, the `XChaCha20Poly1305Compat` implementation maps this to the underlying ChaCha20-Poly1305 cipher by setting the first 4 bytes to zeroes and utilizing the last 8 bytes of the 24-byte input, providing compatibility with CryptoKit's ChaChaPoly interface while maintaining XChaCha20's extended nonce space.

### HKDF Key Derivation

Rather than using the raw recipient public key for encryption, BitChat derives a symmetric encryption key using **HKDF** (HMAC-based Extract-and-Expand Key Derivation Function). The derivation process mixes the recipient's Nostr public key with the nonce to produce the final 256-bit key used for the AEAD operation.

### Ciphertext and Authentication Tag

The plaintext message body undergoes XChaCha20-Poly1305 encryption to produce:
- **Ciphertext**: The encrypted message content
- **Authentication Tag**: A 16-byte Poly1305 tag ensuring integrity and authenticity

### Binary Payload Layout

The final envelope payload follows a strict concatenation format before Base64-URL encoding:

```

[24-byte nonce] || [ciphertext] || [16-byte tag]

```

This binary blob is then encoded and prefixed with `v2:` to form the complete event content.

## Constructing the Private Message Envelope

The following Swift code demonstrates the complete envelope construction process using BitChat's cryptographic primitives:

```swift
import CryptoKit
import Foundation

// 1. Prepare message components
let plainMessage = Data("Hey, meet me at the park!".utf8)
let recipientPubKey = try! NostrIdentity.publicKey(fromHex: "<recipient-hex-pubkey>")
let nonce24 = Data.random(count: 24)  // 24-byte random nonce

// 2. Seal the envelope using XChaCha20-Poly1305
let sealed = try XChaCha20Poly1305Compat.seal(
    plaintext: plainMessage,
    key: recipientPubKey,          // HKDF derives symmetric key from pubkey
    nonce24: nonce24
)

// 3. Assemble BitChat envelope: nonce || ciphertext || tag
let envelopeData = nonce24 + sealed.ciphertext + sealed.tag
let envelopeB64 = Base64URL.encode(envelopeData)

// 4. Create kind-1059 Nostr event
let event = NostrEvent(
    kind: 1059,
    content: "v2:" + envelopeB64,
    tags: [["p", recipientPubKey.hexString]]
)

// 5. Transmit via BitChat Nostr client
try NostrProtocol.shared.send(event: event)

```

The `XChaCha20Poly1305Compat.seal(plaintext:key:nonce24:)` method handles the HKDF key derivation and ChaChaPoly encryption internally, returning the ciphertext and authentication tag separately for manual assembly.

## Receiving and Parsing the Envelope

Upon receiving a kind-1059 event, BitChat processes the envelope through [`NostrEmbeddedBitChat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrEmbeddedBitChat.swift). The parsing workflow follows these steps:

1. **Strip the prefix**: Remove the `v2:` string from the `content` field
2. **Base64-URL decode**: Convert the remaining string to raw binary data
3. **Extract components**: Parse the first 24 bytes as the nonce, the last 16 bytes as the tag, and the middle section as ciphertext
4. **Derive key**: Recompute the HKDF symmetric key using the recipient's private key and extracted nonce
5. **Decrypt and verify**: Call the underlying ChaChaPoly unseal operation to recover plaintext and validate the authentication tag

If any step fails—particularly Poly1305 tag verification—the message is discarded as tampered or corrupted.

## Key Implementation Files

The envelope format is defined across three primary source files in the BitChat repository:

- **[`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift)**: Implements the XChaCha20-Poly1305 compatibility layer, handling nonce transformation, HKDF key derivation, and the `seal`/`open` operations
- **[`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift)**: Manages event creation and transmission, including the `sendPrivateMessage(to:payload:)` method that constructs kind-1059 events
- **[`NostrEmbeddedBitChat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrEmbeddedBitChat.swift)**: Contains the reception logic that parses incoming `v2:` envelopes and routes decrypted content to the UI layer

## Summary

- BitChat uses **kind-1059 Nostr events** to transport private messages, distinguishable from standard Nostr encrypted events
- The envelope format is **proprietary and incompatible** with NIP-44, NIP-59, or other Nostr encryption standards
- Encryption employs **XChaCha20-Poly1305** with a 24-byte nonce mapped to CryptoKit's ChaChaPoly implementation
- Keys are derived via **HKDF** from the recipient's public key and message nonce
- The payload layout follows **nonce || ciphertext || tag**, Base64-URL encoded with a `v2:` prefix
- Implementation spans [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift), [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift), and [`NostrEmbeddedBitChat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrEmbeddedBitChat.swift)

## Frequently Asked Questions

### How does BitChat's encryption differ from Nostr's NIP-44 standard?

BitChat implements a custom XChaCha20-Poly1305 envelope that is fundamentally incompatible with NIP-44. While NIP-44 uses ChaCha20-Poly1305 with a specific key derivation based on shared secrets, BitChat employs HKDF over the recipient's public key with a 24-byte nonce structure and wraps the result in a kind-1059 event with a `v2:` prefix. Only BitChat clients possess the logic to decrypt these messages.

### Why does BitChat use a 24-byte nonce for XChaCha20-Poly1305?

XChaCha20 extends the original ChaCha20 nonce from 12 bytes to 24 bytes to enable safer random nonce generation without collision risks. In BitChat's implementation found in [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift), the 24-byte nonce is mapped to CryptoKit's underlying ChaChaPoly interface by zeroing the first 4 bytes and using the last 8 bytes of the extended nonce, providing interoperability with Apple's cryptographic libraries while maintaining the security benefits of extended nonces.

### Can other Nostr clients read BitChat private messages?

No. The envelope format is proprietary to the BitChat ecosystem. Other Nostr clients will see kind-1059 events with `v2:` prefixed content but lack the HKDF parameters, nonce parsing logic, and key derivation algorithms necessary to decrypt the payload. The messages appear as opaque Base64-URL strings to standard Nostr implementations.

### What happens if the Poly1305 authentication tag verification fails?

BitChat strictly enforces AEAD integrity checks. If the 16-byte Poly1305 tag extracted from the envelope tail does not match the computed tag during decryption, the `XChaCha20Poly1305Compat` layer raises a decryption failure, and [`NostrEmbeddedBitChat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrEmbeddedBitChat.swift) discards the message entirely. This prevents tampered messages or corrupted data from reaching the user interface.