@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.
- package/README.md +394 -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 +1994 -0
- package/android/src/main/java/com/blemesh/BleMeshPackage.kt +16 -0
- package/ios/BleMesh.m +49 -0
- package/ios/BleMesh.swift +1838 -0
- package/kard-network-ble-mesh.podspec +27 -0
- package/lib/commonjs/index.js +275 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/index.js +270 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/index.d.ts +178 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/package.json +99 -0
- package/src/index.ts +448 -0
|
@@ -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()
|