From 171483a1da20fd108c896eb1757c7f601132469c Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:41:17 +0700 Subject: [PATCH] drop duplicate announces based on TTL (#558) --- .../bitchat/android/mesh/SecurityManager.kt | 22 ++++++++----- .../android/mesh/SecurityManagerTest.kt | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt index 7f70a4bb..5beb3384 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -56,18 +56,24 @@ class SecurityManager(private val encryptionService: EncryptionService, private // Duplicate detection val messageID = generateMessageID(packet, peerID) - if (messageType != MessageType.ANNOUNCE) { - if (processedMessages.contains(messageID)) { + + if (processedMessages.contains(messageID)) { + // Check for ANNOUNCE exception: allow if it looks like a direct neighbor (max TTL) + // This ensures we catch the "first announce" on a new connection for binding, + // while still dropping looped/relayed duplicates. + val isFreshAnnounce = messageType == MessageType.ANNOUNCE && + packet.ttl >= com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS + + if (!isFreshAnnounce) { Log.d(TAG, "Dropping duplicate packet: $messageID") return false } - // Add to processed messages - processedMessages.add(messageID) - messageTimestamps[messageID] = currentTime - } else { - // Do not deduplicate ANNOUNCE at the security layer. - // They are signed/idempotent and we need to ensure first-announce per-connection can bind. + Log.d(TAG, "Allowing duplicate ANNOUNCE from direct neighbor: $messageID") } + + // Add to processed messages + processedMessages.add(messageID) + messageTimestamps[messageID] = currentTime // Enforce mandatory signature verification if (!verifyPacketSignature(packet, peerID)) { diff --git a/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt b/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt index b3e18956..aa629ca7 100644 --- a/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt @@ -239,6 +239,37 @@ class SecurityManagerTest { assertFalse("Duplicate packet should be rejected", result2) } + @Test + fun `validatePacket - handles ANNOUNCE duplicates correctly`() { + val announcement = IdentityAnnouncement( + nickname = "New User", + noisePublicKey = otherNoiseKey, + signingPublicKey = otherSigningKey + ) + val payload = announcement.encode()!! + + // 1. Initial Announce (Fresh) + val packet1 = BitchatPacket( + type = MessageType.ANNOUNCE.value, + ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS, // 7u + senderID = unknownPeerID, + payload = payload + ) + packet1.signature = validSignature + + whenever(mockDelegate.getPeerInfo(unknownPeerID)).thenReturn(null) + + assertTrue("First ANNOUNCE should be accepted", securityManager.validatePacket(packet1, unknownPeerID)) + + // 2. Relayed Duplicate (Lower TTL) + val packet2 = packet1.copy(ttl = (com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS - 1u).toUByte()) + assertFalse("Relayed duplicate ANNOUNCE should be rejected", securityManager.validatePacket(packet2, unknownPeerID)) + + // 3. Direct Duplicate (Max TTL) + val packet3 = packet1.copy(ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS) + assertTrue("Fresh duplicate ANNOUNCE should be accepted", securityManager.validatePacket(packet3, unknownPeerID)) + } + private fun setupKnownPeer(peerID: String, signingKey: ByteArray) { val info = PeerInfo( id = peerID,