From d76644e58ad74941ea20753c5fc0ae88f27e820f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:01:49 +0200 Subject: [PATCH] temporary --- .../mesh/BluetoothConnectionManager.kt | 14 ++ .../android/mesh/BluetoothMeshService.kt | 56 ++++- .../bitchat/android/mesh/MessageHandler.kt | 209 +++++++++++++++++- .../bitchat/android/mesh/SecurityManager.kt | 45 +++- .../android/protocol/BinaryProtocol.kt | 3 + .../android/util/ByteArrayExtensions.kt | 8 + .../bitchat/android/util/ByteArrayWrapper.kt | 26 +++ 7 files changed, 344 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/util/ByteArrayExtensions.kt create mode 100644 app/src/main/java/com/bitchat/android/util/ByteArrayWrapper.kt diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt index 80966be1..cebe6a38 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothConnectionManager.kt @@ -99,6 +99,20 @@ class BluetoothConnectionManager( try { isActive = true + + // CRITICAL FIX: To be discoverable by iOS, we MUST set the adapter's name + // to our 8-character peerID. iOS uses the `localName` field for discovery. + try { + if (bluetoothAdapter?.name != myPeerID) { + bluetoothAdapter?.name = myPeerID + Log.d(TAG, "Set Bluetooth adapter name to peerID: $myPeerID for iOS compatibility.") + } + } catch (se: SecurityException) { + Log.e(TAG, "Missing BLUETOOTH_CONNECT permission to set adapter name.", se) + // This is not fatal, but will prevent discovery by iOS clients. + // We can still be discovered if the iOS client initiates the connection. + } + // Start all component managers connectionScope.launch { // Start connection tracker first 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 a3b79d54..ac1f8fbc 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -11,6 +11,7 @@ import com.bitchat.android.model.ReadReceipt import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType import com.bitchat.android.protocol.SpecialRecipients +import com.bitchat.android.util.toHexString import kotlinx.coroutines.* import java.util.* import kotlin.random.Random @@ -36,17 +37,17 @@ class BluetoothMeshService(private val context: Context) { } // My peer identification - same format as iOS - val myPeerID: String = generateCompatiblePeerID() + val myPeerID: ByteArray = generateCompatiblePeerID() // Core components - each handling specific responsibilities private val encryptionService = EncryptionService(context) private val peerManager = PeerManager() private val fragmentManager = FragmentManager() - private val securityManager = SecurityManager(encryptionService, myPeerID) + private val securityManager = SecurityManager(encryptionService, myPeerID.toHexString()) private val storeForwardManager = StoreForwardManager() - private val messageHandler = MessageHandler(myPeerID) - internal val connectionManager = BluetoothConnectionManager(context, myPeerID, fragmentManager) // Made internal for access - private val packetProcessor = PacketProcessor(myPeerID) + private val messageHandler = MessageHandler(myPeerID.toHexString()) + internal val connectionManager = BluetoothConnectionManager(context, myPeerID.toHexString(), fragmentManager) // Made internal for access + private val packetProcessor = PacketProcessor(myPeerID.toHexString()) // Service state management private var isActive = false @@ -119,6 +120,17 @@ class BluetoothMeshService(private val context: Context) { storeForwardManager.sendCachedMessages(peerID) } } + + override fun processNoiseHandshake(peerID: String, payload: ByteArray, isInitiation: Boolean): Boolean { + return try { + // For now, treat as legacy key exchange until full Noise implementation + encryptionService.addPeerPublicKey(peerID, payload) + true + } catch (e: Exception) { + Log.e(TAG, "Failed to process Noise handshake: ${e.message}") + false + } + } } // StoreForwardManager delegates @@ -189,6 +201,36 @@ class BluetoothMeshService(private val context: Context) { return securityManager.decryptFromPeer(encryptedData, senderPeerID) } + // Noise protocol operations + override fun hasNoiseSession(peerID: String): Boolean { + // For now, return false since we don't have full Noise implemented + // This will be updated when NoiseEncryptionService is fully integrated + return false + } + + override fun initiateNoiseHandshake(peerID: String) { + // For now, send a basic key exchange instead of full Noise handshake + // This will be updated when NoiseEncryptionService is fully integrated + Log.d(TAG, "TODO: Initiate full Noise handshake with $peerID") + sendKeyExchangeToDevice() + } + + override fun updatePeerIDBinding(newPeerID: String, fingerprint: String, nickname: String, + publicKey: ByteArray, previousPeerID: String?) { + // Update peer mapping in the PeerManager for peer ID rotation support + peerManager.addOrUpdatePeer(newPeerID, nickname) + + // If there was a previous peer ID, remove it to avoid duplicates + previousPeerID?.let { oldPeerID -> + peerManager.removePeer(oldPeerID) + } + + // Register the public key with the delegate (ChatViewModel) + delegate?.registerPeerPublicKey(newPeerID, publicKey) + + Log.d(TAG, "Updated peer ID binding: $newPeerID (was: $previousPeerID), fingerprint: ${fingerprint.take(16)}...") + } + // Message operations override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? { return delegate?.decryptChannelMessage(encryptedContent, channel) @@ -584,10 +626,10 @@ class BluetoothMeshService(private val context: Context) { /** * Generate peer ID compatible with iOS */ - private fun generateCompatiblePeerID(): String { + private fun generateCompatiblePeerID(): ByteArray { val randomBytes = ByteArray(4) Random.nextBytes(randomBytes) - return randomBytes.joinToString("") { "%02x".format(it) } + return randomBytes } } 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 d9c99905..6c9edd48 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -9,6 +9,8 @@ import com.bitchat.android.model.RoutedPacket import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType import kotlinx.coroutines.* +import org.json.JSONObject +import java.security.MessageDigest import java.util.* import kotlin.random.Random @@ -29,27 +31,136 @@ class MessageHandler(private val myPeerID: String) { private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) /** - * Handle Noise encrypted transport message (temporarily stubbed) + * Handle Noise encrypted transport message */ suspend fun handleNoiseEncrypted(routed: RoutedPacket) { val packet = routed.packet val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "TODO: Handle Noise encrypted message from $peerID (${packet.payload.size} bytes)") + Log.d(TAG, "Processing Noise encrypted message from $peerID (${packet.payload.size} bytes)") - // For now, just log it - this will be implemented with actual Noise protocol handling + // Skip our own messages + if (peerID == myPeerID) return + + try { + // Decrypt the message using the Noise service + val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) + if (decryptedData == null) { + Log.w(TAG, "Failed to decrypt Noise message from $peerID - may need handshake") + return + } + + // Check if it's a special format message (type marker + payload) + if (decryptedData.size > 1) { + val typeMarker = decryptedData[0].toUByte() + + // Check if this is a delivery ACK with the new format + if (typeMarker == MessageType.DELIVERY_ACK.value) { + // Extract the ACK JSON data (skip the type marker) + val ackData = decryptedData.sliceArray(1 until decryptedData.size) + + // Decode the delivery ACK + val ack = DeliveryAck.decode(ackData) + if (ack != null) { + delegate?.onDeliveryAckReceived(ack) + Log.d(TAG, "Processed delivery ACK from $peerID") + return + } + } + + // Check for read receipt with type marker + if (typeMarker == MessageType.READ_RECEIPT.value) { + val receiptData = decryptedData.sliceArray(1 until decryptedData.size) + val receipt = ReadReceipt.decode(receiptData) + if (receipt != null) { + delegate?.onReadReceiptReceived(receipt) + Log.d(TAG, "Processed read receipt from $peerID") + return + } + } + } + + // Try to parse as a full inner packet (for compatibility with other message types) + val innerPacket = BitchatPacket.fromBinaryData(decryptedData) + if (innerPacket != null) { + Log.d(TAG, "Decrypted inner packet type ${innerPacket.type} from $peerID") + + // Create a new routed packet with the decrypted inner packet + val innerRouted = RoutedPacket(innerPacket, peerID, routed.relayAddress) + + // Process the decrypted inner packet recursively + when (MessageType.fromValue(innerPacket.type)) { + MessageType.MESSAGE -> handleMessage(innerRouted) + MessageType.DELIVERY_ACK -> handleDeliveryAck(innerRouted) + MessageType.READ_RECEIPT -> handleReadReceipt(innerRouted) + else -> { + Log.w(TAG, "Unexpected inner packet type: ${innerPacket.type}") + } + } + } else { + Log.w(TAG, "Failed to parse decrypted data as packet from $peerID") + } + + } catch (e: Exception) { + Log.e(TAG, "Error processing Noise encrypted message from $peerID: ${e.message}") + } } /** - * Handle Noise identity announcement (temporarily stubbed) + * Handle Noise identity announcement - supports peer ID rotation */ suspend fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) { val packet = routed.packet val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "TODO: Handle Noise identity announcement from $peerID (${packet.payload.size} bytes)") + Log.d(TAG, "Processing Noise identity announcement from $peerID (${packet.payload.size} bytes)") - // For now, just log it - this will be implemented with peer ID rotation handling + // Skip our own announcements + if (peerID == myPeerID) return + + try { + // Parse the identity announcement + val announcement = parseNoiseIdentityAnnouncement(packet.payload) + if (announcement == null) { + Log.w(TAG, "Failed to parse Noise identity announcement from $peerID") + return + } + + Log.d(TAG, "Parsed identity announcement: peerID=${announcement.peerID}, " + + "nickname=${announcement.nickname}, fingerprint=${announcement.fingerprint?.take(16)}...") + + // Verify the announcement signature (basic validation) + // In a full implementation, this would use cryptographic verification + if (announcement.signature.isEmpty()) { + Log.w(TAG, "Identity announcement from $peerID has no signature") + return + } + + // Update peer binding in the delegate (ChatViewModel/BluetoothMeshService) + delegate?.updatePeerIDBinding( + newPeerID = announcement.peerID, + fingerprint = announcement.fingerprint ?: "", + nickname = announcement.nickname, + publicKey = announcement.publicKey, + previousPeerID = announcement.previousPeerID + ) + + // Check if we need to initiate a handshake with this peer + val hasSession = delegate?.hasNoiseSession(announcement.peerID) ?: false + if (!hasSession) { + Log.d(TAG, "No session with ${announcement.peerID}, may need handshake") + + // Use lexicographic comparison to decide who initiates (prevents both sides from initiating) + if (myPeerID < announcement.peerID) { + delegate?.initiateNoiseHandshake(announcement.peerID) + } + } + + Log.d(TAG, "Successfully processed identity announcement from $peerID") + + } catch (e: Exception) { + Log.e(TAG, "Error processing Noise identity announcement from $peerID: ${e.message}") + } } /** @@ -343,6 +454,45 @@ class MessageHandler(private val myPeerID: String) { } } + /** + * Parse Noise identity announcement from payload + */ + private fun parseNoiseIdentityAnnouncement(payload: ByteArray): NoiseIdentityAnnouncement? { + return try { + val jsonString = String(payload, Charsets.UTF_8) + val json = JSONObject(jsonString) + + val peerID = json.getString("peerID") + val nickname = json.getString("nickname") + val publicKeyBase64 = json.getString("publicKey") + val timestampMs = json.getLong("timestamp") + val signatureBase64 = json.getString("signature") + val previousPeerID = if (json.has("previousPeerID")) json.getString("previousPeerID") else null + + // Decode base64 fields + val publicKey = android.util.Base64.decode(publicKeyBase64, android.util.Base64.DEFAULT) + val signature = android.util.Base64.decode(signatureBase64, android.util.Base64.DEFAULT) + + // Calculate fingerprint from public key + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(publicKey) + val fingerprint = hash.joinToString("") { "%02x".format(it) } + + NoiseIdentityAnnouncement( + peerID = peerID, + nickname = nickname, + publicKey = publicKey, + timestamp = Date(timestampMs), + signature = signature, + fingerprint = fingerprint, + previousPeerID = previousPeerID + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to parse Noise identity announcement: ${e.message}") + null + } + } + /** * Shutdown the handler */ @@ -351,6 +501,47 @@ class MessageHandler(private val myPeerID: String) { } } +/** + * Noise Identity Announcement data class (compatible with iOS version) + */ +data class NoiseIdentityAnnouncement( + val peerID: String, + val nickname: String, + val publicKey: ByteArray, + val timestamp: Date, + val signature: ByteArray, + val fingerprint: String?, + val previousPeerID: String? = null +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NoiseIdentityAnnouncement + + if (peerID != other.peerID) return false + if (nickname != other.nickname) return false + if (!publicKey.contentEquals(other.publicKey)) return false + if (timestamp != other.timestamp) return false + if (!signature.contentEquals(other.signature)) return false + if (fingerprint != other.fingerprint) return false + if (previousPeerID != other.previousPeerID) return false + + return true + } + + override fun hashCode(): Int { + var result = peerID.hashCode() + result = 31 * result + nickname.hashCode() + result = 31 * result + publicKey.contentHashCode() + result = 31 * result + timestamp.hashCode() + result = 31 * result + signature.contentHashCode() + result = 31 * result + (fingerprint?.hashCode() ?: 0) + result = 31 * result + (previousPeerID?.hashCode() ?: 0) + return result + } +} + /** * Delegate interface for message handler callbacks */ @@ -373,6 +564,12 @@ interface MessageHandlerDelegate { fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? + // Noise protocol operations + fun hasNoiseSession(peerID: String): Boolean + fun initiateNoiseHandshake(peerID: String) + fun updatePeerIDBinding(newPeerID: String, fingerprint: String, nickname: String, + publicKey: ByteArray, previousPeerID: String?) + // Message operations fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? 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 94515771..b967f3fb 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -87,16 +87,52 @@ class SecurityManager(private val encryptionService: EncryptionService, private } /** - * Handle Noise handshake packet (temporarily stubbed for basic build) + * Handle Noise handshake packet */ suspend fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean { val packet = routed.packet val peerID = routed.peerID ?: "unknown" - Log.d(TAG, "TODO: Handle Noise handshake step $step from $peerID (${packet.payload.size} bytes)") + Log.d(TAG, "Processing Noise handshake step $step from $peerID (${packet.payload.size} bytes)") - // For now, just treat it as a key exchange for compatibility - return handleKeyExchange(routed) + // Skip our own handshake messages + if (peerID == myPeerID) return false + + if (packet.payload.isEmpty()) { + Log.w(TAG, "Noise handshake packet has empty payload") + return false + } + + // Prevent duplicate handshake processing + val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}" + + if (processedKeyExchanges.contains(exchangeKey)) { + Log.d(TAG, "Already processed handshake: $exchangeKey") + return false + } + + processedKeyExchanges.add(exchangeKey) + + try { + // Process the Noise handshake through the delegate (NoiseEncryptionService) + val success = delegate?.processNoiseHandshake(peerID, packet.payload, step == 1) ?: false + + if (success) { + Log.d(TAG, "Successfully processed Noise handshake step $step from $peerID") + + // Notify delegate + delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress) + + return true + } else { + Log.w(TAG, "Failed to process Noise handshake from $peerID") + return false + } + + } catch (e: Exception) { + Log.e(TAG, "Failed to process Noise handshake from $peerID: ${e.message}") + return false + } } /** @@ -333,4 +369,5 @@ class SecurityManager(private val encryptionService: EncryptionService, private */ interface SecurityManagerDelegate { fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?) + fun processNoiseHandshake(peerID: String, payload: ByteArray, isInitiation: Boolean): Boolean } 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 7a315b57..6aaf5217 100644 --- a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt +++ b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt @@ -80,6 +80,8 @@ data class BitchatPacket( var ttl: UByte ) : Parcelable { + // Secondary constructor removed to enforce ByteArray usage for peerIDs + /* constructor( type: UByte, ttl: UByte, @@ -95,6 +97,7 @@ data class BitchatPacket( signature = null, ttl = ttl ) + */ fun toBinaryData(): ByteArray? { return BinaryProtocol.encode(this) diff --git a/app/src/main/java/com/bitchat/android/util/ByteArrayExtensions.kt b/app/src/main/java/com/bitchat/android/util/ByteArrayExtensions.kt new file mode 100644 index 00000000..b2acf112 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/util/ByteArrayExtensions.kt @@ -0,0 +1,8 @@ +package com.bitchat.android.util + +/** + * Extension function to convert a ByteArray to a hexadecimal string. + */ +fun ByteArray.toHexString(): String { + return this.joinToString("") { "%02x".format(it) } +} diff --git a/app/src/main/java/com/bitchat/android/util/ByteArrayWrapper.kt b/app/src/main/java/com/bitchat/android/util/ByteArrayWrapper.kt new file mode 100644 index 00000000..da078d3d --- /dev/null +++ b/app/src/main/java/com/bitchat/android/util/ByteArrayWrapper.kt @@ -0,0 +1,26 @@ +package com.bitchat.android.util + +import java.util.Arrays + +/** + * A wrapper class for ByteArray to allow it to be used as a key in HashMaps. + * The default ByteArray does not override equals() and hashCode() based on content. + * + * @param bytes The byte array to wrap. + */ +data class ByteArrayWrapper(val bytes: ByteArray) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as ByteArrayWrapper + return Arrays.equals(bytes, other.bytes) + } + + override fun hashCode(): Int { + return Arrays.hashCode(bytes) + } + + fun toHexString(): String { + return bytes.joinToString("") { "%02x".format(it) } + } +}