# How Private Message Encryption Differs Between BitChat's Mesh and Nostr Transports

> Discover how private message encryption differs between BitChat's mesh and Nostr. Understand key derivation with XChaCha20-Poly1305 on both transport layers.

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

---

**BitChat uses XChaCha20-Poly1305 for private messages on both transport layers, but derives encryption keys through different key-agreement mechanisms: Curve25519 DH with HKDF for mesh, and raw X25519 DH for Nostr.**

BitChat is a permissionless messaging application that supports dual transport layers—local peer-to-peer mesh networking and relay-based Nostr protocol communication. Understanding how private message encryption differs between these transports requires examining the source code in `permissionlesstech/bitchat`, particularly the transport implementations and key-derivation protocols.

## Mesh Transport: Local Key Agreement with HKDF

The mesh transport handles offline, peer-to-peer communication where devices connect directly over Bluetooth or local network interfaces. In [`bitchat/Protocols/MeshMessageIdentity.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Protocols/MeshMessageIdentity.swift), BitChat defines a protocol that manages key material for this environment.

### Key Material Structure

Each peer maintains a dedicated mesh key pair separate from Nostr identities:

```swift
protocol MeshMessageIdentity {
    var meshPublicKey: Data { get }
    var meshPrivateKey: Data { get }
    func deriveSharedSecret(with peerPublicKey: Data) throws -> SymmetricKey
}

```

These keys are generated locally and never leave the device, unlike Nostr keys which may be backed up or shared across relay connections.

### Shared Secret Derivation with HKDF

The mesh transport performs a two-step key derivation. First, it executes a Curve25519 Diffie-Hellman exchange, then strengthens the output with HKDF-SHA256:

```swift
extension MeshMessageIdentity {
    func deriveSharedSecret(with peerPublicKey: Data) throws -> SymmetricKey {
        let privateKey = try Curve25519.KeyAgreement.PrivateKey(rawRepresentation: meshPrivateKey)
        let peerPublic = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: peerPublicKey)
        let shared = try privateKey.sharedSecretFromKeyAgreement(with: peerPublic)
        
        return SymmetricKey(data: shared.hkdfDerivedSymmetricKey(
            using: SHA256.self,
            salt: Data(),
            info: Data("bitchat-mesh".utf8),
            outputByteCount: 32
        ))
    }
}

```

The **domain separation string `"bitchat-mesh"`** ensures that mesh encryption keys remain cryptographically isolated from Nostr-derived keys, even if the same underlying Curve25519 private key were somehow reused.

## Nostr Transport: Direct X25519 Key Agreement

The Nostr transport in [`bitchat/Services/NostrTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NostrTransport.swift) provides relay-based messaging compatible with the Nostr protocol ecosystem. It reuses the Nostr identity system managed by `NostrIdentityBridge`.

### Nostr Identity Management

