# How BitChat's Nostr Private Envelope Differs from NIP-17 and NIP-44

> Discover how BitChat's Nostr private envelope differs from NIP-17 and NIP-44 using secp256k1 ECDH custom HKDF and a unique double-wrapper structure for enhanced privacy and security.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: deep-dive
- Published: 2026-08-03

---

**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`](https://github.com/permissionlesstech/bitchat/blob/main/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**:

1. **Inner layer**: An unsigned kind 14 message containing the actual plaintext
2. **Outer layer**: A kind 1059 public envelope that carries the encrypted payload in its `content` field

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`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift), `deriveSharedSecret` |
| **NIP-17** | **Curve25519** (X25519) | Specification-defined |
| **NIP-44** | **Curve25519** (X25519) | `nostr-tools` and other implementations |

In [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift), the shared secret derivation appears as:

```swift
// 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.

```swift
// 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.

```swift
// 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

```swift
// 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

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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.