# How Private Messages Are Encrypted When Routed Over Nostr Using BitChat Private Envelopes

> Discover how BitChat encrypts private messages over Nostr with private envelopes. Learn about XChaCha20-Poly1305 and secp256k1 for secure, anonymous direct messaging on public relays.

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

---

**BitChat encrypts private messages using a three-layer private envelope protocol that combines XChaCha20-Poly1305 authenticated encryption with secp256k1 key agreement to route end-to-end encrypted direct messages over public Nostr relays while hiding sender identity.**

The `permissionlesstech/bitchat` repository implements a custom cryptographic protocol for secure messaging that leverages Nostr's relay network without relying on standard NIP-17 implementations. This architecture wraps sensitive content in nested encryption layers—referred to as **Rumor**, **Seal**, and **Gift-Wrap**—to ensure confidentiality and sender anonymity. The implementation resides primarily in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift), utilizing `XChaCha20Poly1305Compat` for authenticated encryption and the secp256k1 curve for key exchange.

## The Three-Layer Private Envelope Architecture

BitChat's private envelope system constructs messages through three distinct cryptographic layers, each serving a specific security purpose. The protocol uses Nostr event kinds **1059** (outer gift-wrap), **13** (seal), and **14** (rumor/DM), though it does not maintain strict NIP-17 compatibility.

### Layer 1 – The Rumor (Plaintext Payload)

The innermost layer contains the actual message content as an unsigned Nostr event. In [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift), the **Rumor** is instantiated as a `NostrEvent` with `kind = .dm` (kind 14) containing the raw plaintext content.

This layer remains unencrypted initially and carries no cryptographic signatures. It serves purely as the payload container that subsequent layers will encrypt and authenticate. The Rumor includes the message text and timestamp but lacks sender authentication until processed by the Seal layer.

### Layer 2 – The Seal (Authentication and Encryption)

The **Seal** encrypts the Rumor to the recipient's static Nostr public key while cryptographically signing it with the sender's real identity key. This layer provides both confidentiality and non-repudiable sender authentication.

According to the source code in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift), the `createSeal` function performs the following operations:

1. Derives a shared secret using **secp256k1 key agreement** between the sender's Schnorr signing key (`senderIdentity.schnorrSigningKey()`) and the recipient's public key.
2. Runs **HKDF-SHA256** to derive encryption keys from the shared secret.
3. Encrypts the serialized Rumor using **XChaCha20-Poly1305** via `XChaCha20Poly1305Compat`.
4. Signs the resulting ciphertext with the sender's identity key to produce a kind 13 event.

The Seal ensures that only the intended recipient can decrypt the message while allowing them to verify the sender's identity through the attached Schnorr signature (`seal.isValidSignature()`).

### Layer 3 – The Gift-Wrap (Anonymity and Store-and-Forward)

The outermost **Gift-Wrap** layer hides the sender's identity from public Nostr relays and enables asynchronous message delivery. This layer generates an ephemeral throwaway key pair within the `createGiftWrap` function to sever the link between the sender's identity and the relay-visible metadata.

The Gift-Wrap construction process:

- Generates a temporary ephemeral key pair that exists only for this specific message transmission.
- Re-encrypts the Seal layer using XChaCha20-Poly1305 with a fresh key derived from the ephemeral key and recipient's public key.
- Creates a kind 1059 Nostr event containing only a single `p` tag identifying the recipient (`["p", recipientPubkey]`).
- Signs the outer event with the ephemeral key, not the sender's real identity key.

This architecture prevents network observers from correlating messages with specific senders while allowing the recipient to decrypt the content using their static private key.

## Creating Private Envelopes in NostrProtocol.swift

The `createPrivateMessage` function in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) orchestrates the three-layer construction. The following Swift implementation demonstrates the outbound encryption workflow:

```swift
import Foundation

// Load sender's static Nostr identity from secure storage
let senderIdentity = try NostrIdentity.loadFromKeychain()
let recipientPubkey = "npub1example..."
let plaintextMessage = "Sensitive information here"

// Construct the three-layer private envelope
let privateEnvelope = try NostrProtocol.createPrivateMessage(
    content: plaintextMessage,
    recipientPubkey: recipientPubkey,
    senderIdentity: senderIdentity
)

// Publish to any Nostr relay (kind 1059 event)
try NostrRelayManager.shared.publish(event: privateEnvelope)

```

Internally, `createPrivateMessage` calls `createSeal` to generate the encrypted, signed middle layer, then passes this to `createGiftWrap` to generate the final kind 1059 envelope. The implementation uses [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift) for all symmetric encryption operations, ensuring 256-bit security and authenticated data integrity.

## Decrypting Private Messages

Recipients process incoming private envelopes through the inverse operations implemented in `decryptPrivateMessage`. The decryption protocol validates each layer before exposing the plaintext:

