mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 16:25:22 +00:00
Changes bitchat protocol (#265)
* create payload * compiles and can send messages * identityannouncement * DMs work, read receipt not sent yet * works * delete old code * simplify * working * fragment wip * compression wip * use zlib compression * clean * nice * mesh * remove comments
This commit is contained in:
@@ -3,13 +3,10 @@ package com.bitchat.android.mesh
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.protocol.MessagePadding
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.HandshakeRequest
|
||||
import com.bitchat.android.protocol.MessagePadding
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.model.DeliveryAck
|
||||
import com.bitchat.android.model.ReadReceipt
|
||||
import com.bitchat.android.model.NoiseIdentityAnnouncement
|
||||
import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
@@ -64,7 +61,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
init {
|
||||
setupDelegates()
|
||||
messageHandler.packetProcessor = packetProcessor
|
||||
startPeriodicDebugLogging()
|
||||
//startPeriodicDebugLogging()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +92,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
try {
|
||||
delay(30000) // 30 seconds
|
||||
sendBroadcastAnnounce()
|
||||
broadcastNoiseIdentityAnnouncement()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in periodic broadcast announce: ${e.message}")
|
||||
}
|
||||
@@ -131,7 +127,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Send Noise handshake response
|
||||
val responsePacket = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE_RESP.value,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
@@ -224,18 +220,18 @@ class BluetoothMeshService(private val context: Context) {
|
||||
try {
|
||||
// Initiate proper Noise handshake with specific peer
|
||||
val handshakeData = encryptionService.initiateHandshake(peerID)
|
||||
|
||||
|
||||
if (handshakeData != null) {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE_INIT.value,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = handshakeData,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
|
||||
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)")
|
||||
} else {
|
||||
@@ -247,6 +243,15 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? {
|
||||
return try {
|
||||
encryptionService.processHandshakeMessage(payload, peerID)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to process handshake message from $peerID: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePeerIDBinding(newPeerID: String, nickname: String,
|
||||
publicKey: ByteArray, previousPeerID: String?) {
|
||||
|
||||
@@ -270,10 +275,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
return delegate?.decryptChannelMessage(encryptedContent, channel)
|
||||
}
|
||||
|
||||
override fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) {
|
||||
this@BluetoothMeshService.sendDeliveryAck(message, senderPeerID)
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
override fun onMessageReceived(message: BitchatMessage) {
|
||||
delegate?.didReceiveMessage(message)
|
||||
@@ -283,12 +284,12 @@ class BluetoothMeshService(private val context: Context) {
|
||||
delegate?.didReceiveChannelLeave(channel, fromPeer)
|
||||
}
|
||||
|
||||
override fun onDeliveryAckReceived(ack: DeliveryAck) {
|
||||
delegate?.didReceiveDeliveryAck(ack)
|
||||
override fun onDeliveryAckReceived(messageID: String, peerID: String) {
|
||||
delegate?.didReceiveDeliveryAck(messageID, peerID)
|
||||
}
|
||||
|
||||
override fun onReadReceiptReceived(receipt: ReadReceipt) {
|
||||
delegate?.didReceiveReadReceipt(receipt)
|
||||
override fun onReadReceiptReceived(messageID: String, peerID: String) {
|
||||
delegate?.didReceiveReadReceipt(messageID, peerID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,18 +316,14 @@ class BluetoothMeshService(private val context: Context) {
|
||||
return SpecialRecipients.BROADCAST
|
||||
}
|
||||
|
||||
override fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean {
|
||||
return runBlocking { securityManager.handleNoiseHandshake(routed, step) }
|
||||
override fun handleNoiseHandshake(routed: RoutedPacket): Boolean {
|
||||
return runBlocking { securityManager.handleNoiseHandshake(routed) }
|
||||
}
|
||||
|
||||
override fun handleNoiseEncrypted(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleNoiseEncrypted(routed) }
|
||||
}
|
||||
|
||||
override fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleNoiseIdentityAnnouncement(routed) }
|
||||
}
|
||||
|
||||
override fun handleAnnounce(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleAnnounce(routed) }
|
||||
}
|
||||
@@ -343,14 +340,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
return fragmentManager.handleFragment(packet)
|
||||
}
|
||||
|
||||
// override fun handleDeliveryAck(routed: RoutedPacket) {
|
||||
// serviceScope.launch { messageHandler.handleDeliveryAck(routed) }
|
||||
// }
|
||||
|
||||
override fun handleReadReceipt(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleReadReceipt(routed) }
|
||||
}
|
||||
|
||||
override fun sendAnnouncementToPeer(peerID: String) {
|
||||
this@BluetoothMeshService.sendAnnouncementToPeer(peerID)
|
||||
}
|
||||
@@ -376,11 +365,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
delay(200)
|
||||
sendBroadcastAnnounce()
|
||||
}
|
||||
// Send key exchange to newly connected device
|
||||
serviceScope.launch {
|
||||
delay(400) // Ensure connection is stable
|
||||
broadcastNoiseIdentityAnnouncement()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRSSIUpdated(deviceAddress: String, rssi: Int) {
|
||||
@@ -453,371 +437,199 @@ class BluetoothMeshService(private val context: Context) {
|
||||
if (content.isEmpty()) return
|
||||
|
||||
serviceScope.launch {
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
|
||||
val message = BitchatMessage(
|
||||
sender = nickname,
|
||||
content = content,
|
||||
timestamp = Date(),
|
||||
isRelay = false,
|
||||
senderPeerID = myPeerID,
|
||||
mentions = if (mentions.isNotEmpty()) mentions else null,
|
||||
channel = channel
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.MESSAGE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = SpecialRecipients.BROADCAST,
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = content.toByteArray(Charsets.UTF_8),
|
||||
signature = null,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
|
||||
message.toBinaryPayload()?.let { messageData ->
|
||||
// Sign the message: TODO: NOT SIGNED
|
||||
// val signature = securityManager.signPacket(messageData)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.MESSAGE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = SpecialRecipients.BROADCAST,
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = messageData,
|
||||
signature = null,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
|
||||
// Send with random delay and retry for reliability
|
||||
// delay(Random.nextLong(50, 500))
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
}
|
||||
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send private message
|
||||
* Send private message - SIMPLIFIED iOS-compatible version
|
||||
* Uses NoisePayloadType system exactly like iOS SimplifiedBluetoothService
|
||||
*/
|
||||
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) {
|
||||
if (content.isEmpty() || recipientPeerID.isEmpty() || recipientNickname.isEmpty()) return
|
||||
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
|
||||
val message = BitchatMessage(
|
||||
id = messageID ?: UUID.randomUUID().toString(),
|
||||
sender = nickname,
|
||||
content = content,
|
||||
timestamp = Date(),
|
||||
isRelay = false,
|
||||
isPrivate = true,
|
||||
recipientNickname = recipientNickname,
|
||||
senderPeerID = myPeerID
|
||||
)
|
||||
|
||||
message.toBinaryPayload()?.let { messageData ->
|
||||
try {
|
||||
|
||||
// Create inner packet with the padded message data
|
||||
val innerPacket = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = messageData,
|
||||
signature = null,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
|
||||
// Cache for offline favorites
|
||||
if (storeForwardManager.shouldCacheForPeer(recipientPeerID)) {
|
||||
storeForwardManager.cacheMessage(innerPacket, messageID ?: message.id)
|
||||
}
|
||||
|
||||
// Use the new encrypt and broadcast function
|
||||
encryptAndBroadcastNoisePacket(innerPacket, recipientPeerID)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send private message: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send delivery acknowledgment for a received private message
|
||||
*/
|
||||
fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) {
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
val ack = DeliveryAck(
|
||||
originalMessageID = message.id,
|
||||
recipientID = myPeerID,
|
||||
recipientNickname = nickname,
|
||||
hopCount = 0u.toUByte() // Will be calculated during relay
|
||||
)
|
||||
|
||||
try {
|
||||
val ackData = ack.encode() ?: return
|
||||
val typeMarker = MessageType.DELIVERY_ACK.value.toByte()
|
||||
val payloadWithMarker = byteArrayOf(typeMarker) + ackData
|
||||
val encryptedPayload = securityManager.encryptForPeer(payloadWithMarker, senderPeerID)
|
||||
|
||||
if (encryptedPayload == null) {
|
||||
Log.w(TAG, "Failed to encrypt delivery ACK for $senderPeerID")
|
||||
return
|
||||
}
|
||||
|
||||
// Create inner packet with the delivery ACK data
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(senderPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encryptedPayload,
|
||||
signature = null,
|
||||
ttl = 3u
|
||||
)
|
||||
|
||||
// Use the new encrypt and broadcast function
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send delivery ACK: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send read receipt for a received private message
|
||||
*/
|
||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||
serviceScope.launch {
|
||||
// Create the read receipt
|
||||
val receipt = ReadReceipt(
|
||||
originalMessageID = messageID,
|
||||
readerID = myPeerID,
|
||||
readerNickname = readerNickname
|
||||
)
|
||||
|
||||
try {
|
||||
// Encode the receipt
|
||||
val receiptData = receipt.encode()
|
||||
val typeMarker = MessageType.READ_RECEIPT.value.toByte()
|
||||
val payloadWithMarker = byteArrayOf(typeMarker) + receiptData
|
||||
val encryptedPayload = securityManager.encryptForPeer(payloadWithMarker, recipientPeerID)
|
||||
|
||||
if (encryptedPayload == null) {
|
||||
Log.w(TAG, "Failed to encrypt delivery ACK for $recipientPeerID")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Create inner packet with the delivery ACK data
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encryptedPayload,
|
||||
signature = null,
|
||||
ttl = 3u
|
||||
)
|
||||
|
||||
Log.d(TAG, "Sending read receipt for message $messageID to $recipientPeerID")
|
||||
|
||||
// Use the new encrypt and broadcast function
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send read receipt for message $messageID: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a BitchatPacket and broadcast it as a NOISE_ENCRYPTED message
|
||||
* This is the correct protocol implementation - encrypt the entire packet, not just the payload
|
||||
*/
|
||||
private fun encryptAndBroadcastNoisePacket(innerPacket: BitchatPacket, recipientPeerID: String) {
|
||||
serviceScope.launch {
|
||||
try {
|
||||
// Serialize the inner packet to binary data
|
||||
val innerPacketData = innerPacket.toBinaryData()
|
||||
if (innerPacketData == null) {
|
||||
Log.e(TAG, "Failed to serialize inner packet for encryption")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Encrypt the serialized packet using Noise encryption
|
||||
val encryptedPayload = securityManager.encryptForPeer(innerPacketData, recipientPeerID)
|
||||
|
||||
if (encryptedPayload != null) {
|
||||
// Create the outer NOISE_ENCRYPTED packet
|
||||
val outerPacket = BitchatPacket(
|
||||
val finalMessageID = messageID ?: java.util.UUID.randomUUID().toString()
|
||||
|
||||
Log.d(TAG, "📨 Sending PM to $recipientPeerID: ${content.take(30)}...")
|
||||
|
||||
// Check if we have an established Noise session
|
||||
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
|
||||
try {
|
||||
// Create TLV-encoded private message exactly like iOS
|
||||
val privateMessage = com.bitchat.android.model.PrivateMessagePacket(
|
||||
messageID = finalMessageID,
|
||||
content = content
|
||||
)
|
||||
|
||||
val tlvData = privateMessage.encode()
|
||||
if (tlvData == null) {
|
||||
Log.e(TAG, "Failed to encode private message with TLV")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Create message payload with NoisePayloadType prefix: [type byte] + [TLV data]
|
||||
val messagePayload = com.bitchat.android.model.NoisePayload(
|
||||
type = com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE,
|
||||
data = tlvData
|
||||
)
|
||||
|
||||
// Encrypt the payload
|
||||
val encrypted = encryptionService.encrypt(messagePayload.encode(), recipientPeerID)
|
||||
|
||||
// Create NOISE_ENCRYPTED packet exactly like iOS
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encryptedPayload,
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
|
||||
// Broadcast the encrypted packet
|
||||
connectionManager.broadcastPacket(RoutedPacket(outerPacket))
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
Log.d(TAG, "📤 Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)")
|
||||
|
||||
Log.d(TAG, "Encrypted and sent packet type ${innerPacket.type} to $recipientPeerID (${encryptedPayload.size} bytes encrypted)")
|
||||
} else {
|
||||
Log.w(TAG, "Failed to encrypt packet for $recipientPeerID - no session available")
|
||||
// FIXED: Don't send didReceiveMessage for our own sent messages
|
||||
// This was causing self-notifications - iOS doesn't do this
|
||||
// The UI handles showing sent messages through its own message sending logic
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encrypt private message for $recipientPeerID: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
// Fire and forget - initiate handshake but don't queue exactly like iOS
|
||||
Log.d(TAG, "🤝 No session with $recipientPeerID, initiating handshake")
|
||||
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encrypt and broadcast Noise packet to $recipientPeerID: ${e.message}")
|
||||
// FIXED: Don't send didReceiveMessage for our own sent messages
|
||||
// The UI will handle showing the message in the chat interface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send broadcast announce
|
||||
* Send read receipt for a received private message - NEW NoisePayloadType implementation
|
||||
* Uses same encryption approach as iOS SimplifiedBluetoothService
|
||||
*/
|
||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||
serviceScope.launch {
|
||||
Log.d(TAG, "📖 Sending read receipt for message $messageID to $recipientPeerID")
|
||||
|
||||
try {
|
||||
// Create read receipt payload using NoisePayloadType exactly like iOS
|
||||
val readReceiptPayload = com.bitchat.android.model.NoisePayload(
|
||||
type = com.bitchat.android.model.NoisePayloadType.READ_RECEIPT,
|
||||
data = messageID.toByteArray(Charsets.UTF_8)
|
||||
)
|
||||
|
||||
// Encrypt the payload
|
||||
val encrypted = encryptionService.encrypt(readReceiptPayload.encode(), recipientPeerID)
|
||||
|
||||
// Create NOISE_ENCRYPTED packet exactly like iOS
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
ttl = 7u // Same TTL as iOS messageTTL
|
||||
)
|
||||
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
Log.d(TAG, "📤 Sent read receipt to $recipientPeerID for message $messageID")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send read receipt to $recipientPeerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
|
||||
*/
|
||||
fun sendBroadcastAnnounce() {
|
||||
Log.d(TAG, "Sending broadcast announce")
|
||||
serviceScope.launch {
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
|
||||
// Get the static public key for the announcement
|
||||
val staticKey = encryptionService.getStaticPublicKey()
|
||||
if (staticKey == null) {
|
||||
Log.e(TAG, "No static public key available for announcement")
|
||||
return@launch
|
||||
}
|
||||
|
||||
// Create iOS-compatible IdentityAnnouncement with TLV encoding
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey)
|
||||
val tlvPayload = announcement.encode()
|
||||
if (tlvPayload == null) {
|
||||
Log.e(TAG, "Failed to encode announcement as TLV")
|
||||
return@launch
|
||||
}
|
||||
|
||||
val announcePacket = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = nickname.toByteArray()
|
||||
payload = tlvPayload
|
||||
)
|
||||
|
||||
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
||||
Log.d(TAG, "Sent iOS-compatible TLV announce (${tlvPayload.size} bytes)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send announcement to specific peer
|
||||
* Send announcement to specific peer with TLV-encoded identity announcement - exactly like iOS
|
||||
*/
|
||||
private fun sendAnnouncementToPeer(peerID: String) {
|
||||
fun sendAnnouncementToPeer(peerID: String) {
|
||||
if (peerManager.hasAnnouncedToPeer(peerID)) return
|
||||
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
|
||||
// Get the static public key for the announcement
|
||||
val staticKey = encryptionService.getStaticPublicKey()
|
||||
if (staticKey == null) {
|
||||
Log.e(TAG, "No static public key available for peer announcement")
|
||||
return
|
||||
}
|
||||
|
||||
// Create iOS-compatible IdentityAnnouncement with TLV encoding
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey)
|
||||
val tlvPayload = announcement.encode()
|
||||
if (tlvPayload == null) {
|
||||
Log.e(TAG, "Failed to encode peer announcement as TLV")
|
||||
return
|
||||
}
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = nickname.toByteArray()
|
||||
payload = tlvPayload
|
||||
)
|
||||
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
peerManager.markPeerAsAnnouncedTo(peerID)
|
||||
Log.d(TAG, "Sent iOS-compatible TLV peer announce to $peerID (${tlvPayload.size} bytes)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Send key exchange to newly connected device
|
||||
*/
|
||||
fun broadcastNoiseIdentityAnnouncement() {
|
||||
serviceScope.launch {
|
||||
try {
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
|
||||
// Create the identity announcement using proper binary format
|
||||
val announcement = createNoiseIdentityAnnouncement(nickname, null)
|
||||
if (announcement != null) {
|
||||
val announcementData = announcement.toBinaryData()
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.NOISE_IDENTITY_ANNOUNCE.value,
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = announcementData,
|
||||
)
|
||||
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
Log.d(TAG, "Sent NoiseIdentityAnnouncement (${announcementData.size} bytes)")
|
||||
} else {
|
||||
Log.e(TAG, "Failed to create NoiseIdentityAnnouncement")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send NoiseIdentityAnnouncement: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send handshake request to target peer for pending messages
|
||||
*/
|
||||
fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) {
|
||||
serviceScope.launch {
|
||||
try {
|
||||
// Create handshake request
|
||||
val request = HandshakeRequest(
|
||||
requesterID = myPeerID,
|
||||
requesterNickname = delegate?.getNickname() ?: myPeerID,
|
||||
targetID = targetPeerID,
|
||||
pendingMessageCount = pendingCount
|
||||
)
|
||||
|
||||
val requestData = request.toBinaryData()
|
||||
|
||||
// Create packet for handshake request
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.HANDSHAKE_REQUEST.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(targetPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = requestData,
|
||||
ttl = 6u
|
||||
)
|
||||
|
||||
// Broadcast the packet (Android equivalent of both direct and relay attempts)
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
Log.d(TAG, "Sent handshake request to $targetPeerID (pending: $pendingCount, ${requestData.size} bytes)")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send handshake request to $targetPeerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a properly formatted NoiseIdentityAnnouncement exactly like iOS
|
||||
*/
|
||||
private fun createNoiseIdentityAnnouncement(nickname: String, previousPeerID: String?): NoiseIdentityAnnouncement? {
|
||||
return try {
|
||||
// Get the static public key for Noise protocol
|
||||
val staticKey = encryptionService.getStaticPublicKey()
|
||||
if (staticKey == null) {
|
||||
Log.e(TAG, "No static public key available for identity announcement")
|
||||
return null
|
||||
}
|
||||
|
||||
// Get the signing public key for Ed25519 signatures
|
||||
val signingKey = encryptionService.getSigningPublicKey()
|
||||
if (signingKey == null) {
|
||||
Log.e(TAG, "No signing public key available for identity announcement")
|
||||
return null
|
||||
}
|
||||
|
||||
val now = Date()
|
||||
|
||||
// Create the binding data to sign (same format as iOS)
|
||||
val timestampMs = now.time
|
||||
val bindingData = myPeerID.toByteArray(Charsets.UTF_8) +
|
||||
staticKey +
|
||||
timestampMs.toString().toByteArray(Charsets.UTF_8)
|
||||
|
||||
// Sign the binding with our Ed25519 signing key
|
||||
val signature = encryptionService.signData(bindingData) ?: ByteArray(0)
|
||||
|
||||
// Create the identity announcement
|
||||
NoiseIdentityAnnouncement(
|
||||
peerID = myPeerID,
|
||||
publicKey = staticKey,
|
||||
signingPublicKey = signingKey,
|
||||
nickname = nickname,
|
||||
timestamp = now,
|
||||
previousPeerID = previousPeerID,
|
||||
signature = signature
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to create NoiseIdentityAnnouncement: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send leave announcement
|
||||
*/
|
||||
@@ -1014,8 +826,8 @@ interface BluetoothMeshDelegate {
|
||||
fun didReceiveMessage(message: BitchatMessage)
|
||||
fun didUpdatePeerList(peers: List<String>)
|
||||
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
||||
fun didReceiveDeliveryAck(ack: DeliveryAck)
|
||||
fun didReceiveReadReceipt(receipt: ReadReceipt)
|
||||
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
||||
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
||||
fun getNickname(): String?
|
||||
fun isFavorite(peerID: String): Boolean
|
||||
|
||||
@@ -3,25 +3,33 @@ 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.model.FragmentPayload
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Manages message fragmentation and reassembly
|
||||
* Extracted from BluetoothMeshService for better separation of concerns
|
||||
* Manages message fragmentation and reassembly - 100% iOS Compatible
|
||||
*
|
||||
* This implementation exactly matches iOS SimplifiedBluetoothService fragmentation:
|
||||
* - Same fragment payload structure (13-byte header + data)
|
||||
* - Same MTU thresholds and fragment sizes
|
||||
* - Same reassembly logic and timeout handling
|
||||
* - Uses new FragmentPayload model for type safety
|
||||
*/
|
||||
class FragmentManager {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FragmentManager"
|
||||
private const val FRAGMENT_SIZE_THRESHOLD = 512 // 512 bytes
|
||||
private const val MAX_FRAGMENT_SIZE = 500 // Match iOS/Rust for BLE compatibility (185 byte MTU limit)
|
||||
private const val FRAGMENT_TIMEOUT = 30000L // 30 seconds
|
||||
private const val CLEANUP_INTERVAL = 10000L // 10 seconds
|
||||
// iOS values: 512 MTU threshold, 469 max fragment size (512 MTU - headers)
|
||||
private const val FRAGMENT_SIZE_THRESHOLD = 512 // Matches iOS: if data.count > 512
|
||||
private const val MAX_FRAGMENT_SIZE = 469 // Matches iOS: maxFragmentSize = 469
|
||||
private const val FRAGMENT_TIMEOUT = 30000L // Matches iOS: 30 seconds cleanup
|
||||
private const val CLEANUP_INTERVAL = 10000L // 10 seconds cleanup check
|
||||
}
|
||||
|
||||
// Fragment storage
|
||||
// Fragment storage - iOS equivalent: incomingFragments: [String: [Int: Data]]
|
||||
private val incomingFragments = ConcurrentHashMap<String, MutableMap<Int, ByteArray>>()
|
||||
// iOS equivalent: fragmentMetadata: [String: (type: UInt8, total: Int, timestamp: Date)]
|
||||
private val fragmentMetadata = ConcurrentHashMap<String, Triple<UByte, Int, Long>>() // originalType, totalFragments, timestamp
|
||||
|
||||
// Delegate for callbacks
|
||||
@@ -35,51 +43,52 @@ class FragmentManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fragments from a large packet
|
||||
* Create fragments from a large packet - 100% iOS Compatible
|
||||
* Matches iOS sendFragmentedPacket() implementation exactly
|
||||
*/
|
||||
fun createFragments(packet: BitchatPacket): List<BitchatPacket> {
|
||||
val data = packet.toBinaryData() ?: return emptyList()
|
||||
val fullData = packet.toBinaryData() ?: return emptyList()
|
||||
|
||||
if (data.size <= FRAGMENT_SIZE_THRESHOLD) {
|
||||
// iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue
|
||||
if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
|
||||
return listOf(packet) // No fragmentation needed
|
||||
}
|
||||
|
||||
val fragments = mutableListOf<BitchatPacket>()
|
||||
val fragmentID = generateFragmentID()
|
||||
|
||||
// Fragment overhead: 13 bytes (fragment metadata) + 21 bytes (packet header) = 34 bytes total
|
||||
// With 150 byte fragments, total packet = ~184 bytes (within iOS 185 byte MTU)
|
||||
val totalFragments = (data.size + MAX_FRAGMENT_SIZE - 1) / MAX_FRAGMENT_SIZE
|
||||
// iOS: let fragmentID = Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
||||
val fragmentID = FragmentPayload.generateFragmentID()
|
||||
|
||||
Log.d(TAG, "Creating ${totalFragments} fragments for ${data.size} byte packet")
|
||||
// iOS: stride(from: 0, to: fullData.count, by: maxFragmentSize)
|
||||
val fragmentChunks = stride(0, fullData.size, MAX_FRAGMENT_SIZE) { offset ->
|
||||
val endOffset = minOf(offset + MAX_FRAGMENT_SIZE, fullData.size)
|
||||
fullData.sliceArray(offset..<endOffset)
|
||||
}
|
||||
|
||||
for (i in 0 until totalFragments) {
|
||||
val start = i * MAX_FRAGMENT_SIZE
|
||||
val end = minOf(start + MAX_FRAGMENT_SIZE, data.size)
|
||||
val fragmentData = data.sliceArray(start until end)
|
||||
Log.d(TAG, "Creating ${fragmentChunks.size} fragments for ${fullData.size} byte packet (iOS compatible)")
|
||||
|
||||
// iOS: for (index, fragment) in fragments.enumerated()
|
||||
for (index in fragmentChunks.indices) {
|
||||
val fragmentData = fragmentChunks[index]
|
||||
|
||||
val fragmentPayload = createFragmentPayload(
|
||||
// Create iOS-compatible fragment payload
|
||||
val fragmentPayload = FragmentPayload(
|
||||
fragmentID = fragmentID,
|
||||
index = i,
|
||||
total = totalFragments,
|
||||
index = index,
|
||||
total = fragmentChunks.size,
|
||||
originalType = packet.type,
|
||||
data = fragmentData
|
||||
)
|
||||
|
||||
val fragmentType = when (i) {
|
||||
0 -> MessageType.FRAGMENT_START
|
||||
totalFragments - 1 -> MessageType.FRAGMENT_END
|
||||
else -> MessageType.FRAGMENT_CONTINUE
|
||||
}
|
||||
|
||||
// iOS: MessageType.fragment.rawValue (single fragment type)
|
||||
val fragmentPacket = BitchatPacket(
|
||||
type = fragmentType.value,
|
||||
type = MessageType.FRAGMENT.value,
|
||||
ttl = packet.ttl,
|
||||
senderID = packet.senderID,
|
||||
recipientID = packet.recipientID,
|
||||
timestamp = packet.timestamp,
|
||||
payload = fragmentPayload,
|
||||
signature = null // Fragments aren't individually signed
|
||||
payload = fragmentPayload.encode(),
|
||||
signature = null // iOS: signature: nil
|
||||
)
|
||||
|
||||
fragments.add(fragmentPacket)
|
||||
@@ -89,62 +98,82 @@ class FragmentManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming fragment
|
||||
* Handle incoming fragment - 100% iOS Compatible
|
||||
* Matches iOS handleFragment() implementation exactly
|
||||
*/
|
||||
fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
||||
if (packet.payload.size < 13) {
|
||||
// iOS: guard packet.payload.count > 13 else { return }
|
||||
if (packet.payload.size < FragmentPayload.HEADER_SIZE) {
|
||||
Log.w(TAG, "Fragment packet too small: ${packet.payload.size}")
|
||||
return null
|
||||
}
|
||||
|
||||
// Don't process our own fragments - iOS equivalent check
|
||||
// This would be done at a higher level but we'll include for safety
|
||||
|
||||
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())
|
||||
// Use FragmentPayload for type-safe decoding
|
||||
val fragmentPayload = FragmentPayload.decode(packet.payload)
|
||||
if (fragmentPayload == null || !fragmentPayload.isValid()) {
|
||||
Log.w(TAG, "Invalid fragment payload")
|
||||
return null
|
||||
}
|
||||
|
||||
incomingFragments[fragmentID]?.put(index, fragmentData)
|
||||
// iOS: let fragmentID = packet.payload[0..<8].map { String(format: "%02x", $0) }.joined()
|
||||
val fragmentIDString = fragmentPayload.getFragmentIDString()
|
||||
|
||||
// Check if we have all fragments
|
||||
if (incomingFragments[fragmentID]?.size == total) {
|
||||
Log.d(TAG, "All fragments received for $fragmentID, reassembling...")
|
||||
Log.d(TAG, "Received fragment ${fragmentPayload.index}/${fragmentPayload.total} for fragmentID: $fragmentIDString, originalType: ${fragmentPayload.originalType}")
|
||||
|
||||
// iOS: if incomingFragments[fragmentID] == nil
|
||||
if (!incomingFragments.containsKey(fragmentIDString)) {
|
||||
incomingFragments[fragmentIDString] = mutableMapOf()
|
||||
fragmentMetadata[fragmentIDString] = Triple(
|
||||
fragmentPayload.originalType,
|
||||
fragmentPayload.total,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
// iOS: incomingFragments[fragmentID]?[index] = Data(fragmentData)
|
||||
incomingFragments[fragmentIDString]?.put(fragmentPayload.index, fragmentPayload.data)
|
||||
|
||||
// iOS: if let fragments = incomingFragments[fragmentID], fragments.count == total
|
||||
val fragmentMap = incomingFragments[fragmentIDString]
|
||||
if (fragmentMap != null && fragmentMap.size == fragmentPayload.total) {
|
||||
Log.d(TAG, "All fragments received for $fragmentIDString, reassembling...")
|
||||
|
||||
// Reassemble message
|
||||
// iOS reassembly logic: for i in 0..<total { if let fragment = fragments[i] { reassembled.append(fragment) } }
|
||||
val reassembledData = mutableListOf<Byte>()
|
||||
for (i in 0 until total) {
|
||||
incomingFragments[fragmentID]?.get(i)?.let { data ->
|
||||
for (i in 0 until fragmentPayload.total) {
|
||||
fragmentMap[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) {
|
||||
// iOS: if let metadata = fragmentMetadata[fragmentID]
|
||||
val metadata = fragmentMetadata[fragmentIDString]
|
||||
if (metadata != null) {
|
||||
// iOS: let reassembledPacket = BitchatPacket(type: metadata.type, ...)
|
||||
val reassembledPacket = BitchatPacket(
|
||||
type = metadata.first,
|
||||
senderID = packet.senderID,
|
||||
recipientID = packet.recipientID,
|
||||
timestamp = packet.timestamp,
|
||||
payload = reassembledData.toByteArray(),
|
||||
signature = packet.signature,
|
||||
ttl = if (packet.ttl > 0u) (packet.ttl - 1u).toUByte() else 0u
|
||||
)
|
||||
|
||||
// iOS cleanup: incomingFragments.removeValue(forKey: fragmentID)
|
||||
incomingFragments.remove(fragmentIDString)
|
||||
fragmentMetadata.remove(fragmentIDString)
|
||||
|
||||
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")
|
||||
val received = fragmentMap?.size ?: 0
|
||||
Log.d(TAG, "Fragment ${fragmentPayload.index} stored, have $received/${fragmentPayload.total} fragments for $fragmentIDString")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -155,53 +184,51 @@ class FragmentManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fragment payload with metadata
|
||||
* Helper function to match iOS stride functionality
|
||||
* stride(from: 0, to: fullData.count, by: maxFragmentSize)
|
||||
*/
|
||||
private fun createFragmentPayload(
|
||||
fragmentID: ByteArray,
|
||||
index: Int,
|
||||
total: Int,
|
||||
originalType: UByte,
|
||||
data: ByteArray
|
||||
): ByteArray {
|
||||
val payload = ByteArray(13 + data.size)
|
||||
|
||||
// Fragment ID (8 bytes)
|
||||
System.arraycopy(fragmentID, 0, payload, 0, 8)
|
||||
|
||||
// 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
|
||||
private fun <T> stride(from: Int, to: Int, by: Int, transform: (Int) -> T): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
var current = from
|
||||
while (current < to) {
|
||||
result.add(transform(current))
|
||||
current += by
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique fragment ID (8 random bytes to match iOS/Rust)
|
||||
* iOS cleanup - exactly matching performCleanup() implementation
|
||||
* Clean old fragments (> 30 seconds old)
|
||||
*/
|
||||
private fun generateFragmentID(): ByteArray {
|
||||
val fragmentID = ByteArray(8)
|
||||
kotlin.random.Random.nextBytes(fragmentID)
|
||||
return fragmentID
|
||||
private fun cleanupOldFragments() {
|
||||
val now = System.currentTimeMillis()
|
||||
val cutoff = now - FRAGMENT_TIMEOUT
|
||||
|
||||
// iOS: let oldFragments = fragmentMetadata.filter { $0.value.timestamp < cutoff }.map { $0.key }
|
||||
val oldFragments = fragmentMetadata.filter { it.value.third < cutoff }.map { it.key }
|
||||
|
||||
// iOS: for fragmentID in oldFragments { incomingFragments.removeValue(forKey: fragmentID) }
|
||||
for (fragmentID in oldFragments) {
|
||||
incomingFragments.remove(fragmentID)
|
||||
fragmentMetadata.remove(fragmentID)
|
||||
}
|
||||
|
||||
if (oldFragments.isNotEmpty()) {
|
||||
Log.d(TAG, "Cleaned up ${oldFragments.size} old fragment sets (iOS compatible)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
* Get debug information - matches iOS debugging
|
||||
*/
|
||||
fun getDebugInfo(): String {
|
||||
return buildString {
|
||||
appendLine("=== Fragment Manager Debug Info ===")
|
||||
appendLine("=== Fragment Manager Debug Info (iOS Compatible) ===")
|
||||
appendLine("Active Fragment Sets: ${incomingFragments.size}")
|
||||
appendLine("Fragment Size Threshold: $FRAGMENT_SIZE_THRESHOLD bytes")
|
||||
appendLine("Max Fragment Size: $MAX_FRAGMENT_SIZE bytes")
|
||||
|
||||
fragmentMetadata.forEach { (fragmentID, metadata) ->
|
||||
val (originalType, totalFragments, timestamp) = metadata
|
||||
val received = incomingFragments[fragmentID]?.size ?: 0
|
||||
@@ -212,7 +239,7 @@ class FragmentManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic cleanup of old fragments
|
||||
* Start periodic cleanup of old fragments - matches iOS maintenance timer
|
||||
*/
|
||||
private fun startPeriodicCleanup() {
|
||||
managerScope.launch {
|
||||
@@ -223,29 +250,6 @@ class FragmentManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -2,9 +2,7 @@ package com.bitchat.android.mesh
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.DeliveryAck
|
||||
import com.bitchat.android.model.NoiseIdentityAnnouncement
|
||||
import com.bitchat.android.model.ReadReceipt
|
||||
import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
@@ -33,7 +31,8 @@ class MessageHandler(private val myPeerID: String) {
|
||||
private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
/**
|
||||
* Handle Noise encrypted transport message
|
||||
* Handle Noise encrypted transport message - SIMPLIFIED iOS-compatible version
|
||||
* Uses NoisePayloadType system exactly like iOS SimplifiedBluetoothService
|
||||
*/
|
||||
suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
|
||||
val packet = routed.packet
|
||||
@@ -44,6 +43,13 @@ class MessageHandler(private val myPeerID: String) {
|
||||
// Skip our own messages
|
||||
if (peerID == myPeerID) return
|
||||
|
||||
// Check if this message is for us
|
||||
val recipientID = packet.recipientID?.toHexString()
|
||||
if (recipientID != myPeerID) {
|
||||
Log.d(TAG, "🔐 Encrypted message not for me (for $recipientID, I am $myPeerID)")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Decrypt the message using the Noise service
|
||||
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
|
||||
@@ -52,44 +58,66 @@ class MessageHandler(private val myPeerID: String) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a special format message (type marker + payload)
|
||||
if (decryptedData.size > 1) {
|
||||
val typeMarker = decryptedData[0].toUByte()
|
||||
|
||||
// Check if this is a delivery ACK with the new format
|
||||
if (typeMarker == MessageType.DELIVERY_ACK.value) {
|
||||
handleDeliveryAck(decryptedData)
|
||||
Log.d(TAG, "Processed delivery ACK from $peerID")
|
||||
}
|
||||
|
||||
// Check for read receipt with type marker
|
||||
if (typeMarker == MessageType.READ_RECEIPT.value) {
|
||||
val receiptData = decryptedData.sliceArray(1 until decryptedData.size)
|
||||
val receipt = ReadReceipt.decode(receiptData)
|
||||
if (receipt != null) {
|
||||
delegate?.onReadReceiptReceived(receipt)
|
||||
Log.d(TAG, "Processed read receipt from $peerID")
|
||||
return
|
||||
}
|
||||
}
|
||||
if (decryptedData.isEmpty()) {
|
||||
Log.w(TAG, "Decrypted data is empty from $peerID")
|
||||
return
|
||||
}
|
||||
|
||||
// Try to parse as a full inner packet (for compatibility with other message types)
|
||||
val innerPacket = BitchatPacket.fromBinaryData(decryptedData)
|
||||
if (innerPacket != null) {
|
||||
Log.d(TAG, "Decrypted inner packet type ${innerPacket.type} from $peerID")
|
||||
// NEW: Use NoisePayload system exactly like iOS
|
||||
val noisePayload = com.bitchat.android.model.NoisePayload.decode(decryptedData)
|
||||
if (noisePayload == null) {
|
||||
Log.w(TAG, "Failed to parse NoisePayload from $peerID")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "🔓 Decrypted NoisePayload type ${noisePayload.type} from $peerID")
|
||||
|
||||
when (noisePayload.type) {
|
||||
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
|
||||
// Decode TLV private message exactly like iOS
|
||||
val privateMessage = com.bitchat.android.model.PrivateMessagePacket.decode(noisePayload.data)
|
||||
if (privateMessage != null) {
|
||||
Log.d(TAG, "🔓 Decrypted TLV PM from $peerID: ${privateMessage.content.take(30)}...")
|
||||
|
||||
// Create BitchatMessage exactly like iOS
|
||||
val message = BitchatMessage(
|
||||
id = privateMessage.messageID,
|
||||
sender = delegate?.getPeerNickname(peerID) ?: "Unknown",
|
||||
content = privateMessage.content,
|
||||
timestamp = java.util.Date(packet.timestamp.toLong()),
|
||||
isRelay = false,
|
||||
originalSender = null,
|
||||
isPrivate = true,
|
||||
recipientNickname = delegate?.getMyNickname(),
|
||||
senderPeerID = peerID,
|
||||
mentions = null // TODO: Parse mentions if needed
|
||||
)
|
||||
|
||||
// Notify delegate
|
||||
delegate?.onMessageReceived(message)
|
||||
|
||||
// Send delivery ACK exactly like iOS
|
||||
sendDeliveryAck(privateMessage.messageID, peerID)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new routed packet with the decrypted inner packet
|
||||
val innerRouted = RoutedPacket(innerPacket, peerID, routed.relayAddress)
|
||||
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
|
||||
// Handle delivery ACK exactly like iOS
|
||||
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
||||
Log.d(TAG, "📬 Delivery ACK received from $peerID for message $messageID")
|
||||
|
||||
// Simplified: Call delegate with messageID and peerID directly
|
||||
delegate?.onDeliveryAckReceived(messageID, peerID)
|
||||
}
|
||||
|
||||
// Use PacketProcessor to handle the inner packet recursively
|
||||
if (packetProcessor != null) {
|
||||
packetProcessor!!.processPacket(innerRouted)
|
||||
} else {
|
||||
Log.w(TAG, "PacketProcessor reference is null; cannot recursively process inner packet.")
|
||||
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
|
||||
// Handle read receipt exactly like iOS
|
||||
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
||||
Log.d(TAG, "👁️ Read receipt received from $peerID for message $messageID")
|
||||
|
||||
// Simplified: Call delegate with messageID and peerID directly
|
||||
delegate?.onReadReceiptReceived(messageID, peerID)
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Failed to parse decrypted data as packet from $peerID")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -98,77 +126,45 @@ class MessageHandler(private val myPeerID: String) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Noise identity announcement - supports peer ID rotation
|
||||
* Send delivery ACK for a received private message - exactly like iOS
|
||||
*/
|
||||
suspend fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
|
||||
Log.d(TAG, "Processing Noise identity announcement from $peerID (${packet.payload.size} bytes)")
|
||||
|
||||
// Skip our own announcements
|
||||
if (peerID == myPeerID) return
|
||||
|
||||
private suspend fun sendDeliveryAck(messageID: String, senderPeerID: String) {
|
||||
try {
|
||||
// Parse the identity announcement
|
||||
val announcement = NoiseIdentityAnnouncement.fromBinaryData(packet.payload)
|
||||
if (announcement == null) {
|
||||
Log.w(TAG, "Failed to parse Noise identity announcement from $peerID")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Parsed identity announcement: peerID=${announcement.peerID}, " +
|
||||
"nickname=${announcement.nickname}, fingerprint=${announcement.fingerprint?.take(16)}...")
|
||||
|
||||
// Verify the announcement signature using Ed25519 (iOS compatibility)
|
||||
if (announcement.signature.isEmpty()) {
|
||||
Log.w(TAG, "❌ Identity announcement from $peerID has no signature - rejecting")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify signature using the same format as iOS
|
||||
val timestampMs = announcement.timestamp.time
|
||||
val bindingData = announcement.peerID.toByteArray(Charsets.UTF_8) +
|
||||
announcement.publicKey +
|
||||
timestampMs.toString().toByteArray(Charsets.UTF_8)
|
||||
|
||||
val isSignatureValid = delegate?.verifyEd25519Signature(announcement.signature, bindingData, announcement.signingPublicKey) ?: false
|
||||
|
||||
if (!isSignatureValid) {
|
||||
Log.w(TAG, "❌ Signature verification failed for identity announcement from $peerID - rejecting")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "✅ Signature verification successful for identity announcement from $peerID")
|
||||
|
||||
// Update peer binding in the delegate (ChatViewModel/BluetoothMeshService)
|
||||
delegate?.updatePeerIDBinding(
|
||||
newPeerID = announcement.peerID,
|
||||
nickname = announcement.nickname,
|
||||
publicKey = announcement.publicKey,
|
||||
previousPeerID = announcement.previousPeerID
|
||||
// Create ACK payload: [type byte] + [message ID] - exactly like iOS
|
||||
val ackPayload = com.bitchat.android.model.NoisePayload(
|
||||
type = com.bitchat.android.model.NoisePayloadType.DELIVERED,
|
||||
data = messageID.toByteArray(Charsets.UTF_8)
|
||||
)
|
||||
|
||||
// Check if we need to initiate a handshake with this peer
|
||||
val hasSession = delegate?.hasNoiseSession(announcement.peerID) ?: false
|
||||
if (!hasSession) {
|
||||
Log.d(TAG, "No session with ${announcement.peerID}, may need handshake")
|
||||
|
||||
// Use lexicographic comparison to decide who initiates (prevents both sides from initiating)
|
||||
if (myPeerID < announcement.peerID) {
|
||||
delegate?.initiateNoiseHandshake(announcement.peerID)
|
||||
}
|
||||
// Encrypt the payload
|
||||
val encryptedPayload = delegate?.encryptForPeer(ackPayload.encode(), senderPeerID)
|
||||
if (encryptedPayload == null) {
|
||||
Log.w(TAG, "Failed to encrypt delivery ACK for $senderPeerID")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Successfully processed identity announcement from $peerID")
|
||||
// Create NOISE_ENCRYPTED packet exactly like iOS
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(senderPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encryptedPayload,
|
||||
signature = null,
|
||||
ttl = 7u // Same TTL as iOS messageTTL
|
||||
)
|
||||
|
||||
delegate?.sendPacket(packet)
|
||||
Log.d(TAG, "📤 Sent delivery ACK to $senderPeerID for message $messageID")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error processing Noise identity announcement from $peerID: ${e.message}")
|
||||
Log.e(TAG, "Failed to send delivery ACK to $senderPeerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle announce message
|
||||
* Handle announce message with TLV decoding support - exactly like iOS
|
||||
*/
|
||||
suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
|
||||
val packet = routed.packet
|
||||
@@ -176,17 +172,90 @@ class MessageHandler(private val myPeerID: String) {
|
||||
|
||||
if (peerID == myPeerID) return false
|
||||
|
||||
val nickname = String(packet.payload, Charsets.UTF_8)
|
||||
Log.d(TAG, "Received announce from $peerID: $nickname")
|
||||
// Try to decode as iOS-compatible IdentityAnnouncement with TLV format
|
||||
val announcement = IdentityAnnouncement.decode(packet.payload)
|
||||
if (announcement == null) {
|
||||
Log.w(TAG, "Failed to decode announce from $peerID as iOS-compatible TLV format")
|
||||
return false
|
||||
}
|
||||
|
||||
// Notify delegate to handle peer management
|
||||
// Successfully decoded TLV format exactly like iOS
|
||||
Log.d(TAG, "Received iOS-compatible announce from $peerID: nickname=${announcement.nickname}, " +
|
||||
"publicKey=${announcement.publicKey.joinToString("") { "%02x".format(it) }.take(16)}...")
|
||||
|
||||
// Extract nickname and public key from TLV data
|
||||
val nickname = announcement.nickname
|
||||
val publicKey = announcement.publicKey
|
||||
|
||||
// Notify delegate to handle peer management with nickname
|
||||
val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false
|
||||
|
||||
// Update peer ID binding with public key for identity management
|
||||
delegate?.updatePeerIDBinding(
|
||||
newPeerID = peerID,
|
||||
nickname = nickname,
|
||||
publicKey = publicKey,
|
||||
previousPeerID = null
|
||||
)
|
||||
|
||||
// Announce relay is now handled by centralized PacketRelayManager
|
||||
|
||||
Log.d(TAG, "Processed iOS-compatible TLV announce: stored public key for $peerID")
|
||||
return isFirstAnnounce
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Noise handshake - SIMPLIFIED iOS-compatible version
|
||||
* Single handshake type (0x10) with response determined by payload analysis
|
||||
*/
|
||||
suspend fun handleNoiseHandshake(routed: RoutedPacket) {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
|
||||
Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)")
|
||||
|
||||
// Skip our own handshake messages
|
||||
if (peerID == myPeerID) return
|
||||
|
||||
// Check if handshake is addressed to us
|
||||
val recipientID = packet.recipientID?.toHexString()
|
||||
if (recipientID != myPeerID) {
|
||||
Log.d(TAG, "Handshake not for me (for $recipientID, I am $myPeerID)")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Process handshake message through delegate (simplified approach)
|
||||
val response = delegate?.processNoiseHandshakeMessage(packet.payload, peerID)
|
||||
|
||||
if (response != null) {
|
||||
Log.d(TAG, "Generated handshake response for $peerID (${response.size} bytes)")
|
||||
|
||||
// Send response using same packet type (simplified iOS approach)
|
||||
val responsePacket = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = response,
|
||||
signature = null,
|
||||
ttl = 7u // Same TTL as iOS
|
||||
)
|
||||
|
||||
delegate?.sendPacket(responsePacket)
|
||||
Log.d(TAG, "📤 Sent handshake response to $peerID")
|
||||
}
|
||||
|
||||
// Check if session is now established
|
||||
val hasSession = delegate?.hasNoiseSession(peerID) ?: false
|
||||
if (hasSession) {
|
||||
Log.d(TAG, "✅ Noise session established with $peerID")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to process Noise handshake from $peerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle broadcast or private message
|
||||
*/
|
||||
@@ -194,6 +263,11 @@ class MessageHandler(private val myPeerID: String) {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
if (peerID == myPeerID) return
|
||||
val senderNickname = delegate?.getPeerNickname(peerID)
|
||||
if (senderNickname != null) {
|
||||
Log.d(TAG, "Received message from $senderNickname")
|
||||
delegate?.updatePeerNickname(peerID, senderNickname)
|
||||
}
|
||||
|
||||
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
|
||||
|
||||
@@ -215,35 +289,14 @@ class MessageHandler(private val myPeerID: String) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
try {
|
||||
// Parse message
|
||||
val message = BitchatMessage.fromBinaryPayload(packet.payload)
|
||||
if (message != null) {
|
||||
// Check for cover traffic (dummy messages)
|
||||
if (message.content.startsWith("☂DUMMY☂")) {
|
||||
Log.d(TAG, "Discarding cover traffic from $peerID")
|
||||
return // Silently discard
|
||||
}
|
||||
|
||||
delegate?.updatePeerNickname(peerID, message.sender)
|
||||
|
||||
// Handle encrypted channel messages
|
||||
val finalContent = if (message.channel != null && message.isEncrypted && message.encryptedContent != null) {
|
||||
delegate?.decryptChannelMessage(message.encryptedContent, message.channel)
|
||||
?: "[Encrypted message - password required]"
|
||||
} else {
|
||||
message.content
|
||||
}
|
||||
|
||||
// Replace timestamp with current time (same as iOS)
|
||||
val messageWithCurrentTime = message.copy(
|
||||
content = finalContent,
|
||||
senderPeerID = peerID,
|
||||
timestamp = Date() // Use current time instead of original timestamp
|
||||
)
|
||||
|
||||
delegate?.onMessageReceived(messageWithCurrentTime)
|
||||
}
|
||||
|
||||
// Broadcast message relay is now handled by centralized PacketRelayManager
|
||||
val message = BitchatMessage(
|
||||
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||
content = String(packet.payload, Charsets.UTF_8),
|
||||
senderPeerID = peerID,
|
||||
timestamp = Date()
|
||||
)
|
||||
|
||||
delegate?.onMessageReceived(message)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to process broadcast message: ${e.message}")
|
||||
@@ -262,21 +315,14 @@ class MessageHandler(private val myPeerID: String) {
|
||||
}
|
||||
|
||||
// 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 private cover traffic from $peerID")
|
||||
return // Silently discard
|
||||
}
|
||||
|
||||
delegate?.updatePeerNickname(peerID, message.sender)
|
||||
delegate?.onMessageReceived(message)
|
||||
|
||||
// Send delivery ACK
|
||||
delegate?.sendDeliveryAck(message, peerID)
|
||||
}
|
||||
|
||||
val message = BitchatMessage(
|
||||
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||
content = String(packet.payload, Charsets.UTF_8),
|
||||
senderPeerID = peerID,
|
||||
timestamp = Date()
|
||||
)
|
||||
delegate?.onMessageReceived(message)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to process private message from $peerID: ${e.message}")
|
||||
}
|
||||
@@ -301,31 +347,6 @@ class MessageHandler(private val myPeerID: String) {
|
||||
// Leave message relay is now handled by centralized PacketRelayManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delivery acknowledgment
|
||||
*/
|
||||
suspend fun handleDeliveryAck(decryptedData: ByteArray) {
|
||||
val ackData = decryptedData.sliceArray(1 until decryptedData.size)
|
||||
val ack = DeliveryAck.decode(ackData)
|
||||
if (ack != null) {
|
||||
delegate?.onDeliveryAckReceived(ack)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle read receipt
|
||||
*/
|
||||
suspend fun handleReadReceipt(routed: RoutedPacket) {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
val receipt = ReadReceipt.decode(routed.packet.payload)
|
||||
if (receipt != null) {
|
||||
delegate?.onReadReceiptReceived(receipt)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
*/
|
||||
@@ -392,16 +413,16 @@ interface MessageHandlerDelegate {
|
||||
// Noise protocol operations
|
||||
fun hasNoiseSession(peerID: String): Boolean
|
||||
fun initiateNoiseHandshake(peerID: String)
|
||||
fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray?
|
||||
fun updatePeerIDBinding(newPeerID: String, nickname: String,
|
||||
publicKey: ByteArray, previousPeerID: String?)
|
||||
|
||||
// Message operations
|
||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
||||
fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String)
|
||||
|
||||
|
||||
// Callbacks
|
||||
fun onMessageReceived(message: BitchatMessage)
|
||||
fun onChannelLeave(channel: String, fromPeer: String)
|
||||
fun onDeliveryAckReceived(ack: DeliveryAck)
|
||||
fun onReadReceiptReceived(receipt: ReadReceipt)
|
||||
fun onDeliveryAckReceived(messageID: String, peerID: String)
|
||||
fun onReadReceiptReceived(messageID: String, peerID: String)
|
||||
}
|
||||
|
||||
@@ -133,22 +133,16 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
|
||||
// Handle public packet types (no address check needed)
|
||||
when (messageType) {
|
||||
MessageType.NOISE_IDENTITY_ANNOUNCE -> handleNoiseIdentityAnnouncement(routed)
|
||||
MessageType.ANNOUNCE -> handleAnnounce(routed)
|
||||
MessageType.MESSAGE -> handleMessage(routed)
|
||||
MessageType.LEAVE -> handleLeave(routed)
|
||||
MessageType.FRAGMENT_START,
|
||||
MessageType.FRAGMENT_CONTINUE,
|
||||
MessageType.FRAGMENT_END -> handleFragment(routed)
|
||||
MessageType.FRAGMENT -> handleFragment(routed)
|
||||
else -> {
|
||||
// Handle private packet types (address check required)
|
||||
if (packetRelayManager.isPacketAddressedToMe(packet)) {
|
||||
when (messageType) {
|
||||
MessageType.NOISE_HANDSHAKE_INIT -> handleNoiseHandshake(routed, 1)
|
||||
MessageType.NOISE_HANDSHAKE_RESP -> handleNoiseHandshake(routed, 2)
|
||||
MessageType.NOISE_HANDSHAKE -> handleNoiseHandshake(routed)
|
||||
MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed)
|
||||
// MessageType.DELIVERY_ACK -> handleDeliveryAck(routed) // custom packet type...
|
||||
// MessageType.READ_RECEIPT -> handleReadReceipt(routed)
|
||||
else -> {
|
||||
validPacket = false
|
||||
Log.w(TAG, "Unknown message type: ${packet.type}")
|
||||
@@ -170,12 +164,12 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Noise handshake message
|
||||
* Handle Noise handshake message - SIMPLIFIED iOS-compatible version
|
||||
*/
|
||||
private fun handleNoiseHandshake(routed: RoutedPacket, step: Int) {
|
||||
private suspend fun handleNoiseHandshake(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing Noise handshake step $step from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleNoiseHandshake(routed, step)
|
||||
Log.d(TAG, "Processing Noise handshake from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleNoiseHandshake(routed)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,15 +181,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
delegate?.handleNoiseEncrypted(routed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Noise identity announcement (after peer ID rotation)
|
||||
*/
|
||||
private suspend fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing Noise identity announcement from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleNoiseIdentityAnnouncement(routed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle announce message
|
||||
*/
|
||||
@@ -248,15 +233,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
// delegate?.handleDeliveryAck(routed)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Handle read receipt
|
||||
*/
|
||||
private suspend fun handleReadReceipt(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing read receipt from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleReadReceipt(routed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
*/
|
||||
@@ -314,15 +290,12 @@ interface PacketProcessorDelegate {
|
||||
fun getBroadcastRecipient(): ByteArray
|
||||
|
||||
// Message type handlers
|
||||
fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean
|
||||
fun handleNoiseHandshake(routed: RoutedPacket): Boolean
|
||||
fun handleNoiseEncrypted(routed: RoutedPacket)
|
||||
fun handleNoiseIdentityAnnouncement(routed: RoutedPacket)
|
||||
fun handleAnnounce(routed: RoutedPacket)
|
||||
fun handleMessage(routed: RoutedPacket)
|
||||
fun handleLeave(routed: RoutedPacket)
|
||||
fun handleFragment(packet: BitchatPacket): BitchatPacket?
|
||||
// fun handleDeliveryAck(routed: RoutedPacket)
|
||||
fun handleReadReceipt(routed: RoutedPacket)
|
||||
|
||||
// Communication
|
||||
fun sendAnnouncementToPeer(peerID: String)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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
|
||||
|
||||
@@ -88,9 +88,10 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Noise handshake packet
|
||||
* Handle Noise handshake packet - SIMPLIFIED iOS-compatible version
|
||||
* Single handshake type with automatic response handling
|
||||
*/
|
||||
suspend fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean {
|
||||
suspend fun handleNoiseHandshake(routed: RoutedPacket): Boolean {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
|
||||
@@ -120,7 +121,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
Log.d(TAG, "Already processed handshake: $exchangeKey")
|
||||
return false
|
||||
}
|
||||
Log.d(TAG, "Processing Noise handshake step $step from $peerID (${packet.payload.size} bytes)")
|
||||
Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)")
|
||||
processedKeyExchanges.add(exchangeKey)
|
||||
|
||||
try {
|
||||
@@ -128,7 +129,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
val response = encryptionService.processHandshakeMessage(packet.payload, peerID)
|
||||
|
||||
if (response != null) {
|
||||
Log.d(TAG, "Successfully processed Noise handshake step $step from $peerID, sending response")
|
||||
Log.d(TAG, "Successfully processed Noise handshake from $peerID, sending response")
|
||||
// Send handshake response through delegate
|
||||
delegate?.sendHandshakeResponse(peerID, response)
|
||||
}
|
||||
@@ -212,7 +213,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
*/
|
||||
private fun generateMessageID(packet: BitchatPacket, peerID: String): String {
|
||||
return when (MessageType.fromValue(packet.type)) {
|
||||
MessageType.FRAGMENT_START, MessageType.FRAGMENT_CONTINUE, MessageType.FRAGMENT_END -> {
|
||||
MessageType.FRAGMENT -> {
|
||||
// For fragments, include the payload hash to distinguish different fragments
|
||||
"${packet.timestamp}-$peerID-${packet.type}-${packet.payload.contentHashCode()}"
|
||||
}
|
||||
|
||||
@@ -53,10 +53,8 @@ class StoreForwardManager {
|
||||
*/
|
||||
fun cacheMessage(packet: BitchatPacket, messageID: String) {
|
||||
// Skip certain message types (same as iOS)
|
||||
if (packet.type == MessageType.NOISE_HANDSHAKE_INIT.value ||
|
||||
packet.type == MessageType.NOISE_HANDSHAKE_RESP.value ||
|
||||
if (packet.type == MessageType.NOISE_HANDSHAKE.value ||
|
||||
packet.type == MessageType.NOISE_ENCRYPTED.value ||
|
||||
packet.type == MessageType.NOISE_IDENTITY_ANNOUNCE.value ||
|
||||
packet.type == MessageType.ANNOUNCE.value ||
|
||||
packet.type == MessageType.LEAVE.value) {
|
||||
Log.d(TAG, "Skipping cache for message type: ${packet.type}")
|
||||
|
||||
Reference in New Issue
Block a user