Per [`bitchat/Services/NostrIdentityBridge.swift`](https://github.com/permissionlesstech/bitchat/blob/main/bitchat/Services/NostrIdentityBridge.swift), Nostr identities use bech32-encoded keys:

```swift
struct NostrIdentity: Codable {
    let npub: String // public key in bech32
    let nsec: String // private key in bech32
    
    var privateKey: Data {
        return try! bech32Decode(nsec) // raw 32 bytes
    }
    var publicKey: Data {
        return try! bech32Decode(npub) // raw 32 bytes
    }
}

```

### Direct X25519 Derivation Without HKDF

The Nostr transport performs a streamlined key agreement suitable for NIP-24 encrypted direct messages and other Nostr-compatible encryption:

```swift
func sendMessage(_ message: BitchatMessage, to recipientPubKey: String) async throws {
    // 1. Derive shared secret using sender's private key and recipient's public key
    let senderIdentity = try deps.currentIdentity()
    guard let senderPriv = senderIdentity?.privateKey else { 
        throw TransportError.missingIdentity 
    }
    
    let recipientPubKeyData = try Data(hexString: recipientPubKey)
    let sharedSecret = try X25519.KeyAgreement
        .privateKey(from: senderPriv)
        .sharedSecretFromKeyAgreement(withPeerPublicKey: recipientPubKeyData)
    
    // 2. Encrypt the message payload
    let payload = try message.toBinaryPayload()
    let sealedBox = try XChaCha20Poly1305.seal(payload, using: sharedSecret)
    
    // 3. Create and publish Nostr event
    let event = try NostrEvent(encryptedPayload: sealedBox.combined, 
                               from: senderIdentity!.npub)
    deps.sendEvent(event)
}

```

Notice the **absence of HKDF**—the raw X25519 shared secret feeds directly into the encryption algorithm. This design choice aligns with common Nostr encryption patterns and reduces computational overhead for relay communication.

## Shared Encryption Layer: BitchatMessage Utilities

Both transports converge on a unified encryption implementation in [`localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift`](https://github.com/permissionlesstech/bitchat/blob/main/localPackages/BitFoundation/Sources/BitFoundation/BitchatMessage.swift):

```swift
public struct BitchatMessage {
    // MARK: - Encryption helpers (used by transports)
    public static func encrypt(_ payload: Data, sharedSecret: SymmetricKey) throws -> Data {
        let sealedBox = try XChaCha20Poly1305.seal(payload, using: sharedSecret)
        return sealedBox.combined
    }
    
    public static func decrypt(_ sealedData: Data, sharedSecret: SymmetricKey) throws -> Data {
        let sealedBox = try XChaCha20Poly1305.SealedBox(combined: sealedData)
        return try XChaCha20Poly1305.open(sealedBox, using: sharedSecret)
    }
}

```

The `BitchatMessage` struct handles binary serialization and delegates to Apple's **CryptoKit** framework for authenticated encryption. This abstraction allows both transports to share audit-tested cryptographic primitives while maintaining transport-specific key derivation.

## Critical Differences at a Glance

| Characteristic | Mesh Transport | Nostr Transport |
|:--|:--|:--|
| **Key pair type** | `meshPublicKey` / `meshPrivateKey` | `npub` / `nsec` (bech32 encoded) |
| **Key storage** | Secure Enclave / local keychain | Keychain via `NostrIdentityBridge` |
| **DH algorithm** | Curve25519 | X25519 |
| **Key derivation** | HKDF-SHA256 with `"bitchat-mesh"` info | Direct (no KDF) |
| **Transport envelope** | Raw binary over mesh channel | Nostr event with `encryptedPayload` |
| **Relay dependency** | None (direct P2P) | Requires Nostr relay infrastructure |

## Practical Implementation Example

### Encrypting via Mesh Transport

```swift
// Assume localIdentity conforms to MeshMessageIdentity
let peerPublicKey: Data = // ... obtain from mesh peer discovery
let sharedKey = try localIdentity.deriveSharedSecret(with: peerPublicKey)

let message = BitchatMessage(
    id: UUID(),
    senderID: myPeerID,
    content: "Offline secure message",
    isPrivate: true,
    recipientID: remotePeerID,
    timestamp: Date()
)

let payload = try message.toBinaryPayload()
let encrypted = try BitchatMessage.encrypt(payload, sharedSecret: sharedKey)

// Transmit encrypted data over local mesh channel
meshChannel.send(encrypted)

```

### Encrypting via Nostr Transport

```swift
let transport = NostrTransport(dependencies: deps)

let message = BitchatMessage(
    id: UUID(),
    senderID: myPeerID,
    content: "Relayed secure message",
    isPrivate: true,
    recipientID: remotePeerID,
    timestamp: Date()
)

// Asynchronous relay publication
try await transport.sendMessage(message, to: recipientNpubHex)

```

## Summary

- **Both transports use XChaCha20-Poly1305** from CryptoKit for authenticated encryption, implemented in [`BitchatMessage.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BitchatMessage.swift).
- **Mesh transport applies HKDF-SHA256** with a protocol-specific info string to derive 32-byte symmetric keys from Curve25519 DH outputs.
- **Nostr transport uses raw X25519 shared secrets** without additional key derivation, matching Nostr ecosystem conventions.
- **Key isolation is enforced** through separate identity systems: `MeshMessageIdentity` for local mesh keys and `NostrIdentityBridge` for relay-compatible Nostr keys.
- **Code reuse is maximized** via the `BitchatMessage` encryption helpers, while transport-specific requirements dictate key derivation differences.

## Frequently Asked Questions

### Can the same private key be used for both mesh and Nostr transports?

No. BitChat maintains cryptographically separated key hierarchies. The mesh transport uses keys conforming to `MeshMessageIdentity` while Nostr uses `NostrIdentity` with bech32 encoding. Even if both internally use Curve25519, the key derivation paths differ—mesh applies HKDF with domain separation, preventing cross-protocol key collisions.

### Why does the mesh transport use HKDF when Nostr does not?

The mesh transport's HKDF step with the `"bitchat-mesh"` info string provides **explicit domain separation**, ensuring that a compromised mesh session cannot decrypt Nostr messages or vice versa. Nostr omits HKDF for compatibility with existing Nostr client encryption patterns and to minimize relay latency, accepting the trade-off of reduced domain separation.

### How does BitChat handle key rotation for private messages?

Per the source analysis, `NostrIdentityBridge` provides storage and retrieval for Nostr identities, suggesting key rotation would involve generating new `NostrIdentity` instances. For mesh keys, `MeshMessageIdentity` is a protocol that implementations can satisfy with rotating key material, though the specific rotation policy depends on the concrete identity provider not shown in the examined files.

### Are nonces managed automatically in BitChat's encryption?

Yes. The `XChaCha20Poly1305.seal` operation in CryptoKit automatically generates cryptographically random nonces. The resulting `SealedBox.combined` property concatenates nonce, ciphertext, and authentication tag, ensuring safe nonce handling without caller intervention in both [`NostrTransport.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NostrTransport.swift) and [`BitchatMessage.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BitchatMessage.swift).