Files
bitchat-android/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt
T
2025-08-20 00:28:02 +02:00

475 lines
19 KiB
Kotlin

package com.bitchat.android.mesh
import android.util.Log
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.IdentityAnnouncement
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
/**
* 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
// Reference to PacketProcessor for recursive packet handling
var packetProcessor: PacketProcessor? = null
// Coroutines
private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
/**
* Handle Noise encrypted transport message - SIMPLIFIED iOS-compatible version
* Uses NoisePayloadType system exactly like iOS SimplifiedBluetoothService
*/
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
// Check if this message is for us
val recipientID = packet.recipientID?.toHexString()
if (recipientID != myPeerID) {
Log.d(TAG, "🔐 Encrypted message not for me (for $recipientID, I am $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
}
if (decryptedData.isEmpty()) {
Log.w(TAG, "Decrypted data is empty from $peerID")
return
}
// NEW: Use NoisePayload system exactly like iOS
val noisePayload = com.bitchat.android.model.NoisePayload.decode(decryptedData)
if (noisePayload == null) {
Log.w(TAG, "Failed to parse NoisePayload from $peerID")
return
}
Log.d(TAG, "🔓 Decrypted NoisePayload type ${noisePayload.type} from $peerID")
when (noisePayload.type) {
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
// Decode TLV private message exactly like iOS
val privateMessage = com.bitchat.android.model.PrivateMessagePacket.decode(noisePayload.data)
if (privateMessage != null) {
Log.d(TAG, "🔓 Decrypted TLV PM from $peerID: ${privateMessage.content.take(30)}...")
// Create BitchatMessage exactly like iOS
val message = BitchatMessage(
id = privateMessage.messageID,
sender = delegate?.getPeerNickname(peerID) ?: "Unknown",
content = privateMessage.content,
timestamp = java.util.Date(packet.timestamp.toLong()),
isRelay = false,
originalSender = null,
isPrivate = true,
recipientNickname = delegate?.getMyNickname(),
senderPeerID = peerID,
mentions = null // TODO: Parse mentions if needed
)
// Notify delegate
delegate?.onMessageReceived(message)
// Send delivery ACK exactly like iOS
sendDeliveryAck(privateMessage.messageID, peerID)
}
}
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
// Handle delivery ACK exactly like iOS
val messageID = String(noisePayload.data, Charsets.UTF_8)
Log.d(TAG, "📬 Delivery ACK received from $peerID for message $messageID")
// Simplified: Call delegate with messageID and peerID directly
delegate?.onDeliveryAckReceived(messageID, peerID)
}
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
// Handle read receipt exactly like iOS
val messageID = String(noisePayload.data, Charsets.UTF_8)
Log.d(TAG, "👁️ Read receipt received from $peerID for message $messageID")
// Simplified: Call delegate with messageID and peerID directly
delegate?.onReadReceiptReceived(messageID, peerID)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error processing Noise encrypted message from $peerID: ${e.message}")
}
}
/**
* Send delivery ACK for a received private message - exactly like iOS
*/
private suspend fun sendDeliveryAck(messageID: String, senderPeerID: String) {
try {
// Create ACK payload: [type byte] + [message ID] - exactly like iOS
val ackPayload = com.bitchat.android.model.NoisePayload(
type = com.bitchat.android.model.NoisePayloadType.DELIVERED,
data = messageID.toByteArray(Charsets.UTF_8)
)
// Encrypt the payload
val encryptedPayload = delegate?.encryptForPeer(ackPayload.encode(), senderPeerID)
if (encryptedPayload == null) {
Log.w(TAG, "Failed to encrypt delivery ACK for $senderPeerID")
return
}
// Create NOISE_ENCRYPTED packet exactly like iOS
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(senderPeerID),
timestamp = System.currentTimeMillis().toULong(),
payload = encryptedPayload,
signature = null,
ttl = 7u // Same TTL as iOS messageTTL
)
delegate?.sendPacket(packet)
Log.d(TAG, "📤 Sent delivery ACK to $senderPeerID for message $messageID")
} catch (e: Exception) {
Log.e(TAG, "Failed to send delivery ACK to $senderPeerID: ${e.message}")
}
}
/**
* Handle announce message with TLV decoding and signature verification - exactly like iOS
*/
suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
if (peerID == myPeerID) return false
// Try to decode as iOS-compatible IdentityAnnouncement with TLV format
val announcement = IdentityAnnouncement.decode(packet.payload)
if (announcement == null) {
Log.w(TAG, "Failed to decode announce from $peerID as iOS-compatible TLV format")
return false
}
// Verify packet signature using the announced signing public key
var verified = false
if (packet.signature != null) {
// Verify that the packet was signed by the signing private key corresponding to the announced signing public key
verified = delegate?.verifyEd25519Signature(packet.signature!!, packet.toBinaryDataForSigning()!!, announcement.signingPublicKey) ?: false
if (!verified) {
Log.w(TAG, "⚠️ Signature verification for announce failed ${peerID.take(8)}")
}
}
// Check for existing peer with different noise public key
// If existing peer has a different noise public key, do not consider this verified
val existingPeer = (delegate as? BluetoothMeshService)?.let { service ->
// Access PeerManager through service
service.getPeerInfo(peerID)
}
if (existingPeer != null && existingPeer.noisePublicKey != null && !existingPeer.noisePublicKey!!.contentEquals(announcement.noisePublicKey)) {
Log.w(TAG, "⚠️ Announce key mismatch for ${peerID.take(8)}... — keeping unverified")
verified = false
}
// Require verified announce; ignore otherwise (no backward compatibility)
if (!verified) {
Log.w(TAG, "❌ Ignoring unverified announce from ${peerID.take(8)}...")
return false
}
// Successfully decoded TLV format exactly like iOS
Log.d(TAG, "✅ Verified announce from $peerID: nickname=${announcement.nickname}, " +
"noisePublicKey=${announcement.noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., " +
"signingPublicKey=${announcement.signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...")
// Extract nickname and public keys from TLV data
val nickname = announcement.nickname
val noisePublicKey = announcement.noisePublicKey
val signingPublicKey = announcement.signingPublicKey
// Update peer info with verification status through new method
val isFirstAnnounce = (delegate as? BluetoothMeshService)?.let { service ->
service.updatePeerInfo(
peerID = peerID,
nickname = nickname,
noisePublicKey = noisePublicKey,
signingPublicKey = signingPublicKey,
isVerified = true
)
} ?: false
// Update peer ID binding with noise public key for identity management
delegate?.updatePeerIDBinding(
newPeerID = peerID,
nickname = nickname,
publicKey = noisePublicKey,
previousPeerID = null
)
Log.d(TAG, "✅ Processed verified TLV announce: stored identity for $peerID")
return isFirstAnnounce
}
/**
* Handle Noise handshake - SIMPLIFIED iOS-compatible version
* Single handshake type (0x10) with response determined by payload analysis
*/
suspend fun handleNoiseHandshake(routed: RoutedPacket) {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)")
// Skip our own handshake messages
if (peerID == myPeerID) return
// Check if handshake is addressed to us
val recipientID = packet.recipientID?.toHexString()
if (recipientID != myPeerID) {
Log.d(TAG, "Handshake not for me (for $recipientID, I am $myPeerID)")
return
}
try {
// Process handshake message through delegate (simplified approach)
val response = delegate?.processNoiseHandshakeMessage(packet.payload, peerID)
if (response != null) {
Log.d(TAG, "Generated handshake response for $peerID (${response.size} bytes)")
// Send response using same packet type (simplified iOS approach)
val responsePacket = BitchatPacket(
version = 1u,
type = MessageType.NOISE_HANDSHAKE.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(peerID),
timestamp = System.currentTimeMillis().toULong(),
payload = response,
signature = null,
ttl = 7u // Same TTL as iOS
)
delegate?.sendPacket(responsePacket)
Log.d(TAG, "📤 Sent handshake response to $peerID")
}
// Check if session is now established
val hasSession = delegate?.hasNoiseSession(peerID) ?: false
if (hasSession) {
Log.d(TAG, "✅ Noise session established with $peerID")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to process Noise handshake from $peerID: ${e.message}")
}
}
/**
* Handle broadcast or private message
*/
suspend fun handleMessage(routed: RoutedPacket) {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
if (peerID == myPeerID) return
val senderNickname = delegate?.getPeerNickname(peerID)
if (senderNickname != null) {
Log.d(TAG, "Received message from $senderNickname")
delegate?.updatePeerNickname(peerID, senderNickname)
}
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
if (recipientID == null) {
// BROADCAST MESSAGE
handleBroadcastMessage(routed)
} else if (recipientID.toHexString() == myPeerID) {
// PRIVATE MESSAGE FOR US
handlePrivateMessage(packet, peerID)
}
// Message relay is now handled by centralized PacketRelayManager
}
/**
* Handle broadcast message with verification enforcement
*/
private suspend fun handleBroadcastMessage(routed: RoutedPacket) {
val packet = routed.packet
val peerID = routed.peerID ?: "unknown"
// Enforce: only accept public messages from verified peers we know
val peerInfo = (delegate as? BluetoothMeshService)?.getPeerInfo(peerID)
if (peerInfo == null || !peerInfo.isVerifiedNickname) {
Log.w(TAG, "🚫 Dropping public message from unverified or unknown peer ${peerID.take(8)}...")
return
}
try {
// Parse message
val message = BitchatMessage(
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
content = String(packet.payload, Charsets.UTF_8),
senderPeerID = peerID,
timestamp = Date()
)
delegate?.onMessageReceived(message)
} catch (e: Exception) {
Log.e(TAG, "Failed to process broadcast message: ${e.message}")
}
}
/**
* Handle (decrypted) 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
}
// Parse message
val message = BitchatMessage(
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
content = String(packet.payload, Charsets.UTF_8),
senderPeerID = peerID,
timestamp = Date()
)
delegate?.onMessageReceived(message)
} 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
delegate?.removePeer(peerID)
}
// Leave message relay is now handled by centralized PacketRelayManager
}
/**
* Get debug information
*/
fun getDebugInfo(): String {
return buildString {
appendLine("=== Message Handler Debug Info ===")
appendLine("Handler Scope Active: ${handlerScope.isActive}")
appendLine("My Peer ID: $myPeerID")
}
}
/**
* 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
*/
fun shutdown() {
handlerScope.cancel()
}
}
/**
* 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?
fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean
// Noise protocol operations
fun hasNoiseSession(peerID: String): Boolean
fun initiateNoiseHandshake(peerID: String)
fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray?
fun updatePeerIDBinding(newPeerID: 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 onDeliveryAckReceived(messageID: String, peerID: String)
fun onReadReceiptReceived(messageID: String, peerID: String)
}