improve identity announcements

This commit is contained in:
callebtc
2025-07-30 11:00:43 +02:00
parent 34d00839b2
commit 158923a1d9
11 changed files with 81 additions and 90 deletions
@@ -41,13 +41,23 @@ class BluetoothConnectionManager(
// Delegate for component managers to call back to main manager // Delegate for component managers to call back to main manager
private val componentDelegate = object : BluetoothConnectionManagerDelegate { private val componentDelegate = object : BluetoothConnectionManagerDelegate {
override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) { override fun onPacketReceived(packet: BitchatPacket, peerID: String, device: BluetoothDevice?) {
Log.d(TAG, "onPacketReceived: Packet received from ${device?.address} ($peerID)")
device?.let { bluetoothDevice -> device?.let { bluetoothDevice ->
// if connection does not have a peerID yet, we assume that the first package
// we receive from that connection is from the peer
if (!connectionTracker.addressPeerMap.containsKey(device.address)) {
Log.d(TAG, "First packet received from new device: ${bluetoothDevice.address}, assuming peerID: $peerID")
connectionTracker.addressPeerMap[device.address] = peerID
}
// Get current RSSI for this device and update if available // Get current RSSI for this device and update if available
val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address) val currentRSSI = connectionTracker.getBestRSSI(bluetoothDevice.address)
if (currentRSSI != null) { if (currentRSSI != null) {
delegate?.onRSSIUpdated(bluetoothDevice.address, currentRSSI) delegate?.onRSSIUpdated(bluetoothDevice.address, currentRSSI)
} }
} }
if (peerID == myPeerID) return // Ignore messages from self
delegate?.onPacketReceived(packet, peerID, device) delegate?.onPacketReceived(packet, peerID, device)
} }
@@ -88,7 +88,7 @@ class BluetoothConnectionTracker(
* Add a device connection * Add a device connection
*/ */
fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) { fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) {
Log.d(TAG, "Tracker: Adding device connection for $deviceAddress") Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}")
connectedDevices[deviceAddress] = deviceConn connectedDevices[deviceAddress] = deviceConn
pendingConnections.remove(deviceAddress) pendingConnections.remove(deviceAddress)
} }
@@ -133,6 +133,13 @@ class BluetoothGattServerManager(
isClient = false isClient = false
) )
connectionTracker.addDeviceConnection(device.address, deviceConn) connectionTracker.addDeviceConnection(device.address, deviceConn)
connectionScope.launch {
delay(1000)
if (isActive) { // Check if still active
delegate?.onDeviceConnected(device)
}
}
} }
BluetoothProfile.STATE_DISCONNECTED -> { BluetoothProfile.STATE_DISCONNECTED -> {
Log.i(TAG, "Server: Device disconnected ${device.address}") Log.i(TAG, "Server: Device disconnected ${device.address}")
@@ -204,9 +211,9 @@ class BluetoothGattServerManager(
} }
if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) { if (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE.contentEquals(value)) {
Log.d(TAG, "Device ${device.address} subscribed to notifications")
connectionTracker.addSubscribedDevice(device) connectionTracker.addSubscribedDevice(device)
Log.d(TAG, "Server: Connection setup complete for ${device.address}")
connectionScope.launch { connectionScope.launch {
delay(100) delay(100)
if (isActive) { // Check if still active if (isActive) { // Check if still active
@@ -272,7 +279,9 @@ class BluetoothGattServerManager(
*/ */
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun startAdvertising() { private fun startAdvertising() {
if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive) return if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive || bluetoothAdapter == null || !bluetoothAdapter.isMultipleAdvertisementSupported()) {
throw Exception("Missing Bluetooth permissions or BLE advertiser not available")
}
val settings = powerManager.getAdvertiseSettings() val settings = powerManager.getAdvertiseSettings()
@@ -63,11 +63,8 @@ class BluetoothMeshService(private val context: Context) {
init { init {
setupDelegates() setupDelegates()
// Wire up PacketProcessor reference for recursive handling in MessageHandler
messageHandler.packetProcessor = packetProcessor messageHandler.packetProcessor = packetProcessor
sendPeriodicBroadcastAnnounce() startPeriodicDebugLogging()
// startPeriodicDebugLogging()
} }
/** /**
@@ -98,6 +95,7 @@ class BluetoothMeshService(private val context: Context) {
try { try {
delay(30000) // 30 seconds delay(30000) // 30 seconds
sendBroadcastAnnounce() sendBroadcastAnnounce()
broadcastNoiseIdentityAnnouncement()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error in periodic broadcast announce: ${e.message}") Log.e(TAG, "Error in periodic broadcast announce: ${e.message}")
} }
@@ -126,11 +124,7 @@ class BluetoothMeshService(private val context: Context) {
// SecurityManager delegate for key exchange notifications // SecurityManager delegate for key exchange notifications
securityManager.delegate = object : SecurityManagerDelegate { securityManager.delegate = object : SecurityManagerDelegate {
override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?) { override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) {
receivedAddress?.let { address ->
connectionManager.addressPeerMap[address] = peerID
}
// Send announcement and cached messages after key exchange // Send announcement and cached messages after key exchange
serviceScope.launch { serviceScope.launch {
delay(100) delay(100)
@@ -150,7 +144,7 @@ class BluetoothMeshService(private val context: Context) {
recipientID = hexStringToByteArray(peerID), recipientID = hexStringToByteArray(peerID),
timestamp = System.currentTimeMillis().toULong(), timestamp = System.currentTimeMillis().toULong(),
payload = response, payload = response,
ttl = 1u ttl = MAX_TTL
) )
connectionManager.broadcastPacket(RoutedPacket(responsePacket)) connectionManager.broadcastPacket(RoutedPacket(responsePacket))
Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)") Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)")
@@ -243,7 +237,7 @@ class BluetoothMeshService(private val context: Context) {
recipientID = hexStringToByteArray(peerID), recipientID = hexStringToByteArray(peerID),
timestamp = System.currentTimeMillis().toULong(), timestamp = System.currentTimeMillis().toULong(),
payload = handshakeData, payload = handshakeData,
ttl = 1u ttl = MAX_TTL
) )
connectionManager.broadcastPacket(RoutedPacket(packet)) connectionManager.broadcastPacket(RoutedPacket(packet))
@@ -387,13 +381,13 @@ class BluetoothMeshService(private val context: Context) {
override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) { override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) {
// Send initial announcements after services are ready // Send initial announcements after services are ready
serviceScope.launch { serviceScope.launch {
delay(100) delay(200)
sendBroadcastAnnounce() sendBroadcastAnnounce()
} }
// Send key exchange to newly connected device // Send key exchange to newly connected device
serviceScope.launch { serviceScope.launch {
delay(100) // Ensure connection is stable delay(400) // Ensure connection is stable
sendKeyExchangeToDevice() broadcastNoiseIdentityAnnouncement()
} }
} }
@@ -685,7 +679,7 @@ class BluetoothMeshService(private val context: Context) {
val announcePacket = BitchatPacket( val announcePacket = BitchatPacket(
type = MessageType.ANNOUNCE.value, type = MessageType.ANNOUNCE.value,
ttl = 3u, ttl = MAX_TTL,
senderID = myPeerID, senderID = myPeerID,
payload = nickname.toByteArray() payload = nickname.toByteArray()
) )
@@ -703,7 +697,7 @@ class BluetoothMeshService(private val context: Context) {
val nickname = delegate?.getNickname() ?: myPeerID val nickname = delegate?.getNickname() ?: myPeerID
val packet = BitchatPacket( val packet = BitchatPacket(
type = MessageType.ANNOUNCE.value, type = MessageType.ANNOUNCE.value,
ttl = 3u, ttl = MAX_TTL,
senderID = myPeerID, senderID = myPeerID,
payload = nickname.toByteArray() payload = nickname.toByteArray()
) )
@@ -715,7 +709,7 @@ class BluetoothMeshService(private val context: Context) {
/** /**
* Send key exchange to newly connected device * Send key exchange to newly connected device
*/ */
fun sendKeyExchangeToDevice() { fun broadcastNoiseIdentityAnnouncement() {
serviceScope.launch { serviceScope.launch {
try { try {
val nickname = delegate?.getNickname() ?: myPeerID val nickname = delegate?.getNickname() ?: myPeerID
@@ -726,13 +720,11 @@ class BluetoothMeshService(private val context: Context) {
val announcementData = announcement.toBinaryData() val announcementData = announcement.toBinaryData()
val packet = BitchatPacket( val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_IDENTITY_ANNOUNCE.value, type = MessageType.NOISE_IDENTITY_ANNOUNCE.value,
senderID = hexStringToByteArray(myPeerID), ttl = MAX_TTL,
recipientID = SpecialRecipients.BROADCAST, senderID = myPeerID,
timestamp = System.currentTimeMillis().toULong(),
payload = announcementData, payload = announcementData,
ttl = 1u
) )
connectionManager.broadcastPacket(RoutedPacket(packet)) connectionManager.broadcastPacket(RoutedPacket(packet))
@@ -838,7 +830,7 @@ class BluetoothMeshService(private val context: Context) {
val nickname = delegate?.getNickname() ?: myPeerID val nickname = delegate?.getNickname() ?: myPeerID
val packet = BitchatPacket( val packet = BitchatPacket(
type = MessageType.LEAVE.value, type = MessageType.LEAVE.value,
ttl = 1u, ttl = MAX_TTL,
senderID = myPeerID, senderID = myPeerID,
payload = nickname.toByteArray() payload = nickname.toByteArray()
) )
@@ -941,6 +933,8 @@ class BluetoothMeshService(private val context: Context) {
appendLine() appendLine()
appendLine(peerManager.getDebugInfo(connectionManager.addressPeerMap)) appendLine(peerManager.getDebugInfo(connectionManager.addressPeerMap))
appendLine() appendLine()
appendLine(printDeviceAddressesForPeers())
appendLine()
appendLine(peerManager.getFingerprintDebugInfo()) appendLine(peerManager.getFingerprintDebugInfo())
appendLine() appendLine()
appendLine(fragmentManager.getDebugInfo()) appendLine(fragmentManager.getDebugInfo())
@@ -20,8 +20,19 @@ import kotlinx.coroutines.channels.actor
/** /**
* Handles packet broadcasting to connected devices using actor pattern for serialization * Handles packet broadcasting to connected devices using actor pattern for serialization
* *
* SERIALIZATION FIX: Uses Kotlin coroutine actor to serialize all packet broadcasting * In Bluetooth Low Energy (BLE):
* This prevents race conditions when multiple threads try to broadcast simultaneously *
* Peripheral (server):
* Advertises.
* Accepts connections.
* Hosts a GATT server.
* Remote devices read/write/subscribe to characteristics.
*
* Central (client):
* Scans.
* Initiates connections.
* Hosts a GATT client.
* Reads/writes to the peripherals characteristics.
*/ */
class BluetoothPacketBroadcaster( class BluetoothPacketBroadcaster(
private val connectionScope: CoroutineScope, private val connectionScope: CoroutineScope,
@@ -52,9 +63,7 @@ class BluetoothPacketBroadcaster(
Log.d(TAG, "🎭 Created packet broadcaster actor") Log.d(TAG, "🎭 Created packet broadcaster actor")
try { try {
for (request in channel) { for (request in channel) {
Log.d(TAG, "Processing broadcast for packet type ${request.routed.packet.type} (serialized)")
broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic) broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic)
Log.d(TAG, "Completed broadcast for packet type ${request.routed.packet.type}")
} }
} finally { } finally {
Log.d(TAG, "🎭 Packet broadcaster actor terminated") Log.d(TAG, "🎭 Packet broadcaster actor terminated")
@@ -158,25 +167,25 @@ class BluetoothPacketBroadcaster(
// Send to server connections (devices connected to our GATT server) // Send to server connections (devices connected to our GATT server)
subscribedDevices.forEach { device -> subscribedDevices.forEach { device ->
if (device.address == routed.relayAddress) { if (device.address == routed.relayAddress) {
Log.d(TAG, "Skipping broadcast back to relayer: ${device.address}") Log.d(TAG, "Skipping broadcast to client back to relayer: ${device.address}")
return@forEach return@forEach
} }
if (connectionTracker.addressPeerMap[device.address] == senderID) { if (connectionTracker.addressPeerMap[device.address] == senderID) {
Log.d(TAG, "Skipping broadcast back to sender: ${device.address}") Log.d(TAG, "Skipping broadcast to client back to sender: ${device.address}")
return@forEach return@forEach
} }
notifyDevice(device, data, gattServer, characteristic) notifyDevice(device, data, gattServer, characteristic)
} }
// Send to client connections // Send to client connections (GATT servers we are connected to)
connectedDevices.values.forEach { deviceConn -> connectedDevices.values.forEach { deviceConn ->
if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) { if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) {
if (deviceConn.device.address == routed.relayAddress) { if (deviceConn.device.address == routed.relayAddress) {
Log.d(TAG, "Skipping broadcast back to relayer: ${deviceConn.device.address}") Log.d(TAG, "Skipping broadcast to server back to relayer: ${deviceConn.device.address}")
return@forEach return@forEach
} }
if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) { if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) {
Log.d(TAG, "Skipping broadcast back to sender: ${deviceConn.device.address}") Log.d(TAG, "Skipping roadcast to server back to sender: ${deviceConn.device.address}")
return@forEach return@forEach
} }
writeToDeviceConn(deviceConn, data) writeToDeviceConn(deviceConn, data)
@@ -185,7 +194,7 @@ class BluetoothPacketBroadcaster(
} }
/** /**
* Send data to a single device (server side) * Send data to a single device (server->client)
*/ */
private fun notifyDevice( private fun notifyDevice(
device: BluetoothDevice, device: BluetoothDevice,
@@ -211,7 +220,7 @@ class BluetoothPacketBroadcaster(
} }
/** /**
* Send data to a single device (client side) * Send data to a single device (client->server)
*/ */
private fun writeToDeviceConn( private fun writeToDeviceConn(
deviceConn: BluetoothConnectionTracker.DeviceConnection, deviceConn: BluetoothConnectionTracker.DeviceConnection,
@@ -70,7 +70,15 @@ class PacketProcessor(private val myPeerID: String) {
* SURGICAL FIX: Route to per-peer actor for serialized processing * SURGICAL FIX: Route to per-peer actor for serialized processing
*/ */
fun processPacket(routed: RoutedPacket) { fun processPacket(routed: RoutedPacket) {
val peerID = routed.peerID ?: "unknown" Log.d(TAG, "processPacket ${routed.packet.type}")
val peerID = routed.peerID
if (peerID == null) {
Log.w(TAG, "Received packet with no peer ID, skipping")
return
}
// Get or create actor for this peer // Get or create actor for this peer
val actor = actors.getOrPut(peerID) { getOrCreateActorForPeer(peerID) } val actor = actors.getOrPut(peerID) { getOrCreateActorForPeer(peerID) }
@@ -135,7 +135,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
// Check if session is now established (handshake complete) // Check if session is now established (handshake complete)
if (encryptionService.hasEstablishedSession(peerID)) { if (encryptionService.hasEstablishedSession(peerID)) {
Log.d(TAG, "✅ Noise handshake completed with $peerID") Log.d(TAG, "✅ Noise handshake completed with $peerID")
delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress) delegate?.onKeyExchangeCompleted(peerID, packet.payload)
} }
return true return true
@@ -145,48 +145,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
return false return false
} }
} }
// /**
// * Handle key exchange packet
// */
// suspend fun handleKeyExchange(routed: RoutedPacket): Boolean {
// val packet = routed.packet
// val peerID = routed.peerID ?: "unknown"
//
// if (peerID == myPeerID) return false
//
// if (packet.payload.isEmpty()) {
// Log.w(TAG, "Key exchange packet has empty payload")
// return false
// }
//
// // Prevent duplicate key exchange processing
// val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
//
// if (processedKeyExchanges.contains(exchangeKey)) {
// Log.d(TAG, "Already processed key exchange: $exchangeKey")
// return false
// }
//
// processedKeyExchanges.add(exchangeKey)
//
// try {
// // Process the key exchange
// encryptionService.addPeerPublicKey(peerID, packet.payload)
//
// Log.d(TAG, "Successfully processed key exchange from $peerID")
//
// // Notify delegate
// delegate?.onKeyExchangeCompleted(peerID, packet.payload, routed.relayAddress)
//
// return true
//
// } catch (e: Exception) {
// Log.e(TAG, "Failed to process key exchange from $peerID: ${e.message}")
// return false
// }
// }
/** /**
* Verify packet signature * Verify packet signature
*/ */
@@ -377,6 +336,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private
* Delegate interface for security manager callbacks * Delegate interface for security manager callbacks
*/ */
interface SecurityManagerDelegate { interface SecurityManagerDelegate {
fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray, receivedAddress: String?) fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray)
fun sendHandshakeResponse(peerID: String, response: ByteArray) fun sendHandshakeResponse(peerID: String, response: ByteArray)
} }
@@ -36,7 +36,7 @@ class NoiseSessionManager(
*/ */
fun getSession(peerID: String): NoiseSession? { fun getSession(peerID: String): NoiseSession? {
val session = sessions[peerID] val session = sessions[peerID]
Log.d(TAG, "getSession($peerID): ${if (session?.isEstablished() == true) "ESTABLISHED" else "NOT_FOUND"}") // Log.d(TAG, "getSession($peerID): ${if (session?.isEstablished() == true) "ESTABLISHED" else "NOT_FOUND"}")
return session return session
} }
@@ -15,6 +15,8 @@ object CompressionUtil {
* Helper to check if compression is worth it - exact same logic as iOS * Helper to check if compression is worth it - exact same logic as iOS
*/ */
fun shouldCompress(data: ByteArray): Boolean { fun shouldCompress(data: ByteArray): Boolean {
// TODO: COMPRESSION DOESN'T WORK WITH IOS YET
return false
// Don't compress if: // Don't compress if:
// 1. Data is too small // 1. Data is too small
// 2. Data appears to be already compressed (high entropy) // 2. Data appears to be already compressed (high entropy)
@@ -37,7 +37,7 @@ class ChatViewModel(
private val noiseSessionDelegate = object : NoiseSessionDelegate { private val noiseSessionDelegate = object : NoiseSessionDelegate {
override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID) override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID)
override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID) override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID)
override fun sendIdentityAnnouncement() = meshService.sendKeyExchangeToDevice() override fun broadcastNoiseIdentityAnnouncement() = meshService.broadcastNoiseIdentityAnnouncement()
override fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) = meshService.sendHandshakeRequest(targetPeerID, pendingCount) override fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) = meshService.sendHandshakeRequest(targetPeerID, pendingCount)
override fun getMyPeerID(): String = meshService.myPeerID override fun getMyPeerID(): String = meshService.myPeerID
} }
@@ -13,7 +13,7 @@ import android.util.Log
interface NoiseSessionDelegate { interface NoiseSessionDelegate {
fun hasEstablishedSession(peerID: String): Boolean fun hasEstablishedSession(peerID: String): Boolean
fun initiateHandshake(peerID: String) fun initiateHandshake(peerID: String)
fun sendIdentityAnnouncement() fun broadcastNoiseIdentityAnnouncement()
fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte)
fun getMyPeerID(): String fun getMyPeerID(): String
} }
@@ -342,7 +342,7 @@ class PrivateChatManager(
} else { } else {
// They should initiate, we send a Noise identity announcement // 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") Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID")
noiseSessionDelegate.sendIdentityAnnouncement() noiseSessionDelegate.broadcastNoiseIdentityAnnouncement()
// Also send handshake request to this peer // Also send handshake request to this peer
noiseSessionDelegate.sendHandshakeRequest(peerID, 1u) // 1 pending message (the chat we're trying to start) noiseSessionDelegate.sendHandshakeRequest(peerID, 1u) // 1 pending message (the chat we're trying to start)
} }
@@ -414,12 +414,12 @@ class PrivateChatManager(
/** /**
* Send Noise identity announcement to prompt other peer to initiate handshake * Send Noise identity announcement to prompt other peer to initiate handshake
* This follows the same pattern as sendKeyExchangeToDevice() in BluetoothMeshService * This follows the same pattern as broadcastNoiseIdentityAnnouncement() in BluetoothMeshService
*/ */
private fun sendNoiseIdentityAnnouncement(meshService: Any) { private fun sendNoiseIdentityAnnouncement(meshService: Any) {
try { try {
// Call sendKeyExchangeToDevice which sends a NoiseIdentityAnnouncement // Call broadcastNoiseIdentityAnnouncement which sends a NoiseIdentityAnnouncement
val method = meshService::class.java.getDeclaredMethod("sendKeyExchangeToDevice") val method = meshService::class.java.getDeclaredMethod("broadcastNoiseIdentityAnnouncement")
method.invoke(meshService) method.invoke(meshService)
Log.d(TAG, "Successfully sent Noise identity announcement") Log.d(TAG, "Successfully sent Noise identity announcement")
} catch (e: Exception) { } catch (e: Exception) {