mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
240 lines
9.0 KiB
Kotlin
240 lines
9.0 KiB
Kotlin
package com.bitchat.android.mesh
|
|
|
|
import android.os.Build
|
|
import com.bitchat.android.model.IdentityAnnouncement
|
|
import com.bitchat.android.model.BitchatFilePacket
|
|
import com.bitchat.android.model.PeerCapabilities
|
|
import com.bitchat.android.model.RoutedPacket
|
|
import com.bitchat.android.protocol.BitchatPacket
|
|
import com.bitchat.android.protocol.MessageType
|
|
import com.bitchat.android.protocol.SpecialRecipients
|
|
import com.bitchat.android.services.meshgraph.MeshGraphService
|
|
import com.bitchat.android.util.AppConstants
|
|
import kotlinx.coroutines.runBlocking
|
|
import org.junit.After
|
|
import org.junit.Assert.assertFalse
|
|
import org.junit.Assert.assertTrue
|
|
import org.junit.Before
|
|
import org.junit.Test
|
|
import org.junit.runner.RunWith
|
|
import org.mockito.kotlin.any
|
|
import org.mockito.kotlin.anyOrNull
|
|
import org.mockito.kotlin.eq
|
|
import org.mockito.kotlin.isNull
|
|
import org.mockito.kotlin.mock
|
|
import org.mockito.kotlin.never
|
|
import org.mockito.kotlin.verify
|
|
import org.mockito.kotlin.whenever
|
|
import org.robolectric.RobolectricTestRunner
|
|
import org.robolectric.RuntimeEnvironment
|
|
import org.robolectric.annotation.Config
|
|
|
|
@RunWith(RobolectricTestRunner::class)
|
|
@Config(sdk = [Build.VERSION_CODES.P], manifest = Config.NONE)
|
|
class MessageHandlerTest {
|
|
private lateinit var handler: MessageHandler
|
|
private lateinit var delegate: MessageHandlerDelegate
|
|
|
|
private val myPeerID = "1111222233334444"
|
|
private val peerID = "aaaabbbbccccdddd"
|
|
private val nickname = "peer"
|
|
private val noiseKey = ByteArray(32) { 0x0B }
|
|
private val signingKey = ByteArray(32) { 0x0A }
|
|
private val signature = ByteArray(64) { 1 }
|
|
private val announceClockSkewToleranceMs = 10 * 60 * 1000L
|
|
|
|
@Before
|
|
fun setup() {
|
|
MeshGraphService.resetForTesting()
|
|
handler = MessageHandler(myPeerID, RuntimeEnvironment.getApplication())
|
|
delegate = mock()
|
|
handler.delegate = delegate
|
|
|
|
whenever(delegate.getPeerInfo(peerID)).thenReturn(null)
|
|
whenever(delegate.verifyEd25519Signature(any(), any(), any())).thenReturn(true)
|
|
whenever(
|
|
delegate.updatePeerInfoFromVerifiedAnnouncement(
|
|
any(), any(), any(), any(), any(), anyOrNull()
|
|
)
|
|
).thenReturn(true)
|
|
}
|
|
|
|
@After
|
|
fun tearDown() {
|
|
MeshGraphService.resetForTesting()
|
|
}
|
|
|
|
@Test
|
|
fun `handleAnnounce accepts announce within clock skew tolerance for identity binding`() {
|
|
runBlocking {
|
|
val packet = announcePacket(ageMs = AppConstants.Mesh.STALE_PEER_TIMEOUT_MS + 1_000)
|
|
|
|
val result = handler.handleAnnounce(RoutedPacket(packet, peerID, "direct-link"))
|
|
|
|
assertTrue("Announce within clock skew tolerance should still store peer identity", result)
|
|
verify(delegate).updatePeerInfoFromVerifiedAnnouncement(
|
|
eq(peerID), eq(nickname), any(), any(), eq(true), isNull()
|
|
)
|
|
verify(delegate).onVerifiedAnnouncementProcessed(peerID)
|
|
verify(delegate).updatePeerIDBinding(eq(peerID), eq(nickname), any(), isNull())
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `handleAnnounce accepts future announce within clock skew tolerance`() {
|
|
runBlocking {
|
|
val packet = announcePacket(ageMs = -(AppConstants.Mesh.STALE_PEER_TIMEOUT_MS + 1_000))
|
|
|
|
val result = handler.handleAnnounce(RoutedPacket(packet, peerID, "direct-link"))
|
|
|
|
assertTrue("Future announce within clock skew tolerance should still store peer identity", result)
|
|
verify(delegate).updatePeerInfoFromVerifiedAnnouncement(
|
|
eq(peerID), eq(nickname), any(), any(), eq(true), isNull()
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `handleAnnounce stores advertised capabilities including unknown bits`() {
|
|
runBlocking {
|
|
val capabilities = PeerCapabilities(
|
|
PeerCapabilities.PRIVATE_MEDIA.rawValue or (1L shl 15)
|
|
)
|
|
val packet = announcePacket(ageMs = 0, capabilities = capabilities)
|
|
|
|
val result = handler.handleAnnounce(RoutedPacket(packet, peerID, "direct-link"))
|
|
|
|
assertTrue(result)
|
|
verify(delegate).updatePeerInfoFromVerifiedAnnouncement(
|
|
eq(peerID),
|
|
eq(nickname),
|
|
any(),
|
|
any(),
|
|
eq(true),
|
|
eq(capabilities)
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `handleAnnounce rejects announce older than clock skew tolerance`() {
|
|
runBlocking {
|
|
val packet = announcePacket(ageMs = announceClockSkewToleranceMs + 1_000)
|
|
|
|
val result = handler.handleAnnounce(RoutedPacket(packet, peerID, "relay-link"))
|
|
|
|
assertFalse("Announce older than clock skew tolerance should not store peer identity", result)
|
|
verify(delegate, never()).updatePeerInfoFromVerifiedAnnouncement(
|
|
any(), any(), any(), any(), any(), anyOrNull()
|
|
)
|
|
verify(delegate, never()).onVerifiedAnnouncementProcessed(any())
|
|
verify(delegate, never()).updatePeerIDBinding(any(), any(), any(), any())
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `handleAnnounce never promotes capability after invalid signature`() {
|
|
runBlocking {
|
|
whenever(delegate.verifyEd25519Signature(any(), any(), any())).thenReturn(false)
|
|
val packet = announcePacket(ageMs = 0, capabilities = PeerCapabilities.PRIVATE_MEDIA)
|
|
|
|
val result = handler.handleAnnounce(RoutedPacket(packet, peerID, "direct-link"))
|
|
|
|
assertFalse(result)
|
|
verify(delegate, never()).updatePeerInfoFromVerifiedAnnouncement(
|
|
any(), any(), any(), any(), any(), anyOrNull()
|
|
)
|
|
verify(delegate, never()).onVerifiedAnnouncementProcessed(any())
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `directed raw private media requires a valid signature`() {
|
|
runBlocking {
|
|
whenever(delegate.getBroadcastRecipient()).thenReturn(SpecialRecipients.BROADCAST)
|
|
whenever(delegate.getPeerNickname(peerID)).thenReturn(nickname)
|
|
whenever(delegate.verifySignature(any(), eq(peerID))).thenReturn(false)
|
|
val file = BitchatFilePacket(
|
|
fileName = "legacy.jpg",
|
|
fileSize = 3,
|
|
mimeType = "image/jpeg",
|
|
content = byteArrayOf(1, 2, 3)
|
|
)
|
|
val unsigned = BitchatPacket(
|
|
version = 2u,
|
|
type = MessageType.FILE_TRANSFER.value,
|
|
senderID = peerID.hexToBytes(),
|
|
recipientID = myPeerID.hexToBytes(),
|
|
timestamp = System.currentTimeMillis().toULong(),
|
|
payload = file.encode()!!,
|
|
signature = null,
|
|
ttl = 7u
|
|
)
|
|
|
|
handler.handleMessage(RoutedPacket(unsigned, peerID, "direct-link"))
|
|
handler.handleMessage(
|
|
RoutedPacket(unsigned.copy(signature = ByteArray(64) { 1 }), peerID, "direct-link")
|
|
)
|
|
|
|
verify(delegate, never()).onMessageReceived(any())
|
|
}
|
|
}
|
|
|
|
@Test
|
|
fun `valid signed directed raw private media remains interoperable`() {
|
|
runBlocking {
|
|
whenever(delegate.getBroadcastRecipient()).thenReturn(SpecialRecipients.BROADCAST)
|
|
whenever(delegate.getPeerNickname(peerID)).thenReturn(nickname)
|
|
whenever(delegate.verifySignature(any(), eq(peerID))).thenReturn(true)
|
|
val file = BitchatFilePacket(
|
|
fileName = "legacy-valid.jpg",
|
|
fileSize = 3,
|
|
mimeType = "image/jpeg",
|
|
content = byteArrayOf(1, 2, 3)
|
|
)
|
|
val packet = BitchatPacket(
|
|
version = 2u,
|
|
type = MessageType.FILE_TRANSFER.value,
|
|
senderID = peerID.hexToBytes(),
|
|
recipientID = myPeerID.hexToBytes(),
|
|
timestamp = System.currentTimeMillis().toULong(),
|
|
payload = file.encode()!!,
|
|
signature = signature,
|
|
ttl = 7u
|
|
)
|
|
|
|
handler.handleMessage(RoutedPacket(packet, peerID, "direct-link"))
|
|
|
|
verify(delegate).verifySignature(packet, peerID)
|
|
verify(delegate).onMessageReceived(any())
|
|
}
|
|
}
|
|
|
|
private fun announcePacket(
|
|
ageMs: Long,
|
|
ttl: UByte = (AppConstants.MESSAGE_TTL_HOPS.toInt() - 1).toUByte(),
|
|
capabilities: PeerCapabilities? = null
|
|
): BitchatPacket {
|
|
val announcement = IdentityAnnouncement(
|
|
nickname = nickname,
|
|
noisePublicKey = noiseKey,
|
|
signingPublicKey = signingKey,
|
|
capabilities = capabilities
|
|
)
|
|
return BitchatPacket(
|
|
version = 1u,
|
|
type = MessageType.ANNOUNCE.value,
|
|
senderID = peerID.hexToBytes(),
|
|
recipientID = SpecialRecipients.BROADCAST,
|
|
timestamp = (System.currentTimeMillis() - ageMs).toULong(),
|
|
payload = announcement.encode()!!,
|
|
signature = signature,
|
|
ttl = ttl
|
|
)
|
|
}
|
|
|
|
private fun String.hexToBytes(): ByteArray {
|
|
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
|
}
|
|
}
|