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 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
@@ -47,7 +48,10 @@ class FragmentManager {
* Matches iOS sendFragmentedPacket() implementation exactly
*/
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
if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
@@ -150,26 +154,18 @@ class FragmentManager {
}
}
// iOS: if let metadata = fragmentMetadata[fragmentID]
val metadata = fragmentMetadata[fragmentIDString]
if (metadata != 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
)
// Decode the original packet bytes we reassembled, so flags/compression are preserved - iOS fix
val originalPacket = BitchatPacket.fromBinaryData(reassembledData.toByteArray())
if (originalPacket != null) {
// iOS cleanup: incomingFragments.removeValue(forKey: fragmentID)
incomingFragments.remove(fragmentIDString)
fragmentMetadata.remove(fragmentIDString)
Log.d(TAG, "Successfully reassembled packet of ${reassembledData.size} bytes")
return reassembledPacket
Log.d(TAG, "Successfully reassembled and decoded original packet of ${reassembledData.size} bytes")
return originalPacket
} else {
val metadata = fragmentMetadata[fragmentIDString]
Log.e(TAG, "Failed to decode reassembled packet (type=${metadata?.first}, total=${metadata?.second})")
}
} else {
val received = fragmentMap?.size ?: 0
@@ -248,13 +248,24 @@ object BinaryProtocol {
}
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 {
// Remove padding first - exactly same as iOS
val unpaddedData = MessagePadding.unpad(data)
if (raw.size < HEADER_SIZE + SENDER_ID_SIZE) return null
if (unpaddedData.size < HEADER_SIZE + SENDER_ID_SIZE) return null
val buffer = ByteBuffer.wrap(unpaddedData).apply { order(ByteOrder.BIG_ENDIAN) }
val buffer = ByteBuffer.wrap(raw).apply { order(ByteOrder.BIG_ENDIAN) }
// Header
val version = buffer.get().toUByte()
@@ -280,7 +291,7 @@ object BinaryProtocol {
if (hasRecipient) expectedSize += RECIPIENT_ID_SIZE
if (hasSignature) expectedSize += SIGNATURE_SIZE
if (unpaddedData.size < expectedSize) return null
if (raw.size < expectedSize) return null
// SenderID
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 {
if (data.size >= targetSize) return data
val paddingNeeded = targetSize - data.size
// PKCS#7 only supports padding up to 255 bytes
// If we need more padding than that, don't pad - return original data
if (paddingNeeded > 255) return data
// Constrain to 255 to fit a single-byte pad length marker
if (paddingNeeded <= 0 || paddingNeeded > 255) return data
val result = ByteArray(targetSize)
// Copy original data
System.arraycopy(data, 0, result, 0, data.size)
// Standard PKCS#7 padding - fill with random bytes then add padding length
val randomBytes = ByteArray(paddingNeeded - 1)
SecureRandom().nextBytes(randomBytes)
// 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()
// PKCS#7: All pad bytes are equal to the pad length (iOS fix)
for (i in data.size until targetSize) {
result[i] = paddingNeeded.toByte()
}
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 {
if (data.isEmpty()) return data
// Last byte tells us how much padding to remove
val paddingLength = data[data.size - 1].toInt() and 0xFF
if (paddingLength <= 0 || paddingLength > data.size) {
// Invalid padding, return original data
return data
val last = data[data.size - 1]
val paddingLength = last.toInt() and 0xFF
// Must have at least 1 pad byte and not exceed data length
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)
}
}