mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 06:05:21 +00:00
583 lines
22 KiB
Kotlin
583 lines
22 KiB
Kotlin
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)
|
|
}
|