BitChat Private Envelopes vs. Nostr NIP-44 and NIP-59: Key Technical Differences
BitChat's private envelopes are a proprietary encryption format that reuses Nostr event kinds from NIP-44 and NIP-59 (Gift Wrap) but implements a fundamentally incompatible cryptographic construction lacking forward secrecy.
BitChat is an open-source messaging application that combines Bluetooth mesh networking with Nostr relay fallback for offline-capable communication. While the protocol leverages standard Nostr event kinds, its private-envelope encryption system diverges significantly from the canonical NIP-44 and NIP-59 specifications. This article examines the precise technical distinctions based on the permissionlesstech/bitchat source code.
What BitChat Private Envelopes Actually Are
BitChat's private-envelope system serves as the encrypted fallback channel when Bluetooth mesh connectivity is unavailable. In bitchat/Nostr/NostrProtocol.swift, the implementation explicitly declares itself not compatible with standard NIPs:
"This is NOT NIP-44/59 compatible — it is a proprietary envelope format."
The system constructs a three-layer message:
- Rumor (
kind = 14) — an unsigned direct message containing the plaintext - Seal (
kind = 13) — the rumor signed by the sender's real key, then encrypted - Outer envelope (
kind = 1059) — the sealed message wrapped with an ephemeral key
This layering resembles NIP-59 (Gift Wrap) superficially but uses proprietary cryptography throughout.
Event Kind Usage: Same Numbers, Different Meanings
| BitChat Component | Event Kind | Standard NIP Equivalent |
|---|---|---|
| Unsigned DM (rumor) | kind = 14 |
NIP-17 ephemeral chat |
| Signed+encrypted seal | kind = 13 |
NIP-59 seal |
| Final envelope | kind = 1059 |
None (proprietary) |
The createPrivateMessage method (lines 45-92) in NostrProtocol.swift builds this stack:
// 1. Create unsigned rumor (kind 14)
let rumor = Event(
kind: 14,
pubkey: senderIdentity.pubkey,
content: content,
created_at: UInt32(Date().timeIntervalSince1970)
)
// 2. Sign and encrypt as seal (kind 13)
let seal = try createSeal(rumor: rumor, senderIdentity: senderIdentity, recipientPubkey: recipientPubkey)
// 3. Wrap with ephemeral key (kind 1059)
let giftWrap = try createGiftWrap(seal: seal, recipientPubkey: recipientPubkey)
Other Nostr clients receiving these events will fail to decrypt them, as the underlying key derivation does not follow NIP-44.
Cryptographic Construction: The Critical Divergence
Payload Format
Both BitChat and NIP-44 v2 use a v2: prefix followed by Base64-URL encoded data. However, the similarity ends there.
BitChat format (from WHITEPAPER.md lines 85-87):
v2:<base64url(nonce || ciphertext || tag)>
Where:
- Nonce: 24 bytes (random)
- Ciphertext: variable length (ChaCha20)
- Tag: 16 bytes (Poly1305 MAC)
Key Derivation: The "nip44-v2" Label Misleading
BitChat reuses the exact HKDF label "nip44-v2" but omits critical steps from the NIP-44 specification. Per the source analysis:
BitChat schedule (proprietary):
- Generate ephemeral Secp256k1 key pair
- Compute ECDH shared secret with recipient's static public key
HKDF-SHA256(shared_secret, salt=nil, info="nip44-v2")→ encryption key
Standard NIP-44 v2 schedule:
- ECDH between ephemeral and static keys
HKDF-Extractwith conversation-specific saltHKDF-Expandwith"nip44-v2"label plus additional info parameters- Derive distinct keys for encryption and authentication
This omission in BitChat's NostrProtocol.swift implementation (lines 45-92) means the derived keys differ even when the same inputs are used.
Forward Secrecy: The Security Trade-off
BitChat private envelopes lack forward secrecy, as documented in docs/privacy-assessment.md (lines 60-61):
"The seal is encrypted to the recipient's static key. If that key is later compromised, all past sealed envelopes become readable."
| Property | BitChat Private Envelope | NIP-44 v2 | NIP-59 Gift Wrap |
|---|---|---|---|
| Forward secrecy | No | Yes | No (wrapper only) |
| Compromise impact | All past messages exposed | Past messages protected | Depends on inner encryption |
| Key rotation benefit | None for historical messages | Immediate | None for historical messages |
NIP-44 achieves forward secrecy through its per-conversation key derivation that incorporates ephemeral contributions from both parties. BitChat's static-key encryption to the recipient means compromise is catastrophic for message history.
Practical Code Comparison
Creating a BitChat Private Envelope
import Foundation
// From NostrProtocol.swift — proprietary envelope creation
func createPrivateMessageExample() throws -> NostrEvent {
let content = "Sensitive fallback message"
let recipientPubkey = "02c..." // 64-char hex
let event = try NostrProtocol.createPrivateMessage(
content: content,
recipientPubkey: recipientPubkey,
senderIdentity: myIdentity
)
// event.kind == 1059
// event.content starts with "v2:"
return event
}
Decrypting Received Envelopes
// From NostrProtocol.swift lines 96-70
func decryptReceivedMessage(giftWrapEvent: NostrEvent) throws -> (String, String, UInt32) {
let (plaintext, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage(
giftWrap: giftWrapEvent,
recipientIdentity: myIdentity
)
// Returns: (message content, sender's hex pubkey, Unix timestamp)
return (plaintext, senderPubkey, timestamp)
}
What Standard NIP-44 Would Look Like
For comparison, interoperable NIP-44 encryption (tested in bitchatTests/NostrProtocolTests.swift around line 255) would use:
// Hypothetical standard NIP-44 — NOT what BitChat uses
let payload = nip44Encrypt(
plaintext: message,
conversationKey: derivedConversationKey, // Requires both-party contribution
version: .v2
)
// Results in different ciphertext than BitChat's proprietary format
Interoperability Implications
The permissionlesstech/bitchat repository makes clear design trade-offs:
- Proprietary security model: BitChat prioritizes simple key management over forward secrecy, acceptable for its threat model where Bluetooth mesh is primary and Nostr is fallback
- Intentional incompatibility: The header comment in
NostrProtocol.swift(lines 10-16) explicitly warns that this is not NIP-44/17 - Client lock-in: Only BitChat implementations can decrypt these envelopes—standard Nostr clients see opaque
v2:strings
Summary
- BitChat private envelopes use Nostr event kinds 13, 14, and 1059 but implement a proprietary cryptographic format incompatible with NIP-44 and NIP-59
- The
v2:payload prefix and"nip44-v2"HKDF label are misleading—the key derivation omits required NIP-44 steps - No forward secrecy: Encryption to static recipient keys means key compromise exposes all historical messages
- Implementation resides in
bitchat/Nostr/NostrProtocol.swiftwith explicit non-compatibility warnings in the source header - Other Nostr clients cannot decrypt BitChat private envelopes despite superficial format similarities
Frequently Asked Questions
Can standard Nostr clients read BitChat private messages?
No. Although BitChat uses familiar event kinds (13, 14, 1059) and the v2: prefix, the underlying encryption keys are derived through a proprietary schedule. Standard NIP-44-compatible clients will fail to decrypt the payload, treating it as an opaque string. The WHITEPAPER.md explicitly documents this intentional incompatibility.
Why does BitChat reuse the "nip44-v2" HKDF label if it's not NIP-44 compatible?
The label reuse appears to be implementation convenience rather than standards compliance. As noted in the source analysis, BitChat's HKDF calls omit the Extract→Expand sequence and additional info parameters that NIP-44 v2 requires. The resulting keys are therefore different despite the shared label string.
Is the lack of forward secrecy in BitChat private envelopes a vulnerability?
It is a documented design limitation, not an implementation bug. The docs/privacy-assessment.md explicitly states that compromise of a recipient's static key exposes all past sealed envelopes. This trade-off simplifies key management for a Bluetooth-primary application where Nostr serves only as relay fallback. Users requiring forward secrecy should not rely on BitChat's private-envelope fallback.
How can I identify a BitChat private envelope versus standard NIP-59 Gift Wrap?
Check the outer event kind: 1059 indicates BitChat's proprietary format, while standard NIP-59 uses 1059 for the wrapper with NIP-44 v2 for the inner encryption. Since the event kinds overlap, definitive identification requires attempting decryption—the decryptPrivateMessage function in NostrProtocol.swift will succeed only for genuine BitChat envelopes.
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 →