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 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user