@magicred-1/ble-mesh 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1994 @@
1
+ package com.blemesh
2
+
3
+ import android.Manifest
4
+ import android.annotation.SuppressLint
5
+ import android.bluetooth.*
6
+ import android.bluetooth.le.*
7
+ import android.content.Context
8
+ import android.content.pm.PackageManager
9
+ import android.os.Build
10
+ import android.os.ParcelUuid
11
+ import android.util.Log
12
+ import androidx.core.app.ActivityCompat
13
+ import com.facebook.react.bridge.*
14
+ import com.facebook.react.modules.core.DeviceEventManagerModule
15
+ import kotlinx.coroutines.*
16
+ import java.nio.ByteBuffer
17
+ import java.nio.ByteOrder
18
+ import java.security.*
19
+ import java.util.*
20
+ import javax.crypto.Cipher
21
+ import javax.crypto.KeyAgreement
22
+ import javax.crypto.spec.GCMParameterSpec
23
+ import javax.crypto.spec.SecretKeySpec
24
+
25
+ class BleMeshModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
26
+
27
+ companion object {
28
+ private const val TAG = "BleMeshModule"
29
+ private const val SERVICE_UUID_DEBUG = "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5A"
30
+ private const val SERVICE_UUID_RELEASE = "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C"
31
+ private const val CHARACTERISTIC_UUID = "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D"
32
+ private const val MESSAGE_TTL: Byte = 7
33
+ }
34
+
35
+ private val serviceUUID = UUID.fromString(if (BuildConfig.DEBUG) SERVICE_UUID_DEBUG else SERVICE_UUID_RELEASE)
36
+ private val characteristicUUID = UUID.fromString(CHARACTERISTIC_UUID)
37
+
38
+ // BLE Objects
39
+ private var bluetoothManager: BluetoothManager? = null
40
+ private var bluetoothAdapter: BluetoothAdapter? = null
41
+ private var bluetoothLeScanner: BluetoothLeScanner? = null
42
+ private var bluetoothLeAdvertiser: BluetoothLeAdvertiser? = null
43
+ private var gattServer: BluetoothGattServer? = null
44
+ private var gattCharacteristic: BluetoothGattCharacteristic? = null
45
+
46
+ // State
47
+ private var isRunning = false
48
+ private var myNickname = "anon"
49
+ private var myPeerId = ""
50
+ private var myPeerIdBytes = ByteArray(8)
51
+
52
+ // Peer tracking
53
+ private data class PeerInfo(
54
+ val peerId: String,
55
+ var nickname: String,
56
+ var isConnected: Boolean,
57
+ var rssi: Int? = null,
58
+ var lastSeen: Long,
59
+ var noisePublicKey: ByteArray? = null,
60
+ var isVerified: Boolean = false
61
+ )
62
+ private val peers = mutableMapOf<String, PeerInfo>()
63
+ private val connectedDevices = mutableMapOf<String, BluetoothDevice>()
64
+ private val deviceToPeer = mutableMapOf<String, String>()
65
+ private val gattConnections = mutableMapOf<String, BluetoothGatt>()
66
+
67
+ // Encryption
68
+ private var privateKey: KeyPair? = null
69
+ private var signingKey: KeyPair? = null
70
+ private val sessions = mutableMapOf<String, ByteArray>()
71
+
72
+ // Message deduplication
73
+ private val processedMessages = mutableSetOf<String>()
74
+
75
+ // File transfer tracking
76
+ private val activeFileTransfers = mutableMapOf<String, FileTransfer>()
77
+ private val fileTransferFragments = mutableMapOf<String, MutableMap<Int, ByteArray>>()
78
+ private const val FILE_FRAGMENT_SIZE = 180 // Max payload size for fragments
79
+
80
+ // Transaction chunking tracking
81
+ private val pendingTransactionChunks = mutableMapOf<String, TransactionChunks>()
82
+ private data class TransactionChunks(
83
+ val txId: String,
84
+ val senderId: String,
85
+ val totalSize: Int,
86
+ val totalChunks: Int,
87
+ val chunks: MutableMap<Int, ByteArray> = mutableMapOf()
88
+ )
89
+
90
+ // Coroutines
91
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
92
+
93
+ override fun getName(): String = "BleMesh"
94
+
95
+ init {
96
+ bluetoothManager = reactApplicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
97
+ bluetoothAdapter = bluetoothManager?.adapter
98
+ generateIdentity()
99
+ }
100
+
101
+ override fun getConstants(): Map<String, Any> = mapOf(
102
+ "SERVICE_UUID" to serviceUUID.toString(),
103
+ "CHARACTERISTIC_UUID" to characteristicUUID.toString()
104
+ )
105
+
106
+ // MARK: - Identity
107
+
108
+ private fun generateIdentity() {
109
+ try {
110
+ val keyGen = KeyPairGenerator.getInstance("EC")
111
+ keyGen.initialize(256)
112
+
113
+ // Load or generate keys
114
+ val prefs = reactApplicationContext.getSharedPreferences("blemesh", Context.MODE_PRIVATE)
115
+
116
+ val savedPrivateKey = prefs.getString("privateKey", null)
117
+ if (savedPrivateKey != null) {
118
+ // TODO: Properly load saved key
119
+ }
120
+
121
+ privateKey = keyGen.generateKeyPair()
122
+ signingKey = keyGen.generateKeyPair()
123
+
124
+ // Generate peer ID from public key fingerprint
125
+ val digest = MessageDigest.getInstance("SHA-256")
126
+ val hash = digest.digest(privateKey!!.public.encoded)
127
+ myPeerIdBytes = hash.copyOf(8)
128
+ myPeerId = myPeerIdBytes.joinToString("") { "%02x".format(it) }
129
+
130
+ Log.d(TAG, "Generated peer ID: $myPeerId")
131
+ } catch (e: Exception) {
132
+ Log.e(TAG, "Failed to generate identity: ${e.message}")
133
+ }
134
+ }
135
+
136
+ // MARK: - React Native API
137
+
138
+ @ReactMethod
139
+ fun requestPermissions(promise: Promise) {
140
+ // Android permissions are handled at the JS layer via PermissionsAndroid
141
+ // This just checks the current state
142
+ val hasPermissions = hasBluetoothPermissions()
143
+ val result = Arguments.createMap().apply {
144
+ putBoolean("bluetooth", hasPermissions)
145
+ putBoolean("location", hasLocationPermission())
146
+ }
147
+ promise.resolve(result)
148
+ }
149
+
150
+ @ReactMethod
151
+ fun checkPermissions(promise: Promise) {
152
+ val result = Arguments.createMap().apply {
153
+ putBoolean("bluetooth", hasBluetoothPermissions())
154
+ putBoolean("location", hasLocationPermission())
155
+ }
156
+ promise.resolve(result)
157
+ }
158
+
159
+ @ReactMethod
160
+ fun start(nickname: String, promise: Promise) {
161
+ myNickname = nickname
162
+
163
+ // If already running, just update nickname and resolve
164
+ if (isRunning) {
165
+ Log.d(TAG, "Already running, updating nickname to: $nickname")
166
+ sendAnnounce()
167
+ promise.resolve(null)
168
+ return
169
+ }
170
+
171
+ scope.launch {
172
+ try {
173
+ startBleServices()
174
+ isRunning = true
175
+ withContext(Dispatchers.Main) {
176
+ promise.resolve(null)
177
+ }
178
+ } catch (e: Exception) {
179
+ withContext(Dispatchers.Main) {
180
+ promise.reject("START_ERROR", e.message)
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ @ReactMethod
187
+ fun stop(promise: Promise) {
188
+ // If not running, just resolve
189
+ if (!isRunning) {
190
+ Log.d(TAG, "Already stopped, nothing to do")
191
+ promise.resolve(null)
192
+ return
193
+ }
194
+
195
+ scope.launch {
196
+ try {
197
+ sendLeaveAnnouncement()
198
+ stopBleServices()
199
+ isRunning = false
200
+ peers.clear()
201
+ connectedDevices.clear()
202
+ deviceToPeer.clear()
203
+ withContext(Dispatchers.Main) {
204
+ promise.resolve(null)
205
+ }
206
+ } catch (e: Exception) {
207
+ withContext(Dispatchers.Main) {
208
+ promise.reject("STOP_ERROR", e.message)
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ @ReactMethod
215
+ fun setNickname(nickname: String, promise: Promise) {
216
+ myNickname = nickname
217
+ sendAnnounce()
218
+ promise.resolve(null)
219
+ }
220
+
221
+ @ReactMethod
222
+ fun getMyPeerId(promise: Promise) {
223
+ promise.resolve(myPeerId)
224
+ }
225
+
226
+ @ReactMethod
227
+ fun getMyNickname(promise: Promise) {
228
+ promise.resolve(myNickname)
229
+ }
230
+
231
+ @ReactMethod
232
+ fun getPeers(promise: Promise) {
233
+ val peersArray = Arguments.createArray()
234
+ peers.values.forEach { peer ->
235
+ peersArray.pushMap(Arguments.createMap().apply {
236
+ putString("peerId", peer.peerId)
237
+ putString("nickname", peer.nickname)
238
+ putBoolean("isConnected", peer.isConnected)
239
+ peer.rssi?.let { putInt("rssi", it) }
240
+ putDouble("lastSeen", peer.lastSeen.toDouble())
241
+ putBoolean("isVerified", peer.isVerified)
242
+ })
243
+ }
244
+ promise.resolve(peersArray)
245
+ }
246
+
247
+ @ReactMethod
248
+ fun sendMessage(content: String, channel: String?, promise: Promise) {
249
+ val messageId = UUID.randomUUID().toString()
250
+
251
+ scope.launch {
252
+ try {
253
+ val packet = createPacket(
254
+ type = MessageType.MESSAGE.value,
255
+ payload = content.toByteArray(Charsets.UTF_8),
256
+ recipientId = null
257
+ )
258
+ broadcastPacket(packet)
259
+ withContext(Dispatchers.Main) {
260
+ promise.resolve(messageId)
261
+ }
262
+ } catch (e: Exception) {
263
+ withContext(Dispatchers.Main) {
264
+ promise.reject("SEND_ERROR", e.message)
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ @ReactMethod
271
+ fun sendPrivateMessage(content: String, recipientPeerId: String, promise: Promise) {
272
+ val messageId = UUID.randomUUID().toString()
273
+
274
+ scope.launch {
275
+ try {
276
+ if (sessions.containsKey(recipientPeerId)) {
277
+ val encrypted = encryptMessage(content, recipientPeerId)
278
+ if (encrypted != null) {
279
+ val packet = createPacket(
280
+ type = MessageType.NOISE_ENCRYPTED.value,
281
+ payload = encrypted,
282
+ recipientId = hexStringToByteArray(recipientPeerId)
283
+ )
284
+ broadcastPacket(packet)
285
+ }
286
+ } else {
287
+ initiateHandshakeInternal(recipientPeerId)
288
+ }
289
+ withContext(Dispatchers.Main) {
290
+ promise.resolve(messageId)
291
+ }
292
+ } catch (e: Exception) {
293
+ withContext(Dispatchers.Main) {
294
+ promise.reject("SEND_ERROR", e.message)
295
+ }
296
+ }
297
+ }
298
+ }
299
+
300
+ @ReactMethod
301
+ fun sendFile(filePath: String, recipientPeerId: String?, channel: String?, promise: Promise) {
302
+ scope.launch {
303
+ try {
304
+ val transferId = UUID.randomUUID().toString()
305
+ val file = java.io.File(filePath)
306
+
307
+ if (!file.exists()) {
308
+ withContext(Dispatchers.Main) {
309
+ promise.reject("FILE_ERROR", "File does not exist: $filePath")
310
+ }
311
+ return@launch
312
+ }
313
+
314
+ val fileData = file.readBytes()
315
+ val fileName = file.name
316
+ val mimeType = getMimeType(fileName)
317
+ val totalChunks = (fileData.size + FILE_FRAGMENT_SIZE - 1) / FILE_FRAGMENT_SIZE
318
+
319
+ // Build file transfer metadata packet
320
+ val metadataPayload = buildFileTransferMetadata(transferId, fileName, fileData.size, mimeType, totalChunks)
321
+
322
+ val metadataPacket = createPacket(
323
+ type = MessageType.FILE_TRANSFER.value,
324
+ payload = metadataPayload,
325
+ recipientId = recipientPeerId?.let { hexStringToByteArray(it) }
326
+ )
327
+ broadcastPacket(metadataPacket)
328
+
329
+ // Send file fragments
330
+ for (chunkIndex in 0 until totalChunks) {
331
+ val start = chunkIndex * FILE_FRAGMENT_SIZE
332
+ val end = minOf(start + FILE_FRAGMENT_SIZE, fileData.size)
333
+ val chunkData = fileData.copyOfRange(start, end)
334
+
335
+ val fragmentPayload = buildFileFragment(transferId, chunkIndex, totalChunks, chunkData)
336
+
337
+ val fragmentPacket = createPacket(
338
+ type = MessageType.FRAGMENT.value,
339
+ payload = fragmentPayload,
340
+ recipientId = recipientPeerId?.let { hexStringToByteArray(it) }
341
+ )
342
+ broadcastPacket(fragmentPacket)
343
+
344
+ // Small delay to avoid overwhelming the BLE stack
345
+ delay(50)
346
+ }
347
+
348
+ withContext(Dispatchers.Main) {
349
+ promise.resolve(transferId)
350
+ }
351
+ } catch (e: Exception) {
352
+ Log.e(TAG, "Failed to send file: ${e.message}")
353
+ withContext(Dispatchers.Main) {
354
+ promise.reject("FILE_ERROR", e.message)
355
+ }
356
+ }
357
+ }
358
+ }
359
+
360
+ private fun buildFileTransferMetadata(transferId: String, fileName: String, fileSize: Int, mimeType: String, totalChunks: Int): ByteArray {
361
+ val stream = ByteArrayOutputStream()
362
+
363
+ // Transfer ID TLV (tag 0x01)
364
+ val transferIdBytes = transferId.toByteArray(Charsets.UTF_8)
365
+ stream.write(0x01)
366
+ stream.write((transferIdBytes.size shr 8) and 0xFF)
367
+ stream.write(transferIdBytes.size and 0xFF)
368
+ stream.write(transferIdBytes)
369
+
370
+ // File name TLV (tag 0x02)
371
+ val fileNameBytes = fileName.toByteArray(Charsets.UTF_8)
372
+ stream.write(0x02)
373
+ stream.write((fileNameBytes.size shr 8) and 0xFF)
374
+ stream.write(fileNameBytes.size and 0xFF)
375
+ stream.write(fileNameBytes)
376
+
377
+ // File size TLV (tag 0x03) - 4 bytes
378
+ stream.write(0x03)
379
+ stream.write(0x00)
380
+ stream.write(0x04)
381
+ stream.write((fileSize shr 24) and 0xFF)
382
+ stream.write((fileSize shr 16) and 0xFF)
383
+ stream.write((fileSize shr 8) and 0xFF)
384
+ stream.write(fileSize and 0xFF)
385
+
386
+ // MIME type TLV (tag 0x04)
387
+ val mimeTypeBytes = mimeType.toByteArray(Charsets.UTF_8)
388
+ stream.write(0x04)
389
+ stream.write((mimeTypeBytes.size shr 8) and 0xFF)
390
+ stream.write(mimeTypeBytes.size and 0xFF)
391
+ stream.write(mimeTypeBytes)
392
+
393
+ // Total chunks TLV (tag 0x05) - 4 bytes
394
+ stream.write(0x05)
395
+ stream.write(0x00)
396
+ stream.write(0x04)
397
+ stream.write((totalChunks shr 24) and 0xFF)
398
+ stream.write((totalChunks shr 16) and 0xFF)
399
+ stream.write((totalChunks shr 8) and 0xFF)
400
+ stream.write(totalChunks and 0xFF)
401
+
402
+ return stream.toByteArray()
403
+ }
404
+
405
+ private fun buildFileFragment(transferId: String, chunkIndex: Int, totalChunks: Int, chunkData: ByteArray): ByteArray {
406
+ val stream = ByteArrayOutputStream()
407
+
408
+ // Transfer ID TLV (tag 0x01)
409
+ val transferIdBytes = transferId.toByteArray(Charsets.UTF_8)
410
+ stream.write(0x01)
411
+ stream.write((transferIdBytes.size shr 8) and 0xFF)
412
+ stream.write(transferIdBytes.size and 0xFF)
413
+ stream.write(transferIdBytes)
414
+
415
+ // Chunk index TLV (tag 0x02) - 4 bytes
416
+ stream.write(0x02)
417
+ stream.write(0x00)
418
+ stream.write(0x04)
419
+ stream.write((chunkIndex shr 24) and 0xFF)
420
+ stream.write((chunkIndex shr 16) and 0xFF)
421
+ stream.write((chunkIndex shr 8) and 0xFF)
422
+ stream.write(chunkIndex and 0xFF)
423
+
424
+ // Total chunks TLV (tag 0x03) - 4 bytes
425
+ stream.write(0x03)
426
+ stream.write(0x00)
427
+ stream.write(0x04)
428
+ stream.write((totalChunks shr 24) and 0xFF)
429
+ stream.write((totalChunks shr 16) and 0xFF)
430
+ stream.write((totalChunks shr 8) and 0xFF)
431
+ stream.write(totalChunks and 0xFF)
432
+
433
+ // Chunk data TLV (tag 0x04)
434
+ stream.write(0x04)
435
+ stream.write((chunkData.size shr 8) and 0xFF)
436
+ stream.write(chunkData.size and 0xFF)
437
+ stream.write(chunkData)
438
+
439
+ return stream.toByteArray()
440
+ }
441
+
442
+ private fun getMimeType(fileName: String): String {
443
+ val extension = fileName.substringAfterLast('.', "")
444
+ return when (extension.lowercase()) {
445
+ "jpg", "jpeg" -> "image/jpeg"
446
+ "png" -> "image/png"
447
+ "gif" -> "image/gif"
448
+ "pdf" -> "application/pdf"
449
+ "txt" -> "text/plain"
450
+ "mp4" -> "video/mp4"
451
+ "mp3" -> "audio/mpeg"
452
+ else -> "application/octet-stream"
453
+ }
454
+ }
455
+
456
+ @ReactMethod
457
+ fun sendTransaction(
458
+ txId: String,
459
+ serializedTransaction: String,
460
+ recipientPeerId: String?,
461
+ firstSignerPublicKey: String,
462
+ secondSignerPublicKey: String?,
463
+ description: String?,
464
+ promise: Promise
465
+ ) {
466
+ scope.launch {
467
+ try {
468
+ // Build TLV payload for Solana transaction
469
+ val payload = buildSolanaTransactionPayload(
470
+ txId = txId,
471
+ serializedTransaction = serializedTransaction,
472
+ firstSignerPublicKey = firstSignerPublicKey,
473
+ secondSignerPublicKey = secondSignerPublicKey,
474
+ description = description
475
+ )
476
+
477
+ // Check if we need chunking (MTU limit is ~500 bytes for encrypted payload)
478
+ val maxPayloadSize = 450 // Conservative limit for encrypted data
479
+
480
+ if (recipientPeerId != null) {
481
+ // Send to specific peer
482
+ if (sessions.containsKey(recipientPeerId)) {
483
+ sendTransactionToPeer(txId, payload, recipientPeerId, maxPayloadSize)
484
+ } else {
485
+ // No session, initiate handshake first
486
+ initiateHandshakeInternal(recipientPeerId)
487
+ }
488
+ } else {
489
+ // Broadcast to all connected peers with sessions
490
+ Log.d(TAG, "Broadcasting transaction $txId to all peers")
491
+ var sentCount = 0
492
+ sessions.keys.forEach { peerId ->
493
+ sendTransactionToPeer(txId, payload, peerId, maxPayloadSize)
494
+ sentCount++
495
+ }
496
+ Log.d(TAG, "Transaction broadcast to $sentCount peers")
497
+ }
498
+
499
+ withContext(Dispatchers.Main) {
500
+ promise.resolve(txId)
501
+ }
502
+ } catch (e: Exception) {
503
+ Log.e(TAG, "Failed to send transaction: ${e.message}")
504
+ withContext(Dispatchers.Main) {
505
+ promise.reject("TRANSACTION_ERROR", e.message)
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ private fun sendTransactionToPeer(txId: String, payload: ByteArray, recipientPeerId: String, maxPayloadSize: Int) {
512
+ val encryptedPayload = byteArrayOf(NoisePayloadType.SOLANA_TRANSACTION.value) + payload
513
+ val encrypted = encryptPayload(encryptedPayload, recipientPeerId)
514
+
515
+ if (encrypted != null) {
516
+ if (encrypted.size <= maxPayloadSize) {
517
+ // Small enough for single packet
518
+ val packet = createPacket(
519
+ type = MessageType.NOISE_ENCRYPTED.value,
520
+ payload = encrypted,
521
+ recipientId = hexStringToByteArray(recipientPeerId)
522
+ )
523
+ broadcastPacket(packet)
524
+ } else {
525
+ // Need to chunk large transaction
526
+ Log.d(TAG, "Transaction too large (${encrypted.size} bytes), using chunking")
527
+ sendChunkedTransaction(txId, encrypted, recipientPeerId)
528
+ }
529
+ }
530
+ }
531
+
532
+ private suspend fun sendChunkedTransaction(txId: String, encryptedData: ByteArray, recipientPeerId: String) {
533
+ val chunkSize = 400 // Max chunk size for encrypted data
534
+ val totalChunks = (encryptedData.size + chunkSize - 1) / chunkSize
535
+
536
+ Log.d(TAG, "Sending chunked transaction $txId: ${encryptedData.size} bytes in $totalChunks chunks")
537
+
538
+ // Send metadata packet first
539
+ val metadataPayload = buildTransactionChunkMetadata(txId, encryptedData.size, totalChunks)
540
+ val metadataPacket = createPacket(
541
+ type = MessageType.SOLANA_TRANSACTION.value,
542
+ payload = metadataPayload,
543
+ recipientId = hexStringToByteArray(recipientPeerId)
544
+ )
545
+ broadcastPacket(metadataPacket)
546
+
547
+ delay(100) // Give receiver time to prepare
548
+
549
+ // Send chunks
550
+ for (chunkIndex in 0 until totalChunks) {
551
+ val start = chunkIndex * chunkSize
552
+ val end = minOf(start + chunkSize, encryptedData.size)
553
+ val chunkData = encryptedData.copyOfRange(start, end)
554
+
555
+ val chunkPayload = buildTransactionChunk(txId, chunkIndex, totalChunks, chunkData)
556
+ val chunkPacket = createPacket(
557
+ type = MessageType.FRAGMENT.value,
558
+ payload = chunkPayload,
559
+ recipientId = hexStringToByteArray(recipientPeerId)
560
+ )
561
+ broadcastPacket(chunkPacket)
562
+
563
+ Log.d(TAG, "Sent chunk ${chunkIndex + 1}/$totalChunks for transaction $txId")
564
+ delay(50) // Small delay between chunks
565
+ }
566
+ }
567
+
568
+ private fun buildTransactionChunkMetadata(txId: String, totalSize: Int, totalChunks: Int): ByteArray {
569
+ val stream = ByteArrayOutputStream()
570
+
571
+ // Transaction ID TLV (tag 0x01)
572
+ val txIdBytes = txId.toByteArray(Charsets.UTF_8)
573
+ stream.write(0x01)
574
+ stream.write((txIdBytes.size shr 8) and 0xFF)
575
+ stream.write(txIdBytes.size and 0xFF)
576
+ stream.write(txIdBytes)
577
+
578
+ // Total size TLV (tag 0x02) - 4 bytes
579
+ stream.write(0x02)
580
+ stream.write(0x00)
581
+ stream.write(0x04)
582
+ stream.write((totalSize shr 24) and 0xFF)
583
+ stream.write((totalSize shr 16) and 0xFF)
584
+ stream.write((totalSize shr 8) and 0xFF)
585
+ stream.write(totalSize and 0xFF)
586
+
587
+ // Total chunks TLV (tag 0x03) - 4 bytes
588
+ stream.write(0x03)
589
+ stream.write(0x00)
590
+ stream.write(0x04)
591
+ stream.write((totalChunks shr 24) and 0xFF)
592
+ stream.write((totalChunks shr 16) and 0xFF)
593
+ stream.write((totalChunks shr 8) and 0xFF)
594
+ stream.write(totalChunks and 0xFF)
595
+
596
+ return stream.toByteArray()
597
+ }
598
+
599
+ private fun buildTransactionChunk(txId: String, chunkIndex: Int, totalChunks: Int, chunkData: ByteArray): ByteArray {
600
+ val stream = ByteArrayOutputStream()
601
+
602
+ // Transaction ID TLV (tag 0x01)
603
+ val txIdBytes = txId.toByteArray(Charsets.UTF_8)
604
+ stream.write(0x01)
605
+ stream.write((txIdBytes.size shr 8) and 0xFF)
606
+ stream.write(txIdBytes.size and 0xFF)
607
+ stream.write(txIdBytes)
608
+
609
+ // Chunk index TLV (tag 0x02) - 4 bytes
610
+ stream.write(0x02)
611
+ stream.write(0x00)
612
+ stream.write(0x04)
613
+ stream.write((chunkIndex shr 24) and 0xFF)
614
+ stream.write((chunkIndex shr 16) and 0xFF)
615
+ stream.write((chunkIndex shr 8) and 0xFF)
616
+ stream.write(chunkIndex and 0xFF)
617
+
618
+ // Total chunks TLV (tag 0x03) - 4 bytes
619
+ stream.write(0x03)
620
+ stream.write(0x00)
621
+ stream.write(0x04)
622
+ stream.write((totalChunks shr 24) and 0xFF)
623
+ stream.write((totalChunks shr 16) and 0xFF)
624
+ stream.write((totalChunks shr 8) and 0xFF)
625
+ stream.write(totalChunks and 0xFF)
626
+
627
+ // Chunk data TLV (tag 0x04)
628
+ stream.write(0x04)
629
+ stream.write((chunkData.size shr 8) and 0xFF)
630
+ stream.write(chunkData.size and 0xFF)
631
+ stream.write(chunkData)
632
+
633
+ return stream.toByteArray()
634
+ }
635
+
636
+ private fun buildSolanaTransactionPayload(
637
+ txId: String,
638
+ serializedTransaction: String,
639
+ firstSignerPublicKey: String,
640
+ secondSignerPublicKey: String?,
641
+ description: String?
642
+ ): ByteArray {
643
+ val stream = ByteArrayOutputStream()
644
+
645
+ // Transaction ID TLV (tag 0x01)
646
+ val txIdBytes = txId.toByteArray(Charsets.UTF_8)
647
+ stream.write(0x01)
648
+ stream.write((txIdBytes.size shr 8) and 0xFF)
649
+ stream.write(txIdBytes.size and 0xFF)
650
+ stream.write(txIdBytes)
651
+
652
+ // Serialized transaction TLV (tag 0x02) - base64 encoded
653
+ val txBytes = serializedTransaction.toByteArray(Charsets.UTF_8)
654
+ stream.write(0x02)
655
+ stream.write((txBytes.size shr 8) and 0xFF)
656
+ stream.write(txBytes.size and 0xFF)
657
+ stream.write(txBytes)
658
+
659
+ // First signer public key TLV (tag 0x03)
660
+ val firstSignerBytes = firstSignerPublicKey.toByteArray(Charsets.UTF_8)
661
+ stream.write(0x03)
662
+ stream.write((firstSignerBytes.size shr 8) and 0xFF)
663
+ stream.write(firstSignerBytes.size and 0xFF)
664
+ stream.write(firstSignerBytes)
665
+
666
+ // Second signer public key TLV (tag 0x04) - optional
667
+ if (!secondSignerPublicKey.isNullOrEmpty()) {
668
+ val secondSignerBytes = secondSignerPublicKey.toByteArray(Charsets.UTF_8)
669
+ stream.write(0x04)
670
+ stream.write((secondSignerBytes.size shr 8) and 0xFF)
671
+ stream.write(secondSignerBytes.size and 0xFF)
672
+ stream.write(secondSignerBytes)
673
+ }
674
+
675
+ // Description TLV (tag 0x05) - optional
676
+ if (!description.isNullOrEmpty()) {
677
+ val descBytes = description.toByteArray(Charsets.UTF_8)
678
+ stream.write(0x05)
679
+ stream.write((descBytes.size shr 8) and 0xFF)
680
+ stream.write(descBytes.size and 0xFF)
681
+ stream.write(descBytes)
682
+ }
683
+
684
+ return stream.toByteArray()
685
+ }
686
+
687
+ @ReactMethod
688
+ fun respondToTransaction(
689
+ transactionId: String,
690
+ recipientPeerId: String,
691
+ signedTransaction: String?,
692
+ error: String?,
693
+ promise: Promise
694
+ ) {
695
+ scope.launch {
696
+ try {
697
+ // Build TLV payload for response
698
+ val stream = ByteArrayOutputStream()
699
+
700
+ // Transaction ID TLV (tag 0x01)
701
+ val txIdBytes = transactionId.toByteArray(Charsets.UTF_8)
702
+ stream.write(0x01)
703
+ stream.write((txIdBytes.size shr 8) and 0xFF)
704
+ stream.write(txIdBytes.size and 0xFF)
705
+ stream.write(txIdBytes)
706
+
707
+ // Signed transaction TLV (tag 0x02) - optional, base64 encoded
708
+ if (!signedTransaction.isNullOrEmpty()) {
709
+ val signedTxBytes = signedTransaction.toByteArray(Charsets.UTF_8)
710
+ stream.write(0x02)
711
+ stream.write((signedTxBytes.size shr 8) and 0xFF)
712
+ stream.write(signedTxBytes.size and 0xFF)
713
+ stream.write(signedTxBytes)
714
+ }
715
+
716
+ // Error TLV (tag 0x03) - optional
717
+ if (!error.isNullOrEmpty()) {
718
+ val errorBytes = error.toByteArray(Charsets.UTF_8)
719
+ stream.write(0x03)
720
+ stream.write((errorBytes.size shr 8) and 0xFF)
721
+ stream.write(errorBytes.size and 0xFF)
722
+ stream.write(errorBytes)
723
+ }
724
+
725
+ val payload = byteArrayOf(NoisePayloadType.TRANSACTION_RESPONSE.value) + stream.toByteArray()
726
+
727
+ // Send encrypted response
728
+ val encrypted = encryptPayload(payload, recipientPeerId)
729
+ if (encrypted != null) {
730
+ val packet = createPacket(
731
+ type = MessageType.NOISE_ENCRYPTED.value,
732
+ payload = encrypted,
733
+ recipientId = hexStringToByteArray(recipientPeerId)
734
+ )
735
+ broadcastPacket(packet)
736
+ }
737
+
738
+ withContext(Dispatchers.Main) {
739
+ promise.resolve(null)
740
+ }
741
+ } catch (e: Exception) {
742
+ Log.e(TAG, "Failed to respond to transaction: ${e.message}")
743
+ withContext(Dispatchers.Main) {
744
+ promise.reject("TRANSACTION_RESPONSE_ERROR", e.message)
745
+ }
746
+ }
747
+ }
748
+ }
749
+
750
+ @ReactMethod
751
+ fun sendReadReceipt(messageId: String, recipientPeerId: String, promise: Promise) {
752
+ scope.launch {
753
+ try {
754
+ val payload = byteArrayOf(NoisePayloadType.READ_RECEIPT.value) + messageId.toByteArray(Charsets.UTF_8)
755
+ val encrypted = encryptPayload(payload, recipientPeerId)
756
+ if (encrypted != null) {
757
+ val packet = createPacket(
758
+ type = MessageType.NOISE_ENCRYPTED.value,
759
+ payload = encrypted,
760
+ recipientId = hexStringToByteArray(recipientPeerId)
761
+ )
762
+ broadcastPacket(packet)
763
+ }
764
+ withContext(Dispatchers.Main) {
765
+ promise.resolve(null)
766
+ }
767
+ } catch (e: Exception) {
768
+ withContext(Dispatchers.Main) {
769
+ promise.reject("SEND_ERROR", e.message)
770
+ }
771
+ }
772
+ }
773
+ }
774
+
775
+ @ReactMethod
776
+ fun hasEncryptedSession(peerId: String, promise: Promise) {
777
+ promise.resolve(sessions.containsKey(peerId))
778
+ }
779
+
780
+ @ReactMethod
781
+ fun initiateHandshake(peerId: String, promise: Promise) {
782
+ initiateHandshakeInternal(peerId)
783
+ promise.resolve(null)
784
+ }
785
+
786
+ @ReactMethod
787
+ fun getIdentityFingerprint(promise: Promise) {
788
+ try {
789
+ val digest = MessageDigest.getInstance("SHA-256")
790
+ val hash = digest.digest(privateKey!!.public.encoded)
791
+ val fingerprint = hash.joinToString("") { "%02x".format(it) }
792
+ promise.resolve(fingerprint)
793
+ } catch (e: Exception) {
794
+ promise.resolve(null)
795
+ }
796
+ }
797
+
798
+ @ReactMethod
799
+ fun getPeerFingerprint(peerId: String, promise: Promise) {
800
+ val peer = peers[peerId]
801
+ if (peer?.noisePublicKey != null) {
802
+ try {
803
+ val digest = MessageDigest.getInstance("SHA-256")
804
+ val hash = digest.digest(peer.noisePublicKey)
805
+ val fingerprint = hash.joinToString("") { "%02x".format(it) }
806
+ promise.resolve(fingerprint)
807
+ } catch (e: Exception) {
808
+ promise.resolve(null)
809
+ }
810
+ } else {
811
+ promise.resolve(null)
812
+ }
813
+ }
814
+
815
+ @ReactMethod
816
+ fun broadcastAnnounce(promise: Promise) {
817
+ sendAnnounce()
818
+ promise.resolve(null)
819
+ }
820
+
821
+ @ReactMethod
822
+ fun addListener(eventName: String) {
823
+ // Required for RN event emitter
824
+ }
825
+
826
+ @ReactMethod
827
+ fun removeListeners(count: Int) {
828
+ // Required for RN event emitter
829
+ }
830
+
831
+ // MARK: - BLE Services
832
+
833
+ @SuppressLint("MissingPermission")
834
+ private fun startBleServices() {
835
+ if (!hasBluetoothPermissions()) {
836
+ throw Exception("Bluetooth permissions not granted")
837
+ }
838
+
839
+ bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
840
+ bluetoothLeAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser
841
+
842
+ // Start GATT Server
843
+ startGattServer()
844
+
845
+ // Start advertising
846
+ startAdvertising()
847
+
848
+ // Start scanning
849
+ startScanning()
850
+ }
851
+
852
+ @SuppressLint("MissingPermission")
853
+ private fun stopBleServices() {
854
+ bluetoothLeScanner?.stopScan(scanCallback)
855
+ bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback)
856
+ gattServer?.close()
857
+
858
+ gattConnections.values.forEach { it.close() }
859
+ gattConnections.clear()
860
+ }
861
+
862
+ @SuppressLint("MissingPermission")
863
+ private fun startGattServer() {
864
+ gattServer = bluetoothManager?.openGattServer(reactApplicationContext, gattServerCallback)
865
+
866
+ val service = BluetoothGattService(serviceUUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
867
+
868
+ gattCharacteristic = BluetoothGattCharacteristic(
869
+ characteristicUUID,
870
+ BluetoothGattCharacteristic.PROPERTY_READ or
871
+ BluetoothGattCharacteristic.PROPERTY_WRITE or
872
+ BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or
873
+ BluetoothGattCharacteristic.PROPERTY_NOTIFY,
874
+ BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE
875
+ )
876
+
877
+ val descriptor = BluetoothGattDescriptor(
878
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
879
+ BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
880
+ )
881
+ gattCharacteristic?.addDescriptor(descriptor)
882
+
883
+ service.addCharacteristic(gattCharacteristic)
884
+ gattServer?.addService(service)
885
+ }
886
+
887
+ @SuppressLint("MissingPermission")
888
+ private fun startAdvertising() {
889
+ val settings = AdvertiseSettings.Builder()
890
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
891
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
892
+ .setConnectable(true)
893
+ .build()
894
+
895
+ val data = AdvertiseData.Builder()
896
+ .setIncludeDeviceName(false)
897
+ .addServiceUuid(ParcelUuid(serviceUUID))
898
+ .build()
899
+
900
+ bluetoothLeAdvertiser?.startAdvertising(settings, data, advertiseCallback)
901
+ }
902
+
903
+ @SuppressLint("MissingPermission")
904
+ private fun startScanning() {
905
+ val scanFilter = ScanFilter.Builder()
906
+ .setServiceUuid(ParcelUuid(serviceUUID))
907
+ .build()
908
+
909
+ val scanSettings = ScanSettings.Builder()
910
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
911
+ .build()
912
+
913
+ bluetoothLeScanner?.startScan(listOf(scanFilter), scanSettings, scanCallback)
914
+ }
915
+
916
+ // MARK: - BLE Callbacks
917
+
918
+ private val scanCallback = object : ScanCallback() {
919
+ @SuppressLint("MissingPermission")
920
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
921
+ val device = result.device
922
+ val address = device.address
923
+
924
+ if (!connectedDevices.containsKey(address) && !gattConnections.containsKey(address)) {
925
+ Log.d(TAG, "Discovered device: $address")
926
+ connectedDevices[address] = device
927
+ device.connectGatt(reactApplicationContext, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
928
+ }
929
+ }
930
+
931
+ override fun onScanFailed(errorCode: Int) {
932
+ Log.e(TAG, "Scan failed with error: $errorCode")
933
+ sendErrorEvent("SCAN_ERROR", "Scan failed with error code: $errorCode")
934
+ }
935
+ }
936
+
937
+ private val advertiseCallback = object : AdvertiseCallback() {
938
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
939
+ Log.d(TAG, "Advertising started successfully")
940
+ }
941
+
942
+ override fun onStartFailure(errorCode: Int) {
943
+ Log.e(TAG, "Advertising failed with error: $errorCode")
944
+ sendErrorEvent("ADVERTISE_ERROR", "Advertising failed with error code: $errorCode")
945
+ }
946
+ }
947
+
948
+ private val gattCallback = object : BluetoothGattCallback() {
949
+ @SuppressLint("MissingPermission")
950
+ override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
951
+ val address = gatt.device.address
952
+
953
+ when (newState) {
954
+ BluetoothProfile.STATE_CONNECTED -> {
955
+ Log.d(TAG, "Connected to GATT server: $address")
956
+ gattConnections[address] = gatt
957
+ // Request larger MTU for bigger packets (512 bytes)
958
+ Log.d(TAG, "Requesting MTU 512 for $address")
959
+ gatt.requestMtu(512)
960
+ }
961
+ BluetoothProfile.STATE_DISCONNECTED -> {
962
+ Log.d(TAG, "Disconnected from GATT server: $address")
963
+ gattConnections.remove(address)
964
+ handleDeviceDisconnected(address)
965
+
966
+ // Reconnect
967
+ if (isRunning) {
968
+ connectedDevices[address]?.connectGatt(
969
+ reactApplicationContext, false, this, BluetoothDevice.TRANSPORT_LE
970
+ )
971
+ }
972
+ }
973
+ }
974
+ }
975
+
976
+ @SuppressLint("MissingPermission")
977
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
978
+ Log.d(TAG, "onServicesDiscovered: status=$status for ${gatt.device.address}")
979
+ if (status == BluetoothGatt.GATT_SUCCESS) {
980
+ val service = gatt.getService(serviceUUID)
981
+ Log.d(TAG, "Found service: ${service != null}, UUID: $serviceUUID")
982
+ val characteristic = service?.getCharacteristic(characteristicUUID)
983
+ Log.d(TAG, "Found characteristic: ${characteristic != null}")
984
+
985
+ if (characteristic != null) {
986
+ gatt.setCharacteristicNotification(characteristic, true)
987
+
988
+ val descriptor = characteristic.getDescriptor(
989
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
990
+ )
991
+ Log.d(TAG, "Found descriptor: ${descriptor != null}")
992
+ descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
993
+ gatt.writeDescriptor(descriptor)
994
+
995
+ // Send announce
996
+ Log.d(TAG, "Sending announce after services discovered")
997
+ sendAnnounce()
998
+ } else {
999
+ Log.e(TAG, "Characteristic not found on remote device!")
1000
+ }
1001
+ } else {
1002
+ Log.e(TAG, "Service discovery failed with status: $status")
1003
+ }
1004
+ }
1005
+
1006
+ override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
1007
+ val data = characteristic.value
1008
+ Log.d(TAG, "onCharacteristicChanged: received ${data?.size ?: 0} bytes from ${gatt.device.address}")
1009
+ if (data != null) {
1010
+ handleReceivedPacket(data, gatt.device.address)
1011
+ }
1012
+ }
1013
+
1014
+ @SuppressLint("MissingPermission")
1015
+ override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
1016
+ Log.d(TAG, "MTU changed to $mtu for ${gatt.device.address}, status: $status")
1017
+ if (status == BluetoothGatt.GATT_SUCCESS) {
1018
+ // Now discover services after MTU is set
1019
+ gatt.discoverServices()
1020
+ } else {
1021
+ Log.e(TAG, "MTU negotiation failed, discovering services anyway")
1022
+ gatt.discoverServices()
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ private val gattServerCallback = object : BluetoothGattServerCallback() {
1028
+ @SuppressLint("MissingPermission")
1029
+ override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
1030
+ when (newState) {
1031
+ BluetoothProfile.STATE_CONNECTED -> {
1032
+ Log.d(TAG, "Central connected: ${device.address}")
1033
+ connectedDevices[device.address] = device
1034
+ sendAnnounce()
1035
+ }
1036
+ BluetoothProfile.STATE_DISCONNECTED -> {
1037
+ Log.d(TAG, "Central disconnected: ${device.address}")
1038
+ handleDeviceDisconnected(device.address)
1039
+ }
1040
+ }
1041
+ }
1042
+
1043
+ @SuppressLint("MissingPermission")
1044
+ override fun onCharacteristicWriteRequest(
1045
+ device: BluetoothDevice,
1046
+ requestId: Int,
1047
+ characteristic: BluetoothGattCharacteristic,
1048
+ preparedWrite: Boolean,
1049
+ responseNeeded: Boolean,
1050
+ offset: Int,
1051
+ value: ByteArray
1052
+ ) {
1053
+ Log.d(TAG, "onCharacteristicWriteRequest: ${value.size} bytes from ${device.address}, uuid=${characteristic.uuid}")
1054
+ if (characteristic.uuid == characteristicUUID) {
1055
+ Log.d(TAG, "Characteristic matches, handling packet")
1056
+ handleReceivedPacket(value, device.address)
1057
+ } else {
1058
+ Log.d(TAG, "Characteristic UUID mismatch: expected $characteristicUUID")
1059
+ }
1060
+ if (responseNeeded) {
1061
+ gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
1062
+ }
1063
+ }
1064
+
1065
+ override fun onDescriptorWriteRequest(
1066
+ device: BluetoothDevice,
1067
+ requestId: Int,
1068
+ descriptor: BluetoothGattDescriptor,
1069
+ preparedWrite: Boolean,
1070
+ responseNeeded: Boolean,
1071
+ offset: Int,
1072
+ value: ByteArray
1073
+ ) {
1074
+ if (responseNeeded) {
1075
+ gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ // MARK: - Packet Handling
1081
+
1082
+ private fun handleReceivedPacket(data: ByteArray, fromAddress: String) {
1083
+ Log.d(TAG, "handleReceivedPacket: ${data.size} bytes from $fromAddress")
1084
+ val packet = BitchatPacket.decode(data)
1085
+ if (packet == null) {
1086
+ Log.e(TAG, "Failed to decode packet!")
1087
+ return
1088
+ }
1089
+
1090
+ val senderId = packet.senderId.joinToString("") { "%02x".format(it) }
1091
+ Log.d(TAG, "Packet from sender: $senderId, type: ${packet.type}, myPeerId: $myPeerId")
1092
+
1093
+ // Skip our own packets
1094
+ if (senderId == myPeerId) {
1095
+ Log.d(TAG, "Skipping own packet")
1096
+ return
1097
+ }
1098
+
1099
+ // Deduplication
1100
+ val messageId = "$senderId-${packet.timestamp}-${packet.type}"
1101
+ if (processedMessages.contains(messageId)) {
1102
+ Log.d(TAG, "Duplicate packet, skipping")
1103
+ return
1104
+ }
1105
+ processedMessages.add(messageId)
1106
+
1107
+ val messageType = MessageType.fromValue(packet.type)
1108
+ Log.d(TAG, "Processing packet type: $messageType")
1109
+
1110
+ // Handle by type
1111
+ when (messageType) {
1112
+ MessageType.ANNOUNCE -> handleAnnounce(packet, senderId)
1113
+ MessageType.MESSAGE -> handleMessage(packet, senderId)
1114
+ MessageType.NOISE_HANDSHAKE -> handleNoiseHandshake(packet, senderId)
1115
+ MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(packet, senderId)
1116
+ MessageType.LEAVE -> handleLeave(senderId)
1117
+ MessageType.FILE_TRANSFER -> handleFileTransfer(packet, senderId)
1118
+ MessageType.FRAGMENT -> handleFragment(packet, senderId)
1119
+ MessageType.SOLANA_TRANSACTION -> handleTransactionMetadata(packet, senderId)
1120
+ else -> Log.d(TAG, "Unknown message type: ${packet.type}")
1121
+ }
1122
+
1123
+ // Relay if TTL > 0
1124
+ if (packet.ttl > 0) {
1125
+ val relayPacket = packet.copy(ttl = (packet.ttl - 1).toByte())
1126
+ scope.launch {
1127
+ delay((10L..100L).random())
1128
+ relayPacket(relayPacket.encode(), fromAddress)
1129
+ }
1130
+ }
1131
+ }
1132
+
1133
+ @SuppressLint("MissingPermission")
1134
+ private fun relayPacket(data: ByteArray, excludeAddress: String) {
1135
+ // Write to all GATT connections except source
1136
+ gattConnections.filter { it.key != excludeAddress }.forEach { (_, gatt) ->
1137
+ val service = gatt.getService(serviceUUID)
1138
+ val characteristic = service?.getCharacteristic(characteristicUUID)
1139
+ if (characteristic != null) {
1140
+ characteristic.value = data
1141
+ gatt.writeCharacteristic(characteristic)
1142
+ }
1143
+ }
1144
+
1145
+ // Notify all connected devices via GATT server
1146
+ gattCharacteristic?.let { char ->
1147
+ char.value = data
1148
+ connectedDevices.filter { it.key != excludeAddress }.forEach { (_, device) ->
1149
+ gattServer?.notifyCharacteristicChanged(device, char, false)
1150
+ }
1151
+ }
1152
+ }
1153
+
1154
+ private fun handleAnnounce(packet: BitchatPacket, senderId: String) {
1155
+ Log.d(TAG, "handleAnnounce from $senderId, payload size: ${packet.payload.size}")
1156
+ // Parse TLV payload
1157
+ var nickname = senderId
1158
+ var noisePublicKey: ByteArray? = null
1159
+
1160
+ var offset = 0
1161
+ while (offset < packet.payload.size) {
1162
+ if (offset + 3 > packet.payload.size) break
1163
+
1164
+ val tag = packet.payload[offset]
1165
+ val length = ((packet.payload[offset + 1].toInt() and 0xFF) shl 8) or
1166
+ (packet.payload[offset + 2].toInt() and 0xFF)
1167
+ offset += 3
1168
+
1169
+ if (offset + length > packet.payload.size) break
1170
+ val value = packet.payload.copyOfRange(offset, offset + length)
1171
+ offset += length
1172
+
1173
+ when (tag.toInt()) {
1174
+ 0x01 -> nickname = String(value, Charsets.UTF_8)
1175
+ 0x02 -> noisePublicKey = value
1176
+ }
1177
+ }
1178
+
1179
+ Log.d(TAG, "Parsed announce: nickname=$nickname from peer $senderId")
1180
+
1181
+ peers[senderId] = PeerInfo(
1182
+ peerId = senderId,
1183
+ nickname = nickname,
1184
+ isConnected = true,
1185
+ lastSeen = System.currentTimeMillis(),
1186
+ noisePublicKey = noisePublicKey,
1187
+ isVerified = false
1188
+ )
1189
+
1190
+ Log.d(TAG, "Peer added, total peers: ${peers.size}")
1191
+ notifyPeerListUpdated()
1192
+ }
1193
+
1194
+ private fun handleMessage(packet: BitchatPacket, senderId: String) {
1195
+ val content = String(packet.payload, Charsets.UTF_8)
1196
+ val nickname = peers[senderId]?.nickname ?: senderId
1197
+
1198
+ val message = Arguments.createMap().apply {
1199
+ putString("id", UUID.randomUUID().toString())
1200
+ putString("content", content)
1201
+ putString("senderPeerId", senderId)
1202
+ putString("senderNickname", nickname)
1203
+ putDouble("timestamp", packet.timestamp.toDouble())
1204
+ putBoolean("isPrivate", false)
1205
+ }
1206
+
1207
+ sendEvent("onMessageReceived", Arguments.createMap().apply {
1208
+ putMap("message", message)
1209
+ })
1210
+ }
1211
+
1212
+ private fun handleNoiseHandshake(packet: BitchatPacket, senderId: String) {
1213
+ if (packet.payload.size != 65) return // EC public key size
1214
+
1215
+ try {
1216
+ // Derive shared secret
1217
+ val keyFactory = java.security.KeyFactory.getInstance("EC")
1218
+ val keySpec = java.security.spec.X509EncodedKeySpec(packet.payload)
1219
+ val peerPublicKey = keyFactory.generatePublic(keySpec)
1220
+
1221
+ val keyAgreement = KeyAgreement.getInstance("ECDH")
1222
+ keyAgreement.init(privateKey?.private)
1223
+ keyAgreement.doPhase(peerPublicKey, true)
1224
+
1225
+ val sharedSecret = keyAgreement.generateSecret()
1226
+ val digest = MessageDigest.getInstance("SHA-256")
1227
+ val symmetricKey = digest.digest(sharedSecret)
1228
+
1229
+ sessions[senderId] = symmetricKey
1230
+
1231
+ // Update peer's noise public key
1232
+ peers[senderId]?.let { peer ->
1233
+ peers[senderId] = peer.copy(noisePublicKey = packet.payload)
1234
+ }
1235
+
1236
+ // Send response handshake
1237
+ if (packet.recipientId == null || packet.recipientId.contentEquals(myPeerIdBytes)) {
1238
+ initiateHandshakeInternal(senderId)
1239
+ }
1240
+ } catch (e: Exception) {
1241
+ Log.e(TAG, "Handshake failed: ${e.message}")
1242
+ sendErrorEvent("HANDSHAKE_ERROR", e.message ?: "Unknown error")
1243
+ }
1244
+ }
1245
+
1246
+ private fun handleNoiseEncrypted(packet: BitchatPacket, senderId: String) {
1247
+ // Check if message is for us
1248
+ if (packet.recipientId != null && !packet.recipientId.contentEquals(myPeerIdBytes)) {
1249
+ return
1250
+ }
1251
+
1252
+ val sessionKey = sessions[senderId] ?: return
1253
+ val decrypted = decryptPayload(packet.payload, sessionKey) ?: return
1254
+
1255
+ if (decrypted.isEmpty()) return
1256
+
1257
+ val payloadType = NoisePayloadType.fromValue(decrypted[0])
1258
+ val payloadData = decrypted.copyOfRange(1, decrypted.size)
1259
+
1260
+ when (payloadType) {
1261
+ NoisePayloadType.PRIVATE_MESSAGE -> handlePrivateMessage(payloadData, senderId)
1262
+ NoisePayloadType.READ_RECEIPT -> {
1263
+ val messageId = String(payloadData, Charsets.UTF_8)
1264
+ sendEvent("onReadReceipt", Arguments.createMap().apply {
1265
+ putString("messageId", messageId)
1266
+ putString("fromPeerId", senderId)
1267
+ })
1268
+ }
1269
+ NoisePayloadType.DELIVERY_ACK -> {
1270
+ val messageId = String(payloadData, Charsets.UTF_8)
1271
+ sendEvent("onDeliveryAck", Arguments.createMap().apply {
1272
+ putString("messageId", messageId)
1273
+ putString("fromPeerId", senderId)
1274
+ })
1275
+ }
1276
+ NoisePayloadType.SOLANA_TRANSACTION -> handleSolanaTransaction(payloadData, senderId)
1277
+ NoisePayloadType.TRANSACTION_RESPONSE -> handleTransactionResponse(payloadData, senderId)
1278
+ else -> {}
1279
+ }
1280
+ }
1281
+
1282
+ private fun handlePrivateMessage(data: ByteArray, senderId: String) {
1283
+ // Parse TLV private message
1284
+ var messageId: String? = null
1285
+ var content: String? = null
1286
+
1287
+ var offset = 0
1288
+ while (offset < data.size) {
1289
+ if (offset + 3 > data.size) break
1290
+
1291
+ val tag = data[offset]
1292
+ val length = ((data[offset + 1].toInt() and 0xFF) shl 8) or
1293
+ (data[offset + 2].toInt() and 0xFF)
1294
+ offset += 3
1295
+
1296
+ if (offset + length > data.size) break
1297
+ val value = data.copyOfRange(offset, offset + length)
1298
+ offset += length
1299
+
1300
+ when (tag.toInt()) {
1301
+ 0x01 -> messageId = String(value, Charsets.UTF_8)
1302
+ 0x02 -> content = String(value, Charsets.UTF_8)
1303
+ }
1304
+ }
1305
+
1306
+ if (messageId == null || content == null) return
1307
+
1308
+ val nickname = peers[senderId]?.nickname ?: senderId
1309
+
1310
+ val message = Arguments.createMap().apply {
1311
+ putString("id", messageId)
1312
+ putString("content", content)
1313
+ putString("senderPeerId", senderId)
1314
+ putString("senderNickname", nickname)
1315
+ putDouble("timestamp", System.currentTimeMillis().toDouble())
1316
+ putBoolean("isPrivate", true)
1317
+ }
1318
+
1319
+ sendEvent("onMessageReceived", Arguments.createMap().apply {
1320
+ putMap("message", message)
1321
+ })
1322
+ }
1323
+
1324
+ private fun handleSolanaTransaction(data: ByteArray, senderId: String) {
1325
+ // Parse TLV Solana transaction
1326
+ var txId: String? = null
1327
+ var serializedTransaction: String? = null
1328
+ var firstSignerPublicKey: String? = null
1329
+ var secondSignerPublicKey: String? = null
1330
+ var description: String? = null
1331
+
1332
+ var offset = 0
1333
+ while (offset < data.size) {
1334
+ if (offset + 3 > data.size) break
1335
+
1336
+ val tag = data[offset]
1337
+ val length = ((data[offset + 1].toInt() and 0xFF) shl 8) or
1338
+ (data[offset + 2].toInt() and 0xFF)
1339
+ offset += 3
1340
+
1341
+ if (offset + length > data.size) break
1342
+ val value = data.copyOfRange(offset, offset + length)
1343
+ offset += length
1344
+
1345
+ when (tag.toInt()) {
1346
+ 0x01 -> txId = String(value, Charsets.UTF_8)
1347
+ 0x02 -> serializedTransaction = String(value, Charsets.UTF_8)
1348
+ 0x03 -> firstSignerPublicKey = String(value, Charsets.UTF_8)
1349
+ 0x04 -> secondSignerPublicKey = String(value, Charsets.UTF_8)
1350
+ 0x05 -> description = String(value, Charsets.UTF_8)
1351
+ }
1352
+ }
1353
+
1354
+ if (txId == null || serializedTransaction == null || firstSignerPublicKey == null) {
1355
+ Log.e(TAG, "Invalid Solana transaction data")
1356
+ return
1357
+ }
1358
+
1359
+ val txMap = Arguments.createMap().apply {
1360
+ putString("id", txId)
1361
+ putString("serializedTransaction", serializedTransaction)
1362
+ putString("senderPeerId", senderId)
1363
+ putString("firstSignerPublicKey", firstSignerPublicKey)
1364
+ // Second signer is optional - any peer can sign
1365
+ if (secondSignerPublicKey != null) putString("secondSignerPublicKey", secondSignerPublicKey)
1366
+ if (description != null) putString("description", description)
1367
+ putDouble("timestamp", System.currentTimeMillis().toDouble())
1368
+ putBoolean("requiresSecondSigner", true)
1369
+ putBoolean("openForAnySigner", secondSignerPublicKey == null)
1370
+ }
1371
+
1372
+ sendEvent("onTransactionReceived", Arguments.createMap().apply {
1373
+ putMap("transaction", txMap)
1374
+ })
1375
+
1376
+ Log.d(TAG, "Received Solana transaction $txId from $senderId (openForAnySigner=${secondSignerPublicKey == null})")
1377
+ }
1378
+
1379
+ private fun handleTransactionResponse(data: ByteArray, senderId: String) {
1380
+ // Parse TLV transaction response
1381
+ var txId: String? = null
1382
+ var signedTransaction: String? = null
1383
+ var error: String? = null
1384
+
1385
+ var offset = 0
1386
+ while (offset < data.size) {
1387
+ if (offset + 3 > data.size) break
1388
+
1389
+ val tag = data[offset]
1390
+ val length = ((data[offset + 1].toInt() and 0xFF) shl 8) or
1391
+ (data[offset + 2].toInt() and 0xFF)
1392
+ offset += 3
1393
+
1394
+ if (offset + length > data.size) break
1395
+ val value = data.copyOfRange(offset, offset + length)
1396
+ offset += length
1397
+
1398
+ when (tag.toInt()) {
1399
+ 0x01 -> txId = String(value, Charsets.UTF_8)
1400
+ 0x02 -> signedTransaction = String(value, Charsets.UTF_8)
1401
+ 0x03 -> error = String(value, Charsets.UTF_8)
1402
+ }
1403
+ }
1404
+
1405
+ if (txId == null) {
1406
+ Log.e(TAG, "Invalid transaction response")
1407
+ return
1408
+ }
1409
+
1410
+ val responseMap = Arguments.createMap().apply {
1411
+ putString("id", txId)
1412
+ putString("responderPeerId", senderId)
1413
+ if (signedTransaction != null) putString("signedTransaction", signedTransaction)
1414
+ if (error != null) putString("error", error)
1415
+ putDouble("timestamp", System.currentTimeMillis().toDouble())
1416
+ }
1417
+
1418
+ sendEvent("onTransactionResponse", Arguments.createMap().apply {
1419
+ putMap("response", responseMap)
1420
+ })
1421
+
1422
+ Log.d(TAG, "Received transaction response for $txId from $senderId")
1423
+ }
1424
+
1425
+ private fun handleFileTransfer(packet: BitchatPacket, senderId: String) {
1426
+ // Parse file transfer metadata
1427
+ var transferId: String? = null
1428
+ var fileName: String? = null
1429
+ var fileSize: Int? = null
1430
+ var mimeType: String? = null
1431
+ var totalChunks: Int? = null
1432
+
1433
+ var offset = 0
1434
+ while (offset < packet.payload.size) {
1435
+ if (offset + 3 > packet.payload.size) break
1436
+
1437
+ val tag = packet.payload[offset]
1438
+ val length = ((packet.payload[offset + 1].toInt() and 0xFF) shl 8) or
1439
+ (packet.payload[offset + 2].toInt() and 0xFF)
1440
+ offset += 3
1441
+
1442
+ if (offset + length > packet.payload.size) break
1443
+ val value = packet.payload.copyOfRange(offset, offset + length)
1444
+ offset += length
1445
+
1446
+ when (tag.toInt()) {
1447
+ 0x01 -> transferId = String(value, Charsets.UTF_8)
1448
+ 0x02 -> fileName = String(value, Charsets.UTF_8)
1449
+ 0x03 -> fileSize = value.fold(0) { acc, b -> (acc shl 8) or (b.toInt() and 0xFF) }
1450
+ 0x04 -> mimeType = String(value, Charsets.UTF_8)
1451
+ 0x05 -> totalChunks = value.fold(0) { acc, b -> (acc shl 8) or (b.toInt() and 0xFF) }
1452
+ }
1453
+ }
1454
+
1455
+ if (transferId == null || fileName == null || fileSize == null || mimeType == null || totalChunks == null) {
1456
+ Log.e(TAG, "Invalid file transfer metadata")
1457
+ return
1458
+ }
1459
+
1460
+ // Store transfer info
1461
+ activeFileTransfers[transferId] = FileTransfer(
1462
+ id = transferId,
1463
+ fileName = fileName,
1464
+ fileSize = fileSize,
1465
+ mimeType = mimeType,
1466
+ senderPeerId = senderId,
1467
+ totalChunks = totalChunks,
1468
+ receivedChunks = mutableSetOf()
1469
+ )
1470
+ fileTransferFragments[transferId] = mutableMapOf()
1471
+
1472
+ Log.d(TAG, "Started receiving file: $fileName ($fileSize bytes, $totalChunks chunks)")
1473
+ }
1474
+
1475
+ private fun handleFragment(packet: BitchatPacket, senderId: String) {
1476
+ // Parse fragment - could be file or transaction chunk
1477
+ var id: String? = null
1478
+ var chunkIndex: Int? = null
1479
+ var totalChunks: Int? = null
1480
+ var chunkData: ByteArray? = null
1481
+
1482
+ var offset = 0
1483
+ while (offset < packet.payload.size) {
1484
+ if (offset + 3 > packet.payload.size) break
1485
+
1486
+ val tag = packet.payload[offset]
1487
+ val length = ((packet.payload[offset + 1].toInt() and 0xFF) shl 8) or
1488
+ (packet.payload[offset + 2].toInt() and 0xFF)
1489
+ offset += 3
1490
+
1491
+ if (offset + length > packet.payload.size) break
1492
+ val value = packet.payload.copyOfRange(offset, offset + length)
1493
+ offset += length
1494
+
1495
+ when (tag.toInt()) {
1496
+ 0x01 -> id = String(value, Charsets.UTF_8)
1497
+ 0x02 -> chunkIndex = value.fold(0) { acc, b -> (acc shl 8) or (b.toInt() and 0xFF) }
1498
+ 0x03 -> totalChunks = value.fold(0) { acc, b -> (acc shl 8) or (b.toInt() and 0xFF) }
1499
+ 0x04 -> chunkData = value
1500
+ }
1501
+ }
1502
+
1503
+ if (id == null || chunkIndex == null || totalChunks == null || chunkData == null) {
1504
+ Log.e(TAG, "Invalid fragment")
1505
+ return
1506
+ }
1507
+
1508
+ // Check if this is a file fragment
1509
+ if (activeFileTransfers.containsKey(id)) {
1510
+ handleFileFragment(id, chunkIndex, totalChunks, chunkData)
1511
+ } else if (pendingTransactionChunks.containsKey(id)) {
1512
+ handleTransactionChunk(id, chunkIndex, totalChunks, chunkData)
1513
+ } else {
1514
+ Log.w(TAG, "Received fragment for unknown transfer/transaction: $id")
1515
+ }
1516
+ }
1517
+
1518
+ private fun handleFileFragment(transferId: String, chunkIndex: Int, totalChunks: Int, chunkData: ByteArray) {
1519
+ val transfer = activeFileTransfers[transferId] ?: return
1520
+
1521
+ // Store the fragment
1522
+ fileTransferFragments[transferId]?.set(chunkIndex, chunkData)
1523
+ transfer.receivedChunks.add(chunkIndex)
1524
+
1525
+ Log.d(TAG, "Received file chunk $chunkIndex/$totalChunks for transfer $transferId")
1526
+
1527
+ // Check if we have all chunks
1528
+ if (transfer.receivedChunks.size == transfer.totalChunks) {
1529
+ // Reassemble file
1530
+ val reassembledData = ByteArrayOutputStream()
1531
+ for (i in 0 until transfer.totalChunks) {
1532
+ fileTransferFragments[transferId]?.get(i)?.let { reassembledData.write(it) }
1533
+ }
1534
+
1535
+ // Convert to base64
1536
+ val base64Data = android.util.Base64.encodeToString(reassembledData.toByteArray(), android.util.Base64.DEFAULT)
1537
+
1538
+ // Emit file received event
1539
+ val fileMap = Arguments.createMap().apply {
1540
+ putString("id", transfer.id)
1541
+ putString("fileName", transfer.fileName)
1542
+ putInt("fileSize", transfer.fileSize)
1543
+ putString("mimeType", transfer.mimeType)
1544
+ putString("data", base64Data)
1545
+ putString("senderPeerId", transfer.senderPeerId)
1546
+ putDouble("timestamp", System.currentTimeMillis().toDouble())
1547
+ }
1548
+
1549
+ sendEvent("onFileReceived", Arguments.createMap().apply {
1550
+ putMap("file", fileMap)
1551
+ })
1552
+
1553
+ // Clean up
1554
+ activeFileTransfers.remove(transferId)
1555
+ fileTransferFragments.remove(transferId)
1556
+
1557
+ Log.d(TAG, "File transfer complete: ${transfer.fileName}")
1558
+ }
1559
+ }
1560
+
1561
+ private fun handleTransactionChunk(txId: String, chunkIndex: Int, totalChunks: Int, chunkData: ByteArray) {
1562
+ val pendingTx = pendingTransactionChunks[txId] ?: return
1563
+
1564
+ // Store the chunk
1565
+ pendingTx.chunks[chunkIndex] = chunkData
1566
+
1567
+ Log.d(TAG, "Received transaction chunk $chunkIndex/$totalChunks for tx $txId")
1568
+
1569
+ // Check if we have all chunks
1570
+ if (pendingTx.chunks.size == pendingTx.totalChunks) {
1571
+ // Reassemble encrypted data
1572
+ val reassembledData = ByteArrayOutputStream()
1573
+ for (i in 0 until pendingTx.totalChunks) {
1574
+ pendingTx.chunks[i]?.let { reassembledData.write(it) }
1575
+ }
1576
+
1577
+ // Decrypt and process
1578
+ val sessionKey = sessions[pendingTx.senderId]
1579
+ if (sessionKey != null) {
1580
+ val decrypted = decryptPayload(reassembledData.toByteArray(), sessionKey)
1581
+ if (decrypted != null && decrypted.isNotEmpty()) {
1582
+ // Skip the payload type byte and process as Solana transaction
1583
+ val payloadData = decrypted.copyOfRange(1, decrypted.size)
1584
+ handleSolanaTransaction(payloadData, pendingTx.senderId)
1585
+ }
1586
+ }
1587
+
1588
+ // Clean up
1589
+ pendingTransactionChunks.remove(txId)
1590
+
1591
+ Log.d(TAG, "Transaction receive complete: $txId")
1592
+ }
1593
+ }
1594
+
1595
+ private fun handleTransactionMetadata(packet: BitchatPacket, senderId: String) {
1596
+ // Parse transaction chunk metadata
1597
+ var txId: String? = null
1598
+ var totalSize: Int? = null
1599
+ var totalChunks: Int? = null
1600
+
1601
+ var offset = 0
1602
+ while (offset < packet.payload.size) {
1603
+ if (offset + 3 > packet.payload.size) break
1604
+
1605
+ val tag = packet.payload[offset]
1606
+ val length = ((packet.payload[offset + 1].toInt() and 0xFF) shl 8) or
1607
+ (packet.payload[offset + 2].toInt() and 0xFF)
1608
+ offset += 3
1609
+
1610
+ if (offset + length > packet.payload.size) break
1611
+ val value = packet.payload.copyOfRange(offset, offset + length)
1612
+ offset += length
1613
+
1614
+ when (tag.toInt()) {
1615
+ 0x01 -> txId = String(value, Charsets.UTF_8)
1616
+ 0x02 -> totalSize = value.fold(0) { acc, b -> (acc shl 8) or (b.toInt() and 0xFF) }
1617
+ 0x03 -> totalChunks = value.fold(0) { acc, b -> (acc shl 8) or (b.toInt() and 0xFF) }
1618
+ }
1619
+ }
1620
+
1621
+ if (txId == null || totalSize == null || totalChunks == null) {
1622
+ Log.e(TAG, "Invalid transaction metadata")
1623
+ return
1624
+ }
1625
+
1626
+ // Store pending transaction chunks info
1627
+ pendingTransactionChunks[txId] = TransactionChunks(
1628
+ txId = txId,
1629
+ senderId = senderId,
1630
+ totalSize = totalSize,
1631
+ totalChunks = totalChunks
1632
+ )
1633
+
1634
+ Log.d(TAG, "Started receiving chunked transaction: $txId ($totalSize bytes, $totalChunks chunks)")
1635
+ }
1636
+
1637
+ private data class FileTransfer(
1638
+ val id: String,
1639
+ val fileName: String,
1640
+ val fileSize: Int,
1641
+ val mimeType: String,
1642
+ val senderPeerId: String,
1643
+ val totalChunks: Int,
1644
+ val receivedChunks: MutableSet<Int> = mutableSetOf()
1645
+ )
1646
+
1647
+ private fun handleLeave(senderId: String) {
1648
+ peers.remove(senderId)
1649
+ sessions.remove(senderId)
1650
+ notifyPeerListUpdated()
1651
+ }
1652
+
1653
+ private fun handleDeviceDisconnected(address: String) {
1654
+ val peerId = deviceToPeer[address]
1655
+ if (peerId != null) {
1656
+ peers[peerId]?.let { peer ->
1657
+ peers[peerId] = peer.copy(isConnected = false)
1658
+ }
1659
+ notifyPeerListUpdated()
1660
+ }
1661
+ connectedDevices.remove(address)
1662
+ deviceToPeer.remove(address)
1663
+ }
1664
+
1665
+ // MARK: - Private Methods
1666
+
1667
+ private fun sendAnnounce() {
1668
+ val publicKey = privateKey?.public?.encoded ?: return
1669
+ val signingPublicKey = signingKey?.public?.encoded ?: return
1670
+
1671
+ // Build TLV payload
1672
+ val payload = ByteArrayOutputStream().apply {
1673
+ // Nickname TLV (tag 0x01)
1674
+ val nicknameBytes = myNickname.toByteArray(Charsets.UTF_8)
1675
+ write(0x01)
1676
+ write((nicknameBytes.size shr 8) and 0xFF)
1677
+ write(nicknameBytes.size and 0xFF)
1678
+ write(nicknameBytes)
1679
+
1680
+ // Noise public key TLV (tag 0x02)
1681
+ write(0x02)
1682
+ write((publicKey.size shr 8) and 0xFF)
1683
+ write(publicKey.size and 0xFF)
1684
+ write(publicKey)
1685
+
1686
+ // Signing public key TLV (tag 0x03)
1687
+ write(0x03)
1688
+ write((signingPublicKey.size shr 8) and 0xFF)
1689
+ write(signingPublicKey.size and 0xFF)
1690
+ write(signingPublicKey)
1691
+ }.toByteArray()
1692
+
1693
+ val packet = createPacket(MessageType.ANNOUNCE.value, payload, null)
1694
+ broadcastPacket(packet)
1695
+ }
1696
+
1697
+ private fun sendLeaveAnnouncement() {
1698
+ val packet = createPacket(MessageType.LEAVE.value, ByteArray(0), null)
1699
+ broadcastPacket(packet)
1700
+ }
1701
+
1702
+ private fun initiateHandshakeInternal(peerId: String) {
1703
+ val publicKey = privateKey?.public?.encoded ?: return
1704
+ val packet = createPacket(
1705
+ type = MessageType.NOISE_HANDSHAKE.value,
1706
+ payload = publicKey,
1707
+ recipientId = hexStringToByteArray(peerId)
1708
+ )
1709
+ broadcastPacket(packet)
1710
+ }
1711
+
1712
+ private fun createPacket(type: Byte, payload: ByteArray, recipientId: ByteArray?): BitchatPacket {
1713
+ val timestamp = System.currentTimeMillis().toULong()
1714
+
1715
+ var packet = BitchatPacket(
1716
+ version = 1,
1717
+ type = type,
1718
+ senderId = myPeerIdBytes,
1719
+ recipientId = recipientId,
1720
+ timestamp = timestamp,
1721
+ payload = payload,
1722
+ signature = null,
1723
+ ttl = MESSAGE_TTL
1724
+ )
1725
+
1726
+ // Sign the packet
1727
+ try {
1728
+ val signature = Signature.getInstance("SHA256withECDSA")
1729
+ signature.initSign(signingKey?.private)
1730
+ signature.update(packet.dataForSigning())
1731
+ packet = packet.copy(signature = signature.sign())
1732
+ } catch (e: Exception) {
1733
+ Log.e(TAG, "Failed to sign packet: ${e.message}")
1734
+ }
1735
+
1736
+ return packet
1737
+ }
1738
+
1739
+ @SuppressLint("MissingPermission")
1740
+ private fun broadcastPacket(packet: BitchatPacket) {
1741
+ val data = packet.encode()
1742
+
1743
+ // Write to all GATT connections
1744
+ gattConnections.values.forEach { gatt ->
1745
+ val service = gatt.getService(serviceUUID)
1746
+ val characteristic = service?.getCharacteristic(characteristicUUID)
1747
+ if (characteristic != null) {
1748
+ characteristic.value = data
1749
+ gatt.writeCharacteristic(characteristic)
1750
+ }
1751
+ }
1752
+
1753
+ // Notify all connected devices via GATT server
1754
+ gattCharacteristic?.let { char ->
1755
+ char.value = data
1756
+ connectedDevices.values.forEach { device ->
1757
+ gattServer?.notifyCharacteristicChanged(device, char, false)
1758
+ }
1759
+ }
1760
+ }
1761
+
1762
+ private fun encryptMessage(content: String, peerId: String): ByteArray? {
1763
+ val messageId = UUID.randomUUID().toString()
1764
+
1765
+ // Build TLV payload
1766
+ val tlvData = ByteArrayOutputStream().apply {
1767
+ val idBytes = messageId.toByteArray(Charsets.UTF_8)
1768
+ write(0x01)
1769
+ write((idBytes.size shr 8) and 0xFF)
1770
+ write(idBytes.size and 0xFF)
1771
+ write(idBytes)
1772
+
1773
+ val contentBytes = content.toByteArray(Charsets.UTF_8)
1774
+ write(0x02)
1775
+ write((contentBytes.size shr 8) and 0xFF)
1776
+ write(contentBytes.size and 0xFF)
1777
+ write(contentBytes)
1778
+ }.toByteArray()
1779
+
1780
+ val payload = byteArrayOf(NoisePayloadType.PRIVATE_MESSAGE.value) + tlvData
1781
+ return encryptPayload(payload, peerId)
1782
+ }
1783
+
1784
+ private fun encryptPayload(payload: ByteArray, peerId: String): ByteArray? {
1785
+ val sessionKey = sessions[peerId] ?: return null
1786
+
1787
+ return try {
1788
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
1789
+ val nonce = ByteArray(12)
1790
+ SecureRandom().nextBytes(nonce)
1791
+ val spec = GCMParameterSpec(128, nonce)
1792
+ cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sessionKey, "AES"), spec)
1793
+ val encrypted = cipher.doFinal(payload)
1794
+ nonce + encrypted
1795
+ } catch (e: Exception) {
1796
+ Log.e(TAG, "Encryption failed: ${e.message}")
1797
+ null
1798
+ }
1799
+ }
1800
+
1801
+ private fun decryptPayload(encrypted: ByteArray, sessionKey: ByteArray): ByteArray? {
1802
+ if (encrypted.size < 12) return null
1803
+
1804
+ return try {
1805
+ val nonce = encrypted.copyOfRange(0, 12)
1806
+ val ciphertext = encrypted.copyOfRange(12, encrypted.size)
1807
+
1808
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
1809
+ val spec = GCMParameterSpec(128, nonce)
1810
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sessionKey, "AES"), spec)
1811
+ cipher.doFinal(ciphertext)
1812
+ } catch (e: Exception) {
1813
+ Log.e(TAG, "Decryption failed: ${e.message}")
1814
+ null
1815
+ }
1816
+ }
1817
+
1818
+ private fun notifyPeerListUpdated() {
1819
+ val peersArray = Arguments.createArray()
1820
+ peers.values.forEach { peer ->
1821
+ peersArray.pushMap(Arguments.createMap().apply {
1822
+ putString("peerId", peer.peerId)
1823
+ putString("nickname", peer.nickname)
1824
+ putBoolean("isConnected", peer.isConnected)
1825
+ peer.rssi?.let { putInt("rssi", it) }
1826
+ putDouble("lastSeen", peer.lastSeen.toDouble())
1827
+ putBoolean("isVerified", peer.isVerified)
1828
+ })
1829
+ }
1830
+
1831
+ sendEvent("onPeerListUpdated", Arguments.createMap().apply {
1832
+ putArray("peers", peersArray)
1833
+ })
1834
+ }
1835
+
1836
+ private fun sendEvent(eventName: String, params: WritableMap) {
1837
+ reactApplicationContext
1838
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
1839
+ .emit(eventName, params)
1840
+ }
1841
+
1842
+ private fun sendErrorEvent(code: String, message: String) {
1843
+ sendEvent("onError", Arguments.createMap().apply {
1844
+ putString("code", code)
1845
+ putString("message", message)
1846
+ })
1847
+ }
1848
+
1849
+ // MARK: - Permission Helpers
1850
+
1851
+ private fun hasBluetoothPermissions(): Boolean {
1852
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1853
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH_ADVERTISE) == PackageManager.PERMISSION_GRANTED &&
1854
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED &&
1855
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED
1856
+ } else {
1857
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED &&
1858
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED
1859
+ }
1860
+ }
1861
+
1862
+ private fun hasLocationPermission(): Boolean {
1863
+ return ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
1864
+ }
1865
+
1866
+ private fun hexStringToByteArray(hex: String): ByteArray {
1867
+ val result = ByteArray(8)
1868
+ var index = 0
1869
+ var hexIndex = 0
1870
+ while (hexIndex < hex.length && index < 8) {
1871
+ val byte = hex.substring(hexIndex, minOf(hexIndex + 2, hex.length)).toIntOrNull(16) ?: 0
1872
+ result[index] = byte.toByte()
1873
+ hexIndex += 2
1874
+ index++
1875
+ }
1876
+ return result
1877
+ }
1878
+
1879
+ // MARK: - Supporting Types
1880
+
1881
+ enum class MessageType(val value: Byte) {
1882
+ ANNOUNCE(0x01),
1883
+ MESSAGE(0x02),
1884
+ LEAVE(0x03),
1885
+ NOISE_HANDSHAKE(0x04),
1886
+ NOISE_ENCRYPTED(0x05),
1887
+ FILE_TRANSFER(0x06),
1888
+ FRAGMENT(0x07),
1889
+ REQUEST_SYNC(0x08),
1890
+ SOLANA_TRANSACTION(0x09);
1891
+
1892
+ companion object {
1893
+ fun fromValue(value: Byte): MessageType? = values().find { it.value == value }
1894
+ }
1895
+ }
1896
+
1897
+ enum class NoisePayloadType(val value: Byte) {
1898
+ PRIVATE_MESSAGE(0x01),
1899
+ READ_RECEIPT(0x02),
1900
+ DELIVERY_ACK(0x03),
1901
+ FILE_TRANSFER(0x04),
1902
+ VERIFY_CHALLENGE(0x05),
1903
+ VERIFY_RESPONSE(0x06),
1904
+ SOLANA_TRANSACTION(0x07),
1905
+ TRANSACTION_RESPONSE(0x08);
1906
+
1907
+ companion object {
1908
+ fun fromValue(value: Byte): NoisePayloadType? = values().find { it.value == value }
1909
+ }
1910
+ }
1911
+
1912
+ data class BitchatPacket(
1913
+ val version: Byte = 1,
1914
+ val type: Byte,
1915
+ val senderId: ByteArray,
1916
+ val recipientId: ByteArray?,
1917
+ val timestamp: ULong,
1918
+ val payload: ByteArray,
1919
+ val signature: ByteArray?,
1920
+ val ttl: Byte
1921
+ ) {
1922
+ fun dataForSigning(): ByteArray {
1923
+ val buffer = ByteBuffer.allocate(1 + 1 + 8 + 8 + 8 + payload.size + 1)
1924
+ buffer.order(ByteOrder.BIG_ENDIAN)
1925
+ buffer.put(version)
1926
+ buffer.put(type)
1927
+ buffer.put(senderId.copyOf(8))
1928
+ buffer.put(recipientId?.copyOf(8) ?: ByteArray(8))
1929
+ buffer.putLong(timestamp.toLong())
1930
+ buffer.put(payload)
1931
+ buffer.put(ttl)
1932
+ return buffer.array()
1933
+ }
1934
+
1935
+ fun encode(): ByteArray {
1936
+ val buffer = ByteBuffer.allocate(29 + payload.size + (signature?.size ?: 0))
1937
+ buffer.order(ByteOrder.BIG_ENDIAN)
1938
+ buffer.put(version)
1939
+ buffer.put(type)
1940
+ buffer.put(ttl)
1941
+ buffer.put(senderId.copyOf(8))
1942
+ buffer.put(recipientId?.copyOf(8) ?: ByteArray(8))
1943
+ buffer.putLong(timestamp.toLong())
1944
+ buffer.putShort(payload.size.toShort())
1945
+ buffer.put(payload)
1946
+ signature?.let { buffer.put(it) }
1947
+ return buffer.array()
1948
+ }
1949
+
1950
+ companion object {
1951
+ fun decode(data: ByteArray): BitchatPacket? {
1952
+ if (data.size < 29) return null
1953
+
1954
+ val buffer = ByteBuffer.wrap(data)
1955
+ buffer.order(ByteOrder.BIG_ENDIAN)
1956
+
1957
+ val version = buffer.get()
1958
+ val type = buffer.get()
1959
+ val ttl = buffer.get()
1960
+
1961
+ val senderId = ByteArray(8)
1962
+ buffer.get(senderId)
1963
+
1964
+ val recipientIdBytes = ByteArray(8)
1965
+ buffer.get(recipientIdBytes)
1966
+ val recipientId = if (recipientIdBytes.all { it == 0.toByte() }) null else recipientIdBytes
1967
+
1968
+ val timestamp = buffer.long.toULong()
1969
+ val payloadLength = buffer.short.toInt() and 0xFFFF
1970
+
1971
+ if (buffer.remaining() < payloadLength) return null
1972
+ val payload = ByteArray(payloadLength)
1973
+ buffer.get(payload)
1974
+
1975
+ val signature = if (buffer.remaining() >= 64) {
1976
+ ByteArray(64).also { buffer.get(it) }
1977
+ } else null
1978
+
1979
+ return BitchatPacket(
1980
+ version = version,
1981
+ type = type,
1982
+ senderId = senderId,
1983
+ recipientId = recipientId,
1984
+ timestamp = timestamp,
1985
+ payload = payload,
1986
+ signature = signature,
1987
+ ttl = ttl
1988
+ )
1989
+ }
1990
+ }
1991
+ }
1992
+ }
1993
+
1994
+ private class ByteArrayOutputStream : java.io.ByteArrayOutputStream()