# BitChat Private-Envelope Encryption Format for Nostr: Technical Architecture and Implementation

> Explore BitChat's private-envelope encryption for Nostr. Learn its technical architecture, XChaCha20-Poly1305 implementation, and compatibility with Nostr event kinds.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: architecture
- Published: 2026-08-23

---

**BitChat implements a proprietary three-layer "private-envelope" encryption scheme using Nostr event kinds 14, 13, and 1059 with ECDH key exchange and XChaCha20-Poly1305, but it is not compatible with standard NIP-17, NIP-44, or NIP-59 specifications.**

The `permissionlesstech/bitchat` repository contains a Swift-based implementation of this BitChat private-envelope encryption format for Nostr, designed to transport encrypted messages over public relays while hiding sender identity and content from network observers. Unlike standard Nostr private messaging protocols, this format deliberately randomizes timestamps and uses ephemeral keys for the outer envelope to prevent metadata leakage.

## Three-Layer Envelope Architecture

BitChat’s encryption scheme relies on a nested structure where each layer serves a distinct privacy function. The implementation in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) (lines 10-17) defines this architecture, which reuses standard Nostr event kinds but packages them in a non-standard way.

### Inner Message Layer (Kind 14)

The **inner message** carries the actual plaintext `content` as an unsigned event using kind 14 ("DM"). This layer remains unencrypted internally but gets wrapped by subsequent encryption layers. Because it is unsigned at this stage, the content cannot be linked to any specific identity until the seal layer is applied.

### The Seal Layer (Kind 13)

The inner message is encrypted to the recipient’s static secp256k1 public key and then signed with the **sender’s real identity key**, creating a kind 13 "seal" event. According to the source code in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift), this layer authenticates the sender while protecting the content. The encryption uses secp256k1 ECDH for shared secret establishment, followed by HKDF-SHA256 key derivation and XChaCha20-Poly1305 authenticated encryption.

### Gift-Wrap Outer Envelope (Kind 1059)

Finally, the seal gets wrapped using a **one-time ephemeral key** generated per envelope, creating a kind 1059 "gift-wrap" event. This outer layer is signed by the ephemeral key rather than the sender’s stable identity, preventing relays from learning who sent the message. The implementation in `createPrivateMessage` (lines 45-92) handles the complete construction: inner DM → seal → gift-wrap.

## Cryptographic Implementation Details

The BitChat private-envelope format employs modern authenticated encryption with specific key derivation parameters that differ from NIP-44 despite sharing some naming conventions.

### Key Derivation via ECDH and HKDF-SHA256

The symmetric encryption keys are derived through a two-step process:

1. **ECDH Key Exchange**: The shared secret is computed via secp256k1 ECDH between the sender’s private key and the recipient’s static public key.
2. **HKDF-SHA256 Derivation**: The shared secret is fed into HKDF-SHA256 using the info label `"nip44-v2"` (borrowed from NIP-44 but used in BitChat’s specific schedule) to produce a 32-byte symmetric key.

This derivation occurs in the private helper `createSeal` within [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift), though the logic is functionally equivalent to the manual reproduction shown below.

### XChaCha20-Poly1305 Payload Format

Each encrypted `content` field uses a specific payload structure:

- **Prefix**: `v2:` (borrowed from NIP-44 conventions)
- **Encoding**: Base64-url encoding of:
  - 24-byte nonce
  - XChaCha20-Poly1305 ciphertext
  - 16-byte authentication tag

The derived 32-byte key from HKDF-SHA256 is used directly with XChaCha20-Poly1305 to provide both confidentiality and integrity. This implementation is detailed in [`WHITEPAPER.md`](https://github.com/permissionlesstech/bitchat/blob/main/WHITEPAPER.md) (section 5.3, lines 83-88).

## Creating and Decrypting Private Envelopes in Swift

The `NostrProtocol` class provides high-level methods for constructing and parsing these envelopes.

### Creating a Private Message

To send an encrypted message, use the `createPrivateMessage` method:

```swift
import BitChat

func sendPrivateMessage(
    text: String,
    recipientPubkey: String,
    senderIdentity: NostrIdentity   // wraps the sender’s secp256k1 keys
) throws -> NostrEvent {
    // Builds the three-layer envelope (inner DM → seal → gift-wrap)
    return try NostrProtocol.createPrivateMessage(
        content: text,
        recipientPubkey: recipientPubkey,
        senderIdentity: senderIdentity
    )
}

```

This function (lines 45-92 in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift)) handles all cryptographic operations including ECDH, HKDF key derivation, XChaCha20-Poly1305 encryption, and Schnorr signing for the seal layer.

### Decrypting a Received Envelope

Recipients decrypt the full pipeline using `decryptPrivateMessage`:

```swift
import BitChat

func decryptPrivateMessage(
    giftWrap: NostrEvent,            // the kind 1059 event from the relay
    recipientIdentity: NostrIdentity
) throws -> (content: String, senderPubkey: String, timestamp: Int) {
    // Unwraps gift-wrap → verifies seal → opens seal → validates inner DM
    return try NostrProtocol.decryptPrivateMessage(
        giftWrap: giftWrap,
        recipientIdentity: recipientIdentity
    )
}

```

This method (implemented in lines 99-170 of [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift)) returns the plaintext, the authenticated sender’s public key, and the true message timestamp extracted from the encrypted inner layer.

### Manual Key Derivation

For interoperability or auditing, you can reproduce the symmetric key derivation:

```swift
import CryptoKit

func deriveSymmetricKey(
    senderPrivKey: Data,   // secp256k1 private key
    recipientPubKey: Data  // secp256k1 public key
) -> SymmetricKey {
    // 1️⃣ ECDH
    let sharedSecret = try Secp256k1.sharedSecret(
        privateKey: senderPrivKey,
        publicKey:  recipientPubKey
    )
    // 2️⃣ HKDF-SHA256 with the "nip44-v2" info label
    let hkdf = HKDF<SHA256>(salt: Data(),
                            sharedInfo: Data("nip44-v2".utf8),
                            keyLength: 32)
    let keyMaterial = hkdf.deriveKey(inputKeyMaterial: sharedSecret)
    return SymmetricKey(data: keyMaterial)
}

```

## Security Properties and Limitations

The BitChat private-envelope format provides specific privacy guarantees while making explicit trade-offs regarding key compromise scenarios.

### Metadata Protection

- **Sender Anonymity**: Relays only see the kind 1059 outer envelope and the recipient’s public-key tag (`p`). The ephemeral key used for the gift-wrap signature prevents linking messages to the sender’s stable identity.
- **Timing Obfuscation**: The inner message timestamp is encrypted, while the outer envelope’s timestamp is deliberately randomized within ±15 minutes to hide timing patterns and prevent correlation attacks.

### Forward Secrecy Limitations

Because the recipient’s **static secp256k1 key** is used directly for all envelopes addressed to them, the format **lacks forward secrecy**. If the recipient’s static private key is compromised, an attacker can decrypt all previously stored envelopes addressed to that key. This limitation is documented in [`SECURITY.md`](https://github.com/permissionlesstech/bitchat/blob/main/SECURITY.md) and the [`README.md`](https://github.com/permissionlesstech/bitchat/blob/main/README.md) notes that the format is proprietary and not compatible with standard NIP specifications.

## Summary

- **BitChat uses a proprietary three-layer encryption scheme** (kind 14 → 13 → 1059) transported over Nostr relays, implemented in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift).
- **Cryptographic stack**: secp256k1 ECDH for key exchange, HKDF-SHA256 with the `"nip44-v2"` label for key derivation, and XChaCha20-Poly1305 for authenticated encryption.
- **Payload format**: `v2:` prefix followed by Base64-url encoded 24-byte nonce, ciphertext, and 16-byte authentication tag.
- **Privacy features**: Ephemeral gift-wrap keys hide sender identity from relays, and randomized outer timestamps protect against traffic analysis.
- **Security trade-off**: The use of static recipient keys means compromised keys can decrypt all historical messages, creating a forward secrecy vulnerability.
- **Incompatibility**: Despite reusing event kinds and the `v2:` prefix similar to NIP-44, BitChat’s format is not compatible with NIP-17, NIP-44, or NIP-59 clients.

## Frequently Asked Questions

### Is BitChat's private-envelope format compatible with standard Nostr NIPs?

No, BitChat’s private-envelope encryption format is **not compatible** with NIP-17, NIP-44, or NIP-59 specifications. While it reuses the same Nostr event kinds (14, 13, and 1059) and the `v2:` content prefix found in NIP-44, the overall construction schedule and layering are proprietary to BitChat. As noted in the repository’s [`README.md`](https://github.com/permissionlesstech/bitchat/blob/main/README.md), this is an intentional design decision that diverges from standard Nostr private messaging protocols.

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

No, the current implementation **lacks forward secrecy**. Because the recipient’s static secp256k1 public key is used directly in the ECDH key exchange for every envelope, compromise of the recipient’s private key would allow decryption of all stored messages previously sent to that key. The [`SECURITY.md`](https://github.com/permissionlesstech/bitchat/blob/main/SECURITY.md) document explicitly acknowledges this limitation as a trade-off for the current architecture.

### Which Nostr event kinds does BitChat use for private messaging?

BitChat uses three specific event kinds in a nested structure: **kind 14** for the unsigned inner message containing plaintext, **kind 13** for the encrypted "seal" that authenticates the sender, and **kind 1059** for the "gift-wrap" outer envelope that hides the sender’s identity using an ephemeral key. This three-layer approach is defined in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) and detailed in [`WHITEPAPER.md`](https://github.com/permissionlesstech/bitchat/blob/main/WHITEPAPER.md) section 5.3.

### Where is the private-envelope encryption implemented in the BitChat codebase?

The core logic resides in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift), specifically in the `createPrivateMessage` method (lines 45-92) for encryption and `decryptPrivateMessage` (lines 99-170) for decryption. Key material management is handled in [`bitchat/Nostr/NostrIdentity.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrIdentity.swift), which provides the `schnorrSigningKey()` helper used during envelope construction. The specification is documented in [`WHITEPAPER.md`](https://github.com/permissionlesstech/bitchat/blob/main/WHITEPAPER.md) (lines 83-88) and security properties are analyzed in [`SECURITY.md`](https://github.com/permissionlesstech/bitchat/blob/main/SECURITY.md).