```swift
func handleIncomingEvent(_ event: NostrEvent, recipient: NostrIdentity) throws {
    // Validate outer envelope structure (kind 1059, single p-tag)
    guard event.kind == NostrProtocol.EventKind.giftWrap.rawValue,
          event.tags.count == 1,
          event.tags[0][0] == "p",
          event.tags[0][1] == recipient.publicKeyHex else {
        return
    }
    
    // Decrypt the three-layer envelope
    let (content, senderPubkey, timestamp) = try NostrProtocol.decryptPrivateMessage(
        giftWrap: event,
        recipientIdentity: recipient
    )
    
    print("Received: \(content)")
    print("From: \(senderPubkey)")
    print("Sent: \(timestamp)")
}

```

The decryption sequence follows these steps as implemented in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift):

1. **Validate the Gift-Wrap**: Verify the kind 1059 event has exactly one `p` tag, meets size constraints, and carries a valid signature from the ephemeral key.
2. **Unwrap the Gift-Wrap**: Use the recipient's Schnorr signing key to derive the shared secret and decrypt the XChaCha20-Poly1305 ciphertext, revealing the Seal.
3. **Authenticate the Seal**: Confirm the Seal is kind 13 with empty tags and validate its Schnorr signature against the sender's public key extracted from the Seal.
4. **Open the Seal**: Derive the shared secret using the recipient's private key and the sender's public key (from the Seal), then decrypt to reveal the Rumor.
5. **Extract Content**: Return the plaintext, sender identity, and timestamp from the Rumor.

## Security Properties and Limitations

The BitChat private envelope protocol provides **end-to-end encryption** and **sender anonymity** but makes specific trade-offs regarding forward secrecy. As documented in the repository's [`WHITEPAPER.md`](https://github.com/permissionlesstech/bitchat/blob/main/WHITEPAPER.md) (sections 5.2–5.3), the protocol deliberately does not provide forward secrecy for the outer envelope layer.

If an adversary compromises the recipient's static Nostr private key at a future date, they can decrypt all previously stored Gift-Wrap ciphertexts addressed to that key. This limitation exists because the Gift-Wrap layer uses static key agreement rather than ephemeral keys for the recipient side. However, the Seal layer protects the sender identity and message content from passive network observers and provides authentication that cannot be forged by third parties.

## Summary

- **Three-layer architecture**: BitChat uses Rumor (plaintext), Seal (encrypted + signed), and Gift-Wrap (anonymized) layers to construct private messages.
- **Cryptographic primitives**: The implementation relies on secp256k1 key agreement, HKDF-SHA256 key derivation, and XChaCha20-Poly1305 authenticated encryption.
- **Source location**: Core logic resides in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) with encryption utilities in [`bitchat/Nostr/XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/XChaCha20Poly1305Compat.swift).
- **Key functions**: `createPrivateMessage` constructs envelopes while `decryptPrivateMessage` validates and unwraps them.
- **Anonymity**: The Gift-Wrap layer (kind 1059) uses ephemeral keys to hide sender identity from relays.
- **Forward secrecy limitation**: Compromised recipient keys can decrypt historical Gift-Wrap messages, though Seal authentication remains intact.

## Frequently Asked Questions

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

BitChat uses **XChaCha20-Poly1305** for all symmetric encryption operations within the private envelope protocol. This authenticated encryption with associated data (AEAD) algorithm provides 256-bit security and protects against tampering. The keys are derived via HKDF-SHA256 from secp256k1 shared secrets generated through Diffie-Hellman key agreement between sender and recipient keys.

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

The protocol generates an **ephemeral throwaway key pair** during the Gift-Wrap creation phase (`createGiftWrap`). The outer kind 1059 event is signed by this temporary key rather than the sender's real identity key. Since the ephemeral key is discarded immediately after publication, network observers and relays cannot correlate the visible signature with the actual sender's public key, while the intended recipient can still unwrap the layers to verify the true sender identity embedded in the Seal.

### Can someone decrypt my old messages if they steal my private key?

**Yes**, with limitations. According to the BitChat whitepaper sections 5.2–5.3, the private envelope protocol does **not** provide forward secrecy for the Gift-Wrap layer. If an attacker obtains your static Nostr private key, they can decrypt all stored Gift-Wrap messages addressed to your public key. However, they cannot forge past sender signatures or modify the authenticated content within the Seal layer, as that requires the sender's private key.

### Is BitChat compatible with standard Nostr NIP-17 encrypted messages?

**No**, BitChat's private envelope protocol is **not** NIP-17 compatible. While it uses similar event kinds (1059 for gift-wrap, 13 for seals, and 14 for DMs), the specific construction of the encryption layers, key derivation methods, and metadata handling differ from the NIP-17 specification. Messages encrypted with BitChat can only be decrypted by BitChat clients using the [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) implementation.