mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-09-19 04:59:59 +00:00
Merge 38ff6b522a72e8ea88a21fa256beb3fbc58d7777 into c127eb83ab94c069c32d37530d2faecd381cd2a8
This commit is contained in:
commit
3bbf2e0dcc
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user