mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 03:45:21 +00:00
356 lines
12 KiB
Kotlin
356 lines
12 KiB
Kotlin
package com.bitchat.android.protocol
|
|
|
|
import android.os.Parcelable
|
|
import kotlinx.parcelize.Parcelize
|
|
import java.nio.ByteBuffer
|
|
import java.nio.ByteOrder
|
|
import java.util.*
|
|
|
|
/**
|
|
* Message types - exact same as iOS version with Noise Protocol support
|
|
*/
|
|
enum class MessageType(val value: UByte) {
|
|
ANNOUNCE(0x01u),
|
|
// 0x02 was legacy keyExchange - removed
|
|
LEAVE(0x03u),
|
|
MESSAGE(0x04u), // All user messages (private and broadcast)
|
|
FRAGMENT_START(0x05u),
|
|
FRAGMENT_CONTINUE(0x06u),
|
|
FRAGMENT_END(0x07u),
|
|
CHANNEL_ANNOUNCE(0x08u), // Announce password-protected channel status
|
|
CHANNEL_RETENTION(0x09u), // Announce channel retention status
|
|
DELIVERY_ACK(0x0Au), // Acknowledge message received
|
|
DELIVERY_STATUS_REQUEST(0x0Bu), // Request delivery status update
|
|
READ_RECEIPT(0x0Cu), // Message has been read/viewed
|
|
|
|
// Noise Protocol messages - exact same as iOS
|
|
NOISE_HANDSHAKE_INIT(0x10u), // Noise handshake initiation
|
|
NOISE_HANDSHAKE_RESP(0x11u), // Noise handshake response
|
|
NOISE_ENCRYPTED(0x12u), // Noise encrypted transport message
|
|
NOISE_IDENTITY_ANNOUNCE(0x13u), // Announce static public key for discovery
|
|
CHANNEL_KEY_VERIFY_REQUEST(0x14u), // Request key verification for a channel
|
|
CHANNEL_KEY_VERIFY_RESPONSE(0x15u), // Response to key verification request
|
|
CHANNEL_PASSWORD_UPDATE(0x16u), // Distribute new password to channel members
|
|
CHANNEL_METADATA(0x17u), // Announce channel creator and metadata
|
|
|
|
// Protocol version negotiation
|
|
VERSION_HELLO(0x20u), // Initial version announcement
|
|
VERSION_ACK(0x21u); // Version acknowledgment
|
|
|
|
companion object {
|
|
fun fromValue(value: UByte): MessageType? {
|
|
return values().find { it.value == value }
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Special recipient IDs - exact same as iOS version
|
|
*/
|
|
object SpecialRecipients {
|
|
val BROADCAST = ByteArray(8) { 0xFF.toByte() } // All 0xFF = broadcast
|
|
}
|
|
|
|
/**
|
|
* Binary packet format - 100% compatible with iOS version
|
|
*
|
|
* Header (Fixed 13 bytes):
|
|
* - Version: 1 byte
|
|
* - Type: 1 byte
|
|
* - TTL: 1 byte
|
|
* - Timestamp: 8 bytes (UInt64, big-endian)
|
|
* - Flags: 1 byte (bit 0: hasRecipient, bit 1: hasSignature, bit 2: isCompressed)
|
|
* - PayloadLength: 2 bytes (UInt16, big-endian)
|
|
*
|
|
* Variable sections:
|
|
* - SenderID: 8 bytes (fixed)
|
|
* - RecipientID: 8 bytes (if hasRecipient flag set)
|
|
* - Payload: Variable length (includes original size if compressed)
|
|
* - Signature: 64 bytes (if hasSignature flag set)
|
|
*/
|
|
@Parcelize
|
|
data class BitchatPacket(
|
|
val version: UByte = 1u,
|
|
val type: UByte,
|
|
val senderID: ByteArray,
|
|
val recipientID: ByteArray? = null,
|
|
val timestamp: ULong,
|
|
val payload: ByteArray,
|
|
val signature: ByteArray? = null,
|
|
var ttl: UByte
|
|
) : Parcelable {
|
|
|
|
constructor(
|
|
type: UByte,
|
|
ttl: UByte,
|
|
senderID: String,
|
|
payload: ByteArray
|
|
) : this(
|
|
version = 1u,
|
|
type = type,
|
|
senderID = hexStringToByteArray(senderID),
|
|
recipientID = null,
|
|
timestamp = (System.currentTimeMillis()).toULong(),
|
|
payload = payload,
|
|
signature = null,
|
|
ttl = ttl
|
|
)
|
|
|
|
fun toBinaryData(): ByteArray? {
|
|
return BinaryProtocol.encode(this)
|
|
}
|
|
|
|
companion object {
|
|
fun fromBinaryData(data: ByteArray): BitchatPacket? {
|
|
return BinaryProtocol.decode(data)
|
|
}
|
|
|
|
/**
|
|
* Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS
|
|
*/
|
|
private fun hexStringToByteArray(hexString: String): ByteArray {
|
|
val result = ByteArray(8) { 0 } // Initialize with zeros, exactly 8 bytes
|
|
var tempID = hexString
|
|
var index = 0
|
|
|
|
while (tempID.length >= 2 && index < 8) {
|
|
val hexByte = tempID.substring(0, 2)
|
|
val byte = hexByte.toIntOrNull(16)?.toByte()
|
|
if (byte != null) {
|
|
result[index] = byte
|
|
}
|
|
tempID = tempID.substring(2)
|
|
index++
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) return true
|
|
if (javaClass != other?.javaClass) return false
|
|
|
|
other as BitchatPacket
|
|
|
|
if (version != other.version) return false
|
|
if (type != other.type) return false
|
|
if (!senderID.contentEquals(other.senderID)) return false
|
|
if (recipientID != null) {
|
|
if (other.recipientID == null) return false
|
|
if (!recipientID.contentEquals(other.recipientID)) return false
|
|
} else if (other.recipientID != null) return false
|
|
if (timestamp != other.timestamp) return false
|
|
if (!payload.contentEquals(other.payload)) return false
|
|
if (signature != null) {
|
|
if (other.signature == null) return false
|
|
if (!signature.contentEquals(other.signature)) return false
|
|
} else if (other.signature != null) return false
|
|
if (ttl != other.ttl) return false
|
|
|
|
return true
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
var result = version.hashCode()
|
|
result = 31 * result + type.hashCode()
|
|
result = 31 * result + senderID.contentHashCode()
|
|
result = 31 * result + (recipientID?.contentHashCode() ?: 0)
|
|
result = 31 * result + timestamp.hashCode()
|
|
result = 31 * result + payload.contentHashCode()
|
|
result = 31 * result + (signature?.contentHashCode() ?: 0)
|
|
result = 31 * result + ttl.hashCode()
|
|
return result
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Binary Protocol implementation - exact same format as iOS version
|
|
*/
|
|
object BinaryProtocol {
|
|
private const val HEADER_SIZE = 13
|
|
private const val SENDER_ID_SIZE = 8
|
|
private const val RECIPIENT_ID_SIZE = 8
|
|
private const val SIGNATURE_SIZE = 64
|
|
|
|
object Flags {
|
|
const val HAS_RECIPIENT: UByte = 0x01u
|
|
const val HAS_SIGNATURE: UByte = 0x02u
|
|
const val IS_COMPRESSED: UByte = 0x04u
|
|
}
|
|
|
|
fun encode(packet: BitchatPacket): ByteArray? {
|
|
try {
|
|
// Try to compress payload if beneficial
|
|
var payload = packet.payload
|
|
var originalPayloadSize: UShort? = null
|
|
var isCompressed = false
|
|
|
|
if (CompressionUtil.shouldCompress(payload)) {
|
|
CompressionUtil.compress(payload)?.let { compressedPayload ->
|
|
originalPayloadSize = payload.size.toUShort()
|
|
payload = compressedPayload
|
|
isCompressed = true
|
|
}
|
|
}
|
|
|
|
val buffer = ByteBuffer.allocate(4096).apply { order(ByteOrder.BIG_ENDIAN) }
|
|
|
|
// Header
|
|
buffer.put(packet.version.toByte())
|
|
buffer.put(packet.type.toByte())
|
|
buffer.put(packet.ttl.toByte())
|
|
|
|
// Timestamp (8 bytes, big-endian)
|
|
buffer.putLong(packet.timestamp.toLong())
|
|
|
|
// Flags
|
|
var flags: UByte = 0u
|
|
if (packet.recipientID != null) {
|
|
flags = flags or Flags.HAS_RECIPIENT
|
|
}
|
|
if (packet.signature != null) {
|
|
flags = flags or Flags.HAS_SIGNATURE
|
|
}
|
|
if (isCompressed) {
|
|
flags = flags or Flags.IS_COMPRESSED
|
|
}
|
|
buffer.put(flags.toByte())
|
|
|
|
// Payload length (2 bytes, big-endian) - includes original size if compressed
|
|
val payloadDataSize = payload.size + if (isCompressed) 2 else 0
|
|
buffer.putShort(payloadDataSize.toShort())
|
|
|
|
// SenderID (exactly 8 bytes)
|
|
val senderBytes = packet.senderID.take(SENDER_ID_SIZE).toByteArray()
|
|
buffer.put(senderBytes)
|
|
if (senderBytes.size < SENDER_ID_SIZE) {
|
|
buffer.put(ByteArray(SENDER_ID_SIZE - senderBytes.size))
|
|
}
|
|
|
|
// RecipientID (if present)
|
|
packet.recipientID?.let { recipientID ->
|
|
val recipientBytes = recipientID.take(RECIPIENT_ID_SIZE).toByteArray()
|
|
buffer.put(recipientBytes)
|
|
if (recipientBytes.size < RECIPIENT_ID_SIZE) {
|
|
buffer.put(ByteArray(RECIPIENT_ID_SIZE - recipientBytes.size))
|
|
}
|
|
}
|
|
|
|
// Payload (with original size prepended if compressed)
|
|
if (isCompressed) {
|
|
val originalSize = originalPayloadSize
|
|
if (originalSize != null) {
|
|
buffer.putShort(originalSize.toShort())
|
|
}
|
|
}
|
|
buffer.put(payload)
|
|
|
|
// Signature (if present)
|
|
packet.signature?.let { signature ->
|
|
buffer.put(signature.take(SIGNATURE_SIZE).toByteArray())
|
|
}
|
|
|
|
val result = ByteArray(buffer.position())
|
|
buffer.rewind()
|
|
buffer.get(result)
|
|
|
|
// Apply padding to standard block sizes for traffic analysis resistance
|
|
val optimalSize = MessagePadding.optimalBlockSize(result.size)
|
|
val paddedData = MessagePadding.pad(result, optimalSize)
|
|
|
|
return paddedData
|
|
|
|
} catch (e: Exception) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
fun decode(data: ByteArray): BitchatPacket? {
|
|
try {
|
|
// Remove padding first - exactly same as iOS
|
|
val unpaddedData = MessagePadding.unpad(data)
|
|
|
|
if (unpaddedData.size < HEADER_SIZE + SENDER_ID_SIZE) return null
|
|
|
|
val buffer = ByteBuffer.wrap(unpaddedData).apply { order(ByteOrder.BIG_ENDIAN) }
|
|
|
|
// Header
|
|
val version = buffer.get().toUByte()
|
|
if (version != 1u.toUByte()) return null
|
|
|
|
val type = buffer.get().toUByte()
|
|
val ttl = buffer.get().toUByte()
|
|
|
|
// Timestamp
|
|
val timestamp = buffer.getLong().toULong()
|
|
|
|
// Flags
|
|
val flags = buffer.get().toUByte()
|
|
val hasRecipient = (flags and Flags.HAS_RECIPIENT) != 0u.toUByte()
|
|
val hasSignature = (flags and Flags.HAS_SIGNATURE) != 0u.toUByte()
|
|
val isCompressed = (flags and Flags.IS_COMPRESSED) != 0u.toUByte()
|
|
|
|
// Payload length
|
|
val payloadLength = buffer.getShort().toUShort()
|
|
|
|
// Calculate expected total size
|
|
var expectedSize = HEADER_SIZE + SENDER_ID_SIZE + payloadLength.toInt()
|
|
if (hasRecipient) expectedSize += RECIPIENT_ID_SIZE
|
|
if (hasSignature) expectedSize += SIGNATURE_SIZE
|
|
|
|
if (unpaddedData.size < expectedSize) return null
|
|
|
|
// SenderID
|
|
val senderID = ByteArray(SENDER_ID_SIZE)
|
|
buffer.get(senderID)
|
|
|
|
// RecipientID
|
|
val recipientID = if (hasRecipient) {
|
|
val recipientBytes = ByteArray(RECIPIENT_ID_SIZE)
|
|
buffer.get(recipientBytes)
|
|
recipientBytes
|
|
} else null
|
|
|
|
// Payload
|
|
val payload = if (isCompressed) {
|
|
// First 2 bytes are original size
|
|
if (payloadLength.toInt() < 2) return null
|
|
val originalSize = buffer.getShort().toInt()
|
|
|
|
// Compressed payload
|
|
val compressedPayload = ByteArray(payloadLength.toInt() - 2)
|
|
buffer.get(compressedPayload)
|
|
|
|
// Decompress
|
|
CompressionUtil.decompress(compressedPayload, originalSize) ?: return null
|
|
} else {
|
|
val payloadBytes = ByteArray(payloadLength.toInt())
|
|
buffer.get(payloadBytes)
|
|
payloadBytes
|
|
}
|
|
|
|
// Signature
|
|
val signature = if (hasSignature) {
|
|
val signatureBytes = ByteArray(SIGNATURE_SIZE)
|
|
buffer.get(signatureBytes)
|
|
signatureBytes
|
|
} else null
|
|
|
|
return BitchatPacket(
|
|
version = version,
|
|
type = type,
|
|
senderID = senderID,
|
|
recipientID = recipientID,
|
|
timestamp = timestamp,
|
|
payload = payload,
|
|
signature = signature,
|
|
ttl = ttl
|
|
)
|
|
|
|
} catch (e: Exception) {
|
|
return null
|
|
}
|
|
}
|
|
}
|