Add QR verification payloads and mesh wiring

This commit is contained in:
yet300
2025-12-31 18:45:47 +04:00
parent a642b22e0a
commit 0b814be69e
7 changed files with 394 additions and 8 deletions
@@ -7,12 +7,15 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.protocol.MessagePadding import com.bitchat.android.protocol.MessagePadding
import com.bitchat.android.model.RoutedPacket import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.model.IdentityAnnouncement import com.bitchat.android.model.IdentityAnnouncement
import com.bitchat.android.model.NoisePayload
import com.bitchat.android.model.NoisePayloadType
import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType import com.bitchat.android.protocol.MessageType
import com.bitchat.android.protocol.SpecialRecipients import com.bitchat.android.protocol.SpecialRecipients
import com.bitchat.android.model.RequestSyncPacket import com.bitchat.android.model.RequestSyncPacket
import com.bitchat.android.sync.GossipSyncManager import com.bitchat.android.sync.GossipSyncManager
import com.bitchat.android.util.toHexString import com.bitchat.android.util.toHexString
import com.bitchat.android.services.VerificationService
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.* import java.util.*
import kotlin.math.sign import kotlin.math.sign
@@ -72,6 +75,7 @@ class BluetoothMeshService(private val context: Context) {
init { init {
Log.i(TAG, "Initializing BluetoothMeshService for peer=$myPeerID") Log.i(TAG, "Initializing BluetoothMeshService for peer=$myPeerID")
VerificationService.configure(encryptionService)
setupDelegates() setupDelegates()
messageHandler.packetProcessor = packetProcessor messageHandler.packetProcessor = packetProcessor
//startPeriodicDebugLogging() //startPeriodicDebugLogging()
@@ -414,6 +418,14 @@ class BluetoothMeshService(private val context: Context) {
override fun onReadReceiptReceived(messageID: String, peerID: String) { override fun onReadReceiptReceived(messageID: String, peerID: String) {
delegate?.didReceiveReadReceipt(messageID, peerID) delegate?.didReceiveReadReceipt(messageID, peerID)
} }
override fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
delegate?.didReceiveVerifyChallenge(peerID, payload, timestampMs)
}
override fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
delegate?.didReceiveVerifyResponse(peerID, payload, timestampMs)
}
} }
// PacketProcessor delegates // PacketProcessor delegates
@@ -939,6 +951,50 @@ class BluetoothMeshService(private val context: Context) {
} }
} }
} }
// MARK: QR Verification over Noise
fun sendVerifyChallenge(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
val tlv = VerificationService.buildVerifyChallenge(noiseKeyHex, nonceA)
val payload = NoisePayload(
type = NoisePayloadType.VERIFY_CHALLENGE,
data = tlv
)
sendNoisePayloadToPeer(payload, peerID, "verify challenge")
}
fun sendVerifyResponse(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
val tlv = VerificationService.buildVerifyResponse(noiseKeyHex, nonceA) ?: return
val payload = NoisePayload(
type = NoisePayloadType.VERIFY_RESPONSE,
data = tlv
)
sendNoisePayloadToPeer(payload, peerID, "verify response")
}
private fun sendNoisePayloadToPeer(payload: NoisePayload, recipientPeerID: String, label: String) {
serviceScope.launch {
try {
val encrypted = encryptionService.encrypt(payload.encode(), recipientPeerID)
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(recipientPeerID),
timestamp = System.currentTimeMillis().toULong(),
payload = encrypted,
signature = null,
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
)
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
Log.d(TAG, "📤 Sent $label to $recipientPeerID (${payload.data.size} bytes)")
} catch (e: Exception) {
Log.e(TAG, "Failed to send $label to $recipientPeerID: ${e.message}")
}
}
}
/** /**
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS * Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
@@ -1127,6 +1183,10 @@ class BluetoothMeshService(private val context: Context) {
fun getIdentityFingerprint(): String { fun getIdentityFingerprint(): String {
return encryptionService.getIdentityFingerprint() return encryptionService.getIdentityFingerprint()
} }
fun getStaticNoisePublicKey(): ByteArray? {
return encryptionService.getStaticPublicKey()
}
/** /**
* Check if encryption icon should be shown for a peer * Check if encryption icon should be shown for a peer
@@ -1283,6 +1343,8 @@ interface BluetoothMeshDelegate {
fun didReceiveChannelLeave(channel: String, fromPeer: String) fun didReceiveChannelLeave(channel: String, fromPeer: String)
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long)
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long)
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
fun getNickname(): String? fun getNickname(): String?
fun isFavorite(peerID: String): Boolean fun isFavorite(peerID: String): Boolean
@@ -157,6 +157,14 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
// Simplified: Call delegate with messageID and peerID directly // Simplified: Call delegate with messageID and peerID directly
delegate?.onReadReceiptReceived(messageID, peerID) delegate?.onReadReceiptReceived(messageID, peerID)
} }
com.bitchat.android.model.NoisePayloadType.VERIFY_CHALLENGE -> {
Log.d(TAG, "🔐 Verify challenge received from $peerID (${noisePayload.data.size} bytes)")
delegate?.onVerifyChallengeReceived(peerID, noisePayload.data, packet.timestamp.toLong())
}
com.bitchat.android.model.NoisePayloadType.VERIFY_RESPONSE -> {
Log.d(TAG, "🔐 Verify response received from $peerID (${noisePayload.data.size} bytes)")
delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong())
}
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -611,4 +619,6 @@ interface MessageHandlerDelegate {
fun onChannelLeave(channel: String, fromPeer: String) fun onChannelLeave(channel: String, fromPeer: String)
fun onDeliveryAckReceived(messageID: String, peerID: String) fun onDeliveryAckReceived(messageID: String, peerID: String)
fun onReadReceiptReceived(messageID: String, peerID: String) fun onReadReceiptReceived(messageID: String, peerID: String)
fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long)
fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long)
} }
@@ -21,6 +21,8 @@ enum class NoisePayloadType(val value: UByte) {
PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding
READ_RECEIPT(0x02u), // Message was read READ_RECEIPT(0x02u), // Message was read
DELIVERED(0x03u), // Message was delivered DELIVERED(0x03u), // Message was delivered
VERIFY_CHALLENGE(0x10u), // Verification challenge
VERIFY_RESPONSE(0x11u), // Verification response
FILE_TRANSFER(0x20u); FILE_TRANSFER(0x20u);
@@ -2,8 +2,12 @@ package com.bitchat.android.nostr
import android.app.Application import android.app.Application
import android.util.Log import android.util.Log
import com.bitchat.android.model.BitchatFilePacket
import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus import com.bitchat.android.model.DeliveryStatus
import com.bitchat.android.model.NoisePayload
import com.bitchat.android.model.NoisePayloadType
import com.bitchat.android.model.PrivateMessagePacket
import com.bitchat.android.protocol.BitchatPacket import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.services.SeenMessageStore import com.bitchat.android.services.SeenMessageStore
import com.bitchat.android.ui.ChatState import com.bitchat.android.ui.ChatState
@@ -71,7 +75,7 @@ class NostrDirectMessageHandler(
if (packet.type != com.bitchat.android.protocol.MessageType.NOISE_ENCRYPTED.value) return@launch if (packet.type != com.bitchat.android.protocol.MessageType.NOISE_ENCRYPTED.value) return@launch
val noisePayload = com.bitchat.android.model.NoisePayload.decode(packet.payload) ?: return@launch val noisePayload = NoisePayload.decode(packet.payload) ?: return@launch
val messageTimestamp = Date(giftWrap.createdAt * 1000L) val messageTimestamp = Date(giftWrap.createdAt * 1000L)
val convKey = "nostr_${senderPubkey.take(16)}" val convKey = "nostr_${senderPubkey.take(16)}"
repo.putNostrKeyMapping(convKey, senderPubkey) repo.putNostrKeyMapping(convKey, senderPubkey)
@@ -104,7 +108,7 @@ class NostrDirectMessageHandler(
} }
private suspend fun processNoisePayload( private suspend fun processNoisePayload(
payload: com.bitchat.android.model.NoisePayload, payload: NoisePayload,
convKey: String, convKey: String,
senderNickname: String, senderNickname: String,
timestamp: Date, timestamp: Date,
@@ -112,8 +116,8 @@ class NostrDirectMessageHandler(
recipientIdentity: NostrIdentity recipientIdentity: NostrIdentity
) { ) {
when (payload.type) { when (payload.type) {
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> { NoisePayloadType.PRIVATE_MESSAGE -> {
val pm = com.bitchat.android.model.PrivateMessagePacket.decode(payload.data) ?: return val pm = PrivateMessagePacket.decode(payload.data) ?: return
val existingMessages = state.getPrivateChatsValue()[convKey] ?: emptyList() val existingMessages = state.getPrivateChatsValue()[convKey] ?: emptyList()
if (existingMessages.any { it.id == pm.messageID }) return if (existingMessages.any { it.id == pm.messageID }) return
@@ -148,21 +152,21 @@ class NostrDirectMessageHandler(
seenStore.markRead(pm.messageID) seenStore.markRead(pm.messageID)
} }
} }
com.bitchat.android.model.NoisePayloadType.DELIVERED -> { NoisePayloadType.DELIVERED -> {
val messageId = String(payload.data, Charsets.UTF_8) val messageId = String(payload.data, Charsets.UTF_8)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
meshDelegateHandler.didReceiveDeliveryAck(messageId, convKey) meshDelegateHandler.didReceiveDeliveryAck(messageId, convKey)
} }
} }
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> { NoisePayloadType.READ_RECEIPT -> {
val messageId = String(payload.data, Charsets.UTF_8) val messageId = String(payload.data, Charsets.UTF_8)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
meshDelegateHandler.didReceiveReadReceipt(messageId, convKey) meshDelegateHandler.didReceiveReadReceipt(messageId, convKey)
} }
} }
com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER -> { NoisePayloadType.FILE_TRANSFER -> {
// Properly handle encrypted file transfer // Properly handle encrypted file transfer
val file = com.bitchat.android.model.BitchatFilePacket.decode(payload.data) val file = BitchatFilePacket.decode(payload.data)
if (file != null) { if (file != null) {
val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase() val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase()
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(application, file) val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(application, file)
@@ -185,6 +189,8 @@ class NostrDirectMessageHandler(
Log.w(TAG, "⚠️ Failed to decode Nostr file transfer from $convKey") Log.w(TAG, "⚠️ Failed to decode Nostr file transfer from $convKey")
} }
} }
NoisePayloadType.VERIFY_CHALLENGE,
NoisePayloadType.VERIFY_RESPONSE -> Unit // Ignore verification payloads in Nostr direct messages
} }
} }
@@ -0,0 +1,294 @@
package com.bitchat.android.services
import android.net.Uri
import android.util.Base64
import com.bitchat.android.crypto.EncryptionService
import com.bitchat.android.util.AppConstants
import com.bitchat.android.util.dataFromHexString
import com.bitchat.android.util.hexEncodedString
import java.io.ByteArrayOutputStream
import java.security.SecureRandom
import androidx.core.net.toUri
import java.lang.ref.WeakReference
/**
* QR verification helpers: schema, signing, and basic challenge/response helpers.
*/
object VerificationService {
private const val CONTEXT = "bitchat-verify-v1"
private const val RESPONSE_CONTEXT = "bitchat-verify-resp-v1"
private var encryptionServiceRef: WeakReference<EncryptionService>? = null
fun configure(encryptionService: EncryptionService) {
this.encryptionServiceRef = WeakReference(encryptionService)
}
data class VerificationQR(
val v: Int,
val noiseKeyHex: String,
val signKeyHex: String,
val npub: String?,
val nickname: String,
val ts: Long,
val nonceB64: String,
val sigHex: String
) {
fun canonicalBytes(): ByteArray {
val out = ByteArrayOutputStream()
fun appendField(value: String) {
val data = value.toByteArray(Charsets.UTF_8)
val len = minOf(data.size, 255)
out.write(len)
out.write(data, 0, len)
}
appendField(CONTEXT)
appendField(v.toString())
appendField(noiseKeyHex.lowercase())
appendField(signKeyHex.lowercase())
appendField(npub ?: "")
appendField(nickname)
appendField(ts.toString())
appendField(nonceB64)
return out.toByteArray()
}
fun toUrlString(): String {
val builder = Uri.Builder()
.scheme("bitchat")
.authority("verify")
.appendQueryParameter("v", v.toString())
.appendQueryParameter("noise", noiseKeyHex)
.appendQueryParameter("sign", signKeyHex)
.appendQueryParameter("nick", nickname)
.appendQueryParameter("ts", ts.toString())
.appendQueryParameter("nonce", nonceB64)
.appendQueryParameter("sig", sigHex)
if (npub != null) {
builder.appendQueryParameter("npub", npub)
}
return builder.build().toString()
}
companion object {
fun fromUrlString(urlString: String): VerificationQR? {
val uri = runCatching { urlString.toUri() }.getOrNull() ?: return null
if (uri.scheme != "bitchat" || uri.host != "verify") return null
val vStr = uri.getQueryParameter("v") ?: return null
val v = vStr.toIntOrNull() ?: return null
val noise = uri.getQueryParameter("noise") ?: return null
val sign = uri.getQueryParameter("sign") ?: return null
val nick = uri.getQueryParameter("nick") ?: return null
val tsStr = uri.getQueryParameter("ts") ?: return null
val ts = tsStr.toLongOrNull() ?: return null
val nonce = uri.getQueryParameter("nonce") ?: return null
val sig = uri.getQueryParameter("sig") ?: return null
val npub = uri.getQueryParameter("npub")
return VerificationQR(
v = v,
noiseKeyHex = noise,
signKeyHex = sign,
npub = npub,
nickname = nick,
ts = ts,
nonceB64 = nonce,
sigHex = sig
)
}
}
}
fun buildMyQRString(nickname: String, npub: String?): String? {
val service = encryptionServiceRef?.get() ?: return null
val cache = Cache.last
if (cache != null && cache.nickname == nickname && cache.npub == npub) {
if (System.currentTimeMillis() - cache.builtAtMs < 60_000L) {
return cache.value
}
}
val noiseKey = service.getStaticPublicKey()?.hexEncodedString() ?: return null
val signKey = service.getSigningPublicKey()?.hexEncodedString() ?: return null
val ts = System.currentTimeMillis() / 1000L
val nonce = ByteArray(16)
SecureRandom().nextBytes(nonce)
val nonceB64 = Base64.encodeToString(
nonce,
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
)
val payload = VerificationQR(
v = 1,
noiseKeyHex = noiseKey,
signKeyHex = signKey,
npub = npub,
nickname = nickname,
ts = ts,
nonceB64 = nonceB64,
sigHex = ""
)
val signature = service.signData(payload.canonicalBytes()) ?: return null
val signed = payload.copy(sigHex = signature.hexEncodedString())
val out = signed.toUrlString()
Cache.last = CacheEntry(nickname, npub, System.currentTimeMillis(), out)
return out
}
fun verifyScannedQR(
urlString: String,
maxAgeSeconds: Long = AppConstants.Verification.QR_MAX_AGE_SECONDS
): VerificationQR? {
val service = encryptionServiceRef?.get() ?: return null
val qr = VerificationQR.fromUrlString(urlString) ?: return null
val now = System.currentTimeMillis() / 1000L
if (now - qr.ts > maxAgeSeconds) return null
val sig = qr.sigHex.dataFromHexString() ?: return null
val signKey = qr.signKeyHex.dataFromHexString() ?: return null
val ok = service.verifyEd25519Signature(sig, qr.canonicalBytes(), signKey)
return if (ok) qr else null
}
fun buildVerifyChallenge(noiseKeyHex: String, nonceA: ByteArray): ByteArray {
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
val out = ByteArrayOutputStream()
out.write(0x01)
out.write(minOf(noiseData.size, 255))
out.write(noiseData, 0, minOf(noiseData.size, 255))
out.write(0x02)
out.write(minOf(nonceA.size, 255))
out.write(nonceA, 0, minOf(nonceA.size, 255))
return out.toByteArray()
}
fun buildVerifyResponse(noiseKeyHex: String, nonceA: ByteArray): ByteArray? {
val service = encryptionServiceRef?.get() ?: return null
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
val msg = ByteArrayOutputStream()
msg.write(RESPONSE_CONTEXT.toByteArray(Charsets.UTF_8))
msg.write(minOf(noiseData.size, 255))
msg.write(noiseData, 0, minOf(noiseData.size, 255))
msg.write(nonceA)
val sig = service.signData(msg.toByteArray()) ?: return null
val out = ByteArrayOutputStream()
out.write(0x01)
out.write(minOf(noiseData.size, 255))
out.write(noiseData, 0, minOf(noiseData.size, 255))
out.write(0x02)
out.write(minOf(nonceA.size, 255))
out.write(nonceA, 0, minOf(nonceA.size, 255))
out.write(0x03)
out.write(minOf(sig.size, 255))
out.write(sig, 0, minOf(sig.size, 255))
return out.toByteArray()
}
fun parseVerifyChallenge(data: ByteArray): Pair<String, ByteArray>? {
var idx = 0
fun take(n: Int): ByteArray? {
if (idx + n > data.size) return null
val out = data.copyOfRange(idx, idx + n)
idx += n
return out
}
val t1 = take(1) ?: return null
if (t1[0].toInt() != 0x01) return null
val l1 = take(1)?.get(0)?.toInt() ?: return null
val noiseBytes = take(l1) ?: return null
val noise = noiseBytes.toString(Charsets.UTF_8)
val t2 = take(1) ?: return null
if (t2[0].toInt() != 0x02) return null
val l2 = take(1)?.get(0)?.toInt() ?: return null
val nonce = take(l2) ?: return null
return noise to nonce
}
data class VerifyResponse(val noiseKeyHex: String, val nonceA: ByteArray, val signature: ByteArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as VerifyResponse
if (noiseKeyHex != other.noiseKeyHex) return false
if (!nonceA.contentEquals(other.nonceA)) return false
if (!signature.contentEquals(other.signature)) return false
return true
}
override fun hashCode(): Int {
var result = noiseKeyHex.hashCode()
result = 31 * result + nonceA.contentHashCode()
result = 31 * result + signature.contentHashCode()
return result
}
}
fun parseVerifyResponse(data: ByteArray): VerifyResponse? {
var idx = 0
fun take(n: Int): ByteArray? {
if (idx + n > data.size) return null
val out = data.copyOfRange(idx, idx + n)
idx += n
return out
}
val t1 = take(1) ?: return null
if (t1[0].toInt() != 0x01) return null
val l1 = take(1)?.get(0)?.toInt() ?: return null
val noiseBytes = take(l1) ?: return null
val noise = noiseBytes.toString(Charsets.UTF_8)
val t2 = take(1) ?: return null
if (t2[0].toInt() != 0x02) return null
val l2 = take(1)?.get(0)?.toInt() ?: return null
val nonce = take(l2) ?: return null
val t3 = take(1) ?: return null
if (t3[0].toInt() != 0x03) return null
val l3 = take(1)?.get(0)?.toInt() ?: return null
val sig = take(l3) ?: return null
return VerifyResponse(noise, nonce, sig)
}
fun verifyResponseSignature(
noiseKeyHex: String,
nonceA: ByteArray,
signature: ByteArray,
signerPublicKeyHex: String
): Boolean {
val service = encryptionServiceRef?.get() ?: return false
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
val msg = ByteArrayOutputStream()
msg.write(RESPONSE_CONTEXT.toByteArray(Charsets.UTF_8))
msg.write(minOf(noiseData.size, 255))
msg.write(noiseData, 0, minOf(noiseData.size, 255))
msg.write(nonceA)
val signerKey = signerPublicKeyHex.dataFromHexString() ?: return false
return service.verifyEd25519Signature(signature, msg.toByteArray(), signerKey)
}
private data class CacheEntry(
val nickname: String,
val npub: String?,
val builtAtMs: Long,
val value: String
)
private object Cache {
var last: CacheEntry? = null
}
}
@@ -216,6 +216,14 @@ class MeshDelegateHandler(
messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Read(recipientPeerID, Date())) messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Read(recipientPeerID, Date()))
} }
} }
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
// Handled by ChatViewModel for verification flow
}
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
// Handled by ChatViewModel for verification flow
}
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? { override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
return channelManager.decryptChannelMessage(encryptedContent, channel) return channelManager.decryptChannelMessage(encryptedContent, channel)
@@ -58,6 +58,10 @@ object AppConstants {
const val HIGH_NONCE_WARNING_THRESHOLD: Long = 1_000_000_000L const val HIGH_NONCE_WARNING_THRESHOLD: Long = 1_000_000_000L
} }
object Verification {
const val QR_MAX_AGE_SECONDS: Long = 300L // 5 minutes
}
object Protocol { object Protocol {
const val COMPRESSION_THRESHOLD_BYTES: Int = 100 const val COMPRESSION_THRESHOLD_BYTES: Int = 100
} }