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 chunks0x21— 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:
- Generate or load the 32-byte X25519 static private key
- Extract the public key for mesh announcement packets
- Compute
SHA-256(noiseStaticKey)and extract bytes 0-7 (8 bytes total) - 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/:
NoiseSessionManager.swift— Implements thehandshakeXXroutine for initiator and responder rolesNoiseEncryptionService.swift— Handles X25519 DH operations, ChaCha20-Poly1305 AEAD construction, and ciphertext generationMessagePadding.swift— Ensures padded Noise frames fit within the 256-byte BLE packet limitPeerID.swift— Derives short peer IDs from static key fingerprints
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.swiftenforces 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 inPeerID.swift - Payload type identifiers
0x20,0x21, and0x08distinguish file data, authentication state, and voice frames inside encrypted payloads - Implementation resides in
NoiseSessionManager.swift,NoiseEncryptionService.swift, andPeerID.swiftwithin 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →