From 4ef1b9c76576a7ddb5d857de17fcc3632bc39e5c Mon Sep 17 00:00:00 2001 From: Taksh Date: Sat, 15 Aug 2026 13:58:41 +0530 Subject: [PATCH] Pin the collision, the replay, and the peer scoping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collision case fails on main: two packets sharing a 64-byte prefix and a timestamp, where the second was silently dropped. The other two pass before and after on purpose. Replay of an identical packet must still be caught, and the same packet arriving from two different peers must still be tracked separately — strengthening the identity must not quietly weaken either. --- .../android/mesh/SecurityManagerTest.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) 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 2a56e708..e43e11b5 100644 --- a/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt +++ b/app/src/test/kotlin/com/bitchat/android/mesh/SecurityManagerTest.kt @@ -598,4 +598,65 @@ class SecurityManagerTest { private fun String.hexToBytes(): ByteArray = chunked(2).map { it.toInt(16).toByte() }.toByteArray() + + // Duplicate-detection identity. + + /** + * Two distinct packets that agree on their first 64 bytes and share a + * timestamp. The old key hashed only that prefix, with a 32-bit + * `contentHashCode`, so these were "the same packet" and the second was + * silently dropped. + */ + private fun prefixSharingPair(): Pair { + val shared = ByteArray(64) { 0x7 } + val first = BitchatPacket( + version = 1u, + type = MessageType.NOISE_ENCRYPTED.value, + senderID = otherPeerID.hexToByteArrayForTest(), + recipientID = myPeerID.hexToByteArrayForTest(), + timestamp = 1_700_000_000_000uL, + payload = shared + byteArrayOf(0x01, 0x02, 0x03), + ttl = 7u + ) + val second = first.copy(payload = shared + byteArrayOf(0x0A, 0x0B, 0x0C)) + return first to second + } + + @Test + fun `packets differing only past the first 64 bytes are not treated as duplicates`() { + val (first, second) = prefixSharingPair() + + assertTrue(securityManager.validatePacket(first, otherPeerID)) + assertTrue( + "A distinct packet must not be dropped as a duplicate", + securityManager.validatePacket(second, otherPeerID) + ) + } + + @Test + fun `a genuine replay of the same packet is still rejected`() { + // The other half: strengthening the identity must not weaken replay + // protection, which is the reason this cache exists. + val (first, _) = prefixSharingPair() + + assertTrue(securityManager.validatePacket(first, otherPeerID)) + assertFalse( + "The identical packet must still be caught", + securityManager.validatePacket(first, otherPeerID) + ) + } + + @Test + fun `the same packet from two different peers is tracked separately`() { + // Peer scoping is deliberately kept: PacketIdUtil covers the packet's + // own senderID, which is not the peer it was received from once a + // packet has been relayed. + val (first, _) = prefixSharingPair() + + assertTrue(securityManager.validatePacket(first, otherPeerID)) + assertTrue(securityManager.validatePacket(first, unknownPeerID)) + } + + private fun String.hexToByteArrayForTest(): ByteArray = + chunked(2).map { it.toInt(16).toByte() }.toByteArray() }