mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
219 lines
6.7 KiB
Kotlin
219 lines
6.7 KiB
Kotlin
package com.bitchat.android.model
|
|
|
|
import android.os.Parcelable
|
|
import kotlinx.parcelize.Parcelize
|
|
|
|
/**
|
|
* Noise encrypted payload types and handling - 100% compatible with iOS SimplifiedBluetoothService
|
|
*
|
|
* This handles all encrypted content that goes through noiseEncrypted packets:
|
|
* - Private messages with TLV encoding
|
|
* - Delivery acknowledgments
|
|
* - Read receipts
|
|
* - Future encrypted payload types
|
|
*/
|
|
|
|
/**
|
|
* Types of payloads embedded within noiseEncrypted messages
|
|
* Matches iOS NoisePayloadType exactly
|
|
*/
|
|
enum class NoisePayloadType(val value: UByte) {
|
|
PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding
|
|
READ_RECEIPT(0x02u), // Message was read
|
|
DELIVERED(0x03u), // Message was delivered
|
|
VOICE_FRAME(0x08u), // Ephemeral live push-to-talk frame
|
|
VERIFY_CHALLENGE(0x10u), // Verification challenge
|
|
VERIFY_RESPONSE(0x11u), // Verification response
|
|
FILE_TRANSFER(0x20u),
|
|
/** Authenticated capabilities + Ed25519 binding for the current Noise generation. */
|
|
PEER_STATE(0x21u);
|
|
|
|
|
|
companion object {
|
|
// #1434 prerelease iOS builds briefly emitted private files as 0x09. Keep this
|
|
// decode-only: every NoisePayload constructed by Android still encodes FILE_TRANSFER as
|
|
// its canonical 0x20 value, so the compatibility alias cannot leak into new traffic.
|
|
private val PRERELEASE_FILE_TRANSFER_RAW_VALUE = 0x09u.toUByte()
|
|
|
|
fun fromValue(value: UByte): NoisePayloadType? {
|
|
return if (value == PRERELEASE_FILE_TRANSFER_RAW_VALUE) {
|
|
FILE_TRANSFER
|
|
} else {
|
|
values().find { it.value == value }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper class for creating and parsing Noise payloads
|
|
* Matches iOS NoisePayload helper exactly
|
|
*/
|
|
@Parcelize
|
|
data class NoisePayload(
|
|
val type: NoisePayloadType,
|
|
val data: ByteArray
|
|
) : Parcelable {
|
|
|
|
/**
|
|
* Encode payload with type prefix - exactly like iOS
|
|
* Format: [type_byte][payload_data]
|
|
*/
|
|
fun encode(): ByteArray {
|
|
val result = ByteArray(1 + data.size)
|
|
result[0] = type.value.toByte()
|
|
data.copyInto(result, destinationOffset = 1)
|
|
return result
|
|
}
|
|
|
|
companion object {
|
|
/**
|
|
* Decode payload from data - exactly like iOS
|
|
* Expects: [type_byte][payload_data]
|
|
*/
|
|
fun decode(data: ByteArray): NoisePayload? {
|
|
if (data.isEmpty()) return null
|
|
|
|
val typeValue = data[0].toUByte()
|
|
val type = NoisePayloadType.fromValue(typeValue) ?: return null
|
|
|
|
// Extract payload data (remaining bytes after type byte)
|
|
val payloadData = if (data.size > 1) {
|
|
data.copyOfRange(1, data.size)
|
|
} else {
|
|
ByteArray(0)
|
|
}
|
|
|
|
return NoisePayload(type, payloadData)
|
|
}
|
|
}
|
|
|
|
// Override equals and hashCode since we use ByteArray
|
|
override fun equals(other: Any?): Boolean {
|
|
if (this === other) return true
|
|
if (javaClass != other?.javaClass) return false
|
|
|
|
other as NoisePayload
|
|
|
|
if (type != other.type) return false
|
|
if (!data.contentEquals(other.data)) return false
|
|
|
|
return true
|
|
}
|
|
|
|
override fun hashCode(): Int {
|
|
var result = type.hashCode()
|
|
result = 31 * result + data.contentHashCode()
|
|
return result
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Private message packet with TLV encoding - matches iOS PrivateMessagePacket exactly
|
|
*/
|
|
@Parcelize
|
|
data class PrivateMessagePacket(
|
|
val messageID: String,
|
|
val content: String
|
|
) : Parcelable {
|
|
|
|
/**
|
|
* TLV types matching iOS implementation exactly
|
|
*/
|
|
private enum class TLVType(val value: UByte) {
|
|
MESSAGE_ID(0x00u),
|
|
CONTENT(0x01u);
|
|
|
|
companion object {
|
|
fun fromValue(value: UByte): TLVType? {
|
|
return values().find { it.value == value }
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encode to TLV binary data - exactly like iOS
|
|
* Format: [type][length][value] for each field
|
|
*/
|
|
fun encode(): ByteArray? {
|
|
val messageIDData = messageID.toByteArray(Charsets.UTF_8)
|
|
val contentData = content.toByteArray(Charsets.UTF_8)
|
|
|
|
// Check size limits (TLV length field is 1 byte = max 255)
|
|
if (messageIDData.size > 255 || contentData.size > 255) {
|
|
return null
|
|
}
|
|
|
|
val result = mutableListOf<Byte>()
|
|
|
|
// TLV for messageID
|
|
result.add(TLVType.MESSAGE_ID.value.toByte())
|
|
result.add(messageIDData.size.toByte())
|
|
result.addAll(messageIDData.toList())
|
|
|
|
// TLV for content
|
|
result.add(TLVType.CONTENT.value.toByte())
|
|
result.add(contentData.size.toByte())
|
|
result.addAll(contentData.toList())
|
|
|
|
return result.toByteArray()
|
|
}
|
|
|
|
companion object {
|
|
/**
|
|
* Decode from TLV binary data - exactly like iOS
|
|
*/
|
|
fun decode(data: ByteArray): PrivateMessagePacket? {
|
|
var offset = 0
|
|
var messageID: String? = null
|
|
var content: String? = null
|
|
|
|
while (offset + 2 <= data.size) {
|
|
// Read TLV type
|
|
val typeValue = data[offset].toUByte()
|
|
val type = TLVType.fromValue(typeValue) ?: return null
|
|
offset += 1
|
|
|
|
// Read TLV length
|
|
val length = data[offset].toUByte().toInt()
|
|
offset += 1
|
|
|
|
// Check bounds
|
|
if (offset + length > data.size) return null
|
|
|
|
// Read TLV value
|
|
val value = data.copyOfRange(offset, offset + length)
|
|
offset += length
|
|
|
|
when (type) {
|
|
TLVType.MESSAGE_ID -> {
|
|
messageID = String(value, Charsets.UTF_8)
|
|
}
|
|
TLVType.CONTENT -> {
|
|
content = String(value, Charsets.UTF_8)
|
|
}
|
|
}
|
|
}
|
|
|
|
return if (messageID != null && content != null) {
|
|
PrivateMessagePacket(messageID, content)
|
|
} else {
|
|
null
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun toString(): String {
|
|
return "PrivateMessagePacket(messageID='$messageID', content='${content.take(50)}${if (content.length > 50) "..." else ""}')"
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read receipt data class for transport compatibility
|
|
*/
|
|
@Parcelize
|
|
data class ReadReceipt(
|
|
val originalMessageID: String,
|
|
val readerPeerID: String? = null
|
|
) : Parcelable
|