mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 03:05:20 +00:00
seems to work
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Manages message fragmentation and reassembly
|
||||
* Extracted from BluetoothMeshService for better separation of concerns
|
||||
*/
|
||||
class FragmentManager {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FragmentManager"
|
||||
private const val MAX_FRAGMENT_SIZE = 500
|
||||
private const val FRAGMENT_TIMEOUT = 30000L // 30 seconds
|
||||
private const val CLEANUP_INTERVAL = 10000L // 10 seconds
|
||||
}
|
||||
|
||||
// Fragment storage
|
||||
private val incomingFragments = ConcurrentHashMap<String, MutableMap<Int, ByteArray>>()
|
||||
private val fragmentMetadata = ConcurrentHashMap<String, Triple<UByte, Int, Long>>() // originalType, totalFragments, timestamp
|
||||
|
||||
// Delegate for callbacks
|
||||
var delegate: FragmentManagerDelegate? = null
|
||||
|
||||
// Coroutines
|
||||
private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
startPeriodicCleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fragments from a large packet
|
||||
*/
|
||||
fun createFragments(packet: BitchatPacket): List<BitchatPacket> {
|
||||
val data = packet.toBinaryData() ?: return emptyList()
|
||||
|
||||
if (data.size <= MAX_FRAGMENT_SIZE) {
|
||||
return listOf(packet) // No fragmentation needed
|
||||
}
|
||||
|
||||
val fragments = mutableListOf<BitchatPacket>()
|
||||
val fragmentID = generateFragmentID()
|
||||
|
||||
// Calculate header size (13 bytes for fragment metadata)
|
||||
val headerSize = 13
|
||||
val dataPerFragment = MAX_FRAGMENT_SIZE - headerSize
|
||||
val totalFragments = (data.size + dataPerFragment - 1) / dataPerFragment
|
||||
|
||||
Log.d(TAG, "Creating ${totalFragments} fragments for ${data.size} byte packet")
|
||||
|
||||
for (i in 0 until totalFragments) {
|
||||
val start = i * dataPerFragment
|
||||
val end = minOf(start + dataPerFragment, data.size)
|
||||
val fragmentData = data.sliceArray(start until end)
|
||||
|
||||
val fragmentPayload = createFragmentPayload(
|
||||
fragmentID = fragmentID,
|
||||
index = i,
|
||||
total = totalFragments,
|
||||
originalType = packet.type,
|
||||
data = fragmentData
|
||||
)
|
||||
|
||||
val fragmentType = when (i) {
|
||||
0 -> MessageType.FRAGMENT_START
|
||||
totalFragments - 1 -> MessageType.FRAGMENT_END
|
||||
else -> MessageType.FRAGMENT_CONTINUE
|
||||
}
|
||||
|
||||
val fragmentPacket = BitchatPacket(
|
||||
type = fragmentType.value,
|
||||
ttl = packet.ttl,
|
||||
senderID = packet.senderID,
|
||||
recipientID = packet.recipientID,
|
||||
timestamp = packet.timestamp,
|
||||
payload = fragmentPayload,
|
||||
signature = null // Fragments aren't individually signed
|
||||
)
|
||||
|
||||
fragments.add(fragmentPacket)
|
||||
}
|
||||
|
||||
return fragments
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming fragment
|
||||
*/
|
||||
fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
||||
if (packet.payload.size < 13) {
|
||||
Log.w(TAG, "Fragment packet too small: ${packet.payload.size}")
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
// Extract fragment metadata (same format as iOS)
|
||||
val fragmentIDData = packet.payload.sliceArray(0..7)
|
||||
val fragmentID = fragmentIDData.contentHashCode().toString()
|
||||
|
||||
val index = ((packet.payload[8].toInt() and 0xFF) shl 8) or (packet.payload[9].toInt() and 0xFF)
|
||||
val total = ((packet.payload[10].toInt() and 0xFF) shl 8) or (packet.payload[11].toInt() and 0xFF)
|
||||
val originalType = packet.payload[12].toUByte()
|
||||
val fragmentData = packet.payload.sliceArray(13 until packet.payload.size)
|
||||
|
||||
Log.d(TAG, "Received fragment $index/$total for fragmentID: $fragmentID, originalType: $originalType")
|
||||
|
||||
// Store fragment
|
||||
if (!incomingFragments.containsKey(fragmentID)) {
|
||||
incomingFragments[fragmentID] = mutableMapOf()
|
||||
fragmentMetadata[fragmentID] = Triple(originalType, total, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
incomingFragments[fragmentID]?.put(index, fragmentData)
|
||||
|
||||
// Check if we have all fragments
|
||||
if (incomingFragments[fragmentID]?.size == total) {
|
||||
Log.d(TAG, "All fragments received for $fragmentID, reassembling...")
|
||||
|
||||
// Reassemble message
|
||||
val reassembledData = mutableListOf<Byte>()
|
||||
for (i in 0 until total) {
|
||||
incomingFragments[fragmentID]?.get(i)?.let { data ->
|
||||
reassembledData.addAll(data.asIterable())
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and return reassembled packet
|
||||
val reassembledPacket = BitchatPacket.fromBinaryData(reassembledData.toByteArray())
|
||||
|
||||
// Cleanup
|
||||
incomingFragments.remove(fragmentID)
|
||||
fragmentMetadata.remove(fragmentID)
|
||||
|
||||
if (reassembledPacket != null) {
|
||||
Log.d(TAG, "Successfully reassembled packet of ${reassembledData.size} bytes")
|
||||
return reassembledPacket
|
||||
} else {
|
||||
Log.e(TAG, "Failed to parse reassembled packet")
|
||||
}
|
||||
} else {
|
||||
val received = incomingFragments[fragmentID]?.size ?: 0
|
||||
Log.d(TAG, "Fragment $index stored, have $received/$total fragments for $fragmentID")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to handle fragment: ${e.message}")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fragment payload with metadata
|
||||
*/
|
||||
private fun createFragmentPayload(
|
||||
fragmentID: String,
|
||||
index: Int,
|
||||
total: Int,
|
||||
originalType: UByte,
|
||||
data: ByteArray
|
||||
): ByteArray {
|
||||
val payload = ByteArray(13 + data.size)
|
||||
|
||||
// Fragment ID (8 bytes)
|
||||
val idBytes = fragmentID.toByteArray()
|
||||
System.arraycopy(idBytes, 0, payload, 0, minOf(8, idBytes.size))
|
||||
|
||||
// Index (2 bytes, big-endian)
|
||||
payload[8] = ((index shr 8) and 0xFF).toByte()
|
||||
payload[9] = (index and 0xFF).toByte()
|
||||
|
||||
// Total (2 bytes, big-endian)
|
||||
payload[10] = ((total shr 8) and 0xFF).toByte()
|
||||
payload[11] = (total and 0xFF).toByte()
|
||||
|
||||
// Original type (1 byte)
|
||||
payload[12] = originalType.toByte()
|
||||
|
||||
// Fragment data
|
||||
System.arraycopy(data, 0, payload, 13, data.size)
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique fragment ID
|
||||
*/
|
||||
private fun generateFragmentID(): String {
|
||||
return "${System.currentTimeMillis()}-${kotlin.random.Random.nextInt()}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
*/
|
||||
fun getDebugInfo(): String {
|
||||
return buildString {
|
||||
appendLine("=== Fragment Manager Debug Info ===")
|
||||
appendLine("Active Fragment Sets: ${incomingFragments.size}")
|
||||
fragmentMetadata.forEach { (fragmentID, metadata) ->
|
||||
val (originalType, totalFragments, timestamp) = metadata
|
||||
val received = incomingFragments[fragmentID]?.size ?: 0
|
||||
val ageSeconds = (System.currentTimeMillis() - timestamp) / 1000
|
||||
appendLine(" - $fragmentID: $received/$totalFragments fragments, type: $originalType, age: ${ageSeconds}s")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic cleanup of old fragments
|
||||
*/
|
||||
private fun startPeriodicCleanup() {
|
||||
managerScope.launch {
|
||||
while (isActive) {
|
||||
delay(CLEANUP_INTERVAL)
|
||||
cleanupOldFragments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old fragments (older than 30 seconds)
|
||||
*/
|
||||
private fun cleanupOldFragments() {
|
||||
val cutoffTime = System.currentTimeMillis() - FRAGMENT_TIMEOUT
|
||||
val fragmentsToRemove = mutableListOf<String>()
|
||||
|
||||
fragmentMetadata.entries.forEach { (fragmentID, metadata) ->
|
||||
if (metadata.third < cutoffTime) {
|
||||
fragmentsToRemove.add(fragmentID)
|
||||
}
|
||||
}
|
||||
|
||||
fragmentsToRemove.forEach { fragmentID ->
|
||||
incomingFragments.remove(fragmentID)
|
||||
fragmentMetadata.remove(fragmentID)
|
||||
}
|
||||
|
||||
if (fragmentsToRemove.isNotEmpty()) {
|
||||
Log.d(TAG, "Cleaned up ${fragmentsToRemove.size} old fragment sets")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all fragments
|
||||
*/
|
||||
fun clearAllFragments() {
|
||||
incomingFragments.clear()
|
||||
fragmentMetadata.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the manager
|
||||
*/
|
||||
fun shutdown() {
|
||||
managerScope.cancel()
|
||||
clearAllFragments()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate interface for fragment manager callbacks
|
||||
*/
|
||||
interface FragmentManagerDelegate {
|
||||
fun onPacketReassembled(packet: BitchatPacket)
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
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.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
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
|
||||
|
||||
// Coroutines
|
||||
private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
/**
|
||||
* Handle announce message
|
||||
*/
|
||||
suspend fun handleAnnounce(packet: BitchatPacket, peerID: String): Boolean {
|
||||
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(relayPacket)
|
||||
}
|
||||
|
||||
return isFirstAnnounce
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle broadcast or private message
|
||||
*/
|
||||
suspend fun handleMessage(packet: BitchatPacket, peerID: String) {
|
||||
if (peerID == myPeerID) return
|
||||
|
||||
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
|
||||
|
||||
if (recipientID == null) {
|
||||
// BROADCAST MESSAGE
|
||||
handleBroadcastMessage(packet, peerID)
|
||||
} else if (String(recipientID).replace("\u0000", "") == myPeerID) {
|
||||
// PRIVATE MESSAGE FOR US
|
||||
handlePrivateMessage(packet, peerID)
|
||||
} else if (packet.ttl > 0u) {
|
||||
// RELAY MESSAGE
|
||||
relayMessage(packet)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle broadcast message
|
||||
*/
|
||||
private suspend fun handleBroadcastMessage(packet: BitchatPacket, peerID: String) {
|
||||
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(packet)
|
||||
|
||||
} 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(packet: BitchatPacket, peerID: String) {
|
||||
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(relayPacket)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delivery acknowledgment
|
||||
*/
|
||||
suspend fun handleDeliveryAck(packet: BitchatPacket, peerID: String) {
|
||||
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) {
|
||||
// Relay
|
||||
val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte())
|
||||
delegate?.relayPacket(relayPacket)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle read receipt
|
||||
*/
|
||||
suspend fun handleReadReceipt(packet: BitchatPacket, peerID: String) {
|
||||
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) {
|
||||
// Relay
|
||||
val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte())
|
||||
delegate?.relayPacket(relayPacket)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Relay message with adaptive probability (same as iOS)
|
||||
*/
|
||||
private suspend fun relayMessage(packet: BitchatPacket) {
|
||||
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(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")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(packet: BitchatPacket)
|
||||
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?
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Processes incoming packets and routes them to appropriate handlers
|
||||
* Extracted from BluetoothMeshService for better separation of concerns
|
||||
*/
|
||||
class PacketProcessor(private val myPeerID: String) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PacketProcessor"
|
||||
}
|
||||
|
||||
// Delegate for callbacks
|
||||
var delegate: PacketProcessorDelegate? = null
|
||||
|
||||
// Coroutines
|
||||
private val processorScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
/**
|
||||
* Process received packet - main entry point for all incoming packets
|
||||
*/
|
||||
fun processPacket(packet: BitchatPacket, peerID: String) {
|
||||
processorScope.launch {
|
||||
handleReceivedPacket(packet, peerID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle received packet - core protocol logic (exact same as iOS)
|
||||
*/
|
||||
private suspend fun handleReceivedPacket(packet: BitchatPacket, peerID: String) {
|
||||
// Basic validation and security checks
|
||||
if (!delegate?.validatePacketSecurity(packet, peerID)!!) {
|
||||
Log.d(TAG, "Packet failed security validation from $peerID")
|
||||
return
|
||||
}
|
||||
|
||||
// Update last seen timestamp
|
||||
delegate?.updatePeerLastSeen(peerID)
|
||||
|
||||
Log.d(TAG, "Processing packet type ${packet.type} from $peerID")
|
||||
|
||||
// Process based on message type (exact same logic as iOS)
|
||||
when (MessageType.fromValue(packet.type)) {
|
||||
MessageType.KEY_EXCHANGE -> handleKeyExchange(packet, peerID)
|
||||
MessageType.ANNOUNCE -> handleAnnounce(packet, peerID)
|
||||
MessageType.MESSAGE -> handleMessage(packet, peerID)
|
||||
MessageType.LEAVE -> handleLeave(packet, peerID)
|
||||
MessageType.FRAGMENT_START,
|
||||
MessageType.FRAGMENT_CONTINUE,
|
||||
MessageType.FRAGMENT_END -> handleFragment(packet, peerID)
|
||||
MessageType.DELIVERY_ACK -> handleDeliveryAck(packet, peerID)
|
||||
MessageType.READ_RECEIPT -> handleReadReceipt(packet, peerID)
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown message type: ${packet.type}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key exchange message
|
||||
*/
|
||||
private suspend fun handleKeyExchange(packet: BitchatPacket, peerID: String) {
|
||||
Log.d(TAG, "Processing key exchange from $peerID")
|
||||
|
||||
val success = delegate?.handleKeyExchange(packet, peerID) ?: false
|
||||
|
||||
if (success) {
|
||||
// Key exchange successful, send announce and cached messages
|
||||
delay(100)
|
||||
delegate?.sendAnnouncementToPeer(peerID)
|
||||
|
||||
delay(500)
|
||||
delegate?.sendCachedMessages(peerID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle announce message
|
||||
*/
|
||||
private suspend fun handleAnnounce(packet: BitchatPacket, peerID: String) {
|
||||
Log.d(TAG, "Processing announce from $peerID")
|
||||
delegate?.handleAnnounce(packet, peerID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle regular message
|
||||
*/
|
||||
private suspend fun handleMessage(packet: BitchatPacket, peerID: String) {
|
||||
Log.d(TAG, "Processing message from $peerID")
|
||||
delegate?.handleMessage(packet, peerID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle leave message
|
||||
*/
|
||||
private suspend fun handleLeave(packet: BitchatPacket, peerID: String) {
|
||||
Log.d(TAG, "Processing leave from $peerID")
|
||||
delegate?.handleLeave(packet, peerID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle message fragments
|
||||
*/
|
||||
private suspend fun handleFragment(packet: BitchatPacket, peerID: String) {
|
||||
Log.d(TAG, "Processing fragment from $peerID")
|
||||
|
||||
val reassembledPacket = delegate?.handleFragment(packet)
|
||||
if (reassembledPacket != null) {
|
||||
Log.d(TAG, "Fragment reassembled, processing complete message")
|
||||
handleReceivedPacket(reassembledPacket, peerID)
|
||||
}
|
||||
|
||||
// Relay fragment regardless of reassembly
|
||||
if (packet.ttl > 0u) {
|
||||
val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte())
|
||||
delegate?.relayPacket(relayPacket)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delivery acknowledgment
|
||||
*/
|
||||
private suspend fun handleDeliveryAck(packet: BitchatPacket, peerID: String) {
|
||||
Log.d(TAG, "Processing delivery ACK from $peerID")
|
||||
delegate?.handleDeliveryAck(packet, peerID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle read receipt
|
||||
*/
|
||||
private suspend fun handleReadReceipt(packet: BitchatPacket, peerID: String) {
|
||||
Log.d(TAG, "Processing read receipt from $peerID")
|
||||
delegate?.handleReadReceipt(packet, peerID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
*/
|
||||
fun getDebugInfo(): String {
|
||||
return buildString {
|
||||
appendLine("=== Packet Processor Debug Info ===")
|
||||
appendLine("Processor Scope Active: ${processorScope.isActive}")
|
||||
appendLine("My Peer ID: $myPeerID")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the processor
|
||||
*/
|
||||
fun shutdown() {
|
||||
processorScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate interface for packet processor callbacks
|
||||
*/
|
||||
interface PacketProcessorDelegate {
|
||||
// Security validation
|
||||
fun validatePacketSecurity(packet: BitchatPacket, peerID: String): Boolean
|
||||
|
||||
// Peer management
|
||||
fun updatePeerLastSeen(peerID: String)
|
||||
|
||||
// Message type handlers
|
||||
fun handleKeyExchange(packet: BitchatPacket, peerID: String): Boolean
|
||||
fun handleAnnounce(packet: BitchatPacket, peerID: String)
|
||||
fun handleMessage(packet: BitchatPacket, peerID: String)
|
||||
fun handleLeave(packet: BitchatPacket, peerID: String)
|
||||
fun handleFragment(packet: BitchatPacket): BitchatPacket?
|
||||
fun handleDeliveryAck(packet: BitchatPacket, peerID: String)
|
||||
fun handleReadReceipt(packet: BitchatPacket, peerID: String)
|
||||
|
||||
// Communication
|
||||
fun sendAnnouncementToPeer(peerID: String)
|
||||
fun sendCachedMessages(peerID: String)
|
||||
fun relayPacket(packet: BitchatPacket)
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* Manages active peers, nicknames, and RSSI tracking
|
||||
* Extracted from BluetoothMeshService for better separation of concerns
|
||||
*/
|
||||
class PeerManager {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PeerManager"
|
||||
private const val STALE_PEER_TIMEOUT = 180000L // 3 minutes (same as iOS)
|
||||
private const val CLEANUP_INTERVAL = 60000L // 1 minute
|
||||
}
|
||||
|
||||
// Peer tracking data
|
||||
private val peerNicknames = ConcurrentHashMap<String, String>()
|
||||
private val activePeers = ConcurrentHashMap<String, Long>() // peerID -> lastSeen timestamp
|
||||
private val peerRSSI = ConcurrentHashMap<String, Int>()
|
||||
private val announcedPeers = CopyOnWriteArrayList<String>()
|
||||
private val announcedToPeers = CopyOnWriteArrayList<String>()
|
||||
|
||||
// Delegate for callbacks
|
||||
var delegate: PeerManagerDelegate? = null
|
||||
|
||||
// Coroutines
|
||||
private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
startPeriodicCleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update peer last seen timestamp
|
||||
*/
|
||||
fun updatePeerLastSeen(peerID: String) {
|
||||
if (peerID != "unknown") {
|
||||
activePeers[peerID] = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update peer with nickname
|
||||
*/
|
||||
fun addOrUpdatePeer(peerID: String, nickname: String): Boolean {
|
||||
if (peerID == "unknown") return false
|
||||
|
||||
// Clean up stale peer IDs with the same nickname (exact same logic as iOS)
|
||||
val stalePeerIDs = mutableListOf<String>()
|
||||
peerNicknames.forEach { (existingPeerID, existingNickname) ->
|
||||
if (existingNickname == nickname && existingPeerID != peerID) {
|
||||
val lastSeen = activePeers[existingPeerID] ?: 0
|
||||
val wasRecentlySeen = (System.currentTimeMillis() - lastSeen) < 10000
|
||||
if (!wasRecentlySeen) {
|
||||
stalePeerIDs.add(existingPeerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove stale peer IDs
|
||||
stalePeerIDs.forEach { stalePeerID ->
|
||||
removePeer(stalePeerID, notifyDelegate = false)
|
||||
}
|
||||
|
||||
// Check if this is a new peer announcement
|
||||
val isFirstAnnounce = !announcedPeers.contains(peerID)
|
||||
|
||||
// Update peer data
|
||||
peerNicknames[peerID] = nickname
|
||||
activePeers[peerID] = System.currentTimeMillis()
|
||||
|
||||
// Handle first announcement
|
||||
if (isFirstAnnounce) {
|
||||
announcedPeers.add(peerID)
|
||||
delegate?.onPeerConnected(nickname)
|
||||
notifyPeerListUpdate()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove peer
|
||||
*/
|
||||
fun removePeer(peerID: String, notifyDelegate: Boolean = true) {
|
||||
val nickname = peerNicknames.remove(peerID)
|
||||
activePeers.remove(peerID)
|
||||
peerRSSI.remove(peerID)
|
||||
announcedPeers.remove(peerID)
|
||||
announcedToPeers.remove(peerID)
|
||||
|
||||
if (notifyDelegate && nickname != null) {
|
||||
delegate?.onPeerDisconnected(nickname)
|
||||
notifyPeerListUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update peer RSSI
|
||||
*/
|
||||
fun updatePeerRSSI(peerID: String, rssi: Int) {
|
||||
if (peerID != "unknown") {
|
||||
peerRSSI[peerID] = rssi
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if peer has been announced to
|
||||
*/
|
||||
fun hasAnnouncedToPeer(peerID: String): Boolean {
|
||||
return announcedToPeers.contains(peerID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark peer as announced to
|
||||
*/
|
||||
fun markPeerAsAnnouncedTo(peerID: String) {
|
||||
if (!announcedToPeers.contains(peerID)) {
|
||||
announcedToPeers.add(peerID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if peer is active
|
||||
*/
|
||||
fun isPeerActive(peerID: String): Boolean {
|
||||
return activePeers.containsKey(peerID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get peer nickname
|
||||
*/
|
||||
fun getPeerNickname(peerID: String): String? {
|
||||
return peerNicknames[peerID]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all peer nicknames
|
||||
*/
|
||||
fun getAllPeerNicknames(): Map<String, String> {
|
||||
return peerNicknames.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all peer RSSI values
|
||||
*/
|
||||
fun getAllPeerRSSI(): Map<String, Int> {
|
||||
return peerRSSI.toMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of active peer IDs
|
||||
*/
|
||||
fun getActivePeerIDs(): List<String> {
|
||||
return activePeers.keys.toList().sorted()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active peer count
|
||||
*/
|
||||
fun getActivePeerCount(): Int {
|
||||
return activePeers.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all peer data
|
||||
*/
|
||||
fun clearAllPeers() {
|
||||
peerNicknames.clear()
|
||||
activePeers.clear()
|
||||
peerRSSI.clear()
|
||||
announcedPeers.clear()
|
||||
announcedToPeers.clear()
|
||||
notifyPeerListUpdate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
*/
|
||||
fun getDebugInfo(): String {
|
||||
return buildString {
|
||||
appendLine("=== Peer Manager Debug Info ===")
|
||||
appendLine("Active Peers: ${activePeers.size}")
|
||||
activePeers.forEach { (peerID, lastSeen) ->
|
||||
val nickname = peerNicknames[peerID] ?: "Unknown"
|
||||
val timeSince = (System.currentTimeMillis() - lastSeen) / 1000
|
||||
val rssi = peerRSSI[peerID]?.let { "${it} dBm" } ?: "No RSSI"
|
||||
appendLine(" - $peerID ($nickname) - last seen ${timeSince}s ago, RSSI: $rssi")
|
||||
}
|
||||
appendLine("Announced Peers: ${announcedPeers.size}")
|
||||
appendLine("Announced To Peers: ${announcedToPeers.size}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify delegate of peer list updates
|
||||
*/
|
||||
private fun notifyPeerListUpdate() {
|
||||
val peerList = getActivePeerIDs()
|
||||
delegate?.onPeerListUpdated(peerList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic cleanup of stale peers
|
||||
*/
|
||||
private fun startPeriodicCleanup() {
|
||||
managerScope.launch {
|
||||
while (isActive) {
|
||||
delay(CLEANUP_INTERVAL)
|
||||
cleanupStalePeers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up stale peers (same 3-minute threshold as iOS)
|
||||
*/
|
||||
private fun cleanupStalePeers() {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
val peersToRemove = activePeers.entries.filter { (_, lastSeen) ->
|
||||
now - lastSeen > STALE_PEER_TIMEOUT
|
||||
}.map { it.key }
|
||||
|
||||
peersToRemove.forEach { peerID ->
|
||||
Log.d(TAG, "Removing stale peer: $peerID")
|
||||
removePeer(peerID)
|
||||
}
|
||||
|
||||
if (peersToRemove.isNotEmpty()) {
|
||||
Log.d(TAG, "Cleaned up ${peersToRemove.size} stale peers")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the manager
|
||||
*/
|
||||
fun shutdown() {
|
||||
managerScope.cancel()
|
||||
clearAllPeers()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate interface for peer manager callbacks
|
||||
*/
|
||||
interface PeerManagerDelegate {
|
||||
fun onPeerConnected(nickname: String)
|
||||
fun onPeerDisconnected(nickname: String)
|
||||
fun onPeerListUpdated(peerIDs: List<String>)
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import kotlin.collections.mutableSetOf
|
||||
|
||||
/**
|
||||
* Manages security aspects of the mesh network including duplicate detection,
|
||||
* replay attack protection, and key exchange handling
|
||||
* Extracted from BluetoothMeshService for better separation of concerns
|
||||
*/
|
||||
class SecurityManager(private val encryptionService: EncryptionService, private val myPeerID: String) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SecurityManager"
|
||||
private const val MESSAGE_TIMEOUT = 300000L // 5 minutes (same as iOS)
|
||||
private const val CLEANUP_INTERVAL = 300000L // 5 minutes
|
||||
private const val MAX_PROCESSED_MESSAGES = 10000
|
||||
private const val MAX_PROCESSED_KEY_EXCHANGES = 1000
|
||||
}
|
||||
|
||||
// Security tracking
|
||||
private val processedMessages = Collections.synchronizedSet(mutableSetOf<String>())
|
||||
private val processedKeyExchanges = Collections.synchronizedSet(mutableSetOf<String>())
|
||||
private val messageTimestamps = Collections.synchronizedMap(mutableMapOf<String, Long>())
|
||||
|
||||
// Delegate for callbacks
|
||||
var delegate: SecurityManagerDelegate? = null
|
||||
|
||||
// Coroutines
|
||||
private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
startPeriodicCleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate packet security (timestamp, replay attacks, duplicates)
|
||||
*/
|
||||
fun validatePacket(packet: BitchatPacket, peerID: String): Boolean {
|
||||
// Skip validation for our own packets
|
||||
if (peerID == myPeerID) {
|
||||
Log.d(TAG, "Skipping validation for our own packet")
|
||||
return false
|
||||
}
|
||||
|
||||
// TTL check
|
||||
if (packet.ttl == 0u.toUByte()) {
|
||||
Log.d(TAG, "Dropping packet with TTL 0")
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate packet payload
|
||||
if (packet.payload.isEmpty()) {
|
||||
Log.d(TAG, "Dropping packet with empty payload")
|
||||
return false
|
||||
}
|
||||
|
||||
// Replay attack protection (same 5-minute window as iOS)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val packetTime = packet.timestamp.toLong()
|
||||
val timeDiff = kotlin.math.abs(currentTime - packetTime)
|
||||
|
||||
if (timeDiff > MESSAGE_TIMEOUT) {
|
||||
Log.d(TAG, "Dropping old packet from $peerID, time diff: ${timeDiff/1000}s")
|
||||
return false
|
||||
}
|
||||
|
||||
// Duplicate detection
|
||||
val messageID = generateMessageID(packet, peerID)
|
||||
if (processedMessages.contains(messageID)) {
|
||||
Log.d(TAG, "Dropping duplicate packet: $messageID")
|
||||
return false
|
||||
}
|
||||
|
||||
// Add to processed messages
|
||||
processedMessages.add(messageID)
|
||||
messageTimestamps[messageID] = currentTime
|
||||
|
||||
Log.d(TAG, "Packet validation passed for $peerID, messageID: $messageID")
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key exchange packet
|
||||
*/
|
||||
suspend fun handleKeyExchange(packet: BitchatPacket, peerID: String): Boolean {
|
||||
if (peerID == myPeerID) return false
|
||||
|
||||
if (packet.payload.isEmpty()) {
|
||||
Log.w(TAG, "Key exchange packet has empty payload")
|
||||
return false
|
||||
}
|
||||
|
||||
// Prevent duplicate key exchange processing
|
||||
val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
|
||||
|
||||
if (processedKeyExchanges.contains(exchangeKey)) {
|
||||
Log.d(TAG, "Already processed key exchange: $exchangeKey")
|
||||
return false
|
||||
}
|
||||
|
||||
processedKeyExchanges.add(exchangeKey)
|
||||
|
||||
try {
|
||||
// Process the key exchange
|
||||
encryptionService.addPeerPublicKey(peerID, packet.payload)
|
||||
|
||||
Log.d(TAG, "Successfully processed key exchange from $peerID")
|
||||
|
||||
// Notify delegate
|
||||
delegate?.onKeyExchangeCompleted(peerID)
|
||||
|
||||
return true
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to process key exchange from $peerID: ${e.message}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify packet signature
|
||||
*/
|
||||
fun verifySignature(packet: BitchatPacket, peerID: String): Boolean {
|
||||
return packet.signature?.let { signature ->
|
||||
try {
|
||||
val isValid = encryptionService.verify(signature, packet.payload, peerID)
|
||||
if (!isValid) {
|
||||
Log.w(TAG, "Invalid signature for packet from $peerID")
|
||||
}
|
||||
isValid
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to verify signature from $peerID: ${e.message}")
|
||||
false
|
||||
}
|
||||
} ?: true // No signature means verification passes
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign packet payload
|
||||
*/
|
||||
fun signPacket(payload: ByteArray): ByteArray? {
|
||||
return try {
|
||||
encryptionService.sign(payload)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to sign packet: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt payload for specific peer
|
||||
*/
|
||||
fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? {
|
||||
return try {
|
||||
encryptionService.encrypt(data, recipientPeerID)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encrypt for $recipientPeerID: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt payload from specific peer
|
||||
*/
|
||||
fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? {
|
||||
return try {
|
||||
encryptionService.decrypt(encryptedData, senderPeerID)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to decrypt from $senderPeerID: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get combined public key data for key exchange
|
||||
*/
|
||||
fun getCombinedPublicKeyData(): ByteArray {
|
||||
return encryptionService.getCombinedPublicKeyData()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate message ID for duplicate detection
|
||||
*/
|
||||
private fun generateMessageID(packet: BitchatPacket, peerID: String): String {
|
||||
return when (MessageType.fromValue(packet.type)) {
|
||||
MessageType.FRAGMENT_START, MessageType.FRAGMENT_CONTINUE, MessageType.FRAGMENT_END -> {
|
||||
// For fragments, include the payload hash to distinguish different fragments
|
||||
"${packet.timestamp}-$peerID-${packet.type}-${packet.payload.contentHashCode()}"
|
||||
}
|
||||
else -> {
|
||||
// For other messages, use a truncated payload hash
|
||||
val payloadHash = packet.payload.sliceArray(0 until minOf(64, packet.payload.size)).contentHashCode()
|
||||
"${packet.timestamp}-$peerID-$payloadHash"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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-") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
*/
|
||||
fun getDebugInfo(): String {
|
||||
return buildString {
|
||||
appendLine("=== Security Manager Debug Info ===")
|
||||
appendLine("Processed Messages: ${processedMessages.size}")
|
||||
appendLine("Processed Key Exchanges: ${processedKeyExchanges.size}")
|
||||
appendLine("Message Timestamps: ${messageTimestamps.size}")
|
||||
|
||||
if (processedKeyExchanges.isNotEmpty()) {
|
||||
appendLine("Key Exchange History:")
|
||||
processedKeyExchanges.take(10).forEach { exchange ->
|
||||
appendLine(" - $exchange")
|
||||
}
|
||||
if (processedKeyExchanges.size > 10) {
|
||||
appendLine(" ... and ${processedKeyExchanges.size - 10} more")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic cleanup
|
||||
*/
|
||||
private fun startPeriodicCleanup() {
|
||||
managerScope.launch {
|
||||
while (isActive) {
|
||||
delay(CLEANUP_INTERVAL)
|
||||
cleanupOldData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old processed messages and timestamps
|
||||
*/
|
||||
private fun cleanupOldData() {
|
||||
val cutoffTime = System.currentTimeMillis() - MESSAGE_TIMEOUT
|
||||
var removedCount = 0
|
||||
|
||||
// Clean up old message timestamps and corresponding processed messages
|
||||
val messagesToRemove = messageTimestamps.entries.filter { (_, timestamp) ->
|
||||
timestamp < cutoffTime
|
||||
}.map { it.key }
|
||||
|
||||
messagesToRemove.forEach { messageId ->
|
||||
messageTimestamps.remove(messageId)
|
||||
if (processedMessages.remove(messageId)) {
|
||||
removedCount++
|
||||
}
|
||||
}
|
||||
|
||||
// Limit the size of processed messages set
|
||||
if (processedMessages.size > MAX_PROCESSED_MESSAGES) {
|
||||
val excess = processedMessages.size - MAX_PROCESSED_MESSAGES
|
||||
val toRemove = processedMessages.take(excess)
|
||||
processedMessages.removeAll(toRemove.toSet())
|
||||
removeFromMessageTimestamps(toRemove)
|
||||
removedCount += excess
|
||||
}
|
||||
|
||||
// Limit the size of processed key exchanges set
|
||||
if (processedKeyExchanges.size > MAX_PROCESSED_KEY_EXCHANGES) {
|
||||
val excess = processedKeyExchanges.size - MAX_PROCESSED_KEY_EXCHANGES
|
||||
val toRemove = processedKeyExchanges.take(excess)
|
||||
processedKeyExchanges.removeAll(toRemove.toSet())
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
Log.d(TAG, "Cleaned up $removedCount old processed messages")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to remove entries from messageTimestamps
|
||||
*/
|
||||
private fun removeFromMessageTimestamps(messageIds: List<String>) {
|
||||
messageIds.forEach { messageId ->
|
||||
messageTimestamps.remove(messageId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all security data
|
||||
*/
|
||||
fun clearAllData() {
|
||||
processedMessages.clear()
|
||||
processedKeyExchanges.clear()
|
||||
messageTimestamps.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the manager
|
||||
*/
|
||||
fun shutdown() {
|
||||
managerScope.cancel()
|
||||
clearAllData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate interface for security manager callbacks
|
||||
*/
|
||||
interface SecurityManagerDelegate {
|
||||
fun onKeyExchangeCompleted(peerID: String)
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Manages store-and-forward messaging for offline peers
|
||||
* Extracted from BluetoothMeshService for better separation of concerns
|
||||
*/
|
||||
class StoreForwardManager {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "StoreForwardManager"
|
||||
private const val MESSAGE_CACHE_TIMEOUT = 43200000L // 12 hours for regular peers
|
||||
private const val MAX_CACHED_MESSAGES = 100 // For regular peers
|
||||
private const val MAX_CACHED_MESSAGES_FAVORITES = 1000 // For favorites
|
||||
private const val CLEANUP_INTERVAL = 600000L // 10 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class for stored messages
|
||||
*/
|
||||
private data class StoredMessage(
|
||||
val packet: BitchatPacket,
|
||||
val timestamp: Long,
|
||||
val messageID: String,
|
||||
val isForFavorite: Boolean
|
||||
)
|
||||
|
||||
// Message storage
|
||||
private val messageCache = Collections.synchronizedList(mutableListOf<StoredMessage>())
|
||||
private val favoriteMessageQueue = ConcurrentHashMap<String, MutableList<StoredMessage>>()
|
||||
private val deliveredMessages = Collections.synchronizedSet(mutableSetOf<String>())
|
||||
private val cachedMessagesSentToPeer = Collections.synchronizedSet(mutableSetOf<String>())
|
||||
|
||||
// Delegate for callbacks
|
||||
var delegate: StoreForwardManagerDelegate? = null
|
||||
|
||||
// Coroutines
|
||||
private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
startPeriodicCleanup()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache message for offline delivery
|
||||
*/
|
||||
fun cacheMessage(packet: BitchatPacket, messageID: String) {
|
||||
// Skip certain message types (same as iOS)
|
||||
if (packet.type == MessageType.KEY_EXCHANGE.value ||
|
||||
packet.type == MessageType.ANNOUNCE.value ||
|
||||
packet.type == MessageType.LEAVE.value) {
|
||||
Log.d(TAG, "Skipping cache for message type: ${packet.type}")
|
||||
return
|
||||
}
|
||||
|
||||
// Don't cache broadcast messages
|
||||
if (packet.recipientID != null && packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) {
|
||||
Log.d(TAG, "Skipping cache for broadcast message")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine if this is for a favorite peer
|
||||
val recipientPeerID = packet.recipientID?.let { recipientID ->
|
||||
String(recipientID).replace("\u0000", "")
|
||||
}
|
||||
|
||||
if (recipientPeerID.isNullOrEmpty()) {
|
||||
Log.w(TAG, "Cannot cache message without valid recipient")
|
||||
return
|
||||
}
|
||||
|
||||
val isForFavorite = delegate?.isFavorite(recipientPeerID) ?: false
|
||||
|
||||
val storedMessage = StoredMessage(
|
||||
packet = packet,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
messageID = messageID,
|
||||
isForFavorite = isForFavorite
|
||||
)
|
||||
|
||||
if (isForFavorite) {
|
||||
// Store in favorite queue
|
||||
if (!favoriteMessageQueue.containsKey(recipientPeerID)) {
|
||||
favoriteMessageQueue[recipientPeerID] = mutableListOf()
|
||||
}
|
||||
favoriteMessageQueue[recipientPeerID]?.add(storedMessage)
|
||||
|
||||
// Limit favorite queue size
|
||||
if (favoriteMessageQueue[recipientPeerID]?.size ?: 0 > MAX_CACHED_MESSAGES_FAVORITES) {
|
||||
favoriteMessageQueue[recipientPeerID]?.removeAt(0)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Cached message for favorite peer $recipientPeerID (${favoriteMessageQueue[recipientPeerID]?.size} total)")
|
||||
|
||||
} else {
|
||||
// Store in regular cache
|
||||
cleanupMessageCache()
|
||||
|
||||
messageCache.add(storedMessage)
|
||||
|
||||
// Limit cache size
|
||||
if (messageCache.size > MAX_CACHED_MESSAGES) {
|
||||
messageCache.removeAt(0)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Cached message for peer $recipientPeerID (${messageCache.size} total in cache)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send cached messages to peer when they come online
|
||||
*/
|
||||
fun sendCachedMessages(peerID: String) {
|
||||
if (cachedMessagesSentToPeer.contains(peerID)) {
|
||||
Log.d(TAG, "Already sent cached messages to $peerID")
|
||||
return // Already sent cached messages to this peer
|
||||
}
|
||||
|
||||
cachedMessagesSentToPeer.add(peerID)
|
||||
|
||||
managerScope.launch {
|
||||
cleanupMessageCache()
|
||||
|
||||
val messagesToSend = mutableListOf<StoredMessage>()
|
||||
|
||||
// Check favorite queue
|
||||
favoriteMessageQueue[peerID]?.let { favoriteMessages ->
|
||||
val undeliveredFavorites = favoriteMessages.filter { !deliveredMessages.contains(it.messageID) }
|
||||
messagesToSend.addAll(undeliveredFavorites)
|
||||
favoriteMessageQueue.remove(peerID)
|
||||
Log.d(TAG, "Found ${undeliveredFavorites.size} cached favorite messages for $peerID")
|
||||
}
|
||||
|
||||
// Filter regular cached messages for this recipient
|
||||
val recipientMessages = messageCache.filter { storedMessage ->
|
||||
!deliveredMessages.contains(storedMessage.messageID) &&
|
||||
storedMessage.packet.recipientID?.let { recipientID ->
|
||||
String(recipientID).replace("\u0000", "") == peerID
|
||||
} == true
|
||||
}
|
||||
messagesToSend.addAll(recipientMessages)
|
||||
|
||||
if (recipientMessages.isNotEmpty()) {
|
||||
Log.d(TAG, "Found ${recipientMessages.size} cached regular messages for $peerID")
|
||||
}
|
||||
|
||||
// Sort by timestamp
|
||||
messagesToSend.sortBy { it.timestamp }
|
||||
|
||||
if (messagesToSend.isNotEmpty()) {
|
||||
Log.i(TAG, "Sending ${messagesToSend.size} cached messages to $peerID")
|
||||
}
|
||||
|
||||
// Mark as delivered
|
||||
val messageIDsToRemove = messagesToSend.map { it.messageID }
|
||||
deliveredMessages.addAll(messageIDsToRemove)
|
||||
|
||||
// Send with delays to avoid overwhelming the connection
|
||||
messagesToSend.forEachIndexed { index, storedMessage ->
|
||||
delay(index * 100L) // 100ms between messages
|
||||
delegate?.sendPacket(storedMessage.packet)
|
||||
}
|
||||
|
||||
// Remove sent messages from cache
|
||||
messageCache.removeAll { messageIDsToRemove.contains(it.messageID) }
|
||||
|
||||
if (messagesToSend.isNotEmpty()) {
|
||||
Log.d(TAG, "Finished sending ${messagesToSend.size} cached messages to $peerID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if message should be cached for peer
|
||||
*/
|
||||
fun shouldCacheForPeer(recipientPeerID: String): Boolean {
|
||||
// Check if recipient is offline and should cache for favorites
|
||||
val isOffline = !(delegate?.isPeerOnline(recipientPeerID) ?: false)
|
||||
val isRecipientFavorite = delegate?.isFavorite(recipientPeerID) ?: false
|
||||
|
||||
return isOffline && isRecipientFavorite
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark message as delivered
|
||||
*/
|
||||
fun markMessageAsDelivered(messageID: String) {
|
||||
deliveredMessages.add(messageID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached message count for peer
|
||||
*/
|
||||
fun getCachedMessageCount(peerID: String): Int {
|
||||
val favoriteCount = favoriteMessageQueue[peerID]?.size ?: 0
|
||||
val regularCount = messageCache.count { storedMessage ->
|
||||
storedMessage.packet.recipientID?.let { recipientID ->
|
||||
String(recipientID).replace("\u0000", "") == peerID
|
||||
} == true
|
||||
}
|
||||
return favoriteCount + regularCount
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
*/
|
||||
fun getDebugInfo(): String {
|
||||
return buildString {
|
||||
appendLine("=== Store-Forward Manager Debug Info ===")
|
||||
appendLine("Regular Cache: ${messageCache.size}/${MAX_CACHED_MESSAGES}")
|
||||
appendLine("Favorite Queues: ${favoriteMessageQueue.size}")
|
||||
|
||||
favoriteMessageQueue.forEach { (peerID, messages) ->
|
||||
appendLine(" - $peerID: ${messages.size} messages")
|
||||
}
|
||||
|
||||
appendLine("Delivered Messages: ${deliveredMessages.size}")
|
||||
appendLine("Peers Sent Cache: ${cachedMessagesSentToPeer.size}")
|
||||
|
||||
// Cache age analysis
|
||||
val now = System.currentTimeMillis()
|
||||
val regularCacheAges = messageCache.map { (now - it.timestamp) / 1000 }
|
||||
if (regularCacheAges.isNotEmpty()) {
|
||||
val avgAge = regularCacheAges.average().toInt()
|
||||
val maxAge = regularCacheAges.maxOrNull() ?: 0
|
||||
appendLine("Regular Cache Age: avg ${avgAge}s, max ${maxAge}s")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic cleanup
|
||||
*/
|
||||
private fun startPeriodicCleanup() {
|
||||
managerScope.launch {
|
||||
while (isActive) {
|
||||
delay(CLEANUP_INTERVAL)
|
||||
cleanupMessageCache()
|
||||
cleanupDeliveredMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old cached messages (not for favorites)
|
||||
*/
|
||||
private fun cleanupMessageCache() {
|
||||
val cutoffTime = System.currentTimeMillis() - MESSAGE_CACHE_TIMEOUT
|
||||
val sizeBefore = messageCache.size
|
||||
val removed = messageCache.removeAll { !it.isForFavorite && it.timestamp < cutoffTime }
|
||||
|
||||
if (removed) {
|
||||
val removedCount = sizeBefore - messageCache.size
|
||||
Log.d(TAG, "Cleaned up $removedCount old cached messages")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up delivered messages set (prevent memory leak)
|
||||
*/
|
||||
private fun cleanupDeliveredMessages() {
|
||||
if (deliveredMessages.size > 1000) {
|
||||
Log.d(TAG, "Clearing delivered messages set (${deliveredMessages.size} entries)")
|
||||
deliveredMessages.clear()
|
||||
}
|
||||
|
||||
if (cachedMessagesSentToPeer.size > 200) {
|
||||
Log.d(TAG, "Clearing cached messages sent tracking (${cachedMessagesSentToPeer.size} entries)")
|
||||
cachedMessagesSentToPeer.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached data
|
||||
*/
|
||||
fun clearAllCache() {
|
||||
messageCache.clear()
|
||||
favoriteMessageQueue.clear()
|
||||
deliveredMessages.clear()
|
||||
cachedMessagesSentToPeer.clear()
|
||||
Log.d(TAG, "Cleared all cached message data")
|
||||
}
|
||||
|
||||
/**
|
||||
* Force cleanup for testing
|
||||
*/
|
||||
fun forceCleanup() {
|
||||
cleanupMessageCache()
|
||||
cleanupDeliveredMessages()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the manager
|
||||
*/
|
||||
fun shutdown() {
|
||||
managerScope.cancel()
|
||||
clearAllCache()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate interface for store-forward manager callbacks
|
||||
*/
|
||||
interface StoreForwardManagerDelegate {
|
||||
fun isFavorite(peerID: String): Boolean
|
||||
fun isPeerOnline(peerID: String): Boolean
|
||||
fun sendPacket(packet: BitchatPacket)
|
||||
}
|
||||
Reference in New Issue
Block a user