Noise Protocol Parameters for BitChat's Bluetooth Mesh Encryption: Complete Implementation Guide

BitChat implements a fixed Noise_XX_25519_ChaChaPoly_SHA256 configuration for Bluetooth mesh encryption with no runtime algorithm negotiation, using static X25519 keys to derive 8-byte peer IDs from SHA-256 fingerprints.

The permissionlesstech/bitchat repository uses deterministic Noise Protocol parameters to secure its decentralized Bluetooth mesh network. Every encrypted packet relies on hardcoded cryptographic primitives that eliminate negotiation attacks while ensuring consistent security across all mesh nodes.

Core Noise Protocol Configuration

BitChat hardcodes the following Noise Protocol parameters according to the Noise specification notation. There is no runtime negotiation of algorithms.

Parameter Value (Noise notation) Description
Handshake pattern XX Mutual-authentication handshake where both peers exchange static keys after an initial DH exchange
DH function 25519 X25519 (Curve25519) Elliptic Curve Diffie-Hellman
Cipher ChaChaPoly ChaCha20-Poly1305 AEAD providing confidentiality and integrity
Hash SHA-256 Transcript hashing and peer ID derivation
Static key length 32 bytes X25519 private keys stored per device

Payload Type Identifiers

BitChat defines specific payload type IDs placed after the single-byte inner-type tag inside encrypted Noise payloads. These values are documented in docs/PRIVATE-MEDIA-MIGRATION.md and docs/PUSH-TO-TALK-DESIGN.md.

  • 0x20 — Private-file data chunks
  • 0x21 — Authenticated-peer-state (handshake-completion marker)
  • 0x08 — Live-voice frames for push-to-talk functionality

Peer ID Derivation from Static Keys

Each device generates a static Noise key once and stores it in the device keychain. The peer ID derivation follows a deterministic process implemented in PeerID.swift:

  1. Generate or load the 32-byte X25519 static private key
  2. Extract the public key for mesh announcement packets
  3. Compute SHA-256(noiseStaticKey) and extract bytes 0-7 (8 bytes total)
  4. Convert to 16 hexadecimal characters for the packet header

This design ensures the same 32-byte X25519 public key appears in every mesh announce packet, with the short peer ID serving as the immutable header identifier.

Implementation Architecture

The encryption layer spans several Swift files in localPackages/BitFoundation/Sources/BitFoundation/:

Swift Implementation Examples

Generating the Static Noise Key

Devices load a persisted static key generated once during initialization:

import BitFoundation

// Load the persisted static key from the device keychain
let staticKey = try NoiseKeyStore.shared.loadStaticKey()
// Derive the 32-byte X25519 public key for mesh announcements
let staticPublic = staticKey.publicKey

Deriving the 8-Byte Peer ID

The short peer ID calculation uses SHA-256 truncation as defined in docs/PEER-ID-ROTATION.md:

// Generate 16-hex character peer ID from first 8 bytes of hash
let peerID = PeerID.shortFromNoiseKey(staticPublic)
// peerID appears in packet headers as the sender identifier

Performing the Noise XX Handshake

The initiator creates a session using the remote peer's static public key obtained from announce packets:

let session = try NoiseSessionManager.shared.initiateXXHandshake(
    localStaticKey: staticKey,
    remoteStaticPublic: remotePublicKey
)

// Post-handshake: session.sharedSecret contains the 32-byte ChaCha20-Poly1305 key

Encrypting Mesh Payloads

Payloads include a 1-byte type tag followed by data, encrypted with AEAD:

// Prepare private-file payload with type identifier 0x20
let payload = Data([0x20]) + fileChunk

let encrypted = try NoiseEncryptionService.encrypt(
    payload: payload,
    using: session.sharedSecret,
    aad: nil  // Additional authenticated data unused for BLE
)

// Output format: ChaCha20-Poly1305 ciphertext || 16-byte Poly1305 tag

Building BLE Packets

Final packet assembly combines the peer ID with encrypted payload. The total size must fit within 256 bytes:

var packet = Data()
packet.append(peerID)     // 8-byte sender ID from SHA-256 fingerprint
packet.append(encrypted)  // Encrypted Noise payload
// MessagePadding.swift handles automatic padding to ≤ 256 bytes

Decrypting Received Messages

Receivers decrypt using the established session key:

let decrypted = try NoiseEncryptionService.decrypt(
    ciphertext: encrypted,
    using: session.sharedSecret,
    aad: nil
)

// decrypted[0] contains the inner-type byte (e.g., 0x20, 0x21, or 0x08)
// remaining bytes contain the original payload data

Security Characteristics

Key design decisions from the BitChat source code include:

  • No algorithm negotiation — All parameters are compile-time constants, eliminating downgrade attacks
  • Static key immutability — The 32-byte X25519 key never rotates, ensuring persistent peer identities across sessions
  • Deterministic peer IDs — The SHA-256(noiseStaticKey)[0..8] derivation creates collision-resistant short identifiers that never change
  • Frame size constraints — MessagePadding.swift enforces the 256-byte BLE limit including the 16-byte Poly1305 authentication tag

Summary

  • BitChat uses Noise_XX_25519_ChaChaPoly_SHA256 as the fixed cipher suite for all Bluetooth mesh encryption
  • The XX handshake pattern provides mutual authentication through static X25519 key exchange after ephemeral DH
  • Peer IDs are deterministic 8-byte values derived from the first 8 bytes of SHA-256(noiseStaticKey), implemented in PeerID.swift
  • Payload type identifiers 0x20, 0x21, and 0x08 distinguish file data, authentication state, and voice frames inside encrypted payloads
  • Implementation resides in NoiseSessionManager.swift, NoiseEncryptionService.swift, and PeerID.swift within the BitFoundation package

Frequently Asked Questions

What Noise handshake pattern does BitChat use?

BitChat implements the XX pattern, which performs mutual authentication by having both peers exchange static keys after an initial ephemeral Diffie-Hellman exchange. This pattern is implemented in NoiseSessionManager.swift through the handshakeXX routine, providing forward secrecy and identity hiding during the handshake phase.

How is the peer ID generated from Noise keys?

The peer ID derives from the first 8 bytes of SHA-256(noiseStaticKey), implemented in PeerID.swift as a 16-hexadecimal character string. This fingerprint-based approach, documented in docs/PEER-ID-ROTATION.md, ensures the same device always presents the same identifier in mesh announce packets without requiring persistent network state.

Can BitChat negotiate different encryption algorithms at runtime?

No. All Noise Protocol parameters are hardcoded with no runtime negotiation of algorithms, handshake patterns, or cipher suites. The fixed configuration uses X25519 for DH, ChaCha20-Poly1305 for AEAD, and SHA-256 for hashing across all connections, preventing downgrade attacks and ensuring interoperability.

Where are the static Noise keys stored in BitChat?

Static X25519 keys are generated once per device and persisted in the device keychain via NoiseKeyStore. These 32-byte private keys never rotate, ensuring persistent identity across app restarts and mesh reconnections, as the same public key must remain recognizable to existing peers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →