mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 00:45:22 +00:00
refactor
This commit is contained in:
@@ -3,6 +3,7 @@ package com.bitchat.android.favorites
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||
import com.bitchat.android.util.toHexString
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.util.*
|
||||
@@ -95,7 +96,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
||||
|
||||
/** Get favorite status for Noise public key */
|
||||
fun getFavoriteStatus(noisePublicKey: ByteArray): FavoriteRelationship? {
|
||||
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
val keyHex = noisePublicKey.toHexString()
|
||||
return favorites[keyHex]
|
||||
}
|
||||
|
||||
@@ -103,7 +104,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
||||
fun getFavoriteStatus(peerID: String): FavoriteRelationship? {
|
||||
val pid = peerID.lowercase()
|
||||
for ((_, relationship) in favorites) {
|
||||
val noiseKeyHex = relationship.peerNoisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
val noiseKeyHex = relationship.peerNoisePublicKey.toHexString()
|
||||
if (noiseKeyHex.startsWith(pid)) return relationship
|
||||
}
|
||||
return null
|
||||
@@ -111,7 +112,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
||||
|
||||
/** Update Nostr public key for a peer (indexed by Noise key) */
|
||||
fun updateNostrPublicKey(noisePublicKey: ByteArray, nostrPubkey: String) {
|
||||
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
val keyHex = noisePublicKey.toHexString()
|
||||
val existing = favorites[keyHex]
|
||||
|
||||
if (existing != null) {
|
||||
@@ -168,7 +169,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
||||
// Find relationship with matching nostr pubkey (normalized to hex) and then try to map to current peerID via noise key prefix
|
||||
val rel = favorites.values.firstOrNull { it.peerNostrPublicKey?.let { stored -> normalizeNostrKeyToHex(stored) } == targetHex }
|
||||
if (rel != null) {
|
||||
val noiseHex = rel.peerNoisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
val noiseHex = rel.peerNoisePublicKey.toHexString()
|
||||
// Return 16-hex prefix as best-effort if no explicit mapping exists
|
||||
return noiseHex.take(16)
|
||||
}
|
||||
@@ -178,7 +179,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
||||
|
||||
/** Update favorite status */
|
||||
fun updateFavoriteStatus(noisePublicKey: ByteArray, nickname: String, isFavorite: Boolean) {
|
||||
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
val keyHex = noisePublicKey.toHexString()
|
||||
|
||||
val existing = favorites[keyHex]
|
||||
|
||||
@@ -210,7 +211,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
||||
|
||||
/** Update peer favorited-us flag */
|
||||
fun updatePeerFavoritedUs(noisePublicKey: ByteArray, theyFavoritedUs: Boolean) {
|
||||
val keyHex = noisePublicKey.joinToString("") { "%02x".format(it) }
|
||||
val keyHex = noisePublicKey.toHexString()
|
||||
val existing = favorites[keyHex]
|
||||
|
||||
if (existing != null) {
|
||||
@@ -248,7 +249,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
||||
|
||||
/** Find Nostr pubkey by Noise key */
|
||||
fun findNostrPubkey(forNoiseKey: ByteArray): String? {
|
||||
val keyHex = forNoiseKey.joinToString("") { "%02x".format(it) }
|
||||
val keyHex = forNoiseKey.toHexString()
|
||||
return favorites[keyHex]?.peerNostrPublicKey
|
||||
}
|
||||
|
||||
@@ -330,7 +331,7 @@ class FavoritesPersistenceService private constructor(private val context: Conte
|
||||
private fun normalizeNostrKeyToHex(value: String): String? = try {
|
||||
if (value.startsWith("npub1")) {
|
||||
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(value)
|
||||
if (hrp != "npub") null else data.joinToString("") { "%02x".format(it) }
|
||||
if (hrp != "npub") null else data.toHexString()
|
||||
} else value.lowercase()
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
@@ -348,7 +349,7 @@ private data class FavoriteRelationshipData(
|
||||
companion object {
|
||||
fun fromFavoriteRelationship(relationship: FavoriteRelationship): FavoriteRelationshipData {
|
||||
return FavoriteRelationshipData(
|
||||
peerNoisePublicKeyHex = relationship.peerNoisePublicKey.joinToString("") { "%02x".format(it) },
|
||||
peerNoisePublicKeyHex = relationship.peerNoisePublicKey.toHexString(),
|
||||
peerNostrPublicKey = relationship.peerNostrPublicKey,
|
||||
peerNickname = relationship.peerNickname,
|
||||
isFavorite = relationship.isFavorite,
|
||||
|
||||
@@ -4,10 +4,9 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import java.security.MessageDigest
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.bitchat.android.util.hexEncodedString
|
||||
import com.bitchat.android.util.Hashing
|
||||
import androidx.core.content.edit
|
||||
|
||||
/**
|
||||
@@ -175,9 +174,7 @@ class SecureIdentityStateManager(private val context: Context) {
|
||||
* Generate fingerprint from public key (SHA-256 hash)
|
||||
*/
|
||||
fun generateFingerprint(publicKeyData: ByteArray): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(publicKeyData)
|
||||
return hash.hexEncodedString()
|
||||
return Hashing.sha256Hex(publicKeyData)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.util.AppConstants
|
||||
import com.bitchat.android.util.PeerId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -323,7 +324,7 @@ class BluetoothGattClientManager(
|
||||
// Try to extract peerID from Service Data (if available) for stable identity
|
||||
val serviceData = scanRecord?.getServiceData(ParcelUuid(AppConstants.Mesh.Gatt.SERVICE_UUID))
|
||||
val peerID = if (serviceData != null && serviceData.size >= 8) {
|
||||
serviceData.joinToString("") { "%02x".format(it) }
|
||||
PeerId.fromBytes(serviceData)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -515,7 +516,7 @@ class BluetoothGattClientManager(
|
||||
Log.i(TAG, "Client: Received packet from ${gatt.device.address}, size: ${value.size} bytes")
|
||||
val packet = BitchatPacket.fromBinaryData(value)
|
||||
if (packet != null) {
|
||||
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
|
||||
val peerID = PeerId.fromBytes(packet.senderID)
|
||||
Log.d(TAG, "Client: Parsed packet type ${packet.type} from $peerID")
|
||||
delegate?.onPacketReceived(packet, peerID, gatt.device)
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.util.AppConstants
|
||||
import com.bitchat.android.util.PeerId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -223,7 +224,7 @@ class BluetoothGattServerManager(
|
||||
Log.i(TAG, "Server: Received packet from ${device.address}, size: ${value.size} bytes")
|
||||
val packet = BitchatPacket.fromBinaryData(value)
|
||||
if (packet != null) {
|
||||
val peerID = packet.senderID.take(8).toByteArray().joinToString("") { "%02x".format(it) }
|
||||
val peerID = PeerId.fromBytes(packet.senderID)
|
||||
Log.d(TAG, "Server: Parsed packet type ${packet.type} from $peerID")
|
||||
delegate?.onPacketReceived(packet, peerID, device)
|
||||
} else {
|
||||
@@ -377,7 +378,7 @@ class BluetoothGattServerManager(
|
||||
val mode = try {
|
||||
powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]
|
||||
} catch (_: Exception) { "unknown" }
|
||||
Log.i(TAG, "Advertising started (power mode: $mode) with stable ID: ${peerIDBytes.joinToString("") { "%02x".format(it) }}")
|
||||
Log.i(TAG, "Advertising started (power mode: $mode) with stable ID: ${PeerId.fromBytes(peerIDBytes)}")
|
||||
}
|
||||
|
||||
override fun onStartFailure(errorCode: Int) {
|
||||
|
||||
@@ -14,6 +14,8 @@ import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import com.bitchat.android.model.RequestSyncPacket
|
||||
import com.bitchat.android.sync.GossipSyncManager
|
||||
import com.bitchat.android.util.Hashing
|
||||
import com.bitchat.android.util.PeerId
|
||||
import com.bitchat.android.util.toHexString
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import kotlinx.coroutines.*
|
||||
@@ -184,8 +186,8 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val responsePacket = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = PeerId.toBytes(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = response,
|
||||
ttl = MAX_TTL
|
||||
@@ -296,8 +298,8 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = PeerId.toBytes(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = handshakeData,
|
||||
ttl = MAX_TTL
|
||||
@@ -662,7 +664,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.MESSAGE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = SpecialRecipients.BROADCAST,
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = content.toByteArray(Charsets.UTF_8),
|
||||
@@ -692,7 +694,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val packet = BitchatPacket(
|
||||
version = 2u, // FILE_TRANSFER uses v2 for 4-byte payload length to support large files
|
||||
type = MessageType.FILE_TRANSFER.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = SpecialRecipients.BROADCAST,
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
@@ -701,7 +703,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
)
|
||||
val signed = signPacketBeforeBroadcast(packet)
|
||||
// Use a stable transferId based on the file TLV payload for progress tracking
|
||||
val transferId = sha256Hex(payload)
|
||||
val transferId = Hashing.sha256Hex(payload)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
|
||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||
}
|
||||
@@ -744,8 +746,8 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = PeerId.toBytes(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
@@ -755,7 +757,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Sign and send the encrypted packet
|
||||
val signed = signPacketBeforeBroadcast(packet)
|
||||
// Use a stable transferId based on the unencrypted file TLV payload for progress tracking
|
||||
val transferId = sha256Hex(filePayload)
|
||||
val transferId = Hashing.sha256Hex(filePayload)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -777,13 +779,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
return connectionManager.cancelTransfer(transferId)
|
||||
}
|
||||
|
||||
// Local helper to hash payloads to a stable hex ID for progress mapping
|
||||
private fun sha256Hex(bytes: ByteArray): String = try {
|
||||
val md = java.security.MessageDigest.getInstance("SHA-256")
|
||||
md.update(bytes)
|
||||
md.digest().joinToString("") { "%02x".format(it) }
|
||||
} catch (_: Exception) { bytes.size.toString(16) }
|
||||
|
||||
/**
|
||||
* Send private message - SIMPLIFIED iOS-compatible version
|
||||
* Uses NoisePayloadType system exactly like iOS SimplifiedBluetoothService
|
||||
@@ -823,8 +818,8 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = PeerId.toBytes(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
@@ -889,8 +884,8 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = PeerId.toBytes(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
@@ -937,8 +932,8 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = PeerId.toBytes(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
@@ -1247,27 +1242,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign packet before broadcasting using our signing private key
|
||||
*/
|
||||
@@ -1277,12 +1251,12 @@ class BluetoothMeshService(private val context: Context) {
|
||||
val withRoute = try {
|
||||
val rec = packet.recipientID
|
||||
if (rec != null && !rec.contentEquals(SpecialRecipients.BROADCAST)) {
|
||||
val dest = rec.joinToString("") { b -> "%02x".format(b) }
|
||||
val dest = rec.toHexString()
|
||||
val path = com.bitchat.android.services.meshgraph.RoutePlanner.shortestPath(myPeerID, dest)
|
||||
if (path != null && path.size >= 3) {
|
||||
// Exclude first (sender) and last (recipient); only intermediates
|
||||
val intermediates = path.subList(1, path.size - 1)
|
||||
val hopsBytes = intermediates.map { hexStringToByteArray(it) }
|
||||
val hopsBytes = intermediates.map { PeerId.toBytes(it) }
|
||||
// Attach route and upgrade to v2 (required for HAS_ROUTE flag)
|
||||
packet.copy(route = hopsBytes, version = 2u)
|
||||
} else packet.copy(route = null)
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.util.Log
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.util.Hashing
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -122,7 +123,7 @@ class BluetoothPacketBroadcaster(
|
||||
val packet = routed.packet
|
||||
val isFile = packet.type == MessageType.FILE_TRANSFER.value
|
||||
// Prefer caller-provided transferId (e.g., for encrypted media), else derive for FILE_TRANSFER
|
||||
val transferId = routed.transferId ?: (if (isFile) sha256Hex(packet.payload) else null)
|
||||
val transferId = routed.transferId ?: (if (isFile) Hashing.sha256Hex(packet.payload) else null)
|
||||
// Check if we need to fragment
|
||||
if (fragmentManager != null) {
|
||||
val fragments = try {
|
||||
@@ -193,7 +194,7 @@ class BluetoothPacketBroadcaster(
|
||||
val data = packet.toBinaryData() ?: return false
|
||||
val isFile = packet.type == MessageType.FILE_TRANSFER.value
|
||||
// Prefer caller-provided transferId (e.g., for encrypted media), else derive for FILE_TRANSFER
|
||||
val transferId = routed.transferId ?: (if (isFile) sha256Hex(packet.payload) else null)
|
||||
val transferId = routed.transferId ?: (if (isFile) Hashing.sha256Hex(packet.payload) else null)
|
||||
if (transferId != null) {
|
||||
TransferProgressManager.start(transferId, 1)
|
||||
}
|
||||
@@ -236,13 +237,6 @@ class BluetoothPacketBroadcaster(
|
||||
return false
|
||||
}
|
||||
|
||||
private fun sha256Hex(bytes: ByteArray): String = try {
|
||||
val md = java.security.MessageDigest.getInstance("SHA-256")
|
||||
md.update(bytes)
|
||||
md.digest().joinToString("") { "%02x".format(it) }
|
||||
} catch (_: Exception) { bytes.size.toString(16) }
|
||||
|
||||
|
||||
/**
|
||||
* Public entry point for broadcasting - submits request to actor for serialization
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.sync.PacketIdUtil
|
||||
import com.bitchat.android.util.PeerId
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
@@ -181,8 +182,8 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(senderPeerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = PeerId.toBytes(senderPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encryptedPayload,
|
||||
signature = null,
|
||||
@@ -302,8 +303,8 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
val responsePacket = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = PeerId.toBytes(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = response,
|
||||
signature = null,
|
||||
@@ -468,27 +469,6 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex string peer ID to binary data (8 bytes) - same as iOS implementation
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown the handler
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.bitchat.android.protocol.MessageType
|
||||
import android.util.Log
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.util.PeerId
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.random.Random
|
||||
@@ -66,7 +67,7 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
Log.w(TAG, "Packet with duplicate hops dropped")
|
||||
return
|
||||
}
|
||||
val myIdBytes = hexStringToPeerBytes(myPeerID)
|
||||
val myIdBytes = PeerId.toBytes(myPeerID)
|
||||
val index = route.indexOfFirst { it.contentEquals(myIdBytes) }
|
||||
if (index >= 0) {
|
||||
val nextHopIdHex: String? = run {
|
||||
@@ -186,15 +187,3 @@ interface PacketRelayManagerDelegate {
|
||||
fun broadcastPacket(routed: RoutedPacket)
|
||||
fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean
|
||||
}
|
||||
|
||||
private fun hexStringToPeerBytes(hex: String): ByteArray {
|
||||
val result = ByteArray(8)
|
||||
var idx = 0
|
||||
var out = 0
|
||||
while (idx + 1 < hex.length && out < 8) {
|
||||
val b = hex.substring(idx, idx + 2).toIntOrNull(16)?.toByte() ?: 0
|
||||
result[out++] = b
|
||||
idx += 2
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.util.Log
|
||||
import java.security.MessageDigest
|
||||
import com.bitchat.android.util.Hashing
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
@@ -204,9 +204,7 @@ class PeerFingerprintManager private constructor() {
|
||||
* @return The hex-encoded SHA-256 hash
|
||||
*/
|
||||
private fun calculateFingerprint(publicKey: ByteArray): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(publicKey)
|
||||
return hash.joinToString("") { "%02x".format(it) }
|
||||
return Hashing.sha256Hex(publicKey)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvReader
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.protocol.UnknownTlvPolicy
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
@@ -7,16 +11,13 @@ import java.nio.ByteOrder
|
||||
* BitchatFilePacket: TLV-encoded file transfer payload for BLE mesh.
|
||||
* TLVs:
|
||||
* - 0x01: filename (UTF-8)
|
||||
* - 0x02: file size (8 bytes, UInt64)
|
||||
* - 0x02: file size (4 bytes, UInt32)
|
||||
* - 0x03: mime type (UTF-8)
|
||||
* - 0x04: content (bytes) — may appear multiple times for large files
|
||||
* - 0x04: content (bytes)
|
||||
*
|
||||
* Length field for TLV is 2 bytes (UInt16, big-endian) for all TLVs.
|
||||
* For large files, CONTENT is chunked into multiple TLVs of up to 65535 bytes each.
|
||||
*
|
||||
* Note: The outer BitchatPacket uses version 2 (4-byte payload length), so this
|
||||
* TLV payload can exceed 64 KiB even though each TLV value is limited to 65535 bytes.
|
||||
* Transport-level fragmentation then splits the final packet for BLE MTU.
|
||||
* FILE_NAME, FILE_SIZE, and MIME_TYPE use 2-byte big-endian lengths.
|
||||
* CONTENT uses a 4-byte big-endian length so the outer v2 packet can carry
|
||||
* payloads larger than 64 KiB before transport fragmentation.
|
||||
*/
|
||||
data class BitchatFilePacket(
|
||||
val fileName: String,
|
||||
@@ -30,87 +31,53 @@ data class BitchatFilePacket(
|
||||
}
|
||||
|
||||
fun encode(): ByteArray? {
|
||||
try {
|
||||
android.util.Log.d("BitchatFilePacket", "🔄 Encoding: name=$fileName, size=$fileSize, mime=$mimeType")
|
||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
||||
val mimeBytes = mimeType.toByteArray(Charsets.UTF_8)
|
||||
// Validate bounds for 2-byte TLV lengths (per-TLV). CONTENT may exceed 65535 and will be chunked.
|
||||
if (nameBytes.size > 0xFFFF || mimeBytes.size > 0xFFFF) {
|
||||
android.util.Log.e("BitchatFilePacket", "❌ TLV field too large: name=${nameBytes.size}, mime=${mimeBytes.size} (max: 65535)")
|
||||
return try {
|
||||
val nameBytes = fileName.toByteArray(Charsets.UTF_8)
|
||||
val mimeBytes = mimeType.toByteArray(Charsets.UTF_8)
|
||||
if (nameBytes.size > 0xFFFF || mimeBytes.size > 0xFFFF) {
|
||||
return null
|
||||
}
|
||||
if (content.size > 0xFFFF) {
|
||||
android.util.Log.d("BitchatFilePacket", "📦 Content exceeds 65535 bytes (${content.size}); will be split into multiple CONTENT TLVs")
|
||||
} else {
|
||||
android.util.Log.d("BitchatFilePacket", "📏 TLV sizes OK: name=${nameBytes.size}, mime=${mimeBytes.size}, content=${content.size}")
|
||||
}
|
||||
val sizeFieldLen = 4 // UInt32 for FILE_SIZE (changed from 8 bytes)
|
||||
val contentLenFieldLen = 4 // UInt32 for CONTENT TLV as requested
|
||||
|
||||
// Compute capacity: header TLVs + single CONTENT TLV with 4-byte length
|
||||
val contentTLVBytes = 1 + contentLenFieldLen + content.size
|
||||
val capacity = (1 + 2 + nameBytes.size) + (1 + 2 + sizeFieldLen) + (1 + 2 + mimeBytes.size) + contentTLVBytes
|
||||
val buf = ByteBuffer.allocate(capacity).order(ByteOrder.BIG_ENDIAN)
|
||||
val sizeBytes = ByteBuffer.allocate(4)
|
||||
.order(ByteOrder.BIG_ENDIAN)
|
||||
.putInt(fileSize.toInt())
|
||||
.array()
|
||||
|
||||
// FILE_NAME
|
||||
buf.put(TLVType.FILE_NAME.v.toByte())
|
||||
buf.putShort(nameBytes.size.toShort())
|
||||
buf.put(nameBytes)
|
||||
|
||||
// FILE_SIZE (4 bytes)
|
||||
buf.put(TLVType.FILE_SIZE.v.toByte())
|
||||
buf.putShort(sizeFieldLen.toShort())
|
||||
buf.putInt(fileSize.toInt())
|
||||
|
||||
// MIME_TYPE
|
||||
buf.put(TLVType.MIME_TYPE.v.toByte())
|
||||
buf.putShort(mimeBytes.size.toShort())
|
||||
buf.put(mimeBytes)
|
||||
|
||||
// CONTENT (single TLV with 4-byte length)
|
||||
buf.put(TLVType.CONTENT.v.toByte())
|
||||
buf.putInt(content.size)
|
||||
buf.put(content)
|
||||
|
||||
val result = buf.array()
|
||||
android.util.Log.d("BitchatFilePacket", "✅ Encoded successfully: ${result.size} bytes total")
|
||||
return result
|
||||
TlvWriter()
|
||||
.put(TLVType.FILE_NAME.v.toInt(), nameBytes, TlvLengthSize.TWO_BYTES)
|
||||
.put(TLVType.FILE_SIZE.v.toInt(), sizeBytes, TlvLengthSize.TWO_BYTES)
|
||||
.put(TLVType.MIME_TYPE.v.toInt(), mimeBytes, TlvLengthSize.TWO_BYTES)
|
||||
.put(TLVType.CONTENT.v.toInt(), content, TlvLengthSize.FOUR_BYTES)
|
||||
.toByteArray()
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("BitchatFilePacket", "❌ Encoding failed: ${e.message}", e)
|
||||
return null
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun decode(data: ByteArray): BitchatFilePacket? {
|
||||
android.util.Log.d("BitchatFilePacket", "🔄 Decoding ${data.size} bytes")
|
||||
try {
|
||||
var off = 0
|
||||
val knownTypes = TLVType.values().map { it.v.toInt() }.toSet()
|
||||
val fields = TlvReader.decode(
|
||||
data = data,
|
||||
defaultLengthSize = TlvLengthSize.TWO_BYTES,
|
||||
unknownPolicy = UnknownTlvPolicy.FAIL,
|
||||
knownTypes = knownTypes
|
||||
) { type ->
|
||||
if (type == TLVType.CONTENT.v.toInt()) TlvLengthSize.FOUR_BYTES else TlvLengthSize.TWO_BYTES
|
||||
} ?: return null
|
||||
|
||||
var name: String? = null
|
||||
var size: Long? = null
|
||||
var mime: String? = null
|
||||
var contentBytes: ByteArray? = null
|
||||
while (off + 3 <= data.size) { // minimum TLV header size (type + 2 bytes length)
|
||||
val t = TLVType.from(data[off].toUByte()) ?: return null
|
||||
off += 1
|
||||
// CONTENT uses 4-byte length; others use 2-byte length
|
||||
val len: Int
|
||||
if (t == TLVType.CONTENT) {
|
||||
if (off + 4 > data.size) return null
|
||||
len = ((data[off].toInt() and 0xFF) shl 24) or ((data[off + 1].toInt() and 0xFF) shl 16) or ((data[off + 2].toInt() and 0xFF) shl 8) or (data[off + 3].toInt() and 0xFF)
|
||||
off += 4
|
||||
} else {
|
||||
if (off + 2 > data.size) return null
|
||||
len = ((data[off].toInt() and 0xFF) shl 8) or (data[off + 1].toInt() and 0xFF)
|
||||
off += 2
|
||||
}
|
||||
if (len < 0 || off + len > data.size) return null
|
||||
val value = data.copyOfRange(off, off + len)
|
||||
off += len
|
||||
for (field in fields) {
|
||||
val t = TLVType.from(field.type.toUByte()) ?: return null
|
||||
val value = field.value
|
||||
when (t) {
|
||||
TLVType.FILE_NAME -> name = String(value, Charsets.UTF_8)
|
||||
TLVType.FILE_SIZE -> {
|
||||
if (len != 4) return null
|
||||
if (value.size != 4) return null
|
||||
val bb = ByteBuffer.wrap(value).order(ByteOrder.BIG_ENDIAN)
|
||||
size = bb.int.toLong()
|
||||
}
|
||||
@@ -128,14 +95,10 @@ data class BitchatFilePacket(
|
||||
val c = contentBytes ?: return null
|
||||
val s = size ?: c.size.toLong()
|
||||
val m = mime ?: "application/octet-stream"
|
||||
val result = BitchatFilePacket(n, s, m, c)
|
||||
android.util.Log.d("BitchatFilePacket", "✅ Decoded: name=$n, size=$s, mime=$m, content=${c.size} bytes")
|
||||
return result
|
||||
return BitchatFilePacket(n, s, m, c)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("BitchatFilePacket", "❌ Decoding failed: ${e.message}", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.util.toHexString
|
||||
|
||||
/**
|
||||
* FragmentPayload - 100% iOS-compatible fragment payload structure
|
||||
@@ -111,7 +112,7 @@ data class FragmentPayload(
|
||||
* Get fragment ID as hex string for logging/debugging
|
||||
*/
|
||||
fun getFragmentIDString(): String {
|
||||
return fragmentID.joinToString("") { "%02x".format(it) }
|
||||
return fragmentID.toHexString()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvReader
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.protocol.UnknownTlvPolicy
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.bitchat.android.util.*
|
||||
|
||||
/**
|
||||
* Identity announcement structure with TLV encoding
|
||||
@@ -36,29 +40,15 @@ data class IdentityAnnouncement(
|
||||
fun encode(): ByteArray? {
|
||||
val nicknameData = nickname.toByteArray(Charsets.UTF_8)
|
||||
|
||||
// Check size limits
|
||||
if (nicknameData.size > 255 || noisePublicKey.size > 255 || signingPublicKey.size > 255) {
|
||||
return null
|
||||
return try {
|
||||
TlvWriter()
|
||||
.put(TLVType.NICKNAME.value.toInt(), nicknameData, TlvLengthSize.ONE_BYTE)
|
||||
.put(TLVType.NOISE_PUBLIC_KEY.value.toInt(), noisePublicKey, TlvLengthSize.ONE_BYTE)
|
||||
.put(TLVType.SIGNING_PUBLIC_KEY.value.toInt(), signingPublicKey, TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
val result = mutableListOf<Byte>()
|
||||
|
||||
// TLV for nickname
|
||||
result.add(TLVType.NICKNAME.value.toByte())
|
||||
result.add(nicknameData.size.toByte())
|
||||
result.addAll(nicknameData.toList())
|
||||
|
||||
// TLV for noise public key
|
||||
result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte())
|
||||
result.add(noisePublicKey.size.toByte())
|
||||
result.addAll(noisePublicKey.toList())
|
||||
|
||||
// TLV for signing public key
|
||||
result.add(TLVType.SIGNING_PUBLIC_KEY.value.toByte())
|
||||
result.add(signingPublicKey.size.toByte())
|
||||
result.addAll(signingPublicKey.toList())
|
||||
|
||||
return result.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -66,45 +56,29 @@ data class IdentityAnnouncement(
|
||||
* Decode from TLV binary data matching iOS implementation
|
||||
*/
|
||||
fun decode(data: ByteArray): IdentityAnnouncement? {
|
||||
// Create defensive copy
|
||||
val dataCopy = data.copyOf()
|
||||
|
||||
var offset = 0
|
||||
var nickname: String? = null
|
||||
var noisePublicKey: ByteArray? = null
|
||||
var signingPublicKey: ByteArray? = null
|
||||
|
||||
while (offset + 2 <= dataCopy.size) {
|
||||
// Read TLV type
|
||||
val typeValue = dataCopy[offset].toUByte()
|
||||
val type = TLVType.fromValue(typeValue)
|
||||
offset += 1
|
||||
|
||||
// Read TLV length
|
||||
val length = dataCopy[offset].toUByte().toInt()
|
||||
offset += 1
|
||||
|
||||
// Check bounds
|
||||
if (offset + length > dataCopy.size) return null
|
||||
|
||||
// Read TLV value
|
||||
val value = dataCopy.sliceArray(offset until offset + length)
|
||||
offset += length
|
||||
|
||||
// Process known TLV types, skip unknown ones for forward compatibility
|
||||
|
||||
val knownTypes = TLVType.values().map { it.value.toInt() }.toSet()
|
||||
val fields = TlvReader.decode(
|
||||
data = data,
|
||||
defaultLengthSize = TlvLengthSize.ONE_BYTE,
|
||||
unknownPolicy = UnknownTlvPolicy.SKIP,
|
||||
knownTypes = knownTypes
|
||||
) ?: return null
|
||||
|
||||
for (field in fields) {
|
||||
val type = TLVType.fromValue(field.type.toUByte()) ?: continue
|
||||
when (type) {
|
||||
TLVType.NICKNAME -> {
|
||||
nickname = String(value, Charsets.UTF_8)
|
||||
nickname = String(field.value, Charsets.UTF_8)
|
||||
}
|
||||
TLVType.NOISE_PUBLIC_KEY -> {
|
||||
noisePublicKey = value
|
||||
noisePublicKey = field.value
|
||||
}
|
||||
TLVType.SIGNING_PUBLIC_KEY -> {
|
||||
signingPublicKey = value
|
||||
}
|
||||
null -> {
|
||||
// Unknown TLV; skip (tolerant decoder for forward compatibility)
|
||||
continue
|
||||
signingPublicKey = field.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +114,6 @@ data class IdentityAnnouncement(
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "IdentityAnnouncement(nickname='$nickname', noisePublicKey=${noisePublicKey.joinToString("") { "%02x".format(it) }.take(16)}..., signingPublicKey=${signingPublicKey.joinToString("") { "%02x".format(it) }.take(16)}...)"
|
||||
return "IdentityAnnouncement(nickname='$nickname', noisePublicKey=${noisePublicKey.toHexString().take(16)}..., signingPublicKey=${signingPublicKey.toHexString().take(16)}...)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvReader
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.protocol.UnknownTlvPolicy
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
@@ -127,24 +131,14 @@ data class PrivateMessagePacket(
|
||||
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
|
||||
return try {
|
||||
TlvWriter()
|
||||
.put(TLVType.MESSAGE_ID.value.toInt(), messageIDData, TlvLengthSize.ONE_BYTE)
|
||||
.put(TLVType.CONTENT.value.toInt(), contentData, TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
} catch (_: IllegalArgumentException) {
|
||||
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 {
|
||||
@@ -152,33 +146,25 @@ data class PrivateMessagePacket(
|
||||
* 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
|
||||
|
||||
|
||||
val knownTypes = TLVType.values().map { it.value.toInt() }.toSet()
|
||||
val fields = TlvReader.decode(
|
||||
data = data,
|
||||
defaultLengthSize = TlvLengthSize.ONE_BYTE,
|
||||
unknownPolicy = UnknownTlvPolicy.FAIL,
|
||||
knownTypes = knownTypes
|
||||
) ?: return null
|
||||
|
||||
for (field in fields) {
|
||||
val type = TLVType.fromValue(field.type.toUByte()) ?: return null
|
||||
when (type) {
|
||||
TLVType.MESSAGE_ID -> {
|
||||
messageID = String(value, Charsets.UTF_8)
|
||||
messageID = String(field.value, Charsets.UTF_8)
|
||||
}
|
||||
TLVType.CONTENT -> {
|
||||
content = String(value, Charsets.UTF_8)
|
||||
content = String(field.value, Charsets.UTF_8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvReader
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.protocol.UnknownTlvPolicy
|
||||
import com.bitchat.android.sync.SyncDefaults
|
||||
|
||||
/**
|
||||
@@ -15,30 +19,21 @@ data class RequestSyncPacket(
|
||||
val data: ByteArray
|
||||
) {
|
||||
fun encode(): ByteArray {
|
||||
val out = ArrayList<Byte>()
|
||||
fun putTLV(t: Int, v: ByteArray) {
|
||||
out.add(t.toByte())
|
||||
val len = v.size
|
||||
out.add(((len ushr 8) and 0xFF).toByte())
|
||||
out.add((len and 0xFF).toByte())
|
||||
out.addAll(v.toList())
|
||||
}
|
||||
// P
|
||||
putTLV(0x01, byteArrayOf(p.toByte()))
|
||||
// M (uint32)
|
||||
val m32 = m.coerceAtMost(0xffff_ffffL)
|
||||
putTLV(
|
||||
0x02,
|
||||
byteArrayOf(
|
||||
((m32 ushr 24) and 0xFF).toByte(),
|
||||
((m32 ushr 16) and 0xFF).toByte(),
|
||||
((m32 ushr 8) and 0xFF).toByte(),
|
||||
(m32 and 0xFF).toByte()
|
||||
return TlvWriter()
|
||||
.put(0x01, byteArrayOf(p.toByte()), TlvLengthSize.TWO_BYTES)
|
||||
.put(
|
||||
0x02,
|
||||
byteArrayOf(
|
||||
((m32 ushr 24) and 0xFF).toByte(),
|
||||
((m32 ushr 16) and 0xFF).toByte(),
|
||||
((m32 ushr 8) and 0xFF).toByte(),
|
||||
(m32 and 0xFF).toByte()
|
||||
),
|
||||
TlvLengthSize.TWO_BYTES
|
||||
)
|
||||
)
|
||||
// data
|
||||
putTLV(0x03, data)
|
||||
return out.toByteArray()
|
||||
.put(0x03, data, TlvLengthSize.TWO_BYTES)
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -46,28 +41,30 @@ data class RequestSyncPacket(
|
||||
const val MAX_ACCEPT_FILTER_BYTES: Int = SyncDefaults.MAX_ACCEPT_FILTER_BYTES
|
||||
|
||||
fun decode(data: ByteArray): RequestSyncPacket? {
|
||||
var off = 0
|
||||
var p: Int? = null
|
||||
var m: Long? = null
|
||||
var payload: ByteArray? = null
|
||||
|
||||
while (off + 3 <= data.size) {
|
||||
val t = (data[off].toInt() and 0xFF); off += 1
|
||||
val len = ((data[off].toInt() and 0xFF) shl 8) or (data[off+1].toInt() and 0xFF); off += 2
|
||||
if (off + len > data.size) return null
|
||||
val v = data.copyOfRange(off, off + len); off += len
|
||||
when (t) {
|
||||
0x01 -> if (len == 1) p = (v[0].toInt() and 0xFF)
|
||||
0x02 -> if (len == 4) {
|
||||
val mm = ((v[0].toLong() and 0xFF) shl 24) or
|
||||
((v[1].toLong() and 0xFF) shl 16) or
|
||||
((v[2].toLong() and 0xFF) shl 8) or
|
||||
(v[3].toLong() and 0xFF)
|
||||
val fields = TlvReader.decode(
|
||||
data = data,
|
||||
defaultLengthSize = TlvLengthSize.TWO_BYTES,
|
||||
unknownPolicy = UnknownTlvPolicy.SKIP,
|
||||
knownTypes = setOf(0x01, 0x02, 0x03)
|
||||
) ?: return null
|
||||
|
||||
for (field in fields) {
|
||||
when (field.type) {
|
||||
0x01 -> if (field.value.size == 1) p = (field.value[0].toInt() and 0xFF)
|
||||
0x02 -> if (field.value.size == 4) {
|
||||
val mm = ((field.value[0].toLong() and 0xFF) shl 24) or
|
||||
((field.value[1].toLong() and 0xFF) shl 16) or
|
||||
((field.value[2].toLong() and 0xFF) shl 8) or
|
||||
(field.value[3].toLong() and 0xFF)
|
||||
m = mm
|
||||
}
|
||||
0x03 -> {
|
||||
if (v.size > MAX_ACCEPT_FILTER_BYTES) return null
|
||||
payload = v
|
||||
if (field.value.size > MAX_ACCEPT_FILTER_BYTES) return null
|
||||
payload = field.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.bitchat.android.noise
|
||||
|
||||
import android.util.Log
|
||||
import java.security.MessageDigest
|
||||
import com.bitchat.android.util.Hashing
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKeyFactory
|
||||
@@ -174,9 +174,7 @@ class NoiseChannelEncryption {
|
||||
val key = channelKeys[channel] ?: return null
|
||||
|
||||
return try {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(key.encoded)
|
||||
hash.joinToString("") { "%02x".format(it) }
|
||||
Hashing.sha256Hex(key.encoded)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to calculate key commitment: ${e.message}")
|
||||
null
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.util.Log
|
||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||
import com.bitchat.android.mesh.PeerFingerprintManager
|
||||
import com.bitchat.android.noise.southernstorm.protocol.Noise
|
||||
import java.security.MessageDigest
|
||||
import com.bitchat.android.util.Hashing
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@@ -133,9 +133,7 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
* Get our identity fingerprint (SHA-256 hash of static public key)
|
||||
*/
|
||||
fun getIdentityFingerprint(): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(staticIdentityPublicKey)
|
||||
return hash.joinToString("") { "%02x".format(it) }
|
||||
return Hashing.sha256Hex(staticIdentityPublicKey)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,9 +385,7 @@ class NoiseEncryptionService(private val context: Context) {
|
||||
* Calculate fingerprint from public key (SHA-256 hash)
|
||||
*/
|
||||
private fun calculateFingerprint(publicKey: ByteArray): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val hash = digest.digest(publicKey)
|
||||
return hash.joinToString("") { "%02x".format(it) }
|
||||
return Hashing.sha256Hex(publicKey)
|
||||
}
|
||||
|
||||
// MARK: - Packet Signing/Verification
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.bitchat.android.nostr
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.util.hexToByteArray
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.bitchat.android.nostr
|
||||
|
||||
import com.bitchat.android.util.hexToByteArray
|
||||
import com.bitchat.android.util.toHexString
|
||||
import org.bouncycastle.crypto.ec.CustomNamedCurves
|
||||
import org.bouncycastle.crypto.params.ECDomainParameters
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters
|
||||
|
||||
@@ -6,6 +6,9 @@ import com.bitchat.android.model.PrivateMessagePacket
|
||||
import com.bitchat.android.model.NoisePayloadType
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.util.Hex
|
||||
import com.bitchat.android.util.PeerId
|
||||
import com.bitchat.android.util.toHexString
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@@ -41,8 +44,8 @@ object NostrEmbeddedBitChat {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(senderPeerID),
|
||||
recipientID = hexStringToByteArray(recipientIDHex),
|
||||
senderID = PeerId.toBytes(senderPeerID),
|
||||
recipientID = PeerId.toBytes(recipientIDHex),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
signature = null,
|
||||
@@ -81,8 +84,8 @@ object NostrEmbeddedBitChat {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(senderPeerID),
|
||||
recipientID = hexStringToByteArray(recipientIDHex),
|
||||
senderID = PeerId.toBytes(senderPeerID),
|
||||
recipientID = PeerId.toBytes(recipientIDHex),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
signature = null,
|
||||
@@ -118,7 +121,7 @@ object NostrEmbeddedBitChat {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(senderPeerID),
|
||||
senderID = PeerId.toBytes(senderPeerID),
|
||||
recipientID = null, // No recipient for geohash DMs
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
@@ -153,7 +156,7 @@ object NostrEmbeddedBitChat {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(senderPeerID),
|
||||
senderID = PeerId.toBytes(senderPeerID),
|
||||
recipientID = null, // No recipient for geohash DMs
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
@@ -174,16 +177,16 @@ object NostrEmbeddedBitChat {
|
||||
*/
|
||||
private fun normalizeRecipientPeerID(recipientPeerID: String): String {
|
||||
try {
|
||||
val maybeData = hexStringToByteArray(recipientPeerID)
|
||||
val maybeData = Hex.decode(recipientPeerID) ?: return recipientPeerID
|
||||
return when (maybeData.size) {
|
||||
32 -> {
|
||||
// Treat as Noise static public key; derive peerID from fingerprint
|
||||
// For now, return first 8 bytes as hex (simplified)
|
||||
maybeData.take(8).joinToString("") { "%02x".format(it) }
|
||||
maybeData.copyOfRange(0, 8).toHexString()
|
||||
}
|
||||
8 -> {
|
||||
// Already an 8-byte peer ID
|
||||
recipientPeerID
|
||||
PeerId.fromBytes(maybeData)
|
||||
}
|
||||
else -> {
|
||||
// Fallback: return as-is (expecting 16 hex chars)
|
||||
@@ -207,28 +210,4 @@ object NostrEmbeddedBitChat {
|
||||
.replace("=", "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex string to byte array
|
||||
*/
|
||||
private fun hexStringToByteArray(hexString: String): ByteArray {
|
||||
if (hexString.length % 2 != 0) {
|
||||
return ByteArray(8) // Return 8-byte array filled with zeros
|
||||
}
|
||||
|
||||
val result = ByteArray(8) { 0 } // Exactly 8 bytes like iOS
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ package com.bitchat.android.nostr
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.security.MessageDigest
|
||||
import com.bitchat.android.util.Hashing
|
||||
import com.bitchat.android.util.toHexString
|
||||
|
||||
/**
|
||||
* Nostr Event structure following NIP-01
|
||||
@@ -134,13 +135,11 @@ data class NostrEvent(
|
||||
val gson = GsonBuilder().disableHtmlEscaping().create()
|
||||
val jsonString = gson.toJson(serialized)
|
||||
|
||||
// SHA256 hash of the JSON string
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val jsonBytes = jsonString.toByteArray(Charsets.UTF_8)
|
||||
val hash = digest.digest(jsonBytes)
|
||||
val hash = Hashing.sha256(jsonBytes)
|
||||
|
||||
// Convert to hex
|
||||
val hexId = hash.joinToString("") { "%02x".format(it) }
|
||||
val hexId = hash.toHexString()
|
||||
|
||||
return Pair(hexId, hash)
|
||||
}
|
||||
@@ -217,15 +216,3 @@ object NostrKind {
|
||||
const val EPHEMERAL_EVENT = 20000 // For geohash channels
|
||||
const val GEOHASH_PRESENCE = 20001 // For geohash presence heartbeat
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension functions for hex encoding/decoding
|
||||
*/
|
||||
fun String.hexToByteArray(): ByteArray {
|
||||
check(length % 2 == 0) { "Must have an even length" }
|
||||
return chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
fun ByteArray.toHexString(): String = joinToString("") { "%02x".format(it) }
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.bitchat.android.nostr
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.identity.SecureIdentityStateManager
|
||||
import java.security.MessageDigest
|
||||
import com.bitchat.android.util.Hashing
|
||||
import com.bitchat.android.util.hexToByteArray
|
||||
import com.bitchat.android.util.toHexString
|
||||
import java.security.SecureRandom
|
||||
|
||||
/**
|
||||
@@ -25,7 +27,7 @@ data class NostrIdentity(
|
||||
*/
|
||||
fun generate(): NostrIdentity {
|
||||
val (privateKeyHex, publicKeyHex) = NostrCrypto.generateKeyPair()
|
||||
val npub = Bech32.encode("npub", publicKeyHex.hexToByteArrayLocal())
|
||||
val npub = Bech32.encode("npub", publicKeyHex.hexToByteArray())
|
||||
|
||||
Log.d(TAG, "Generated new Nostr identity: npub=$npub")
|
||||
|
||||
@@ -46,7 +48,7 @@ data class NostrIdentity(
|
||||
}
|
||||
|
||||
val publicKeyHex = NostrCrypto.derivePublicKey(privateKeyHex)
|
||||
val npub = Bech32.encode("npub", publicKeyHex.hexToByteArrayLocal())
|
||||
val npub = Bech32.encode("npub", publicKeyHex.hexToByteArray())
|
||||
|
||||
return NostrIdentity(
|
||||
privateKeyHex = privateKeyHex,
|
||||
@@ -61,10 +63,8 @@ data class NostrIdentity(
|
||||
*/
|
||||
fun fromSeed(seed: String): NostrIdentity {
|
||||
// Hash the seed to create a private key
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val seedBytes = seed.toByteArray(Charsets.UTF_8)
|
||||
val privateKeyBytes = digest.digest(seedBytes)
|
||||
val privateKeyHex = privateKeyBytes.joinToString("") { "%02x".format(it) }
|
||||
val privateKeyHex = Hashing.sha256Hex(seedBytes)
|
||||
|
||||
return fromPrivateKey(privateKeyHex)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ object NostrIdentityBridge {
|
||||
// Try a few iterations to ensure a valid key can be formed (exactly like iOS)
|
||||
for (i in 0 until 10) {
|
||||
val candidateKey = candidateKey(seed, geohashBytes, i.toUInt())
|
||||
val candidateKeyHex = candidateKey.toHexStringLocal()
|
||||
val candidateKeyHex = candidateKey.toHexString()
|
||||
|
||||
if (NostrCrypto.isValidPrivateKey(candidateKeyHex)) {
|
||||
val identity = NostrIdentity.fromPrivateKey(candidateKeyHex)
|
||||
@@ -164,10 +164,9 @@ object NostrIdentityBridge {
|
||||
|
||||
// As a final fallback, hash the seed+msg and try again (exactly like iOS)
|
||||
val combined = seed + geohashBytes
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val fallbackKey = digest.digest(combined)
|
||||
val fallbackKey = Hashing.sha256(combined)
|
||||
|
||||
val fallbackIdentity = NostrIdentity.fromPrivateKey(fallbackKey.toHexStringLocal())
|
||||
val fallbackIdentity = NostrIdentity.fromPrivateKey(fallbackKey.toHexString())
|
||||
|
||||
// Cache the fallback result too
|
||||
geohashIdentityCache[forGeohash] = fallbackIdentity
|
||||
@@ -280,15 +279,6 @@ object NostrIdentityBridge {
|
||||
}
|
||||
}
|
||||
|
||||
// Extension functions for data conversion
|
||||
private fun String.hexToByteArrayLocal(): ByteArray {
|
||||
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
}
|
||||
|
||||
private fun ByteArray.toHexStringLocal(): String {
|
||||
return joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
private fun UInt.toLittleEndianBytes(): ByteArray {
|
||||
val bytes = ByteArray(4)
|
||||
bytes[0] = (this and 0xFFu).toByte()
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.bitchat.android.nostr
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.security.MessageDigest
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.bitchat.android.nostr
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.util.hexToByteArray
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.model.ReadReceipt
|
||||
import com.bitchat.android.model.NoisePayloadType
|
||||
import com.bitchat.android.util.PeerId
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
@@ -79,7 +81,7 @@ class NostrTransport(
|
||||
Log.e(TAG, "NostrTransport: recipient key not npub (hrp=$hrp)")
|
||||
return@launch
|
||||
}
|
||||
data.joinToString("") { "%02x".format(it) }
|
||||
data.toHexString()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "NostrTransport: failed to decode npub -> hex: $e")
|
||||
return@launch
|
||||
@@ -174,7 +176,7 @@ class NostrTransport(
|
||||
scheduleNextReadAck()
|
||||
return@launch
|
||||
}
|
||||
data.joinToString("") { "%02x".format(it) }
|
||||
data.toHexString()
|
||||
} catch (e: Exception) {
|
||||
scheduleNextReadAck()
|
||||
return@launch
|
||||
@@ -248,7 +250,7 @@ class NostrTransport(
|
||||
val recipientHex = try {
|
||||
val (hrp, data) = Bech32.decode(recipientNostrPubkey)
|
||||
if (hrp != "npub") return@launch
|
||||
data.joinToString("") { "%02x".format(it) }
|
||||
data.toHexString()
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
}
|
||||
@@ -306,7 +308,7 @@ class NostrTransport(
|
||||
val recipientHex = try {
|
||||
val (hrp, data) = Bech32.decode(recipientNostrPubkey)
|
||||
if (hrp != "npub") return@launch
|
||||
data.joinToString("") { "%02x".format(it) }
|
||||
data.toHexString()
|
||||
} catch (e: Exception) {
|
||||
return@launch
|
||||
}
|
||||
@@ -486,7 +488,7 @@ class NostrTransport(
|
||||
com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNostrPubkeyForPeerID(peerID)?.let { return it }
|
||||
|
||||
// 2) Legacy path: resolve by noise public key association
|
||||
val noiseKey = hexStringToByteArray(peerID)
|
||||
val noiseKey = PeerId.toBytes(peerID)
|
||||
val favoriteStatus = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(noiseKey)
|
||||
if (favoriteStatus?.peerNostrPublicKey != null) return favoriteStatus.peerNostrPublicKey
|
||||
|
||||
@@ -503,14 +505,6 @@ class NostrTransport(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert full hex string to byte array
|
||||
*/
|
||||
private fun hexStringToByteArray(hexString: String): ByteArray {
|
||||
val clean = if (hexString.length % 2 == 0) hexString else "0$hexString"
|
||||
return clean.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
transportScope.cancel()
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ package com.bitchat.android.nostr
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import com.bitchat.android.util.Hashing
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -284,20 +284,6 @@ object RelayDirectory {
|
||||
} catch (_: Exception) { "error" }
|
||||
|
||||
private fun streamSha256Hex(input: InputStream): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val buf = ByteArray(8192)
|
||||
var read: Int
|
||||
while (true) {
|
||||
read = input.read(buf)
|
||||
if (read <= 0) break
|
||||
digest.update(buf, 0, read)
|
||||
}
|
||||
val bytes = digest.digest()
|
||||
return bytes.joinToString("") { b ->
|
||||
val v = b.toInt() and 0xff
|
||||
val s = Integer.toHexString(v)
|
||||
if (s.length == 1) "0$s" else s
|
||||
}
|
||||
return Hashing.sha256Hex(input)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bitchat.android.protocol
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.bitchat.android.util.PeerId
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
@@ -71,7 +72,7 @@ data class BitchatPacket(
|
||||
) : this(
|
||||
version = 1u,
|
||||
type = type,
|
||||
senderID = hexStringToByteArray(senderID),
|
||||
senderID = PeerId.toBytes(senderID),
|
||||
recipientID = null,
|
||||
timestamp = (System.currentTimeMillis()).toULong(),
|
||||
payload = payload,
|
||||
@@ -108,27 +109,6 @@ data class BitchatPacket(
|
||||
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 {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.bitchat.android.protocol
|
||||
|
||||
enum class TlvLengthSize(val byteCount: Int, val maxValue: Long) {
|
||||
ONE_BYTE(1, 0xFFL),
|
||||
TWO_BYTES(2, 0xFFFFL),
|
||||
FOUR_BYTES(4, 0xFFFF_FFFFL)
|
||||
}
|
||||
|
||||
enum class UnknownTlvPolicy {
|
||||
SKIP,
|
||||
FAIL
|
||||
}
|
||||
|
||||
data class TlvField(
|
||||
val type: Int,
|
||||
val value: ByteArray
|
||||
)
|
||||
|
||||
class TlvWriter {
|
||||
private val output = ArrayList<Byte>()
|
||||
|
||||
fun put(type: Int, value: ByteArray, lengthSize: TlvLengthSize): TlvWriter {
|
||||
require(type in 0..0xFF) { "TLV type must fit in one byte" }
|
||||
require(value.size.toLong() <= lengthSize.maxValue) {
|
||||
"TLV value length ${value.size} exceeds ${lengthSize.maxValue}"
|
||||
}
|
||||
|
||||
output.add(type.toByte())
|
||||
writeLength(value.size.toLong(), lengthSize)
|
||||
output.addAll(value.toList())
|
||||
return this
|
||||
}
|
||||
|
||||
fun toByteArray(): ByteArray {
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
private fun writeLength(length: Long, lengthSize: TlvLengthSize) {
|
||||
when (lengthSize) {
|
||||
TlvLengthSize.ONE_BYTE -> output.add((length and 0xFF).toByte())
|
||||
TlvLengthSize.TWO_BYTES -> {
|
||||
output.add(((length ushr 8) and 0xFF).toByte())
|
||||
output.add((length and 0xFF).toByte())
|
||||
}
|
||||
TlvLengthSize.FOUR_BYTES -> {
|
||||
output.add(((length ushr 24) and 0xFF).toByte())
|
||||
output.add(((length ushr 16) and 0xFF).toByte())
|
||||
output.add(((length ushr 8) and 0xFF).toByte())
|
||||
output.add((length and 0xFF).toByte())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object TlvReader {
|
||||
fun decode(
|
||||
data: ByteArray,
|
||||
defaultLengthSize: TlvLengthSize,
|
||||
unknownPolicy: UnknownTlvPolicy = UnknownTlvPolicy.SKIP,
|
||||
knownTypes: Set<Int>? = null,
|
||||
lengthSizeForType: (Int) -> TlvLengthSize = { defaultLengthSize }
|
||||
): List<TlvField>? {
|
||||
val fields = ArrayList<TlvField>()
|
||||
var offset = 0
|
||||
|
||||
while (offset < data.size) {
|
||||
if (data.size - offset < 1) return null
|
||||
val type = data[offset].toInt() and 0xFF
|
||||
offset += 1
|
||||
|
||||
val lengthSize = lengthSizeForType(type)
|
||||
if (data.size - offset < lengthSize.byteCount) return null
|
||||
|
||||
val length = readLength(data, offset, lengthSize) ?: return null
|
||||
offset += lengthSize.byteCount
|
||||
|
||||
if (length > Int.MAX_VALUE || data.size - offset < length.toInt()) return null
|
||||
val valueLength = length.toInt()
|
||||
val value = data.copyOfRange(offset, offset + valueLength)
|
||||
offset += valueLength
|
||||
|
||||
if (knownTypes != null && type !in knownTypes) {
|
||||
when (unknownPolicy) {
|
||||
UnknownTlvPolicy.SKIP -> continue
|
||||
UnknownTlvPolicy.FAIL -> return null
|
||||
}
|
||||
}
|
||||
|
||||
fields.add(TlvField(type, value))
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
private fun readLength(data: ByteArray, offset: Int, lengthSize: TlvLengthSize): Long? {
|
||||
return when (lengthSize) {
|
||||
TlvLengthSize.ONE_BYTE -> data[offset].toLong() and 0xFFL
|
||||
TlvLengthSize.TWO_BYTES -> {
|
||||
((data[offset].toLong() and 0xFFL) shl 8) or
|
||||
(data[offset + 1].toLong() and 0xFFL)
|
||||
}
|
||||
TlvLengthSize.FOUR_BYTES -> {
|
||||
((data[offset].toLong() and 0xFFL) shl 24) or
|
||||
((data[offset + 1].toLong() and 0xFFL) shl 16) or
|
||||
((data[offset + 2].toLong() and 0xFFL) shl 8) or
|
||||
(data[offset + 3].toLong() and 0xFFL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.bitchat.android.services
|
||||
|
||||
import com.bitchat.android.ui.ChatState
|
||||
import com.bitchat.android.util.hexToByteArray
|
||||
import com.bitchat.android.util.toHexString
|
||||
|
||||
object ConversationAliasResolver {
|
||||
|
||||
@@ -19,7 +21,7 @@ object ConversationAliasResolver {
|
||||
if (pubHex != null) {
|
||||
val noiseKey = findNoiseKeyForNostr(pubHex)
|
||||
if (noiseKey != null) {
|
||||
val noiseHex = noiseKey.joinToString("") { b -> "%02x".format(b) }
|
||||
val noiseHex = noiseKey.toHexString()
|
||||
// Prefer a connected mesh peer that matches this noise key
|
||||
val meshPeer = connectedPeers.firstOrNull { pid ->
|
||||
meshNoiseKeyForPeer(pid)?.contentEquals(noiseKey) == true
|
||||
@@ -29,7 +31,7 @@ object ConversationAliasResolver {
|
||||
}
|
||||
} else if (peer.length == 64 && peer.matches(Regex("^[0-9a-fA-F]+$"))) {
|
||||
// Peer is full noise key hex: upgrade to active mesh peer if available
|
||||
val noiseKey = peer.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
val noiseKey = peer.hexToByteArray()
|
||||
val meshPeer = connectedPeers.firstOrNull { pid ->
|
||||
meshNoiseKeyForPeer(pid)?.contentEquals(noiseKey) == true
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.util.Log
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
import com.bitchat.android.model.ReadReceipt
|
||||
import com.bitchat.android.nostr.NostrTransport
|
||||
import com.bitchat.android.util.hexToByteArrayOrNull
|
||||
import com.bitchat.android.util.toHexString
|
||||
|
||||
/**
|
||||
* Routes messages between BLE mesh and Nostr transports, matching iOS behavior.
|
||||
@@ -164,7 +166,7 @@ class MessageRouter private constructor(
|
||||
return try {
|
||||
// Full Noise key hex
|
||||
if (peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
||||
val noiseKey = hexToBytes(peerID)
|
||||
val noiseKey = peerID.hexToByteArrayOrNull() ?: return false
|
||||
val fav = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(noiseKey)
|
||||
fav?.isMutual == true && fav.peerNostrPublicKey != null
|
||||
} else if (peerID.length == 16 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
||||
@@ -177,16 +179,11 @@ class MessageRouter private constructor(
|
||||
} catch (_: Exception) { false }
|
||||
}
|
||||
|
||||
private fun hexToBytes(hex: String): ByteArray {
|
||||
val clean = if (hex.length % 2 == 0) hex else "0$hex"
|
||||
return clean.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
}
|
||||
|
||||
private fun resolveMeshPeerForNoiseHex(noiseHex: String): String? {
|
||||
return try {
|
||||
mesh.getPeerNicknames().keys.firstOrNull { pid ->
|
||||
val info = mesh.getPeerInfo(pid)
|
||||
val keyHex = info?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||
val keyHex = info?.noisePublicKey?.toHexString()
|
||||
keyHex != null && keyHex.equals(noiseHex, ignoreCase = true)
|
||||
}
|
||||
} catch (_: Exception) { null }
|
||||
@@ -197,7 +194,7 @@ class MessageRouter private constructor(
|
||||
peers.forEach { pid ->
|
||||
flushOutboxFor(pid)
|
||||
val noiseHex = try {
|
||||
mesh.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||
mesh.getPeerInfo(pid)?.noisePublicKey?.toHexString()
|
||||
} catch (_: Exception) { null }
|
||||
noiseHex?.let { flushOutboxFor(it) }
|
||||
}
|
||||
@@ -207,7 +204,7 @@ class MessageRouter private constructor(
|
||||
fun onSessionEstablished(peerID: String) {
|
||||
flushOutboxFor(peerID)
|
||||
val noiseHex = try {
|
||||
mesh.getPeerInfo(peerID)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||
mesh.getPeerInfo(peerID)?.noisePublicKey?.toHexString()
|
||||
} catch (_: Exception) { null }
|
||||
noiseHex?.let { flushOutboxFor(it) }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package com.bitchat.android.services.meshgraph
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvReader
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.protocol.UnknownTlvPolicy
|
||||
import com.bitchat.android.util.PeerId
|
||||
|
||||
/**
|
||||
* Gossip TLV helpers for embedding direct neighbor peer IDs in ANNOUNCE payloads.
|
||||
@@ -15,12 +20,14 @@ object GossipTLV {
|
||||
*/
|
||||
fun encodeNeighbors(peerIDs: List<String>): ByteArray {
|
||||
val unique = peerIDs.distinct().take(10)
|
||||
val valueBytes = unique.flatMap { id -> hexStringPeerIdTo8Bytes(id).toList() }.toByteArray()
|
||||
val valueBytes = unique.flatMap { id -> PeerId.toBytes(id).toList() }.toByteArray()
|
||||
if (valueBytes.size > 255) {
|
||||
// Safety check, though 10*8 = 80 bytes, so well under 255
|
||||
Log.w("GossipTLV", "Neighbors value exceeds 255, truncating")
|
||||
}
|
||||
return byteArrayOf(DIRECT_NEIGHBORS_TYPE.toByte(), valueBytes.size.toByte()) + valueBytes
|
||||
return TlvWriter()
|
||||
.put(DIRECT_NEIGHBORS_TYPE.toInt(), valueBytes, TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,22 +36,21 @@ object GossipTLV {
|
||||
*/
|
||||
fun decodeNeighborsFromAnnouncementPayload(payload: ByteArray): List<String>? {
|
||||
val result = mutableListOf<String>()
|
||||
var offset = 0
|
||||
while (offset + 2 <= payload.size) {
|
||||
val type = payload[offset].toUByte()
|
||||
val len = payload[offset + 1].toUByte().toInt()
|
||||
offset += 2
|
||||
if (offset + len > payload.size) break
|
||||
val value = payload.sliceArray(offset until offset + len)
|
||||
offset += len
|
||||
val fields = TlvReader.decode(
|
||||
data = payload,
|
||||
defaultLengthSize = TlvLengthSize.ONE_BYTE,
|
||||
unknownPolicy = UnknownTlvPolicy.SKIP,
|
||||
knownTypes = setOf(DIRECT_NEIGHBORS_TYPE.toInt())
|
||||
) ?: return null
|
||||
|
||||
if (type == DIRECT_NEIGHBORS_TYPE) {
|
||||
for (field in fields) {
|
||||
if (field.type == DIRECT_NEIGHBORS_TYPE.toInt()) {
|
||||
// Value is N*8 bytes of peer IDs
|
||||
var pos = 0
|
||||
while (pos + 8 <= value.size) {
|
||||
val idBytes = value.sliceArray(pos until pos + 8)
|
||||
result.add(bytesToPeerIdHex(idBytes))
|
||||
pos += 8
|
||||
while (pos + PeerId.BYTE_LENGTH <= field.value.size) {
|
||||
val idBytes = field.value.sliceArray(pos until pos + PeerId.BYTE_LENGTH)
|
||||
result.add(PeerId.fromBytes(idBytes))
|
||||
pos += PeerId.BYTE_LENGTH
|
||||
}
|
||||
return result // present (possibly empty)
|
||||
}
|
||||
@@ -52,26 +58,4 @@ object GossipTLV {
|
||||
// Not present
|
||||
return null
|
||||
}
|
||||
|
||||
private fun hexStringPeerIdTo8Bytes(hexString: String): ByteArray {
|
||||
val clean = hexString.lowercase().take(16)
|
||||
val result = ByteArray(8) { 0 }
|
||||
var idx = 0
|
||||
var out = 0
|
||||
while (idx + 1 < clean.length && out < 8) {
|
||||
val byteStr = clean.substring(idx, idx + 2)
|
||||
val b = byteStr.toIntOrNull(16)?.toByte() ?: 0
|
||||
result[out++] = b
|
||||
idx += 2
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun bytesToPeerIdHex(bytes: ByteArray): String {
|
||||
val sb = StringBuilder()
|
||||
for (b in bytes.take(8)) {
|
||||
sb.append(String.format("%02x", b))
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import com.bitchat.android.model.RequestSyncPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import com.bitchat.android.util.PeerId
|
||||
import com.bitchat.android.util.hexToByteArray
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@@ -101,7 +104,7 @@ class GossipSyncManager(
|
||||
if (!isBroadcastMessage && !isAnnouncement) return
|
||||
|
||||
val idBytes = PacketIdUtil.computeIdBytes(packet)
|
||||
val id = idBytes.joinToString("") { b -> "%02x".format(b) }
|
||||
val id = idBytes.toHexString()
|
||||
|
||||
if (isBroadcastMessage) {
|
||||
synchronized(messages) {
|
||||
@@ -122,7 +125,7 @@ class GossipSyncManager(
|
||||
return
|
||||
}
|
||||
// senderID is fixed-size 8 bytes; map to hex string for key
|
||||
val sender = packet.senderID.joinToString("") { b -> "%02x".format(b) }
|
||||
val sender = packet.senderID.toHexString()
|
||||
latestAnnouncementByPeer[sender] = id to packet
|
||||
// Enforce capacity (remove oldest when exceeded)
|
||||
val cap = configProvider.seenCapacity().coerceAtLeast(1)
|
||||
@@ -138,7 +141,7 @@ class GossipSyncManager(
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.REQUEST_SYNC.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS // neighbors only
|
||||
@@ -153,8 +156,8 @@ class GossipSyncManager(
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.REQUEST_SYNC.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
senderID = PeerId.toBytes(myPeerID),
|
||||
recipientID = PeerId.toBytes(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS // neighbor only
|
||||
@@ -177,7 +180,7 @@ class GossipSyncManager(
|
||||
// 1) Announcements: send latest per peerID if remote doesn't have them
|
||||
for ((_, pair) in latestAnnouncementByPeer.entries) {
|
||||
val (id, pkt) = pair
|
||||
val idBytes = hexToBytes(id)
|
||||
val idBytes = id.hexToByteArray()
|
||||
if (!mightContain(idBytes)) {
|
||||
// Send original packet unchanged to requester only (keep local TTL)
|
||||
val toSend = pkt.copy(ttl = com.bitchat.android.util.AppConstants.SYNC_TTL_HOPS)
|
||||
@@ -198,31 +201,6 @@ class GossipSyncManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun hexStringToByteArray(hexString: String): ByteArray {
|
||||
val result = ByteArray(8) { 0 }
|
||||
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
|
||||
}
|
||||
|
||||
private fun hexToBytes(hex: String): ByteArray {
|
||||
val clean = if (hex.length % 2 == 0) hex else "0$hex"
|
||||
val out = ByteArray(clean.length / 2)
|
||||
var i = 0
|
||||
while (i < clean.length) {
|
||||
out[i/2] = clean.substring(i, i+2).toInt(16).toByte()
|
||||
i += 2
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private fun buildGcsPayload(): ByteArray {
|
||||
// Collect candidates: latest announcement per peer + recent broadcast messages
|
||||
val list = ArrayList<BitchatPacket>()
|
||||
@@ -276,7 +254,7 @@ class GossipSyncManager(
|
||||
val toRemove = mutableListOf<String>()
|
||||
synchronized(messages) {
|
||||
for ((id, message) in messages) {
|
||||
val sender = message.senderID.joinToString("") { b -> "%02x".format(b) }
|
||||
val sender = message.senderID.toHexString()
|
||||
if (sender == peerID) toRemove.add(id)
|
||||
}
|
||||
}
|
||||
@@ -300,7 +278,7 @@ class GossipSyncManager(
|
||||
val idsToRemove = mutableListOf<String>()
|
||||
synchronized(messages) {
|
||||
for ((id, message) in messages) {
|
||||
val sender = message.senderID.joinToString("") { b -> "%02x".format(b) }
|
||||
val sender = message.senderID.toHexString()
|
||||
if (sender == key) {
|
||||
idsToRemove.add(id)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.bitchat.android.sync
|
||||
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import java.security.MessageDigest
|
||||
import com.bitchat.android.util.Hashing
|
||||
import com.bitchat.android.util.toHexString
|
||||
|
||||
/**
|
||||
* Deterministic packet ID helper for sync purposes.
|
||||
@@ -11,21 +12,22 @@ import java.security.MessageDigest
|
||||
*/
|
||||
object PacketIdUtil {
|
||||
fun computeIdBytes(packet: BitchatPacket): ByteArray {
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
md.update(packet.type.toByte())
|
||||
md.update(packet.senderID)
|
||||
// Timestamp as 8 bytes big-endian
|
||||
val data = ByteArray(1 + packet.senderID.size + 8 + packet.payload.size)
|
||||
var offset = 0
|
||||
data[offset++] = packet.type.toByte()
|
||||
packet.senderID.copyInto(data, destinationOffset = offset)
|
||||
offset += packet.senderID.size
|
||||
|
||||
val ts = packet.timestamp.toLong()
|
||||
for (i in 7 downTo 0) {
|
||||
md.update(((ts ushr (i * 8)) and 0xFF).toByte())
|
||||
data[offset++] = ((ts ushr (i * 8)) and 0xFF).toByte()
|
||||
}
|
||||
md.update(packet.payload)
|
||||
val digest = md.digest()
|
||||
return digest.copyOf(16) // 128-bit ID
|
||||
packet.payload.copyInto(data, destinationOffset = offset)
|
||||
|
||||
return Hashing.sha256(data).copyOf(16) // 128-bit ID
|
||||
}
|
||||
|
||||
fun computeIdHex(packet: BitchatPacket): String {
|
||||
return computeIdBytes(packet).joinToString("") { b -> "%02x".format(b) }
|
||||
return computeIdBytes(packet).toHexString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.bitchat.android.ui
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@@ -30,7 +30,6 @@ import com.bitchat.android.noise.NoiseSession
|
||||
import com.bitchat.android.nostr.GeohashAliasRegistry
|
||||
import com.bitchat.android.util.dataFromHexString
|
||||
import com.bitchat.android.util.hexEncodedString
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Refactored ChatViewModel - Main coordinator for bitchat functionality
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.bitchat.android.mesh.BluetoothMeshService
|
||||
import com.bitchat.android.model.BitchatFilePacket
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.BitchatMessageType
|
||||
import com.bitchat.android.util.Hashing
|
||||
import java.util.Date
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Handles media file sending operations (voice notes, images, generic files)
|
||||
@@ -181,8 +181,8 @@ class MediaSendingManager(
|
||||
}
|
||||
Log.d(TAG, "🔒 Encoded private packet: ${payload.size} bytes")
|
||||
|
||||
val transferId = sha256Hex(payload)
|
||||
val contentHash = sha256Hex(filePacket.content)
|
||||
val transferId = Hashing.sha256Hex(payload)
|
||||
val contentHash = Hashing.sha256Hex(filePacket.content)
|
||||
|
||||
Log.d(TAG, "📤 FILE_TRANSFER send (private): name='${filePacket.fileName}', size=${filePacket.fileSize}, mime='${filePacket.mimeType}', sha256=$contentHash, to=${toPeerID.take(8)} transferId=${transferId.take(16)}…")
|
||||
|
||||
@@ -232,8 +232,8 @@ class MediaSendingManager(
|
||||
}
|
||||
Log.d(TAG, "🔓 Encoded broadcast packet: ${payload.size} bytes")
|
||||
|
||||
val transferId = sha256Hex(payload)
|
||||
val contentHash = sha256Hex(filePacket.content)
|
||||
val transferId = Hashing.sha256Hex(payload)
|
||||
val contentHash = Hashing.sha256Hex(filePacket.content)
|
||||
|
||||
Log.d(TAG, "📤 FILE_TRANSFER send (broadcast): name='${filePacket.fileName}', size=${filePacket.fileSize}, mime='${filePacket.mimeType}', sha256=$contentHash, transferId=${transferId.take(16)}…")
|
||||
|
||||
@@ -339,11 +339,4 @@ class MediaSendingManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun sha256Hex(bytes: ByteArray): String = try {
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
md.update(bytes)
|
||||
md.digest().joinToString("") { "%02x".format(it) }
|
||||
} catch (_: Exception) {
|
||||
bytes.size.toString(16)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.bitchat.android.ui.NotificationTextUtils
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.DeliveryStatus
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
@@ -130,7 +131,7 @@ class MeshDelegateHandler(
|
||||
favs.firstNotNullOfOrNull { rel ->
|
||||
rel.peerNostrPublicKey?.let { s ->
|
||||
runCatching { com.bitchat.android.nostr.Bech32.decode(s) }.getOrNull()?.let { dec ->
|
||||
if (dec.first == "npub") dec.second.joinToString("") { b -> "%02x".format(b) } else null
|
||||
if (dec.first == "npub") dec.second.toHexString() else null
|
||||
}
|
||||
}
|
||||
}?.takeIf { it.startsWith(prefix, ignoreCase = true) }
|
||||
@@ -154,7 +155,7 @@ class MeshDelegateHandler(
|
||||
} catch (_: Exception) { null }
|
||||
|
||||
if (favoriteRel?.isMutual == true) {
|
||||
val noiseHex = favoriteRel.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
||||
val noiseHex = favoriteRel.peerNoisePublicKey.toHexString()
|
||||
if (noiseHex != currentPeer) {
|
||||
com.bitchat.android.services.ConversationAliasResolver.unifyChatsIntoPeer(
|
||||
state = state,
|
||||
@@ -175,14 +176,14 @@ class MeshDelegateHandler(
|
||||
try {
|
||||
val info = getPeerInfo(pid)
|
||||
val noiseKey = info?.noisePublicKey ?: return@forEach
|
||||
val noiseHex = noiseKey.joinToString("") { b -> "%02x".format(b) }
|
||||
val noiseHex = noiseKey.toHexString()
|
||||
|
||||
// Derive temp nostr key from favorites npub
|
||||
val npub = com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNostrPubkey(noiseKey)
|
||||
val tempNostrKey: String? = try {
|
||||
if (npub != null) {
|
||||
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npub)
|
||||
if (hrp == "npub") "nostr_${data.joinToString("") { b -> "%02x".format(b) }.take(16)}" else null
|
||||
if (hrp == "npub") "nostr_${data.toHexString().take(16)}" else null
|
||||
} else null
|
||||
} catch (_: Exception) { null }
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.bitchat.android.ui
|
||||
|
||||
import com.bitchat.android.R
|
||||
import android.util.Log
|
||||
import com.bitchat.android.util.toHexString
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -333,7 +334,7 @@ fun PeopleSection(
|
||||
// Build mapping of connected peerID -> noise key hex to unify with offline favorites
|
||||
val noiseHexByPeerID: Map<String, String> = connectedPeers.associateWith { pid ->
|
||||
try {
|
||||
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.toHexString()
|
||||
} catch (_: Exception) { null }
|
||||
}.filterValues { it != null }.mapValues { it.value!! }
|
||||
|
||||
@@ -367,7 +368,7 @@ fun PeopleSection(
|
||||
// Offline favorites (exclude ones mapped to connected)
|
||||
val offlineFavorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites()
|
||||
offlineFavorites.forEach { fav ->
|
||||
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
||||
val favPeerID = fav.peerNoisePublicKey.toHexString()
|
||||
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
||||
if (!isMappedToConnected) {
|
||||
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
|
||||
@@ -435,7 +436,7 @@ fun PeopleSection(
|
||||
|
||||
// Append offline favorites we actively favorite (and not currently connected)
|
||||
offlineFavorites.forEach { fav ->
|
||||
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
||||
val favPeerID = fav.peerNoisePublicKey.toHexString()
|
||||
// If any connected peer maps to this noise key, skip showing the offline entry
|
||||
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
|
||||
if (isMappedToConnected) return@forEach
|
||||
@@ -446,7 +447,7 @@ fun PeopleSection(
|
||||
if (npubOrHex != null) {
|
||||
val hex = if (npubOrHex.startsWith("npub")) {
|
||||
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npubOrHex)
|
||||
if (hrp == "npub") data.joinToString("") { "%02x".format(it) } else null
|
||||
if (hrp == "npub") data.toHexString() else null
|
||||
} else {
|
||||
npubOrHex.lowercase()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.bitchat.android.ui
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.DeliveryStatus
|
||||
import com.bitchat.android.mesh.PeerFingerprintManager
|
||||
import java.security.MessageDigest
|
||||
import com.bitchat.android.util.Hashing
|
||||
import com.bitchat.android.util.hexToByteArray
|
||||
import com.bitchat.android.util.toHexString
|
||||
|
||||
import com.bitchat.android.mesh.BluetoothMeshService
|
||||
import java.util.*
|
||||
@@ -131,10 +133,8 @@ class PrivateChatManager(
|
||||
// compute a synthetic fingerprint (SHA-256 of public key) to allow unfollowing offline peers
|
||||
if (fingerprint == null && peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
||||
try {
|
||||
val pubBytes = peerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
val digest = java.security.MessageDigest.getInstance("SHA-256")
|
||||
val fpBytes = digest.digest(pubBytes)
|
||||
fingerprint = fpBytes.joinToString("") { "%02x".format(it) }
|
||||
val pubBytes = peerID.hexToByteArray()
|
||||
fingerprint = Hashing.sha256Hex(pubBytes)
|
||||
Log.d(TAG, "Computed fingerprint from noise key hex for offline toggle: $fingerprint")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to compute fingerprint from noise key hex: ${e.message}")
|
||||
@@ -432,13 +432,13 @@ class PrivateChatManager(
|
||||
|
||||
// If we know the sender's Nostr pubkey for this peer via favorites, derive temp key
|
||||
try {
|
||||
val noiseKeyBytes = targetPeerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
val noiseKeyBytes = targetPeerID.hexToByteArray()
|
||||
val npub = com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNostrPubkey(noiseKeyBytes)
|
||||
if (npub != null) {
|
||||
// Normalize to hex to match how we formed temp keys (nostr_<pub16>)
|
||||
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npub)
|
||||
if (hrp == "npub") {
|
||||
val pubHex = data.joinToString("") { "%02x".format(it) }
|
||||
val pubHex = data.toHexString()
|
||||
tryMergeKeys.add("nostr_${pubHex.take(16)}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.noise.NoiseSession
|
||||
import com.bitchat.android.nostr.GeohashAliasRegistry
|
||||
import com.bitchat.android.services.VerificationService
|
||||
import com.bitchat.android.util.Hashing
|
||||
import com.bitchat.android.util.dataFromHexString
|
||||
import com.bitchat.android.util.hexEncodedString
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -16,7 +17,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.security.MessageDigest
|
||||
import java.util.Date
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@@ -333,8 +333,7 @@ class VerificationHandler(
|
||||
}
|
||||
|
||||
fun fingerprintFromNoiseBytes(bytes: ByteArray): String {
|
||||
val hash = MessageDigest.getInstance("SHA-256").digest(bytes)
|
||||
return hash.hexEncodedString()
|
||||
return Hashing.sha256Hex(bytes)
|
||||
}
|
||||
|
||||
private data class PendingVerification(
|
||||
|
||||
@@ -12,24 +12,11 @@ import java.util.*
|
||||
// MARK: - Hex Encoding/Decoding Extensions
|
||||
|
||||
fun ByteArray.hexEncodedString(): String {
|
||||
if (this.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
return this.joinToString("") { "%02x".format(it) }
|
||||
return Hex.encode(this)
|
||||
}
|
||||
|
||||
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
|
||||
return Hex.decode(this)
|
||||
}
|
||||
|
||||
// MARK: - Binary Encoding Utilities
|
||||
@@ -201,7 +188,7 @@ class BinaryDataReader(private val data: ByteArray) {
|
||||
offset += 16
|
||||
|
||||
// Convert 16 bytes to UUID string format
|
||||
val uuid = uuidData.joinToString("") { "%02x".format(it) }
|
||||
val uuid = uuidData.toHexString()
|
||||
|
||||
// Insert hyphens at proper positions: 8-4-4-4-12
|
||||
val result = StringBuilder()
|
||||
@@ -340,7 +327,7 @@ fun ByteArray.readUUID(at: IntArray): String? {
|
||||
at[0] += 16
|
||||
|
||||
// Convert 16 bytes to UUID string format
|
||||
val uuid = uuidData.joinToString("") { "%02x".format(it) }
|
||||
val uuid = uuidData.toHexString()
|
||||
|
||||
// Insert hyphens at proper positions: 8-4-4-4-12
|
||||
val result = StringBuilder()
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.bitchat.android.util
|
||||
|
||||
/**
|
||||
* Extension function to convert a ByteArray to a hexadecimal string.
|
||||
*/
|
||||
fun ByteArray.toHexString(): String {
|
||||
return this.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
@@ -21,6 +21,6 @@ data class ByteArrayWrapper(val bytes: ByteArray) {
|
||||
}
|
||||
|
||||
fun toHexString(): String {
|
||||
return bytes.joinToString("") { "%02x".format(it) }
|
||||
return Hex.encode(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.bitchat.android.util
|
||||
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
|
||||
object Hashing {
|
||||
fun sha256(bytes: ByteArray): ByteArray {
|
||||
return MessageDigest.getInstance("SHA-256").digest(bytes)
|
||||
}
|
||||
|
||||
fun sha256Hex(bytes: ByteArray): String {
|
||||
return Hex.encode(sha256(bytes))
|
||||
}
|
||||
|
||||
fun sha256Hex(text: String): String {
|
||||
return sha256Hex(text.toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
|
||||
fun sha256Hex(input: InputStream): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
|
||||
while (true) {
|
||||
val read = input.read(buffer)
|
||||
if (read <= 0) break
|
||||
digest.update(buffer, 0, read)
|
||||
}
|
||||
|
||||
return Hex.encode(digest.digest())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.bitchat.android.util
|
||||
|
||||
object Hex {
|
||||
private val digits = "0123456789abcdef".toCharArray()
|
||||
|
||||
fun encode(bytes: ByteArray): String {
|
||||
val chars = CharArray(bytes.size * 2)
|
||||
var index = 0
|
||||
for (byte in bytes) {
|
||||
val value = byte.toInt() and 0xFF
|
||||
chars[index++] = digits[value ushr 4]
|
||||
chars[index++] = digits[value and 0x0F]
|
||||
}
|
||||
return String(chars)
|
||||
}
|
||||
|
||||
fun decode(hex: String, allowOddLength: Boolean = false): ByteArray? {
|
||||
val clean = hex.trim()
|
||||
val normalized = when {
|
||||
clean.length % 2 == 0 -> clean
|
||||
allowOddLength -> "0$clean"
|
||||
else -> return null
|
||||
}
|
||||
|
||||
val output = ByteArray(normalized.length / 2)
|
||||
var inputIndex = 0
|
||||
var outputIndex = 0
|
||||
while (inputIndex < normalized.length) {
|
||||
val high = normalized[inputIndex].digitToIntOrNull(16) ?: return null
|
||||
val low = normalized[inputIndex + 1].digitToIntOrNull(16) ?: return null
|
||||
output[outputIndex++] = ((high shl 4) or low).toByte()
|
||||
inputIndex += 2
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
fun decodeOrThrow(hex: String, allowOddLength: Boolean = false): ByteArray {
|
||||
return decode(hex, allowOddLength)
|
||||
?: throw IllegalArgumentException("Invalid hex string")
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteArray.toHexString(): String = Hex.encode(this)
|
||||
|
||||
fun String.hexToByteArray(): ByteArray = Hex.decodeOrThrow(this)
|
||||
|
||||
fun String.hexToByteArrayOrNull(allowOddLength: Boolean = false): ByteArray? {
|
||||
return Hex.decode(this, allowOddLength)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.bitchat.android.util
|
||||
|
||||
object PeerId {
|
||||
const val BYTE_LENGTH = 8
|
||||
const val HEX_LENGTH = BYTE_LENGTH * 2
|
||||
|
||||
fun fromBytes(bytes: ByteArray): String {
|
||||
return Hex.encode(bytes.copyOf(BYTE_LENGTH))
|
||||
}
|
||||
|
||||
fun parse(peerIdHex: String): ByteArray? {
|
||||
val clean = peerIdHex.trim()
|
||||
if (clean.length != HEX_LENGTH) return null
|
||||
return Hex.decode(clean)?.takeIf { it.size == BYTE_LENGTH }
|
||||
}
|
||||
|
||||
fun toBytes(peerIdHex: String): ByteArray {
|
||||
val result = ByteArray(BYTE_LENGTH)
|
||||
val clean = peerIdHex.trim()
|
||||
var inputIndex = 0
|
||||
var outputIndex = 0
|
||||
|
||||
while (inputIndex + 1 < clean.length && outputIndex < BYTE_LENGTH) {
|
||||
val value = clean.substring(inputIndex, inputIndex + 2).toIntOrNull(16)
|
||||
if (value != null) {
|
||||
result[outputIndex] = value.toByte()
|
||||
}
|
||||
inputIndex += 2
|
||||
outputIndex += 1
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun normalize(peerIdHex: String): String {
|
||||
return fromBytes(toBytes(peerIdHex))
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ package com.bitchat.android.mesh
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.util.PeerId
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
@@ -37,8 +38,8 @@ class PacketRelayManagerTest {
|
||||
private fun createPacket(route: List<ByteArray>?, recipient: String? = null): BitchatPacket {
|
||||
return BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
senderID = hexStringToPeerBytes(otherPeerID),
|
||||
recipientID = recipient?.let { hexStringToPeerBytes(it) },
|
||||
senderID = PeerId.toBytes(otherPeerID),
|
||||
recipientID = recipient?.let { PeerId.toBytes(it) },
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = "hello".toByteArray(),
|
||||
ttl = 5u,
|
||||
@@ -49,8 +50,8 @@ class PacketRelayManagerTest {
|
||||
@Test
|
||||
fun `packet with duplicate hops is dropped`() = runTest {
|
||||
val route = listOf(
|
||||
hexStringToPeerBytes(nextHopPeerID),
|
||||
hexStringToPeerBytes(nextHopPeerID)
|
||||
PeerId.toBytes(nextHopPeerID),
|
||||
PeerId.toBytes(nextHopPeerID)
|
||||
)
|
||||
val packet = createPacket(route)
|
||||
val routedPacket = RoutedPacket(packet, otherPeerID)
|
||||
@@ -64,8 +65,8 @@ class PacketRelayManagerTest {
|
||||
@Test
|
||||
fun `valid source-routed packet is relayed to next hop`() = runTest {
|
||||
val route = listOf(
|
||||
hexStringToPeerBytes(myPeerID),
|
||||
hexStringToPeerBytes(nextHopPeerID)
|
||||
PeerId.toBytes(myPeerID),
|
||||
PeerId.toBytes(nextHopPeerID)
|
||||
)
|
||||
val packet = createPacket(route, finalRecipientID)
|
||||
val routedPacket = RoutedPacket(packet, otherPeerID)
|
||||
@@ -80,7 +81,7 @@ class PacketRelayManagerTest {
|
||||
@Test
|
||||
fun `last hop does not relay further`() = runTest {
|
||||
val route = listOf(
|
||||
hexStringToPeerBytes(myPeerID)
|
||||
PeerId.toBytes(myPeerID)
|
||||
)
|
||||
val packet = createPacket(route, finalRecipientID)
|
||||
val routedPacket = RoutedPacket(packet, otherPeerID)
|
||||
@@ -103,15 +104,4 @@ class PacketRelayManagerTest {
|
||||
verify(delegate).broadcastPacket(any())
|
||||
}
|
||||
|
||||
private fun hexStringToPeerBytes(hex: String): ByteArray {
|
||||
val result = ByteArray(8)
|
||||
var idx = 0
|
||||
var out = 0
|
||||
while (idx + 1 < hex.length && out < 8) {
|
||||
val b = hex.substring(idx, idx + 2).toIntOrNull(16)?.toByte() ?: 0
|
||||
result[out++] = b
|
||||
idx += 2
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import com.bitchat.android.protocol.TlvLengthSize
|
||||
import com.bitchat.android.protocol.TlvWriter
|
||||
import com.bitchat.android.services.meshgraph.GossipTLV
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class TlvPacketRegressionTest {
|
||||
|
||||
@Test
|
||||
fun `identity announcement skips unknown tlv fields`() {
|
||||
val noiseKey = ByteArray(32) { 0x11 }
|
||||
val signingKey = ByteArray(32) { 0x22 }
|
||||
val encoded = TlvWriter()
|
||||
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x01, "alice".toByteArray(), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x02, noiseKey, TlvLengthSize.ONE_BYTE)
|
||||
.put(0x03, signingKey, TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
|
||||
val decoded = IdentityAnnouncement.decode(encoded)
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertEquals("alice", decoded!!.nickname)
|
||||
assertArrayEquals(noiseKey, decoded.noisePublicKey)
|
||||
assertArrayEquals(signingKey, decoded.signingPublicKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `private message rejects unknown tlv fields`() {
|
||||
val encoded = TlvWriter()
|
||||
.put(0x00, "msg-1".toByteArray(), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x01, "hello".toByteArray(), TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
|
||||
assertNull(PrivateMessagePacket.decode(encoded))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `request sync skips unknown tlv fields and enforces payload cap`() {
|
||||
val encoded = TlvWriter()
|
||||
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x01, byteArrayOf(4), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x02, byteArrayOf(0, 0, 0, 16), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x03, byteArrayOf(1, 2, 3), TlvLengthSize.TWO_BYTES)
|
||||
.toByteArray()
|
||||
val tooLarge = TlvWriter()
|
||||
.put(0x01, byteArrayOf(4), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x02, byteArrayOf(0, 0, 0, 16), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x03, ByteArray(RequestSyncPacket.MAX_ACCEPT_FILTER_BYTES + 1), TlvLengthSize.TWO_BYTES)
|
||||
.toByteArray()
|
||||
|
||||
val decoded = RequestSyncPacket.decode(encoded)
|
||||
|
||||
assertNotNull(decoded)
|
||||
assertEquals(4, decoded!!.p)
|
||||
assertEquals(16L, decoded.m)
|
||||
assertArrayEquals(byteArrayOf(1, 2, 3), decoded.data)
|
||||
assertNull(RequestSyncPacket.decode(tooLarge))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `file packet uses four byte content length for large payloads`() {
|
||||
val content = ByteArray(70_000) { (it % 251).toByte() }
|
||||
val packet = BitchatFilePacket(
|
||||
fileName = "large.bin",
|
||||
fileSize = content.size.toLong(),
|
||||
mimeType = "application/octet-stream",
|
||||
content = content
|
||||
)
|
||||
|
||||
val encoded = packet.encode()
|
||||
val decoded = BitchatFilePacket.decode(encoded!!)
|
||||
val contentTypeOffset = encoded.indexOf(0x04.toByte())
|
||||
val contentLength =
|
||||
((encoded[contentTypeOffset + 1].toInt() and 0xFF) shl 24) or
|
||||
((encoded[contentTypeOffset + 2].toInt() and 0xFF) shl 16) or
|
||||
((encoded[contentTypeOffset + 3].toInt() and 0xFF) shl 8) or
|
||||
(encoded[contentTypeOffset + 4].toInt() and 0xFF)
|
||||
|
||||
assertEquals(content.size, contentLength)
|
||||
assertNotNull(decoded)
|
||||
assertArrayEquals(content, decoded!!.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `file packet rejects unknown tlv fields`() {
|
||||
val encoded = TlvWriter()
|
||||
.put(0x7f, byteArrayOf(0x55), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x01, "x.bin".toByteArray(), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x02, byteArrayOf(0, 0, 0, 1), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x03, "application/octet-stream".toByteArray(), TlvLengthSize.TWO_BYTES)
|
||||
.put(0x04, byteArrayOf(1), TlvLengthSize.FOUR_BYTES)
|
||||
.toByteArray()
|
||||
|
||||
assertNull(BitchatFilePacket.decode(encoded))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `gossip tlv uses shared peer id and tlv helpers`() {
|
||||
val encoded = GossipTLV.encodeNeighbors(listOf("0102030405060708", "aabbcc"))
|
||||
|
||||
assertEquals(listOf("0102030405060708", "aabbcc0000000000"), GossipTLV.decodeNeighborsFromAnnouncementPayload(encoded))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.bitchat.android.protocol
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class TlvTest {
|
||||
|
||||
@Test
|
||||
fun `writer and reader support one two and four byte lengths`() {
|
||||
val oneByteValue = byteArrayOf(0x7f)
|
||||
val twoByteValue = ByteArray(256) { it.toByte() }
|
||||
val fourByteValue = ByteArray(70_000) { (it % 251).toByte() }
|
||||
|
||||
val encoded = TlvWriter()
|
||||
.put(0x01, oneByteValue, TlvLengthSize.ONE_BYTE)
|
||||
.put(0x02, twoByteValue, TlvLengthSize.TWO_BYTES)
|
||||
.put(0x03, fourByteValue, TlvLengthSize.FOUR_BYTES)
|
||||
.toByteArray()
|
||||
|
||||
val fields = TlvReader.decode(encoded, TlvLengthSize.ONE_BYTE) { type ->
|
||||
when (type) {
|
||||
0x02 -> TlvLengthSize.TWO_BYTES
|
||||
0x03 -> TlvLengthSize.FOUR_BYTES
|
||||
else -> TlvLengthSize.ONE_BYTE
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(3, fields!!.size)
|
||||
assertArrayEquals(oneByteValue, fields[0].value)
|
||||
assertArrayEquals(twoByteValue, fields[1].value)
|
||||
assertArrayEquals(fourByteValue, fields[2].value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown tlv policy skips or fails consistently`() {
|
||||
val encoded = TlvWriter()
|
||||
.put(0x01, byteArrayOf(1), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x7f, byteArrayOf(9), TlvLengthSize.ONE_BYTE)
|
||||
.put(0x02, byteArrayOf(2), TlvLengthSize.ONE_BYTE)
|
||||
.toByteArray()
|
||||
|
||||
val skipped = TlvReader.decode(
|
||||
data = encoded,
|
||||
defaultLengthSize = TlvLengthSize.ONE_BYTE,
|
||||
unknownPolicy = UnknownTlvPolicy.SKIP,
|
||||
knownTypes = setOf(0x01, 0x02)
|
||||
)
|
||||
val failed = TlvReader.decode(
|
||||
data = encoded,
|
||||
defaultLengthSize = TlvLengthSize.ONE_BYTE,
|
||||
unknownPolicy = UnknownTlvPolicy.FAIL,
|
||||
knownTypes = setOf(0x01, 0x02)
|
||||
)
|
||||
|
||||
assertEquals(listOf(0x01, 0x02), skipped!!.map { it.type })
|
||||
assertNull(failed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reader rejects truncated values and writer rejects oversized values`() {
|
||||
val truncated = byteArrayOf(0x01, 0x03, 0x41)
|
||||
|
||||
assertNull(TlvReader.decode(truncated, TlvLengthSize.ONE_BYTE))
|
||||
|
||||
try {
|
||||
TlvWriter().put(0x01, ByteArray(256), TlvLengthSize.ONE_BYTE)
|
||||
throw AssertionError("Expected oversized one-byte TLV value to fail")
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Expected.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.bitchat.android.ui.debug
|
||||
|
||||
import android.os.Build
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [Build.VERSION_CODES.P], manifest = Config.NONE)
|
||||
class DebugSettingsManagerTest {
|
||||
|
||||
private lateinit var manager: DebugSettingsManager
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
manager = DebugSettingsManager.getInstance()
|
||||
manager.resetForTesting()
|
||||
manager.setVerboseLoggingEnabled(true)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
manager.resetForTesting()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logIncoming emits one typed packet trace`() {
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 7u,
|
||||
senderID = "aaaabbbbccccdddd",
|
||||
payload = byteArrayOf(1, 2, 3)
|
||||
)
|
||||
|
||||
manager.logIncoming(
|
||||
packet = packet,
|
||||
fromPeerID = "aaaabbbbccccdddd",
|
||||
fromNickname = "alice",
|
||||
fromDeviceAddress = "AA:BB:CC:DD:EE:FF",
|
||||
myPeerID = "1111222233334444"
|
||||
)
|
||||
|
||||
val packetMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.PacketEvent>()
|
||||
|
||||
assertEquals("Incoming packet should create one packet event", 1, packetMessages.size)
|
||||
assertTrue(packetMessages.single().content.contains("Incoming"))
|
||||
assertTrue(packetMessages.single().content.contains("MESSAGE"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logOutgoing relay emits one relay event without packet duplicate`() {
|
||||
manager.logOutgoing(
|
||||
packetType = "MESSAGE",
|
||||
toPeerID = "bbbbccccddddeeee",
|
||||
toNickname = "bob",
|
||||
toDeviceAddress = "11:22:33:44:55:66",
|
||||
previousHopPeerID = "aaaabbbbccccdddd",
|
||||
packetVersion = 1u,
|
||||
routeInfo = "routed: 2 hops",
|
||||
ttl = 6u
|
||||
)
|
||||
|
||||
val packetMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.PacketEvent>()
|
||||
val relayMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.RelayEvent>()
|
||||
|
||||
assertEquals("Relay send should not also create a packet event", 0, packetMessages.size)
|
||||
assertEquals("Relay send should create one relay event", 1, relayMessages.size)
|
||||
assertTrue(relayMessages.single().content.contains("Relay"))
|
||||
assertTrue(relayMessages.single().content.contains("MESSAGE"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validation failures are hidden when verbose logging is disabled`() {
|
||||
manager.setVerboseLoggingEnabled(false)
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.MESSAGE.value,
|
||||
ttl = 7u,
|
||||
senderID = "aaaabbbbccccdddd",
|
||||
payload = byteArrayOf(1)
|
||||
)
|
||||
|
||||
manager.logPacketValidationFailure(packet, "aaaabbbbccccdddd", "invalid signature")
|
||||
|
||||
val validationMessages = manager.debugMessages.value.filterIsInstance<DebugMessage.ValidationEvent>()
|
||||
assertEquals(0, validationMessages.size)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.bitchat.android.util
|
||||
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class HexPeerHashingTest {
|
||||
|
||||
@Test
|
||||
fun `hex encodes lowercase and decodes mixed case`() {
|
||||
val bytes = byteArrayOf(0x00, 0x0f, 0x10, 0x7f, 0xff.toByte())
|
||||
|
||||
assertEquals("000f107fff", Hex.encode(bytes))
|
||||
assertArrayEquals(bytes, Hex.decode("000F107fFF"))
|
||||
assertEquals("000f107fff", bytes.toHexString())
|
||||
assertArrayEquals(bytes, "000f107fff".hexToByteArray())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hex decode rejects malformed input unless odd length is explicitly allowed`() {
|
||||
assertNull(Hex.decode("abc"))
|
||||
assertArrayEquals(byteArrayOf(0x0a, 0xbc.toByte()), Hex.decode("abc", allowOddLength = true))
|
||||
assertNull(Hex.decode("00xx"))
|
||||
assertNull("00xx".hexToByteArrayOrNull())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `peer id parsing is fixed width with truncation and zero fill`() {
|
||||
assertArrayEquals(
|
||||
byteArrayOf(0x11, 0x22, 0x00, 0x44, 0, 0, 0, 0),
|
||||
PeerId.toBytes("1122zz44")
|
||||
)
|
||||
assertArrayEquals(
|
||||
byteArrayOf(0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77),
|
||||
PeerId.toBytes("00112233445566778899")
|
||||
)
|
||||
assertEquals("1122004400000000", PeerId.normalize("1122zz44"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `peer id strict parse requires exactly eight valid bytes`() {
|
||||
val bytes = byteArrayOf(0, 1, 2, 3, 4, 5, 6, 7)
|
||||
|
||||
assertArrayEquals(bytes, PeerId.parse("0001020304050607"))
|
||||
assertEquals("0001020304050607", PeerId.fromBytes(bytes))
|
||||
assertNull(PeerId.parse("000102"))
|
||||
assertNull(PeerId.parse("00010203040506xx"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hashing sha256 hex matches known vector`() {
|
||||
assertEquals(
|
||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
||||
Hashing.sha256Hex("abc")
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user