From 5e4afae4cd0c62380c2d91218704431d2ff9a96c Mon Sep 17 00:00:00 2001 From: Taksh Date: Sun, 16 Aug 2026 08:30:56 +0530 Subject: [PATCH] Reject future-dated packets, not just future-dated LEAVE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validatePacket bounds the timestamp of exactly one packet type. LEAVE gets a symmetric five-minute window; ANNOUNCE gets one in AnnouncementIdentityValidator. Everything else — MESSAGE, FRAGMENT, FILE_TRANSFER, VOICE_FRAME — is admitted with any timestamp its sender signs, because a signature authenticates the sender's choice without sanity-checking it. That timestamp then decides ordering downstream. GossipSyncManager builds its advertised sync filter with sortByDescending { it.timestamp } and takes the top N, so broadcasts stamped far in the future occupy every slot and keep real traffic out of gossip sync — not just locally, but for every neighbor that syncs against us. The public timeline orders on the same field. Bound the future direction for every type at the allowance that already admits the peer's announcement. A device skewed further than that cannot get an ANNOUNCE verified, so it has no working session today and nothing that currently works starts failing. LEAVE keeps its tighter symmetric window. The past deliberately stays unbounded: store-and-forward replays packets cached for up to twelve hours, and bounding it would drop them silently. --- .../mesh/AnnouncementIdentityValidator.kt | 6 ++++- .../bitchat/android/mesh/SecurityManager.kt | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/AnnouncementIdentityValidator.kt b/app/src/main/java/com/bitchat/android/mesh/AnnouncementIdentityValidator.kt index 89b833b1..dfe146e5 100644 --- a/app/src/main/java/com/bitchat/android/mesh/AnnouncementIdentityValidator.kt +++ b/app/src/main/java/com/bitchat/android/mesh/AnnouncementIdentityValidator.kt @@ -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, 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 3f684679..f392277e 100644 --- a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -57,6 +57,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.