seems to work

This commit is contained in:
callebtc
2025-07-08 23:38:35 +02:00
parent d40e2cbda8
commit 9ba8dca82a
10 changed files with 1957 additions and 0 deletions
@@ -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)
}