This commit is contained in:
CC
2026-06-10 14:14:28 +02:00
parent 2bd2751a6a
commit fc38a8c6b6
57 changed files with 1157 additions and 621 deletions
@@ -4,6 +4,7 @@ package com.bitchat.android.mesh
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.util.PeerId
import com.bitchat.android.util.toHexString
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
@@ -37,8 +38,8 @@ class PacketRelayManagerTest {
private fun createPacket(route: List<ByteArray>?, recipient: String? = null): BitchatPacket {
return BitchatPacket(
type = MessageType.MESSAGE.value,
senderID = hexStringToPeerBytes(otherPeerID),
recipientID = recipient?.let { hexStringToPeerBytes(it) },
senderID = PeerId.toBytes(otherPeerID),
recipientID = recipient?.let { PeerId.toBytes(it) },
timestamp = System.currentTimeMillis().toULong(),
payload = "hello".toByteArray(),
ttl = 5u,
@@ -49,8 +50,8 @@ class PacketRelayManagerTest {
@Test
fun `packet with duplicate hops is dropped`() = runTest {
val route = listOf(
hexStringToPeerBytes(nextHopPeerID),
hexStringToPeerBytes(nextHopPeerID)
PeerId.toBytes(nextHopPeerID),
PeerId.toBytes(nextHopPeerID)
)
val packet = createPacket(route)
val routedPacket = RoutedPacket(packet, otherPeerID)
@@ -64,8 +65,8 @@ class PacketRelayManagerTest {
@Test
fun `valid source-routed packet is relayed to next hop`() = runTest {
val route = listOf(
hexStringToPeerBytes(myPeerID),
hexStringToPeerBytes(nextHopPeerID)
PeerId.toBytes(myPeerID),
PeerId.toBytes(nextHopPeerID)
)
val packet = createPacket(route, finalRecipientID)
val routedPacket = RoutedPacket(packet, otherPeerID)
@@ -80,7 +81,7 @@ class PacketRelayManagerTest {
@Test
fun `last hop does not relay further`() = runTest {
val route = listOf(
hexStringToPeerBytes(myPeerID)
PeerId.toBytes(myPeerID)
)
val packet = createPacket(route, finalRecipientID)
val routedPacket = RoutedPacket(packet, otherPeerID)
@@ -103,15 +104,4 @@ class PacketRelayManagerTest {
verify(delegate).broadcastPacket(any())
}
private fun hexStringToPeerBytes(hex: String): ByteArray {
val result = ByteArray(8)
var idx = 0
var out = 0
while (idx + 1 < hex.length && out < 8) {
val b = hex.substring(idx, idx + 2).toIntOrNull(16)?.toByte() ?: 0
result[out++] = b
idx += 2
}
return result
}
}
@@ -0,0 +1,110 @@
package com.bitchat.android.model
import com.bitchat.android.protocol.TlvLengthSize
import com.bitchat.android.protocol.TlvWriter
import com.bitchat.android.services.meshgraph.GossipTLV
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
class TlvPacketRegressionTest {
@Test
fun `identity announcement skips unknown tlv fields`() {
val noiseKey = ByteArray(32) { 0x11 }
val signingKey = ByteArray(32) { 0x22 }
val encoded = TlvWriter()
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.ONE_BYTE)
.put(0x01, "alice".toByteArray(), TlvLengthSize.ONE_BYTE)
.put(0x02, noiseKey, TlvLengthSize.ONE_BYTE)
.put(0x03, signingKey, TlvLengthSize.ONE_BYTE)
.toByteArray()
val decoded = IdentityAnnouncement.decode(encoded)
assertNotNull(decoded)
assertEquals("alice", decoded!!.nickname)
assertArrayEquals(noiseKey, decoded.noisePublicKey)
assertArrayEquals(signingKey, decoded.signingPublicKey)
}
@Test
fun `private message rejects unknown tlv fields`() {
val encoded = TlvWriter()
.put(0x00, "msg-1".toByteArray(), TlvLengthSize.ONE_BYTE)
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.ONE_BYTE)
.put(0x01, "hello".toByteArray(), TlvLengthSize.ONE_BYTE)
.toByteArray()
assertNull(PrivateMessagePacket.decode(encoded))
}
@Test
fun `request sync skips unknown tlv fields and enforces payload cap`() {
val encoded = TlvWriter()
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.TWO_BYTES)
.put(0x01, byteArrayOf(4), TlvLengthSize.TWO_BYTES)
.put(0x02, byteArrayOf(0, 0, 0, 16), TlvLengthSize.TWO_BYTES)
.put(0x03, byteArrayOf(1, 2, 3), TlvLengthSize.TWO_BYTES)
.toByteArray()
val tooLarge = TlvWriter()
.put(0x01, byteArrayOf(4), TlvLengthSize.TWO_BYTES)
.put(0x02, byteArrayOf(0, 0, 0, 16), TlvLengthSize.TWO_BYTES)
.put(0x03, ByteArray(RequestSyncPacket.MAX_ACCEPT_FILTER_BYTES + 1), TlvLengthSize.TWO_BYTES)
.toByteArray()
val decoded = RequestSyncPacket.decode(encoded)
assertNotNull(decoded)
assertEquals(4, decoded!!.p)
assertEquals(16L, decoded.m)
assertArrayEquals(byteArrayOf(1, 2, 3), decoded.data)
assertNull(RequestSyncPacket.decode(tooLarge))
}
@Test
fun `file packet uses four byte content length for large payloads`() {
val content = ByteArray(70_000) { (it % 251).toByte() }
val packet = BitchatFilePacket(
fileName = "large.bin",
fileSize = content.size.toLong(),
mimeType = "application/octet-stream",
content = content
)
val encoded = packet.encode()
val decoded = BitchatFilePacket.decode(encoded!!)
val contentTypeOffset = encoded.indexOf(0x04.toByte())
val contentLength =
((encoded[contentTypeOffset + 1].toInt() and 0xFF) shl 24) or
((encoded[contentTypeOffset + 2].toInt() and 0xFF) shl 16) or
((encoded[contentTypeOffset + 3].toInt() and 0xFF) shl 8) or
(encoded[contentTypeOffset + 4].toInt() and 0xFF)
assertEquals(content.size, contentLength)
assertNotNull(decoded)
assertArrayEquals(content, decoded!!.content)
}
@Test
fun `file packet rejects unknown tlv fields`() {
val encoded = TlvWriter()
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.TWO_BYTES)
.put(0x01, "x.bin".toByteArray(), TlvLengthSize.TWO_BYTES)
.put(0x02, byteArrayOf(0, 0, 0, 1), TlvLengthSize.TWO_BYTES)
.put(0x03, "application/octet-stream".toByteArray(), TlvLengthSize.TWO_BYTES)
.put(0x04, byteArrayOf(1), TlvLengthSize.FOUR_BYTES)
.toByteArray()
assertNull(BitchatFilePacket.decode(encoded))
}
@Test
fun `gossip tlv uses shared peer id and tlv helpers`() {
val encoded = GossipTLV.encodeNeighbors(listOf("0102030405060708", "aabbcc"))
assertEquals(listOf("0102030405060708", "aabbcc0000000000"), GossipTLV.decodeNeighborsFromAnnouncementPayload(encoded))
}
}
@@ -0,0 +1,74 @@
package com.bitchat.android.protocol
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class TlvTest {
@Test
fun `writer and reader support one two and four byte lengths`() {
val oneByteValue = byteArrayOf(0x7f)
val twoByteValue = ByteArray(256) { it.toByte() }
val fourByteValue = ByteArray(70_000) { (it % 251).toByte() }
val encoded = TlvWriter()
.put(0x01, oneByteValue, TlvLengthSize.ONE_BYTE)
.put(0x02, twoByteValue, TlvLengthSize.TWO_BYTES)
.put(0x03, fourByteValue, TlvLengthSize.FOUR_BYTES)
.toByteArray()
val fields = TlvReader.decode(encoded, TlvLengthSize.ONE_BYTE) { type ->
when (type) {
0x02 -> TlvLengthSize.TWO_BYTES
0x03 -> TlvLengthSize.FOUR_BYTES
else -> TlvLengthSize.ONE_BYTE
}
}
assertEquals(3, fields!!.size)
assertArrayEquals(oneByteValue, fields[0].value)
assertArrayEquals(twoByteValue, fields[1].value)
assertArrayEquals(fourByteValue, fields[2].value)
}
@Test
fun `unknown tlv policy skips or fails consistently`() {
val encoded = TlvWriter()
.put(0x01, byteArrayOf(1), TlvLengthSize.ONE_BYTE)
.put(0x7f, byteArrayOf(9), TlvLengthSize.ONE_BYTE)
.put(0x02, byteArrayOf(2), TlvLengthSize.ONE_BYTE)
.toByteArray()
val skipped = TlvReader.decode(
data = encoded,
defaultLengthSize = TlvLengthSize.ONE_BYTE,
unknownPolicy = UnknownTlvPolicy.SKIP,
knownTypes = setOf(0x01, 0x02)
)
val failed = TlvReader.decode(
data = encoded,
defaultLengthSize = TlvLengthSize.ONE_BYTE,
unknownPolicy = UnknownTlvPolicy.FAIL,
knownTypes = setOf(0x01, 0x02)
)
assertEquals(listOf(0x01, 0x02), skipped!!.map { it.type })
assertNull(failed)
}
@Test
fun `reader rejects truncated values and writer rejects oversized values`() {
val truncated = byteArrayOf(0x01, 0x03, 0x41)
assertNull(TlvReader.decode(truncated, TlvLengthSize.ONE_BYTE))
try {
TlvWriter().put(0x01, ByteArray(256), TlvLengthSize.ONE_BYTE)
throw AssertionError("Expected oversized one-byte TLV value to fail")
} catch (_: IllegalArgumentException) {
// Expected.
}
}
}
@@ -0,0 +1,94 @@
package com.bitchat.android.ui.debug
import android.os.Build
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.P], manifest = Config.NONE)
class DebugSettingsManagerTest {
private lateinit var manager: DebugSettingsManager
@Before
fun setup() {
manager = DebugSettingsManager.getInstance()
manager.resetForTesting()
manager.setVerboseLoggingEnabled(true)
}
@After
fun tearDown() {
manager.resetForTesting()
}
@Test
fun `logIncoming emits one typed packet trace`() {
val packet = BitchatPacket(
type = MessageType.MESSAGE.value,
ttl = 7u,
senderID = "aaaabbbbccccdddd",
payload = byteArrayOf(1, 2, 3)
)
manager.logIncoming(
packet = packet,
fromPeerID = "aaaabbbbccccdddd",
fromNickname = "alice",
fromDeviceAddress = "AA:BB:CC:DD:EE:FF",
myPeerID = "1111222233334444"
)
val packetMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.PacketEvent>()
assertEquals("Incoming packet should create one packet event", 1, packetMessages.size)
assertTrue(packetMessages.single().content.contains("Incoming"))
assertTrue(packetMessages.single().content.contains("MESSAGE"))
}
@Test
fun `logOutgoing relay emits one relay event without packet duplicate`() {
manager.logOutgoing(
packetType = "MESSAGE",
toPeerID = "bbbbccccddddeeee",
toNickname = "bob",
toDeviceAddress = "11:22:33:44:55:66",
previousHopPeerID = "aaaabbbbccccdddd",
packetVersion = 1u,
routeInfo = "routed: 2 hops",
ttl = 6u
)
val packetMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.PacketEvent>()
val relayMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.RelayEvent>()
assertEquals("Relay send should not also create a packet event", 0, packetMessages.size)
assertEquals("Relay send should create one relay event", 1, relayMessages.size)
assertTrue(relayMessages.single().content.contains("Relay"))
assertTrue(relayMessages.single().content.contains("MESSAGE"))
}
@Test
fun `validation failures are hidden when verbose logging is disabled`() {
manager.setVerboseLoggingEnabled(false)
val packet = BitchatPacket(
type = MessageType.MESSAGE.value,
ttl = 7u,
senderID = "aaaabbbbccccdddd",
payload = byteArrayOf(1)
)
manager.logPacketValidationFailure(packet, "aaaabbbbccccdddd", "invalid signature")
val validationMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.ValidationEvent>()
assertEquals(0, validationMessages.size)
}
}
@@ -0,0 +1,58 @@
package com.bitchat.android.util
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class HexPeerHashingTest {
@Test
fun `hex encodes lowercase and decodes mixed case`() {
val bytes = byteArrayOf(0x00, 0x0f, 0x10, 0x7f, 0xff.toByte())
assertEquals("000f107fff", Hex.encode(bytes))
assertArrayEquals(bytes, Hex.decode("000F107fFF"))
assertEquals("000f107fff", bytes.toHexString())
assertArrayEquals(bytes, "000f107fff".hexToByteArray())
}
@Test
fun `hex decode rejects malformed input unless odd length is explicitly allowed`() {
assertNull(Hex.decode("abc"))
assertArrayEquals(byteArrayOf(0x0a, 0xbc.toByte()), Hex.decode("abc", allowOddLength = true))
assertNull(Hex.decode("00xx"))
assertNull("00xx".hexToByteArrayOrNull())
}
@Test
fun `peer id parsing is fixed width with truncation and zero fill`() {
assertArrayEquals(
byteArrayOf(0x11, 0x22, 0x00, 0x44, 0, 0, 0, 0),
PeerId.toBytes("1122zz44")
)
assertArrayEquals(
byteArrayOf(0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77),
PeerId.toBytes("00112233445566778899")
)
assertEquals("1122004400000000", PeerId.normalize("1122zz44"))
}
@Test
fun `peer id strict parse requires exactly eight valid bytes`() {
val bytes = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7)
assertArrayEquals(bytes, PeerId.parse("0001020304050607"))
assertEquals("0001020304050607", PeerId.fromBytes(bytes))
assertNull(PeerId.parse("000102"))
assertNull(PeerId.parse("00010203040506xx"))
}
@Test
fun `hashing sha256 hex matches known vector`() {
assertEquals(
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
Hashing.sha256Hex("abc")
)
}
}