Merge pull request #882: strengthen packet deduplication

Use full-payload packet IDs for duplicate detection and simplify the explanatory comment.
This commit is contained in:
callebtc 2026-09-07 23:33:41 +03:00 committed by GitHub
commit 010789adde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 14 deletions

View File

@ -3,6 +3,7 @@ package com.bitchat.android.mesh
import android.util.Log
import com.bitchat.android.crypto.EncryptionService
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.sync.PacketIdUtil
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.noise.AuthenticatedNoiseSession
@ -238,21 +239,9 @@ class SecurityManager(private val encryptionService: EncryptionService, private
return encryptionService.getCombinedPublicKeyData()
}
/**
* Generate message ID for duplicate detection
*/
/** Deduplicates by peer and a hash of packet type, sender, timestamp, and full payload. */
private fun generateMessageID(packet: BitchatPacket, peerID: String): String {
return when (MessageType.fromValue(packet.type)) {
MessageType.FRAGMENT -> {
// For fragments, include the payload hash to distinguish different fragments
"${packet.timestamp}-$peerID-${packet.type}-${packet.payload.contentHashCode()}"
}
else -> {
// For other messages, use a truncated payload hash
val payloadHash = packet.payload.sliceArray(0 until minOf(64, packet.payload.size)).contentHashCode()
"${packet.timestamp}-$peerID-$payloadHash"
}
}
return "$peerID-${PacketIdUtil.computeIdHex(packet)}"
}
/**

View File

@ -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<BitchatPacket, BitchatPacket> {
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()
}