# How BitChat Implements Private Messages over Nostr: Proprietary Three-Layer Protocol Explained

> Discover how BitChat secures private messages on Nostr using its unique three-layer protocol with ECDH and XChaCha20-Poly1305, diverging from standard NIPs for enhanced privacy.

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

---

**BitChat implements private messages over Nostr by wrapping plaintext rumors in a proprietary three-layer envelope—rumor, seal, and gift-wrap—using ECDH key exchange, HKDF-SHA256 derivation, and XChaCha20-Poly1305 encryption, but it deliberately diverges from standard NIP-17, NIP-44, and NIP-59 specifications.**

The `permissionlesstech/bitchat` repository builds a custom cryptographic stack on top of Nostr to protect direct message metadata and content. While it reuses familiar Nostr event kinds, its payload format, key derivation, and sender-anonymity mechanics are intentionally proprietary and not backward-compatible with standard NIPs. The core logic lives in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) and explicitly rejects interoperability with official Nostr Improvement Proposals as documented in [`WHITEPAPER.md`](https://github.com/permissionlesstech/bitchat/blob/main/WHITEPAPER.md) and [`SECURITY.md`](https://github.com/permissionlesstech/bitchat/blob/main/SECURITY.md).

## BitChat Three-Layer Private Message Envelope

BitChat constructs private messages as nested Nostr events. Each layer adds a distinct privacy property, culminating in a kind-1059 gift-wrap event that relays can route without learning the sender’s stable identity.

### Rumor Layer (Kind 14)

The innermost layer is the **rumor**. It is a standard `NostrEvent` containing the sender’s public key, a timestamp, and the plaintext message content. Unlike typical Nostr events, the rumor is intentionally unsigned.

### Seal Layer (Kind 13)

The **seal** encrypts the serialized rumor for the recipient and signs it with the sender’s real Schnorr identity key. In [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift), the `createSeal` function derives a symmetric key from an ECDH shared secret over secp256k1, encrypts the rumor payload, and produces a kind-13 event.

### Gift-Wrap Layer (Kind 1059)

The outermost **gift-wrap** hides the sender’s identity using a one-time ephemeral key. The `createGiftWrap` method generates a fresh `P256K.Schnorr.PrivateKey`, encrypts the seal payload, and signs the outer event with that ephemeral wrap key. The resulting kind-1059 event carries a single `p` tag pointing to the recipient’s public key and nothing else.

## Cryptographic Primitives in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift)

The encryption pipeline relies on a small set of well-defined cryptographic operations implemented in Swift.

### ECDH Shared Secret and Key Derivation

The `deriveSharedSecret` function converts a Schnorr private key into a `KeyAgreement` key, reconstructs a compressed secp256k1 public key from the counterparty (testing both even and odd Y when only the X coordinate is supplied), and executes ECDH.

The `derivePrivateEnvelopeKey` function then feeds that shared secret into **HKDF-SHA256** with an empty salt and the info string `"nip44-v2"`. This produces the symmetric key used for the envelope encryption.

### XChaCha20-Poly1305 and the `v2:` Payload Format

BitChat encrypts every layer with **XChaCha20-Poly1305** via `XChaCha20Poly1305Compat.seal` and `open`. The resulting ciphertext uses a proprietary `v2:` prefix:

```swift
v2:<base64url(nonce24 || ciphertext || tag)>

```

The nonce is 24 bytes. The authentication tag is 16 bytes. The entire payload is base64url-encoded and prefixed with `v2:`, signaling the BitChat-only format.

## How BitChat Differs from Standard NIPs

BitChat’s design intentionally breaks compatibility with official Nostr private-message standards. The following differences are explicitly acknowledged in the repository’s documentation.

- **Kind numbers and payload layout**: BitChat reuses event kinds 13, 14, and 1059 from the NIP-17/NIP-59 family but embeds a proprietary payload layout. Standard NIP-44 introduces its own kind 44 with a different structure.

- **Payload format versioning**: BitChat prefixes ciphertext with `v2:` using a 24-byte nonce and 16-byte tag. NIP-44 uses a `v4:` prefix with a different HKDF schedule and derivation logic.

- **Key derivation salt and info strings**: BitChat calls HKDF-SHA256 with an empty salt and the info string `"nip44-v2"`. NIP-44 derives its conversation key via HKDF-extract using `"nip44"` as the salt and a different info parameter.

- **Forward secrecy**: BitChat does not provide forward secrecy because the shared secret is derived from the recipient’s long-term Nostr key. NIP-44 can be implemented with ephemeral keys that provide forward secrecy.

- **Sender anonymity and relay metadata**: The BitChat gift-wrap is signed by an ephemeral key, and the sender’s real identity signature is sealed inside the encrypted payload. Relays see only the recipient’s `p` tag and randomized timestamps. NIP-17 and NIP-59 may expose the sender’s public key in the outer event or require explicit sender-identity fields.

- **Timestamp randomization**: BitChat randomizes the outer event timestamp by ±15 minutes through `randomizedTimestamp` to prevent timing correlation. The true timestamp is recovered from the decrypted rumor. NIP-44 does not prescribe timestamp randomization.

## Sending and Receiving Private Messages in BitChat

The [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) file exposes high-level methods for creating and consuming private-message envelopes.

### Sending a Private Message

```swift
let sender = try NostrIdentity(pubkeyHex: "...", privateKeyHex: "...")
let recipientPubkey = "abcdef…"

let event = try NostrProtocol.createPrivateMessage(
    content: "Hello, secret world!",
    recipientPubkey: recipientPubkey,
    senderIdentity: sender
)

```

Calling `createPrivateMessage` internally invokes `createSeal` and then `createGiftWrap` according to the implementation in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) lines 45-94. The returned `NostrEvent` is a kind-1059 gift-wrap ready for relay publication.

