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.
- package/README.md +216 -0
- package/android/build.gradle +94 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +18 -0
- package/android/src/main/AndroidManifestNew.xml +17 -0
- package/android/src/main/java/com/blemesh/BleMeshModule.kt +1143 -0
- package/android/src/main/java/com/blemesh/BleMeshPackage.kt +16 -0
- package/ios/BleMesh.m +45 -0
- package/ios/BleMesh.swift +1075 -0
- package/kard-network-ble-mesh.podspec +27 -0
- package/lib/commonjs/index.js +241 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/index.js +236 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/index.d.ts +103 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/package.json +90 -0
- package/src/index.ts +334 -0
|
@@ -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()
|