Merge 38ff6b522a72e8ea88a21fa256beb3fbc58d7777 into c127eb83ab94c069c32d37530d2faecd381cd2a8

This commit is contained in:
Taksh Kothari 2026-09-14 09:43:25 +05:30 committed by GitHub
commit 3bbf2e0dcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 103 additions and 1 deletions

View File

@ -8,7 +8,11 @@ import com.bitchat.android.util.toHexString
/** Canonical, side-effect-free preflight for a self-signed mesh announcement. */
object AnnouncementIdentityValidator {
private const val MAX_CLOCK_SKEW_MS = 10 * 60 * 1_000L
/**
* Clock skew tolerated on a signed announcement. Shared with [SecurityManager] so no packet
* type is held to a stricter future bound than the announcement that admits the peer.
*/
internal const val MAX_CLOCK_SKEW_MS = 10 * 60 * 1_000L
fun verify(
packet: BitchatPacket,

View File

@ -58,6 +58,29 @@ class SecurityManager(private val encryptionService: EncryptionService, private
val currentTime = System.currentTimeMillis()
val messageType = MessageType.fromValue(packet.type)
// A signed packet's timestamp is authenticated as the sender's choice, not sanity-checked,
// and it survives into every downstream ordering decision. GossipSyncManager builds its
// sync filter from `sortByDescending { it.timestamp }`, so future-dated broadcasts take
// every advertised slot and crowd real traffic out of gossip sync across the mesh; the
// public timeline orders on the same field. Only ANNOUNCE was bounded, in
// AnnouncementIdentityValidator.
//
// Bound the future direction for every type, at the same allowance that admits the peer's
// announcement — a device skewed further than this already cannot be verified, so nothing
// that works today starts failing. The past stays open: store-and-forward replays packets
// cached for up to twelve hours.
val nowForSkew = currentTime.coerceAtLeast(0).toULong()
if (packet.timestamp > nowForSkew &&
packet.timestamp - nowForSkew >
AnnouncementIdentityValidator.MAX_CLOCK_SKEW_MS.toULong()
) {
Log.w(
TAG,
"Dropping future-dated ${messageType?.name ?: packet.type} from $peerID"
)
return false
}
// LEAVE mutates presence immediately and cannot be safely replayed after the in-memory
// duplicate cache expires (or after an app restart). Bound it to the same five-minute
// security window used for duplicate retention while tolerating symmetric clock skew.

View File

@ -276,6 +276,81 @@ class SecurityManagerTest {
assertFalse("Future-dated LEAVE must not extend its replay lifetime", securityManager.validatePacket(future, otherPeerID))
}
private fun signedMessage(offsetMs: Long): BitchatPacket = BitchatPacket(
type = MessageType.MESSAGE.value,
ttl = 7u,
senderID = MeshPacketUtils.hexStringToByteArray(otherPeerID),
timestamp = (System.currentTimeMillis() + offsetMs).toULong(),
payload = dummyPayload
).also { it.signature = validSignature }
@Test
fun `validatePacket rejects future-dated broadcast messages`() {
setupKnownPeer(otherPeerID, otherSigningKey)
// GossipSyncManager advertises the newest packets first, so a future-dated broadcast
// takes a sync slot permanently and crowds real traffic out across the mesh.
val future = signedMessage(AnnouncementIdentityValidator.MAX_CLOCK_SKEW_MS + 60_000L)
assertFalse(
"A signed but future-dated MESSAGE must not be admitted",
securityManager.validatePacket(future, otherPeerID)
)
}
@Test
fun `validatePacket admits messages inside the announcement clock-skew allowance`() {
setupKnownPeer(otherPeerID, otherSigningKey)
// A peer skewed further than this cannot get its ANNOUNCE accepted either, so the bound
// must not be tighter than the one that admits the peer in the first place.
val skewed = signedMessage(AnnouncementIdentityValidator.MAX_CLOCK_SKEW_MS - 60_000L)
assertTrue(
"A tolerable forward clock skew must still deliver",
securityManager.validatePacket(skewed, otherPeerID)
)
}
@Test
fun `validatePacket still admits old messages for store and forward`() {
setupKnownPeer(otherPeerID, otherSigningKey)
// StoreForward caches for 12 hours; bounding the past would silently drop that replay.
val old = signedMessage(-6 * 60 * 60 * 1_000L)
assertTrue(
"Store-and-forward replay must keep working",
securityManager.validatePacket(old, otherPeerID)
)
}
@Test
fun `validatePacket rejects future-dated packets of every relayed type`() {
setupKnownPeer(otherPeerID, otherSigningKey)
val futureOffset = AnnouncementIdentityValidator.MAX_CLOCK_SKEW_MS + 60_000L
listOf(
MessageType.MESSAGE,
MessageType.FILE_TRANSFER,
MessageType.VOICE_FRAME,
MessageType.FRAGMENT
).forEach { type ->
val packet = BitchatPacket(
type = type.value,
ttl = 7u,
senderID = MeshPacketUtils.hexStringToByteArray(otherPeerID),
timestamp = (System.currentTimeMillis() + futureOffset).toULong(),
payload = dummyPayload
).also { it.signature = validSignature }
assertFalse(
"${type.name} must not be admitted with a future timestamp",
securityManager.validatePacket(packet, otherPeerID)
)
}
}
@Test
fun `validatePacket - accepts ANNOUNCE packet from unknown peer (extracts key)`() {
val announcement = IdentityAnnouncement(