mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:25:19 +00:00
cleanup logging
This commit is contained in:
@@ -78,7 +78,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
VerificationService.configure(encryptionService)
|
||||
setupDelegates()
|
||||
messageHandler.packetProcessor = packetProcessor
|
||||
//startPeriodicDebugLogging()
|
||||
|
||||
// Initialize sync manager (needs serviceScope)
|
||||
gossipSyncManager = GossipSyncManager(
|
||||
@@ -117,29 +116,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
peerManager.isPeerDirectlyConnected = { peerID ->
|
||||
connectionManager.addressPeerMap.containsValue(peerID)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Delegates set up; GossipSyncManager initialized")
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic debug logging every 10 seconds
|
||||
*/
|
||||
private fun startPeriodicDebugLogging() {
|
||||
serviceScope.launch {
|
||||
Log.d(TAG, "Starting periodic debug logging loop")
|
||||
while (isActive) {
|
||||
try {
|
||||
delay(10000) // 10 seconds
|
||||
if (isActive) { // Double-check before logging
|
||||
val debugInfo = getDebugStatus()
|
||||
Log.d(TAG, "=== PERIODIC DEBUG STATUS ===\n$debugInfo\n=== END DEBUG STATUS ===")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in periodic debug logging: ${e.message}")
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Periodic debug logging loop ended (isActive=$isActive)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,7 +123,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
*/
|
||||
private fun sendPeriodicBroadcastAnnounce() {
|
||||
serviceScope.launch {
|
||||
Log.d(TAG, "Starting periodic announce loop")
|
||||
while (isActive) {
|
||||
try {
|
||||
delay(30000) // 30 seconds
|
||||
@@ -156,7 +131,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
Log.e(TAG, "Error in periodic broadcast announce: ${e.message}")
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Periodic announce loop ended (isActive=$isActive)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +138,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
* Setup delegate connections between components
|
||||
*/
|
||||
private fun setupDelegates() {
|
||||
Log.d(TAG, "Setting up component delegates")
|
||||
// Provide nickname resolver to BLE broadcaster and debug manager
|
||||
try {
|
||||
val resolver: (String) -> String? = { pid -> peerManager.getPeerNickname(pid) }
|
||||
@@ -187,7 +160,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// 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}")
|
||||
}
|
||||
@@ -199,7 +171,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) {
|
||||
// Send announcement and cached messages after key exchange
|
||||
serviceScope.launch {
|
||||
Log.d(TAG, "Key exchange completed with $peerID; sending follow-ups")
|
||||
delay(100)
|
||||
sendAnnouncementToPeer(peerID)
|
||||
|
||||
@@ -222,7 +193,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Sign the handshake response
|
||||
val signedPacket = signPacketBeforeBroadcast(responsePacket)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
Log.d(TAG, "Sent Noise handshake response to $peerID (${response.size} bytes)")
|
||||
}
|
||||
|
||||
override fun getPeerInfo(peerID: String): PeerInfo? {
|
||||
@@ -336,7 +306,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Sign the handshake packet before broadcasting
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)")
|
||||
} else {
|
||||
Log.w(TAG, "Failed to generate Noise handshake data for $peerID")
|
||||
}
|
||||
@@ -358,12 +327,11 @@ class BluetoothMeshService(private val context: Context) {
|
||||
override fun updatePeerIDBinding(newPeerID: String, nickname: String,
|
||||
publicKey: ByteArray, previousPeerID: String?) {
|
||||
|
||||
Log.d(TAG, "Updating peer ID binding: $newPeerID (was: $previousPeerID) with nickname: $nickname and public key: ${publicKey.toHexString().take(16)}...")
|
||||
// Update peer mapping in the PeerManager for peer ID rotation support
|
||||
peerManager.addOrUpdatePeer(newPeerID, nickname)
|
||||
|
||||
// Store fingerprint for the peer via centralized fingerprint manager
|
||||
val fingerprint = peerManager.storeFingerprintForPeer(newPeerID, publicKey)
|
||||
peerManager.storeFingerprintForPeer(newPeerID, publicKey)
|
||||
|
||||
// Index existing Nostr mapping by the new peerID if we have it
|
||||
try {
|
||||
@@ -376,8 +344,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
previousPeerID?.let { oldPeerID ->
|
||||
peerManager.removePeer(oldPeerID)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Updated peer ID binding: $newPeerID (was: $previousPeerID), fingerprint: ${fingerprint.take(16)}...")
|
||||
}
|
||||
|
||||
// Message operations
|
||||
@@ -488,7 +454,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
if (isDirect) {
|
||||
// Bind or rebind this device address to the announcing peer
|
||||
connectionManager.addressPeerMap[deviceAddress] = pid
|
||||
Log.d(TAG, "Mapped device $deviceAddress to peer $pid (TTL=${routed.packet.ttl})")
|
||||
|
||||
// Mark as directly connected - refresh UI state
|
||||
try { peerManager.refreshPeerList() } catch (_: Exception) { }
|
||||
@@ -572,7 +537,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
override fun onDeviceConnected(device: android.bluetooth.BluetoothDevice) {
|
||||
// Send initial announcements after services are ready
|
||||
serviceScope.launch {
|
||||
Log.d(TAG, "Device connected: ${device.address}; scheduling announce")
|
||||
delay(200)
|
||||
sendBroadcastAnnounce()
|
||||
}
|
||||
@@ -587,7 +551,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnected(device: android.bluetooth.BluetoothDevice) {
|
||||
Log.d(TAG, "Device disconnected: ${device.address}")
|
||||
val addr = device.address
|
||||
// Remove mapping and, if that was the last direct path for the peer, clear direct flag
|
||||
val peer = connectionManager.addressPeerMap[addr]
|
||||
@@ -638,10 +601,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()
|
||||
Log.d(TAG, "GossipSyncManager started")
|
||||
} else {
|
||||
Log.e(TAG, "Failed to start Bluetooth services")
|
||||
}
|
||||
@@ -663,14 +624,11 @@ class BluetoothMeshService(private val context: Context) {
|
||||
sendLeaveAnnouncement()
|
||||
|
||||
serviceScope.launch {
|
||||
Log.d(TAG, "Stopping subcomponents and cancelling scope...")
|
||||
delay(200) // Give leave message time to send
|
||||
|
||||
// Stop all components
|
||||
gossipSyncManager.stop()
|
||||
Log.d(TAG, "GossipSyncManager stopped")
|
||||
connectionManager.stopServices()
|
||||
Log.d(TAG, "BluetoothConnectionManager stop requested")
|
||||
peerManager.shutdown()
|
||||
fragmentManager.shutdown()
|
||||
securityManager.shutdown()
|
||||
@@ -691,9 +649,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
*/
|
||||
fun isReusable(): Boolean {
|
||||
val reusable = !terminated && serviceScope.isActive && connectionManager.isReusable()
|
||||
if (!reusable) {
|
||||
Log.d(TAG, "isReusable=false (terminated=$terminated, scopeActive=${serviceScope.isActive}, connReusable=${connectionManager.isReusable()})")
|
||||
}
|
||||
return reusable
|
||||
}
|
||||
|
||||
@@ -728,13 +683,11 @@ class BluetoothMeshService(private val context: Context) {
|
||||
*/
|
||||
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
|
||||
@@ -763,8 +716,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
*/
|
||||
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)) {
|
||||
@@ -775,7 +726,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
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(
|
||||
@@ -789,7 +739,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
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(
|
||||
@@ -808,7 +757,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// 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)
|
||||
@@ -846,9 +794,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
serviceScope.launch {
|
||||
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 {
|
||||
@@ -888,7 +834,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Sign the packet before broadcasting
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
Log.d(TAG, "📤 Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)")
|
||||
|
||||
// FIXED: Don't send didReceiveMessage for our own sent messages
|
||||
// This was causing self-notifications - iOS doesn't do this
|
||||
@@ -899,7 +844,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
} 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)
|
||||
|
||||
// FIXED: Don't send didReceiveMessage for our own sent messages
|
||||
@@ -914,8 +858,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
*/
|
||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||
serviceScope.launch {
|
||||
Log.d(TAG, "📖 Sending read receipt for message $messageID to $recipientPeerID")
|
||||
|
||||
// Route geohash read receipts via MessageRouter instead of here
|
||||
val geo = runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance() }.getOrNull()
|
||||
val isGeoAlias = try {
|
||||
@@ -931,7 +873,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Avoid duplicate read receipts: check persistent store first
|
||||
val seenStore = try { com.bitchat.android.services.SeenMessageStore.getInstance(context.applicationContext) } catch (_: Exception) { null }
|
||||
if (seenStore?.hasRead(messageID) == true) {
|
||||
Log.d(TAG, "Skipping read receipt for $messageID - already marked read")
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -959,7 +900,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Sign the packet before broadcasting
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
Log.d(TAG, "📤 Sent read receipt to $recipientPeerID for message $messageID")
|
||||
|
||||
// Persist as read after successful send
|
||||
try { seenStore?.markRead(messageID) } catch (_: Exception) { }
|
||||
@@ -1007,7 +947,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
Log.d(TAG, "📤 Sent $label to $recipientPeerID (${payload.data.size} bytes)")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send $label to $recipientPeerID: ${e.message}")
|
||||
}
|
||||
@@ -1018,7 +957,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
|
||||
*/
|
||||
fun sendBroadcastAnnounce() {
|
||||
Log.d(TAG, "Sending broadcast announce")
|
||||
serviceScope.launch {
|
||||
val nickname = try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { myPeerID }
|
||||
|
||||
@@ -1071,7 +1009,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
} ?: announcePacket
|
||||
|
||||
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) { }
|
||||
}
|
||||
@@ -1135,7 +1072,6 @@ 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) { }
|
||||
@@ -1347,7 +1283,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Exclude first (sender) and last (recipient); only intermediates
|
||||
val intermediates = path.subList(1, path.size - 1)
|
||||
val hopsBytes = intermediates.map { hexStringToByteArray(it) }
|
||||
Log.d(TAG, "✅ Signed packet type ${packet.type} (route ${hopsBytes.size} hops: $intermediates)")
|
||||
// Attach route and upgrade to v2 (required for HAS_ROUTE flag)
|
||||
packet.copy(route = hopsBytes, version = 2u)
|
||||
} else packet.copy(route = null)
|
||||
@@ -1364,7 +1299,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Sign the packet data using our signing key
|
||||
val signature = encryptionService.signData(packetDataForSigning)
|
||||
if (signature != null) {
|
||||
Log.d(TAG, "✅ Signed packet type ${packet.type} (signature ${signature.size} bytes)")
|
||||
withRoute.copy(signature = signature)
|
||||
} else {
|
||||
Log.w(TAG, "Failed to sign packet type ${packet.type}, sending unsigned")
|
||||
@@ -1393,7 +1327,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
securityManager.clearAllData()
|
||||
peerManager.clearAllPeers()
|
||||
peerManager.clearAllFingerprints()
|
||||
Log.d(TAG, "✅ Cleared all mesh service internal data")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error clearing mesh service internal data: ${e.message}")
|
||||
}
|
||||
@@ -1407,7 +1340,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
try {
|
||||
// Clear encryption service persistent identity (includes Ed25519 signing keys)
|
||||
encryptionService.clearPersistentIdentity()
|
||||
Log.d(TAG, "✅ Cleared all encryption data")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Error clearing encryption data: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -58,9 +58,6 @@ class BluetoothPacketBroadcaster(
|
||||
nicknameResolver = resolver
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug logging helper - can be easily removed/disabled for production
|
||||
*/
|
||||
private fun logPacketRelay(
|
||||
typeName: String,
|
||||
senderPeerID: String,
|
||||
@@ -74,10 +71,13 @@ class BluetoothPacketBroadcaster(
|
||||
routeInfo: String? = null
|
||||
) {
|
||||
try {
|
||||
val fromNick = incomingPeer?.let { nicknameResolver?.invoke(it) }
|
||||
val toNick = toPeer?.let { nicknameResolver?.invoke(it) }
|
||||
val manager = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
|
||||
// Always log outgoing for the actual transmission target
|
||||
val origin = senderNick?.takeIf { it.isNotBlank() }?.let { "$it ($senderPeerID)" } ?: senderPeerID
|
||||
val incoming = incomingAddr?.let { "in=$it" }
|
||||
val relayInfo = listOfNotNull(routeInfo, "origin=$origin", incoming)
|
||||
.joinToString(" ")
|
||||
.takeIf { it.isNotBlank() }
|
||||
manager.logOutgoing(
|
||||
packetType = typeName,
|
||||
toPeerID = toPeer,
|
||||
@@ -85,23 +85,8 @@ class BluetoothPacketBroadcaster(
|
||||
toDeviceAddress = toDeviceAddress,
|
||||
previousHopPeerID = incomingPeer,
|
||||
packetVersion = packetVersion,
|
||||
routeInfo = routeInfo
|
||||
)
|
||||
// Keep the verbose relay message for human readability
|
||||
manager.logPacketRelayDetailed(
|
||||
packetType = typeName,
|
||||
senderPeerID = senderPeerID,
|
||||
senderNickname = senderNick,
|
||||
fromPeerID = incomingPeer,
|
||||
fromNickname = fromNick,
|
||||
fromDeviceAddress = incomingAddr,
|
||||
toPeerID = toPeer,
|
||||
toNickname = toNick,
|
||||
toDeviceAddress = toDeviceAddress,
|
||||
ttl = ttl,
|
||||
isRelay = true,
|
||||
packetVersion = packetVersion,
|
||||
routeInfo = routeInfo
|
||||
routeInfo = relayInfo,
|
||||
ttl = ttl
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
// Silently ignore debug logging failures
|
||||
@@ -124,13 +109,8 @@ class BluetoothPacketBroadcaster(
|
||||
private val broadcasterActor = broadcasterScope.actor<BroadcastRequest>(
|
||||
capacity = Channel.UNLIMITED
|
||||
) {
|
||||
Log.d(TAG, "🎭 Created packet broadcaster actor")
|
||||
try {
|
||||
for (request in channel) {
|
||||
broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic)
|
||||
}
|
||||
} finally {
|
||||
Log.d(TAG, "🎭 Packet broadcaster actor terminated")
|
||||
for (request in channel) {
|
||||
broadcastSinglePacketInternal(request.routed, request.gattServer, request.characteristic)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,9 +121,6 @@ class BluetoothPacketBroadcaster(
|
||||
) {
|
||||
val packet = routed.packet
|
||||
val isFile = packet.type == MessageType.FILE_TRANSFER.value
|
||||
if (isFile) {
|
||||
Log.d(TAG, "📤 Broadcasting FILE_TRANSFER: ${packet.payload.size} bytes")
|
||||
}
|
||||
// Prefer caller-provided transferId (e.g., for encrypted media), else derive for FILE_TRANSFER
|
||||
val transferId = routed.transferId ?: (if (isFile) sha256Hex(packet.payload) else null)
|
||||
// Check if we need to fragment
|
||||
@@ -158,10 +135,6 @@ class BluetoothPacketBroadcaster(
|
||||
return
|
||||
}
|
||||
if (fragments.size > 1) {
|
||||
if (isFile) {
|
||||
Log.d(TAG, "🔀 File needs ${fragments.size} fragments")
|
||||
}
|
||||
Log.d(TAG, "Fragmenting packet into ${fragments.size} fragments")
|
||||
if (transferId != null) {
|
||||
TransferProgressManager.start(transferId, fragments.size)
|
||||
}
|
||||
@@ -219,9 +192,6 @@ class BluetoothPacketBroadcaster(
|
||||
val packet = routed.packet
|
||||
val data = packet.toBinaryData() ?: return false
|
||||
val isFile = packet.type == MessageType.FILE_TRANSFER.value
|
||||
if (isFile) {
|
||||
Log.d(TAG, "📤 Broadcasting FILE_TRANSFER: ${packet.payload.size} bytes")
|
||||
}
|
||||
// Prefer caller-provided transferId (e.g., for encrypted media), else derive for FILE_TRANSFER
|
||||
val transferId = routed.transferId ?: (if (isFile) sha256Hex(packet.payload) else null)
|
||||
if (transferId != null) {
|
||||
@@ -355,7 +325,6 @@ class BluetoothPacketBroadcaster(
|
||||
// If we are the sender and a source route is defined, we must send ONLY to the first hop.
|
||||
if (packet.senderID.toHexString() == myPeerID && !packet.route.isNullOrEmpty()) {
|
||||
val firstHop = packet.route!![0].toHexString()
|
||||
Log.d(TAG, "Source Routing: Packet has explicit route, attempting to send to first hop: $firstHop")
|
||||
|
||||
var sent = false
|
||||
|
||||
@@ -364,7 +333,6 @@ class BluetoothPacketBroadcaster(
|
||||
.firstOrNull { connectionTracker.addressPeerMap[it.address] == firstHop }
|
||||
|
||||
if (serverTarget != null) {
|
||||
Log.d(TAG, "Source Routing: sending directly to first hop (server conn) $firstHop: ${serverTarget.address}")
|
||||
if (notifyDevice(serverTarget, data, gattServer, characteristic)) {
|
||||
val toPeer = connectionTracker.addressPeerMap[serverTarget.address]
|
||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, serverTarget.address, packet.ttl, packet.version, routeInfo)
|
||||
@@ -378,7 +346,6 @@ class BluetoothPacketBroadcaster(
|
||||
.firstOrNull { connectionTracker.addressPeerMap[it.device.address] == firstHop }
|
||||
|
||||
if (clientTarget != null) {
|
||||
Log.d(TAG, "Source Routing: sending directly to first hop (client conn) $firstHop: ${clientTarget.device.address}")
|
||||
if (writeToDeviceConn(clientTarget, data)) {
|
||||
val toPeer = connectionTracker.addressPeerMap[clientTarget.device.address]
|
||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, clientTarget.device.address, packet.ttl, packet.version, routeInfo)
|
||||
@@ -401,7 +368,6 @@ class BluetoothPacketBroadcaster(
|
||||
|
||||
// If found, send directly
|
||||
if (targetDevice != null) {
|
||||
Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}")
|
||||
if (notifyDevice(targetDevice, data, gattServer, characteristic)) {
|
||||
val toPeer = connectionTracker.addressPeerMap[targetDevice.address]
|
||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDevice.address, packet.ttl, packet.version, routeInfo)
|
||||
@@ -415,7 +381,6 @@ class BluetoothPacketBroadcaster(
|
||||
|
||||
// If found, send directly
|
||||
if (targetDeviceConn != null) {
|
||||
Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}")
|
||||
if (writeToDeviceConn(targetDeviceConn, data)) {
|
||||
val toPeer = connectionTracker.addressPeerMap[targetDeviceConn.device.address]
|
||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDeviceConn.device.address, packet.ttl, packet.version, routeInfo)
|
||||
@@ -427,19 +392,15 @@ class BluetoothPacketBroadcaster(
|
||||
// Else, continue with broadcasting to all devices
|
||||
val subscribedDevices = connectionTracker.getSubscribedDevices()
|
||||
val connectedDevices = connectionTracker.getConnectedDevices()
|
||||
|
||||
Log.i(TAG, "Broadcasting packet v${packet.version} type ${packet.type} to ${subscribedDevices.size} server + ${connectedDevices.size} client connections")
|
||||
|
||||
val senderID = packet.senderID.toHexString()
|
||||
|
||||
// Send to server connections (devices connected to our GATT server)
|
||||
subscribedDevices.forEach { device ->
|
||||
if (device.address == routed.relayAddress) {
|
||||
Log.d(TAG, "Skipping broadcast to client back to relayer: ${device.address}")
|
||||
return@forEach
|
||||
}
|
||||
if (connectionTracker.addressPeerMap[device.address] == senderID) {
|
||||
Log.d(TAG, "Skipping broadcast to client back to sender: ${device.address}")
|
||||
return@forEach
|
||||
}
|
||||
val sent = notifyDevice(device, data, gattServer, characteristic)
|
||||
@@ -453,11 +414,9 @@ class BluetoothPacketBroadcaster(
|
||||
connectedDevices.values.forEach { deviceConn ->
|
||||
if (deviceConn.isClient && deviceConn.gatt != null && deviceConn.characteristic != null) {
|
||||
if (deviceConn.device.address == routed.relayAddress) {
|
||||
Log.d(TAG, "Skipping broadcast to server back to relayer: ${deviceConn.device.address}")
|
||||
return@forEach
|
||||
}
|
||||
if (connectionTracker.addressPeerMap[deviceConn.device.address] == senderID) {
|
||||
Log.d(TAG, "Skipping roadcast to server back to sender: ${deviceConn.device.address}")
|
||||
return@forEach
|
||||
}
|
||||
val sent = writeToDeviceConn(deviceConn, data)
|
||||
@@ -534,14 +493,10 @@ class BluetoothPacketBroadcaster(
|
||||
* Shutdown the broadcaster actor gracefully
|
||||
*/
|
||||
fun shutdown() {
|
||||
Log.d(TAG, "Shutting down BluetoothPacketBroadcaster actor")
|
||||
|
||||
// Close the actor gracefully
|
||||
broadcasterActor.close()
|
||||
|
||||
// Cancel the broadcaster scope
|
||||
broadcasterScope.cancel()
|
||||
|
||||
Log.d(TAG, "BluetoothPacketBroadcaster shutdown complete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,16 +39,13 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
|
||||
Log.d(TAG, "Processing Noise encrypted message from $peerID (${packet.payload.size} bytes)")
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -72,15 +69,11 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
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)}...")
|
||||
|
||||
// Handle favorite/unfavorite notifications embedded as PMs
|
||||
val pmContent = privateMessage.content
|
||||
if (pmContent.startsWith("[FAVORITED]") || pmContent.startsWith("[UNFAVORITED]")) {
|
||||
@@ -116,7 +109,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
// Handle encrypted file transfer; generate unique message ID
|
||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(noisePayload.data)
|
||||
if (file != null) {
|
||||
Log.d(TAG, "🔓 Decrypted encrypted file from $peerID: name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}'")
|
||||
val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase()
|
||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||
val message = BitchatMessage(
|
||||
@@ -130,8 +122,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
recipientNickname = delegate?.getMyNickname(),
|
||||
senderPeerID = peerID
|
||||
)
|
||||
|
||||
Log.d(TAG, "📄 Saved encrypted incoming file to $savedPath (msgId=$uniqueMsgId)")
|
||||
delegate?.onMessageReceived(message)
|
||||
|
||||
// Send delivery ACK with generated message ID
|
||||
@@ -144,7 +134,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
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)
|
||||
@@ -153,17 +142,14 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
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)
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.VERIFY_CHALLENGE -> {
|
||||
Log.d(TAG, "🔐 Verify challenge received from $peerID (${noisePayload.data.size} bytes)")
|
||||
delegate?.onVerifyChallengeReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
||||
}
|
||||
com.bitchat.android.model.NoisePayloadType.VERIFY_RESPONSE -> {
|
||||
Log.d(TAG, "🔐 Verify response received from $peerID (${noisePayload.data.size} bytes)")
|
||||
delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong())
|
||||
}
|
||||
}
|
||||
@@ -204,7 +190,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
)
|
||||
|
||||
delegate?.sendPacket(packet)
|
||||
Log.d(TAG, "📤 Sent delivery ACK to $senderPeerID for message $messageID")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send delivery ACK to $senderPeerID: ${e.message}")
|
||||
@@ -260,11 +245,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
return false
|
||||
}
|
||||
|
||||
// Successfully decoded TLV format exactly like iOS
|
||||
Log.d(TAG, "✅ Verified announce from $peerID: nickname=${announcement.nickname}, " +
|
||||
"noisePublicKey=${announcement.noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., " +
|
||||
"signingPublicKey=${announcement.signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...")
|
||||
|
||||
// Extract nickname and public keys from TLV data
|
||||
val nickname = announcement.nickname
|
||||
val noisePublicKey = announcement.noisePublicKey
|
||||
@@ -293,8 +273,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
com.bitchat.android.services.meshgraph.MeshGraphService.getInstance()
|
||||
.updateFromAnnouncement(peerID, nickname, neighborsOrNull, packet.timestamp)
|
||||
} catch (_: Exception) { }
|
||||
|
||||
Log.d(TAG, "✅ Processed verified TLV announce: stored identity for $peerID")
|
||||
return isFirstAnnounce
|
||||
}
|
||||
|
||||
@@ -305,16 +283,13 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
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
|
||||
}
|
||||
|
||||
@@ -323,8 +298,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
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,
|
||||
@@ -338,13 +311,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
)
|
||||
|
||||
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) {
|
||||
@@ -361,7 +327,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
if (peerID == myPeerID) return
|
||||
val senderNickname = delegate?.getPeerNickname(peerID)
|
||||
if (senderNickname != null) {
|
||||
Log.d(TAG, "Received message from $senderNickname")
|
||||
delegate?.updatePeerNickname(peerID, senderNickname)
|
||||
}
|
||||
|
||||
@@ -396,9 +361,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
val isFileTransfer = com.bitchat.android.protocol.MessageType.fromValue(packet.type) == com.bitchat.android.protocol.MessageType.FILE_TRANSFER
|
||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
||||
if (file != null) {
|
||||
if (isFileTransfer) {
|
||||
Log.d(TAG, "📥 FILE_TRANSFER decode success (broadcast): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}")
|
||||
}
|
||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||
val message = BitchatMessage(
|
||||
id = PacketIdUtil.computeIdHex(packet).uppercase(),
|
||||
@@ -408,7 +370,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
senderPeerID = peerID,
|
||||
timestamp = Date(packet.timestamp.toLong())
|
||||
)
|
||||
Log.d(TAG, "📄 Saved incoming file to $savedPath")
|
||||
delegate?.onMessageReceived(message)
|
||||
return
|
||||
} else if (isFileTransfer) {
|
||||
@@ -444,9 +405,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
val isFileTransfer = com.bitchat.android.protocol.MessageType.fromValue(packet.type) == com.bitchat.android.protocol.MessageType.FILE_TRANSFER
|
||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
||||
if (file != null) {
|
||||
if (isFileTransfer) {
|
||||
Log.d(TAG, "📥 FILE_TRANSFER decode success (private): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}")
|
||||
}
|
||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||
val message = BitchatMessage(
|
||||
id = java.util.UUID.randomUUID().toString().uppercase(),
|
||||
@@ -458,7 +416,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
isPrivate = true,
|
||||
recipientNickname = delegate?.getMyNickname()
|
||||
)
|
||||
Log.d(TAG, "📄 Saved incoming file to $savedPath")
|
||||
delegate?.onMessageReceived(message)
|
||||
return
|
||||
} else if (isFileTransfer) {
|
||||
|
||||
@@ -16,8 +16,7 @@ import kotlinx.coroutines.channels.actor
|
||||
* from the same peer simultaneously, causing session management conflicts.
|
||||
*/
|
||||
class PacketProcessor(private val myPeerID: String) {
|
||||
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PacketProcessor"
|
||||
}
|
||||
@@ -43,18 +42,11 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
private val peerActors = mutableMapOf<String, CompletableDeferred<Unit>>()
|
||||
|
||||
@OptIn(ObsoleteCoroutinesApi::class)
|
||||
private fun getOrCreateActorForPeer(peerID: String) = processorScope.actor<RoutedPacket>(
|
||||
private fun getOrCreateActorForPeer() = processorScope.actor<RoutedPacket>(
|
||||
capacity = Channel.UNLIMITED
|
||||
) {
|
||||
Log.d(TAG, "🎭 Created packet actor for peer: ${formatPeerForLog(peerID)}")
|
||||
try {
|
||||
for (packet in channel) {
|
||||
Log.d(TAG, "📦 Processing packet type ${packet.packet.type} from ${formatPeerForLog(peerID)} (serialized)")
|
||||
handleReceivedPacket(packet)
|
||||
Log.d(TAG, "Completed packet type ${packet.packet.type} from ${formatPeerForLog(peerID)}")
|
||||
}
|
||||
} finally {
|
||||
Log.d(TAG, "🎭 Packet actor for ${formatPeerForLog(peerID)} terminated")
|
||||
for (packet in channel) {
|
||||
handleReceivedPacket(packet)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +63,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* SURGICAL FIX: Route to per-peer actor for serialized processing
|
||||
*/
|
||||
fun processPacket(routed: RoutedPacket) {
|
||||
Log.d(TAG, "processPacket ${routed.packet.type}")
|
||||
val peerID = routed.peerID
|
||||
|
||||
if (peerID == null) {
|
||||
@@ -80,7 +71,7 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
}
|
||||
|
||||
// Get or create actor for this peer
|
||||
val actor = actors.getOrPut(peerID) { getOrCreateActorForPeer(peerID) }
|
||||
val actor = actors.getOrPut(peerID) { getOrCreateActorForPeer() }
|
||||
|
||||
// Send packet to peer's dedicated actor for serialized processing
|
||||
processorScope.launch {
|
||||
@@ -125,22 +116,12 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
|
||||
// Basic validation and security checks
|
||||
if (!delegate?.validatePacketSecurity(packet, peerID)!!) {
|
||||
Log.d(TAG, "Packet failed security validation from ${formatPeerForLog(peerID)}")
|
||||
return
|
||||
}
|
||||
|
||||
var validPacket = true
|
||||
val messageType = MessageType.fromValue(packet.type)
|
||||
Log.d(TAG, "Processing packet type ${messageType} from ${formatPeerForLog(peerID)}")
|
||||
// Verbose logging to debug manager (and chat via ChatViewModel observer)
|
||||
try {
|
||||
val mt = messageType?.name ?: packet.type.toString()
|
||||
val routeDevice = routed.relayAddress
|
||||
val nick = delegate?.getPeerNickname(peerID)
|
||||
debugManager?.logIncomingPacket(peerID, nick, mt, routeDevice)
|
||||
} catch (_: Exception) { }
|
||||
|
||||
|
||||
|
||||
// Handle public packet types (no address check needed)
|
||||
when (messageType) {
|
||||
MessageType.ANNOUNCE -> handleAnnounce(routed)
|
||||
@@ -161,8 +142,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
Log.w(TAG, "Unknown message type: ${packet.type}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Private packet type ${messageType} not addressed to us (from: ${formatPeerForLog(peerID)} to ${packet.recipientID?.let { it.joinToString("") { b -> "%02x".format(b) } }}), skipping")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,8 +159,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* Handle Noise handshake message - SIMPLIFIED iOS-compatible version
|
||||
*/
|
||||
private suspend fun handleNoiseHandshake(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing Noise handshake from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleNoiseHandshake(routed)
|
||||
}
|
||||
|
||||
@@ -189,8 +166,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* Handle Noise encrypted transport message
|
||||
*/
|
||||
private suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing Noise encrypted message from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleNoiseEncrypted(routed)
|
||||
}
|
||||
|
||||
@@ -198,8 +173,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* Handle announce message
|
||||
*/
|
||||
private suspend fun handleAnnounce(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing announce from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleAnnounce(routed)
|
||||
}
|
||||
|
||||
@@ -207,8 +180,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* Handle regular message
|
||||
*/
|
||||
private suspend fun handleMessage(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing message from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleMessage(routed)
|
||||
}
|
||||
|
||||
@@ -216,8 +187,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* Handle leave message
|
||||
*/
|
||||
private suspend fun handleLeave(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing leave from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleLeave(routed)
|
||||
}
|
||||
|
||||
@@ -225,12 +194,8 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* Handle message fragments
|
||||
*/
|
||||
private suspend fun handleFragment(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing fragment from ${formatPeerForLog(peerID)}")
|
||||
|
||||
val reassembledPacket = delegate?.handleFragment(routed.packet)
|
||||
if (reassembledPacket != null) {
|
||||
Log.d(TAG, "Fragment reassembled, processing complete message")
|
||||
handleReceivedPacket(RoutedPacket(reassembledPacket, routed.peerID, routed.relayAddress))
|
||||
}
|
||||
|
||||
@@ -241,20 +206,9 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* Handle REQUEST_SYNC packets (public, TTL=1)
|
||||
*/
|
||||
private suspend fun handleRequestSync(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing REQUEST_SYNC from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleRequestSync(routed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delivery acknowledgment
|
||||
*/
|
||||
// private suspend fun handleDeliveryAck(routed: RoutedPacket) {
|
||||
// val peerID = routed.peerID ?: "unknown"
|
||||
// Log.d(TAG, "Processing delivery ACK from ${formatPeerForLog(peerID)}")
|
||||
// delegate?.handleDeliveryAck(routed)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Get debug information
|
||||
*/
|
||||
@@ -278,8 +232,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
* Shutdown the processor and all peer actors
|
||||
*/
|
||||
fun shutdown() {
|
||||
Log.d(TAG, "Shutting down PacketProcessor and ${actors.size} peer actors")
|
||||
|
||||
// Close all peer actors gracefully
|
||||
actors.values.forEach { actor ->
|
||||
actor.close()
|
||||
@@ -291,8 +243,6 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
|
||||
// Cancel the main scope
|
||||
processorScope.cancel()
|
||||
|
||||
Log.d(TAG, "PacketProcessor shutdown complete")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ import kotlin.random.Random
|
||||
* All packets that aren't specifically addressed to us get processed here.
|
||||
*/
|
||||
class PacketRelayManager(private val myPeerID: String) {
|
||||
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PacketRelayManager"
|
||||
}
|
||||
@@ -40,30 +39,24 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
suspend fun handlePacketRelay(routed: RoutedPacket) {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
|
||||
Log.d(TAG, "Evaluating relay for packet type ${packet.type} from ${peerID} (TTL: ${packet.ttl})")
|
||||
|
||||
|
||||
// Double-check this packet isn't addressed to us
|
||||
if (isPacketAddressedToMe(packet)) {
|
||||
Log.d(TAG, "Packet addressed to us, skipping relay")
|
||||
return
|
||||
}
|
||||
|
||||
// Skip our own packets
|
||||
if (peerID == myPeerID) {
|
||||
Log.d(TAG, "Packet from ourselves, skipping relay")
|
||||
return
|
||||
}
|
||||
|
||||
// Check TTL and decrement
|
||||
if (packet.ttl == 0u.toUByte()) {
|
||||
Log.d(TAG, "TTL expired, not relaying packet")
|
||||
return
|
||||
}
|
||||
|
||||
// Decrement TTL by 1
|
||||
val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte())
|
||||
Log.d(TAG, "Decremented TTL from ${packet.ttl} to ${relayPacket.ttl}")
|
||||
|
||||
// Source-based routing: if route is set and includes us, try targeted next-hop forwarding
|
||||
val route = relayPacket.route
|
||||
@@ -88,7 +81,6 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
if (nextHopIdHex != null) {
|
||||
val success = try { delegate?.sendToPeer(nextHopIdHex, RoutedPacket(relayPacket, peerID, routed.relayAddress)) } catch (_: Exception) { false } ?: false
|
||||
if (success) {
|
||||
Log.i(TAG, "📦 Source-route relay: ${peerID.take(8)} -> ${nextHopIdHex.take(8)} (type ${'$'}{packet.type}, TTL ${'$'}{relayPacket.ttl})")
|
||||
return
|
||||
} else {
|
||||
Log.w(TAG, "Source-route next hop ${nextHopIdHex.take(8)} not directly connected; falling back to broadcast")
|
||||
@@ -98,11 +90,9 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
}
|
||||
|
||||
// Apply relay logic based on packet type and debug switch
|
||||
val shouldRelay = isRelayEnabled() && shouldRelayPacket(relayPacket, peerID)
|
||||
val shouldRelay = isRelayEnabled() && shouldRelayPacket(relayPacket)
|
||||
if (shouldRelay) {
|
||||
relayPacket(RoutedPacket(relayPacket, peerID, routed.relayAddress))
|
||||
} else {
|
||||
Log.d(TAG, "Relay decision: NOT relaying packet type ${packet.type}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,10 +121,9 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
/**
|
||||
* Determine if we should relay this packet based on type and network conditions
|
||||
*/
|
||||
private fun shouldRelayPacket(packet: BitchatPacket, fromPeerID: String): Boolean {
|
||||
private fun shouldRelayPacket(packet: BitchatPacket): Boolean {
|
||||
// Always relay if TTL is high enough (indicates important message)
|
||||
if (packet.ttl >= 4u) {
|
||||
Log.d(TAG, "High TTL (${packet.ttl}), relaying")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -143,7 +132,6 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
|
||||
// Small networks always relay to ensure connectivity
|
||||
if (networkSize <= 3) {
|
||||
Log.d(TAG, "Small network (${networkSize} peers), relaying")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -156,17 +144,13 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
else -> 0.4 // Lowest probability for very large networks
|
||||
}
|
||||
|
||||
val shouldRelay = Random.nextDouble() < relayProb
|
||||
Log.d(TAG, "Network size: ${networkSize}, Relay probability: ${relayProb}, Decision: ${shouldRelay}")
|
||||
|
||||
return shouldRelay
|
||||
return Random.nextDouble() < relayProb
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually broadcast the packet for relay
|
||||
*/
|
||||
private fun relayPacket(routed: RoutedPacket) {
|
||||
Log.d(TAG, "🔄 Relaying packet type ${routed.packet.type} with TTL ${routed.packet.ttl}")
|
||||
delegate?.broadcastPacket(routed)
|
||||
}
|
||||
|
||||
@@ -186,7 +170,6 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
* Shutdown the relay manager
|
||||
*/
|
||||
fun shutdown() {
|
||||
Log.d(TAG, "Shutting down PacketRelayManager")
|
||||
relayScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.ui.debug.DebugSettingsManager
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
@@ -23,6 +24,11 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
private const val CLEANUP_INTERVAL = com.bitchat.android.util.AppConstants.Security.CLEANUP_INTERVAL_MS // 5 minutes
|
||||
private const val MAX_PROCESSED_MESSAGES = com.bitchat.android.util.AppConstants.Security.MAX_PROCESSED_MESSAGES
|
||||
private const val MAX_PROCESSED_KEY_EXCHANGES = com.bitchat.android.util.AppConstants.Security.MAX_PROCESSED_KEY_EXCHANGES
|
||||
private val SIGNATURE_REQUIRED_TYPES = setOf(
|
||||
MessageType.ANNOUNCE,
|
||||
MessageType.MESSAGE,
|
||||
MessageType.FILE_TRANSFER
|
||||
)
|
||||
}
|
||||
|
||||
// Security tracking
|
||||
@@ -32,6 +38,14 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
|
||||
// Delegate for callbacks
|
||||
var delegate: SecurityManagerDelegate? = null
|
||||
|
||||
private val debugManager by lazy {
|
||||
try {
|
||||
DebugSettingsManager.getInstance()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Coroutines
|
||||
private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
@@ -46,12 +60,18 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
fun validatePacket(packet: BitchatPacket, peerID: String): Boolean {
|
||||
// Skip validation for our own packets
|
||||
if (peerID == myPeerID) {
|
||||
Log.d(TAG, "Skipping validation for our own packet")
|
||||
return false
|
||||
}
|
||||
|
||||
// Replay attack protection (same 5-minute window as iOS)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val timestampResult = validateTimestamp(packet, currentTime)
|
||||
if (!timestampResult.isValid) {
|
||||
logValidationFailure(packet, peerID, timestampResult.reason, timestampResult.detail)
|
||||
Log.w(TAG, "Dropping packet from $peerID: ${timestampResult.reason}")
|
||||
return false
|
||||
}
|
||||
|
||||
val messageType = MessageType.fromValue(packet.type)
|
||||
|
||||
// Duplicate detection
|
||||
@@ -65,23 +85,24 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
packet.ttl >= com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
|
||||
|
||||
if (!isFreshAnnounce) {
|
||||
Log.d(TAG, "Dropping duplicate packet: $messageID")
|
||||
logValidationFailure(packet, peerID, "duplicate packet")
|
||||
return false
|
||||
}
|
||||
Log.d(TAG, "Allowing duplicate ANNOUNCE from direct neighbor: $messageID")
|
||||
}
|
||||
|
||||
// Enforce mandatory signature verification
|
||||
val signatureResult = verifyPacketSignature(packet, peerID)
|
||||
if (!signatureResult.isValid) {
|
||||
logValidationFailure(packet, peerID, signatureResult.reason, signatureResult.detail)
|
||||
Log.w(TAG, "Dropping packet from $peerID: ${signatureResult.reason}")
|
||||
return false
|
||||
}
|
||||
|
||||
// Add to processed messages
|
||||
// Add to processed messages only after validation succeeds so invalid packets
|
||||
// cannot poison duplicate detection for a later valid retry.
|
||||
processedMessages.add(messageID)
|
||||
messageTimestamps[messageID] = currentTime
|
||||
|
||||
// Enforce mandatory signature verification
|
||||
if (!verifyPacketSignature(packet, peerID)) {
|
||||
Log.w(TAG, "Dropping packet from $peerID due to signature verification failure")
|
||||
return false
|
||||
}
|
||||
|
||||
Log.d(TAG, "Packet validation passed for $peerID, messageID: $messageID")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -95,7 +116,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
|
||||
// Skip handshakes not addressed to us
|
||||
if (packet.recipientID?.toHexString() != myPeerID) {
|
||||
Log.d(TAG, "Skipping handshake not addressed to us: $peerID")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -106,7 +126,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
// drop the existing session so we can re-establish cleanly.
|
||||
var forcedRehandshake = false
|
||||
if (encryptionService.hasEstablishedSession(peerID)) {
|
||||
Log.d(TAG, "Received new Noise handshake from $peerID with an existing session. Dropping old session to re-handshake.")
|
||||
Log.i(TAG, "Re-establishing Noise session with $peerID")
|
||||
try {
|
||||
encryptionService.removePeer(peerID)
|
||||
forcedRehandshake = true
|
||||
@@ -124,10 +144,8 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
|
||||
|
||||
if (!forcedRehandshake && processedKeyExchanges.contains(exchangeKey)) {
|
||||
Log.d(TAG, "Already processed handshake: $exchangeKey")
|
||||
return false
|
||||
}
|
||||
Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)")
|
||||
processedKeyExchanges.add(exchangeKey)
|
||||
|
||||
try {
|
||||
@@ -135,13 +153,11 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
val response = encryptionService.processHandshakeMessage(packet.payload, peerID)
|
||||
|
||||
if (response != null) {
|
||||
Log.d(TAG, "Successfully processed Noise handshake from $peerID, sending response")
|
||||
// Send handshake response through delegate
|
||||
delegate?.sendHandshakeResponse(peerID, response)
|
||||
}
|
||||
// Check if session is now established (handshake complete)
|
||||
if (encryptionService.hasEstablishedSession(peerID)) {
|
||||
Log.d(TAG, "✅ Noise handshake completed with $peerID")
|
||||
delegate?.onKeyExchangeCompleted(peerID, packet.payload)
|
||||
}
|
||||
return true
|
||||
@@ -235,32 +251,33 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
* Verify packet signature using peer's signing public key
|
||||
* Returns true only if signature is present and valid
|
||||
*/
|
||||
private fun verifyPacketSignature(packet: BitchatPacket, peerID: String): Boolean {
|
||||
private fun verifyPacketSignature(packet: BitchatPacket, peerID: String): ValidationResult {
|
||||
try {
|
||||
// only verify ANNOUNCE, MESSAGE, and FILE_TRANSFER
|
||||
if (MessageType.fromValue(packet.type) !in setOf(
|
||||
MessageType.ANNOUNCE,
|
||||
MessageType.MESSAGE,
|
||||
MessageType.FILE_TRANSFER
|
||||
)) {
|
||||
return true
|
||||
val messageType = MessageType.fromValue(packet.type)
|
||||
if (messageType !in SIGNATURE_REQUIRED_TYPES) {
|
||||
return ValidationResult.valid()
|
||||
}
|
||||
// 1. Mandatory Signature Check
|
||||
if (packet.signature == null) {
|
||||
Log.w(TAG, "❌ Signature check for $peerID: NO_SIGNATURE (packet type ${packet.type})")
|
||||
return false
|
||||
return ValidationResult.invalid(
|
||||
reason = "missing signature",
|
||||
detail = "${messageType?.name ?: packet.type.toString()} requires Ed25519 signature"
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Get Signing Public Key
|
||||
var signingPublicKey: ByteArray? = null
|
||||
|
||||
if (MessageType.fromValue(packet.type) == MessageType.ANNOUNCE) {
|
||||
if (messageType == MessageType.ANNOUNCE) {
|
||||
// Special Case: ANNOUNCE packets carry their own signing key
|
||||
try {
|
||||
val announcement = com.bitchat.android.model.IdentityAnnouncement.decode(packet.payload)
|
||||
signingPublicKey = announcement?.signingPublicKey
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to decode announcement for key extraction: ${e.message}")
|
||||
return ValidationResult.invalid(
|
||||
reason = "invalid announcement signature key",
|
||||
detail = e.message
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Standard Case: Get key from known peer info
|
||||
@@ -271,15 +288,19 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
if (signingPublicKey == null) {
|
||||
// If we don't have a key (and it's not an announce), we can't verify.
|
||||
// For security, we must reject packets from unknown peers unless it's an announce.
|
||||
Log.w(TAG, "❌ Signature check for $peerID: NO_SIGNING_KEY_AVAILABLE (packet type ${packet.type})")
|
||||
return false
|
||||
return ValidationResult.invalid(
|
||||
reason = "no signing key",
|
||||
detail = "cannot verify ${messageType?.name ?: packet.type.toString()} from unknown peer"
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Get Canonical Data
|
||||
val packetDataForSigning = packet.toBinaryDataForSigning()
|
||||
if (packetDataForSigning == null) {
|
||||
Log.w(TAG, "❌ Signature check for $peerID: ENCODING_ERROR (packet type ${packet.type})")
|
||||
return false
|
||||
return ValidationResult.invalid(
|
||||
reason = "packet encoding error",
|
||||
detail = "could not build canonical signing bytes"
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Verify Signature
|
||||
@@ -291,16 +312,54 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
)
|
||||
|
||||
if (isSignatureValid) {
|
||||
// Log.v(TAG, "✅ Signature verified for $peerID (type ${packet.type})")
|
||||
return true
|
||||
return ValidationResult.valid()
|
||||
} else {
|
||||
Log.w(TAG, "❌ Signature INVALID for $peerID (type ${packet.type})")
|
||||
return false
|
||||
return ValidationResult.invalid(
|
||||
reason = "invalid signature",
|
||||
detail = "Ed25519 verification failed"
|
||||
)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Signature verification error for $peerID: ${e.message}")
|
||||
return false
|
||||
return ValidationResult.invalid(
|
||||
reason = "signature verification error",
|
||||
detail = e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateTimestamp(packet: BitchatPacket, currentTime: Long): ValidationResult {
|
||||
val packetTime = packet.timestamp.toLong()
|
||||
if (packetTime <= 0L) {
|
||||
return ValidationResult.invalid("invalid timestamp", "timestamp=${packet.timestamp}")
|
||||
}
|
||||
|
||||
val ageMs = currentTime - packetTime
|
||||
return when {
|
||||
ageMs > MESSAGE_TIMEOUT -> ValidationResult.invalid(
|
||||
reason = "stale timestamp",
|
||||
detail = "age=${ageMs / 1000}s exceeds ${MESSAGE_TIMEOUT / 1000}s"
|
||||
)
|
||||
ageMs < -MESSAGE_TIMEOUT -> ValidationResult.invalid(
|
||||
reason = "future timestamp",
|
||||
detail = "clock skew=${(-ageMs) / 1000}s exceeds ${MESSAGE_TIMEOUT / 1000}s"
|
||||
)
|
||||
else -> ValidationResult.valid()
|
||||
}
|
||||
}
|
||||
|
||||
private fun logValidationFailure(packet: BitchatPacket, peerID: String, reason: String, detail: String? = null) {
|
||||
debugManager?.logPacketValidationFailure(packet, peerID, reason, detail)
|
||||
}
|
||||
|
||||
private data class ValidationResult(
|
||||
val isValid: Boolean,
|
||||
val reason: String,
|
||||
val detail: String? = null
|
||||
) {
|
||||
companion object {
|
||||
fun valid() = ValidationResult(true, "")
|
||||
fun invalid(reason: String, detail: String? = null) = ValidationResult(false, reason, detail)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,9 +439,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
processedKeyExchanges.removeAll(toRemove.toSet())
|
||||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
Log.d(TAG, "Cleaned up $removedCount old processed messages")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,8 +49,6 @@ class NoiseChannelEncryption {
|
||||
// Store key and password
|
||||
channelKeys[channel] = key
|
||||
channelPasswords[channel] = password
|
||||
|
||||
Log.d(TAG, "Set password for channel $channel")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to set password for channel $channel: ${e.message}")
|
||||
}
|
||||
@@ -62,7 +60,6 @@ class NoiseChannelEncryption {
|
||||
fun removeChannelPassword(channel: String) {
|
||||
channelKeys.remove(channel)
|
||||
channelPasswords.remove(channel)
|
||||
Log.d(TAG, "Removed password for channel $channel")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -271,6 +268,5 @@ class NoiseChannelEncryption {
|
||||
fun clear() {
|
||||
channelKeys.clear()
|
||||
channelPasswords.clear()
|
||||
Log.d(TAG, "Cleared all channel encryption data")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
if (loadedKeyPair != null) {
|
||||
staticIdentityPrivateKey = loadedKeyPair.first
|
||||
staticIdentityPublicKey = loadedKeyPair.second
|
||||
Log.d(TAG, "Loaded existing static identity key: ${calculateFingerprint(staticIdentityPublicKey)}")
|
||||
} else {
|
||||
// Generate new identity key pair
|
||||
val keyPair = generateKeyPair()
|
||||
@@ -96,7 +95,6 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
|
||||
// Save to secure storage
|
||||
identityStateManager.saveStaticKey(staticIdentityPrivateKey, staticIdentityPublicKey)
|
||||
Log.d(TAG, "Generated and saved new static identity key")
|
||||
}
|
||||
|
||||
// Load or create Ed25519 signing key (persistent across sessions)
|
||||
@@ -104,7 +102,6 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
if (loadedSigningKeyPair != null) {
|
||||
signingPrivateKey = loadedSigningKeyPair.first
|
||||
signingPublicKey = loadedSigningKeyPair.second
|
||||
Log.d(TAG, "Loaded existing Ed25519 signing key")
|
||||
} else {
|
||||
// Generate new Ed25519 signing key pair
|
||||
val signingKeyPair = generateEd25519KeyPair()
|
||||
@@ -113,7 +110,6 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
|
||||
// Save to secure storage
|
||||
identityStateManager.saveSigningKey(signingPrivateKey, signingPublicKey)
|
||||
Log.d(TAG, "Generated and saved new Ed25519 signing key")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +165,7 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
// 4. Re-initialize SessionManager with new keys
|
||||
initializeSessionManager()
|
||||
|
||||
Log.d(TAG, "✅ Identity cleared and keys rotated")
|
||||
Log.i(TAG, "Identity cleared and keys rotated")
|
||||
}
|
||||
|
||||
// MARK: - Handshake Management
|
||||
@@ -182,7 +178,6 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
return try {
|
||||
sessionManager.initiateHandshake(peerID)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initiate handshake with $peerID: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -195,7 +190,6 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
return try {
|
||||
sessionManager.processHandshakeMessage(peerID, data)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to process handshake from $peerID: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -221,7 +215,7 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
*/
|
||||
fun encrypt(data: ByteArray, peerID: String): ByteArray? {
|
||||
if (!hasEstablishedSession(peerID)) {
|
||||
Log.w(TAG, "No established session with $peerID, handshake required. TODO: IMPLEMENT HANDSHAKE INIT")
|
||||
Log.i(TAG, "No established session with $peerID; requesting Noise handshake")
|
||||
onHandshakeRequired?.invoke(peerID)
|
||||
return null
|
||||
}
|
||||
@@ -339,7 +333,7 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
* Initiate rekey for a session (replaces old session with new handshake)
|
||||
*/
|
||||
fun initiateRekey(peerID: String): ByteArray? {
|
||||
Log.d(TAG, "Initiating rekey for session with $peerID")
|
||||
Log.i(TAG, "Initiating Noise rekey with $peerID")
|
||||
|
||||
// Remove old session
|
||||
sessionManager.removeSession(peerID)
|
||||
@@ -385,8 +379,6 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
// Calculate fingerprint for logging and callback
|
||||
val fingerprint = calculateFingerprint(remoteStaticKey)
|
||||
|
||||
Log.d(TAG, "Session established with $peerID, fingerprint: ${fingerprint.take(16)}...")
|
||||
|
||||
// Notify about authentication
|
||||
onPeerAuthenticated?.invoke(peerID, fingerprint)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package com.bitchat.android.noise
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.noise.southernstorm.protocol.*
|
||||
import com.bitchat.android.util.toHexString
|
||||
import java.security.SecureRandom
|
||||
|
||||
|
||||
/**
|
||||
@@ -123,7 +121,6 @@ class NoiseSession(
|
||||
}
|
||||
// Extract ciphertext (remaining bytes)
|
||||
val ciphertext = combinedPayload.copyOfRange(NONCE_SIZE_BYTES, combinedPayload.size)
|
||||
Log.d(TAG, "Extracted nonce: $extractedNonce, ciphertext size: ${ciphertext.size}")
|
||||
return Pair(extractedNonce, ciphertext)
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -200,7 +197,6 @@ class NoiseSession(
|
||||
try {
|
||||
// Validate static keys
|
||||
validateStaticKeys()
|
||||
Log.d(TAG, "Created ${if (isInitiator) "initiator" else "responder"} session for $peerID")
|
||||
} catch (e: Exception) {
|
||||
state = NoiseSessionState.Failed(e)
|
||||
Log.e(TAG, "Failed to initialize Noise session: ${e.message}")
|
||||
@@ -225,8 +221,6 @@ class NoiseSession(
|
||||
if (localStaticPublicKey.all { it == 0.toByte() }) {
|
||||
throw IllegalArgumentException("Local static public key cannot be all zeros")
|
||||
}
|
||||
|
||||
Log.d(TAG, "Static keys validated successfully - private: ${localStaticPrivateKey.size} bytes, public: ${localStaticPublicKey.size} bytes")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -235,57 +229,23 @@ class NoiseSession(
|
||||
*/
|
||||
private fun initializeNoiseHandshake(role: Int) {
|
||||
try {
|
||||
Log.d(TAG, "Creating HandshakeState with role: ${if (role == HandshakeState.INITIATOR) "INITIATOR" else "RESPONDER"}")
|
||||
|
||||
// LOGGING: Track Android handshake initialization (matching iOS)
|
||||
Log.d(TAG, "=== ANDROID NOISE SESSION - BEFORE HANDSHAKE INIT ===")
|
||||
Log.d(TAG, "Creating NoiseHandshakeState for peer: $peerID")
|
||||
Log.d(TAG, "Role: ${if (role == HandshakeState.INITIATOR) "INITIATOR" else "RESPONDER"}")
|
||||
|
||||
handshakeState = HandshakeState(PROTOCOL_NAME, role)
|
||||
Log.d(TAG, "HandshakeState created successfully")
|
||||
|
||||
Log.d(TAG, "=== ANDROID NOISE SESSION - AFTER HANDSHAKE INIT ===")
|
||||
Log.d(TAG, "NoiseHandshakeState created and mixPreMessageKeys() completed")
|
||||
|
||||
if (handshakeState?.needsLocalKeyPair() == true) {
|
||||
Log.d(TAG, "Local static key pair is required for XX pattern")
|
||||
|
||||
val localKeyPair = handshakeState?.getLocalKeyPair()
|
||||
if (localKeyPair != null) {
|
||||
// FIXED: Use the provided persistent identity keys with our local fork
|
||||
// Our local fork properly supports setting pre-existing keys
|
||||
Log.d(TAG, "Setting persistent static identity keys...")
|
||||
|
||||
localKeyPair.setPrivateKey(localStaticPrivateKey, 0)
|
||||
|
||||
if (!localKeyPair.hasPrivateKey() || !localKeyPair.hasPublicKey()) {
|
||||
throw IllegalStateException("Failed to set static identity keys - local fork issue")
|
||||
}
|
||||
|
||||
Log.d(TAG, "✓ Successfully set persistent static identity keys")
|
||||
Log.d(TAG, "Algorithm: ${localKeyPair.dhName}")
|
||||
Log.d(TAG, "Private key length: ${localKeyPair.privateKeyLength}")
|
||||
Log.d(TAG, "Public key length: ${localKeyPair.publicKeyLength}")
|
||||
|
||||
// Verify the keys were set correctly
|
||||
val verifyPrivate = ByteArray(32)
|
||||
val verifyPublic = ByteArray(32)
|
||||
localKeyPair.getPrivateKey(verifyPrivate, 0)
|
||||
localKeyPair.getPublicKey(verifyPublic, 0)
|
||||
|
||||
Log.d(TAG, "Persistent identity public key: ${localStaticPublicKey.joinToString("") { "%02x".format(it) }}")
|
||||
Log.d(TAG, "Set public key: ${verifyPublic.joinToString("") { "%02x".format(it) }}")
|
||||
|
||||
} else {
|
||||
throw IllegalStateException("HandshakeState returned null for local key pair")
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.d(TAG, "Local static key pair not needed for this handshake pattern/role")
|
||||
}
|
||||
handshakeState?.start()
|
||||
Log.d(TAG, "Handshake state started successfully with persistent identity keys")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception during handshake initialization: ${e.message}", e)
|
||||
@@ -303,8 +263,6 @@ class NoiseSession(
|
||||
*/
|
||||
@Synchronized
|
||||
fun startHandshake(): ByteArray {
|
||||
Log.d(TAG, "Starting noise XX handshake with $peerID as INITIATOR")
|
||||
|
||||
if (!isInitiator) {
|
||||
throw IllegalStateException("Only initiator can start handshake")
|
||||
}
|
||||
@@ -329,11 +287,9 @@ class NoiseSession(
|
||||
Log.w(TAG, "Warning: XX message 1 size ${firstMessage.size} != expected $XX_MESSAGE_1_SIZE")
|
||||
}
|
||||
|
||||
Log.d(TAG, "Sending XX handshake message 1 to $peerID (${firstMessage.size} bytes) currentPattern: $currentPattern")
|
||||
return firstMessage
|
||||
} catch (e: Exception) {
|
||||
state = NoiseSessionState.Failed(e)
|
||||
Log.e(TAG, "Failed to start handshake: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -344,14 +300,11 @@ class NoiseSession(
|
||||
*/
|
||||
@Synchronized
|
||||
fun processHandshakeMessage(message: ByteArray): ByteArray? {
|
||||
Log.d(TAG, "Processing handshake message from $peerID (${message.size} bytes)")
|
||||
|
||||
try {
|
||||
// Initialize as responder if receiving first message
|
||||
if (state == NoiseSessionState.Uninitialized && !isInitiator) {
|
||||
initializeNoiseHandshake(HandshakeState.RESPONDER)
|
||||
state = NoiseSessionState.Handshaking
|
||||
Log.d(TAG, "Initialized as RESPONDER for XX handshake with $peerID")
|
||||
}
|
||||
|
||||
if (state != NoiseSessionState.Handshaking) {
|
||||
@@ -364,13 +317,11 @@ class NoiseSession(
|
||||
val payloadBuffer = ByteArray(XX_MESSAGE_2_SIZE + MAX_PAYLOAD_SIZE) // Buffer for any payload data
|
||||
|
||||
// Read the incoming message - the Noise library will handle validation
|
||||
val payloadLength = handshakeStateLocal.readMessage(message, 0, message.size, payloadBuffer, 0)
|
||||
handshakeStateLocal.readMessage(message, 0, message.size, payloadBuffer, 0)
|
||||
currentPattern++
|
||||
Log.d(TAG, "Read handshake message, payload length: $payloadLength currentPattern: $currentPattern")
|
||||
|
||||
// Check what action the handshake state wants us to take next
|
||||
val action = handshakeStateLocal.getAction()
|
||||
Log.d(TAG, "Handshake action after processing message: $action")
|
||||
|
||||
return when (action) {
|
||||
HandshakeState.WRITE_MESSAGE -> {
|
||||
@@ -380,7 +331,6 @@ class NoiseSession(
|
||||
currentPattern++
|
||||
val response = responseBuffer.copyOf(responseLength)
|
||||
|
||||
Log.d(TAG, "Generated handshake response: ${response.size} bytes, action still: ${handshakeStateLocal.getAction()} currentPattern: $currentPattern")
|
||||
completeHandshake()
|
||||
response
|
||||
}
|
||||
@@ -388,7 +338,6 @@ class NoiseSession(
|
||||
HandshakeState.SPLIT -> {
|
||||
// Handshake complete, split into transport keys
|
||||
completeHandshake()
|
||||
Log.d(TAG, "SPLIT ✅ XX handshake completed with $peerID")
|
||||
null
|
||||
}
|
||||
|
||||
@@ -398,19 +347,16 @@ class NoiseSession(
|
||||
|
||||
HandshakeState.READ_MESSAGE -> {
|
||||
// Noise library expects us to read another message
|
||||
Log.d(TAG, "Handshake waiting for next message from $peerID")
|
||||
null
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.d(TAG, "Handshake action: $action - no immediate action needed")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
state = NoiseSessionState.Failed(e)
|
||||
Log.e(TAG, "Handshake failed with $peerID: ${e.message}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -423,9 +369,6 @@ class NoiseSession(
|
||||
if (currentPattern < NOISE_XX_PATTERN_LENGTH) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Completing XX handshake with $peerID")
|
||||
|
||||
try {
|
||||
// Split handshake state into transport ciphers
|
||||
val cipherPair = handshakeState?.split()
|
||||
@@ -439,7 +382,6 @@ class NoiseSession(
|
||||
if (remoteDH != null) {
|
||||
remoteStaticPublicKey = ByteArray(32)
|
||||
remoteDH.getPublicKey(remoteStaticPublicKey!!, 0)
|
||||
Log.d(TAG, "Remote static public key: ${remoteStaticPublicKey!!.joinToString("") { "%02x".format(it) }}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,11 +401,8 @@ class NoiseSession(
|
||||
replayWindow = ByteArray(REPLAY_WINDOW_BYTES)
|
||||
|
||||
state = NoiseSessionState.Established
|
||||
Log.d(TAG, "Handshake completed with $peerID as isInitiator: $isInitiator - transport keys derived")
|
||||
Log.d(TAG, "✅ XX handshake completed with $peerID")
|
||||
} catch (e: Exception) {
|
||||
state = NoiseSessionState.Failed(e)
|
||||
Log.e(TAG, "Failed to complete handshake: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -526,17 +465,9 @@ class NoiseSession(
|
||||
Log.w(TAG, "High nonce value detected: $currentNonce - consider rekeying")
|
||||
}
|
||||
|
||||
Log.d(TAG, "✅ ANDROID ENCRYPT: ${data.size} → ${combinedPayload.size} bytes (nonce: $currentNonce, ciphertextLength+TAG: ${ciphertextLength}) for $peerID (msg #$messagesSent, role: ${if (isInitiator) "INITIATOR" else "RESPONDER"})")
|
||||
return combinedPayload
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Real encryption failed - exception: ${e.message}")
|
||||
|
||||
// ENHANCED: Log cipher state for debugging
|
||||
if (sendCipher != null) {
|
||||
Log.e(TAG, "Send cipher state: ${sendCipher!!.javaClass.simpleName}")
|
||||
}
|
||||
|
||||
throw SessionError.EncryptionFailed
|
||||
}
|
||||
}
|
||||
@@ -596,19 +527,9 @@ class NoiseSession(
|
||||
}
|
||||
|
||||
val result = plaintext.copyOf(plaintextLength)
|
||||
Log.d(TAG, "✅ ANDROID DECRYPT: ${combinedPayload.size} → ${result.size} bytes from $peerID (nonce: $extractedNonce, highest: $highestReceivedNonce, role: ${if (isInitiator) "INITIATOR" else "RESPONDER"})")
|
||||
return result
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Decryption failed - exception: ${e.message}")
|
||||
|
||||
// ENHANCED: Log cipher state and session details for debugging
|
||||
if (receiveCipher != null) {
|
||||
Log.e(TAG, "Receive cipher state: ${receiveCipher!!.javaClass.simpleName}")
|
||||
}
|
||||
Log.e(TAG, "Session state: $state, highest received nonce: $highestReceivedNonce")
|
||||
Log.e(TAG, "Input data size: ${combinedPayload.size} bytes")
|
||||
|
||||
throw SessionError.DecryptionFailed
|
||||
}
|
||||
}
|
||||
@@ -709,9 +630,6 @@ class NoiseSession(
|
||||
if (state !is NoiseSessionState.Failed) {
|
||||
state = NoiseSessionState.Failed(Exception("Session destroyed"))
|
||||
}
|
||||
|
||||
Log.d(TAG, "Session destroyed for $peerID")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error during session cleanup: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ class NoiseSessionManager(
|
||||
*/
|
||||
fun addSession(peerID: String, session: NoiseSession) {
|
||||
sessions[peerID] = session
|
||||
Log.d(TAG, "Added new session for $peerID")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,14 +44,13 @@ class NoiseSessionManager(
|
||||
fun removeSession(peerID: String) {
|
||||
sessions[peerID]?.destroy()
|
||||
sessions.remove(peerID)
|
||||
Log.d(TAG, "Removed session for $peerID")
|
||||
}
|
||||
|
||||
/**
|
||||
* SIMPLIFIED: Initiate handshake - no tie breaker, just start
|
||||
*/
|
||||
fun initiateHandshake(peerID: String): ByteArray {
|
||||
Log.d(TAG, "initiateHandshake($peerID)")
|
||||
Log.i(TAG, "Starting Noise handshake with $peerID")
|
||||
|
||||
// Remove any existing session first
|
||||
removeSession(peerID)
|
||||
@@ -64,15 +62,13 @@ class NoiseSessionManager(
|
||||
localStaticPrivateKey = localStaticPrivateKey,
|
||||
localStaticPublicKey = localStaticPublicKey
|
||||
)
|
||||
Log.d(TAG, "Storing new INITIATOR session for $peerID")
|
||||
addSession(peerID, session)
|
||||
|
||||
try {
|
||||
val handshakeData = session.startHandshake()
|
||||
Log.d(TAG, "Started handshake with $peerID as INITIATOR")
|
||||
return handshakeData
|
||||
return session.startHandshake()
|
||||
} catch (e: Exception) {
|
||||
sessions.remove(peerID)
|
||||
Log.e(TAG, "Failed to start Noise handshake with $peerID: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -81,14 +77,12 @@ class NoiseSessionManager(
|
||||
* Handle incoming handshake message
|
||||
*/
|
||||
fun processHandshakeMessage(peerID: String, message: ByteArray): ByteArray? {
|
||||
Log.d(TAG, "processHandshakeMessage($peerID, ${message.size} bytes)")
|
||||
|
||||
try {
|
||||
var session = getSession(peerID)
|
||||
|
||||
// If no session exists, create one as responder
|
||||
if (session == null) {
|
||||
Log.d(TAG, "Creating new RESPONDER session for $peerID")
|
||||
Log.i(TAG, "Responding to Noise handshake from $peerID")
|
||||
session = NoiseSession(
|
||||
peerID = peerID,
|
||||
isInitiator = false,
|
||||
@@ -103,7 +97,7 @@ class NoiseSessionManager(
|
||||
|
||||
// Check if session is established
|
||||
if (session.isEstablished()) {
|
||||
Log.d(TAG, "✅ Session ESTABLISHED with $peerID")
|
||||
Log.i(TAG, "Noise session established with $peerID")
|
||||
val remoteStaticKey = session.getRemoteStaticPublicKey()
|
||||
if (remoteStaticKey != null) {
|
||||
onSessionEstablished?.invoke(peerID, remoteStaticKey)
|
||||
@@ -137,11 +131,9 @@ class NoiseSessionManager(
|
||||
fun decrypt(encryptedData: ByteArray, peerID: String): ByteArray {
|
||||
val session = getSession(peerID)
|
||||
if (session == null) {
|
||||
Log.e(TAG, "No session found for $peerID when trying to decrypt")
|
||||
throw IllegalStateException("No session found for $peerID")
|
||||
}
|
||||
if (!session.isEstablished()) {
|
||||
Log.e(TAG, "Session not established with $peerID when trying to decrypt")
|
||||
throw IllegalStateException("Session not established with $peerID")
|
||||
}
|
||||
return session.decrypt(encryptedData)
|
||||
@@ -151,9 +143,7 @@ class NoiseSessionManager(
|
||||
* Check if session is established with peer
|
||||
*/
|
||||
fun hasEstablishedSession(peerID: String): Boolean {
|
||||
val hasSession = getSession(peerID)?.isEstablished() ?: false
|
||||
Log.d(TAG, "hasEstablishedSession($peerID): $hasSession")
|
||||
return hasSession
|
||||
return getSession(peerID)?.isEstablished() ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +200,6 @@ class NoiseSessionManager(
|
||||
fun shutdown() {
|
||||
sessions.values.forEach { it.destroy() }
|
||||
sessions.clear()
|
||||
Log.d(TAG, "Noise session manager shut down")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-30
@@ -22,8 +22,6 @@
|
||||
|
||||
package com.bitchat.android.noise.southernstorm.protocol;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -35,8 +33,6 @@ import javax.crypto.ShortBufferException;
|
||||
*/
|
||||
public class HandshakeState implements Destroyable {
|
||||
|
||||
private static final String TAG = "AndroidHandshake";
|
||||
|
||||
private SymmetricState symmetric;
|
||||
private boolean isInitiator;
|
||||
private DHState localKeyPair;
|
||||
@@ -467,17 +463,6 @@ public class HandshakeState implements Destroyable {
|
||||
// Empty value for when the prologue is not supplied.
|
||||
private static final byte[] emptyPrologue = new byte [0];
|
||||
|
||||
/**
|
||||
* Converts a byte array to hex string for logging (matching iOS hex format)
|
||||
*/
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the handshake running.
|
||||
*
|
||||
@@ -524,19 +509,11 @@ public class HandshakeState implements Destroyable {
|
||||
throw new IllegalStateException("Pre-shared key required");
|
||||
}
|
||||
|
||||
// Log the symmetric state BEFORE any mixing operations (matching iOS)
|
||||
Log.d(TAG, "=== ANDROID HANDSHAKE START - INITIAL STATE ===");
|
||||
Log.d(TAG, "Protocol: " + symmetric.getProtocolName());
|
||||
Log.d(TAG, "Role: " + (isInitiator ? "INITIATOR" : "RESPONDER"));
|
||||
Log.d(TAG, "Initial symmetric hash: " + bytesToHex(symmetric.getHandshakeHash()));
|
||||
|
||||
// Hash the prologue value.
|
||||
Log.d(TAG, "Mixing empty prologue");
|
||||
if (prologue != null)
|
||||
symmetric.mixHash(prologue, 0, prologue.length);
|
||||
else
|
||||
symmetric.mixHash(emptyPrologue, 0, 0);
|
||||
Log.d(TAG, "Hash after empty prologue: " + bytesToHex(symmetric.getHandshakeHash()));
|
||||
|
||||
// Hash the pre-shared key into the chaining key and handshake hash.
|
||||
if (preSharedKey != null)
|
||||
@@ -544,7 +521,6 @@ public class HandshakeState implements Destroyable {
|
||||
|
||||
// Mix the pre-supplied public keys into the handshake hash.
|
||||
if (isInitiator) {
|
||||
Log.d(TAG, "XX pattern - no pre-message keys to mix");
|
||||
if ((requirements & LOCAL_PREMSG) != 0)
|
||||
symmetric.mixPublicKey(localKeyPair);
|
||||
if ((requirements & FALLBACK_PREMSG) != 0) {
|
||||
@@ -557,7 +533,6 @@ public class HandshakeState implements Destroyable {
|
||||
if ((requirements & REMOTE_PREMSG) != 0)
|
||||
symmetric.mixPublicKey(remotePublicKey);
|
||||
} else {
|
||||
Log.d(TAG, "XX pattern - no pre-message keys to mix");
|
||||
if ((requirements & REMOTE_PREMSG) != 0)
|
||||
symmetric.mixPublicKey(remotePublicKey);
|
||||
if ((requirements & FALLBACK_PREMSG) != 0) {
|
||||
@@ -571,11 +546,6 @@ public class HandshakeState implements Destroyable {
|
||||
symmetric.mixPublicKey(localKeyPair);
|
||||
}
|
||||
|
||||
// Log final state after all initialization (matching iOS)
|
||||
Log.d(TAG, "=== ANDROID HANDSHAKE START - FINAL STATE ===");
|
||||
Log.d(TAG, "Final symmetric hash after mixPreMessageKeys(): " + bytesToHex(symmetric.getHandshakeHash()));
|
||||
Log.d(TAG, "===========================================");
|
||||
|
||||
// The handshake has officially started - set the first action.
|
||||
if (isInitiator)
|
||||
action = WRITE_MESSAGE;
|
||||
|
||||
-48
@@ -22,8 +22,6 @@
|
||||
|
||||
package com.bitchat.android.noise.southernstorm.protocol;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.DigestException;
|
||||
import java.security.MessageDigest;
|
||||
@@ -38,8 +36,6 @@ import javax.crypto.ShortBufferException;
|
||||
*/
|
||||
class SymmetricState implements Destroyable {
|
||||
|
||||
private static final String TAG = "AndroidSymmetric";
|
||||
|
||||
private String name;
|
||||
private CipherState cipher;
|
||||
private MessageDigest hash;
|
||||
@@ -47,17 +43,6 @@ class SymmetricState implements Destroyable {
|
||||
private byte[] h;
|
||||
private byte[] prev_h;
|
||||
|
||||
/**
|
||||
* Converts a byte array to hex string for logging (matching iOS hex format)
|
||||
*/
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new symmetric state object.
|
||||
*
|
||||
@@ -94,14 +79,6 @@ class SymmetricState implements Destroyable {
|
||||
}
|
||||
|
||||
System.arraycopy(h, 0, ck, 0, hashLength);
|
||||
|
||||
// LOGGING: Initial symmetric state after protocol name initialization (matching iOS)
|
||||
Log.d(TAG, "=== ANDROID SYMMETRIC STATE INITIALIZED ===");
|
||||
Log.d(TAG, "Protocol: " + protocolName);
|
||||
Log.d(TAG, "Initial hash (h): " + bytesToHex(h));
|
||||
Log.d(TAG, "Initial chaining key (ck): " + bytesToHex(ck));
|
||||
Log.d(TAG, "Hash length: " + h.length);
|
||||
Log.d(TAG, "=========================================");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,14 +111,6 @@ class SymmetricState implements Destroyable {
|
||||
*/
|
||||
public void mixKey(byte[] data, int offset, int length)
|
||||
{
|
||||
// LOGGING: Before mixKey operation (matching iOS)
|
||||
byte[] inputData = new byte[length];
|
||||
System.arraycopy(data, offset, inputData, 0, length);
|
||||
Log.d(TAG, "*** Android mixKey() BEFORE ***");
|
||||
Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData));
|
||||
Log.d(TAG, "Current CK: " + bytesToHex(ck));
|
||||
Log.d(TAG, "Current Hash: " + bytesToHex(h));
|
||||
|
||||
int keyLength = cipher.getKeyLength();
|
||||
byte[] tempKey = new byte [keyLength];
|
||||
try {
|
||||
@@ -150,12 +119,6 @@ class SymmetricState implements Destroyable {
|
||||
} finally {
|
||||
Noise.destroy(tempKey);
|
||||
}
|
||||
|
||||
// LOGGING: After mixKey operation (matching iOS)
|
||||
Log.d(TAG, "*** Android mixKey() AFTER ***");
|
||||
Log.d(TAG, "New CK: " + bytesToHex(ck));
|
||||
Log.d(TAG, "Hash unchanged: " + bytesToHex(h));
|
||||
Log.d(TAG, "Cipher now has key: " + (cipher.getMACLength() > 0));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,18 +130,7 @@ class SymmetricState implements Destroyable {
|
||||
*/
|
||||
public void mixHash(byte[] data, int offset, int length)
|
||||
{
|
||||
// LOGGING: Before mixHash operation (matching iOS)
|
||||
byte[] inputData = new byte[length];
|
||||
System.arraycopy(data, offset, inputData, 0, length);
|
||||
Log.d(TAG, "*** Android mixHash() BEFORE ***");
|
||||
Log.d(TAG, "Input data (" + length + " bytes): " + bytesToHex(inputData));
|
||||
Log.d(TAG, "Current Hash: " + bytesToHex(h));
|
||||
|
||||
hashTwo(h, 0, h.length, data, offset, length, h, 0, h.length);
|
||||
|
||||
// LOGGING: After mixHash operation (matching iOS)
|
||||
Log.d(TAG, "*** Android mixHash() AFTER ***");
|
||||
Log.d(TAG, "New Hash: " + bytesToHex(h));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
||||
import java.util.Date
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.util.toHexString
|
||||
|
||||
/**
|
||||
@@ -378,97 +379,6 @@ class DebugSettingsManager private constructor() {
|
||||
}
|
||||
}
|
||||
|
||||
fun logIncomingPacket(senderPeerID: String, senderNickname: String?, messageType: String, viaDeviceId: String?) {
|
||||
if (verboseLoggingEnabled.value) {
|
||||
val who = if (!senderNickname.isNullOrBlank()) "$senderNickname ($senderPeerID)" else senderPeerID
|
||||
val routeInfo = if (!viaDeviceId.isNullOrBlank()) " via $viaDeviceId" else " (direct)"
|
||||
addDebugMessage(DebugMessage.PacketEvent(
|
||||
"📦 Received $messageType from $who$routeInfo"
|
||||
))
|
||||
}
|
||||
}
|
||||
fun logPacketRelay(
|
||||
packetType: String,
|
||||
originalPeerID: String,
|
||||
originalNickname: String?,
|
||||
viaDeviceId: String?
|
||||
) {
|
||||
// Backward-compatible simple API; delegate to detailed formatter with best effort
|
||||
logPacketRelayDetailed(
|
||||
packetType = packetType,
|
||||
senderPeerID = originalPeerID,
|
||||
senderNickname = originalNickname,
|
||||
fromPeerID = null,
|
||||
fromNickname = null,
|
||||
fromDeviceAddress = viaDeviceId,
|
||||
toPeerID = null,
|
||||
toNickname = null,
|
||||
toDeviceAddress = null,
|
||||
ttl = null,
|
||||
isRelay = true
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// New, more detailed relay logger used by the mesh/broadcaster
|
||||
fun logPacketRelayDetailed(
|
||||
packetType: String,
|
||||
senderPeerID: String?,
|
||||
senderNickname: String?,
|
||||
fromPeerID: String?,
|
||||
fromNickname: String?,
|
||||
fromDeviceAddress: String?,
|
||||
toPeerID: String?,
|
||||
toNickname: String?,
|
||||
toDeviceAddress: String?,
|
||||
ttl: UByte?,
|
||||
isRelay: Boolean = true,
|
||||
packetVersion: UByte = 1u,
|
||||
routeInfo: String? = null
|
||||
) {
|
||||
// Build message only if verbose logging is enabled, but always update stats
|
||||
val senderLabel = when {
|
||||
!senderNickname.isNullOrBlank() && !senderPeerID.isNullOrBlank() -> "$senderNickname ($senderPeerID)"
|
||||
!senderNickname.isNullOrBlank() -> senderNickname
|
||||
!senderPeerID.isNullOrBlank() -> senderPeerID
|
||||
else -> "unknown"
|
||||
}
|
||||
val fromName = when {
|
||||
!fromNickname.isNullOrBlank() -> fromNickname
|
||||
!fromPeerID.isNullOrBlank() -> fromPeerID
|
||||
else -> "unknown"
|
||||
}
|
||||
val toName = when {
|
||||
!toNickname.isNullOrBlank() -> toNickname
|
||||
!toPeerID.isNullOrBlank() -> toPeerID
|
||||
else -> "unknown"
|
||||
}
|
||||
|
||||
val fromAddr = fromDeviceAddress ?: "?"
|
||||
val toAddr = toDeviceAddress ?: "?"
|
||||
val ttlStr = ttl?.toString() ?: "?"
|
||||
val routeStr = if (routeInfo != null) " $routeInfo" else ""
|
||||
|
||||
if (verboseLoggingEnabled.value) {
|
||||
if (isRelay) {
|
||||
// Relay: show [previousPeer] -> [nextPeer]
|
||||
addDebugMessage(
|
||||
DebugMessage.RelayEvent(
|
||||
"♻️ Relayed v$packetVersion $packetType by $senderLabel from $fromName (${fromPeerID ?: "?"}, $fromAddr) to $toName (${toPeerID ?: "?"}, $toAddr) with TTL $ttlStr$routeStr"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
addDebugMessage(
|
||||
DebugMessage.PacketEvent(
|
||||
"📤 Sent v$packetVersion $packetType by $senderLabel to $toName (${toPeerID ?: "?"}, $toAddr) with TTL $ttlStr$routeStr"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Do not update counters here; this path is for readable logs only.
|
||||
}
|
||||
|
||||
// MARK: - Debug Events for Animation
|
||||
sealed class MeshVisualEvent {
|
||||
data class PacketActivity(val peerID: String) : MeshVisualEvent()
|
||||
@@ -490,17 +400,30 @@ class DebugSettingsManager private constructor() {
|
||||
// Peer nickname resolver
|
||||
private var nicknameResolver: ((String) -> String?)? = null
|
||||
fun setNicknameResolver(resolver: (String) -> String?) { nicknameResolver = resolver }
|
||||
|
||||
private fun packetTypeName(type: UByte): String {
|
||||
return MessageType.fromValue(type)?.name ?: "0x%02X".format(type.toInt())
|
||||
}
|
||||
|
||||
private fun peerLabel(peerID: String?, nickname: String?): String {
|
||||
return when {
|
||||
!nickname.isNullOrBlank() && !peerID.isNullOrBlank() -> "$nickname ($peerID)"
|
||||
!nickname.isNullOrBlank() -> nickname
|
||||
!peerID.isNullOrBlank() -> peerID
|
||||
else -> "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit incoming/outgoing logging to avoid double counting
|
||||
fun logIncoming(packet: BitchatPacket, fromPeerID: String, fromNickname: String?, fromDeviceAddress: String?, myPeerID: String) {
|
||||
val packetType = packet.type.toString()
|
||||
val packetType = packetTypeName(packet.type)
|
||||
val packetVersion = packet.version
|
||||
val route = packet.route
|
||||
val routeInfo = if (!route.isNullOrEmpty()) "routed: ${route.size} hops" else null
|
||||
|
||||
if (verboseLoggingEnabled.value) {
|
||||
val resolvedNick = fromNickname ?: nicknameResolver?.invoke(fromPeerID) ?: "unknown"
|
||||
val who = if (resolvedNick != "unknown") "$resolvedNick ($fromPeerID)" else fromPeerID
|
||||
val who = peerLabel(fromPeerID, resolvedNick.takeUnless { it == "unknown" })
|
||||
val routeStr = if (routeInfo != null) " $routeInfo" else ""
|
||||
addDebugMessage(DebugMessage.PacketEvent("📥 Incoming v$packetVersion $packetType from $who (${fromDeviceAddress ?: "?"})$routeStr"))
|
||||
}
|
||||
@@ -537,11 +460,45 @@ class DebugSettingsManager private constructor() {
|
||||
if (visible) updateRelayStatsFromTimestamps()
|
||||
}
|
||||
|
||||
fun logOutgoing(packetType: String, toPeerID: String?, toNickname: String?, toDeviceAddress: String?, previousHopPeerID: String? = null, packetVersion: UByte = 1u, routeInfo: String? = null) {
|
||||
fun logPacketValidationFailure(packet: BitchatPacket, fromPeerID: String, reason: String, detail: String? = null) {
|
||||
if (!verboseLoggingEnabled.value) return
|
||||
|
||||
val packetType = packetTypeName(packet.type)
|
||||
val detailText = detail?.takeIf { it.isNotBlank() }?.let { " - $it" } ?: ""
|
||||
addDebugMessage(
|
||||
DebugMessage.ValidationEvent(
|
||||
"⚠️ Dropped v${packet.version} $packetType from $fromPeerID: $reason$detailText (ttl=${packet.ttl})"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun logOutgoing(
|
||||
packetType: String,
|
||||
toPeerID: String?,
|
||||
toNickname: String?,
|
||||
toDeviceAddress: String?,
|
||||
previousHopPeerID: String? = null,
|
||||
packetVersion: UByte = 1u,
|
||||
routeInfo: String? = null,
|
||||
ttl: UByte? = null
|
||||
) {
|
||||
if (verboseLoggingEnabled.value) {
|
||||
val who = toNickname ?: toPeerID ?: "unknown"
|
||||
val who = peerLabel(toPeerID, toNickname)
|
||||
val routeStr = if (routeInfo != null) " $routeInfo" else ""
|
||||
addDebugMessage(DebugMessage.PacketEvent("📤 Outgoing v$packetVersion $packetType to $who (${toPeerID ?: "?"}, ${toDeviceAddress ?: "?"})$routeStr"))
|
||||
val ttlStr = ttl?.let { " ttl=$it" } ?: ""
|
||||
if (!previousHopPeerID.isNullOrBlank()) {
|
||||
addDebugMessage(
|
||||
DebugMessage.RelayEvent(
|
||||
"📤 Relay v$packetVersion $packetType from $previousHopPeerID to $who (${toDeviceAddress ?: "?"})$ttlStr$routeStr"
|
||||
)
|
||||
)
|
||||
} else {
|
||||
addDebugMessage(
|
||||
DebugMessage.PacketEvent(
|
||||
"📤 Outgoing v$packetVersion $packetType to $who (${toDeviceAddress ?: "?"})$ttlStr$routeStr"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
val now = System.currentTimeMillis()
|
||||
val visible = _debugSheetVisible.value
|
||||
@@ -577,6 +534,43 @@ class DebugSettingsManager private constructor() {
|
||||
_scanResults.value = emptyList()
|
||||
addDebugMessage(DebugMessage.SystemMessage("🗑️ Scan results cleared"))
|
||||
}
|
||||
|
||||
fun resetForTesting() {
|
||||
_verboseLoggingEnabled.value = false
|
||||
debugMessageQueue.clear()
|
||||
scanResultsQueue.clear()
|
||||
_debugMessages.value = emptyList()
|
||||
_scanResults.value = emptyList()
|
||||
_connectedDevices.value = emptyList()
|
||||
_relayStats.value = PacketRelayStats()
|
||||
relayTimestamps.clear()
|
||||
incomingTimestamps.clear()
|
||||
outgoingTimestamps.clear()
|
||||
perDeviceRelayTimestamps.clear()
|
||||
perPeerRelayTimestamps.clear()
|
||||
perDeviceIncoming.clear()
|
||||
perDeviceOutgoing.clear()
|
||||
perPeerIncoming.clear()
|
||||
perPeerOutgoing.clear()
|
||||
deviceIncomingTotalsMap.clear()
|
||||
deviceOutgoingTotalsMap.clear()
|
||||
peerIncomingTotalsMap.clear()
|
||||
peerOutgoingTotalsMap.clear()
|
||||
_perDeviceLastSecond.value = emptyMap()
|
||||
_perPeerLastSecond.value = emptyMap()
|
||||
_perDeviceIncomingLastSecond.value = emptyMap()
|
||||
_perDeviceOutgoingLastSecond.value = emptyMap()
|
||||
_perPeerIncomingLastSecond.value = emptyMap()
|
||||
_perPeerOutgoingLastSecond.value = emptyMap()
|
||||
_perDeviceIncomingLastMinute.value = emptyMap()
|
||||
_perDeviceOutgoingLastMinute.value = emptyMap()
|
||||
_perPeerIncomingLastMinute.value = emptyMap()
|
||||
_perPeerOutgoingLastMinute.value = emptyMap()
|
||||
_perDeviceIncomingTotalsFlow.value = emptyMap()
|
||||
_perDeviceOutgoingTotalsFlow.value = emptyMap()
|
||||
_perPeerIncomingTotalsFlow.value = emptyMap()
|
||||
_perPeerOutgoingTotalsFlow.value = emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Data Models
|
||||
@@ -589,6 +583,7 @@ sealed class DebugMessage(val content: String, val timestamp: Date = Date()) {
|
||||
class PeerEvent(content: String) : DebugMessage(content)
|
||||
class PacketEvent(content: String) : DebugMessage(content)
|
||||
class RelayEvent(content: String) : DebugMessage(content)
|
||||
class ValidationEvent(content: String) : DebugMessage(content)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.ui.debug.DebugMessage
|
||||
import com.bitchat.android.ui.debug.DebugSettingsManager
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -23,6 +25,7 @@ class SecurityManagerTest {
|
||||
private lateinit var securityManager: SecurityManager
|
||||
private lateinit var fakeEncryptionService: FakeEncryptionService
|
||||
private lateinit var mockDelegate: SecurityManagerDelegate
|
||||
private lateinit var debugSettingsManager: DebugSettingsManager
|
||||
|
||||
private val myPeerID = "1111222233334444"
|
||||
private val otherPeerID = "aaaabbbbccccdddd"
|
||||
@@ -61,6 +64,10 @@ class SecurityManagerTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
debugSettingsManager = DebugSettingsManager.getInstance()
|
||||
debugSettingsManager.resetForTesting()
|
||||
debugSettingsManager.setVerboseLoggingEnabled(true)
|
||||
|
||||
fakeEncryptionService = FakeEncryptionService()
|
||||
mockDelegate = mock()
|
||||
|
||||
@@ -73,6 +80,9 @@ class SecurityManagerTest {
|
||||
if (::securityManager.isInitialized) {
|
||||
securityManager.shutdown()
|
||||
}
|
||||
if (::debugSettingsManager.isInitialized) {
|
||||
debugSettingsManager.resetForTesting()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,6 +98,7 @@ class SecurityManagerTest {
|
||||
val result = securityManager.validatePacket(packet, otherPeerID)
|
||||
|
||||
assertFalse("Packet without signature should be rejected", result)
|
||||
assertValidationLogContains("missing signature")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -105,6 +116,41 @@ class SecurityManagerTest {
|
||||
val result = securityManager.validatePacket(packet, otherPeerID)
|
||||
|
||||
assertFalse("Packet with invalid signature should be rejected", result)
|
||||
assertValidationLogContains("invalid signature")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - rejects stale timestamp and logs validation failure`() {
|
||||
setupKnownPeer(otherPeerID, otherSigningKey)
|
||||
val staleTimestamp = (System.currentTimeMillis() -
|
||||
com.bitchat.android.util.AppConstants.Security.MESSAGE_TIMEOUT_MS -
|
||||
1_000L).toULong()
|
||||
|
||||
val packet = packetWithTimestamp(staleTimestamp)
|
||||
packet.signature = validSignature
|
||||
|
||||
val result = securityManager.validatePacket(packet, otherPeerID)
|
||||
|
||||
assertFalse("Packet with stale timestamp should be rejected", result)
|
||||
assertValidationLogContains("stale timestamp")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validatePacket - invalid signature does not poison duplicate cache`() {
|
||||
setupKnownPeer(otherPeerID, otherSigningKey)
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 10u,
|
||||
senderID = otherPeerID,
|
||||
payload = dummyPayload
|
||||
)
|
||||
packet.signature = invalidSignature
|
||||
|
||||
assertFalse("Invalid signed packet should be rejected", securityManager.validatePacket(packet, otherPeerID))
|
||||
|
||||
packet.signature = validSignature
|
||||
assertTrue("Valid retry should not be rejected as a duplicate", securityManager.validatePacket(packet, otherPeerID))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -283,4 +329,34 @@ class SecurityManagerTest {
|
||||
)
|
||||
whenever(mockDelegate.getPeerInfo(peerID)).thenReturn(info)
|
||||
}
|
||||
|
||||
private fun assertValidationLogContains(expected: String) {
|
||||
val validationMessages = debugSettingsManager.debugMessages.value
|
||||
.filterIsInstance<DebugMessage.ValidationEvent>()
|
||||
|
||||
assertTrue(
|
||||
"Expected validation log containing '$expected', got ${validationMessages.map { it.content }}",
|
||||
validationMessages.any { it.content.contains(expected, ignoreCase = true) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun packetWithTimestamp(timestamp: ULong): BitchatPacket {
|
||||
return BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.MESSAGE.value,
|
||||
senderID = peerIDBytes(otherPeerID),
|
||||
recipientID = null,
|
||||
timestamp = timestamp,
|
||||
payload = dummyPayload,
|
||||
signature = null,
|
||||
ttl = 10u
|
||||
)
|
||||
}
|
||||
|
||||
private fun peerIDBytes(peerID: String): ByteArray {
|
||||
return peerID.chunked(2)
|
||||
.take(8)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user