# How BitChat's Nostr Integration Handles Private Envelopes Compared to NIP-17/NIP-44

> Discover how BitChat's Nostr integration handles private envelopes using a proprietary protocol incompatible with NIP-17/NIP-44. Learn about its unique encryption and authentication methods.

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

---

**BitChat implements a proprietary three-layer private envelope protocol that reuses NIP-17/NIP-44 event kinds but uses custom XChaCha20-Poly1305 encryption, unique HKDF key derivation without salt, and explicit sender authentication that makes it incompatible with standard Nostr specifications.**

BitChat extends Nostr's public-key infrastructure with a custom privacy architecture for direct messaging. Although the protocol employs standard event kinds including 13, 14, and 1059, its cryptographic implementation in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) intentionally diverges from the canonical NIP-17 and NIP-44 specifications to provide enhanced sender anonymity and authentication guarantees.

## BitChat's Three-Layer Envelope Architecture

BitChat constructs private messages through a nested hierarchy of cryptographic layers: the rumor, the seal, and the gift-wrap. This structure provides defense-in-depth while maintaining unlinkability from relay operators.

### The Rumor Layer (Unsigned Payload)

At the core of every private message sits the **rumor**, implemented as an unsigned direct message event of kind 1. Unlike NIP-44, which encrypts the plaintext directly, BitChat wraps the message content in a standard `dm` event format without cryptographic signatures. This inner payload remains unencrypted until the seal layer is applied, creating a clear separation between message content and transport metadata.

### The Seal Layer (Sender Authentication)

The seal encrypts the rumor using **XChaCha20-Poly1305** and authenticates the sender via the `createSeal` function (lines 77-99 in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift)). The implementation generates a 24-byte nonce and produces ciphertext prefixed with `v2:` followed by Base64URL-encoded `nonce || ciphertext || tag` (see the `encrypt` implementation at lines 89-106).

Critically, BitChat signs this sealed envelope with the **sender's real Schnorr key** rather than an ephemeral identity. This design choice ensures that recipients can verify sender authenticity even after unwrapping the gift-wrap layer, whereas NIP-44 signs the encrypted payload directly with the sender's key, exposing the sender identity to relays.

### The Gift-Wrap Layer (Sender Anonymity)

To prevent relay-level metadata analysis, `createGiftWrap` (lines 102-130) generates a fresh ephemeral Schnorr key pair for each message. The function encrypts the sealed rumor using this temporary key and constructs a kind 1059 outer event containing a single `p` tag pointing to the recipient.

The outer envelope must comply with strict size constraints enforced by `maximumPrivateEnvelopeCiphertextBytes` (set to 64 KB at line 43), protecting recipients from maliciously large payloads that could cause denial-of-service during decryption.

## Cryptographic Divergence from NIP-17 and NIP-44

While BitChat's event kinds mirror NIP-17 and NIP-44 conventions, the implementation details reveal intentional incompatibilities designed to optimize for specific security properties.

### XChaCha20-Poly1305 vs ChaCha20-Poly1305

BitChat utilizes **XChaCha20-Poly1305** as implemented in [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift), whereas NIP-44 employs standard ChaCha20-Poly1305. The extended nonce variant provides better resistance to nonce collision attacks in high-volume messaging scenarios, though it requires custom implementation rather than relying on standard Nostr libraries.

### Custom HKDF Key Derivation

Both protocols employ HKDF-SHA256 for symmetric key derivation, but the parameters diverge significantly. In `derivePrivateEnvelopeKey` (lines 903-916), BitChat uses an **empty salt** with the info string `"nip44-v2"` solely for backward compatibility labeling. Conversely, NIP-44 uses `"nip44-v2"` as the HKDF salt itself with a different construction.

This difference means that even with identical shared secrets, BitChat and standard NIP-44 implementations will derive different encryption keys, rendering cross-client decryption impossible despite similar event structures.

### Signature Hierarchy and Identity Protection

BitChat's signature strategy creates a strict separation between transport and identity layers. The gift-wrap layer bears only the ephemeral key signature, while the seal layer carries the sender's long-term identity signature. This separation guarantees that relays cannot link messages to specific senders based on signature data alone.

NIP-44, by contrast, signs the complete encrypted payload with the sender's persistent key, making sender identity visible to any relay processing the message. BitChat's additional signature verification steps in `decryptPrivateMessage` (starting at line 96) enforce authenticity checks at each layer before revealing inner content.

