# How BitChat Encrypts Private Messages Over the Nostr Protocol: A Technical Deep Dive

> Discover how BitChat secures private messages on Nostr using X25519 ECDH, HKDF-SHA-256, and XChaCha20-Poly1305 encryption. Understand the technical deep dive into its private envelope scheme.

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

---

**BitChat employs a custom "private-envelope" encryption scheme that combines X25519-style ECDH key agreement, HKDF-SHA-256 key derivation, and XChaCha20-Poly1305 authenticated encryption to secure direct messages transported over the Nostr network.**

The permissionlesstech/bitchat repository implements a proprietary end-to-end encryption pipeline for iOS devices that protects message content from Nostr relays and intermediary nodes. This encryption process for private messages over the Nostr path in BitChat diverges from standard NIP-44 implementations while maintaining partial compatibility through specific HKDF parameters.

## The Private Envelope Encryption Flow

BitChat uses a **six-step private-envelope scheme** implemented in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) to transform plaintext into secure ciphertext ready for Nostr event propagation.

### Key Agreement via X25519-Style ECDH

The process begins with **Elliptic Curve Diffie-Hellman (ECDH)** key exchange. The sender invokes `deriveSharedSecret` using their Schnorr private key and the recipient's compressed P-256K public key.

This operation performs an X25519-style key agreement that correctly handles the even/odd-Y bit for x-only public keys, ensuring consistent shared secret derivation regardless of public key compression format.

### HKDF-SHA-256 Key Derivation

The raw ECDH shared secret feeds into **HKDF-SHA-256** via the `derivePrivateEnvelopeKey` function. The derivation uses:

- **Salt**: Empty (zero-length)
- **Info string**: `"nip44-v2"` (retained for backward compatibility only)
- **Output**: 32-byte symmetric encryption key

This step isolates the asymmetric key material from the symmetric cipher, providing domain separation through the HKDF extract-and-expand process.

### XChaCha20-Poly1305 Authenticated Encryption

BitChat encrypts the UTF-8 plaintext using **XChaCha20-Poly1305** via `XChaCha20Poly1305Compat.seal`. The implementation generates a cryptographically random **24-byte nonce** using `SecRandomCopyBytes` for each message.

The [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift) helper constructs the XChaCha20 cipher by:

1. Deriving a sub-key via **HChaCha20** from the 32-byte HKDF output and the first 16 bytes of the nonce.
2. Reducing the 24-byte nonce to a 12-byte ChaCha20 nonce (four zero bytes concatenated with the last 8 bytes of the original nonce).
3. Applying ChaCha20-Poly1305 AEAD to produce ciphertext and a 16-byte authentication tag.

### Wire Format and Envelope Structure

The encrypted payload serializes into a compact string format:

```

v2:<base64url(nonce24 || ciphertext || tag)>

```

The `v2:` prefix identifies the envelope version. The payload concatenates the 24-byte nonce, variable-length ciphertext, and 16-byte Poly1305 tag, then encodes the entire byte sequence using base64url encoding (URL-safe Base64 without padding).

## Implementation in BitChat Source Code

The encryption pipeline spans four core files in the `bitchat/Nostr/` directory:

