mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 00:25:20 +00:00
475 lines
19 KiB
Kotlin
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)
|
|
}
|