How Bitchat Android Implements End-to-End Encryption on BLE Mesh Networks

Bitchat Android secures peer-to-peer messages over Bluetooth Low-Energy (BLE) mesh by building on the Noise Protocol Framework, establishing static key sessions with forward secrecy and signing every packet with Ed25519 for integrity.

Bitchat Android enables decentralized messaging over BLE mesh networks without centralized servers. The application implements robust end-to-end encryption using the Noise Protocol Framework to protect message confidentiality and authenticity across multi-hop Bluetooth connections. This article examines the specific implementation details found in the permissionlesstech/bitchat-android repository, tracing how cryptographic keys are exchanged, payloads are encrypted, and packets are verified before reaching their destination.

Three-Layer Encryption Architecture

The encryption system splits responsibilities across three coordinated components that handle key establishment, payload protection, and transport security.

Noise-Based Key Exchange

In app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt, the handleNoiseHandshake function processes incoming handshake frames to establish mutual trust. When a handshake packet arrives, the method delegates to EncryptionService.processHandshakeMessageWithResult, which negotiates a static key pair and handshake token to create an AuthenticatedNoiseSession. This session persists in memory for the duration of the peer connection, enabling zero-round-trip encryption after the initial handshake.

Symmetric Payload Encryption

Once a session is active, EncryptionService.encrypt (or encryptForSession) encrypts raw message payloads using the session-derived symmetric key. The plaintext is wrapped in a NoisePayload structure that includes a type tag—such as PRIVATE_MESSAGE or READ_RECEIPT—allowing the receiver to route decrypted content correctly without parsing the ciphertext.

Ed25519 Signing and BLE Transport

Before transmission, BluetoothMeshService.signPacketBeforeBroadcast signs the encrypted payload with the sender's Ed25519 signing key. The encrypted data and signature are packaged into a BitchatPacket with type NOISE_ENCRYPTED. The service then broadcasts this signed packet over the BLE mesh using broadcastRoutedPacket, ensuring that intermediate hops cannot undetectably modify message contents.

End-to-End Encryption Pipeline Step-by-Step

The complete flow from sender to receiver involves nine distinct cryptographic operations:

  1. Session Detection: BluetoothMeshService checks encryptionService.hasEstablishedSession to determine if a Noise session exists with the recipient.
  2. Handshake Initiation: If no session exists, EncryptionService.initiateHandshake generates a NOISE_HANDSHAKE packet via NoiseEncryptionService.
  3. Handshake Processing: The remote SecurityManager.handleNoiseHandshake receives the packet and calls EncryptionService.processHandshakeMessageWithResult to establish the session.
  4. Payload Preparation: The sender encodes the message into a PrivateMessagePacket, wraps it in a NoisePayload with type PRIVATE_MESSAGE, and serializes it to bytes.
  5. Encryption: EncryptionService.encrypt encrypts the payload using the session key.
  6. Packet Construction: BluetoothMeshService creates a BitchatPacket with version, timestamp, TTL, and the encrypted payload.
  7. Signing: signPacketBeforeBroadcast adds an Ed25519 signature over the canonical packet data.
  8. Broadcast: broadcastRoutedPacket transmits the signed packet over BLE.
  9. Decryption and Validation: The receiver's SecurityManager.validatePacket checks the signature and replay status, then EncryptionService.decryptWithSession recovers the plaintext for MessageHandler to route.

Code Implementation Examples

The following Kotlin snippets demonstrate the sending and receiving logic found in the source.

Sending an Encrypted Private Message

// Inside BluetoothMeshService.sendPrivateMessage()
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
    // 1️⃣ Encode the TLV private message
    val privateMessage = PrivateMessagePacket(messageID = uuid, content = content)
    val tlv = privateMessage.encode() ?: return

    // 2️⃣ Wrap in a NoisePayload (type = PRIVATE_MESSAGE)
    val payload = NoisePayload(
        type = NoisePayloadType.PRIVATE_MESSAGE,
        data = tlv
    ).encode()

    // 3️⃣ Encrypt with the Noise session
    val encrypted = encryptionService.encrypt(payload, recipientPeerID)

    // 4️⃣ Build a NOISE_ENCRYPTED packet and sign it
    val packet = BitchatPacket(
        version = 1u,
        type = MessageType.NOISE_ENCRYPTED.value,
        senderID = hexStringToByteArray(myPeerID),
        recipientID = hexStringToByteArray(recipientPeerID),
        timestamp = System.currentTimeMillis().toULong(),
        payload = encrypted,
        signature = null,
        ttl = MAX_TTL
    )
    val signed = signPacketBeforeBroadcast(packet)

    // 5️⃣ Broadcast over BLE
    broadcastRoutedPacket(RoutedPacket(signed))
}

