BitChat Private Message Envelope Format: XChaCha20-Poly1305 Over Nostr Explained
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, 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:
{
"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. 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:
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. The parsing workflow follows these steps:
- Strip the prefix: Remove the
v2:string from thecontentfield - Base64-URL decode: Convert the remaining string to raw binary data
- Extract components: Parse the first 24 bytes as the nonce, the last 16 bytes as the tag, and the middle section as ciphertext
- Derive key: Recompute the HKDF symmetric key using the recipient's private key and extracted nonce
- 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: Implements the XChaCha20-Poly1305 compatibility layer, handling nonce transformation, HKDF key derivation, and theseal/openoperationsNostrProtocol.swift: Manages event creation and transmission, including thesendPrivateMessage(to:payload:)method that constructs kind-1059 eventsNostrEmbeddedBitChat.swift: Contains the reception logic that parses incomingv2: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,NostrProtocol.swift, andNostrEmbeddedBitChat.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, 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 discards the message entirely. This prevents tampered messages or corrupted data from reaching the user interface.
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 →