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.ReadReceipt import com.bitchat.android.model.RoutedPacket import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType import kotlinx.coroutines.* import org.json.JSONObject import java.security.MessageDigest import java.util.* import kotlin.random.Random /** * Handles processing of different message types * Extracted from BluetoothMeshService for better separation of concerns */ class MessageHandler(private val myPeerID: String) { companion object { private const val TAG = "MessageHandler" } // Delegate for callbacks var delegate: MessageHandlerDelegate? = 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) // Process the decrypted inner packet recursively when (MessageType.fromValue(innerPacket.type)) { MessageType.MESSAGE -> handleMessage(innerRouted) MessageType.DELIVERY_ACK -> handleDeliveryAck(innerRouted) MessageType.READ_RECEIPT -> handleReadReceipt(innerRouted) else -> { Log.w(TAG, "Unexpected inner packet type: ${innerPacket.type}") } } } 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 = parseNoiseIdentityAnnouncement(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 */ suspend fun handleAnnounce(routed: RoutedPacket): Boolean { val packet = routed.packet val peerID = routed.peerID ?: "unknown" if (peerID == myPeerID) return false val nickname = String(packet.payload, Charsets.UTF_8) Log.d(TAG, "Received announce from $peerID: $nickname") // Notify delegate to handle peer management val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false // Relay announce if TTL > 0 if (packet.ttl > 1u) { val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) delay(Random.nextLong(100, 300)) delegate?.relayPacket(RoutedPacket(relayPacket, peerID, routed.relayAddress)) } return isFirstAnnounce } /** * Handle broadcast or private message */ suspend fun handleMessage(routed: RoutedPacket) { val packet = routed.packet val peerID = routed.peerID ?: "unknown" if (peerID == myPeerID) return val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) } if (recipientID == null) { // BROADCAST MESSAGE handleBroadcastMessage(routed) } else if (String(recipientID).replace("\u0000", "") == myPeerID) { // PRIVATE MESSAGE FOR US handlePrivateMessage(packet, peerID) } else if (packet.ttl > 0u) { // RELAY MESSAGE relayMessage(routed) } } /** * Handle broadcast message */ private suspend fun handleBroadcastMessage(routed: RoutedPacket) { val packet = routed.packet val peerID = routed.peerID ?: "unknown" try { // Parse message val message = BitchatMessage.fromBinaryPayload(packet.payload) if (message != null) { // Check for cover traffic (dummy messages) if (message.content.startsWith("☂DUMMY☂")) { Log.d(TAG, "Discarding cover traffic from $peerID") return // Silently discard } delegate?.updatePeerNickname(peerID, message.sender) // Handle encrypted channel messages val finalContent = if (message.channel != null && message.isEncrypted && message.encryptedContent != null) { delegate?.decryptChannelMessage(message.encryptedContent, message.channel) ?: "[Encrypted message - password required]" } else { message.content } // Replace timestamp with current time (same as iOS) val messageWithCurrentTime = message.copy( content = finalContent, senderPeerID = peerID, timestamp = Date() // Use current time instead of original timestamp ) delegate?.onMessageReceived(messageWithCurrentTime) } // Relay broadcast messages relayMessage(routed) } catch (e: Exception) { Log.e(TAG, "Failed to process broadcast message: ${e.message}") } } /** * Handle private message addressed to us */ private suspend fun handlePrivateMessage(packet: BitchatPacket, peerID: String) { try { // Verify signature if present if (packet.signature != null && !delegate?.verifySignature(packet, peerID)!!) { 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) if (message != null) { // Check for cover traffic (dummy messages) if (message.content.startsWith("☂DUMMY☂")) { Log.d(TAG, "Discarding private cover traffic from $peerID") return // Silently discard } 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) // Send delivery ACK sendDeliveryAck(message, peerID) } } catch (e: Exception) { Log.e(TAG, "Failed to process private message from $peerID: ${e.message}") } } /** * Handle leave message */ suspend fun handleLeave(routed: RoutedPacket) { val packet = routed.packet val peerID = routed.peerID ?: "unknown" val content = String(packet.payload, Charsets.UTF_8) if (content.startsWith("#")) { // Channel leave delegate?.onChannelLeave(content, peerID) } else { // Peer disconnect val nickname = delegate?.getPeerNickname(peerID) delegate?.removePeer(peerID) if (nickname != null) { delegate?.onPeerDisconnected(nickname) } } // Relay if TTL > 0 if (packet.ttl > 1u) { val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) delegate?.relayPacket(routed.copy(packet = relayPacket)) } } /** * Handle delivery acknowledgment */ suspend fun handleDeliveryAck(routed: RoutedPacket) { val packet = routed.packet val peerID = routed.peerID ?: "unknown" if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) { try { val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) if (decryptedData != null) { val ack = DeliveryAck.decode(decryptedData) if (ack != null) { delegate?.onDeliveryAckReceived(ack) } } } catch (e: Exception) { Log.e(TAG, "Failed to decrypt delivery ACK: ${e.message}") } } else if (packet.ttl > 0u && String(packet.senderID).replace("\u0000", "") != myPeerID) { // Relay val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) delegate?.relayPacket(routed.copy(packet = relayPacket)) } } /** * Handle read receipt */ suspend fun handleReadReceipt(routed: RoutedPacket) { val packet = routed.packet val peerID = routed.peerID ?: "unknown" if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) { try { val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) if (decryptedData != null) { val receipt = ReadReceipt.decode(decryptedData) if (receipt != null) { delegate?.onReadReceiptReceived(receipt) } } } catch (e: Exception) { Log.e(TAG, "Failed to decrypt read receipt: ${e.message}") } } else if (packet.ttl > 0u && String(packet.senderID).replace("\u0000", "") != myPeerID) { // Relay val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) delegate?.relayPacket(routed.copy(packet = relayPacket)) } } /** * Relay message with adaptive probability (same as iOS) */ private suspend fun relayMessage(routed: RoutedPacket) { val packet = routed.packet if (packet.ttl == 0u.toUByte()) return val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) // Check network size and apply adaptive relay probability val networkSize = delegate?.getNetworkSize() ?: 1 val relayProb = when { networkSize <= 10 -> 1.0 networkSize <= 30 -> 0.85 networkSize <= 50 -> 0.7 networkSize <= 100 -> 0.55 else -> 0.4 } val shouldRelay = relayPacket.ttl >= 4u || networkSize <= 3 || Random.nextDouble() < relayProb if (shouldRelay) { val delay = Random.nextLong(50, 500) // Random delay like iOS delay(delay) delegate?.relayPacket(routed.copy(packet = relayPacket)) } } /** * 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 */ fun getDebugInfo(): String { return buildString { appendLine("=== Message Handler Debug Info ===") appendLine("Handler Scope Active: ${handlerScope.isActive}") appendLine("My Peer ID: $myPeerID") } } /** * Parse Noise identity announcement from payload */ private fun parseNoiseIdentityAnnouncement(payload: ByteArray): NoiseIdentityAnnouncement? { return try { val jsonString = String(payload, Charsets.UTF_8) val json = JSONObject(jsonString) val peerID = json.getString("peerID") val nickname = json.getString("nickname") val publicKeyBase64 = json.getString("publicKey") val timestampMs = json.getLong("timestamp") val signatureBase64 = json.getString("signature") val previousPeerID = if (json.has("previousPeerID")) json.getString("previousPeerID") else null // Decode base64 fields val publicKey = android.util.Base64.decode(publicKeyBase64, android.util.Base64.DEFAULT) val signature = android.util.Base64.decode(signatureBase64, android.util.Base64.DEFAULT) // Calculate fingerprint from public key val digest = MessageDigest.getInstance("SHA-256") val hash = digest.digest(publicKey) val fingerprint = hash.joinToString("") { "%02x".format(it) } NoiseIdentityAnnouncement( peerID = peerID, nickname = nickname, publicKey = publicKey, timestamp = Date(timestampMs), signature = signature, fingerprint = fingerprint, previousPeerID = previousPeerID ) } catch (e: Exception) { Log.e(TAG, "Failed to parse Noise identity announcement: ${e.message}") null } } /** * Shutdown the handler */ fun shutdown() { handlerScope.cancel() } } /** * Noise Identity Announcement data class (compatible with iOS version) */ data class NoiseIdentityAnnouncement( val peerID: String, val nickname: String, val publicKey: ByteArray, val timestamp: Date, val signature: ByteArray, val fingerprint: String?, val previousPeerID: String? = null ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as NoiseIdentityAnnouncement if (peerID != other.peerID) return false if (nickname != other.nickname) return false if (!publicKey.contentEquals(other.publicKey)) return false if (timestamp != other.timestamp) return false if (!signature.contentEquals(other.signature)) return false if (fingerprint != other.fingerprint) return false if (previousPeerID != other.previousPeerID) return false return true } override fun hashCode(): Int { var result = peerID.hashCode() result = 31 * result + nickname.hashCode() result = 31 * result + publicKey.contentHashCode() result = 31 * result + timestamp.hashCode() result = 31 * result + signature.contentHashCode() result = 31 * result + (fingerprint?.hashCode() ?: 0) result = 31 * result + (previousPeerID?.hashCode() ?: 0) return result } } /** * Delegate interface for message handler callbacks */ interface MessageHandlerDelegate { // Peer management fun addOrUpdatePeer(peerID: String, nickname: String): Boolean fun removePeer(peerID: String) fun updatePeerNickname(peerID: String, nickname: String) fun getPeerNickname(peerID: String): String? fun getNetworkSize(): Int fun getMyNickname(): String? // Packet operations fun sendPacket(packet: BitchatPacket) fun relayPacket(routed: RoutedPacket) fun getBroadcastRecipient(): ByteArray // Cryptographic operations fun verifySignature(packet: BitchatPacket, peerID: String): Boolean 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? // Callbacks fun onMessageReceived(message: BitchatMessage) fun onChannelLeave(channel: String, fromPeer: String) fun onPeerDisconnected(nickname: String) fun onDeliveryAckReceived(ack: DeliveryAck) fun onReadReceiptReceived(receipt: ReadReceipt) }