diff --git a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt index 3a10f998..ada67f92 100644 --- a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt @@ -3,7 +3,6 @@ package com.bitchat.android.mesh import android.util.Log import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType -import com.bitchat.android.protocol.MessagePadding import com.bitchat.android.model.FragmentPayload import kotlinx.coroutines.* import java.util.concurrent.ConcurrentHashMap @@ -66,20 +65,19 @@ class FragmentManager { Log.w(TAG, "Rejecting invalid outbound fragment limit: $maxFragments") return emptyList() } - val encoded = packet.toBinaryData() - if (encoded == null) { + // Fragment the unpadded frame; each fragment will be encoded (and padded) independently - iOS fix. + // Ask the encoder to skip padding rather than un-padding afterwards: + // pad() declines a request needing more than 255 bytes and + // optimalBlockSize() returns the input size above 2048, so frames of + // 513-768, 1009-1792 and >=2033 bytes are never padded. Those end + // with the last byte of the signature, and unpad() reads a value of + // 0x01 as a one-byte pad and strips a real signature byte. + val fullData = packet.toBinaryData(padding = false) + if (fullData == null) { Log.e(TAG, "Failed to encode packet to binary data") return emptyList() } - // Fragment the unpadded frame; each fragment will be encoded (and padded) independently - iOS fix - val fullData = try { - MessagePadding.unpad(encoded) - } catch (e: Exception) { - Log.e(TAG, "Failed to unpad data: ${e.message}", e) - return emptyList() - } - // iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) { return listOf(packet) // No fragmentation needed diff --git a/app/src/test/java/com/bitchat/android/mesh/FragmentManagerTest.kt b/app/src/test/java/com/bitchat/android/mesh/FragmentManagerTest.kt index bf3e3d01..a0be567e 100644 --- a/app/src/test/java/com/bitchat/android/mesh/FragmentManagerTest.kt +++ b/app/src/test/java/com/bitchat/android/mesh/FragmentManagerTest.kt @@ -3,6 +3,7 @@ package com.bitchat.android.mesh import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType import com.bitchat.android.model.FragmentPayload +import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull @@ -244,6 +245,68 @@ class FragmentManagerTest { assertEquals(257, plan(low).size) } + /** + * pad() declines any request needing more than 255 bytes and + * optimalBlockSize() returns the input size unchanged above 2048, so frames + * of 513-768, 1009-1792 and >=2033 bytes are emitted unpadded. The frame + * then ends with the last byte of the signature, which is uniformly random, + * and a value of 0x01 reads as a one-byte PKCS#7 pad. + */ + @Test + fun `an unpadded frame keeps every byte through fragmentation`() { + for (payloadSize in listOf(600, 1500, 2400)) { + val packet = BitchatPacket( + version = 1u, + type = MessageType.MESSAGE.value, + senderID = hexStringToByteArray(senderID), + recipientID = hexStringToByteArray(recipientID), + timestamp = 1u, + payload = ByteArray(payloadSize).also { Random(payloadSize.toLong()).nextBytes(it) }, + signature = ByteArray(64) { index -> if (index == 63) 0x01 else 0x7F }, + ttl = 7u + ) + + val frame = packet.toBinaryData(padding = false)!! + val padded = packet.toBinaryData()!! + assertEquals("frame of $payloadSize should not be padded", frame.size, padded.size) + + val fragments = fragmentManager.createFragments(packet) + assertTrue("frame of $payloadSize should fragment", fragments.size > 1) + + val reassembled = fragments + .map { FragmentPayload.decode(it.payload)!!.data } + .reduce { acc, next -> acc + next } + assertArrayEquals( + "fragments of a $payloadSize-byte payload must carry the whole frame", + frame, + reassembled + ) + } + } + + /** A frame that really was padded must still fragment unpadded. */ + @Test + fun `a padded frame is fragmented without its padding`() { + val packet = BitchatPacket( + version = 1u, + type = MessageType.MESSAGE.value, + senderID = hexStringToByteArray(senderID), + recipientID = hexStringToByteArray(recipientID), + timestamp = 1u, + payload = ByteArray(900).also { Random(900).nextBytes(it) }, + signature = ByteArray(64) { 0x7F }, + ttl = 7u + ) + + val frame = packet.toBinaryData(padding = false)!! + assertTrue("frame should be padded", packet.toBinaryData()!!.size > frame.size) + + val reassembled = fragmentManager.createFragments(packet) + .map { FragmentPayload.decode(it.payload)!!.data } + .reduce { acc, next -> acc + next } + assertArrayEquals(frame, reassembled) + } + private fun hexStringToByteArray(hexString: String): ByteArray { val result = ByteArray(8) for (i in 0 until 8) {