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

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 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). 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, 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:

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:

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 with cryptographic primitives in XChaCha20Poly1305Compat.swift and identity management in 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 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), 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 and applies to all layers of the envelope.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →