# How BitChat Implements a Layered Encryption Protocol for Private Messages over Nostr

> Discover how BitChat's layered encryption protocol secures private messages over Nostr. Explore its two-layer scheme combining Nostr security and a private envelope for ultimate confidentiality.

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

---

**BitChat uses a two-layer encryption scheme that combines Nostr transport security with a proprietary application-level "private envelope" to protect message confidentiality from public relays.**

The BitChat messaging application (`permissionlesstech/bitchat`) routes private conversations over the public Nostr relay network while preventing relays from reading message content or identifying recipients. This article explains the layered encryption protocol for private messages over Nostr, detailing how the Swift implementation in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) wraps sensitive payloads in a custom encrypted envelope before they ever reach decentralized infrastructure.

## The Two-Layer Encryption Architecture

BitChat’s security model relies on distinct encryption layers: one protecting data in transit across the Nostr protocol, and another ensuring only the intended recipient can read the message content.

### Transport-Level Encryption (Nostr)

At the network layer, BitChat publishes messages as Nostr **kind 1059 events**. Each event carries a **secp256k1 Schnorr signature** created with the sender’s private key. This provides authenticity and integrity—relays can verify the sender’s identity and detect tampering—but the `content` field remains encrypted to prevent snooping.

### Application-Level Encryption (Private Envelope)

Before a message enters the Nostr event, BitChat encrypts the payload using a proprietary "private-envelope" format defined in [`WHITEPAPER.md`](https://github.com/permissionlesstech/bitchat/blob/main/WHITEPAPER.md). This envelope ensures that only the recipient possesses the keys necessary to decrypt the content, even if the relay colludes or the event leaks.

The envelope construction follows this sequence:

1. **Derive shared secret**: Uses **secp256k1 ECDH** via `deriveSharedSecret` between the sender’s private key and recipient’s public key (lines 95–104 of [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift)).
2. **Derive symmetric key**: Applies **HKDF-SHA256** through `derivePrivateEnvelopeKey` to generate an encryption key from the shared secret (lines 104–107).
3. **Encrypt payload**: Seals the plaintext using **XChaCha20-Poly1305** with a 24-byte random nonce, producing ciphertext and a 16-byte authentication tag (lines 118–122).
4. **Format envelope**: Prepends the literal `v2:` prefix and encodes the concatenated nonce+ciphertext+tag as **base64-url**.

Because this encryption occurs *before* Nostr event creation, relays observe only the sender’s public key, payload size, and timing metadata—they cannot determine the message content or the recipient’s static Nostr key.

## Cryptographic Implementation Details

The implementation in [`bitchat/Nostr/NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Nostr/NostrProtocol.swift) handles the envelope lifecycle through several specialized functions.

### Shared Secret Derivation (ECDH)

The `deriveSharedSecret` function computes an ECDH shared point between the sender’s secp256k1 private scalar and the recipient’s x-only public key. This yields the initial entropy from which all message keys derive.

### Key Derivation (HKDF-SHA256)

`derivePrivateEnvelopeKey` feeds the shared secret into HKDF-SHA256 to produce a uniform 256-bit symmetric key. This construction prevents direct use of raw ECDH output and enables domain separation for the encryption context.

### Symmetric Encryption (XChaCha20-Poly1305)

BitChat utilizes `XChaCha20Poly1305Compat.seal` (defined in [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift)) to handle the 24-byte nonce required by the XChaCha20-Poly1305 construction. The extended nonce space allows safer random generation without collision concerns, while Poly1305 provides authenticated encryption protecting against ciphertext manipulation.

### Envelope Format and Decoding

The final envelope string follows the pattern:

```

v2:<base64url(24-byte nonce || ciphertext || 16-byte tag)>

```

During decryption, [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) validates the `v2:` prefix, decodes the base64-url payload, and attempts decryption with both even-Y and odd-Y public-key formats to accommodate x-only pubkey representations (lines 52–74).

## Code Examples

### Encrypting a Private Message

This Swift function demonstrates creating an encrypted envelope using the internal `encrypt` method:

```swift
import BitChat

func createEncryptedEnvelope(
    message: String,
    recipientPubkey: String,
    senderPrivateKey: P256K.Schnorr.PrivateKey
) throws -> String {
    // Returns the "v2:..." string ready for Nostr event content
    return try NostrProtocol.encrypt(
        plaintext: message,
        recipientPubkey: recipientPubkey,
        senderKey: senderPrivateKey
    )
}

```

The implementation internally performs ECDH, HKDF-SHA256 key derivation, generates a secure 24-byte nonce, and seals the message with XChaCha20-Poly1305.

### Decrypting a Received Envelope

Recipients verify and open envelopes using the complementary `decrypt` function:

```swift
func decryptMessage(
    envelope: String,                 // Must start with "v2:"
    senderPubkey: String,
    recipientPrivateKey: P256K.Schnorr.PrivateKey
) throws -> String {
    return try NostrProtocol.decrypt(
        ciphertext: envelope,
        senderPubkey: senderPubkey,
        recipientKey: recipientPrivateKey
    )
}

```

The decryption routine handles base64-url decoding, shared secret recomputation, and automatic retry with alternating Y-parity bits when dealing with compressed public keys.

### Publishing to Nostr Relays

Once encrypted, the envelope embeds into a standard Nostr event:

```swift
let encryptedContent = try createEncryptedEnvelope(
    message: "Hello, private world",
    recipientPubkey: recipientHex,
    senderPrivateKey: myKey
)

let event = NostrEvent(
    kind: 1059,
    content: encryptedContent,
    tags: [],
    createdAt: Date()
)

try nostrClient.publish(event)  // Signed with secp256k1 Schnorr before transmission

```

## Security Properties and Limitations

The layered encryption protocol for private messages over Nostr provides **confidentiality** and **integrity** against passive relay observers and active network attackers. However, as noted in [`docs/privacy-assessment.md`](https://github.com/permissionlesstech/bitchat/blob/main/docs/privacy-assessment.md), the protocol does **not** provide forward secrecy. If an attacker compromises the recipient’s static Nostr private key at a future date, they can decrypt all past messages encrypted to that key because the ECDH shared secret remains deterministic.

Additionally, this envelope format is **not** compatible with standard Nostr encryption specifications such as NIP-44, NIP-17, or NIP-59. It represents a deliberate design choice by BitChat to prioritize recipient privacy over protocol compliance, ensuring that relays cannot identify the intended recipient from event metadata.

## Summary

- **Two-layer protection**: Nostr Schnorr signatures provide transport authenticity while BitChat’s private envelope protects content confidentiality.
- **ECDH + HKDF + XChaCha20-Poly1305**: The cryptographic stack derives shared secrets via secp256k1 ECDH, keys via HKDF-SHA256, and encrypts via XChaCha20-Poly1305 with 24-byte nonces.
- **v2: envelope format**: Encrypted payloads use a `v2:` prefix with base64-url encoding, implemented in [`NostrProtocol.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrProtocol.swift) and [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift).
- **No forward secrecy**: Static key compromise exposes historical messages; the protocol prioritizes metadata privacy over perfect forward secrecy.
- **Kind 1059 events**: All private messages travel as kind 1059 Nostr events, signed but opaque to relay infrastructure.

## Frequently Asked Questions

### How does BitChat’s encryption differ from NIP-44?

BitChat’s private envelope uses secp256k1 ECDH combined with XChaCha20-Poly1305 and a `v2:` framing format, whereas NIP-44 uses ChaCha20-Poly1305 with a different nonce construction and key derivation strategy. The BitChat implementation specifically avoids revealing recipient identity in Nostr tags, which standard NIP-44 implementations typically expose.

### Why does BitChat use XChaCha20-Poly1305 instead of AES-GCM?

XChaCha20-Poly1305 utilizes a 24-byte nonce, allowing safer random nonce generation without collision risks associated with shorter 12-byte nonces. This choice simplifies the implementation in [`XChaCha20Poly1305Compat.swift`](https://github.com/permissionlesstech/bitchat/blob/main/XChaCha20Poly1305Compat.swift) while maintaining high security margins for long-lived messaging sessions.

### Can relays determine who is receiving a private message?

No. Because the recipient’s public key does not appear in Nostr event tags and the content is encrypted via the private envelope before transmission, relays see only the sender’s public key, the payload size, and timing information. The recipient identity remains computationally hidden from relay operators.

### What happens if someone steals my BitChat private key?

If your secp256k1 Schnorr private key is compromised, an attacker can derive the same shared secrets for all past messages sent to you and decrypt them, since the protocol lacks forward secrecy. You should rotate keys immediately and treat previously sent messages as potentially exposed.