## Security Boundaries and Size Limits

The `decryptPrivateMessage` function implements comprehensive input validation before cryptographic processing. It first validates the outer envelope against the 64 KB size limit, then verifies event kind and tag structure, and finally authenticates signatures before each decryption layer.

These guards prevent resource exhaustion attacks and ensure that only properly formed gift-wraps are processed. The decryption flow unwraps the gift-wrap using the recipient's private key, verifies the seal's signature against the sender's public key, and finally parses the inner rumor as a kind 1 event.

## Working with BitChat Envelopes in Code

Creating a private message requires constructing the three-layer envelope through the protocol's convenience methods:

```swift
let sender = try NostrIdentity(for: "sender-private-key-hex")
let recipientPubkey = "recipient-npub-hex"

let event = try NostrProtocol.createPrivateMessage(
    content: "Hello, BitChat!",
    recipientPubkey: recipientPubkey,
    senderIdentity: sender
)
// Returns a kind-1059 gift-wrap ready for relay publication

```

Decryption reverses the process while enforcing all security checks:

```swift
let recipient = try NostrIdentity(for: "recipient-private-key-hex")
let (text, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage(
    giftWrap: receivedEvent,
    recipientIdentity: recipient
)
// Returns original text, authenticated sender public key, and timestamp

```

Both operations automatically handle the HKDF key derivation, nonce generation, and size validation defined in the source files.

## Summary

- BitChat implements a custom three-layer protocol (rumor/seal/gift-wrap) incompatible with NIP-17/NIP-44 despite reusing event kinds 13, 14, and 1059.
- **Encryption**: Uses XChaCha20-Poly1305 with a `v2:` prefix and Base64URL encoding rather than NIP-44's ChaCha20 construction.
- **Key Derivation**: Employs HKDF-SHA256 with empty salt and `"nip44-v2"` as info string, whereas NIP-44 uses this value as the salt.
- **Authentication**: Separates ephemeral gift-wrap signatures from persistent seal signatures, preventing relay-level sender identification while maintaining recipient-side authentication.
- **Limits**: Enforces a 64 KB maximum ciphertext size (`maximumPrivateEnvelopeCiphertextBytes`) to prevent denial-of-service attacks.
- **Source Files**: Core logic resides in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) with cryptographic primitives in [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift) and identity management in [`NostrIdentity.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrIdentity.swift).

## Frequently Asked Questions

### Why does BitChat use XChaCha20-Poly1305 instead of the ChaCha20 specified in NIP-44?

BitChat utilizes **XChaCha20-Poly1305** to leverage its 192-bit nonce, which provides enhanced security against nonce collision attacks in high-volume messaging scenarios compared to the standard 96-bit nonce in ChaCha20. This choice requires custom implementation in [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift) rather than using standard Nostr cryptographic libraries, trading compatibility for improved randomization guarantees.

### Can BitChat messages be decrypted by standard NIP-17 or NIP-44 compatible clients?

No, BitChat's private envelopes are **intentionally incompatible** with standard NIP-17 and NIP-44 implementations. The protocol uses different HKDF parameters (empty salt versus NIP-44's "nip44-v2" salt), distinct encryption prefixes (`v2:` marker), and XChaCha20-Poly1305 instead of ChaCha20-Poly1305. As noted in the file header comments (lines 10-16 of [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift)), BitChat explicitly states it is not NIP-17, NIP-44, or NIP-59 compatible despite reusing similar event kind numbers.

### How does BitChat prevent relays from identifying message senders?

BitChat achieves sender anonymity through the **gift-wrap layer**, which uses a freshly generated ephemeral Schnorr key for each message. The outer kind-1059 event is signed by this temporary key, while the sender's real identity is only revealed in the inner seal layer encrypted to the recipient. This ensures relays see only unidentifiable ephemeral public keys rather than the sender's persistent identity, unlike NIP-44 where the sender's key signs the outer encrypted payload directly.

### What happens if a private message exceeds 64 KB?

The `decryptPrivateMessage` function enforces a hard limit of **64 KB** (`maximumPrivateEnvelopeCiphertextBytes`) on any ciphertext before cryptographic processing begins. If a received gift-wrap exceeds this size, the decryption routine rejects the input immediately to prevent memory exhaustion and computational denial-of-service attacks. This boundary is set at line 43 of [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) and applies to all layers of the envelope.