### Decrypting a Received Gift-Wrap

```swift
let recipient = try NostrIdentity(pubkeyHex: "...", privateKeyHex: "...")
let receivedEvent: NostrEvent = /* fetched from relay */

let (text, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage(
    giftWrap: receivedEvent,
    recipientIdentity: recipient
)

print("From: \(senderPubkey) at \(timestamp): \(text)")

```

The `decryptPrivateMessage` function validates the outer envelope, unwraps the gift-wrap, authenticates the seal, opens the seal, and returns the plaintext rumor along with the original sender identity and true timestamp.

### Testing Malformed Envelopes

For unit-test verification, the repository includes a debug-only helper that produces an intentionally invalid seal signature:

```swift
let brokenEvent = try NostrProtocol.createPrivateMessageWithInvalidSealSignatureForTesting(
    content: "Bad DM",
    recipientPubkey: recipientPubkey,
    senderIdentity: sender
)

```

This helper is defined in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) lines 84-104 under `#if DEBUG` and is exercised in [`bitchatTests/NostrProtocolTests.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchatTests/NostrProtocolTests.swift).

## Summary

- BitChat implements **private messages over Nostr** via a nested three-layer envelope: rumor, seal, and gift-wrap.
- The protocol uses **ECDH** over secp256k1, **HKDF-SHA256** with the info string `"nip44-v2"`, and **XChaCha20-Poly1305** for authenticated encryption.
- The proprietary `v2:` payload format and kind reuse make the implementation **incompatible** with NIP-17, NIP-44, and NIP-59.
- **Sender anonymity** is achieved by signing the outer gift-wrap with an ephemeral key while hiding the real identity inside the encrypted seal.
- Outer timestamps are **randomized by ±15 minutes** to frustrate timing analysis, a property not required by standard NIPs.

## Frequently Asked Questions

### Is BitChat compatible with NIP-44 private messages?

No. According to [`WHITEPAPER.md`](https://github.com/permissionlesstech/bitchat/blob/main/WHITEPAPER.md) lines 85-87 and [`SECURITY.md`](https://github.com/permissionlesstech/bitchat/blob/main/SECURITY.md) lines 25-27, BitChat’s proprietary private-envelope protocol is explicitly **not NIP-17/NIP-44/NIP-59 compatible**. The `v2:` payload prefix, empty-salt HKDF derivation, and proprietary event nesting prevent interoperability with standard Nostr clients.

### How does BitChat hide the sender's identity from Nostr relays?

BitChat hides the sender’s stable identity by signing the outer kind-1059 gift-wrap event with a **one-time ephemeral Schnorr key**. The sender’s real public key appears only inside the encrypted seal payload, which relays cannot read. The outer event exposes only a single `p` tag containing the recipient’s public key.

### What encryption algorithm does BitChat use for direct messages?

BitChat uses **XChaCha20-Poly1305** for authenticated encryption with associated data. The symmetric key is derived via HKDF-SHA256 from an ECDH shared secret, and all operations are performed in [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift) through the `seal` and `open` methods.

### Does BitChat provide forward secrecy for private messages?

No. BitChat does not implement forward secrecy for private messages over Nostr because the shared secret is derived from the recipient’s long-term secp256k1 key. If the recipient’s private key is compromised, all past messages encrypted to that key can be decrypted. Standard NIP-44 implementations may use ephemeral keys to achieve forward secrecy, but BitChat does not.