How BitChat's Nostr Private Envelope Differs from NIP-17 and NIP-44
BitChat uses a proprietary private-envelope format that reuses some Nostr conventions but diverges from NIP-17 and NIP-44 by employing secp256k1 ECDH key agreement instead of Curve25519, custom HKDF derivation, and a unique double-wrapper envelope structure.
BitChat's approach to encrypted messaging on Nostr represents a deliberate architectural departure from standardized specifications. While the protocol adopts familiar elements like the v2: prefix and XChaCha20-Poly1305 encryption, its implementation in NostrProtocol.swift introduces fundamental differences in key exchange, derivation, and message structure that render it incompatible with mainstream Nostr clients.
What Is BitChat's Private Envelope?
BitChat's private-envelope is a custom encryption layer for transporting messages over Nostr relays. The design centers on reusing existing Nostr secp256k1 keypairs rather than introducing separate encryption keys.
The format operates through a double-wrapper architecture:
- Inner layer: An unsigned kind 14 message containing the actual plaintext
- Outer layer: A kind 1059 public envelope that carries the encrypted payload in its
contentfield
This layering is documented in the repository's WHITEPAPER【/cache/repos/github.com/permissionlesstech/bitchat/main/WHITEPAPER.md#L85-L87】 and implemented across several methods in the core protocol file.
Key Agreement: Secp256k1 vs. Curve25519
The most significant divergence from NIP-17 and NIP-44 lies in BitChat's key agreement mechanism.
| Specification | Curve | Implementation Location |
|---|---|---|
| BitChat | secp256k1 (same as Nostr signatures) | NostrProtocol.swift, deriveSharedSecret |
| NIP-17 | Curve25519 (X25519) | Specification-defined |
| NIP-44 | Curve25519 (X25519) | nostr-tools and other implementations |
In NostrProtocol.swift, the shared secret derivation appears as:
// From bitchat/Nostr/NostrProtocol.swift#L99-L106
func deriveSharedSecret(privateKey: secp256k1.Signing.PrivateKey,
publicKey: secp256k1.Signing.PublicKey) -> Data {
// ECDH on secp256k1 with compressed public key format
let sharedSecret = try! privateKey.sharedSecretFromKeyAgreement(
with: publicKey
)
return sharedSecret
}
This choice eliminates the need for separate key management but introduces compatibility barriers. NIP-17 and NIP-44 both require X25519 keypairs, which are mathematically incompatible with secp256k1 points.
Key Derivation and the NIP-44-v2 Label Controversy
BitChat's HKDF-SHA256 derivation reuses the label "nip44-v2" while producing different output keys than actual NIP-44 implementations.
// From bitchat/Nostr/NostrProtocol.swift#L103-L106
func derivePrivateEnvelopeKey(sharedSecret: Data) -> Data {
let salt = Data() // Empty salt
let info = "nip44-v2".data(using: .utf8)!
return HKDF<SHA256>.deriveKey(
inputKeyMaterial: .init(data: sharedSecret),
salt: salt,
info: info,
outputByteCount: 32
)
}
Critical distinction: Despite the label reuse, BitChat does not follow NIP-44's key schedule. The secp256k1-derived shared secret produces entirely different key material than the X25519-based derivation in compliant implementations.
The repository's privacy-assessment explicitly acknowledges this incompatibility【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/privacy-assessment.md#L60-L61】:
"The private envelope format is not NIP-17, NIP-44, or NIP-59 compatible."
Cipher and Nonce Handling
BitChat converges with NIP-44 on XChaCha20-Poly1305 with 24-byte random nonces, diverging from NIP-17's typical ChaCha20-Poly1305 construction.
// Nonce generation from bitchat/Nostr/NostrProtocol.swift#L12-L14
var nonce = Data(count: 24)
let status = SecRandomCopyBytes(kSecRandomDefault, 24, &nonce)
guard status == errSecSuccess else { throw EncryptionError.rngFailure }
The final envelope format concatenates:
v2:<base64url(nonce || ciphertext || tag)>
This matches NIP-44's serialization pattern but with BitChat-derived keys, creating a superficial similarity that masks cryptographic incompatibility.
Forward Secrecy Limitations
None of these protocols provide forward secrecy—a shared limitation worth understanding:
- BitChat: Envelopes sealed to static recipient keys; compromise reveals all prior messages【/cache/repos/github.com/permissionlesstech/bitchat/main/WHITEPAPER.md#L81-L86】
- NIP-17: Static X25519 keys with no ephemeral exchange
- NIP-44: Static X25519 keys with no ephemeral exchange
The privacy-assessment【/cache/repos/github.com/permissionlesstech/bitchat/main/docs/privacy-assessment.md#L60-L61】 documents this explicitly as an accepted trade-off for implementation simplicity.
Practical Implementation
Encrypting a Message
// Encrypt a plaintext message for a Nostr recipient
let plaintext = "Hello, peer!"
let recipientPubkey = "npub1..." // Recipient's Nostr npub (hex)
let senderKey = try NostrIdentity.generate().privateKey
let encryptedPayload = try NostrProtocol.encrypt(
plaintext: plaintext,
recipientPubkey: recipientPubkey,
senderKey: senderKey
)
// encryptedPayload is a string like "v2:<base64url-nonce-cipher-tag>"
Decrypting a Received Envelope
// Decrypt a received private-envelope
let sealedEvent = receivedNostrEvent // NostrEvent containing `content`
let recipientKey = try NostrIdentity.generate().privateKey
let plaintext = try NostrProtocol.decrypt(
ciphertext: sealedEvent.content,
senderPubkey: sealedEvent.pubkey,
recipientKey: recipientKey
)
// plaintext now holds the original message string
Both methods delegate to internal helpers at lines 89-106 and 29-40 of NostrProtocol.swift【/cache/repos/github.com/permissionlesstech/bitchat/main/bitchat/Nostr/NostrProtocol.swift#L89-L106】【/cache/repos/github.com/permissionlesstech/bitchat/main/bitchat/Nostr/NostrProtocol.swift#L29-L40】.
Interoperability Reality
BitChat's private envelope is proprietary by design. The kind 1059/14 double-wrapper structure carries no signaling mechanism that would allow standard Nostr clients to recognize or process these messages. Other clients encounter:
- An unrecognized kind 1059 event
- Opaque
v2:content that decrypts to gibberish with NIP-44 keys
This represents a closed ecosystem choice rather than an implementation gap. The architectural documentation consistently frames this as intentional.
Summary
- BitChat's private envelope is a custom, secp256k1-based encryption format incompatible with NIP-17 and NIP-44
- Key agreement uses ECDH on secp256k1 instead of Curve25519, reusing Nostr signing keys
- HKDF derivation repurposes the
"nip44-v2"label but produces different keys due to the secp256k1 shared secret - Double-wrapper structure (kind 1059 outer, kind 14 inner) is unique to BitChat
- No forward secrecy in any of these protocols—static keys protect all historical messages
- Proprietary by design: only BitChat clients can process these envelopes
Frequently Asked Questions
Can BitChat messages be read by standard Nostr clients?
No. Standard Nostr clients implementing NIP-17 or NIP-44 cannot decrypt BitChat private envelopes. The secp256k1-based key agreement produces different shared secrets than the Curve25519 exchange used by the NIPs, and the double-wrapper envelope structure (kind 1059 containing encrypted kind 14) has no corresponding specification.
Why does BitChat reuse the "nip44-v2" HKDF label if it's incompatible?
The label reuse appears to be a naming convenience rather than standards compliance. According to the source code in NostrProtocol.swift【/cache/repos/github.com/permissionlesstech/bitchat/main/bitchat/Nostr/NostrProtocol.swift#L103-L106】, the string "nip44-v2" is hardcoded as the HKDF info parameter, but because the underlying shared secret comes from secp256k1 ECDH rather than X25519, the derived key material differs completely from actual NIP-44 implementations.
Is BitChat's encryption stronger or weaker than NIP-44?
The security properties are comparable but incompatible. Both use XChaCha20-Poly1305 with 24-byte random nonces. BitChat's secp256k1 ECDH is well-established, though Curve25519 is generally preferred for ECDH due to implementation safety. Neither protocol provides forward secrecy. The repository's privacy-assessment explicitly documents these trade-offs as accepted design choices.
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 →