From 4a512f51bff8200a2ac8f195fbdebeb10c917fdf Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 23 Jul 2025 00:02:19 +0200 Subject: [PATCH] idendityannouncement refactor and update to new binary protocol --- .../android/crypto/EncryptionService.kt | 29 ++ .../android/mesh/BluetoothMeshService.kt | 96 ++++- .../model/NoiseIdentityAnnouncement.kt | 177 +++++++-- .../android/noise/NoiseEncryptionService.kt | 2 +- .../android/util/BinaryEncodingUtils.kt | 365 ++++++++++++++++++ 5 files changed, 617 insertions(+), 52 deletions(-) create mode 100644 app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt diff --git a/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt b/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt index ea943305..7457de1a 100644 --- a/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/crypto/EncryptionService.kt @@ -54,6 +54,35 @@ class EncryptionService(private val context: Context) { 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 * For backward compatibility with old key exchange packets diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 39ffff24..3ad2ae0f 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -8,6 +8,7 @@ import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.RoutedPacket import com.bitchat.android.model.DeliveryAck import com.bitchat.android.model.ReadReceipt +import com.bitchat.android.model.NoiseIdentityAnnouncement import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.MessageType 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() { - // For discovery, broadcast our static identity key (32 bytes) so peers can identify us - // This is not a handshake initiation, but identity announcement - val identityData = encryptionService.getCombinedPublicKeyData() - - val packet = BitchatPacket( - version = 1u, - type = MessageType.NOISE_IDENTITY_ANNOUNCE.value, // Changed to identity announce - senderID = hexStringToByteArray(myPeerID), - recipientID = SpecialRecipients.BROADCAST, - timestamp = System.currentTimeMillis().toULong(), - payload = identityData, - ttl = 1u - ) - - connectionManager.broadcastPacket(RoutedPacket(packet)) - Log.d(TAG, "Sent Noise identity announcement (${identityData.size} bytes)") + serviceScope.launch { + try { + val nickname = delegate?.getNickname() ?: myPeerID + + // Create the identity announcement using proper binary format + val announcement = createNoiseIdentityAnnouncement(nickname, null) + if (announcement != null) { + val announcementData = announcement.toBinaryData() + + val packet = BitchatPacket( + version = 1u, + type = MessageType.NOISE_IDENTITY_ANNOUNCE.value, + senderID = hexStringToByteArray(myPeerID), + recipientID = SpecialRecipients.BROADCAST, + timestamp = System.currentTimeMillis().toULong(), + 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 + } } /** diff --git a/app/src/main/java/com/bitchat/android/model/NoiseIdentityAnnouncement.kt b/app/src/main/java/com/bitchat/android/model/NoiseIdentityAnnouncement.kt index 559abfcf..2416df61 100644 --- a/app/src/main/java/com/bitchat/android/model/NoiseIdentityAnnouncement.kt +++ b/app/src/main/java/com/bitchat/android/model/NoiseIdentityAnnouncement.kt @@ -1,22 +1,31 @@ package com.bitchat.android.model import android.util.Log -import org.json.JSONObject +import com.bitchat.android.util.* import java.security.MessageDigest import java.util.* /** * Noise Identity Announcement data class (compatible with iOS version) + * Enhanced identity announcement with rotation support and binary protocol */ data class NoiseIdentityAnnouncement( - val peerID: String, - val nickname: String, - val publicKey: ByteArray, - val timestamp: Date, - val signature: ByteArray, - val fingerprint: String?, - val previousPeerID: String? = null + val peerID: String, // Current ephemeral peer ID + val publicKey: ByteArray, // Noise static public key + val signingPublicKey: ByteArray, // Ed25519 signing public key + val nickname: String, // Current nickname + val timestamp: Date, // When this binding was created + val previousPeerID: String?, // Previous peer ID (for smooth transition) + 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 { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -26,9 +35,9 @@ data class NoiseIdentityAnnouncement( if (peerID != other.peerID) return false if (nickname != other.nickname) return false if (!publicKey.contentEquals(other.publicKey)) return false + if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false if (timestamp != other.timestamp) return false if (!signature.contentEquals(other.signature)) return false - if (fingerprint != other.fingerprint) return false if (previousPeerID != other.previousPeerID) return false return true @@ -38,53 +47,153 @@ data class NoiseIdentityAnnouncement( var result = peerID.hashCode() result = 31 * result + nickname.hashCode() result = 31 * result + publicKey.contentHashCode() + result = 31 * result + signingPublicKey.contentHashCode() result = 31 * result + timestamp.hashCode() result = 31 * result + signature.contentHashCode() - result = 31 * result + (fingerprint?.hashCode() ?: 0) result = 31 * result + (previousPeerID?.hashCode() ?: 0) 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 { 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 { - val jsonString = String(payload, Charsets.UTF_8) - val json = JSONObject(jsonString) + // Create defensive copy + val dataCopy = data.copyOf() - val peerID = json.getString("peerID") - val nickname = json.getString("nickname") - val publicKeyBase64 = json.getString("publicKey") - val timestampMs = json.getLong("timestamp") - val signatureBase64 = json.getString("signature") - val previousPeerID = if (json.has("previousPeerID")) json.getString("previousPeerID") else null + // Minimum size check: flags(1) + peerID(8) + min data lengths + if (dataCopy.size < 20) { + Log.w(TAG, "Data too small for NoiseIdentityAnnouncement: ${dataCopy.size} bytes") + return null + } - // Decode base64 fields - val publicKey = android.util.Base64.decode(publicKeyBase64, android.util.Base64.DEFAULT) - val signature = android.util.Base64.decode(signatureBase64, android.util.Base64.DEFAULT) + val offsetArray = intArrayOf(0) - // Calculate fingerprint from public key - val digest = MessageDigest.getInstance("SHA-256") - val hash = digest.digest(publicKey) - val fingerprint = hash.joinToString("") { "%02x".format(it) } + val flags = dataCopy.readUInt8(offsetArray) ?: run { + Log.w(TAG, "Failed to read flags") + return null + } + 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, - nickname = nickname, publicKey = publicKey, - timestamp = Date(timestampMs), - signature = signature, - fingerprint = fingerprint, - previousPeerID = previousPeerID + signingPublicKey = signingPublicKey, + nickname = nickname, + timestamp = timestamp, + previousPeerID = previousPeerID, + signature = signature ) + } 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 } } + + /** + * 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 + } } } diff --git a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt index 9d2b61b3..96a624fe 100644 --- a/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt +++ b/app/src/main/java/com/bitchat/android/noise/NoiseEncryptionService.kt @@ -152,7 +152,7 @@ class NoiseEncryptionService(private val context: Context) { */ fun encrypt(data: ByteArray, peerID: String): ByteArray? { 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) return null } diff --git a/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt b/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt new file mode 100644 index 00000000..549f5971 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/util/BinaryEncodingUtils.kt @@ -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() + + // Make buffer accessible for direct manipulation when needed + val buffer: MutableList 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 +}