mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 07:45:22 +00:00
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:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user