BitChat Private-Envelope Encryption Format for Nostr: Technical Architecture and Implementation
BitChat implements a proprietary three-layer "private-envelope" encryption scheme using Nostr event kinds 14, 13, and 1059 with ECDH key exchange and XChaCha20-Poly1305, but it is not compatible with standard NIP-17, NIP-44, or NIP-59 specifications.
The permissionlesstech/bitchat repository contains a Swift-based implementation of this BitChat private-envelope encryption format for Nostr, designed to transport encrypted messages over public relays while hiding sender identity and content from network observers. Unlike standard Nostr private messaging protocols, this format deliberately randomizes timestamps and uses ephemeral keys for the outer envelope to prevent metadata leakage.
Three-Layer Envelope Architecture
BitChat’s encryption scheme relies on a nested structure where each layer serves a distinct privacy function. The implementation in bitchat/Nostr/NostrProtocol.swift (lines 10-17) defines this architecture, which reuses standard Nostr event kinds but packages them in a non-standard way.
Inner Message Layer (Kind 14)
The inner message carries the actual plaintext content as an unsigned event using kind 14 ("DM"). This layer remains unencrypted internally but gets wrapped by subsequent encryption layers. Because it is unsigned at this stage, the content cannot be linked to any specific identity until the seal layer is applied.
The Seal Layer (Kind 13)
The inner message is encrypted to the recipient’s static secp256k1 public key and then signed with the sender’s real identity key, creating a kind 13 "seal" event. According to the source code in NostrProtocol.swift, this layer authenticates the sender while protecting the content. The encryption uses secp256k1 ECDH for shared secret establishment, followed by HKDF-SHA256 key derivation and XChaCha20-Poly1305 authenticated encryption.
Gift-Wrap Outer Envelope (Kind 1059)
Finally, the seal gets wrapped using a one-time ephemeral key generated per envelope, creating a kind 1059 "gift-wrap" event. This outer layer is signed by the ephemeral key rather than the sender’s stable identity, preventing relays from learning who sent the message. The implementation in createPrivateMessage (lines 45-92) handles the complete construction: inner DM → seal → gift-wrap.
Cryptographic Implementation Details
The BitChat private-envelope format employs modern authenticated encryption with specific key derivation parameters that differ from NIP-44 despite sharing some naming conventions.
Key Derivation via ECDH and HKDF-SHA256
The symmetric encryption keys are derived through a two-step process:
- ECDH Key Exchange: The shared secret is computed via secp256k1 ECDH between the sender’s private key and the recipient’s static public key.
- HKDF-SHA256 Derivation: The shared secret is fed into HKDF-SHA256 using the info label
"nip44-v2"(borrowed from NIP-44 but used in BitChat’s specific schedule) to produce a 32-byte symmetric key.
This derivation occurs in the private helper createSeal within NostrProtocol.swift, though the logic is functionally equivalent to the manual reproduction shown below.
XChaCha20-Poly1305 Payload Format
Each encrypted content field uses a specific payload structure:
- Prefix:
v2:(borrowed from NIP-44 conventions) - Encoding: Base64-url encoding of:
- 24-byte nonce
- XChaCha20-Poly1305 ciphertext
- 16-byte authentication tag
The derived 32-byte key from HKDF-SHA256 is used directly with XChaCha20-Poly1305 to provide both confidentiality and integrity. This implementation is detailed in WHITEPAPER.md (section 5.3, lines 83-88).
Creating and Decrypting Private Envelopes in Swift
The NostrProtocol class provides high-level methods for constructing and parsing these envelopes.
Creating a Private Message
To send an encrypted message, use the createPrivateMessage method:
import BitChat
func sendPrivateMessage(
text: String,
recipientPubkey: String,
senderIdentity: NostrIdentity // wraps the sender’s secp256k1 keys
) throws -> NostrEvent {
// Builds the three-layer envelope (inner DM → seal → gift-wrap)
return try NostrProtocol.createPrivateMessage(
content: text,
recipientPubkey: recipientPubkey,
senderIdentity: senderIdentity
)
}
This function (lines 45-92 in NostrProtocol.swift) handles all cryptographic operations including ECDH, HKDF key derivation, XChaCha20-Poly1305 encryption, and Schnorr signing for the seal layer.
Decrypting a Received Envelope
Recipients decrypt the full pipeline using decryptPrivateMessage:
import BitChat
func decryptPrivateMessage(
giftWrap: NostrEvent, // the kind 1059 event from the relay
recipientIdentity: NostrIdentity
) throws -> (content: String, senderPubkey: String, timestamp: Int) {
// Unwraps gift-wrap → verifies seal → opens seal → validates inner DM
return try NostrProtocol.decryptPrivateMessage(
giftWrap: giftWrap,
recipientIdentity: recipientIdentity
)
}
This method (implemented in lines 99-170 of NostrProtocol.swift) returns the plaintext, the authenticated sender’s public key, and the true message timestamp extracted from the encrypted inner layer.
Manual Key Derivation
For interoperability or auditing, you can reproduce the symmetric key derivation:
import CryptoKit
func deriveSymmetricKey(
senderPrivKey: Data, // secp256k1 private key
recipientPubKey: Data // secp256k1 public key
) -> SymmetricKey {
// 1️⃣ ECDH
let sharedSecret = try Secp256k1.sharedSecret(
privateKey: senderPrivKey,
publicKey: recipientPubKey
)
// 2️⃣ HKDF-SHA256 with the "nip44-v2" info label
let hkdf = HKDF<SHA256>(salt: Data(),
sharedInfo: Data("nip44-v2".utf8),
keyLength: 32)
let keyMaterial = hkdf.deriveKey(inputKeyMaterial: sharedSecret)
return SymmetricKey(data: keyMaterial)
}
Security Properties and Limitations
The BitChat private-envelope format provides specific privacy guarantees while making explicit trade-offs regarding key compromise scenarios.
Metadata Protection
- Sender Anonymity: Relays only see the kind 1059 outer envelope and the recipient’s public-key tag (
p). The ephemeral key used for the gift-wrap signature prevents linking messages to the sender’s stable identity. - Timing Obfuscation: The inner message timestamp is encrypted, while the outer envelope’s timestamp is deliberately randomized within ±15 minutes to hide timing patterns and prevent correlation attacks.
Forward Secrecy Limitations
Because the recipient’s static secp256k1 key is used directly for all envelopes addressed to them, the format lacks forward secrecy. If the recipient’s static private key is compromised, an attacker can decrypt all previously stored envelopes addressed to that key. This limitation is documented in SECURITY.md and the README.md notes that the format is proprietary and not compatible with standard NIP specifications.
Summary
- BitChat uses a proprietary three-layer encryption scheme (kind 14 → 13 → 1059) transported over Nostr relays, implemented in
bitchat/Nostr/NostrProtocol.swift. - Cryptographic stack: secp256k1 ECDH for key exchange, HKDF-SHA256 with the
"nip44-v2"label for key derivation, and XChaCha20-Poly1305 for authenticated encryption. - Payload format:
v2:prefix followed by Base64-url encoded 24-byte nonce, ciphertext, and 16-byte authentication tag. - Privacy features: Ephemeral gift-wrap keys hide sender identity from relays, and randomized outer timestamps protect against traffic analysis.
- Security trade-off: The use of static recipient keys means compromised keys can decrypt all historical messages, creating a forward secrecy vulnerability.
- Incompatibility: Despite reusing event kinds and the
v2:prefix similar to NIP-44, BitChat’s format is not compatible with NIP-17, NIP-44, or NIP-59 clients.
Frequently Asked Questions
Is BitChat's private-envelope format compatible with standard Nostr NIPs?
No, BitChat’s private-envelope encryption format is not compatible with NIP-17, NIP-44, or NIP-59 specifications. While it reuses the same Nostr event kinds (14, 13, and 1059) and the v2: content prefix found in NIP-44, the overall construction schedule and layering are proprietary to BitChat. As noted in the repository’s README.md, this is an intentional design decision that diverges from standard Nostr private messaging protocols.
Does BitChat provide forward secrecy for encrypted messages?
No, the current implementation lacks forward secrecy. Because the recipient’s static secp256k1 public key is used directly in the ECDH key exchange for every envelope, compromise of the recipient’s private key would allow decryption of all stored messages previously sent to that key. The SECURITY.md document explicitly acknowledges this limitation as a trade-off for the current architecture.
Which Nostr event kinds does BitChat use for private messaging?
BitChat uses three specific event kinds in a nested structure: kind 14 for the unsigned inner message containing plaintext, kind 13 for the encrypted "seal" that authenticates the sender, and kind 1059 for the "gift-wrap" outer envelope that hides the sender’s identity using an ephemeral key. This three-layer approach is defined in NostrProtocol.swift and detailed in WHITEPAPER.md section 5.3.
Where is the private-envelope encryption implemented in the BitChat codebase?
The core logic resides in bitchat/Nostr/NostrProtocol.swift, specifically in the createPrivateMessage method (lines 45-92) for encryption and decryptPrivateMessage (lines 99-170) for decryption. Key material management is handled in bitchat/Nostr/NostrIdentity.swift, which provides the schnorrSigningKey() helper used during envelope construction. The specification is documented in WHITEPAPER.md (lines 83-88) and security properties are analyzed in SECURITY.md.
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 →