Implement Noise XX Handshake Protocol for Direct Messages (#180)

* noise

* works?

* noise

* temporary

* better

* wip: use subnet

* better

* barely working

* werk

* subnet

* fix peer ID

* 8 byte peer ID

* wip noise

* wip fixes for noise

* std lib for noise

* noise handshake one step further

* buffers

* use fork

* fix imports

* simplify counter

* remove trash

* hashing

* no prologue

* nice

* wip, use noise encryption

* peer ID hex

* simplify session manager

* heavy logging

* use singleton

* Fix Noise session race condition with elegant per-peer actor serialization

- Use Kotlin coroutine actors for per-peer packet processing
- Each peer gets dedicated actor that processes packets sequentially
- Eliminates race conditions in session management without complex locking
- Single surgical change in PacketProcessor - minimal, maintainable
- Leverages Kotlin's native concurrency primitives

* decrypt correctly

* iniator works now

* clean code and fix signature to null

* better

* no signature in private message

* small fixes

* refactor ack

* refactor but untested

* messages working

* wip ack

* wip fix ack

* more logging

* pending tracker

* keep pending connections on errors

* less logging

* refactor model

* refactor frombinarydata

* idendityannouncement refactor and update to new binary protocol

* fix keys

* refix keys

* dms work

* revert to mainnet

* do not change bluetooth adapter name

* keep code but uncomment

* clean up comments

* cleanup comments
This commit is contained in:
callebtc
2025-07-24 12:01:46 +02:00
committed by GitHub
parent 9e9231353b
commit c3c395832c
58 changed files with 15209 additions and 517 deletions
@@ -98,7 +98,17 @@ class BluetoothConnectionManager(
try {
isActive = true
// set the adapter's name to our 8-character peerID for iOS privacy, TODO: Make this configurable
// try {
// if (bluetoothAdapter?.name != myPeerID) {
// bluetoothAdapter?.name = myPeerID
// Log.d(TAG, "Set Bluetooth adapter name to peerID: $myPeerID for iOS compatibility.")
// }
// } catch (se: SecurityException) {
// Log.e(TAG, "Missing BLUETOOTH_CONNECT permission to set adapter name.", se)
// }
// Start all component managers
connectionScope.launch {
// Start connection tracker first
@@ -209,7 +219,7 @@ class BluetoothConnectionManager(
Log.i(TAG, "Power mode changed to: $newMode")
connectionScope.launch {
// CRITICAL FIX: Avoid rapid scan restarts by checking if we need to change scan behavior
// Avoid rapid scan restarts by checking if we need to change scan behavior
val wasUsingDutyCycle = powerManager.shouldUseDutyCycle()
// Update advertising with new power settings
@@ -88,6 +88,7 @@ class BluetoothConnectionTracker(
* Add a device connection
*/
fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) {
Log.d(TAG, "Tracker: Adding device connection for $deviceAddress")
connectedDevices[deviceAddress] = deviceConn
pendingConnections.remove(deviceAddress)
}
@@ -180,16 +181,22 @@ class BluetoothConnectionTracker(
* Add a pending connection attempt
*/
fun addPendingConnection(deviceAddress: String): Boolean {
Log.d(TAG, "Tracker: Adding pending connection for $deviceAddress")
synchronized(pendingConnections) {
// Double-check inside synchronized block
val currentAttempt = pendingConnections[deviceAddress]
if (currentAttempt != null && !currentAttempt.isExpired() && !currentAttempt.shouldRetry()) {
Log.d(TAG, "Tracker: Connection attempt already in progress for $deviceAddress")
return false
}
if (currentAttempt != null) {
Log.d(TAG, "Tracker: current attempt: $currentAttempt")
}
// Update connection attempt atomically
val attempts = (currentAttempt?.attempts ?: 0) + 1
pendingConnections[deviceAddress] = ConnectionAttempt(attempts)
Log.d(TAG, "Tracker: Added pending connection for $deviceAddress (attempts: $attempts)")
return true
}
}
@@ -242,8 +249,6 @@ class BluetoothConnectionTracker(
subscribedDevices.removeAll { it.address == deviceAddress }
addressPeerMap.remove(deviceAddress)
}
// CRITICAL FIX: Always remove from pending connections when cleaning up
// This prevents failed connections from blocking future attempts
pendingConnections.remove(deviceAddress)
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
}
@@ -318,7 +323,7 @@ class BluetoothConnectionTracker(
appendLine("Connected Devices: ${connectedDevices.size} / ${powerManager.getMaxConnections()}")
connectedDevices.forEach { (address, deviceConn) ->
val age = (System.currentTimeMillis() - deviceConn.connectedAt) / 1000
appendLine(" - $address (${if (deviceConn.isClient) "client" else "server"}, ${age}s, RSSI: ${deviceConn.rssi})")
appendLine(" - $address (we're ${if (deviceConn.isClient) "client" else "server"}, ${age}s, RSSI: ${deviceConn.rssi})")
}
appendLine()
appendLine("Subscribed Devices (server mode): ${subscribedDevices.size}")
@@ -47,7 +47,7 @@ class BluetoothGattClientManager(
// Scan management
private var scanCallback: ScanCallback? = null
// CRITICAL FIX: Scan rate limiting to prevent "scanning too frequently" errors
// Scan rate limiting to prevent "scanning too frequently" errors
private var lastScanStartTime = 0L
private var lastScanStopTime = 0L
private var isCurrentlyScanning = false
@@ -160,7 +160,7 @@ class BluetoothGattClientManager(
private fun startScanning() {
if (!permissionManager.hasBluetoothPermissions() || bleScanner == null || !isActive) return
// CRITICAL FIX: Rate limit scan starts to prevent "scanning too frequently" errors
// Rate limit scan starts to prevent "scanning too frequently" errors
val currentTime = System.currentTimeMillis()
if (isCurrentlyScanning) {
Log.d(TAG, "Scan already in progress, skipping start request")
@@ -192,6 +192,7 @@ class BluetoothGattClientManager(
scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
// Log.d(TAG, "Scan result received: ${result.device.address}")
handleScanResult(result)
}
@@ -276,6 +277,8 @@ class BluetoothGattClientManager(
if (!hasOurService) {
return
}
// Log.d(TAG, "Received scan result from $deviceAddress - already connected: ${connectionTracker.isDeviceConnected(deviceAddress)}")
// Store RSSI from scan results for later use (especially for server connections)
connectionTracker.updateScanRSSI(deviceAddress, rssi)
@@ -314,7 +317,7 @@ class BluetoothGattClientManager(
@Suppress("DEPRECATION")
private fun connectToDevice(device: BluetoothDevice, rssi: Int) {
if (!permissionManager.hasBluetoothPermissions()) return
val deviceAddress = device.address
Log.i(TAG, "Connecting to bitchat device: $deviceAddress")
@@ -337,10 +340,9 @@ class BluetoothGattClientManager(
}
} else {
Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress")
connectionTracker.cleanupDeviceConnection(deviceAddress)
}
connectionTracker.cleanupDeviceConnection(deviceAddress)
connectionScope.launch {
delay(500) // CLEANUP_DELAY
try {
@@ -372,7 +374,7 @@ class BluetoothGattClientManager(
gatt.discoverServices()
} else {
Log.w(TAG, "MTU negotiation failed for $deviceAddress with status: $status. Disconnecting.")
connectionTracker.removePendingConnection(deviceAddress)
//connectionTracker.removePendingConnection(deviceAddress)
gatt.disconnect()
}
}
@@ -420,10 +422,10 @@ class BluetoothGattClientManager(
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val value = characteristic.value
Log.d(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes")
Log.i(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes")
val packet = BitchatPacket.fromBinaryData(value)
if (packet != null) {
val peerID = String(packet.senderID).replace("\u0000", "")
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID")
delegate?.onPacketReceived(packet, peerID, gatt.device)
} else {
@@ -453,13 +455,15 @@ class BluetoothGattClientManager(
val gatt = device.connectGatt(context, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
if (gatt == null) {
Log.e(TAG, "connectGatt returned null for $deviceAddress")
connectionTracker.removePendingConnection(deviceAddress)
// keep the pending connection so we can avoid too many reconnections attempts, TODO: needs testing
// connectionTracker.removePendingConnection(deviceAddress)
} else {
Log.d(TAG, "Client: GATT connection initiated successfully for $deviceAddress")
}
} catch (e: Exception) {
Log.e(TAG, "Client: Exception connecting to $deviceAddress: ${e.message}")
connectionTracker.removePendingConnection(deviceAddress)
// keep the pending connection so we can avoid too many reconnections attempts, TODO: needs testing
// connectionTracker.removePendingConnection(deviceAddress)
}
}
@@ -174,7 +174,7 @@ class BluetoothGattServerManager(
Log.i(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes")
val packet = BitchatPacket.fromBinaryData(value)
if (packet != null) {
val peerID = String(packet.senderID).replace("\u0000", "")
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID")
delegate?.onPacketReceived(packet, peerID, device)
} else {
@@ -3,16 +3,19 @@ package com.bitchat.android.mesh
import android.content.Context
import android.util.Log
import com.bitchat.android.crypto.EncryptionService
import com.bitchat.android.crypto.MessagePadding
import com.bitchat.android.protocol.MessagePadding
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.model.DeliveryAck
import com.bitchat.android.model.ReadReceipt
import com.bitchat.android.model.NoiseIdentityAnnouncement
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.protocol.SpecialRecipients
import com.bitchat.android.util.toHexString
import kotlinx.coroutines.*
import java.util.*
import kotlin.math.sign
import kotlin.random.Random
/**
@@ -59,7 +62,10 @@ class BluetoothMeshService(private val context: Context) {
init {
setupDelegates()
startPeriodicDebugLogging()
// Wire up PacketProcessor reference for recursive handling in MessageHandler
messageHandler.packetProcessor = packetProcessor
//startPeriodicDebugLogging()
}
/**
@@ -115,10 +121,25 @@ class BluetoothMeshService(private val context: Context) {
delay(100)
sendAnnouncementToPeer(peerID)
delay(500)
delay(1000)
storeForwardManager.sendCachedMessages(peerID)
}
}
override fun sendHandshakeResponse(peerID: String, response: ByteArray) {
// Send Noise handshake response
val responsePacket = BitchatPacket(
version = 1u,
type = MessageType.NOISE_HANDSHAKE_RESP.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(peerID),
timestamp = System.currentTimeMillis().toULong(),
payload = response,
ttl = 1u
)
connectionManager.broadcastPacket(RoutedPacket(responsePacket))
Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)")
}
}
// StoreForwardManager delegates
@@ -189,11 +210,63 @@ class BluetoothMeshService(private val context: Context) {
return securityManager.decryptFromPeer(encryptedData, senderPeerID)
}
// Noise protocol operations
override fun hasNoiseSession(peerID: String): Boolean {
return encryptionService.hasEstablishedSession(peerID)
}
override fun initiateNoiseHandshake(peerID: String) {
try {
// Initiate proper Noise handshake with specific peer
val handshakeData = encryptionService.initiateHandshake(peerID)
if (handshakeData != null) {
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_HANDSHAKE_INIT.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(peerID),
timestamp = System.currentTimeMillis().toULong(),
payload = handshakeData,
ttl = 1u
)
connectionManager.broadcastPacket(RoutedPacket(packet))
Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)")
} else {
Log.w(TAG, "Failed to generate Noise handshake data for $peerID")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to initiate Noise handshake with $peerID: ${e.message}")
}
}
override fun updatePeerIDBinding(newPeerID: String, fingerprint: String, nickname: String,
publicKey: ByteArray, previousPeerID: String?) {
// Update peer mapping in the PeerManager for peer ID rotation support
peerManager.addOrUpdatePeer(newPeerID, nickname)
// If there was a previous peer ID, remove it to avoid duplicates
previousPeerID?.let { oldPeerID ->
peerManager.removePeer(oldPeerID)
}
// Register the public key with the delegate (ChatViewModel)
delegate?.registerPeerPublicKey(newPeerID, publicKey)
Log.d(TAG, "Updated peer ID binding: $newPeerID (was: $previousPeerID), fingerprint: ${fingerprint.take(16)}...")
}
// Message operations
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
return delegate?.decryptChannelMessage(encryptedContent, channel)
}
override fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) {
this@BluetoothMeshService.sendDeliveryAck(message, senderPeerID)
}
// Callbacks
override fun onMessageReceived(message: BitchatMessage) {
delegate?.didReceiveMessage(message)
@@ -226,8 +299,16 @@ class BluetoothMeshService(private val context: Context) {
peerManager.updatePeerLastSeen(peerID)
}
override fun handleKeyExchange(routed: RoutedPacket): Boolean {
return runBlocking { securityManager.handleKeyExchange(routed) }
override fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean {
return runBlocking { securityManager.handleNoiseHandshake(routed, step) }
}
override fun handleNoiseEncrypted(routed: RoutedPacket) {
serviceScope.launch { messageHandler.handleNoiseEncrypted(routed) }
}
override fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
serviceScope.launch { messageHandler.handleNoiseIdentityAnnouncement(routed) }
}
override fun handleAnnounce(routed: RoutedPacket) {
@@ -274,6 +355,11 @@ class BluetoothMeshService(private val context: Context) {
}
override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) {
// Send initial announcements after services are ready
serviceScope.launch {
delay(100)
sendBroadcastAnnounce()
}
// Send key exchange to newly connected device
serviceScope.launch {
delay(100) // Ensure connection is stable
@@ -304,11 +390,6 @@ class BluetoothMeshService(private val context: Context) {
if (connectionManager.startServices()) {
isActive = true
// Send initial announcements after services are ready
serviceScope.launch {
delay(1000)
sendBroadcastAnnounce()
}
} else {
Log.e(TAG, "Failed to start Bluetooth services")
}
@@ -365,16 +446,17 @@ class BluetoothMeshService(private val context: Context) {
)
message.toBinaryPayload()?.let { messageData ->
// Sign the message
val signature = securityManager.signPacket(messageData)
// Sign the message: TODO: NOT SIGNED
// val signature = securityManager.signPacket(messageData)
val packet = BitchatPacket(
version = 1u,
type = MessageType.MESSAGE.value,
senderID = myPeerID.toByteArray(),
senderID = hexStringToByteArray(myPeerID),
recipientID = SpecialRecipients.BROADCAST,
timestamp = System.currentTimeMillis().toULong(),
payload = messageData,
signature = signature,
signature = null,
ttl = MAX_TTL
)
@@ -391,54 +473,129 @@ class BluetoothMeshService(private val context: Context) {
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) {
if (content.isEmpty() || recipientPeerID.isEmpty() || recipientNickname.isEmpty()) return
serviceScope.launch {
val nickname = delegate?.getNickname() ?: myPeerID
val nickname = delegate?.getNickname() ?: myPeerID
val message = BitchatMessage(
id = messageID ?: UUID.randomUUID().toString(),
sender = nickname,
content = content,
timestamp = Date(),
isRelay = false,
isPrivate = true,
recipientNickname = recipientNickname,
senderPeerID = myPeerID
)
message.toBinaryPayload()?.let { messageData ->
try {
// Create inner packet with the padded message data
val innerPacket = BitchatPacket(
type = MessageType.MESSAGE.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(recipientPeerID),
timestamp = System.currentTimeMillis().toULong(),
payload = messageData,
signature = null,
ttl = MAX_TTL
)
// Cache for offline favorites
if (storeForwardManager.shouldCacheForPeer(recipientPeerID)) {
storeForwardManager.cacheMessage(innerPacket, messageID ?: message.id)
}
// Use the new encrypt and broadcast function
encryptAndBroadcastNoisePacket(innerPacket, recipientPeerID)
} catch (e: Exception) {
Log.e(TAG, "Failed to send private message: ${e.message}")
}
}
}
/**
* Send delivery acknowledgment for a received private message
*/
fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) {
val nickname = delegate?.getNickname() ?: myPeerID
val ack = DeliveryAck(
originalMessageID = message.id,
recipientID = myPeerID,
recipientNickname = nickname,
hopCount = 0u // Will be calculated during relay
)
try {
// TODO: THIS FORMAT FOR DELIVERY ACKS SHOULD BE DEPRECATED
val ackData = ack.encode() ?: return
val typeMarker = MessageType.DELIVERY_ACK.value.toByte()
val payloadWithMarker = byteArrayOf(typeMarker) + ackData
val encryptedPayload = securityManager.encryptForPeer(payloadWithMarker, senderPeerID)
if (encryptedPayload == null) {
Log.w(TAG, "Failed to encrypt delivery ACK for $senderPeerID")
return
}
val message = BitchatMessage(
id = messageID ?: UUID.randomUUID().toString(),
sender = nickname,
content = content,
timestamp = Date(),
isRelay = false,
isPrivate = true,
recipientNickname = recipientNickname,
senderPeerID = myPeerID
// Create inner packet with the delivery ACK data
val packet = BitchatPacket(
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(senderPeerID),
timestamp = System.currentTimeMillis().toULong(),
payload = encryptedPayload,
signature = null,
ttl = 3u
)
message.toBinaryPayload()?.let { messageData ->
try {
// Pad and encrypt
val blockSize = MessagePadding.optimalBlockSize(messageData.size)
val paddedData = MessagePadding.pad(messageData, blockSize)
val encryptedPayload = securityManager.encryptForPeer(paddedData, recipientPeerID)
if (encryptedPayload != null) {
// Sign
val signature = securityManager.signPacket(encryptedPayload)
val packet = BitchatPacket(
type = MessageType.MESSAGE.value,
senderID = myPeerID.toByteArray(),
recipientID = recipientPeerID.toByteArray(),
timestamp = System.currentTimeMillis().toULong(),
payload = encryptedPayload,
signature = signature,
ttl = MAX_TTL
)
// Cache for offline favorites
if (storeForwardManager.shouldCacheForPeer(recipientPeerID)) {
storeForwardManager.cacheMessage(packet, messageID ?: message.id)
}
// Send with delay
delay(Random.nextLong(50, 500))
connectionManager.broadcastPacket(RoutedPacket(packet))
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send private message: ${e.message}")
// Use the new encrypt and broadcast function
connectionManager.broadcastPacket(RoutedPacket(packet))
} catch (e: Exception) {
Log.e(TAG, "Failed to send delivery ACK: ${e.message}")
}
}
/**
* Encrypt a BitchatPacket and broadcast it as a NOISE_ENCRYPTED message
* This is the correct protocol implementation - encrypt the entire packet, not just the payload
*/
private fun encryptAndBroadcastNoisePacket(innerPacket: BitchatPacket, recipientPeerID: String) {
serviceScope.launch {
try {
// Serialize the inner packet to binary data
val innerPacketData = innerPacket.toBinaryData()
if (innerPacketData == null) {
Log.e(TAG, "Failed to serialize inner packet for encryption")
return@launch
}
// Encrypt the serialized packet using Noise encryption
val encryptedPayload = securityManager.encryptForPeer(innerPacketData, recipientPeerID)
if (encryptedPayload != null) {
// Create the outer NOISE_ENCRYPTED packet
val outerPacket = BitchatPacket(
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(recipientPeerID),
timestamp = System.currentTimeMillis().toULong(),
payload = encryptedPayload,
signature = null,
ttl = MAX_TTL
)
// Broadcast the encrypted packet
connectionManager.broadcastPacket(RoutedPacket(outerPacket))
Log.d(TAG, "Encrypted and sent packet type ${innerPacket.type} to $recipientPeerID (${encryptedPayload.size} bytes encrypted)")
} else {
Log.w(TAG, "Failed to encrypt packet for $recipientPeerID - no session available")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to encrypt and broadcast Noise packet to $recipientPeerID: ${e.message}")
}
}
}
@@ -488,19 +645,86 @@ class BluetoothMeshService(private val context: Context) {
}
/**
* Send key exchange
* Send Noise identity announcement (broadcast our static public key and signing key)
* Now properly formatted as NoiseIdentityAnnouncement to match iOS
*/
private fun sendKeyExchangeToDevice() {
val publicKeyData = securityManager.getCombinedPublicKeyData()
val packet = BitchatPacket(
type = MessageType.KEY_EXCHANGE.value,
ttl = 1u,
senderID = myPeerID,
payload = publicKeyData
)
connectionManager.broadcastPacket(RoutedPacket(packet))
Log.d(TAG, "Sent key exchange")
serviceScope.launch {
try {
val nickname = delegate?.getNickname() ?: myPeerID
// Create the identity announcement using proper binary format
val announcement = createNoiseIdentityAnnouncement(nickname, null)
if (announcement != null) {
val announcementData = announcement.toBinaryData()
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_IDENTITY_ANNOUNCE.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = SpecialRecipients.BROADCAST,
timestamp = System.currentTimeMillis().toULong(),
payload = announcementData,
ttl = 1u
)
connectionManager.broadcastPacket(RoutedPacket(packet))
Log.d(TAG, "Sent NoiseIdentityAnnouncement (${announcementData.size} bytes)")
} else {
Log.e(TAG, "Failed to create NoiseIdentityAnnouncement")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send NoiseIdentityAnnouncement: ${e.message}")
}
}
}
/**
* Create a properly formatted NoiseIdentityAnnouncement exactly like iOS
*/
private fun createNoiseIdentityAnnouncement(nickname: String, previousPeerID: String?): NoiseIdentityAnnouncement? {
return try {
// Get the static public key for Noise protocol
val staticKey = encryptionService.getStaticPublicKey()
if (staticKey == null) {
Log.e(TAG, "No static public key available for identity announcement")
return null
}
// Get the signing public key for Ed25519 signatures
val signingKey = encryptionService.getSigningPublicKey()
if (signingKey == null) {
Log.e(TAG, "No signing public key available for identity announcement")
return null
}
val now = Date()
// Create the binding data to sign (same format as iOS)
val timestampMs = now.time
val bindingData = myPeerID.toByteArray(Charsets.UTF_8) +
staticKey +
timestampMs.toString().toByteArray(Charsets.UTF_8)
// Sign the binding with our Ed25519 signing key
val signature = encryptionService.signData(bindingData) ?: ByteArray(0)
// Create the identity announcement
NoiseIdentityAnnouncement(
peerID = myPeerID,
publicKey = staticKey,
signingPublicKey = signingKey,
nickname = nickname,
timestamp = now,
previousPeerID = previousPeerID,
signature = signature
)
} catch (e: Exception) {
Log.e(TAG, "Failed to create NoiseIdentityAnnouncement: ${e.message}")
null
}
}
/**
@@ -528,6 +752,43 @@ class BluetoothMeshService(private val context: Context) {
*/
fun getPeerRSSI(): Map<String, Int> = peerManager.getAllPeerRSSI()
/**
* Check if we have an established Noise session with a peer
*/
fun hasEstablishedSession(peerID: String): Boolean {
return encryptionService.hasEstablishedSession(peerID)
}
/**
* Get peer fingerprint for identity management
*/
fun getPeerFingerprint(peerID: String): String? {
return encryptionService.getPeerFingerprint(peerID)
}
/**
* Get our identity fingerprint
*/
fun getIdentityFingerprint(): String {
return encryptionService.getIdentityFingerprint()
}
/**
* Check if encryption icon should be shown for a peer
*/
fun shouldShowEncryptionIcon(peerID: String): Boolean {
return encryptionService.hasEstablishedSession(peerID)
}
/**
* Get all peers with established encrypted sessions
*/
fun getEncryptedPeers(): List<String> {
// SIMPLIFIED: Return empty list for now since we don't have direct access to sessionManager
// This method is not critical for the session retention fix
return emptyList()
}
/**
* Get device address for a specific peer ID
*/
@@ -574,13 +835,34 @@ class BluetoothMeshService(private val context: Context) {
}
/**
* Generate peer ID compatible with iOS
* Generate peer ID compatible with iOS - exactly 8 bytes (16 hex characters)
*/
private fun generateCompatiblePeerID(): String {
val randomBytes = ByteArray(4)
val randomBytes = ByteArray(8) // 8 bytes = 16 hex characters (like iOS)
Random.nextBytes(randomBytes)
return randomBytes.joinToString("") { "%02x".format(it) }
}
/**
* Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS
*/
private fun hexStringToByteArray(hexString: String): ByteArray {
val result = ByteArray(8) { 0 } // Initialize with zeros, exactly 8 bytes
var tempID = hexString
var index = 0
while (tempID.length >= 2 && index < 8) {
val hexByte = tempID.substring(0, 2)
val byte = hexByte.toIntOrNull(16)?.toByte()
if (byte != null) {
result[index] = byte
}
tempID = tempID.substring(2)
index++
}
return result
}
}
/**
@@ -8,11 +8,20 @@ import android.util.Log
import com.bitchat.android.protocol.SpecialRecipients
import com.bitchat.android.model.RoutedPacket
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
/**
* Handles packet broadcasting to connected devices
* Handles packet broadcasting to connected devices using actor pattern for serialization
*
* SERIALIZATION FIX: Uses Kotlin coroutine actor to serialize all packet broadcasting
* This prevents race conditions when multiple threads try to broadcast simultaneously
*/
class BluetoothPacketBroadcaster(
private val connectionScope: CoroutineScope,
@@ -25,14 +34,40 @@ class BluetoothPacketBroadcaster(
private const val CLEANUP_DELAY = 500L
}
// Data class to hold broadcast request information
private data class BroadcastRequest(
val routed: RoutedPacket,
val gattServer: BluetoothGattServer?,
val characteristic: BluetoothGattCharacteristic?
)
// Actor scope for the broadcaster
private val broadcasterScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// SERIALIZATION: Actor to serialize all broadcast operations
@OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class)
private val broadcasterActor = broadcasterScope.actor<BroadcastRequest>(
capacity = Channel.UNLIMITED
) {
Log.d(TAG, "🎭 Created packet broadcaster actor")
try {
for (request in channel) {
Log.d(TAG, "Processing broadcast for packet type ${request.routed.packet.type} (serialized)")
broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic)
Log.d(TAG, "Completed broadcast for packet type ${request.routed.packet.type}")
}
} finally {
Log.d(TAG, "🎭 Packet broadcaster actor terminated")
}
}
fun broadcastPacket(
routed: RoutedPacket,
gattServer: BluetoothGattServer?,
characteristic: BluetoothGattCharacteristic?
) {
val packet = routed.packet
val data = packet.toBinaryData() ?: return
// Check if we need to fragment
// Check if we need to fragment
if (fragmentManager != null) {
val fragments = fragmentManager.createFragments(packet)
if (fragments.size > 1) {
@@ -41,7 +76,7 @@ class BluetoothPacketBroadcaster(
fragments.forEach { fragment ->
broadcastSinglePacket(RoutedPacket(fragment), gattServer, characteristic)
// 20ms delay between fragments (matching iOS/Rust)
delay(20)
delay(200)
}
}
return
@@ -54,12 +89,32 @@ class BluetoothPacketBroadcaster(
/**
* Broadcast single packet to connected devices with connection limit enforcement
* Public entry point for broadcasting - submits request to actor for serialization
*/
fun broadcastSinglePacket(
routed: RoutedPacket,
gattServer: BluetoothGattServer?,
characteristic: BluetoothGattCharacteristic?
) {
// Submit broadcast request to actor for serialized processing
broadcasterScope.launch {
try {
broadcasterActor.send(BroadcastRequest(routed, gattServer, characteristic))
} catch (e: Exception) {
Log.w(TAG, "Failed to send broadcast request to actor: ${e.message}")
// Fallback to direct processing if actor fails
broadcastSinglePacketInternal(routed, gattServer, characteristic)
}
}
}
/**
* Internal broadcast implementation - runs in serialized actor context
*/
private suspend fun broadcastSinglePacketInternal(
routed: RoutedPacket,
gattServer: BluetoothGattServer?,
characteristic: BluetoothGattCharacteristic?
) {
val packet = routed.packet
val data = packet.toBinaryData() ?: return
@@ -96,7 +151,7 @@ class BluetoothPacketBroadcaster(
val subscribedDevices = connectionTracker.getSubscribedDevices()
val connectedDevices = connectionTracker.getConnectedDevices()
Log.d(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections")
Log.i(TAG, "Broadcasting packet type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections")
val senderID = String(packet.senderID).replace("\u0000", "")
@@ -177,4 +232,31 @@ class BluetoothPacketBroadcaster(
false
}
}
/**
* Get debug information
*/
fun getDebugInfo(): String {
return buildString {
appendLine("=== Packet Broadcaster Debug Info ===")
appendLine("Broadcaster Scope Active: ${broadcasterScope.isActive}")
appendLine("Actor Channel Closed: ${broadcasterActor.isClosedForSend}")
appendLine("Connection Scope Active: ${connectionScope.isActive}")
}
}
/**
* Shutdown the broadcaster actor gracefully
*/
fun shutdown() {
Log.d(TAG, "Shutting down BluetoothPacketBroadcaster actor")
// Close the actor gracefully
broadcasterActor.close()
// Cancel the broadcaster scope
broadcasterScope.cancel()
Log.d(TAG, "BluetoothPacketBroadcaster shutdown complete")
}
}
@@ -14,7 +14,8 @@ class FragmentManager {
companion object {
private const val TAG = "FragmentManager"
private const val MAX_FRAGMENT_SIZE = 150 // Match iOS/Rust for BLE compatibility (185 byte MTU limit)
private const val FRAGMENT_SIZE_THRESHOLD = 512 // 512 bytes
private const val MAX_FRAGMENT_SIZE = 500 // Match iOS/Rust for BLE compatibility (185 byte MTU limit)
private const val FRAGMENT_TIMEOUT = 30000L // 30 seconds
private const val CLEANUP_INTERVAL = 10000L // 10 seconds
}
@@ -39,7 +40,7 @@ class FragmentManager {
fun createFragments(packet: BitchatPacket): List<BitchatPacket> {
val data = packet.toBinaryData() ?: return emptyList()
if (data.size <= MAX_FRAGMENT_SIZE) {
if (data.size <= FRAGMENT_SIZE_THRESHOLD) {
return listOf(packet) // No fragmentation needed
}
@@ -1,13 +1,14 @@
package com.bitchat.android.mesh
import android.util.Log
import com.bitchat.android.crypto.MessagePadding
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryAck
import com.bitchat.android.model.NoiseIdentityAnnouncement
import com.bitchat.android.model.ReadReceipt
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.util.toHexString
import kotlinx.coroutines.*
import java.util.*
import kotlin.random.Random
@@ -25,9 +26,142 @@ class MessageHandler(private val myPeerID: String) {
// Delegate for callbacks
var delegate: MessageHandlerDelegate? = null
// Reference to PacketProcessor for recursive packet handling
var packetProcessor: PacketProcessor? = null
// Coroutines
private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
/**
* Handle Noise encrypted transport message
*/
suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
Log.d(TAG, "Processing Noise encrypted message from $peerID (${packet.payload.size} bytes)")
// Skip our own messages
if (peerID == myPeerID) return
try {
// Decrypt the message using the Noise service
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
if (decryptedData == null) {
Log.w(TAG, "Failed to decrypt Noise message from $peerID - may need handshake")
return
}
// Check if it's a special format message (type marker + payload)
if (decryptedData.size > 1) {
val typeMarker = decryptedData[0].toUByte()
// Check if this is a delivery ACK with the new format
if (typeMarker == MessageType.DELIVERY_ACK.value) {
// Extract the ACK JSON data (skip the type marker)
val ackData = decryptedData.sliceArray(1 until decryptedData.size)
// Decode the delivery ACK
val ack = DeliveryAck.decode(ackData)
if (ack != null) {
delegate?.onDeliveryAckReceived(ack)
Log.d(TAG, "Processed delivery ACK from $peerID")
return
}
}
// Check for read receipt with type marker
if (typeMarker == MessageType.READ_RECEIPT.value) {
val receiptData = decryptedData.sliceArray(1 until decryptedData.size)
val receipt = ReadReceipt.decode(receiptData)
if (receipt != null) {
delegate?.onReadReceiptReceived(receipt)
Log.d(TAG, "Processed read receipt from $peerID")
return
}
}
}
// Try to parse as a full inner packet (for compatibility with other message types)
val innerPacket = BitchatPacket.fromBinaryData(decryptedData)
if (innerPacket != null) {
Log.d(TAG, "Decrypted inner packet type ${innerPacket.type} from $peerID")
// Create a new routed packet with the decrypted inner packet
val innerRouted = RoutedPacket(innerPacket, peerID, routed.relayAddress)
// Use PacketProcessor to handle the inner packet recursively
if (packetProcessor != null) {
packetProcessor!!.processPacket(innerRouted)
} else {
Log.w(TAG, "PacketProcessor reference is null; cannot recursively process inner packet.")
}
} else {
Log.w(TAG, "Failed to parse decrypted data as packet from $peerID")
}
} catch (e: Exception) {
Log.e(TAG, "Error processing Noise encrypted message from $peerID: ${e.message}")
}
}
/**
* Handle Noise identity announcement - supports peer ID rotation
*/
suspend fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
Log.d(TAG, "Processing Noise identity announcement from $peerID (${packet.payload.size} bytes)")
// Skip our own announcements
if (peerID == myPeerID) return
try {
// Parse the identity announcement
val announcement = NoiseIdentityAnnouncement.fromBinaryData(packet.payload)
if (announcement == null) {
Log.w(TAG, "Failed to parse Noise identity announcement from $peerID")
return
}
Log.d(TAG, "Parsed identity announcement: peerID=${announcement.peerID}, " +
"nickname=${announcement.nickname}, fingerprint=${announcement.fingerprint?.take(16)}...")
// Verify the announcement signature (basic validation)
// In a full implementation, this would use cryptographic verification
if (announcement.signature.isEmpty()) {
Log.w(TAG, "Identity announcement from $peerID has no signature")
return
}
// Update peer binding in the delegate (ChatViewModel/BluetoothMeshService)
delegate?.updatePeerIDBinding(
newPeerID = announcement.peerID,
fingerprint = announcement.fingerprint ?: "",
nickname = announcement.nickname,
publicKey = announcement.publicKey,
previousPeerID = announcement.previousPeerID
)
// Check if we need to initiate a handshake with this peer
val hasSession = delegate?.hasNoiseSession(announcement.peerID) ?: false
if (!hasSession) {
Log.d(TAG, "No session with ${announcement.peerID}, may need handshake")
// Use lexicographic comparison to decide who initiates (prevents both sides from initiating)
if (myPeerID < announcement.peerID) {
delegate?.initiateNoiseHandshake(announcement.peerID)
}
}
Log.d(TAG, "Successfully processed identity announcement from $peerID")
} catch (e: Exception) {
Log.e(TAG, "Error processing Noise identity announcement from $peerID: ${e.message}")
}
}
/**
* Handle announce message
*/
@@ -62,11 +196,14 @@ class MessageHandler(private val myPeerID: String) {
if (peerID == myPeerID) return
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
var recipientIDString = ""
if (recipientID != null) {
recipientIDString = recipientID.toHexString()
}
if (recipientID == null) {
// BROADCAST MESSAGE
handleBroadcastMessage(routed)
} else if (String(recipientID).replace("\u0000", "") == myPeerID) {
} else if (recipientID.toHexString() == myPeerID) {
// PRIVATE MESSAGE FOR US
handlePrivateMessage(packet, peerID)
} else if (packet.ttl > 0u) {
@@ -120,7 +257,7 @@ class MessageHandler(private val myPeerID: String) {
}
/**
* Handle private message addressed to us
* Handle (decrypted) private message addressed to us
*/
private suspend fun handlePrivateMessage(packet: BitchatPacket, peerID: String) {
try {
@@ -129,18 +266,9 @@ class MessageHandler(private val myPeerID: String) {
Log.w(TAG, "Invalid signature for private message from $peerID")
return
}
// Decrypt message
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
if (decryptedData == null) {
Log.e(TAG, "Failed to decrypt private message from $peerID")
return
}
val unpaddedData = MessagePadding.unpad(decryptedData)
// Parse message
val message = BitchatMessage.fromBinaryPayload(unpaddedData)
val message = BitchatMessage.fromBinaryPayload(packet.payload)
if (message != null) {
// Check for cover traffic (dummy messages)
if (message.content.startsWith("☂DUMMY☂")) {
@@ -149,17 +277,10 @@ class MessageHandler(private val myPeerID: String) {
}
delegate?.updatePeerNickname(peerID, message.sender)
// Replace timestamp with current time (same as iOS)
val messageWithCurrentTime = message.copy(
senderPeerID = peerID,
timestamp = Date() // Use current time instead of original timestamp
)
delegate?.onMessageReceived(messageWithCurrentTime)
delegate?.onMessageReceived(message)
// Send delivery ACK
sendDeliveryAck(message, peerID)
// delegate?.sendDeliveryAck(message, peerID)
}
} catch (e: Exception) {
@@ -272,42 +393,6 @@ class MessageHandler(private val myPeerID: String) {
}
}
/**
* Send delivery acknowledgment for a received private message
*/
private fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) {
handlerScope.launch {
val nickname = delegate?.getMyNickname() ?: myPeerID
val ack = DeliveryAck(
originalMessageID = message.id,
recipientID = myPeerID,
recipientNickname = nickname,
hopCount = 0u // Will be calculated during relay
)
try {
val ackData = ack.encode() ?: return@launch
val encryptedPayload = delegate?.encryptForPeer(ackData, senderPeerID)
if (encryptedPayload != null) {
val packet = BitchatPacket(
type = MessageType.DELIVERY_ACK.value,
senderID = myPeerID.toByteArray(),
recipientID = senderPeerID.toByteArray(),
timestamp = System.currentTimeMillis().toULong(),
payload = encryptedPayload,
signature = null,
ttl = 3u
)
delegate?.sendPacket(packet)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to send delivery ACK: ${e.message}")
}
}
}
/**
* Get debug information
*/
@@ -319,6 +404,27 @@ class MessageHandler(private val myPeerID: String) {
}
}
/**
* Convert hex string peer ID to binary data (8 bytes) - same as iOS implementation
*/
private fun hexStringToByteArray(hexString: String): ByteArray {
val result = ByteArray(8) { 0 } // Initialize with zeros, exactly 8 bytes
var tempID = hexString
var index = 0
while (tempID.length >= 2 && index < 8) {
val hexByte = tempID.substring(0, 2)
val byte = hexByte.toIntOrNull(16)?.toByte()
if (byte != null) {
result[index] = byte
}
tempID = tempID.substring(2)
index++
}
return result
}
/**
* Shutdown the handler
*/
@@ -349,8 +455,15 @@ interface MessageHandlerDelegate {
fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray?
fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray?
// Noise protocol operations
fun hasNoiseSession(peerID: String): Boolean
fun initiateNoiseHandshake(peerID: String)
fun updatePeerIDBinding(newPeerID: String, fingerprint: String, nickname: String,
publicKey: ByteArray, previousPeerID: String?)
// Message operations
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String)
// Callbacks
fun onMessageReceived(message: BitchatMessage)
@@ -5,10 +5,15 @@ import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.model.RoutedPacket
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.actor
/**
* Processes incoming packets and routes them to appropriate handlers
* Extracted from BluetoothMeshService for better separation of concerns
*
* Per-peer packet serialization using Kotlin coroutine actors
* Prevents race condition where multiple threads process packets
* from the same peer simultaneously, causing session management conflicts.
*/
class PacketProcessor(private val myPeerID: String) {
@@ -22,12 +27,49 @@ class PacketProcessor(private val myPeerID: String) {
// Coroutines
private val processorScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Per-peer actors to serialize packet processing
// Each peer gets its own actor that processes packets sequentially
// This prevents race conditions in session management
private val peerActors = mutableMapOf<String, CompletableDeferred<Unit>>()
@OptIn(ObsoleteCoroutinesApi::class)
private fun getOrCreateActorForPeer(peerID: String) = processorScope.actor<RoutedPacket>(
capacity = Channel.UNLIMITED
) {
Log.d(TAG, "🎭 Created packet actor for peer: $peerID")
try {
for (packet in channel) {
Log.d(TAG, "📦 Processing packet type ${packet.packet.type} from $peerID (serialized)")
handleReceivedPacket(packet)
Log.d(TAG, "Completed packet type ${packet.packet.type} from $peerID")
}
} finally {
Log.d(TAG, "🎭 Packet actor for $peerID terminated")
}
}
// Cache actors to reuse them
private val actors = mutableMapOf<String, kotlinx.coroutines.channels.SendChannel<RoutedPacket>>()
/**
* Process received packet - main entry point for all incoming packets
* SURGICAL FIX: Route to per-peer actor for serialized processing
*/
fun processPacket(routed: RoutedPacket) {
val peerID = routed.peerID ?: "unknown"
// Get or create actor for this peer
val actor = actors.getOrPut(peerID) { getOrCreateActorForPeer(peerID) }
// Send packet to peer's dedicated actor for serialized processing
processorScope.launch {
handleReceivedPacket(routed)
try {
actor.send(routed)
} catch (e: Exception) {
Log.w(TAG, "Failed to send packet to actor for $peerID: ${e.message}")
// Fallback to direct processing if actor fails
handleReceivedPacket(routed)
}
}
}
@@ -43,15 +85,15 @@ class PacketProcessor(private val myPeerID: String) {
Log.d(TAG, "Packet failed security validation from $peerID")
return
}
// Update last seen timestamp
delegate?.updatePeerLastSeen(peerID)
var validPacket = true
Log.d(TAG, "Processing packet type ${packet.type} from $peerID")
// Process based on message type (exact same logic as iOS)
val DEBUG_MESSAGE_TYPE = MessageType.fromValue(packet.type)
when (MessageType.fromValue(packet.type)) {
MessageType.KEY_EXCHANGE -> handleKeyExchange(routed)
MessageType.NOISE_HANDSHAKE_INIT -> handleNoiseHandshake(routed, 1)
MessageType.NOISE_HANDSHAKE_RESP -> handleNoiseHandshake(routed, 2)
MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed)
MessageType.NOISE_IDENTITY_ANNOUNCE -> handleNoiseIdentityAnnouncement(routed)
MessageType.ANNOUNCE -> handleAnnounce(routed)
MessageType.MESSAGE -> handleMessage(routed)
MessageType.LEAVE -> handleLeave(routed)
@@ -61,22 +103,27 @@ class PacketProcessor(private val myPeerID: String) {
MessageType.DELIVERY_ACK -> handleDeliveryAck(routed)
MessageType.READ_RECEIPT -> handleReadReceipt(routed)
else -> {
validPacket = false
Log.w(TAG, "Unknown message type: ${packet.type}")
}
}
// Update last seen timestamp
if (validPacket)
delegate?.updatePeerLastSeen(peerID)
}
/**
* Handle key exchange message
* Handle Noise handshake message
*/
private suspend fun handleKeyExchange(routed: RoutedPacket) {
private suspend fun handleNoiseHandshake(routed: RoutedPacket, step: Int) {
val peerID = routed.peerID ?: "unknown"
Log.d(TAG, "Processing key exchange from $peerID")
Log.d(TAG, "Processing Noise handshake step $step from $peerID")
val success = delegate?.handleKeyExchange(routed) ?: false
val success = delegate?.handleNoiseHandshake(routed, step) ?: false
if (success) {
// Key exchange successful, send announce and cached messages
// Handshake successful, may need to send announce and cached messages
// This will be determined by the Noise implementation when session is established
delay(100)
delegate?.sendAnnouncementToPeer(peerID)
@@ -85,6 +132,24 @@ class PacketProcessor(private val myPeerID: String) {
}
}
/**
* Handle Noise encrypted transport message
*/
private suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
val peerID = routed.peerID ?: "unknown"
Log.d(TAG, "Processing Noise encrypted message from $peerID")
delegate?.handleNoiseEncrypted(routed)
}
/**
* Handle Noise identity announcement (after peer ID rotation)
*/
private suspend fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
val peerID = routed.peerID ?: "unknown"
Log.d(TAG, "Processing Noise identity announcement from $peerID")
delegate?.handleNoiseIdentityAnnouncement(routed)
}
/**
* Handle announce message
*/
@@ -151,15 +216,34 @@ class PacketProcessor(private val myPeerID: String) {
return buildString {
appendLine("=== Packet Processor Debug Info ===")
appendLine("Processor Scope Active: ${processorScope.isActive}")
appendLine("Active Peer Actors: ${actors.size}")
appendLine("My Peer ID: $myPeerID")
if (actors.isNotEmpty()) {
appendLine("Peer Actors:")
actors.keys.forEach { peerID ->
appendLine(" - $peerID")
}
}
}
}
/**
* Shutdown the processor
* Shutdown the processor and all peer actors
*/
fun shutdown() {
Log.d(TAG, "Shutting down PacketProcessor and ${actors.size} peer actors")
// Close all peer actors gracefully
actors.values.forEach { actor ->
actor.close()
}
actors.clear()
// Cancel the main scope
processorScope.cancel()
Log.d(TAG, "PacketProcessor shutdown complete")
}
}
@@ -174,7 +258,9 @@ interface PacketProcessorDelegate {
fun updatePeerLastSeen(peerID: String)
// Message type handlers
fun handleKeyExchange(routed: RoutedPacket): Boolean
fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean
fun handleNoiseEncrypted(routed: RoutedPacket)
fun handleNoiseIdentityAnnouncement(routed: RoutedPacket)
fun handleAnnounce(routed: RoutedPacket)
fun handleMessage(routed: RoutedPacket)
fun handleLeave(routed: RoutedPacket)
@@ -113,10 +113,6 @@ class PowerManager(private val context: Context) {
* Get scan settings optimized for current power mode
*/
fun getScanSettings(): ScanSettings {
// CRITICAL FIX: Set reportDelay to 0 for all modes.
// When using a custom duty cycle, we want scan results delivered immediately,
// not batched. A non-zero report delay can conflict with the scan window,
// causing missed results if the scan stops before the delay is met.
val builder = ScanSettings.Builder()
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
@@ -5,6 +5,7 @@ import com.bitchat.android.crypto.EncryptionService
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.util.toHexString
import kotlinx.coroutines.*
import java.util.*
import kotlin.collections.mutableSetOf
@@ -86,6 +87,65 @@ class SecurityManager(private val encryptionService: EncryptionService, private
return true
}
/**
* Handle Noise handshake packet
*/
suspend fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
// Skip handshakes not addressed to us
if (packet.recipientID?.toHexString() != myPeerID) {
Log.d(TAG, "Skipping handshake not addressed to us: $peerID")
return false
}
// Skip our own handshake messages
if (peerID == myPeerID) return false
if (encryptionService.hasEstablishedSession(peerID)) {
Log.d(TAG, "Handshake already completed with $peerID")
return true
}
if (packet.payload.isEmpty()) {
Log.w(TAG, "Noise handshake packet has empty payload")
return false
}
// Prevent duplicate handshake processing
val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
if (processedKeyExchanges.contains(exchangeKey)) {
Log.d(TAG, "Already processed handshake: $exchangeKey")
return false
}
Log.d(TAG, "Processing Noise handshake step $step from $peerID (${packet.payload.size} bytes)")
processedKeyExchanges.add(exchangeKey)
try {
// Process the Noise handshake through the updated EncryptionService
val response = encryptionService.processHandshakeMessage(packet.payload, peerID)
if (response != null) {
Log.d(TAG, "Successfully processed Noise handshake step $step from $peerID, sending response")
// Send handshake response through delegate
delegate?.sendHandshakeResponse(peerID, response)
}
// Check if session is now established (handshake complete)
if (encryptionService.hasEstablishedSession(peerID)) {
Log.d(TAG, "✅ Noise handshake completed with $peerID")
delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress)
}
return true
} catch (e: Exception) {
Log.e(TAG, "Failed to process Noise handshake from $peerID: ${e.message}")
return false
}
}
/**
* Handle key exchange packet
*/
@@ -209,9 +269,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
* Check if we have encryption keys for a peer
*/
fun hasKeysForPeer(peerID: String): Boolean {
// This would need to be implemented in EncryptionService
// For now, we'll assume we have keys if we processed a key exchange
return processedKeyExchanges.any { it.startsWith("$peerID-") }
return encryptionService.hasEstablishedSession(peerID)
}
/**
@@ -320,4 +378,5 @@ class SecurityManager(private val encryptionService: EncryptionService, private
*/
interface SecurityManagerDelegate {
fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?)
fun sendHandshakeResponse(peerID: String, response: ByteArray)
}
@@ -53,7 +53,10 @@ class StoreForwardManager {
*/
fun cacheMessage(packet: BitchatPacket, messageID: String) {
// Skip certain message types (same as iOS)
if (packet.type == MessageType.KEY_EXCHANGE.value ||
if (packet.type == MessageType.NOISE_HANDSHAKE_INIT.value ||
packet.type == MessageType.NOISE_HANDSHAKE_RESP.value ||
packet.type == MessageType.NOISE_ENCRYPTED.value ||
packet.type == MessageType.NOISE_IDENTITY_ANNOUNCE.value ||
packet.type == MessageType.ANNOUNCE.value ||
packet.type == MessageType.LEAVE.value) {
Log.d(TAG, "Skipping cache for message type: ${packet.type}")