kard-network-ble-mesh 1.0.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,1143 @@
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
+ // Coroutines
76
+ private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
77
+
78
+ override fun getName(): String = "BleMesh"
79
+
80
+ init {
81
+ bluetoothManager = reactApplicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
82
+ bluetoothAdapter = bluetoothManager?.adapter
83
+ generateIdentity()
84
+ }
85
+
86
+ override fun getConstants(): Map<String, Any> = mapOf(
87
+ "SERVICE_UUID" to serviceUUID.toString(),
88
+ "CHARACTERISTIC_UUID" to characteristicUUID.toString()
89
+ )
90
+
91
+ // MARK: - Identity
92
+
93
+ private fun generateIdentity() {
94
+ try {
95
+ val keyGen = KeyPairGenerator.getInstance("EC")
96
+ keyGen.initialize(256)
97
+
98
+ // Load or generate keys
99
+ val prefs = reactApplicationContext.getSharedPreferences("blemesh", Context.MODE_PRIVATE)
100
+
101
+ val savedPrivateKey = prefs.getString("privateKey", null)
102
+ if (savedPrivateKey != null) {
103
+ // TODO: Properly load saved key
104
+ }
105
+
106
+ privateKey = keyGen.generateKeyPair()
107
+ signingKey = keyGen.generateKeyPair()
108
+
109
+ // Generate peer ID from public key fingerprint
110
+ val digest = MessageDigest.getInstance("SHA-256")
111
+ val hash = digest.digest(privateKey!!.public.encoded)
112
+ myPeerIdBytes = hash.copyOf(8)
113
+ myPeerId = myPeerIdBytes.joinToString("") { "%02x".format(it) }
114
+
115
+ Log.d(TAG, "Generated peer ID: $myPeerId")
116
+ } catch (e: Exception) {
117
+ Log.e(TAG, "Failed to generate identity: ${e.message}")
118
+ }
119
+ }
120
+
121
+ // MARK: - React Native API
122
+
123
+ @ReactMethod
124
+ fun requestPermissions(promise: Promise) {
125
+ // Android permissions are handled at the JS layer via PermissionsAndroid
126
+ // This just checks the current state
127
+ val hasPermissions = hasBluetoothPermissions()
128
+ val result = Arguments.createMap().apply {
129
+ putBoolean("bluetooth", hasPermissions)
130
+ putBoolean("location", hasLocationPermission())
131
+ }
132
+ promise.resolve(result)
133
+ }
134
+
135
+ @ReactMethod
136
+ fun checkPermissions(promise: Promise) {
137
+ val result = Arguments.createMap().apply {
138
+ putBoolean("bluetooth", hasBluetoothPermissions())
139
+ putBoolean("location", hasLocationPermission())
140
+ }
141
+ promise.resolve(result)
142
+ }
143
+
144
+ @ReactMethod
145
+ fun start(nickname: String, promise: Promise) {
146
+ myNickname = nickname
147
+
148
+ scope.launch {
149
+ try {
150
+ startBleServices()
151
+ isRunning = true
152
+ withContext(Dispatchers.Main) {
153
+ promise.resolve(null)
154
+ }
155
+ } catch (e: Exception) {
156
+ withContext(Dispatchers.Main) {
157
+ promise.reject("START_ERROR", e.message)
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ @ReactMethod
164
+ fun stop(promise: Promise) {
165
+ scope.launch {
166
+ try {
167
+ sendLeaveAnnouncement()
168
+ stopBleServices()
169
+ isRunning = false
170
+ peers.clear()
171
+ connectedDevices.clear()
172
+ deviceToPeer.clear()
173
+ withContext(Dispatchers.Main) {
174
+ promise.resolve(null)
175
+ }
176
+ } catch (e: Exception) {
177
+ withContext(Dispatchers.Main) {
178
+ promise.reject("STOP_ERROR", e.message)
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ @ReactMethod
185
+ fun setNickname(nickname: String, promise: Promise) {
186
+ myNickname = nickname
187
+ sendAnnounce()
188
+ promise.resolve(null)
189
+ }
190
+
191
+ @ReactMethod
192
+ fun getMyPeerId(promise: Promise) {
193
+ promise.resolve(myPeerId)
194
+ }
195
+
196
+ @ReactMethod
197
+ fun getMyNickname(promise: Promise) {
198
+ promise.resolve(myNickname)
199
+ }
200
+
201
+ @ReactMethod
202
+ fun getPeers(promise: Promise) {
203
+ val peersArray = Arguments.createArray()
204
+ peers.values.forEach { peer ->
205
+ peersArray.pushMap(Arguments.createMap().apply {
206
+ putString("peerId", peer.peerId)
207
+ putString("nickname", peer.nickname)
208
+ putBoolean("isConnected", peer.isConnected)
209
+ peer.rssi?.let { putInt("rssi", it) }
210
+ putDouble("lastSeen", peer.lastSeen.toDouble())
211
+ putBoolean("isVerified", peer.isVerified)
212
+ })
213
+ }
214
+ promise.resolve(peersArray)
215
+ }
216
+
217
+ @ReactMethod
218
+ fun sendMessage(content: String, channel: String?, promise: Promise) {
219
+ val messageId = UUID.randomUUID().toString()
220
+
221
+ scope.launch {
222
+ try {
223
+ val packet = createPacket(
224
+ type = MessageType.MESSAGE.value,
225
+ payload = content.toByteArray(Charsets.UTF_8),
226
+ recipientId = null
227
+ )
228
+ broadcastPacket(packet)
229
+ withContext(Dispatchers.Main) {
230
+ promise.resolve(messageId)
231
+ }
232
+ } catch (e: Exception) {
233
+ withContext(Dispatchers.Main) {
234
+ promise.reject("SEND_ERROR", e.message)
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ @ReactMethod
241
+ fun sendPrivateMessage(content: String, recipientPeerId: String, promise: Promise) {
242
+ val messageId = UUID.randomUUID().toString()
243
+
244
+ scope.launch {
245
+ try {
246
+ if (sessions.containsKey(recipientPeerId)) {
247
+ val encrypted = encryptMessage(content, recipientPeerId)
248
+ if (encrypted != null) {
249
+ val packet = createPacket(
250
+ type = MessageType.NOISE_ENCRYPTED.value,
251
+ payload = encrypted,
252
+ recipientId = hexStringToByteArray(recipientPeerId)
253
+ )
254
+ broadcastPacket(packet)
255
+ }
256
+ } else {
257
+ initiateHandshakeInternal(recipientPeerId)
258
+ }
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 sendFile(filePath: String, recipientPeerId: String?, channel: String?, promise: Promise) {
272
+ val transferId = UUID.randomUUID().toString()
273
+ // TODO: Implement file transfer
274
+ promise.resolve(transferId)
275
+ }
276
+
277
+ @ReactMethod
278
+ fun sendReadReceipt(messageId: String, recipientPeerId: String, promise: Promise) {
279
+ scope.launch {
280
+ try {
281
+ val payload = byteArrayOf(NoisePayloadType.READ_RECEIPT.value) + messageId.toByteArray(Charsets.UTF_8)
282
+ val encrypted = encryptPayload(payload, recipientPeerId)
283
+ if (encrypted != null) {
284
+ val packet = createPacket(
285
+ type = MessageType.NOISE_ENCRYPTED.value,
286
+ payload = encrypted,
287
+ recipientId = hexStringToByteArray(recipientPeerId)
288
+ )
289
+ broadcastPacket(packet)
290
+ }
291
+ withContext(Dispatchers.Main) {
292
+ promise.resolve(null)
293
+ }
294
+ } catch (e: Exception) {
295
+ withContext(Dispatchers.Main) {
296
+ promise.reject("SEND_ERROR", e.message)
297
+ }
298
+ }
299
+ }
300
+ }
301
+
302
+ @ReactMethod
303
+ fun hasEncryptedSession(peerId: String, promise: Promise) {
304
+ promise.resolve(sessions.containsKey(peerId))
305
+ }
306
+
307
+ @ReactMethod
308
+ fun initiateHandshake(peerId: String, promise: Promise) {
309
+ initiateHandshakeInternal(peerId)
310
+ promise.resolve(null)
311
+ }
312
+
313
+ @ReactMethod
314
+ fun getIdentityFingerprint(promise: Promise) {
315
+ try {
316
+ val digest = MessageDigest.getInstance("SHA-256")
317
+ val hash = digest.digest(privateKey!!.public.encoded)
318
+ val fingerprint = hash.joinToString("") { "%02x".format(it) }
319
+ promise.resolve(fingerprint)
320
+ } catch (e: Exception) {
321
+ promise.resolve(null)
322
+ }
323
+ }
324
+
325
+ @ReactMethod
326
+ fun getPeerFingerprint(peerId: String, promise: Promise) {
327
+ val peer = peers[peerId]
328
+ if (peer?.noisePublicKey != null) {
329
+ try {
330
+ val digest = MessageDigest.getInstance("SHA-256")
331
+ val hash = digest.digest(peer.noisePublicKey)
332
+ val fingerprint = hash.joinToString("") { "%02x".format(it) }
333
+ promise.resolve(fingerprint)
334
+ } catch (e: Exception) {
335
+ promise.resolve(null)
336
+ }
337
+ } else {
338
+ promise.resolve(null)
339
+ }
340
+ }
341
+
342
+ @ReactMethod
343
+ fun broadcastAnnounce(promise: Promise) {
344
+ sendAnnounce()
345
+ promise.resolve(null)
346
+ }
347
+
348
+ @ReactMethod
349
+ fun addListener(eventName: String) {
350
+ // Required for RN event emitter
351
+ }
352
+
353
+ @ReactMethod
354
+ fun removeListeners(count: Int) {
355
+ // Required for RN event emitter
356
+ }
357
+
358
+ // MARK: - BLE Services
359
+
360
+ @SuppressLint("MissingPermission")
361
+ private fun startBleServices() {
362
+ if (!hasBluetoothPermissions()) {
363
+ throw Exception("Bluetooth permissions not granted")
364
+ }
365
+
366
+ bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner
367
+ bluetoothLeAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser
368
+
369
+ // Start GATT Server
370
+ startGattServer()
371
+
372
+ // Start advertising
373
+ startAdvertising()
374
+
375
+ // Start scanning
376
+ startScanning()
377
+ }
378
+
379
+ @SuppressLint("MissingPermission")
380
+ private fun stopBleServices() {
381
+ bluetoothLeScanner?.stopScan(scanCallback)
382
+ bluetoothLeAdvertiser?.stopAdvertising(advertiseCallback)
383
+ gattServer?.close()
384
+
385
+ gattConnections.values.forEach { it.close() }
386
+ gattConnections.clear()
387
+ }
388
+
389
+ @SuppressLint("MissingPermission")
390
+ private fun startGattServer() {
391
+ gattServer = bluetoothManager?.openGattServer(reactApplicationContext, gattServerCallback)
392
+
393
+ val service = BluetoothGattService(serviceUUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
394
+
395
+ gattCharacteristic = BluetoothGattCharacteristic(
396
+ characteristicUUID,
397
+ BluetoothGattCharacteristic.PROPERTY_READ or
398
+ BluetoothGattCharacteristic.PROPERTY_WRITE or
399
+ BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE or
400
+ BluetoothGattCharacteristic.PROPERTY_NOTIFY,
401
+ BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE
402
+ )
403
+
404
+ val descriptor = BluetoothGattDescriptor(
405
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"),
406
+ BluetoothGattDescriptor.PERMISSION_READ or BluetoothGattDescriptor.PERMISSION_WRITE
407
+ )
408
+ gattCharacteristic?.addDescriptor(descriptor)
409
+
410
+ service.addCharacteristic(gattCharacteristic)
411
+ gattServer?.addService(service)
412
+ }
413
+
414
+ @SuppressLint("MissingPermission")
415
+ private fun startAdvertising() {
416
+ val settings = AdvertiseSettings.Builder()
417
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
418
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
419
+ .setConnectable(true)
420
+ .build()
421
+
422
+ val data = AdvertiseData.Builder()
423
+ .setIncludeDeviceName(false)
424
+ .addServiceUuid(ParcelUuid(serviceUUID))
425
+ .build()
426
+
427
+ bluetoothLeAdvertiser?.startAdvertising(settings, data, advertiseCallback)
428
+ }
429
+
430
+ @SuppressLint("MissingPermission")
431
+ private fun startScanning() {
432
+ val scanFilter = ScanFilter.Builder()
433
+ .setServiceUuid(ParcelUuid(serviceUUID))
434
+ .build()
435
+
436
+ val scanSettings = ScanSettings.Builder()
437
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
438
+ .build()
439
+
440
+ bluetoothLeScanner?.startScan(listOf(scanFilter), scanSettings, scanCallback)
441
+ }
442
+
443
+ // MARK: - BLE Callbacks
444
+
445
+ private val scanCallback = object : ScanCallback() {
446
+ @SuppressLint("MissingPermission")
447
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
448
+ val device = result.device
449
+ val address = device.address
450
+
451
+ if (!connectedDevices.containsKey(address) && !gattConnections.containsKey(address)) {
452
+ Log.d(TAG, "Discovered device: $address")
453
+ connectedDevices[address] = device
454
+ device.connectGatt(reactApplicationContext, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
455
+ }
456
+ }
457
+
458
+ override fun onScanFailed(errorCode: Int) {
459
+ Log.e(TAG, "Scan failed with error: $errorCode")
460
+ sendErrorEvent("SCAN_ERROR", "Scan failed with error code: $errorCode")
461
+ }
462
+ }
463
+
464
+ private val advertiseCallback = object : AdvertiseCallback() {
465
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
466
+ Log.d(TAG, "Advertising started successfully")
467
+ }
468
+
469
+ override fun onStartFailure(errorCode: Int) {
470
+ Log.e(TAG, "Advertising failed with error: $errorCode")
471
+ sendErrorEvent("ADVERTISE_ERROR", "Advertising failed with error code: $errorCode")
472
+ }
473
+ }
474
+
475
+ private val gattCallback = object : BluetoothGattCallback() {
476
+ @SuppressLint("MissingPermission")
477
+ override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
478
+ val address = gatt.device.address
479
+
480
+ when (newState) {
481
+ BluetoothProfile.STATE_CONNECTED -> {
482
+ Log.d(TAG, "Connected to GATT server: $address")
483
+ gattConnections[address] = gatt
484
+ gatt.discoverServices()
485
+ }
486
+ BluetoothProfile.STATE_DISCONNECTED -> {
487
+ Log.d(TAG, "Disconnected from GATT server: $address")
488
+ gattConnections.remove(address)
489
+ handleDeviceDisconnected(address)
490
+
491
+ // Reconnect
492
+ if (isRunning) {
493
+ connectedDevices[address]?.connectGatt(
494
+ reactApplicationContext, false, this, BluetoothDevice.TRANSPORT_LE
495
+ )
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ @SuppressLint("MissingPermission")
502
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
503
+ if (status == BluetoothGatt.GATT_SUCCESS) {
504
+ val service = gatt.getService(serviceUUID)
505
+ val characteristic = service?.getCharacteristic(characteristicUUID)
506
+
507
+ if (characteristic != null) {
508
+ gatt.setCharacteristicNotification(characteristic, true)
509
+
510
+ val descriptor = characteristic.getDescriptor(
511
+ UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
512
+ )
513
+ descriptor?.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
514
+ gatt.writeDescriptor(descriptor)
515
+
516
+ // Send announce
517
+ sendAnnounce()
518
+ }
519
+ }
520
+ }
521
+
522
+ override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
523
+ val data = characteristic.value
524
+ if (data != null) {
525
+ handleReceivedPacket(data, gatt.device.address)
526
+ }
527
+ }
528
+ }
529
+
530
+ private val gattServerCallback = object : BluetoothGattServerCallback() {
531
+ @SuppressLint("MissingPermission")
532
+ override fun onConnectionStateChange(device: BluetoothDevice, status: Int, newState: Int) {
533
+ when (newState) {
534
+ BluetoothProfile.STATE_CONNECTED -> {
535
+ Log.d(TAG, "Central connected: ${device.address}")
536
+ connectedDevices[device.address] = device
537
+ sendAnnounce()
538
+ }
539
+ BluetoothProfile.STATE_DISCONNECTED -> {
540
+ Log.d(TAG, "Central disconnected: ${device.address}")
541
+ handleDeviceDisconnected(device.address)
542
+ }
543
+ }
544
+ }
545
+
546
+ @SuppressLint("MissingPermission")
547
+ override fun onCharacteristicWriteRequest(
548
+ device: BluetoothDevice,
549
+ requestId: Int,
550
+ characteristic: BluetoothGattCharacteristic,
551
+ preparedWrite: Boolean,
552
+ responseNeeded: Boolean,
553
+ offset: Int,
554
+ value: ByteArray
555
+ ) {
556
+ if (characteristic.uuid == characteristicUUID) {
557
+ handleReceivedPacket(value, device.address)
558
+ }
559
+ if (responseNeeded) {
560
+ gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
561
+ }
562
+ }
563
+
564
+ override fun onDescriptorWriteRequest(
565
+ device: BluetoothDevice,
566
+ requestId: Int,
567
+ descriptor: BluetoothGattDescriptor,
568
+ preparedWrite: Boolean,
569
+ responseNeeded: Boolean,
570
+ offset: Int,
571
+ value: ByteArray
572
+ ) {
573
+ if (responseNeeded) {
574
+ gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null)
575
+ }
576
+ }
577
+ }
578
+
579
+ // MARK: - Packet Handling
580
+
581
+ private fun handleReceivedPacket(data: ByteArray, fromAddress: String) {
582
+ val packet = BitchatPacket.decode(data) ?: return
583
+
584
+ val senderId = packet.senderId.joinToString("") { "%02x".format(it) }
585
+
586
+ // Skip our own packets
587
+ if (senderId == myPeerId) return
588
+
589
+ // Deduplication
590
+ val messageId = "$senderId-${packet.timestamp}-${packet.type}"
591
+ if (processedMessages.contains(messageId)) return
592
+ processedMessages.add(messageId)
593
+
594
+ // Handle by type
595
+ when (MessageType.fromValue(packet.type)) {
596
+ MessageType.ANNOUNCE -> handleAnnounce(packet, senderId)
597
+ MessageType.MESSAGE -> handleMessage(packet, senderId)
598
+ MessageType.NOISE_HANDSHAKE -> handleNoiseHandshake(packet, senderId)
599
+ MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(packet, senderId)
600
+ MessageType.LEAVE -> handleLeave(senderId)
601
+ else -> {}
602
+ }
603
+
604
+ // Relay if TTL > 0
605
+ if (packet.ttl > 0) {
606
+ val relayPacket = packet.copy(ttl = (packet.ttl - 1).toByte())
607
+ scope.launch {
608
+ delay((10L..100L).random())
609
+ relayPacket(relayPacket.encode(), fromAddress)
610
+ }
611
+ }
612
+ }
613
+
614
+ @SuppressLint("MissingPermission")
615
+ private fun relayPacket(data: ByteArray, excludeAddress: String) {
616
+ // Write to all GATT connections except source
617
+ gattConnections.filter { it.key != excludeAddress }.forEach { (_, gatt) ->
618
+ val service = gatt.getService(serviceUUID)
619
+ val characteristic = service?.getCharacteristic(characteristicUUID)
620
+ if (characteristic != null) {
621
+ characteristic.value = data
622
+ gatt.writeCharacteristic(characteristic)
623
+ }
624
+ }
625
+
626
+ // Notify all connected devices via GATT server
627
+ gattCharacteristic?.let { char ->
628
+ char.value = data
629
+ connectedDevices.filter { it.key != excludeAddress }.forEach { (_, device) ->
630
+ gattServer?.notifyCharacteristicChanged(device, char, false)
631
+ }
632
+ }
633
+ }
634
+
635
+ private fun handleAnnounce(packet: BitchatPacket, senderId: String) {
636
+ // Parse TLV payload
637
+ var nickname = senderId
638
+ var noisePublicKey: ByteArray? = null
639
+
640
+ var offset = 0
641
+ while (offset < packet.payload.size) {
642
+ if (offset + 3 > packet.payload.size) break
643
+
644
+ val tag = packet.payload[offset]
645
+ val length = ((packet.payload[offset + 1].toInt() and 0xFF) shl 8) or
646
+ (packet.payload[offset + 2].toInt() and 0xFF)
647
+ offset += 3
648
+
649
+ if (offset + length > packet.payload.size) break
650
+ val value = packet.payload.copyOfRange(offset, offset + length)
651
+ offset += length
652
+
653
+ when (tag.toInt()) {
654
+ 0x01 -> nickname = String(value, Charsets.UTF_8)
655
+ 0x02 -> noisePublicKey = value
656
+ }
657
+ }
658
+
659
+ peers[senderId] = PeerInfo(
660
+ peerId = senderId,
661
+ nickname = nickname,
662
+ isConnected = true,
663
+ lastSeen = System.currentTimeMillis(),
664
+ noisePublicKey = noisePublicKey,
665
+ isVerified = false
666
+ )
667
+
668
+ notifyPeerListUpdated()
669
+ }
670
+
671
+ private fun handleMessage(packet: BitchatPacket, senderId: String) {
672
+ val content = String(packet.payload, Charsets.UTF_8)
673
+ val nickname = peers[senderId]?.nickname ?: senderId
674
+
675
+ val message = Arguments.createMap().apply {
676
+ putString("id", UUID.randomUUID().toString())
677
+ putString("content", content)
678
+ putString("senderPeerId", senderId)
679
+ putString("senderNickname", nickname)
680
+ putDouble("timestamp", packet.timestamp.toDouble())
681
+ putBoolean("isPrivate", false)
682
+ }
683
+
684
+ sendEvent("onMessageReceived", Arguments.createMap().apply {
685
+ putMap("message", message)
686
+ })
687
+ }
688
+
689
+ private fun handleNoiseHandshake(packet: BitchatPacket, senderId: String) {
690
+ if (packet.payload.size != 65) return // EC public key size
691
+
692
+ try {
693
+ // Derive shared secret
694
+ val keyFactory = java.security.KeyFactory.getInstance("EC")
695
+ val keySpec = java.security.spec.X509EncodedKeySpec(packet.payload)
696
+ val peerPublicKey = keyFactory.generatePublic(keySpec)
697
+
698
+ val keyAgreement = KeyAgreement.getInstance("ECDH")
699
+ keyAgreement.init(privateKey?.private)
700
+ keyAgreement.doPhase(peerPublicKey, true)
701
+
702
+ val sharedSecret = keyAgreement.generateSecret()
703
+ val digest = MessageDigest.getInstance("SHA-256")
704
+ val symmetricKey = digest.digest(sharedSecret)
705
+
706
+ sessions[senderId] = symmetricKey
707
+
708
+ // Update peer's noise public key
709
+ peers[senderId]?.let { peer ->
710
+ peers[senderId] = peer.copy(noisePublicKey = packet.payload)
711
+ }
712
+
713
+ // Send response handshake
714
+ if (packet.recipientId == null || packet.recipientId.contentEquals(myPeerIdBytes)) {
715
+ initiateHandshakeInternal(senderId)
716
+ }
717
+ } catch (e: Exception) {
718
+ Log.e(TAG, "Handshake failed: ${e.message}")
719
+ sendErrorEvent("HANDSHAKE_ERROR", e.message ?: "Unknown error")
720
+ }
721
+ }
722
+
723
+ private fun handleNoiseEncrypted(packet: BitchatPacket, senderId: String) {
724
+ // Check if message is for us
725
+ if (packet.recipientId != null && !packet.recipientId.contentEquals(myPeerIdBytes)) {
726
+ return
727
+ }
728
+
729
+ val sessionKey = sessions[senderId] ?: return
730
+ val decrypted = decryptPayload(packet.payload, sessionKey) ?: return
731
+
732
+ if (decrypted.isEmpty()) return
733
+
734
+ val payloadType = NoisePayloadType.fromValue(decrypted[0])
735
+ val payloadData = decrypted.copyOfRange(1, decrypted.size)
736
+
737
+ when (payloadType) {
738
+ NoisePayloadType.PRIVATE_MESSAGE -> handlePrivateMessage(payloadData, senderId)
739
+ NoisePayloadType.READ_RECEIPT -> {
740
+ val messageId = String(payloadData, Charsets.UTF_8)
741
+ sendEvent("onReadReceipt", Arguments.createMap().apply {
742
+ putString("messageId", messageId)
743
+ putString("fromPeerId", senderId)
744
+ })
745
+ }
746
+ NoisePayloadType.DELIVERY_ACK -> {
747
+ val messageId = String(payloadData, Charsets.UTF_8)
748
+ sendEvent("onDeliveryAck", Arguments.createMap().apply {
749
+ putString("messageId", messageId)
750
+ putString("fromPeerId", senderId)
751
+ })
752
+ }
753
+ else -> {}
754
+ }
755
+ }
756
+
757
+ private fun handlePrivateMessage(data: ByteArray, senderId: String) {
758
+ // Parse TLV private message
759
+ var messageId: String? = null
760
+ var content: String? = null
761
+
762
+ var offset = 0
763
+ while (offset < data.size) {
764
+ if (offset + 3 > data.size) break
765
+
766
+ val tag = data[offset]
767
+ val length = ((data[offset + 1].toInt() and 0xFF) shl 8) or
768
+ (data[offset + 2].toInt() and 0xFF)
769
+ offset += 3
770
+
771
+ if (offset + length > data.size) break
772
+ val value = data.copyOfRange(offset, offset + length)
773
+ offset += length
774
+
775
+ when (tag.toInt()) {
776
+ 0x01 -> messageId = String(value, Charsets.UTF_8)
777
+ 0x02 -> content = String(value, Charsets.UTF_8)
778
+ }
779
+ }
780
+
781
+ if (messageId == null || content == null) return
782
+
783
+ val nickname = peers[senderId]?.nickname ?: senderId
784
+
785
+ val message = Arguments.createMap().apply {
786
+ putString("id", messageId)
787
+ putString("content", content)
788
+ putString("senderPeerId", senderId)
789
+ putString("senderNickname", nickname)
790
+ putDouble("timestamp", System.currentTimeMillis().toDouble())
791
+ putBoolean("isPrivate", true)
792
+ }
793
+
794
+ sendEvent("onMessageReceived", Arguments.createMap().apply {
795
+ putMap("message", message)
796
+ })
797
+ }
798
+
799
+ private fun handleLeave(senderId: String) {
800
+ peers.remove(senderId)
801
+ sessions.remove(senderId)
802
+ notifyPeerListUpdated()
803
+ }
804
+
805
+ private fun handleDeviceDisconnected(address: String) {
806
+ val peerId = deviceToPeer[address]
807
+ if (peerId != null) {
808
+ peers[peerId]?.let { peer ->
809
+ peers[peerId] = peer.copy(isConnected = false)
810
+ }
811
+ notifyPeerListUpdated()
812
+ }
813
+ connectedDevices.remove(address)
814
+ deviceToPeer.remove(address)
815
+ }
816
+
817
+ // MARK: - Private Methods
818
+
819
+ private fun sendAnnounce() {
820
+ val publicKey = privateKey?.public?.encoded ?: return
821
+ val signingPublicKey = signingKey?.public?.encoded ?: return
822
+
823
+ // Build TLV payload
824
+ val payload = ByteArrayOutputStream().apply {
825
+ // Nickname TLV (tag 0x01)
826
+ val nicknameBytes = myNickname.toByteArray(Charsets.UTF_8)
827
+ write(0x01)
828
+ write((nicknameBytes.size shr 8) and 0xFF)
829
+ write(nicknameBytes.size and 0xFF)
830
+ write(nicknameBytes)
831
+
832
+ // Noise public key TLV (tag 0x02)
833
+ write(0x02)
834
+ write((publicKey.size shr 8) and 0xFF)
835
+ write(publicKey.size and 0xFF)
836
+ write(publicKey)
837
+
838
+ // Signing public key TLV (tag 0x03)
839
+ write(0x03)
840
+ write((signingPublicKey.size shr 8) and 0xFF)
841
+ write(signingPublicKey.size and 0xFF)
842
+ write(signingPublicKey)
843
+ }.toByteArray()
844
+
845
+ val packet = createPacket(MessageType.ANNOUNCE.value, payload, null)
846
+ broadcastPacket(packet)
847
+ }
848
+
849
+ private fun sendLeaveAnnouncement() {
850
+ val packet = createPacket(MessageType.LEAVE.value, ByteArray(0), null)
851
+ broadcastPacket(packet)
852
+ }
853
+
854
+ private fun initiateHandshakeInternal(peerId: String) {
855
+ val publicKey = privateKey?.public?.encoded ?: return
856
+ val packet = createPacket(
857
+ type = MessageType.NOISE_HANDSHAKE.value,
858
+ payload = publicKey,
859
+ recipientId = hexStringToByteArray(peerId)
860
+ )
861
+ broadcastPacket(packet)
862
+ }
863
+
864
+ private fun createPacket(type: Byte, payload: ByteArray, recipientId: ByteArray?): BitchatPacket {
865
+ val timestamp = System.currentTimeMillis().toULong()
866
+
867
+ var packet = BitchatPacket(
868
+ version = 1,
869
+ type = type,
870
+ senderId = myPeerIdBytes,
871
+ recipientId = recipientId,
872
+ timestamp = timestamp,
873
+ payload = payload,
874
+ signature = null,
875
+ ttl = MESSAGE_TTL
876
+ )
877
+
878
+ // Sign the packet
879
+ try {
880
+ val signature = Signature.getInstance("SHA256withECDSA")
881
+ signature.initSign(signingKey?.private)
882
+ signature.update(packet.dataForSigning())
883
+ packet = packet.copy(signature = signature.sign())
884
+ } catch (e: Exception) {
885
+ Log.e(TAG, "Failed to sign packet: ${e.message}")
886
+ }
887
+
888
+ return packet
889
+ }
890
+
891
+ @SuppressLint("MissingPermission")
892
+ private fun broadcastPacket(packet: BitchatPacket) {
893
+ val data = packet.encode()
894
+
895
+ // Write to all GATT connections
896
+ gattConnections.values.forEach { gatt ->
897
+ val service = gatt.getService(serviceUUID)
898
+ val characteristic = service?.getCharacteristic(characteristicUUID)
899
+ if (characteristic != null) {
900
+ characteristic.value = data
901
+ gatt.writeCharacteristic(characteristic)
902
+ }
903
+ }
904
+
905
+ // Notify all connected devices via GATT server
906
+ gattCharacteristic?.let { char ->
907
+ char.value = data
908
+ connectedDevices.values.forEach { device ->
909
+ gattServer?.notifyCharacteristicChanged(device, char, false)
910
+ }
911
+ }
912
+ }
913
+
914
+ private fun encryptMessage(content: String, peerId: String): ByteArray? {
915
+ val messageId = UUID.randomUUID().toString()
916
+
917
+ // Build TLV payload
918
+ val tlvData = ByteArrayOutputStream().apply {
919
+ val idBytes = messageId.toByteArray(Charsets.UTF_8)
920
+ write(0x01)
921
+ write((idBytes.size shr 8) and 0xFF)
922
+ write(idBytes.size and 0xFF)
923
+ write(idBytes)
924
+
925
+ val contentBytes = content.toByteArray(Charsets.UTF_8)
926
+ write(0x02)
927
+ write((contentBytes.size shr 8) and 0xFF)
928
+ write(contentBytes.size and 0xFF)
929
+ write(contentBytes)
930
+ }.toByteArray()
931
+
932
+ val payload = byteArrayOf(NoisePayloadType.PRIVATE_MESSAGE.value) + tlvData
933
+ return encryptPayload(payload, peerId)
934
+ }
935
+
936
+ private fun encryptPayload(payload: ByteArray, peerId: String): ByteArray? {
937
+ val sessionKey = sessions[peerId] ?: return null
938
+
939
+ return try {
940
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
941
+ val nonce = ByteArray(12)
942
+ SecureRandom().nextBytes(nonce)
943
+ val spec = GCMParameterSpec(128, nonce)
944
+ cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(sessionKey, "AES"), spec)
945
+ val encrypted = cipher.doFinal(payload)
946
+ nonce + encrypted
947
+ } catch (e: Exception) {
948
+ Log.e(TAG, "Encryption failed: ${e.message}")
949
+ null
950
+ }
951
+ }
952
+
953
+ private fun decryptPayload(encrypted: ByteArray, sessionKey: ByteArray): ByteArray? {
954
+ if (encrypted.size < 12) return null
955
+
956
+ return try {
957
+ val nonce = encrypted.copyOfRange(0, 12)
958
+ val ciphertext = encrypted.copyOfRange(12, encrypted.size)
959
+
960
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
961
+ val spec = GCMParameterSpec(128, nonce)
962
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sessionKey, "AES"), spec)
963
+ cipher.doFinal(ciphertext)
964
+ } catch (e: Exception) {
965
+ Log.e(TAG, "Decryption failed: ${e.message}")
966
+ null
967
+ }
968
+ }
969
+
970
+ private fun notifyPeerListUpdated() {
971
+ val peersArray = Arguments.createArray()
972
+ peers.values.forEach { peer ->
973
+ peersArray.pushMap(Arguments.createMap().apply {
974
+ putString("peerId", peer.peerId)
975
+ putString("nickname", peer.nickname)
976
+ putBoolean("isConnected", peer.isConnected)
977
+ peer.rssi?.let { putInt("rssi", it) }
978
+ putDouble("lastSeen", peer.lastSeen.toDouble())
979
+ putBoolean("isVerified", peer.isVerified)
980
+ })
981
+ }
982
+
983
+ sendEvent("onPeerListUpdated", Arguments.createMap().apply {
984
+ putArray("peers", peersArray)
985
+ })
986
+ }
987
+
988
+ private fun sendEvent(eventName: String, params: WritableMap) {
989
+ reactApplicationContext
990
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
991
+ .emit(eventName, params)
992
+ }
993
+
994
+ private fun sendErrorEvent(code: String, message: String) {
995
+ sendEvent("onError", Arguments.createMap().apply {
996
+ putString("code", code)
997
+ putString("message", message)
998
+ })
999
+ }
1000
+
1001
+ // MARK: - Permission Helpers
1002
+
1003
+ private fun hasBluetoothPermissions(): Boolean {
1004
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1005
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH_ADVERTISE) == PackageManager.PERMISSION_GRANTED &&
1006
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED &&
1007
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED
1008
+ } else {
1009
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED &&
1010
+ ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED
1011
+ }
1012
+ }
1013
+
1014
+ private fun hasLocationPermission(): Boolean {
1015
+ return ActivityCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
1016
+ }
1017
+
1018
+ private fun hexStringToByteArray(hex: String): ByteArray {
1019
+ val result = ByteArray(8)
1020
+ var index = 0
1021
+ var hexIndex = 0
1022
+ while (hexIndex < hex.length && index < 8) {
1023
+ val byte = hex.substring(hexIndex, minOf(hexIndex + 2, hex.length)).toIntOrNull(16) ?: 0
1024
+ result[index] = byte.toByte()
1025
+ hexIndex += 2
1026
+ index++
1027
+ }
1028
+ return result
1029
+ }
1030
+
1031
+ // MARK: - Supporting Types
1032
+
1033
+ enum class MessageType(val value: Byte) {
1034
+ ANNOUNCE(0x01),
1035
+ MESSAGE(0x02),
1036
+ LEAVE(0x03),
1037
+ NOISE_HANDSHAKE(0x04),
1038
+ NOISE_ENCRYPTED(0x05),
1039
+ FILE_TRANSFER(0x06),
1040
+ FRAGMENT(0x07),
1041
+ REQUEST_SYNC(0x08);
1042
+
1043
+ companion object {
1044
+ fun fromValue(value: Byte): MessageType? = entries.find { it.value == value }
1045
+ }
1046
+ }
1047
+
1048
+ enum class NoisePayloadType(val value: Byte) {
1049
+ PRIVATE_MESSAGE(0x01),
1050
+ READ_RECEIPT(0x02),
1051
+ DELIVERY_ACK(0x03),
1052
+ FILE_TRANSFER(0x04),
1053
+ VERIFY_CHALLENGE(0x05),
1054
+ VERIFY_RESPONSE(0x06);
1055
+
1056
+ companion object {
1057
+ fun fromValue(value: Byte): NoisePayloadType? = entries.find { it.value == value }
1058
+ }
1059
+ }
1060
+
1061
+ data class BitchatPacket(
1062
+ val version: Byte = 1,
1063
+ val type: Byte,
1064
+ val senderId: ByteArray,
1065
+ val recipientId: ByteArray?,
1066
+ val timestamp: ULong,
1067
+ val payload: ByteArray,
1068
+ val signature: ByteArray?,
1069
+ val ttl: Byte
1070
+ ) {
1071
+ fun dataForSigning(): ByteArray {
1072
+ val buffer = ByteBuffer.allocate(1 + 1 + 8 + 8 + 8 + payload.size + 1)
1073
+ buffer.order(ByteOrder.BIG_ENDIAN)
1074
+ buffer.put(version)
1075
+ buffer.put(type)
1076
+ buffer.put(senderId.copyOf(8))
1077
+ buffer.put(recipientId?.copyOf(8) ?: ByteArray(8))
1078
+ buffer.putLong(timestamp.toLong())
1079
+ buffer.put(payload)
1080
+ buffer.put(ttl)
1081
+ return buffer.array()
1082
+ }
1083
+
1084
+ fun encode(): ByteArray {
1085
+ val buffer = ByteBuffer.allocate(29 + payload.size + (signature?.size ?: 0))
1086
+ buffer.order(ByteOrder.BIG_ENDIAN)
1087
+ buffer.put(version)
1088
+ buffer.put(type)
1089
+ buffer.put(ttl)
1090
+ buffer.put(senderId.copyOf(8))
1091
+ buffer.put(recipientId?.copyOf(8) ?: ByteArray(8))
1092
+ buffer.putLong(timestamp.toLong())
1093
+ buffer.putShort(payload.size.toShort())
1094
+ buffer.put(payload)
1095
+ signature?.let { buffer.put(it) }
1096
+ return buffer.array()
1097
+ }
1098
+
1099
+ companion object {
1100
+ fun decode(data: ByteArray): BitchatPacket? {
1101
+ if (data.size < 29) return null
1102
+
1103
+ val buffer = ByteBuffer.wrap(data)
1104
+ buffer.order(ByteOrder.BIG_ENDIAN)
1105
+
1106
+ val version = buffer.get()
1107
+ val type = buffer.get()
1108
+ val ttl = buffer.get()
1109
+
1110
+ val senderId = ByteArray(8)
1111
+ buffer.get(senderId)
1112
+
1113
+ val recipientIdBytes = ByteArray(8)
1114
+ buffer.get(recipientIdBytes)
1115
+ val recipientId = if (recipientIdBytes.all { it == 0.toByte() }) null else recipientIdBytes
1116
+
1117
+ val timestamp = buffer.long.toULong()
1118
+ val payloadLength = buffer.short.toInt() and 0xFFFF
1119
+
1120
+ if (buffer.remaining() < payloadLength) return null
1121
+ val payload = ByteArray(payloadLength)
1122
+ buffer.get(payload)
1123
+
1124
+ val signature = if (buffer.remaining() >= 64) {
1125
+ ByteArray(64).also { buffer.get(it) }
1126
+ } else null
1127
+
1128
+ return BitchatPacket(
1129
+ version = version,
1130
+ type = type,
1131
+ senderId = senderId,
1132
+ recipientId = recipientId,
1133
+ timestamp = timestamp,
1134
+ payload = payload,
1135
+ signature = signature,
1136
+ ttl = ttl
1137
+ )
1138
+ }
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ private class ByteArrayOutputStream : java.io.ByteArrayOutputStream()