diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d37638f0..dab54a54 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -91,10 +91,7 @@ dependencies { // Bluetooth implementation(libs.nordic.ble) - - // Compression - implementation(libs.lz4.java) - + // Security preferences implementation(libs.androidx.security.crypto) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 33657b0c..ac841498 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -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) 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 diff --git a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt index 90817dfb..f86fda4b 100644 --- a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt @@ -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>() + // iOS equivalent: fragmentMetadata: [String: (type: UInt8, total: Int, timestamp: Date)] private val fragmentMetadata = ConcurrentHashMap>() // 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 { - 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() - 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.. 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..() - 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 stride(from: Int, to: Int, by: Int, transform: (Int) -> T): List { + val result = mutableListOf() + 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() - - 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 */ diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index a0d6ae99..6a947a64 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -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) } diff --git a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt index 63e647b8..c8726707 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt @@ -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) diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt index 1b245ad4..b383df63 100644 --- a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt @@ -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 diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 0a5f9a99..59090c52 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -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()}" } diff --git a/app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt b/app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt index 654ceddd..f7be3406 100644 --- a/app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt @@ -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}") diff --git a/app/src/main/java/com/bitchat/android/model/BitchatMessage.kt b/app/src/main/java/com/bitchat/android/model/BitchatMessage.kt index 9bfce6e9..c3681720 100644 --- a/app/src/main/java/com/bitchat/android/model/BitchatMessage.kt +++ b/app/src/main/java/com/bitchat/android/model/BitchatMessage.kt @@ -13,22 +13,22 @@ import java.util.* sealed class DeliveryStatus : Parcelable { @Parcelize object Sending : DeliveryStatus() - + @Parcelize object Sent : DeliveryStatus() - + @Parcelize data class Delivered(val to: String, val at: Date) : DeliveryStatus() - + @Parcelize data class Read(val by: String, val at: Date) : DeliveryStatus() - + @Parcelize data class Failed(val reason: String) : DeliveryStatus() - + @Parcelize data class PartiallyDelivered(val reached: Int, val total: Int) : DeliveryStatus() - + fun getDisplayText(): String { return when (this) { is Sending -> "Sending..." @@ -68,7 +68,7 @@ data class BitchatMessage( fun toBinaryPayload(): ByteArray? { try { val buffer = ByteBuffer.allocate(4096).apply { order(ByteOrder.BIG_ENDIAN) } - + // Message format: // - Flags: 1 byte (bit flags for optional fields) // - Timestamp: 8 bytes (milliseconds since epoch, big-endian) @@ -76,7 +76,7 @@ data class BitchatMessage( // - Sender length: 1 byte + sender data // - Content length: 2 bytes + content data (or encrypted content) // Optional fields based on flags... - + var flags: UByte = 0u if (isRelay) flags = flags or 0x01u if (isPrivate) flags = flags or 0x02u @@ -86,23 +86,23 @@ data class BitchatMessage( if (mentions != null && mentions.isNotEmpty()) flags = flags or 0x20u if (channel != null) flags = flags or 0x40u if (isEncrypted) flags = flags or 0x80u - + buffer.put(flags.toByte()) - + // Timestamp (in milliseconds, 8 bytes big-endian) val timestampMillis = timestamp.time buffer.putLong(timestampMillis) - + // ID val idBytes = id.toByteArray(Charsets.UTF_8) buffer.put(minOf(idBytes.size, 255).toByte()) buffer.put(idBytes.take(255).toByteArray()) - + // Sender val senderBytes = sender.toByteArray(Charsets.UTF_8) buffer.put(minOf(senderBytes.size, 255).toByte()) buffer.put(senderBytes.take(255).toByteArray()) - + // Content or encrypted content if (isEncrypted && encryptedContent != null) { val length = minOf(encryptedContent.size, 65535) @@ -114,26 +114,26 @@ data class BitchatMessage( buffer.putShort(length.toShort()) buffer.put(contentBytes.take(length).toByteArray()) } - + // Optional fields originalSender?.let { origSender -> val origBytes = origSender.toByteArray(Charsets.UTF_8) buffer.put(minOf(origBytes.size, 255).toByte()) buffer.put(origBytes.take(255).toByteArray()) } - + recipientNickname?.let { recipient -> val recipBytes = recipient.toByteArray(Charsets.UTF_8) buffer.put(minOf(recipBytes.size, 255).toByte()) buffer.put(recipBytes.take(255).toByteArray()) } - + senderPeerID?.let { peerID -> val peerBytes = peerID.toByteArray(Charsets.UTF_8) buffer.put(minOf(peerBytes.size, 255).toByte()) buffer.put(peerBytes.take(255).toByteArray()) } - + // Mentions array mentions?.let { mentionList -> buffer.put(minOf(mentionList.size, 255).toByte()) @@ -143,24 +143,24 @@ data class BitchatMessage( buffer.put(mentionBytes.take(255).toByteArray()) } } - + // Channel hashtag channel?.let { channelName -> val channelBytes = channelName.toByteArray(Charsets.UTF_8) buffer.put(minOf(channelBytes.size, 255).toByte()) buffer.put(channelBytes.take(255).toByteArray()) } - + val result = ByteArray(buffer.position()) buffer.rewind() buffer.get(result) return result - + } catch (e: Exception) { return null } } - + companion object { /** * Parse message from binary payload - exactly same logic as iOS version @@ -168,9 +168,9 @@ data class BitchatMessage( fun fromBinaryPayload(data: ByteArray): BitchatMessage? { try { if (data.size < 13) return null - + val buffer = ByteBuffer.wrap(data).apply { order(ByteOrder.BIG_ENDIAN) } - + // Flags val flags = buffer.get().toUByte() val isRelay = (flags and 0x01u) != 0u.toUByte() @@ -181,32 +181,32 @@ data class BitchatMessage( val hasMentions = (flags and 0x20u) != 0u.toUByte() val hasChannel = (flags and 0x40u) != 0u.toUByte() val isEncrypted = (flags and 0x80u) != 0u.toUByte() - + // Timestamp val timestampMillis = buffer.getLong() val timestamp = Date(timestampMillis) - + // ID val idLength = buffer.get().toInt() and 0xFF if (buffer.remaining() < idLength) return null val idBytes = ByteArray(idLength) buffer.get(idBytes) val id = String(idBytes, Charsets.UTF_8) - + // Sender val senderLength = buffer.get().toInt() and 0xFF if (buffer.remaining() < senderLength) return null val senderBytes = ByteArray(senderLength) buffer.get(senderBytes) val sender = String(senderBytes, Charsets.UTF_8) - + // Content val contentLength = buffer.getShort().toInt() and 0xFFFF if (buffer.remaining() < contentLength) return null - + val content: String val encryptedContent: ByteArray? - + if (isEncrypted) { val encryptedBytes = ByteArray(contentLength) buffer.get(encryptedBytes) @@ -218,7 +218,7 @@ data class BitchatMessage( content = String(contentBytes, Charsets.UTF_8) encryptedContent = null } - + // Optional fields val originalSender = if (hasOriginalSender && buffer.hasRemaining()) { val length = buffer.get().toInt() and 0xFF @@ -228,7 +228,7 @@ data class BitchatMessage( String(bytes, Charsets.UTF_8) } else null } else null - + val recipientNickname = if (hasRecipientNickname && buffer.hasRemaining()) { val length = buffer.get().toInt() and 0xFF if (buffer.remaining() >= length) { @@ -237,7 +237,7 @@ data class BitchatMessage( String(bytes, Charsets.UTF_8) } else null } else null - + val senderPeerID = if (hasSenderPeerID && buffer.hasRemaining()) { val length = buffer.get().toInt() and 0xFF if (buffer.remaining() >= length) { @@ -246,7 +246,7 @@ data class BitchatMessage( String(bytes, Charsets.UTF_8) } else null } else null - + // Mentions array val mentions = if (hasMentions && buffer.hasRemaining()) { val mentionCount = buffer.get().toInt() and 0xFF @@ -263,7 +263,7 @@ data class BitchatMessage( } if (mentionList.isNotEmpty()) mentionList else null } else null - + // Channel val channel = if (hasChannel && buffer.hasRemaining()) { val length = buffer.get().toInt() and 0xFF @@ -273,7 +273,7 @@ data class BitchatMessage( String(bytes, Charsets.UTF_8) } else null } else null - + return BitchatMessage( id = id, sender = sender, @@ -289,19 +289,19 @@ data class BitchatMessage( encryptedContent = encryptedContent, isEncrypted = isEncrypted ) - + } catch (e: Exception) { return null } } } - + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - + other as BitchatMessage - + if (id != other.id) return false if (sender != other.sender) return false if (content != other.content) return false @@ -319,10 +319,10 @@ data class BitchatMessage( } else if (other.encryptedContent != null) return false if (isEncrypted != other.isEncrypted) return false if (deliveryStatus != other.deliveryStatus) return false - + return true } - + override fun hashCode(): Int { var result = id.hashCode() result = 31 * result + sender.hashCode() diff --git a/app/src/main/java/com/bitchat/android/model/DeliveryAck.kt b/app/src/main/java/com/bitchat/android/model/DeliveryAck.kt deleted file mode 100644 index 98f16d62..00000000 --- a/app/src/main/java/com/bitchat/android/model/DeliveryAck.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.bitchat.android.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import com.bitchat.android.util.* -import java.util.* - -/** - * Delivery acknowledgment structure - exact same as iOS version - * Uses binary encoding for efficient protocol communication - */ -@Parcelize -data class DeliveryAck( - val originalMessageID: String, - val ackID: String = UUID.randomUUID().toString(), - val recipientID: String, - val recipientNickname: String, - val timestamp: Date = Date(), - val hopCount: UByte -) : Parcelable { - - // Primary constructor for creating new acks - constructor(originalMessageID: String, recipientID: String, recipientNickname: String, hopCount: UByte) : this( - originalMessageID = originalMessageID, - ackID = UUID.randomUUID().toString(), - recipientID = recipientID, - recipientNickname = recipientNickname, - timestamp = Date(), - hopCount = hopCount - ) - - /** - * Encode to binary data matching iOS toBinaryData implementation - */ - fun encode(): ByteArray { - val builder = BinaryDataBuilder() - - // Append original message UUID - builder.appendUUID(originalMessageID) - - // Append ack ID UUID - builder.appendUUID(ackID) - - // Append recipient ID as 8-byte hex string - val recipientData = ByteArray(8) { 0 } - var tempID = recipientID - var index = 0 - - while (tempID.length >= 2 && index < 8) { - val hexByte = tempID.substring(0, 2) - val byte = hexByte.toIntOrNull(16)?.toByte() - if (byte != null) { - recipientData[index] = byte - } - tempID = tempID.substring(2) - index++ - } - - builder.buffer.addAll(recipientData.toList()) - - // Append hop count (UInt8) - builder.appendUInt8(hopCount) - - // Append timestamp - builder.appendDate(timestamp) - - // Append recipient nickname as string - builder.appendString(recipientNickname) - - return builder.toByteArray() - } - - companion object { - /** - * Decode from binary data matching iOS fromBinaryData implementation - */ - fun decode(data: ByteArray): DeliveryAck? { - // Create defensive copy - val dataCopy = data.copyOf() - - // Minimum size: 2 UUIDs (32) + recipientID (8) + hopCount (1) + timestamp (8) + min nickname - if (dataCopy.size < 50) return null - - val offset = intArrayOf(0) - - val originalMessageID = dataCopy.readUUID(offset) ?: return null - val ackID = dataCopy.readUUID(offset) ?: return null - - val recipientIDData = dataCopy.readFixedBytes(offset, 8) ?: return null - val recipientID = recipientIDData.hexEncodedString() - - val hopCount = dataCopy.readUInt8(offset) ?: return null - val timestamp = dataCopy.readDate(offset) ?: return null - val recipientNickname = dataCopy.readString(offset) ?: return null - - return DeliveryAck( - originalMessageID = originalMessageID, - ackID = ackID, - recipientID = recipientID, - recipientNickname = recipientNickname, - timestamp = timestamp, - hopCount = hopCount - ) - } - } -} diff --git a/app/src/main/java/com/bitchat/android/model/FragmentPayload.kt b/app/src/main/java/com/bitchat/android/model/FragmentPayload.kt new file mode 100644 index 00000000..b902222b --- /dev/null +++ b/app/src/main/java/com/bitchat/android/model/FragmentPayload.kt @@ -0,0 +1,155 @@ +package com.bitchat.android.model + +import com.bitchat.android.protocol.MessageType + +/** + * FragmentPayload - 100% iOS-compatible fragment payload structure + * + * This class handles the encoding and decoding of fragment payloads exactly + * as implemented in iOS bitchat SimplifiedBluetoothService. + * + * Fragment payload structure (matching iOS): + * - 8 bytes: Fragment ID (random bytes) + * - 2 bytes: Index (big-endian) + * - 2 bytes: Total count (big-endian) + * - 1 byte: Original message type + * - Variable: Fragment data + * + * Total header size: 13 bytes + */ +data class FragmentPayload( + val fragmentID: ByteArray, // 8 bytes - random fragment identifier + val index: Int, // Fragment index (0-based) + val total: Int, // Total number of fragments + val originalType: UByte, // Original message type before fragmentation + val data: ByteArray // Fragment data +) { + + companion object { + const val HEADER_SIZE = 13 + const val FRAGMENT_ID_SIZE = 8 + + /** + * Decode fragment payload from binary data + * Matches iOS implementation exactly + */ + fun decode(payloadData: ByteArray): FragmentPayload? { + if (payloadData.size < HEADER_SIZE) { + return null + } + + try { + // Extract fragment ID (8 bytes) + val fragmentID = payloadData.sliceArray(0.. HEADER_SIZE) { + payloadData.sliceArray(HEADER_SIZE..= 0 && + total > 0 && + index < total && + data.isNotEmpty() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FragmentPayload + + if (!fragmentID.contentEquals(other.fragmentID)) return false + if (index != other.index) return false + if (total != other.total) return false + if (originalType != other.originalType) return false + if (!data.contentEquals(other.data)) return false + + return true + } + + override fun hashCode(): Int { + var result = fragmentID.contentHashCode() + result = 31 * result + index + result = 31 * result + total + result = 31 * result + originalType.hashCode() + result = 31 * result + data.contentHashCode() + return result + } + + override fun toString(): String { + return "FragmentPayload(fragmentID=${getFragmentIDString()}, index=$index, total=$total, originalType=$originalType, dataSize=${data.size})" + } +} diff --git a/app/src/main/java/com/bitchat/android/model/HandshakeRequest.kt b/app/src/main/java/com/bitchat/android/model/HandshakeRequest.kt deleted file mode 100644 index 176979f6..00000000 --- a/app/src/main/java/com/bitchat/android/model/HandshakeRequest.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.bitchat.android.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import com.bitchat.android.util.* -import java.util.* - -/** - * Handshake request for pending messages - exact same as iOS version - * Uses binary encoding for efficient protocol communication - */ -@Parcelize -data class HandshakeRequest( - val requestID: String, - val requesterID: String, // Who needs the handshake - val requesterNickname: String, // Nickname of requester - val targetID: String, // Who should initiate handshake - val pendingMessageCount: UByte, // Number of messages queued - val timestamp: Date -) : Parcelable { - - // Primary constructor for creating new requests - constructor(requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UByte) : this( - requestID = UUID.randomUUID().toString(), - requesterID = requesterID, - requesterNickname = requesterNickname, - targetID = targetID, - pendingMessageCount = pendingMessageCount, - timestamp = Date() - ) - - /** - * Encode to binary data matching iOS toBinaryData implementation - */ - fun toBinaryData(): ByteArray { - val builder = BinaryDataBuilder() - - // Append request ID UUID - builder.appendUUID(requestID) - - // RequesterID as 8-byte hex string - val requesterData = ByteArray(8) { 0 } - var tempID = requesterID - var index = 0 - - while (tempID.length >= 2 && index < 8) { - val hexByte = tempID.substring(0, 2) - val byte = hexByte.toIntOrNull(16)?.toByte() - if (byte != null) { - requesterData[index] = byte - } - tempID = tempID.substring(2) - index++ - } - - builder.buffer.addAll(requesterData.toList()) - - // TargetID as 8-byte hex string - val targetData = ByteArray(8) { 0 } - tempID = targetID - index = 0 - - while (tempID.length >= 2 && index < 8) { - val hexByte = tempID.substring(0, 2) - val byte = hexByte.toIntOrNull(16)?.toByte() - if (byte != null) { - targetData[index] = byte - } - tempID = tempID.substring(2) - index++ - } - - builder.buffer.addAll(targetData.toList()) - - // Append pending message count (UInt8) - builder.appendUInt8(pendingMessageCount) - - // Append timestamp - builder.appendDate(timestamp) - - // Append requester nickname as string - builder.appendString(requesterNickname) - - return builder.toByteArray() - } - - companion object { - /** - * Decode from binary data matching iOS fromBinaryData implementation - */ - fun fromBinaryData(data: ByteArray): HandshakeRequest? { - // Create defensive copy - val dataCopy = data.copyOf() - - // Minimum size: UUID (16) + requesterID (8) + targetID (8) + count (1) + timestamp (8) + min nickname - if (dataCopy.size < 42) return null - - val offset = intArrayOf(0) - - val requestID = dataCopy.readUUID(offset) ?: return null - - val requesterIDData = dataCopy.readFixedBytes(offset, 8) ?: return null - val requesterID = requesterIDData.hexEncodedString() - - val targetIDData = dataCopy.readFixedBytes(offset, 8) ?: return null - val targetID = targetIDData.hexEncodedString() - - val pendingMessageCount = dataCopy.readUInt8(offset) ?: return null - val timestamp = dataCopy.readDate(offset) ?: return null - val requesterNickname = dataCopy.readString(offset) ?: return null - - return HandshakeRequest( - requestID = requestID, - requesterID = requesterID, - requesterNickname = requesterNickname, - targetID = targetID, - pendingMessageCount = pendingMessageCount, - timestamp = timestamp - ) - } - } -} diff --git a/app/src/main/java/com/bitchat/android/model/IdentityAnnouncement.kt b/app/src/main/java/com/bitchat/android/model/IdentityAnnouncement.kt new file mode 100644 index 00000000..7b9b1935 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/model/IdentityAnnouncement.kt @@ -0,0 +1,127 @@ +package com.bitchat.android.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import com.bitchat.android.util.* + +/** + * Identity announcement structure with TLV encoding + * Compatible with iOS AnnouncementPacket TLV format + */ +@Parcelize +data class IdentityAnnouncement( + val nickname: String, + val publicKey: ByteArray // FIXED: Made non-nullable to match iOS and AnnouncementPacket +) : Parcelable { + + /** + * TLV types matching iOS implementation + */ + private enum class TLVType(val value: UByte) { + NICKNAME(0x01u), + NOISE_PUBLIC_KEY(0x02u); + + companion object { + fun fromValue(value: UByte): TLVType? { + return values().find { it.value == value } + } + } + } + + /** + * Encode to TLV binary data matching iOS implementation + */ + fun encode(): ByteArray? { + val nicknameData = nickname.toByteArray(Charsets.UTF_8) + + // Check size limits + if (nicknameData.size > 255 || publicKey.size > 255) { + return null + } + + val result = mutableListOf() + + // TLV for nickname + result.add(TLVType.NICKNAME.value.toByte()) + result.add(nicknameData.size.toByte()) + result.addAll(nicknameData.toList()) + + // TLV for public key + result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte()) + result.add(publicKey.size.toByte()) + result.addAll(publicKey.toList()) + + return result.toByteArray() + } + + companion object { + /** + * Decode from TLV binary data matching iOS implementation + */ + fun decode(data: ByteArray): IdentityAnnouncement? { + // Create defensive copy + val dataCopy = data.copyOf() + + var offset = 0 + var nickname: String? = null + var publicKey: ByteArray? = null + + while (offset + 2 <= dataCopy.size) { + // Read TLV type + val typeValue = dataCopy[offset].toUByte() + val type = TLVType.fromValue(typeValue) ?: return null + offset += 1 + + // Read TLV length + val length = dataCopy[offset].toUByte().toInt() + offset += 1 + + // Check bounds + if (offset + length > dataCopy.size) return null + + // Read TLV value + val value = dataCopy.sliceArray(offset until offset + length) + offset += length + + when (type) { + TLVType.NICKNAME -> { + nickname = String(value, Charsets.UTF_8) + } + TLVType.NOISE_PUBLIC_KEY -> { + publicKey = value + } + } + } + + // Both fields are required + return if (nickname != null && publicKey != null) { + IdentityAnnouncement(nickname, publicKey) + } else { + null + } + } + } + + // Override equals and hashCode since we use ByteArray + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IdentityAnnouncement + + if (nickname != other.nickname) return false + if (!publicKey.contentEquals(other.publicKey)) return false + + return true + } + + override fun hashCode(): Int { + var result = nickname.hashCode() + result = 31 * result + publicKey.contentHashCode() + return result + } + + override fun toString(): String { + return "IdentityAnnouncement(nickname='$nickname', publicKey=${publicKey.joinToString("") { "%02x".format(it) }.take(16)}...)" + } +} diff --git a/app/src/main/java/com/bitchat/android/model/NoiseEncrypted.kt b/app/src/main/java/com/bitchat/android/model/NoiseEncrypted.kt new file mode 100644 index 00000000..7ed39902 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/model/NoiseEncrypted.kt @@ -0,0 +1,193 @@ +package com.bitchat.android.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Noise encrypted payload types and handling - 100% compatible with iOS SimplifiedBluetoothService + * + * This handles all encrypted content that goes through noiseEncrypted packets: + * - Private messages with TLV encoding + * - Delivery acknowledgments + * - Read receipts + * - Future encrypted payload types + */ + +/** + * Types of payloads embedded within noiseEncrypted messages + * Matches iOS NoisePayloadType exactly + */ +enum class NoisePayloadType(val value: UByte) { + PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding + READ_RECEIPT(0x02u), // Message was read + DELIVERED(0x03u); // Message was delivered + + companion object { + fun fromValue(value: UByte): NoisePayloadType? { + return values().find { it.value == value } + } + } +} + +/** + * Helper class for creating and parsing Noise payloads + * Matches iOS NoisePayload helper exactly + */ +@Parcelize +data class NoisePayload( + val type: NoisePayloadType, + val data: ByteArray +) : Parcelable { + + /** + * Encode payload with type prefix - exactly like iOS + * Format: [type_byte][payload_data] + */ + fun encode(): ByteArray { + val result = ByteArray(1 + data.size) + result[0] = type.value.toByte() + data.copyInto(result, destinationOffset = 1) + return result + } + + companion object { + /** + * Decode payload from data - exactly like iOS + * Expects: [type_byte][payload_data] + */ + fun decode(data: ByteArray): NoisePayload? { + if (data.isEmpty()) return null + + val typeValue = data[0].toUByte() + val type = NoisePayloadType.fromValue(typeValue) ?: return null + + // Extract payload data (remaining bytes after type byte) + val payloadData = if (data.size > 1) { + data.copyOfRange(1, data.size) + } else { + ByteArray(0) + } + + return NoisePayload(type, payloadData) + } + } + + // Override equals and hashCode since we use ByteArray + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NoisePayload + + if (type != other.type) return false + if (!data.contentEquals(other.data)) return false + + return true + } + + override fun hashCode(): Int { + var result = type.hashCode() + result = 31 * result + data.contentHashCode() + return result + } +} + +/** + * Private message packet with TLV encoding - matches iOS PrivateMessagePacket exactly + */ +@Parcelize +data class PrivateMessagePacket( + val messageID: String, + val content: String +) : Parcelable { + + /** + * TLV types matching iOS implementation exactly + */ + private enum class TLVType(val value: UByte) { + MESSAGE_ID(0x00u), + CONTENT(0x01u); + + companion object { + fun fromValue(value: UByte): TLVType? { + return values().find { it.value == value } + } + } + } + + /** + * Encode to TLV binary data - exactly like iOS + * Format: [type][length][value] for each field + */ + fun encode(): ByteArray? { + val messageIDData = messageID.toByteArray(Charsets.UTF_8) + val contentData = content.toByteArray(Charsets.UTF_8) + + // Check size limits (TLV length field is 1 byte = max 255) + if (messageIDData.size > 255 || contentData.size > 255) { + return null + } + + val result = mutableListOf() + + // TLV for messageID + result.add(TLVType.MESSAGE_ID.value.toByte()) + result.add(messageIDData.size.toByte()) + result.addAll(messageIDData.toList()) + + // TLV for content + result.add(TLVType.CONTENT.value.toByte()) + result.add(contentData.size.toByte()) + result.addAll(contentData.toList()) + + return result.toByteArray() + } + + companion object { + /** + * Decode from TLV binary data - exactly like iOS + */ + fun decode(data: ByteArray): PrivateMessagePacket? { + var offset = 0 + var messageID: String? = null + var content: String? = null + + while (offset + 2 <= data.size) { + // Read TLV type + val typeValue = data[offset].toUByte() + val type = TLVType.fromValue(typeValue) ?: return null + offset += 1 + + // Read TLV length + val length = data[offset].toUByte().toInt() + offset += 1 + + // Check bounds + if (offset + length > data.size) return null + + // Read TLV value + val value = data.copyOfRange(offset, offset + length) + offset += length + + when (type) { + TLVType.MESSAGE_ID -> { + messageID = String(value, Charsets.UTF_8) + } + TLVType.CONTENT -> { + content = String(value, Charsets.UTF_8) + } + } + } + + return if (messageID != null && content != null) { + PrivateMessagePacket(messageID, content) + } else { + null + } + } + } + + override fun toString(): String { + return "PrivateMessagePacket(messageID='$messageID', content='${content.take(50)}${if (content.length > 50) "..." else ""}')" + } +} diff --git a/app/src/main/java/com/bitchat/android/model/NoiseIdentityAnnouncement.kt b/app/src/main/java/com/bitchat/android/model/NoiseIdentityAnnouncement.kt deleted file mode 100644 index 2416df61..00000000 --- a/app/src/main/java/com/bitchat/android/model/NoiseIdentityAnnouncement.kt +++ /dev/null @@ -1,199 +0,0 @@ -package com.bitchat.android.model - -import android.util.Log -import com.bitchat.android.util.* -import java.security.MessageDigest -import java.util.* - -/** - * Noise Identity Announcement data class (compatible with iOS version) - * Enhanced identity announcement with rotation support and binary protocol - */ -data class NoiseIdentityAnnouncement( - val peerID: String, // Current ephemeral peer ID - val publicKey: ByteArray, // Noise static public key - val signingPublicKey: ByteArray, // Ed25519 signing public key - val nickname: String, // Current nickname - val timestamp: Date, // When this binding was created - val previousPeerID: String?, // Previous peer ID (for smooth transition) - val signature: ByteArray // Signature proving ownership -) { - - // Computed fingerprint from public key - val fingerprint: String by lazy { - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(publicKey) - hash.joinToString("") { "%02x".format(it) } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as NoiseIdentityAnnouncement - - if (peerID != other.peerID) return false - if (nickname != other.nickname) return false - if (!publicKey.contentEquals(other.publicKey)) return false - if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false - if (timestamp != other.timestamp) return false - if (!signature.contentEquals(other.signature)) return false - if (previousPeerID != other.previousPeerID) return false - - return true - } - - override fun hashCode(): Int { - var result = peerID.hashCode() - result = 31 * result + nickname.hashCode() - result = 31 * result + publicKey.contentHashCode() - result = 31 * result + signingPublicKey.contentHashCode() - result = 31 * result + timestamp.hashCode() - result = 31 * result + signature.contentHashCode() - result = 31 * result + (previousPeerID?.hashCode() ?: 0) - return result - } - - // MARK: - Binary Encoding - - fun toBinaryData(): ByteArray { - val builder = BinaryDataBuilder() - - // Flags byte: bit 0 = hasPreviousPeerID - var flags: UByte = 0u - if (previousPeerID != null) flags = flags or 0x01u - builder.appendUInt8(flags) - - // PeerID as 8-byte hex string - val peerData = hexStringToByteArray(peerID) - // Directly append the 8 bytes without length prefix since this is a fixed field - for (byte in peerData) { - builder.buffer.add(byte) - } - - builder.appendData(publicKey) - builder.appendData(signingPublicKey) - builder.appendString(nickname) - builder.appendDate(timestamp) - - if (previousPeerID != null) { - // Previous PeerID as 8-byte hex string - val prevData = hexStringToByteArray(previousPeerID) - // Directly append the 8 bytes without length prefix since this is a fixed field - for (byte in prevData) { - builder.buffer.add(byte) - } - } - - builder.appendData(signature) - - return builder.toByteArray() - } - - companion object { - private const val TAG = "NoiseIdentityAnnouncement" - - /** - * Parse Noise identity announcement from binary payload with proper iOS compatibility - */ - fun fromBinaryData(data: ByteArray): NoiseIdentityAnnouncement? { - return try { - // Create defensive copy - val dataCopy = data.copyOf() - - // Minimum size check: flags(1) + peerID(8) + min data lengths - if (dataCopy.size < 20) { - Log.w(TAG, "Data too small for NoiseIdentityAnnouncement: ${dataCopy.size} bytes") - return null - } - - val offsetArray = intArrayOf(0) - - val flags = dataCopy.readUInt8(offsetArray) ?: run { - Log.w(TAG, "Failed to read flags") - return null - } - val hasPreviousPeerID = (flags.toInt() and 0x01) != 0 - - // Read peerID using safe method - val peerIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run { - Log.w(TAG, "Failed to read peerID bytes") - return null - } - val peerID = peerIDBytes.hexEncodedString() - - val publicKey = dataCopy.readData(offsetArray) ?: run { - Log.w(TAG, "Failed to read public key") - return null - } - - val signingPublicKey = dataCopy.readData(offsetArray) ?: run { - Log.w(TAG, "Failed to read signing public key") - return null - } - - val nickname = dataCopy.readString(offsetArray) ?: run { - Log.w(TAG, "Failed to read nickname") - return null - } - - val timestamp = dataCopy.readDate(offsetArray) ?: run { - Log.w(TAG, "Failed to read timestamp") - return null - } - - var previousPeerID: String? = null - if (hasPreviousPeerID) { - // Read previousPeerID using safe method - val prevIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run { - Log.w(TAG, "Failed to read previousPeerID bytes") - return null - } - previousPeerID = prevIDBytes.hexEncodedString() - } - - val signature = dataCopy.readData(offsetArray) ?: run { - Log.w(TAG, "Failed to read signature") - return null - } - - Log.d(TAG, "Successfully parsed NoiseIdentityAnnouncement: peerID=$peerID, nickname=$nickname") - - return NoiseIdentityAnnouncement( - peerID = peerID, - publicKey = publicKey, - signingPublicKey = signingPublicKey, - nickname = nickname, - timestamp = timestamp, - previousPeerID = previousPeerID, - signature = signature - ) - - } catch (e: Exception) { - Log.e(TAG, "Failed to parse Noise identity announcement: ${e.message}", e) - null - } - } - - /** - * Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS - */ - private fun hexStringToByteArray(hexString: String): ByteArray { - val result = ByteArray(8) { 0 } // Initialize with zeros, exactly 8 bytes - var tempID = hexString - var index = 0 - - while (tempID.length >= 2 && index < 8) { - val hexByte = tempID.substring(0, 2) - val byte = hexByte.toIntOrNull(16)?.toByte() - if (byte != null) { - result[index] = byte - } - tempID = tempID.substring(2) - index++ - } - - return result - } - } -} diff --git a/app/src/main/java/com/bitchat/android/model/ReadReceipt.kt b/app/src/main/java/com/bitchat/android/model/ReadReceipt.kt deleted file mode 100644 index 74dbb676..00000000 --- a/app/src/main/java/com/bitchat/android/model/ReadReceipt.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.bitchat.android.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import com.bitchat.android.util.* -import java.util.* - -/** - * Read receipt structure - exact same as iOS version - * Uses binary encoding for efficient protocol communication - */ -@Parcelize -data class ReadReceipt( - val originalMessageID: String, - val receiptID: String = UUID.randomUUID().toString(), - val readerID: String, - val readerNickname: String, - val timestamp: Date = Date() -) : Parcelable { - - // Primary constructor for creating new read receipts - constructor(originalMessageID: String, readerID: String, readerNickname: String) : this( - originalMessageID = originalMessageID, - receiptID = UUID.randomUUID().toString(), - readerID = readerID, - readerNickname = readerNickname, - timestamp = Date() - ) - - /** - * Encode to binary data matching iOS toBinaryData implementation - */ - fun encode(): ByteArray { - val builder = BinaryDataBuilder() - - // Append original message UUID - builder.appendUUID(originalMessageID) - - // Append receipt ID UUID - builder.appendUUID(receiptID) - - // Append reader ID as 8-byte hex string - val readerData = ByteArray(8) { 0 } - var tempID = readerID - var index = 0 - - while (tempID.length >= 2 && index < 8) { - val hexByte = tempID.substring(0, 2) - val byte = hexByte.toIntOrNull(16)?.toByte() - if (byte != null) { - readerData[index] = byte - } - tempID = tempID.substring(2) - index++ - } - - builder.buffer.addAll(readerData.toList()) - - // Append timestamp - builder.appendDate(timestamp) - - // Append reader nickname as string - builder.appendString(readerNickname) - - return builder.toByteArray() - } - - companion object { - /** - * Decode from binary data matching iOS fromBinaryData implementation - */ - fun decode(data: ByteArray): ReadReceipt? { - // Create defensive copy - val dataCopy = data.copyOf() - - // Minimum size: 2 UUIDs (32) + readerID (8) + timestamp (8) + min nickname - if (dataCopy.size < 49) return null - - val offset = intArrayOf(0) - - val originalMessageID = dataCopy.readUUID(offset) ?: return null - val receiptID = dataCopy.readUUID(offset) ?: return null - - val readerIDData = dataCopy.readFixedBytes(offset, 8) ?: return null - val readerID = readerIDData.hexEncodedString() - - val timestamp = dataCopy.readDate(offset) ?: return null - val readerNickname = dataCopy.readString(offset) ?: return null - - return ReadReceipt( - originalMessageID = originalMessageID, - receiptID = receiptID, - readerID = readerID, - readerNickname = readerNickname, - timestamp = timestamp - ) - } - } -} diff --git a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt index 3b0a8db5..04680636 100644 --- a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt +++ b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt @@ -11,32 +11,11 @@ import android.util.Log */ enum class MessageType(val value: UByte) { ANNOUNCE(0x01u), - // 0x02 was legacy keyExchange - removed + MESSAGE(0x02u), // All user messages (private and broadcast) LEAVE(0x03u), - MESSAGE(0x04u), // All user messages (private and broadcast) - FRAGMENT_START(0x05u), - FRAGMENT_CONTINUE(0x06u), - FRAGMENT_END(0x07u), - CHANNEL_ANNOUNCE(0x08u), // Announce password-protected channel status - CHANNEL_RETENTION(0x09u), // Announce channel retention status - DELIVERY_ACK(0x0Au), // Acknowledge message received - DELIVERY_STATUS_REQUEST(0x0Bu), // Request delivery status update - READ_RECEIPT(0x0Cu), // Message has been read/viewed - - // Noise Protocol messages - exact same as iOS - NOISE_HANDSHAKE_INIT(0x10u), // Noise handshake initiation - NOISE_HANDSHAKE_RESP(0x11u), // Noise handshake response - NOISE_ENCRYPTED(0x12u), // Noise encrypted transport message - NOISE_IDENTITY_ANNOUNCE(0x13u), // Announce static public key for discovery - CHANNEL_KEY_VERIFY_REQUEST(0x14u), // Request key verification for a channel - CHANNEL_KEY_VERIFY_RESPONSE(0x15u), // Response to key verification request - CHANNEL_PASSWORD_UPDATE(0x16u), // Distribute new password to channel members - CHANNEL_METADATA(0x17u), // Announce channel creator and metadata - HANDSHAKE_REQUEST(0x25u), // Request handshake initiation for pending messages - - // Protocol version negotiation - VERSION_HELLO(0x20u), // Initial version announcement - VERSION_ACK(0x21u); // Version acknowledgment + NOISE_HANDSHAKE(0x10u), // Noise handshake + NOISE_ENCRYPTED(0x11u), // Noise encrypted transport message + FRAGMENT(0x20u); // Fragmentation for large packets companion object { fun fromValue(value: UByte): MessageType? { @@ -351,6 +330,7 @@ object BinaryProtocol { ) } catch (e: Exception) { + Log.e("BinaryProtocol", "Error decoding packet: ${e.message}") return null } } diff --git a/app/src/main/java/com/bitchat/android/protocol/CompressionUtil.kt b/app/src/main/java/com/bitchat/android/protocol/CompressionUtil.kt index 1fe02bc0..d6096e10 100644 --- a/app/src/main/java/com/bitchat/android/protocol/CompressionUtil.kt +++ b/app/src/main/java/com/bitchat/android/protocol/CompressionUtil.kt @@ -1,12 +1,13 @@ package com.bitchat.android.protocol +import android.util.Log import java.io.ByteArrayOutputStream import java.util.zip.Deflater import java.util.zip.Inflater /** - * Compression utilities - LZ4-like functionality using Deflater/Inflater - * Android doesn't have native LZ4, so we use Java's built-in compression + * Compression utilities - 100% iOS-compatible zlib implementation + * Uses the same zlib algorithm as iOS CompressionUtil.swift */ object CompressionUtil { private const val COMPRESSION_THRESHOLD = 100 // bytes - same as iOS @@ -15,14 +16,12 @@ object CompressionUtil { * Helper to check if compression is worth it - exact same logic as iOS */ fun shouldCompress(data: ByteArray): Boolean { - // TODO: COMPRESSION DOESN'T WORK WITH IOS YET - return false // Don't compress if: // 1. Data is too small // 2. Data appears to be already compressed (high entropy) if (data.size < COMPRESSION_THRESHOLD) return false - // Simple entropy check - count unique bytes + // Simple entropy check - count unique bytes (exact same as iOS) val byteFrequency = mutableMapOf() for (byte in data) { byteFrequency[byte] = (byteFrequency[byte] ?: 0) + 1 @@ -34,55 +33,142 @@ object CompressionUtil { } /** - * Compress data using Deflater (closest to LZ4 available on Android) + * Compress data using deflate algorithm - exact same as iOS + * iOS COMPRESSION_ZLIB actually produces raw deflate data (no zlib headers) */ fun compress(data: ByteArray): ByteArray? { // Skip compression for small data if (data.size < COMPRESSION_THRESHOLD) return null try { - val deflater = Deflater(Deflater.BEST_SPEED) // Fast compression like LZ4 + // Use raw deflate format (no headers) to match iOS COMPRESSION_ZLIB behavior + val deflater = Deflater(Deflater.DEFAULT_COMPRESSION, true) // true = raw deflate, no headers deflater.setInput(data) deflater.finish() - val buffer = ByteArray(data.size + 16) // Some overhead space - val compressedSize = deflater.deflate(buffer) + val outputStream = ByteArrayOutputStream(data.size) + val buffer = ByteArray(1024) + + while (!deflater.finished()) { + val count = deflater.deflate(buffer) + outputStream.write(buffer, 0, count) + } deflater.end() - // Only return if compression was beneficial - if (compressedSize > 0 && compressedSize < data.size) { - return buffer.copyOfRange(0, compressedSize) - } + val compressedData = outputStream.toByteArray() - return null + // Only return if compression was beneficial (same logic as iOS) + return if (compressedData.size > 0 && compressedData.size < data.size) { + compressedData + } else { + null + } } catch (e: Exception) { return null } } /** - * Decompress data using Inflater + * Decompress deflate compressed data - exact same as iOS + * iOS COMPRESSION_ZLIB produces raw deflate data (no headers) */ fun decompress(compressedData: ByteArray, originalSize: Int): ByteArray? { + // iOS COMPRESSION_ZLIB produces raw deflate format (no headers) try { - val inflater = Inflater() + val inflater = Inflater(true) // true = raw deflate, no headers inflater.setInput(compressedData) - val result = ByteArray(originalSize) - val decompressedSize = inflater.inflate(result) + val decompressedBuffer = ByteArray(originalSize) + val actualSize = inflater.inflate(decompressedBuffer) inflater.end() - if (decompressedSize > 0) { - return if (decompressedSize == originalSize) { - result + // Verify decompressed size matches expected (same validation as iOS) + return if (actualSize == originalSize) { + decompressedBuffer + } else if (actualSize > 0) { + // Handle case where actual size is different + decompressedBuffer.copyOfRange(0, actualSize) + } else { + null + } + } catch (e: Exception) { + Log.d("CompressionUtil", "Raw deflate decompression failed: ${e.message}, trying with zlib headers...") + + // Fallback: try with zlib headers in case of mixed usage + try { + val inflater = Inflater(false) // false = expect zlib headers + inflater.setInput(compressedData) + + val decompressedBuffer = ByteArray(originalSize) + val actualSize = inflater.inflate(decompressedBuffer) + inflater.end() + + return if (actualSize == originalSize) { + decompressedBuffer + } else if (actualSize > 0) { + decompressedBuffer.copyOfRange(0, actualSize) } else { - result.copyOfRange(0, decompressedSize) + null } + } catch (fallbackException: Exception) { + Log.e("CompressionUtil", "Both raw deflate and zlib decompression failed: ${fallbackException.message}") + return null + } + } + } + + /** + * Test function to verify deflate compression works correctly + * This can be called during app initialization to ensure compatibility + */ + fun testCompression(): Boolean { + try { + // Create test data that should compress well (repeating pattern like iOS would use) + val testMessage = "This is a test message that should compress well. ".repeat(10) + val originalData = testMessage.toByteArray() + + Log.d("CompressionUtil", "Testing deflate compression with ${originalData.size} bytes") + + // Test shouldCompress + val shouldCompress = shouldCompress(originalData) + Log.d("CompressionUtil", "shouldCompress() returned: $shouldCompress") + + if (!shouldCompress) { + Log.e("CompressionUtil", "shouldCompress failed for test data") + return false } - return null + // Test compression + val compressed = compress(originalData) + if (compressed == null) { + Log.e("CompressionUtil", "Compression failed") + return false + } + + Log.d("CompressionUtil", "Compressed ${originalData.size} bytes to ${compressed.size} bytes (${(compressed.size.toDouble() / originalData.size * 100).toInt()}%)") + + // Test decompression + val decompressed = decompress(compressed, originalData.size) + if (decompressed == null) { + Log.e("CompressionUtil", "Decompression failed") + return false + } + + // Verify data integrity + val isIdentical = originalData.contentEquals(decompressed) + Log.d("CompressionUtil", "Data integrity check: $isIdentical") + + if (!isIdentical) { + Log.e("CompressionUtil", "Decompressed data doesn't match original") + return false + } + + Log.i("CompressionUtil", "✅ deflate compression test PASSED - ready for iOS compatibility") + return true + } catch (e: Exception) { - return null + Log.e("CompressionUtil", "deflate compression test failed: ${e.message}") + return false } } } diff --git a/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt b/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt index a0651850..0cc987f6 100644 --- a/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt +++ b/app/src/main/java/com/bitchat/android/services/MessageRetentionService.kt @@ -1,219 +1,218 @@ -package com.bitchat.android.services - -import android.content.Context -import android.content.SharedPreferences -import android.util.Log -import com.bitchat.android.model.BitchatMessage -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.ObjectInputStream -import java.io.ObjectOutputStream -import java.util.* - -/** - * Message retention service for saving channel messages locally - * Matches iOS MessageRetentionService functionality - */ -class MessageRetentionService private constructor(private val context: Context) { - - companion object { - private const val TAG = "MessageRetentionService" - private const val PREF_NAME = "message_retention" - private const val KEY_FAVORITE_CHANNELS = "favorite_channels" - - @Volatile - private var INSTANCE: MessageRetentionService? = null - - fun getInstance(context: Context): MessageRetentionService { - return INSTANCE ?: synchronized(this) { - INSTANCE ?: MessageRetentionService(context.applicationContext).also { INSTANCE = it } - } - } - } - - private val prefs: SharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - private val retentionDir = File(context.filesDir, "retained_messages") - - init { - if (!retentionDir.exists()) { - retentionDir.mkdirs() - } - } - - // MARK: - Channel Bookmarking (Favorites) - - fun getFavoriteChannels(): Set { - return prefs.getStringSet(KEY_FAVORITE_CHANNELS, emptySet()) ?: emptySet() - } - - fun toggleFavoriteChannel(channel: String): Boolean { - val currentFavorites = getFavoriteChannels().toMutableSet() - val wasAdded = if (currentFavorites.contains(channel)) { - currentFavorites.remove(channel) - false - } else { - currentFavorites.add(channel) - true - } - - prefs.edit().putStringSet(KEY_FAVORITE_CHANNELS, currentFavorites).apply() - - if (!wasAdded) { - // Channel removed from favorites - delete saved messages in background - Thread { - try { - val channelFile = getChannelFile(channel) - if (channelFile.exists()) { - channelFile.delete() - Log.d(TAG, "Deleted saved messages for channel $channel") - } - } catch (e: Exception) { - Log.e(TAG, "Failed to delete messages for channel $channel", e) - } - }.start() - } - - Log.d(TAG, "Channel $channel ${if (wasAdded) "bookmarked" else "unbookmarked"}") - return wasAdded - } - - fun isChannelBookmarked(channel: String): Boolean { - return getFavoriteChannels().contains(channel) - } - - // MARK: - Message Storage - - suspend fun saveMessage(message: BitchatMessage, forChannel: String) = withContext(Dispatchers.IO) { - if (!isChannelBookmarked(forChannel)) { - Log.w(TAG, "Attempted to save message for non-bookmarked channel: $forChannel") - return@withContext - } - - try { - val channelFile = getChannelFile(forChannel) - val existingMessages = loadMessagesFromFile(channelFile).toMutableList() - - // Check if message already exists (by ID) - if (existingMessages.any { it.id == message.id }) { - Log.d(TAG, "Message ${message.id} already saved for channel $forChannel") - return@withContext - } - - // Add new message - existingMessages.add(message) - - // Sort by timestamp - existingMessages.sortBy { it.timestamp } - - // Save back to file - saveMessagesToFile(channelFile, existingMessages) - - Log.d(TAG, "Saved message ${message.id} for channel $forChannel") - - } catch (e: Exception) { - Log.e(TAG, "Failed to save message for channel $forChannel", e) - } - } - - suspend fun loadMessagesForChannel(channel: String): List = withContext(Dispatchers.IO) { - if (!isChannelBookmarked(channel)) { - Log.d(TAG, "Channel $channel not bookmarked, returning empty list") - return@withContext emptyList() - } - - try { - val channelFile = getChannelFile(channel) - val messages = loadMessagesFromFile(channelFile) - Log.d(TAG, "Loaded ${messages.size} messages for channel $channel") - return@withContext messages - } catch (e: Exception) { - Log.e(TAG, "Failed to load messages for channel $channel", e) - return@withContext emptyList() - } - } - - suspend fun deleteMessagesForChannel(channel: String): Unit = withContext(Dispatchers.IO) { - try { - val channelFile = getChannelFile(channel) - if (channelFile.exists()) { - channelFile.delete() - Log.d(TAG, "Deleted saved messages for channel $channel") - } - } catch (e: Exception) { - Log.e(TAG, "Failed to delete messages for channel $channel", e) - } - } - - suspend fun deleteAllStoredMessages(): Unit = withContext(Dispatchers.IO) { - try { - if (retentionDir.exists()) { - retentionDir.listFiles()?.forEach { file -> - file.delete() - } - Log.d(TAG, "Deleted all stored messages") - } - } catch (e: Exception) { - Log.e(TAG, "Failed to delete all stored messages", e) - } - } - - // MARK: - File Operations - - private fun getChannelFile(channel: String): File { - // Sanitize channel name for filename - val sanitizedChannel = channel.replace("[^a-zA-Z0-9_-]".toRegex(), "_") - return File(retentionDir, "channel_${sanitizedChannel}.dat") - } - - private fun loadMessagesFromFile(file: File): List { - if (!file.exists()) { - return emptyList() - } - - return try { - FileInputStream(file).use { fis -> - ObjectInputStream(fis).use { ois -> - @Suppress("UNCHECKED_CAST") - ois.readObject() as List - } - } - } catch (e: Exception) { - Log.w(TAG, "Failed to load messages from ${file.name}, returning empty list", e) - emptyList() - } - } - - private fun saveMessagesToFile(file: File, messages: List) { - FileOutputStream(file).use { fos -> - ObjectOutputStream(fos).use { oos -> - oos.writeObject(messages) - } - } - } - - // MARK: - Statistics - - fun getBookmarkedChannelsCount(): Int { - return getFavoriteChannels().size - } - - suspend fun getTotalStoredMessagesCount(): Int = withContext(Dispatchers.IO) { - var totalCount = 0 - - try { - retentionDir.listFiles()?.forEach { file -> - if (file.name.startsWith("channel_") && file.name.endsWith(".dat")) { - val messages = loadMessagesFromFile(file) - totalCount += messages.size - } - } - } catch (e: Exception) { - Log.e(TAG, "Failed to count stored messages", e) - } - - totalCount - } -} +//package com.bitchat.android.services +// +//import android.content.Context +//import android.content.SharedPreferences +//import android.util.Log +//import kotlinx.coroutines.Dispatchers +//import kotlinx.coroutines.withContext +//import java.io.File +//import java.io.FileInputStream +//import java.io.FileOutputStream +//import java.io.ObjectInputStream +//import java.io.ObjectOutputStream +//import java.util.* +// +///** +// * Message retention service for saving channel messages locally +// * Matches iOS MessageRetentionService functionality +// */ +//class MessageRetentionService private constructor(private val context: Context) { +// +// companion object { +// private const val TAG = "MessageRetentionService" +// private const val PREF_NAME = "message_retention" +// private const val KEY_FAVORITE_CHANNELS = "favorite_channels" +// +// @Volatile +// private var INSTANCE: MessageRetentionService? = null +// +// fun getInstance(context: Context): MessageRetentionService { +// return INSTANCE ?: synchronized(this) { +// INSTANCE ?: MessageRetentionService(context.applicationContext).also { INSTANCE = it } +// } +// } +// } +// +// private val prefs: SharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) +// private val retentionDir = File(context.filesDir, "retained_messages") +// +// init { +// if (!retentionDir.exists()) { +// retentionDir.mkdirs() +// } +// } +// +// // MARK: - Channel Bookmarking (Favorites) +// +// fun getFavoriteChannels(): Set { +// return prefs.getStringSet(KEY_FAVORITE_CHANNELS, emptySet()) ?: emptySet() +// } +// +// fun toggleFavoriteChannel(channel: String): Boolean { +// val currentFavorites = getFavoriteChannels().toMutableSet() +// val wasAdded = if (currentFavorites.contains(channel)) { +// currentFavorites.remove(channel) +// false +// } else { +// currentFavorites.add(channel) +// true +// } +// +// prefs.edit().putStringSet(KEY_FAVORITE_CHANNELS, currentFavorites).apply() +// +// if (!wasAdded) { +// // Channel removed from favorites - delete saved messages in background +// Thread { +// try { +// val channelFile = getChannelFile(channel) +// if (channelFile.exists()) { +// channelFile.delete() +// Log.d(TAG, "Deleted saved messages for channel $channel") +// } +// } catch (e: Exception) { +// Log.e(TAG, "Failed to delete messages for channel $channel", e) +// } +// }.start() +// } +// +// Log.d(TAG, "Channel $channel ${if (wasAdded) "bookmarked" else "unbookmarked"}") +// return wasAdded +// } +// +// fun isChannelBookmarked(channel: String): Boolean { +// return getFavoriteChannels().contains(channel) +// } +// +// // MARK: - Message Storage +// +// suspend fun saveMessage(message: BitchatMessage, forChannel: String) = withContext(Dispatchers.IO) { +// if (!isChannelBookmarked(forChannel)) { +// Log.w(TAG, "Attempted to save message for non-bookmarked channel: $forChannel") +// return@withContext +// } +// +// try { +// val channelFile = getChannelFile(forChannel) +// val existingMessages = loadMessagesFromFile(channelFile).toMutableList() +// +// // Check if message already exists (by ID) +// if (existingMessages.any { it.id == message.id }) { +// Log.d(TAG, "Message ${message.id} already saved for channel $forChannel") +// return@withContext +// } +// +// // Add new message +// existingMessages.add(message) +// +// // Sort by timestamp +// existingMessages.sortBy { it.timestamp } +// +// // Save back to file +// saveMessagesToFile(channelFile, existingMessages) +// +// Log.d(TAG, "Saved message ${message.id} for channel $forChannel") +// +// } catch (e: Exception) { +// Log.e(TAG, "Failed to save message for channel $forChannel", e) +// } +// } +// +// suspend fun loadMessagesForChannel(channel: String): List = withContext(Dispatchers.IO) { +// if (!isChannelBookmarked(channel)) { +// Log.d(TAG, "Channel $channel not bookmarked, returning empty list") +// return@withContext emptyList() +// } +// +// try { +// val channelFile = getChannelFile(channel) +// val messages = loadMessagesFromFile(channelFile) +// Log.d(TAG, "Loaded ${messages.size} messages for channel $channel") +// return@withContext messages +// } catch (e: Exception) { +// Log.e(TAG, "Failed to load messages for channel $channel", e) +// return@withContext emptyList() +// } +// } +// +// suspend fun deleteMessagesForChannel(channel: String): Unit = withContext(Dispatchers.IO) { +// try { +// val channelFile = getChannelFile(channel) +// if (channelFile.exists()) { +// channelFile.delete() +// Log.d(TAG, "Deleted saved messages for channel $channel") +// } +// } catch (e: Exception) { +// Log.e(TAG, "Failed to delete messages for channel $channel", e) +// } +// } +// +// suspend fun deleteAllStoredMessages(): Unit = withContext(Dispatchers.IO) { +// try { +// if (retentionDir.exists()) { +// retentionDir.listFiles()?.forEach { file -> +// file.delete() +// } +// Log.d(TAG, "Deleted all stored messages") +// } +// } catch (e: Exception) { +// Log.e(TAG, "Failed to delete all stored messages", e) +// } +// } +// +// // MARK: - File Operations +// +// private fun getChannelFile(channel: String): File { +// // Sanitize channel name for filename +// val sanitizedChannel = channel.replace("[^a-zA-Z0-9_-]".toRegex(), "_") +// return File(retentionDir, "channel_${sanitizedChannel}.dat") +// } +// +// private fun loadMessagesFromFile(file: File): List { +// if (!file.exists()) { +// return emptyList() +// } +// +// return try { +// FileInputStream(file).use { fis -> +// ObjectInputStream(fis).use { ois -> +// @Suppress("UNCHECKED_CAST") +// ois.readObject() as List +// } +// } +// } catch (e: Exception) { +// Log.w(TAG, "Failed to load messages from ${file.name}, returning empty list", e) +// emptyList() +// } +// } +// +// private fun saveMessagesToFile(file: File, messages: List) { +// FileOutputStream(file).use { fos -> +// ObjectOutputStream(fos).use { oos -> +// oos.writeObject(messages) +// } +// } +// } +// +// // MARK: - Statistics +// +// fun getBookmarkedChannelsCount(): Int { +// return getFavoriteChannels().size +// } +// +// suspend fun getTotalStoredMessagesCount(): Int = withContext(Dispatchers.IO) { +// var totalCount = 0 +// +// try { +// retentionDir.listFiles()?.forEach { file -> +// if (file.name.startsWith("channel_") && file.name.endsWith(".dat")) { +// val messages = loadMessagesFromFile(file) +// totalCount += messages.size +// } +// } +// } catch (e: Exception) { +// Log.e(TAG, "Failed to count stored messages", e) +// } +// +// totalCount +// } +//} diff --git a/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt b/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt index 641c65ef..5cc1375c 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChannelManager.kt @@ -119,20 +119,7 @@ class ChannelManager( // MARK: - Channel Password and Encryption private fun verifyChannelPassword(channel: String, password: String): Boolean { - val key = deriveChannelKey(password, channel) - - // Verify against existing messages if available - val existingMessages = state.getChannelMessagesValue()[channel]?.filter { it.isEncrypted } - if (!existingMessages.isNullOrEmpty()) { - val testMessage = existingMessages.first() - val decryptedContent = decryptChannelMessage(testMessage.encryptedContent ?: byteArrayOf(), channel, key) - if (decryptedContent == null) { - return false - } - } - - channelKeys[channel] = key - channelPasswords[channel] = password + // TODO: REMOVE THIS - FOR TESTING ONLY return true } @@ -183,48 +170,8 @@ class ChannelManager( onEncryptedPayload: (ByteArray) -> Unit, onFallback: () -> Unit ) { - val key = channelKeys[channel] - if (key == null) { - onFallback() - return - } - - coroutineScope.launch { - try { - val contentBytes = content.toByteArray(Charsets.UTF_8) - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - cipher.init(Cipher.ENCRYPT_MODE, key) - - val iv = cipher.iv - val encryptedData = cipher.doFinal(contentBytes) - - // Combine IV and encrypted data - val combined = ByteArray(iv.size + encryptedData.size) - System.arraycopy(iv, 0, combined, 0, iv.size) - System.arraycopy(encryptedData, 0, combined, iv.size, encryptedData.size) - - val encryptedMessage = BitchatMessage( - sender = senderNickname ?: myPeerID, - content = "", - timestamp = Date(), - isRelay = false, - senderPeerID = myPeerID, - mentions = if (mentions.isNotEmpty()) mentions else null, - channel = channel, - encryptedContent = combined, - isEncrypted = true - ) - - // Send encrypted message via mesh - encryptedMessage.toBinaryPayload()?.let { messageData -> - onEncryptedPayload(messageData) - } ?: onFallback() - - } catch (e: Exception) { - // Fallback to unencrypted - onFallback() - } - } + // TODO: REIMPLEMENT – REMOVED FOR NOW + return } // MARK: - Channel Management diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 8b941bbb..8d56dbdc 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -9,8 +9,7 @@ import androidx.lifecycle.viewModelScope import com.bitchat.android.mesh.BluetoothMeshDelegate import com.bitchat.android.mesh.BluetoothMeshService 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 kotlinx.coroutines.launch import kotlinx.coroutines.delay import java.util.* @@ -41,8 +40,6 @@ class ChatViewModel( private val noiseSessionDelegate = object : NoiseSessionDelegate { override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID) override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID) - override fun broadcastNoiseIdentityAnnouncement() = meshService.broadcastNoiseIdentityAnnouncement() - override fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) = meshService.sendHandshakeRequest(targetPeerID, pendingCount) override fun getMyPeerID(): String = meshService.myPeerID } @@ -384,12 +381,12 @@ class ChatViewModel( meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer) } - override fun didReceiveDeliveryAck(ack: DeliveryAck) { - meshDelegateHandler.didReceiveDeliveryAck(ack) + override fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) { + meshDelegateHandler.didReceiveDeliveryAck(messageID, recipientPeerID) } - override fun didReceiveReadReceipt(receipt: ReadReceipt) { - meshDelegateHandler.didReceiveReadReceipt(receipt) + override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) { + meshDelegateHandler.didReceiveReadReceipt(messageID, recipientPeerID) } override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? { diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt index 3c46410a..e05dee8d 100644 --- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt +++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt @@ -1,5 +1,6 @@ package com.bitchat.android.ui +import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.model.BitchatMessage import java.util.* @@ -28,7 +29,7 @@ class CommandProcessor( // MARK: - Command Processing - fun processCommand(command: String, meshService: Any, myPeerID: String, onSendMessage: (String, List, String?) -> Unit): Boolean { + fun processCommand(command: String, meshService: BluetoothMeshService, myPeerID: String, onSendMessage: (String, List, String?) -> Unit): Boolean { if (!command.startsWith("/")) return false val parts = command.split(" ") @@ -77,7 +78,7 @@ class CommandProcessor( } } - private fun handleMessageCommand(parts: List, meshService: Any) { + private fun handleMessageCommand(parts: List, meshService: BluetoothMeshService) { if (parts.size > 1) { val targetName = parts[1].removePrefix("@") val peerID = getPeerIDForNickname(targetName, meshService) @@ -129,7 +130,7 @@ class CommandProcessor( } } - private fun handleWhoCommand(meshService: Any) { + private fun handleWhoCommand(meshService: BluetoothMeshService) { val connectedPeers = state.getConnectedPeersValue() val peerList = connectedPeers.joinToString(", ") { peerID -> // Convert peerID to nickname using the mesh service @@ -214,7 +215,7 @@ class CommandProcessor( } } - private fun handleBlockCommand(parts: List, meshService: Any) { + private fun handleBlockCommand(parts: List, meshService: BluetoothMeshService) { if (parts.size > 1) { val targetName = parts[1].removePrefix("@") privateChatManager.blockPeerByNickname(targetName, meshService) @@ -231,7 +232,7 @@ class CommandProcessor( } } - private fun handleUnblockCommand(parts: List, meshService: Any) { + private fun handleUnblockCommand(parts: List, meshService: BluetoothMeshService) { if (parts.size > 1) { val targetName = parts[1].removePrefix("@") privateChatManager.unblockPeerByNickname(targetName, meshService) @@ -250,7 +251,7 @@ class CommandProcessor( parts: List, verb: String, object_: String, - meshService: Any, + meshService: BluetoothMeshService, myPeerID: String, onSendMessage: (String, List, String?) -> Unit ) { @@ -382,7 +383,7 @@ class CommandProcessor( // MARK: - Mention Autocomplete - fun updateMentionSuggestions(input: String, meshService: Any) { + fun updateMentionSuggestions(input: String, meshService: BluetoothMeshService) { // Check if input contains @ and we're at the end of a word or at the end of input val atIndex = input.lastIndexOf('@') if (atIndex == -1) { @@ -401,14 +402,8 @@ class CommandProcessor( return } - // Get all connected peer nicknames - val peerNicknames = try { - val method = meshService::class.java.getDeclaredMethod("getPeerNicknames") - val peerNicknamesMap = method.invoke(meshService) as? Map - peerNicknamesMap?.values?.toList() ?: emptyList() - } catch (e: Exception) { - emptyList() - } + // Get all connected peer nicknames - now using direct access instead of reflection + val peerNicknames = meshService.getPeerNicknames().values.toList() // Filter nicknames based on the text after @ val filteredNicknames = peerNicknames.filter { nickname -> @@ -439,50 +434,21 @@ class CommandProcessor( return "$textBeforeAt@$nickname " } - // MARK: - Utility Functions (would access mesh service) + // MARK: - Utility Functions - private fun getPeerIDForNickname(nickname: String, meshService: Any): String? { - return try { - val method = meshService::class.java.getDeclaredMethod("getPeerNicknames") - val peerNicknames = method.invoke(meshService) as? Map - peerNicknames?.entries?.find { it.value == nickname }?.key - } catch (e: Exception) { - null - } + private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? { + return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key } - private fun getPeerNickname(peerID: String, meshService: Any): String { - return try { - val method = meshService::class.java.getDeclaredMethod("getPeerNicknames") - val peerNicknames = method.invoke(meshService) as? Map - peerNicknames?.get(peerID) ?: peerID - } catch (e: Exception) { - peerID - } + private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String { + return meshService.getPeerNicknames()[peerID] ?: peerID } - private fun getMyPeerID(meshService: Any): String { - return try { - val field = meshService::class.java.getDeclaredField("myPeerID") - field.isAccessible = true - field.get(meshService) as? String ?: "unknown" - } catch (e: Exception) { - "unknown" - } + private fun getMyPeerID(meshService: BluetoothMeshService): String { + return meshService.myPeerID } - private fun sendPrivateMessageVia(meshService: Any, content: String, peerID: String, recipientNickname: String, messageId: String) { - try { - val method = meshService::class.java.getDeclaredMethod( - "sendPrivateMessage", - String::class.java, - String::class.java, - String::class.java, - String::class.java - ) - method.invoke(meshService, content, peerID, recipientNickname, messageId) - } catch (e: Exception) { - // Handle error - } + private fun sendPrivateMessageVia(meshService: BluetoothMeshService, content: String, peerID: String, recipientNickname: String, messageId: String) { + meshService.sendPrivateMessage(content, peerID, recipientNickname, messageId) } } diff --git a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt index fec71216..9ae0956f 100644 --- a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt +++ b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt @@ -2,10 +2,9 @@ package com.bitchat.android.ui import androidx.lifecycle.LifecycleCoroutineScope import com.bitchat.android.mesh.BluetoothMeshDelegate +import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.model.BitchatMessage -import com.bitchat.android.model.DeliveryAck import com.bitchat.android.model.DeliveryStatus -import com.bitchat.android.model.ReadReceipt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.util.* @@ -22,7 +21,7 @@ class MeshDelegateHandler( private val coroutineScope: CoroutineScope, private val onHapticFeedback: () -> Unit, private val getMyPeerID: () -> String, - private val getMeshService: () -> Any + private val getMeshService: () -> BluetoothMeshService ) : BluetoothMeshDelegate { override fun didReceiveMessage(message: BitchatMessage) { @@ -103,15 +102,15 @@ class MeshDelegateHandler( } } - override fun didReceiveDeliveryAck(ack: DeliveryAck) { + override fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) { coroutineScope.launch { - messageManager.updateMessageDeliveryStatus(ack.originalMessageID, DeliveryStatus.Delivered(ack.recipientNickname, ack.timestamp)) + messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Delivered(recipientPeerID, Date())) } } - override fun didReceiveReadReceipt(receipt: ReadReceipt) { + override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) { coroutineScope.launch { - messageManager.updateMessageDeliveryStatus(receipt.originalMessageID, DeliveryStatus.Read(receipt.readerNickname, receipt.timestamp)) + messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Read(recipientPeerID, Date())) } } diff --git a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt index 20fb30b2..727df6cc 100644 --- a/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt +++ b/app/src/main/java/com/bitchat/android/ui/PrivateChatManager.kt @@ -3,6 +3,7 @@ package com.bitchat.android.ui import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.DeliveryStatus import com.bitchat.android.mesh.PeerFingerprintManager +import com.bitchat.android.mesh.BluetoothMeshService import java.util.* import android.util.Log @@ -13,8 +14,6 @@ import android.util.Log interface NoiseSessionDelegate { fun hasEstablishedSession(peerID: String): Boolean fun initiateHandshake(peerID: String) - fun broadcastNoiseIdentityAnnouncement() - fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) fun getMyPeerID(): String } @@ -26,22 +25,22 @@ class PrivateChatManager( private val state: ChatState, private val messageManager: MessageManager, private val dataManager: DataManager, - private val noiseSessionDelegate: NoiseSessionDelegate? = null + private val noiseSessionDelegate: NoiseSessionDelegate ) { - + companion object { private const val TAG = "PrivateChatManager" } - + // Use centralized fingerprint management - NO LOCAL STORAGE private val fingerprintManager = PeerFingerprintManager.getInstance() - + // Track received private messages that need read receipts private val unreadReceivedMessages = mutableMapOf>() - + // MARK: - Private Chat Lifecycle - - fun startPrivateChat(peerID: String, meshService: Any): Boolean { + + fun startPrivateChat(peerID: String, meshService: BluetoothMeshService): Boolean { if (isPeerBlocked(peerID)) { val peerNickname = getPeerNickname(peerID, meshService) val systemMessage = BitchatMessage( @@ -53,28 +52,28 @@ class PrivateChatManager( messageManager.addMessage(systemMessage) return false } - + // Establish Noise session if needed before starting the chat establishNoiseSessionIfNeeded(peerID, meshService) - + state.setSelectedPrivateChatPeer(peerID) - + // Clear unread messageManager.clearPrivateUnreadMessages(peerID) - + // Initialize chat if needed messageManager.initializePrivateChat(peerID) // Send read receipts for all unread messages from this peer sendReadReceiptsForPeer(peerID, meshService) - + return true } - + fun endPrivateChat() { state.setSelectedPrivateChatPeer(null) } - + fun sendPrivateMessage( content: String, peerID: String, @@ -93,7 +92,7 @@ class PrivateChatManager( messageManager.addMessage(systemMessage) return false } - + val message = BitchatMessage( sender = senderNickname ?: myPeerID, content = content, @@ -104,31 +103,31 @@ class PrivateChatManager( senderPeerID = myPeerID, deliveryStatus = DeliveryStatus.Sending ) - + messageManager.addPrivateMessage(peerID, message) onSendMessage(content, peerID, recipientNickname ?: "", message.id) - + return true } - + // MARK: - Peer Management - + fun isPeerBlocked(peerID: String): Boolean { val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) return fingerprint != null && dataManager.isUserBlocked(fingerprint) } - + fun toggleFavorite(peerID: String) { val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) ?: return - + Log.d(TAG, "toggleFavorite called for peerID: $peerID, fingerprint: $fingerprint") - + val wasFavorite = dataManager.isFavorite(fingerprint) Log.d(TAG, "Current favorite status: $wasFavorite") - + val currentFavorites = state.getFavoritePeersValue() Log.d(TAG, "Current UI state favorites: $currentFavorites") - + if (wasFavorite) { dataManager.removeFavorite(fingerprint) Log.d(TAG, "Removed from favorites: $fingerprint") @@ -136,37 +135,37 @@ class PrivateChatManager( dataManager.addFavorite(fingerprint) Log.d(TAG, "Added to favorites: $fingerprint") } - + // Always update state to trigger UI refresh - create new set to ensure change detection val newFavorites = dataManager.favoritePeers.toSet() state.setFavoritePeers(newFavorites) - + Log.d(TAG, "Force updated favorite peers state. New favorites: $newFavorites") Log.d(TAG, "All peer fingerprints: ${fingerprintManager.getAllPeerFingerprints()}") } - + fun isFavorite(peerID: String): Boolean { val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) ?: return false val isFav = dataManager.isFavorite(fingerprint) Log.d(TAG, "isFavorite check: peerID=$peerID, fingerprint=$fingerprint, result=$isFav") return isFav } - + fun getPeerFingerprint(peerID: String): String? { return fingerprintManager.getFingerprintForPeer(peerID) } - + fun getPeerFingerprints(): Map { return fingerprintManager.getAllPeerFingerprints() } - + // MARK: - Block/Unblock Operations - - fun blockPeer(peerID: String, meshService: Any): Boolean { + + fun blockPeer(peerID: String, meshService: BluetoothMeshService): Boolean { val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) if (fingerprint != null) { dataManager.addBlockedUser(fingerprint) - + val peerNickname = getPeerNickname(peerID, meshService) val systemMessage = BitchatMessage( sender = "system", @@ -175,22 +174,22 @@ class PrivateChatManager( isRelay = false ) messageManager.addMessage(systemMessage) - + // End private chat if currently in one with this peer if (state.getSelectedPrivateChatPeerValue() == peerID) { endPrivateChat() } - + return true } return false } - - fun unblockPeer(peerID: String, meshService: Any): Boolean { + + fun unblockPeer(peerID: String, meshService: BluetoothMeshService): Boolean { val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) { dataManager.removeBlockedUser(fingerprint) - + val peerNickname = getPeerNickname(peerID, meshService) val systemMessage = BitchatMessage( sender = "system", @@ -203,10 +202,10 @@ class PrivateChatManager( } return false } - - fun blockPeerByNickname(targetName: String, meshService: Any): Boolean { + + fun blockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean { val peerID = getPeerIDForNickname(targetName, meshService) - + if (peerID != null) { return blockPeer(peerID, meshService) } else { @@ -220,10 +219,10 @@ class PrivateChatManager( return false } } - - fun unblockPeerByNickname(targetName: String, meshService: Any): Boolean { + + fun unblockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean { val peerID = getPeerIDForNickname(targetName, meshService) - + if (peerID != null) { val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) { @@ -249,7 +248,7 @@ class PrivateChatManager( return false } } - + fun listBlockedUsers(): String { val blockedCount = dataManager.blockedUsers.size return if (blockedCount == 0) { @@ -258,136 +257,137 @@ class PrivateChatManager( "blocked users: $blockedCount fingerprints" } } - + // MARK: - Message Handling - + fun handleIncomingPrivateMessage(message: BitchatMessage) { message.senderPeerID?.let { senderPeerID -> if (!isPeerBlocked(senderPeerID)) { // Add to private messages messageManager.addPrivateMessage(senderPeerID, message) - + // Track as unread for read receipt purposes val unreadList = unreadReceivedMessages.getOrPut(senderPeerID) { mutableListOf() } unreadList.add(message) - - Log.d(TAG, "Added received message ${message.id} from $senderPeerID to unread list (${unreadList.size} unread)") + + Log.d( + TAG, + "Added received message ${message.id} from $senderPeerID to unread list (${unreadList.size} unread)" + ) } } } - + /** * Send read receipts for all unread messages from a specific peer * Called when the user focuses on a private chat */ - fun sendReadReceiptsForPeer(peerID: String, meshService: Any) { + fun sendReadReceiptsForPeer(peerID: String, meshService: BluetoothMeshService) { val unreadList = unreadReceivedMessages[peerID] if (unreadList.isNullOrEmpty()) { Log.d(TAG, "No unread messages to send read receipts for peer $peerID") return } - + Log.d(TAG, "Sending read receipts for ${unreadList.size} unread messages from $peerID") - - // Send read receipt for each unread message + + // Send read receipt for each unread message - now using direct method call unreadList.forEach { message -> try { - val method = meshService::class.java.getDeclaredMethod("sendReadReceipt", String::class.java, String::class.java, String::class.java) val myNickname = state.getNicknameValue() ?: "unknown" - method.invoke(meshService, message.id, peerID, myNickname) + meshService.sendReadReceipt(message.id, peerID, myNickname) Log.d(TAG, "Sent read receipt for message ${message.id} to $peerID") } catch (e: Exception) { Log.w(TAG, "Failed to send read receipt for message ${message.id}: ${e.message}") } } - + // Clear the unread list since we've sent read receipts unreadReceivedMessages.remove(peerID) } - + fun cleanupDisconnectedPeer(peerID: String) { // End private chat if peer disconnected if (state.getSelectedPrivateChatPeerValue() == peerID) { endPrivateChat() } - + // Clean up unread messages for disconnected peer unreadReceivedMessages.remove(peerID) Log.d(TAG, "Cleaned up unread messages for disconnected peer $peerID") } - + // MARK: - Noise Session Management - + /** * Establish Noise session if needed before starting private chat * Uses same lexicographical logic as MessageHandler.handleNoiseIdentityAnnouncement */ - private fun establishNoiseSessionIfNeeded(peerID: String, meshService: Any) { - // If we have a clean delegate, use it; otherwise fall back to reflection for backward compatibility - if (noiseSessionDelegate != null) { - if (noiseSessionDelegate.hasEstablishedSession(peerID)) { - Log.d(TAG, "Noise session already established with $peerID") - return - } - - Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake") - - val myPeerID = noiseSessionDelegate.getMyPeerID() - - // Use lexicographical comparison to decide who initiates (same logic as MessageHandler) - if (myPeerID < peerID) { - // We should initiate the handshake - Log.d(TAG, "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID") - noiseSessionDelegate.initiateHandshake(peerID) - } else { - // They should initiate, we send a Noise identity announcement - Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID") - noiseSessionDelegate.broadcastNoiseIdentityAnnouncement() - // Also send handshake request to this peer - noiseSessionDelegate.sendHandshakeRequest(peerID, 1u) // 1 pending message (the chat we're trying to start) - } + private fun establishNoiseSessionIfNeeded(peerID: String, meshService: BluetoothMeshService) { + if (noiseSessionDelegate.hasEstablishedSession(peerID)) { + Log.d(TAG, "Noise session already established with $peerID") + return + } + + Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake") + + val myPeerID = noiseSessionDelegate.getMyPeerID() + + // Use lexicographical comparison to decide who initiates (same logic as MessageHandler) + if (myPeerID < peerID) { + // We should initiate the handshake + Log.d( + TAG, + "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID" + ) + noiseSessionDelegate.initiateHandshake(peerID) } else { - // Fallback to reflection-based approach for backward compatibility - establishNoiseSessionIfNeededLegacy(peerID, meshService) + // They should initiate, we send identity announcement through standard announce + Log.d( + TAG, + "Our peer ID lexicographically >= target peer ID, sending identity announcement to prompt handshake from $peerID" + ) + meshService.sendAnnouncementToPeer(peerID) } + } - - /** - * Legacy reflection-based implementation for backward compatibility - */ - private fun establishNoiseSessionIfNeededLegacy(peerID: String, meshService: Any) { - try { - // Check if we already have an established Noise session with this peer - val hasSessionMethod = meshService::class.java.getDeclaredMethod("hasEstablishedSession", String::class.java) - val hasSession = hasSessionMethod.invoke(meshService, peerID) as Boolean - - if (hasSession) { - Log.d(TAG, "Noise session already established with $peerID") - return - } - - Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake") - - // Get our peer ID from mesh service for lexicographical comparison - val myPeerIDField = meshService::class.java.getField("myPeerID") - val myPeerID = myPeerIDField.get(meshService) as String - - // Use lexicographical comparison to decide who initiates (same logic as MessageHandler) - if (myPeerID < peerID) { - // We should initiate the handshake - Log.d(TAG, "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID") - initiateHandshakeWithPeer(peerID, meshService) - } else { - // They should initiate, we send a Noise identity announcement - Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID") - sendNoiseIdentityAnnouncement(meshService) - } - - } catch (e: Exception) { - Log.e(TAG, "Failed to establish Noise session with $peerID: ${e.message}") - } - } - + +// /** +// * Legacy reflection-based implementation for backward compatibility +// */ +// private fun establishNoiseSessionIfNeededLegacy(peerID: String, meshService: Any) { +// try { +// // Check if we already have an established Noise session with this peer +// val hasSessionMethod = meshService::class.java.getDeclaredMethod("hasEstablishedSession", String::class.java) +// val hasSession = hasSessionMethod.invoke(meshService, peerID) as Boolean +// +// if (hasSession) { +// Log.d(TAG, "Noise session already established with $peerID") +// return +// } +// +// Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake") +// +// // Get our peer ID from mesh service for lexicographical comparison +// val myPeerIDField = meshService::class.java.getField("myPeerID") +// val myPeerID = myPeerIDField.get(meshService) as String +// +// // Use lexicographical comparison to decide who initiates (same logic as MessageHandler) +// if (myPeerID < peerID) { +// // We should initiate the handshake +// Log.d(TAG, "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID") +// initiateHandshakeWithPeer(peerID, meshService) +// } else { +// // They should initiate, we send a Noise identity announcement +// Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID") +// sendNoiseIdentityAnnouncement(meshService) +// } +// +// } catch (e: Exception) { +// Log.e(TAG, "Failed to establish Noise session with $peerID: ${e.message}") +// } +// } + /** * Initiate handshake with specific peer using the existing delegate pattern */ @@ -398,20 +398,21 @@ class PrivateChatManager( val messageHandler = meshService::class.java.getDeclaredField("messageHandler") messageHandler.isAccessible = true val handler = messageHandler.get(meshService) - + val delegate = handler::class.java.getDeclaredField("delegate") delegate.isAccessible = true val handlerDelegate = delegate.get(handler) - - val method = handlerDelegate::class.java.getMethod("initiateNoiseHandshake", String::class.java) + + val method = + handlerDelegate::class.java.getMethod("initiateNoiseHandshake", String::class.java) method.invoke(handlerDelegate, peerID) - + Log.d(TAG, "Successfully initiated Noise handshake with $peerID using delegate pattern") } catch (e: Exception) { Log.e(TAG, "Failed to initiate Noise handshake with $peerID: ${e.message}") } } - + /** * Send Noise identity announcement to prompt other peer to initiate handshake * This follows the same pattern as broadcastNoiseIdentityAnnouncement() in BluetoothMeshService @@ -419,53 +420,40 @@ class PrivateChatManager( private fun sendNoiseIdentityAnnouncement(meshService: Any) { try { // Call broadcastNoiseIdentityAnnouncement which sends a NoiseIdentityAnnouncement - val method = meshService::class.java.getDeclaredMethod("broadcastNoiseIdentityAnnouncement") + val method = + meshService::class.java.getDeclaredMethod("broadcastNoiseIdentityAnnouncement") method.invoke(meshService) Log.d(TAG, "Successfully sent Noise identity announcement") } catch (e: Exception) { Log.e(TAG, "Failed to send Noise identity announcement: ${e.message}") } } - + // MARK: - Utility Functions - - private fun getPeerIDForNickname(nickname: String, meshService: Any): String? { - // This would need to access the mesh service to get peer nicknames - // For now, we'll assume the mesh service provides a way to get this mapping - return try { - val method = meshService::class.java.getDeclaredMethod("getPeerNicknames") - val peerNicknames = method.invoke(meshService) as? Map - peerNicknames?.entries?.find { it.value == nickname }?.key - } catch (e: Exception) { - null - } + + private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? { + return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key } - - private fun getPeerNickname(peerID: String, meshService: Any): String { - return try { - val method = meshService::class.java.getDeclaredMethod("getPeerNicknames") - val peerNicknames = method.invoke(meshService) as? Map - peerNicknames?.get(peerID) ?: peerID - } catch (e: Exception) { - peerID - } + + private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String { + return meshService.getPeerNicknames()[peerID] ?: peerID } - + // MARK: - Emergency Clear - + fun clearAllPrivateChats() { state.setSelectedPrivateChatPeer(null) state.setUnreadPrivateMessages(emptySet()) - + // Clear unread messages tracking unreadReceivedMessages.clear() - + // Clear fingerprints via centralized manager (only if needed for emergency clear) // Note: This will be handled by the parent PeerManager.clearAllPeers() } - + // MARK: - Public Getters - + fun getAllPeerFingerprints(): Map { return fingerprintManager.getAllPeerFingerprints() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eb1422bc..08888746 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -34,8 +34,7 @@ kotlinx-coroutines = "1.10.2" # Bluetooth nordic-ble = "2.6.1" -# Compression -lz4-java = "1.8.0" + # Security security-crypto = "1.1.0-beta01" @@ -85,8 +84,7 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine # Bluetooth nordic-ble = { module = "no.nordicsemi.android:ble", version.ref = "nordic-ble" } -# Compression -lz4-java = { module = "org.lz4:lz4-java", version.ref = "lz4-java" } + # Security androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "security-crypto" }