Merge branch 'main' into gossip-routing-2

This commit is contained in:
callebtc
2025-09-21 08:43:04 +02:00
93 changed files with 7908 additions and 1618 deletions
@@ -10,6 +10,8 @@ import com.bitchat.android.model.IdentityAnnouncement
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.protocol.SpecialRecipients
import com.bitchat.android.model.RequestSyncPacket
import com.bitchat.android.sync.GossipSyncManager
import com.bitchat.android.util.toHexString
import kotlinx.coroutines.*
import java.util.*
@@ -37,18 +39,19 @@ class BluetoothMeshService(private val context: Context) {
private const val MAX_TTL: UByte = 7u
}
// My peer identification - same format as iOS
val myPeerID: String = generateCompatiblePeerID()
// Core components - each handling specific responsibilities
private val encryptionService = EncryptionService(context)
// My peer identification - derived from persisted Noise identity fingerprint (first 16 hex chars)
val myPeerID: String = encryptionService.getIdentityFingerprint().take(16)
private val peerManager = PeerManager()
private val fragmentManager = FragmentManager()
private val securityManager = SecurityManager(encryptionService, myPeerID)
private val storeForwardManager = StoreForwardManager()
private val messageHandler = MessageHandler(myPeerID)
private val messageHandler = MessageHandler(myPeerID, context.applicationContext)
internal val connectionManager = BluetoothConnectionManager(context, myPeerID, fragmentManager) // Made internal for access
private val packetProcessor = PacketProcessor(myPeerID)
private lateinit var gossipSyncManager: GossipSyncManager
// Service state management
private var isActive = false
@@ -63,6 +66,38 @@ class BluetoothMeshService(private val context: Context) {
setupDelegates()
messageHandler.packetProcessor = packetProcessor
//startPeriodicDebugLogging()
// Initialize sync manager (needs serviceScope)
gossipSyncManager = GossipSyncManager(
myPeerID = myPeerID,
scope = serviceScope,
configProvider = object : GossipSyncManager.ConfigProvider {
override fun seenCapacity(): Int = try {
com.bitchat.android.ui.debug.DebugPreferenceManager.getSeenPacketCapacity(500)
} catch (_: Exception) { 500 }
override fun gcsMaxBytes(): Int = try {
com.bitchat.android.ui.debug.DebugPreferenceManager.getGcsMaxFilterBytes(400)
} catch (_: Exception) { 400 }
override fun gcsTargetFpr(): Double = try {
com.bitchat.android.ui.debug.DebugPreferenceManager.getGcsFprPercent(1.0) / 100.0
} catch (_: Exception) { 0.01 }
}
)
// Wire sync manager delegate
gossipSyncManager.delegate = object : GossipSyncManager.Delegate {
override fun sendPacket(packet: BitchatPacket) {
connectionManager.broadcastPacket(RoutedPacket(packet))
}
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
connectionManager.sendPacketToPeer(peerID, packet)
}
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
return signPacketBeforeBroadcast(packet)
}
}
}
/**
@@ -113,6 +148,16 @@ class BluetoothMeshService(private val context: Context) {
override fun onPeerListUpdated(peerIDs: List<String>) {
delegate?.didUpdatePeerList(peerIDs)
}
override fun onPeerRemoved(peerID: String) {
try { gossipSyncManager.removeAnnouncementForPeer(peerID) } catch (_: Exception) { }
// Also drop any Noise session state for this peer when they go offline
try {
encryptionService.removePeer(peerID)
Log.d(TAG, "Removed Noise session for offline peer $peerID")
} catch (e: Exception) {
Log.w(TAG, "Failed to remove Noise session for $peerID: ${e.message}")
}
}
}
// SecurityManager delegate for key exchange notifications
@@ -380,13 +425,26 @@ class BluetoothMeshService(private val context: Context) {
} catch (_: Exception) { }
}
} catch (_: Exception) { }
// Schedule initial sync for this new directly connected peer only
try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { }
}
}
// Track for sync
try { gossipSyncManager.onPublicPacketSeen(routed.packet) } catch (_: Exception) { }
}
}
override fun handleMessage(routed: RoutedPacket) {
serviceScope.launch { messageHandler.handleMessage(routed) }
// Track broadcast messages for sync
try {
val pkt = routed.packet
val isBroadcast = (pkt.recipientID == null || pkt.recipientID.contentEquals(SpecialRecipients.BROADCAST))
if (isBroadcast && pkt.type == MessageType.MESSAGE.value) {
gossipSyncManager.onPublicPacketSeen(pkt)
}
} catch (_: Exception) { }
}
override fun handleLeave(routed: RoutedPacket) {
@@ -394,6 +452,13 @@ class BluetoothMeshService(private val context: Context) {
}
override fun handleFragment(packet: BitchatPacket): BitchatPacket? {
// Track broadcast fragments for gossip sync
try {
val isBroadcast = (packet.recipientID == null || packet.recipientID.contentEquals(SpecialRecipients.BROADCAST))
if (isBroadcast && packet.type == MessageType.FRAGMENT.value) {
gossipSyncManager.onPublicPacketSeen(packet)
}
} catch (_: Exception) { }
return fragmentManager.handleFragment(packet)
}
@@ -412,6 +477,13 @@ class BluetoothMeshService(private val context: Context) {
override fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean {
return connectionManager.sendToPeer(peerID, routed)
}
override fun handleRequestSync(routed: RoutedPacket) {
// Decode request and respond with missing packets
val fromPeer = routed.peerID ?: return
val req = RequestSyncPacket.decode(routed.packet.payload) ?: return
gossipSyncManager.handleRequestSync(fromPeer, req)
}
}
// BluetoothConnectionManager delegates
@@ -484,6 +556,8 @@ class BluetoothMeshService(private val context: Context) {
// Start periodic announcements for peer discovery and connectivity
sendPeriodicBroadcastAnnounce()
Log.d(TAG, "Started periodic broadcast announcements (every 30 seconds)")
// Start periodic syncs
gossipSyncManager.start()
} else {
Log.e(TAG, "Failed to start Bluetooth services")
}
@@ -508,6 +582,7 @@ class BluetoothMeshService(private val context: Context) {
delay(200) // Give leave message time to send
// Stop all components
gossipSyncManager.stop()
connectionManager.stopServices()
peerManager.shutdown()
fragmentManager.shutdown()
@@ -541,8 +616,123 @@ class BluetoothMeshService(private val context: Context) {
// Sign the packet before broadcasting
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
// Track our own broadcast message for sync
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
}
}
/**
* Send a file over mesh as a broadcast MESSAGE (public mesh timeline/channels).
*/
fun sendFileBroadcast(file: com.bitchat.android.model.BitchatFilePacket) {
try {
Log.d(TAG, "📤 sendFileBroadcast: name=${file.fileName}, size=${file.fileSize}")
val payload = file.encode()
if (payload == null) {
Log.e(TAG, "❌ Failed to encode file packet in sendFileBroadcast")
return
}
Log.d(TAG, "📦 Encoded payload: ${payload.size} bytes")
serviceScope.launch {
val packet = BitchatPacket(
version = 2u, // FILE_TRANSFER uses v2 for 4-byte payload length to support large files
type = MessageType.FILE_TRANSFER.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = SpecialRecipients.BROADCAST,
timestamp = System.currentTimeMillis().toULong(),
payload = payload,
signature = null,
ttl = MAX_TTL
)
val signed = signPacketBeforeBroadcast(packet)
// Use a stable transferId based on the file TLV payload for progress tracking
val transferId = sha256Hex(payload)
connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
}
} catch (e: Exception) {
Log.e(TAG, "❌ sendFileBroadcast failed: ${e.message}", e)
Log.e(TAG, "❌ File: name=${file.fileName}, size=${file.fileSize}")
}
}
/**
* Send a file as an encrypted private message using Noise protocol
*/
fun sendFilePrivate(recipientPeerID: String, file: com.bitchat.android.model.BitchatFilePacket) {
try {
Log.d(TAG, "📤 sendFilePrivate (ENCRYPTED): to=$recipientPeerID, name=${file.fileName}, size=${file.fileSize}")
serviceScope.launch {
// Check if we have an established Noise session
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
try {
// Encode the file packet as TLV
val filePayload = file.encode()
if (filePayload == null) {
Log.e(TAG, "❌ Failed to encode file packet for private send")
return@launch
}
Log.d(TAG, "📦 Encoded file TLV: ${filePayload.size} bytes")
// Create NoisePayload wrapper (type byte + file TLV data) - same as iOS
val noisePayload = com.bitchat.android.model.NoisePayload(
type = com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER,
data = filePayload
)
// Encrypt the payload using Noise
val encrypted = encryptionService.encrypt(noisePayload.encode(), recipientPeerID)
if (encrypted == null) {
Log.e(TAG, "❌ Failed to encrypt file for $recipientPeerID")
return@launch
}
Log.d(TAG, "🔐 Encrypted file payload: ${encrypted.size} bytes")
// Create NOISE_ENCRYPTED packet (not FILE_TRANSFER!)
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
)
// Sign and send the encrypted packet
val signed = signPacketBeforeBroadcast(packet)
// Use a stable transferId based on the unencrypted file TLV payload for progress tracking
val transferId = sha256Hex(filePayload)
connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
Log.d(TAG, "✅ Sent encrypted file to $recipientPeerID")
} catch (e: Exception) {
Log.e(TAG, "❌ Failed to encrypt file for $recipientPeerID: ${e.message}", e)
}
} else {
// No session - initiate handshake but don't queue file
Log.w(TAG, "⚠️ No Noise session with $recipientPeerID for file transfer, initiating handshake")
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
}
}
} catch (e: Exception) {
Log.e(TAG, "❌ sendFilePrivate failed: ${e.message}", e)
Log.e(TAG, "❌ File: to=$recipientPeerID, name=${file.fileName}, size=${file.fileSize}")
}
}
fun cancelFileTransfer(transferId: String): Boolean {
return connectionManager.cancelTransfer(transferId)
}
// Local helper to hash payloads to a stable hex ID for progress mapping
private fun sha256Hex(bytes: ByteArray): String = try {
val md = java.security.MessageDigest.getInstance("SHA-256")
md.update(bytes)
md.digest().joinToString("") { "%02x".format(it) }
} catch (_: Exception) { bytes.size.toString(16) }
/**
* Send private message - SIMPLIFIED iOS-compatible version
@@ -726,6 +916,8 @@ class BluetoothMeshService(private val context: Context) {
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)")
// Track announce for sync
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
}
}
@@ -788,6 +980,9 @@ class BluetoothMeshService(private val context: Context) {
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
peerManager.markPeerAsAnnouncedTo(peerID)
Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)")
// Track announce for sync
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
}
/**
@@ -950,15 +1145,6 @@ class BluetoothMeshService(private val context: Context) {
}
}
/**
* Generate peer ID compatible with iOS - exactly 8 bytes (16 hex characters)
*/
private fun generateCompatiblePeerID(): String {
val randomBytes = ByteArray(8) // 8 bytes = 16 hex characters (like iOS)
Random.nextBytes(randomBytes)
return randomBytes.joinToString("") { "%02x".format(it) }
}
/**
* Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS
*/