BitChat Private Envelope Format vs NIP-17 and NIP-44: Complete Technical Comparison
BitChat implements a proprietary "private-envelope" protocol that reuses NIP-17 gift-wrap event kinds (1059, 13, 14) but diverges from both NIP-17 and NIP-44 by employing XChaCha20-Poly1305 encryption, custom HKDF parameters, and a v2: payload prefix, rendering it incompatible with standard Nostr clients.
BitChat, developed by permissionlesstech, provides end-to-end encrypted messaging over Nostr relays. According to the source code in permissionlesstech/bitchat, the protocol wraps messages in a custom cryptographic envelope that superficially resembles the NIP-17 (Gift Wrap) standard while using encryption primitives that differ from NIP-44 (Encrypted Payloads). Understanding these distinctions is critical for developers attempting to integrate with or audit the BitChat protocol.
Event Structure and Message Kinds
Standard NIP-17 defines a gift-wrap protocol that hides metadata by wrapping an encrypted payload in a kind 1059 event. The inner payload (kind 13 "seal" and kind 14 "rumor") contains the actual message and metadata.
BitChat adopts these exact kind numbers for relay transport, as defined in NostrProtocol.swift (lines 20-30):
- Kind 1059: Outer gift-wrap event stored on relays
- Kind 13: Inner "seal" containing the encrypted content
- Kind 14: "Rumor" containing the plaintext message details (inside the seal)
However, while NIP-17 typically uses NIP-44 for the encryption layer inside the seal, BitChat replaces this with a custom cipher suite.
Encryption Primitives: XChaCha20 vs ChaCha20
The most significant divergence occurs at the cryptographic primitive level.
NIP-44 specifies ChaCha20-Poly1305 with a 12-byte nonce for payload encryption. BitChat instead implements XChaCha20-Poly1305, which uses a 24-byte nonce to mitigate nonce-collision risks.
The encrypt function in NostrProtocol.swift (lines 90-102) handles this using 24-byte random nonces. This choice provides enhanced security margins for long-lived message histories but breaks interoperability with NIP-44 implementations.
Key Derivation and HKDF Parameters
Both protocols use HKDF-SHA256 for symmetric key derivation from the ECDH shared secret, but the parameters differ critically:
- NIP-44: Uses the literal string
"nip44"as the salt parameter to HKDF. - BitChat: Uses the string
"nip44-v2"as the info parameter (not salt) to HKDF.
As implemented in derivePrivateEnvelopeKey (lines 106-112 of NostrProtocol.swift), BitChat's derivation produces a different output key than NIP-44 even when given identical sender and recipient keys. This incompatible key derivation prevents cross-protocol decryption.
Payload Layout and Version Markers
BitChat enforces strict payload identification through version prefixes:
- NIP-44: Uses the
v4:prefix followed by base64url-encodednonce(12 bytes)‖ciphertext‖tag(16 bytes). - BitChat: Uses the
v2:prefix followed by base64url-encodednonce(24 bytes)‖ciphertext‖tag(16 bytes).
The encrypt and decrypt functions (lines 121-128 and 132-138) explicitly check for this v2: marker. Standard NIP-44 v4: payloads are rejected during the decryption process, ensuring the client only processes BitChat-specific envelopes.
Authentication and Trust Model
NIP-44 authenticates messages solely through the ChaCha20-Poly1305 cipher tag; the sender's identity is proven by the Nostr event signature on the outer wrapper.
BitChat adds an additional authentication layer. The inner seal (kind 13) is cryptographically signed by the sender's real identity key before encryption. The createSeal function (lines 77-84) handles this signing, and decryptPrivateMessage verifies the signature (lines 31-35) to confirm sender authenticity before exposing the message content. This provides non-repudiation inside the encrypted envelope, even if the outer gift-wrap uses randomized metadata.
Swift Implementation Examples
Creating a Private Message
The following creates a BitChat-compatible gift-wrap (kind 1059) ready for relay publication:
// Create a private message (outer kind 1059) for a recipient
let event = try NostrProtocol.createPrivateMessage(
content: "Hello, secure world!",
recipientPubkey: recipientHex,
senderIdentity: myNostrIdentity
)
// `event` is a fully-signed NostrEvent ready for relay publication.
// The payload (`event.content`) begins with "v2:" and contains the XChaCha20-Poly1305 ciphertext.
// See the creation flow in NostrProtocol.swift lines 45-95.
Decrypting a Received Envelope
Decryption unwraps the gift-wrap, verifies the inner seal signature, and returns the plaintext:
// Assume `giftWrap` is the NostrEvent fetched from the relay
let (plainText, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage(
giftWrap: giftWrap,
recipientIdentity: myNostrIdentity
)
// `plainText` holds the original message, `senderPubkey` is the authenticated sender,
// and `timestamp` is the true message time (the outer event's randomized timestamp is ignored).
// Decryption logic follows the steps in NostrProtocol.swift decryptPrivateMessage function.
Summary
- Event Kinds: BitChat uses NIP-17 kinds (1059, 13, 14) for transport but proprietary encryption for content.
- Cipher: XChaCha20-Poly1305 (24-byte nonce) replaces NIP-44's ChaCha20-Poly1305 (12-byte nonce).
- Key Derivation: HKDF with info parameter
"nip44-v2"replaces NIP-44's salt parameter"nip44". - Payload Marker:
v2:prefix identifies BitChat envelopes, incompatible with NIP-44'sv4:. - Authentication: Inner seal signatures provide sender verification beyond the cipher authentication tag.
- Forward Secrecy: Neither BitChat nor NIP-44 provide forward secrecy; both rely on static keys for ECDH.
Frequently Asked Questions
Is BitChat compatible with standard NIP-17 clients?
No. While BitChat uses the same event kinds as NIP-17 (1059 for gift-wrap, 13/14 for seal/rumor), the encryption layer uses XChaCha20-Poly1305 and the v2: prefix rather than the NIP-44 encryption expected by NIP-17 implementations. Standard clients will fail to decrypt BitChat messages.
Why does BitChat use "v2" instead of NIP-44's "v4"?
The v2: marker indicates the use of XChaCha20-Poly1305 with 24-byte nonces and the custom HKDF derivation with "nip44-v2" info. This distinguishes BitChat's format from NIP-44's v4: (ChaCha20-Poly1305 with 12-byte nonces) and previous BitChat experimental versions.
Does BitChat provide better security than NIP-44?
BitChat uses XChaCha20-Poly1305 which allows safer random nonce generation due to the 192-bit nonce space, reducing collision probability compared to ChaCha20's 96-bit nonces. However, like NIP-44, it lacks forward secrecy—compromise of a private key allows decryption of all past messages, as noted in the NostrProtocol.swift header comments.
Can BitChat users receive messages from standard Nostr direct message clients?
No. BitChat specifically filters for kind 1059 events with content prefixed by v2:. Standard NIP-44 encrypted messages (typically kind 4 with v4: content or gift-wraps using standard NIP-44 encryption) are rejected by the decryptPrivateMessage validation logic.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →