# BitChat Private Envelopes vs. Nostr NIP-44 and NIP-59: Key Technical Differences

> Explore the key technical differences between BitChat private envelopes and Nostr NIP-44 NIP-59. Understand BitChat's incompatible encryption and lack of forward secrecy.

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

---

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

1. **Rumor** (`kind = 14`) — an unsigned direct message containing the plaintext
2. **Seal** (`kind = 13`) — the rumor signed by the sender's real key, then encrypted
3. **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`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) builds this stack:

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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):**
1. Generate ephemeral Secp256k1 key pair
2. Compute ECDH shared secret with recipient's **static** public key
3. `HKDF-SHA256(shared_secret, salt=nil, info="nip44-v2")` → encryption key

**Standard NIP-44 v2 schedule:**
1. ECDH between ephemeral and static keys
2. `HKDF-Extract` with conversation-specific salt
3. `HKDF-Expand` with `"nip44-v2"` label **plus additional info parameters**
4. Derive distinct keys for encryption and authentication

This omission in BitChat's [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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

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

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/NostrProtocolTests.swift) around line 255) would use:

```swift
// 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`](https://github.com/permissionlesstech/bitchat/blob/main/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.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) with 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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/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`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) will succeed only for genuine BitChat envelopes.