From 4acfafb998b1ca0ace16168ae55dba5d50c91d0d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:16:23 +0200 Subject: [PATCH] fix fragmentation baby (#266) --- .../bitchat/android/mesh/FragmentManager.kt | 30 ++++++-------- .../android/protocol/BinaryProtocol.kt | 23 ++++++++--- .../android/protocol/MessagePadding.kt | 41 ++++++++++--------- 3 files changed, 51 insertions(+), 43 deletions(-) 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 f86fda4b..6529b2c3 100644 --- a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt +++ b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt @@ -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 { - 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 diff --git a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt index 04680636..b5b17008 100644 --- a/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt +++ b/app/src/main/java/com/bitchat/android/protocol/BinaryProtocol.kt @@ -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) diff --git a/app/src/main/java/com/bitchat/android/protocol/MessagePadding.kt b/app/src/main/java/com/bitchat/android/protocol/MessagePadding.kt index 20457d2c..5b606897 100644 --- a/app/src/main/java/com/bitchat/android/protocol/MessagePadding.kt +++ b/app/src/main/java/com/bitchat/android/protocol/MessagePadding.kt @@ -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) } }