cleanup logging

This commit is contained in:
CC
2026-06-10 13:57:03 +02:00
parent b079ae8742
commit 2bd2751a6a
14 changed files with 300 additions and 579 deletions
@@ -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")
}
}
@@ -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;
@@ -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()
}
}