Decrypting and Handling Received Messages

// Inside SecurityManager.decryptFromPeer()
fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): NoiseDecryptionResult? {
    return try {
        // Decrypt via Noise session; throws if session missing/invalid
        encryptionService.decryptWithSession(encryptedData, senderPeerID)
    } catch (e: Exception) {
        Log.e(TAG, "Failed to decrypt from $senderPeerID: ${e.message}")
        null
    }
}

// After decryption, MessageHandler extracts the NoisePayload
val decrypted = securityManager.decryptFromPeer(pkt.payload, peerID)
val payload = NoisePayload.decode(decrypted?.payload ?: return)
when (payload.type) {
    NoisePayloadType.PRIVATE_MESSAGE -> handlePrivateMessage(payload.data)
    NoisePayloadType.READ_RECEIPT   -> handleReadReceipt(payload.data)
    // … other types
}

Core Source Files and Responsibilities

File Purpose
app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt Handles handshake processing (handleNoiseHandshake), duplicate detection, signature verification (verifySignature), and encrypted payload routing.
app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt Wraps NoiseEncryptionService to provide encrypt, decryptWithSession, and session management APIs.
app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt Orchestrates BLE transport, manages packet signing (signPacketBeforeBroadcast), and broadcasts encrypted messages.
app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt Implements Noise protocol primitives including handshake state machines and symmetric encryption.
app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt Decodes decrypted NoisePayload objects and dispatches messages to the UI or storage layer.

Why Noise Protocol for BLE Mesh?

The implementation chooses Noise over alternatives like TLS or Signal Protocol because it provides lightweight, authenticated encryption with forward secrecy while minimizing round trips—critical for unreliable BLE connections. The static key approach in AuthenticatedNoiseSession ensures that once peers have exchanged keys, subsequent messages require no additional handshake overhead, reducing latency on energy-constrained mesh networks according to the source code.

Summary

  • Noise Protocol Foundation: Bitchat Android uses SecurityManager and EncryptionService to establish Noise sessions with forward secrecy and static key authentication.
  • Layered Encryption: Payloads are encrypted with session-derived symmetric keys, wrapped in typed NoisePayload structures, and transmitted as NOISE_ENCRYPTED packets.
  • Cryptographic Integrity: Every packet is signed with Ed25519 before broadcast via BluetoothMeshService, preventing tampering by intermediate mesh nodes.
  • Zero-Round-Trip Messaging: Established sessions allow immediate encryption without repeated handshakes, optimizing for BLE mesh bandwidth constraints.

Frequently Asked Questions

Does Bitchat Android use Signal Protocol for encryption?

No. According to the source code in EncryptionService.kt, Bitchat Android implements the Noise Protocol Framework rather than the Signal Protocol (Double Ratchet). Noise provides the authenticated encryption and forward secrecy required for BLE mesh while maintaining compatibility with the iOS implementation.

How are encryption keys exchanged between peers?

Keys are exchanged via SecurityManager.handleNoiseHandshake, which processes NOISE_HANDSHAKE packets using EncryptionService.processHandshakeMessageWithResult. This establishes an AuthenticatedNoiseSession containing static keys and a handshake token, stored per peer ID.

Can intermediate nodes read message contents in the BLE mesh?

No. Messages are encrypted end-to-end using EncryptionService.encrypt with session-specific symmetric keys derived from the Noise handshake. Intermediate mesh nodes only see the signed, encrypted BitchatPacket and cannot access the plaintext NoisePayload without the recipient's private key.

What happens if a Noise session expires or is missing?

If encryptionService.hasEstablishedSession returns false, BluetoothMeshService initiates a new handshake via EncryptionService.initiateHandshake before sending the private message. The application queues messages or triggers handshake packets automatically to ensure encryption is always active before payload transmission.

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 →