mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:45:19 +00:00
fix fragmentation baby (#266)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user