fix fragmentation baby (#266)

This commit is contained in:
callebtc
2025-08-19 11:16:23 +02:00
committed by GitHub
parent 9795e2ce8a
commit 4acfafb998
3 changed files with 51 additions and 43 deletions
@@ -3,6 +3,7 @@ package com.bitchat.android.mesh
import android.util.Log import android.util.Log
import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType import com.bitchat.android.protocol.MessageType
import com.bitchat.android.protocol.MessagePadding
import com.bitchat.android.model.FragmentPayload import com.bitchat.android.model.FragmentPayload
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -47,7 +48,10 @@ class FragmentManager {
* Matches iOS sendFragmentedPacket() implementation exactly * Matches iOS sendFragmentedPacket() implementation exactly
*/ */
fun createFragments(packet: BitchatPacket): List<BitchatPacket> { fun createFragments(packet: BitchatPacket): List<BitchatPacket> {
val fullData = packet.toBinaryData() ?: return emptyList() val encoded = packet.toBinaryData() ?: return emptyList()
// Fragment the unpadded frame; each fragment will be encoded (and padded) independently - iOS fix
val fullData = MessagePadding.unpad(encoded)
// iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue // iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue
if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) { if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
@@ -150,26 +154,18 @@ class FragmentManager {
} }
} }
// iOS: if let metadata = fragmentMetadata[fragmentID] // Decode the original packet bytes we reassembled, so flags/compression are preserved - iOS fix
val metadata = fragmentMetadata[fragmentIDString] val originalPacket = BitchatPacket.fromBinaryData(reassembledData.toByteArray())
if (metadata != null) { if (originalPacket != null) {
// iOS: let reassembledPacket = BitchatPacket(type: metadata.type, ...)
val reassembledPacket = BitchatPacket(
type = metadata.first,
senderID = packet.senderID,
recipientID = packet.recipientID,
timestamp = packet.timestamp,
payload = reassembledData.toByteArray(),
signature = packet.signature,
ttl = if (packet.ttl > 0u) (packet.ttl - 1u).toUByte() else 0u
)
// iOS cleanup: incomingFragments.removeValue(forKey: fragmentID) // iOS cleanup: incomingFragments.removeValue(forKey: fragmentID)
incomingFragments.remove(fragmentIDString) incomingFragments.remove(fragmentIDString)
fragmentMetadata.remove(fragmentIDString) fragmentMetadata.remove(fragmentIDString)
Log.d(TAG, "Successfully reassembled packet of ${reassembledData.size} bytes") Log.d(TAG, "Successfully reassembled and decoded original packet of ${reassembledData.size} bytes")
return reassembledPacket return originalPacket
} else {
val metadata = fragmentMetadata[fragmentIDString]
Log.e(TAG, "Failed to decode reassembled packet (type=${metadata?.first}, total=${metadata?.second})")
} }
} else { } else {
val received = fragmentMap?.size ?: 0 val received = fragmentMap?.size ?: 0
@@ -248,13 +248,24 @@ object BinaryProtocol {
} }
fun decode(data: ByteArray): BitchatPacket? { fun decode(data: ByteArray): BitchatPacket? {
// Try decode as-is first (robust when padding wasn't applied) - iOS fix
decodeCore(data)?.let { return it }
// If that fails, try after removing padding
val unpadded = MessagePadding.unpad(data)
if (unpadded.contentEquals(data)) return null // No padding was removed, already failed
return decodeCore(unpadded)
}
/**
* Core decoding implementation used by decode() with and without padding removal - iOS fix
*/
private fun decodeCore(raw: ByteArray): BitchatPacket? {
try { try {
// Remove padding first - exactly same as iOS if (raw.size < HEADER_SIZE + SENDER_ID_SIZE) return null
val unpaddedData = MessagePadding.unpad(data)
if (unpaddedData.size < HEADER_SIZE + SENDER_ID_SIZE) return null val buffer = ByteBuffer.wrap(raw).apply { order(ByteOrder.BIG_ENDIAN) }
val buffer = ByteBuffer.wrap(unpaddedData).apply { order(ByteOrder.BIG_ENDIAN) }
// Header // Header
val version = buffer.get().toUByte() val version = buffer.get().toUByte()
@@ -280,7 +291,7 @@ object BinaryProtocol {
if (hasRecipient) expectedSize += RECIPIENT_ID_SIZE if (hasRecipient) expectedSize += RECIPIENT_ID_SIZE
if (hasSignature) expectedSize += SIGNATURE_SIZE if (hasSignature) expectedSize += SIGNATURE_SIZE
if (unpaddedData.size < expectedSize) return null if (raw.size < expectedSize) return null
// SenderID // SenderID
val senderID = ByteArray(SENDER_ID_SIZE) val senderID = ByteArray(SENDER_ID_SIZE)
@@ -30,48 +30,49 @@ object MessagePadding {
} }
/** /**
* Add PKCS#7-style padding to reach target size - exact same as iOS * Add PKCS#7-style padding to reach target size - FIXED: proper PKCS#7 (iOS compatible)
*/ */
fun pad(data: ByteArray, targetSize: Int): ByteArray { fun pad(data: ByteArray, targetSize: Int): ByteArray {
if (data.size >= targetSize) return data if (data.size >= targetSize) return data
val paddingNeeded = targetSize - data.size val paddingNeeded = targetSize - data.size
// PKCS#7 only supports padding up to 255 bytes // Constrain to 255 to fit a single-byte pad length marker
// If we need more padding than that, don't pad - return original data if (paddingNeeded <= 0 || paddingNeeded > 255) return data
if (paddingNeeded > 255) return data
val result = ByteArray(targetSize) val result = ByteArray(targetSize)
// Copy original data // Copy original data
System.arraycopy(data, 0, result, 0, data.size) System.arraycopy(data, 0, result, 0, data.size)
// Standard PKCS#7 padding - fill with random bytes then add padding length // PKCS#7: All pad bytes are equal to the pad length (iOS fix)
val randomBytes = ByteArray(paddingNeeded - 1) for (i in data.size until targetSize) {
SecureRandom().nextBytes(randomBytes) result[i] = paddingNeeded.toByte()
}
// Copy random bytes
System.arraycopy(randomBytes, 0, result, data.size, paddingNeeded - 1)
// Last byte tells how much padding was added
result[result.size - 1] = paddingNeeded.toByte()
return result return result
} }
/** /**
* Remove padding from data - exact same as iOS * Remove padding from data - FIXED: strict PKCS#7 validation (iOS compatible)
*/ */
fun unpad(data: ByteArray): ByteArray { fun unpad(data: ByteArray): ByteArray {
if (data.isEmpty()) return data if (data.isEmpty()) return data
// Last byte tells us how much padding to remove val last = data[data.size - 1]
val paddingLength = data[data.size - 1].toInt() and 0xFF val paddingLength = last.toInt() and 0xFF
if (paddingLength <= 0 || paddingLength > data.size) {
// Invalid padding, return original data // Must have at least 1 pad byte and not exceed data length
return data if (paddingLength <= 0 || paddingLength > data.size) return data
// Verify PKCS#7: all last N bytes equal to pad length (iOS fix)
val start = data.size - paddingLength
for (i in start until data.size) {
if (data[i] != last) {
return data // Invalid padding, return original
}
} }
return data.copyOfRange(0, data.size - paddingLength) return data.copyOfRange(0, start)
} }
} }