mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 11:05:20 +00:00
idendityannouncement refactor and update to new binary protocol
This commit is contained in:
@@ -54,6 +54,35 @@ class EncryptionService(private val context: Context) {
|
|||||||
return noiseService.getStaticPublicKeyData()
|
return noiseService.getStaticPublicKeyData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get our static public key for Noise protocol (for identity announcements)
|
||||||
|
*/
|
||||||
|
fun getStaticPublicKey(): ByteArray? {
|
||||||
|
return noiseService.getStaticPublicKeyData()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get our signing public key for Ed25519 signatures (for identity announcements)
|
||||||
|
* Note: In the current implementation, this returns the same as static key
|
||||||
|
* In a full implementation, this would be a separate Ed25519 key
|
||||||
|
*/
|
||||||
|
fun getSigningPublicKey(): ByteArray? {
|
||||||
|
// For now, return the static public key as placeholder
|
||||||
|
// In a full implementation, this would be a separate Ed25519 signing key
|
||||||
|
return noiseService.getStaticPublicKeyData()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign data using our signing key (for identity announcements)
|
||||||
|
* Note: In the current simplified implementation, this returns empty signature
|
||||||
|
* In a full implementation, this would use Ed25519 signing
|
||||||
|
*/
|
||||||
|
fun signData(data: ByteArray): ByteArray? {
|
||||||
|
// For now, return empty signature as placeholder
|
||||||
|
// In a full implementation, this would use Ed25519 to sign the data
|
||||||
|
return ByteArray(64) // Ed25519 signature length placeholder
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add peer's public key and start handshake if needed
|
* Add peer's public key and start handshake if needed
|
||||||
* For backward compatibility with old key exchange packets
|
* For backward compatibility with old key exchange packets
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.bitchat.android.model.BitchatMessage
|
|||||||
import com.bitchat.android.model.RoutedPacket
|
import com.bitchat.android.model.RoutedPacket
|
||||||
import com.bitchat.android.model.DeliveryAck
|
import com.bitchat.android.model.DeliveryAck
|
||||||
import com.bitchat.android.model.ReadReceipt
|
import com.bitchat.android.model.ReadReceipt
|
||||||
|
import com.bitchat.android.model.NoiseIdentityAnnouncement
|
||||||
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.SpecialRecipients
|
import com.bitchat.android.protocol.SpecialRecipients
|
||||||
@@ -644,25 +645,86 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send identity announcement (broadcast our static public key)
|
* Send Noise identity announcement (broadcast our static public key and signing key)
|
||||||
|
* Now properly formatted as NoiseIdentityAnnouncement to match iOS
|
||||||
*/
|
*/
|
||||||
private fun sendKeyExchangeToDevice() {
|
private fun sendKeyExchangeToDevice() {
|
||||||
// For discovery, broadcast our static identity key (32 bytes) so peers can identify us
|
serviceScope.launch {
|
||||||
// This is not a handshake initiation, but identity announcement
|
try {
|
||||||
val identityData = encryptionService.getCombinedPublicKeyData()
|
val nickname = delegate?.getNickname() ?: myPeerID
|
||||||
|
|
||||||
val packet = BitchatPacket(
|
// Create the identity announcement using proper binary format
|
||||||
version = 1u,
|
val announcement = createNoiseIdentityAnnouncement(nickname, null)
|
||||||
type = MessageType.NOISE_IDENTITY_ANNOUNCE.value, // Changed to identity announce
|
if (announcement != null) {
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
val announcementData = announcement.toBinaryData()
|
||||||
recipientID = SpecialRecipients.BROADCAST,
|
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
val packet = BitchatPacket(
|
||||||
payload = identityData,
|
version = 1u,
|
||||||
ttl = 1u
|
type = MessageType.NOISE_IDENTITY_ANNOUNCE.value,
|
||||||
)
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
|
recipientID = SpecialRecipients.BROADCAST,
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
Log.d(TAG, "Sent Noise identity announcement (${identityData.size} bytes)")
|
payload = announcementData,
|
||||||
|
ttl = 1u
|
||||||
|
)
|
||||||
|
|
||||||
|
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||||
|
Log.d(TAG, "Sent NoiseIdentityAnnouncement (${announcementData.size} bytes)")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to create NoiseIdentityAnnouncement")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to send NoiseIdentityAnnouncement: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a properly formatted NoiseIdentityAnnouncement exactly like iOS
|
||||||
|
*/
|
||||||
|
private fun createNoiseIdentityAnnouncement(nickname: String, previousPeerID: String?): NoiseIdentityAnnouncement? {
|
||||||
|
return try {
|
||||||
|
// Get the static public key for Noise protocol
|
||||||
|
val staticKey = encryptionService.getStaticPublicKey()
|
||||||
|
if (staticKey == null) {
|
||||||
|
Log.e(TAG, "No static public key available for identity announcement")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the signing public key for Ed25519 signatures
|
||||||
|
val signingKey = encryptionService.getSigningPublicKey()
|
||||||
|
if (signingKey == null) {
|
||||||
|
Log.e(TAG, "No signing public key available for identity announcement")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = Date()
|
||||||
|
|
||||||
|
// Create the binding data to sign (same format as iOS)
|
||||||
|
val timestampMs = now.time
|
||||||
|
val bindingData = myPeerID.toByteArray(Charsets.UTF_8) +
|
||||||
|
staticKey +
|
||||||
|
timestampMs.toString().toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
|
// Sign the binding with our Ed25519 signing key
|
||||||
|
val signature = encryptionService.signData(bindingData) ?: ByteArray(0)
|
||||||
|
|
||||||
|
// Create the identity announcement
|
||||||
|
NoiseIdentityAnnouncement(
|
||||||
|
peerID = myPeerID,
|
||||||
|
publicKey = staticKey,
|
||||||
|
signingPublicKey = signingKey,
|
||||||
|
nickname = nickname,
|
||||||
|
timestamp = now,
|
||||||
|
previousPeerID = previousPeerID,
|
||||||
|
signature = signature
|
||||||
|
)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to create NoiseIdentityAnnouncement: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
package com.bitchat.android.model
|
package com.bitchat.android.model
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import org.json.JSONObject
|
import com.bitchat.android.util.*
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Noise Identity Announcement data class (compatible with iOS version)
|
* Noise Identity Announcement data class (compatible with iOS version)
|
||||||
|
* Enhanced identity announcement with rotation support and binary protocol
|
||||||
*/
|
*/
|
||||||
data class NoiseIdentityAnnouncement(
|
data class NoiseIdentityAnnouncement(
|
||||||
val peerID: String,
|
val peerID: String, // Current ephemeral peer ID
|
||||||
val nickname: String,
|
val publicKey: ByteArray, // Noise static public key
|
||||||
val publicKey: ByteArray,
|
val signingPublicKey: ByteArray, // Ed25519 signing public key
|
||||||
val timestamp: Date,
|
val nickname: String, // Current nickname
|
||||||
val signature: ByteArray,
|
val timestamp: Date, // When this binding was created
|
||||||
val fingerprint: String?,
|
val previousPeerID: String?, // Previous peer ID (for smooth transition)
|
||||||
val previousPeerID: String? = null
|
val signature: ByteArray // Signature proving ownership
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
// Computed fingerprint from public key
|
||||||
|
val fingerprint: String by lazy {
|
||||||
|
val digest = MessageDigest.getInstance("SHA-256")
|
||||||
|
val hash = digest.digest(publicKey)
|
||||||
|
hash.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
@@ -26,9 +35,9 @@ data class NoiseIdentityAnnouncement(
|
|||||||
if (peerID != other.peerID) return false
|
if (peerID != other.peerID) return false
|
||||||
if (nickname != other.nickname) return false
|
if (nickname != other.nickname) return false
|
||||||
if (!publicKey.contentEquals(other.publicKey)) return false
|
if (!publicKey.contentEquals(other.publicKey)) return false
|
||||||
|
if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false
|
||||||
if (timestamp != other.timestamp) return false
|
if (timestamp != other.timestamp) return false
|
||||||
if (!signature.contentEquals(other.signature)) return false
|
if (!signature.contentEquals(other.signature)) return false
|
||||||
if (fingerprint != other.fingerprint) return false
|
|
||||||
if (previousPeerID != other.previousPeerID) return false
|
if (previousPeerID != other.previousPeerID) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -38,53 +47,153 @@ data class NoiseIdentityAnnouncement(
|
|||||||
var result = peerID.hashCode()
|
var result = peerID.hashCode()
|
||||||
result = 31 * result + nickname.hashCode()
|
result = 31 * result + nickname.hashCode()
|
||||||
result = 31 * result + publicKey.contentHashCode()
|
result = 31 * result + publicKey.contentHashCode()
|
||||||
|
result = 31 * result + signingPublicKey.contentHashCode()
|
||||||
result = 31 * result + timestamp.hashCode()
|
result = 31 * result + timestamp.hashCode()
|
||||||
result = 31 * result + signature.contentHashCode()
|
result = 31 * result + signature.contentHashCode()
|
||||||
result = 31 * result + (fingerprint?.hashCode() ?: 0)
|
|
||||||
result = 31 * result + (previousPeerID?.hashCode() ?: 0)
|
result = 31 * result + (previousPeerID?.hashCode() ?: 0)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Binary Encoding
|
||||||
|
|
||||||
|
fun toBinaryData(): ByteArray {
|
||||||
|
val builder = BinaryDataBuilder()
|
||||||
|
|
||||||
|
// Flags byte: bit 0 = hasPreviousPeerID
|
||||||
|
var flags: UByte = 0u
|
||||||
|
if (previousPeerID != null) flags = flags or 0x01u
|
||||||
|
builder.appendUInt8(flags)
|
||||||
|
|
||||||
|
// PeerID as 8-byte hex string
|
||||||
|
val peerData = hexStringToByteArray(peerID)
|
||||||
|
// Directly append the 8 bytes without length prefix since this is a fixed field
|
||||||
|
for (byte in peerData) {
|
||||||
|
builder.buffer.add(byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.appendData(publicKey)
|
||||||
|
builder.appendData(signingPublicKey)
|
||||||
|
builder.appendString(nickname)
|
||||||
|
builder.appendDate(timestamp)
|
||||||
|
|
||||||
|
if (previousPeerID != null) {
|
||||||
|
// Previous PeerID as 8-byte hex string
|
||||||
|
val prevData = hexStringToByteArray(previousPeerID)
|
||||||
|
// Directly append the 8 bytes without length prefix since this is a fixed field
|
||||||
|
for (byte in prevData) {
|
||||||
|
builder.buffer.add(byte)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.appendData(signature)
|
||||||
|
|
||||||
|
return builder.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "NoiseIdentityAnnouncement"
|
private const val TAG = "NoiseIdentityAnnouncement"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse Noise identity announcement from binary payload
|
* Parse Noise identity announcement from binary payload with proper iOS compatibility
|
||||||
*/
|
*/
|
||||||
fun fromBinaryData(payload: ByteArray): NoiseIdentityAnnouncement? {
|
fun fromBinaryData(data: ByteArray): NoiseIdentityAnnouncement? {
|
||||||
return try {
|
return try {
|
||||||
val jsonString = String(payload, Charsets.UTF_8)
|
// Create defensive copy
|
||||||
val json = JSONObject(jsonString)
|
val dataCopy = data.copyOf()
|
||||||
|
|
||||||
val peerID = json.getString("peerID")
|
// Minimum size check: flags(1) + peerID(8) + min data lengths
|
||||||
val nickname = json.getString("nickname")
|
if (dataCopy.size < 20) {
|
||||||
val publicKeyBase64 = json.getString("publicKey")
|
Log.w(TAG, "Data too small for NoiseIdentityAnnouncement: ${dataCopy.size} bytes")
|
||||||
val timestampMs = json.getLong("timestamp")
|
return null
|
||||||
val signatureBase64 = json.getString("signature")
|
}
|
||||||
val previousPeerID = if (json.has("previousPeerID")) json.getString("previousPeerID") else null
|
|
||||||
|
|
||||||
// Decode base64 fields
|
val offsetArray = intArrayOf(0)
|
||||||
val publicKey = android.util.Base64.decode(publicKeyBase64, android.util.Base64.DEFAULT)
|
|
||||||
val signature = android.util.Base64.decode(signatureBase64, android.util.Base64.DEFAULT)
|
|
||||||
|
|
||||||
// Calculate fingerprint from public key
|
val flags = dataCopy.readUInt8(offsetArray) ?: run {
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
Log.w(TAG, "Failed to read flags")
|
||||||
val hash = digest.digest(publicKey)
|
return null
|
||||||
val fingerprint = hash.joinToString("") { "%02x".format(it) }
|
}
|
||||||
|
val hasPreviousPeerID = (flags.toInt() and 0x01) != 0
|
||||||
|
|
||||||
NoiseIdentityAnnouncement(
|
// Read peerID using safe method
|
||||||
|
val peerIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run {
|
||||||
|
Log.w(TAG, "Failed to read peerID bytes")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val peerID = peerIDBytes.hexEncodedString()
|
||||||
|
|
||||||
|
val publicKey = dataCopy.readData(offsetArray) ?: run {
|
||||||
|
Log.w(TAG, "Failed to read public key")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val signingPublicKey = dataCopy.readData(offsetArray) ?: run {
|
||||||
|
Log.w(TAG, "Failed to read signing public key")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val nickname = dataCopy.readString(offsetArray) ?: run {
|
||||||
|
Log.w(TAG, "Failed to read nickname")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val timestamp = dataCopy.readDate(offsetArray) ?: run {
|
||||||
|
Log.w(TAG, "Failed to read timestamp")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousPeerID: String? = null
|
||||||
|
if (hasPreviousPeerID) {
|
||||||
|
// Read previousPeerID using safe method
|
||||||
|
val prevIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run {
|
||||||
|
Log.w(TAG, "Failed to read previousPeerID bytes")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
previousPeerID = prevIDBytes.hexEncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
|
val signature = dataCopy.readData(offsetArray) ?: run {
|
||||||
|
Log.w(TAG, "Failed to read signature")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Successfully parsed NoiseIdentityAnnouncement: peerID=$peerID, nickname=$nickname")
|
||||||
|
|
||||||
|
return NoiseIdentityAnnouncement(
|
||||||
peerID = peerID,
|
peerID = peerID,
|
||||||
nickname = nickname,
|
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
timestamp = Date(timestampMs),
|
signingPublicKey = signingPublicKey,
|
||||||
signature = signature,
|
nickname = nickname,
|
||||||
fingerprint = fingerprint,
|
timestamp = timestamp,
|
||||||
previousPeerID = previousPeerID
|
previousPeerID = previousPeerID,
|
||||||
|
signature = signature
|
||||||
)
|
)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to parse Noise identity announcement: ${e.message}")
|
Log.e(TAG, "Failed to parse Noise identity announcement: ${e.message}", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
*/
|
*/
|
||||||
fun encrypt(data: ByteArray, peerID: String): ByteArray? {
|
fun encrypt(data: ByteArray, peerID: String): ByteArray? {
|
||||||
if (!hasEstablishedSession(peerID)) {
|
if (!hasEstablishedSession(peerID)) {
|
||||||
Log.w(TAG, "No established session with $peerID, handshake required")
|
Log.w(TAG, "No established session with $peerID, handshake required. TODO: IMPLEMENT HANDSHAKE INIT")
|
||||||
onHandshakeRequired?.invoke(peerID)
|
onHandshakeRequired?.invoke(peerID)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,365 @@
|
|||||||
|
package com.bitchat.android.util
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.ByteOrder
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binary encoding utilities for efficient protocol messages
|
||||||
|
* Compatible with iOS version BinaryEncodingUtils.swift
|
||||||
|
*/
|
||||||
|
|
||||||
|
// MARK: - Hex Encoding/Decoding Extensions
|
||||||
|
|
||||||
|
fun ByteArray.hexEncodedString(): String {
|
||||||
|
if (this.isEmpty()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return this.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.dataFromHexString(): ByteArray? {
|
||||||
|
val len = this.length / 2
|
||||||
|
val data = ByteArray(len)
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
for (i in 0 until len) {
|
||||||
|
val hexByte = this.substring(i * 2, i * 2 + 2)
|
||||||
|
val byte = hexByte.toIntOrNull(16)?.toByte() ?: return null
|
||||||
|
data[index++] = byte
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Binary Encoding Utilities
|
||||||
|
|
||||||
|
class BinaryDataBuilder {
|
||||||
|
private val _buffer = mutableListOf<Byte>()
|
||||||
|
|
||||||
|
// Make buffer accessible for direct manipulation when needed
|
||||||
|
val buffer: MutableList<Byte> get() = _buffer
|
||||||
|
|
||||||
|
// MARK: Writing
|
||||||
|
|
||||||
|
fun appendUInt8(value: UByte) {
|
||||||
|
buffer.add(value.toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendUInt16(value: UShort) {
|
||||||
|
buffer.add(((value.toInt() shr 8) and 0xFF).toByte())
|
||||||
|
buffer.add((value.toInt() and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendUInt32(value: UInt) {
|
||||||
|
buffer.add(((value.toLong() shr 24) and 0xFF).toByte())
|
||||||
|
buffer.add(((value.toLong() shr 16) and 0xFF).toByte())
|
||||||
|
buffer.add(((value.toLong() shr 8) and 0xFF).toByte())
|
||||||
|
buffer.add((value.toLong() and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendUInt64(value: ULong) {
|
||||||
|
for (i in 7 downTo 0) {
|
||||||
|
buffer.add(((value.toLong() shr (i * 8)) and 0xFF).toByte())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendString(string: String, maxLength: Int = 255) {
|
||||||
|
val data = string.toByteArray(Charsets.UTF_8)
|
||||||
|
val length = minOf(data.size, maxLength)
|
||||||
|
|
||||||
|
if (maxLength <= 255) {
|
||||||
|
buffer.add(length.toByte())
|
||||||
|
} else {
|
||||||
|
appendUInt16(length.toUShort())
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.addAll(data.take(length).toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendData(data: ByteArray, maxLength: Int = 65535) {
|
||||||
|
val length = minOf(data.size, maxLength)
|
||||||
|
|
||||||
|
if (maxLength <= 255) {
|
||||||
|
buffer.add(length.toByte())
|
||||||
|
} else {
|
||||||
|
appendUInt16(length.toUShort())
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.addAll(data.take(length).toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendDate(date: Date) {
|
||||||
|
val timestamp = (date.time).toULong() // milliseconds
|
||||||
|
appendUInt64(timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun appendUUID(uuid: String) {
|
||||||
|
// Convert UUID string to 16 bytes
|
||||||
|
val uuidData = ByteArray(16)
|
||||||
|
|
||||||
|
val cleanUUID = uuid.replace("-", "")
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
for (i in 0 until 16) {
|
||||||
|
if (index + 1 < cleanUUID.length) {
|
||||||
|
val hexByte = cleanUUID.substring(index, index + 2)
|
||||||
|
uuidData[i] = hexByte.toIntOrNull(16)?.toByte() ?: 0
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.addAll(uuidData.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toByteArray(): ByteArray {
|
||||||
|
return buffer.toByteArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Binary Data Reading Extensions
|
||||||
|
|
||||||
|
class BinaryDataReader(private val data: ByteArray) {
|
||||||
|
private var offset = 0
|
||||||
|
|
||||||
|
// MARK: Reading
|
||||||
|
|
||||||
|
fun readUInt8(): UByte? {
|
||||||
|
if (offset >= data.size) return null
|
||||||
|
val value = data[offset].toUByte()
|
||||||
|
offset += 1
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readUInt16(): UShort? {
|
||||||
|
if (offset + 2 > data.size) return null
|
||||||
|
val value = ((data[offset].toUByte().toInt() shl 8) or
|
||||||
|
(data[offset + 1].toUByte().toInt())).toUShort()
|
||||||
|
offset += 2
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readUInt32(): UInt? {
|
||||||
|
if (offset + 4 > data.size) return null
|
||||||
|
val value = ((data[offset].toUByte().toUInt() shl 24) or
|
||||||
|
(data[offset + 1].toUByte().toUInt() shl 16) or
|
||||||
|
(data[offset + 2].toUByte().toUInt() shl 8) or
|
||||||
|
(data[offset + 3].toUByte().toUInt()))
|
||||||
|
offset += 4
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readUInt64(): ULong? {
|
||||||
|
if (offset + 8 > data.size) return null
|
||||||
|
var value = 0UL
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
value = (value shl 8) or data[offset + i].toUByte().toULong()
|
||||||
|
}
|
||||||
|
offset += 8
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readString(maxLength: Int = 255): String? {
|
||||||
|
val length: Int = if (maxLength <= 255) {
|
||||||
|
readUInt8()?.toInt() ?: return null
|
||||||
|
} else {
|
||||||
|
readUInt16()?.toInt() ?: return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset + length > data.size) return null
|
||||||
|
|
||||||
|
val stringData = data.sliceArray(offset until offset + length)
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
return String(stringData, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readData(maxLength: Int = 65535): ByteArray? {
|
||||||
|
val length: Int = if (maxLength <= 255) {
|
||||||
|
readUInt8()?.toInt() ?: return null
|
||||||
|
} else {
|
||||||
|
readUInt16()?.toInt() ?: return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset + length > data.size) return null
|
||||||
|
|
||||||
|
val data = this.data.sliceArray(offset until offset + length)
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readDate(): Date? {
|
||||||
|
val timestamp = readUInt64() ?: return null
|
||||||
|
return Date(timestamp.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readUUID(): String? {
|
||||||
|
if (offset + 16 > data.size) return null
|
||||||
|
|
||||||
|
val uuidData = data.sliceArray(offset until offset + 16)
|
||||||
|
offset += 16
|
||||||
|
|
||||||
|
// Convert 16 bytes to UUID string format
|
||||||
|
val uuid = uuidData.joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
|
// Insert hyphens at proper positions: 8-4-4-4-12
|
||||||
|
val result = StringBuilder()
|
||||||
|
for ((index, char) in uuid.withIndex()) {
|
||||||
|
if (index == 8 || index == 12 || index == 16 || index == 20) {
|
||||||
|
result.append("-")
|
||||||
|
}
|
||||||
|
result.append(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString().uppercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readFixedBytes(count: Int): ByteArray? {
|
||||||
|
if (offset + count > data.size) return null
|
||||||
|
|
||||||
|
val data = this.data.sliceArray(offset until offset + count)
|
||||||
|
offset += count
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current offset position
|
||||||
|
val currentOffset: Int get() = offset
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Binary Message Protocol
|
||||||
|
|
||||||
|
interface BinaryEncodable {
|
||||||
|
fun toBinaryData(): ByteArray
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Message Type Registry
|
||||||
|
|
||||||
|
enum class BinaryMessageType(val value: UByte) {
|
||||||
|
DELIVERY_ACK(0x01u),
|
||||||
|
READ_RECEIPT(0x02u),
|
||||||
|
CHANNEL_KEY_VERIFY_REQUEST(0x03u),
|
||||||
|
CHANNEL_KEY_VERIFY_RESPONSE(0x04u),
|
||||||
|
CHANNEL_PASSWORD_UPDATE(0x05u),
|
||||||
|
CHANNEL_METADATA(0x06u),
|
||||||
|
VERSION_HELLO(0x07u),
|
||||||
|
VERSION_ACK(0x08u),
|
||||||
|
NOISE_IDENTITY_ANNOUNCEMENT(0x09u),
|
||||||
|
NOISE_MESSAGE(0x0Au);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: UByte): BinaryMessageType? {
|
||||||
|
return values().find { it.value == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extension functions for ByteArray to support iOS-style data manipulation
|
||||||
|
fun ByteArray.readUInt8(at: IntArray): UByte? {
|
||||||
|
val offset = at[0]
|
||||||
|
if (offset >= this.size) return null
|
||||||
|
val value = this[offset].toUByte()
|
||||||
|
at[0] += 1
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.readUInt16(at: IntArray): UShort? {
|
||||||
|
val offset = at[0]
|
||||||
|
if (offset + 2 > this.size) return null
|
||||||
|
val value = ((this[offset].toUByte().toInt() shl 8) or
|
||||||
|
(this[offset + 1].toUByte().toInt())).toUShort()
|
||||||
|
at[0] += 2
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.readUInt32(at: IntArray): UInt? {
|
||||||
|
val offset = at[0]
|
||||||
|
if (offset + 4 > this.size) return null
|
||||||
|
val value = ((this[offset].toUByte().toUInt() shl 24) or
|
||||||
|
(this[offset + 1].toUByte().toUInt() shl 16) or
|
||||||
|
(this[offset + 2].toUByte().toUInt() shl 8) or
|
||||||
|
(this[offset + 3].toUByte().toUInt()))
|
||||||
|
at[0] += 4
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.readUInt64(at: IntArray): ULong? {
|
||||||
|
val offset = at[0]
|
||||||
|
if (offset + 8 > this.size) return null
|
||||||
|
var value = 0UL
|
||||||
|
for (i in 0 until 8) {
|
||||||
|
value = (value shl 8) or this[offset + i].toUByte().toULong()
|
||||||
|
}
|
||||||
|
at[0] += 8
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.readString(at: IntArray, maxLength: Int = 255): String? {
|
||||||
|
val length: Int = if (maxLength <= 255) {
|
||||||
|
readUInt8(at)?.toInt() ?: return null
|
||||||
|
} else {
|
||||||
|
readUInt16(at)?.toInt() ?: return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val offset = at[0]
|
||||||
|
if (offset + length > this.size) return null
|
||||||
|
|
||||||
|
val stringData = this.sliceArray(offset until offset + length)
|
||||||
|
at[0] += length
|
||||||
|
|
||||||
|
return String(stringData, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.readData(at: IntArray, maxLength: Int = 65535): ByteArray? {
|
||||||
|
val length: Int = if (maxLength <= 255) {
|
||||||
|
readUInt8(at)?.toInt() ?: return null
|
||||||
|
} else {
|
||||||
|
readUInt16(at)?.toInt() ?: return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val offset = at[0]
|
||||||
|
if (offset + length > this.size) return null
|
||||||
|
|
||||||
|
val data = this.sliceArray(offset until offset + length)
|
||||||
|
at[0] += length
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.readDate(at: IntArray): Date? {
|
||||||
|
val timestamp = readUInt64(at) ?: return null
|
||||||
|
return Date(timestamp.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.readUUID(at: IntArray): String? {
|
||||||
|
val offset = at[0]
|
||||||
|
if (offset + 16 > this.size) return null
|
||||||
|
|
||||||
|
val uuidData = this.sliceArray(offset until offset + 16)
|
||||||
|
at[0] += 16
|
||||||
|
|
||||||
|
// Convert 16 bytes to UUID string format
|
||||||
|
val uuid = uuidData.joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
|
// Insert hyphens at proper positions: 8-4-4-4-12
|
||||||
|
val result = StringBuilder()
|
||||||
|
for ((index, char) in uuid.withIndex()) {
|
||||||
|
if (index == 8 || index == 12 || index == 16 || index == 20) {
|
||||||
|
result.append("-")
|
||||||
|
}
|
||||||
|
result.append(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString().uppercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ByteArray.readFixedBytes(at: IntArray, count: Int): ByteArray? {
|
||||||
|
val offset = at[0]
|
||||||
|
if (offset + count > this.size) return null
|
||||||
|
|
||||||
|
val data = this.sliceArray(offset until offset + count)
|
||||||
|
at[0] += count
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user