- **[`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift)**: Contains the primary `encrypt()` and `decrypt()` methods, `deriveSharedSecret`, `derivePrivateEnvelopeKey`, and envelope framing logic.
- **[`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift)**: Implements the XChaCha20 construction including HChaCha20 sub-key derivation and nonce conversion utilities.
- **[`Base64URLCoding.swift`](https://github.com/permissionlesstech/bitchat/blob/main/Base64URLCoding.swift)**: Handles base64url encode/decode operations for the final envelope string.
- **[`NostrIdentity.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrIdentity.swift)**: Defines the Schnorr key types (`P256K.Schnorr.PrivateKey`) used for ECDH operations.

## Practical Code Examples

### Encrypting a Message

```swift
// Sender-side encryption
let encryptedEnvelope = try NostrProtocol.encrypt(
    plaintext: "Hello, world!",                 // UTF-8 message content
    recipientPubkey: recipientHexPubkey,        // Recipient's compressed P-256K pubkey (hex)
    senderKey: senderSchnorrPrivateKey         // Sender's Schnorr private key
)
// Result: "v2:..." string ready for Nostr event content field

```

### Decrypting a Message

```swift
// Receiver-side decryption
let plaintext = try NostrProtocol.decrypt(
    ciphertext: encryptedEnvelope,              // The "v2:..." string from Nostr event
    senderPubkey: senderHexPubkey,              // Sender's compressed pubkey (hex)
    recipientKey: receiverSchnorrPrivateKey     // Receiver's Schnorr private key
)
// Returns original "Hello, world!" UTF-8 string

```

The decryption process reverses the encryption steps: stripping the `v2:` prefix, base64url decoding, extracting the nonce and tag, re-deriving the shared secret and symmetric key (including even/odd-Y handling), and finally opening the XChaCha20-Poly1305 box via `XChaCha20Poly1305Compat.open`.

## Differences from NIP-44 Standard

While BitChat uses the `"nip44-v2"` HKDF info string for interoperability signaling, the actual cryptographic implementation differs from NIP-44 in three critical ways:

- **Envelope Framing**: BitChat uses the `v2:` prefix with a `nonce24||ciphertext||tag` layout, whereas NIP-44 specifies different framing and length prefixes.
- **Key Schedule**: BitChat derives a single envelope key via HKDF-SHA-256 that encrypts the entire payload. NIP-44 derives per-message subkeys using a different construction.
- **Cipher Selection**: BitChat explicitly implements XChaCha20-Poly1305 with custom HChaCha20 sub-key derivation, diverging from NIP-44's prescribed cipher suites.

## Summary

- **BitChat's private-envelope scheme** uses ECDH (X25519-style) between Schnorr keys to establish shared secrets.
- **HKDF-SHA-256** with info string `"nip44-v2"` derives the 32-byte symmetric key used for encryption.
- **XChaCha20-Poly1305** provides authenticated encryption with 24-byte random nonces generated via `SecRandomCopyBytes`.
- **Wire format** concatenates nonce, ciphertext, and tag into a base64url string prefixed with `v2:`.
- **Core implementation** resides in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) and [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift), handling the complete encrypt/decrypt lifecycle.

## Frequently Asked Questions

### What encryption algorithm does BitChat use for Nostr private messages?

BitChat uses **XChaCha20-Poly1305** for authenticated symmetric encryption, implemented through a custom compatibility layer in [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift). The algorithm extends standard ChaCha20-Poly1305 to support 192-bit (24-byte) nonces via HChaCha20 sub-key derivation, providing resistance to nonce-collision attacks without requiring a random number generator for the cipher stream itself.

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

BitChat diverges from NIP-44 in envelope structure and key derivation. While NIP-44 uses specific per-message key derivation and framing, BitChat employs a **custom private-envelope format** with the `v2:` prefix and derives a single envelope key via HKDF-SHA-256 that protects the entire message payload. The `"nip44-v2"` info string exists only for compatibility signaling and does not indicate strict NIP-44 compliance.

### Where is the encryption logic implemented in the BitChat codebase?

The primary encryption logic resides in **[`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift)**, which orchestrates the ECDH key agreement, HKDF key derivation, and envelope construction. The low-level XChaCha20-Poly1305 implementation lives in **[`bitchat/Nostr/XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/XChaCha20Poly1305Compat.swift)**, while **[`Base64URLCoding.swift`](https://github.com/permissionlesstech/bitchat/blob/main/Base64URLCoding.swift)** and **[`NostrIdentity.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrIdentity.swift)** provide supporting encoding and key type definitions.

### Why does BitChat use Schnorr keys for ECDH instead of standard Nostr encryption keys?

BitChat performs **X25519-style ECDH using Schnorr private keys** and compressed P-256K public keys to leverage existing key infrastructure while maintaining compatibility with Nostr's public key formats. The `deriveSharedSecret` function handles the conversion between Schnorr signing keys and the ECDH key agreement protocol, including proper handling of the even/odd-Y coordinate for x-only public keys as defined in BIP-340.