@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,1838 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreBluetooth
|
|
3
|
+
import CryptoKit
|
|
4
|
+
|
|
5
|
+
@objc(BleMesh)
|
|
6
|
+
class BleMesh: RCTEventEmitter {
|
|
7
|
+
|
|
8
|
+
// MARK: - Constants
|
|
9
|
+
|
|
10
|
+
#if DEBUG
|
|
11
|
+
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5A")
|
|
12
|
+
#else
|
|
13
|
+
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C")
|
|
14
|
+
#endif
|
|
15
|
+
static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
|
|
16
|
+
|
|
17
|
+
private let messageTTL: UInt8 = 7
|
|
18
|
+
|
|
19
|
+
// MARK: - BLE Objects
|
|
20
|
+
|
|
21
|
+
private var centralManager: CBCentralManager?
|
|
22
|
+
private var peripheralManager: CBPeripheralManager?
|
|
23
|
+
private var characteristic: CBMutableCharacteristic?
|
|
24
|
+
|
|
25
|
+
// MARK: - State
|
|
26
|
+
|
|
27
|
+
private var isRunning = false
|
|
28
|
+
private var myNickname: String = "anon"
|
|
29
|
+
private var myPeerID: String = ""
|
|
30
|
+
private var myPeerIDData: Data = Data()
|
|
31
|
+
|
|
32
|
+
// Peer tracking
|
|
33
|
+
private struct PeerInfo {
|
|
34
|
+
let peerId: String
|
|
35
|
+
var nickname: String
|
|
36
|
+
var isConnected: Bool
|
|
37
|
+
var rssi: Int?
|
|
38
|
+
var lastSeen: Date
|
|
39
|
+
var noisePublicKey: Data?
|
|
40
|
+
var isVerified: Bool
|
|
41
|
+
}
|
|
42
|
+
private var peers: [String: PeerInfo] = [:]
|
|
43
|
+
private var peripherals: [String: CBPeripheral] = [:]
|
|
44
|
+
private var peripheralToPeer: [String: String] = [:]
|
|
45
|
+
private var subscribedCentrals: [CBCentral] = []
|
|
46
|
+
|
|
47
|
+
// Encryption
|
|
48
|
+
private var privateKey: Curve25519.KeyAgreement.PrivateKey?
|
|
49
|
+
private var signingKey: Curve25519.Signing.PrivateKey?
|
|
50
|
+
private var sessions: [String: Data] = [:]
|
|
51
|
+
|
|
52
|
+
// Message deduplication
|
|
53
|
+
private var processedMessages: Set<String> = []
|
|
54
|
+
|
|
55
|
+
// File transfer tracking
|
|
56
|
+
private struct FileTransfer {
|
|
57
|
+
let id: String
|
|
58
|
+
let fileName: String
|
|
59
|
+
let fileSize: Int
|
|
60
|
+
let mimeType: String
|
|
61
|
+
let senderPeerId: String
|
|
62
|
+
let totalChunks: Int
|
|
63
|
+
var receivedChunks: Set<Int> = []
|
|
64
|
+
}
|
|
65
|
+
private var activeFileTransfers: [String: FileTransfer] = [:]
|
|
66
|
+
private var fileTransferFragments: [String: [Int: Data]] = [:]
|
|
67
|
+
private let fileFragmentSize = 180 // Max payload size for fragments
|
|
68
|
+
|
|
69
|
+
// Transaction chunking tracking
|
|
70
|
+
private struct TransactionChunks {
|
|
71
|
+
let txId: String
|
|
72
|
+
let senderId: String
|
|
73
|
+
let totalSize: Int
|
|
74
|
+
let totalChunks: Int
|
|
75
|
+
var chunks: [Int: Data] = [:]
|
|
76
|
+
}
|
|
77
|
+
private var pendingTransactionChunks: [String: TransactionChunks] = [:]
|
|
78
|
+
|
|
79
|
+
// Queues
|
|
80
|
+
private let bleQueue = DispatchQueue(label: "mesh.bluetooth", qos: .userInitiated)
|
|
81
|
+
private let messageQueue = DispatchQueue(label: "mesh.message", attributes: .concurrent)
|
|
82
|
+
|
|
83
|
+
// MARK: - RCTEventEmitter
|
|
84
|
+
|
|
85
|
+
override init() {
|
|
86
|
+
super.init()
|
|
87
|
+
generateIdentity()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
override static func moduleName() -> String! {
|
|
91
|
+
return "BleMesh"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
override func supportedEvents() -> [String]! {
|
|
95
|
+
return [
|
|
96
|
+
"onPeerListUpdated",
|
|
97
|
+
"onMessageReceived",
|
|
98
|
+
"onFileReceived",
|
|
99
|
+
"onTransactionReceived",
|
|
100
|
+
"onTransactionResponse",
|
|
101
|
+
"onConnectionStateChanged",
|
|
102
|
+
"onReadReceipt",
|
|
103
|
+
"onDeliveryAck",
|
|
104
|
+
"onError"
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override static func requiresMainQueueSetup() -> Bool {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// MARK: - Identity
|
|
113
|
+
|
|
114
|
+
private func generateIdentity() {
|
|
115
|
+
// Generate or load keys
|
|
116
|
+
if let savedPrivateKey = loadKey(forKey: "mesh.privateKey") {
|
|
117
|
+
privateKey = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: savedPrivateKey)
|
|
118
|
+
}
|
|
119
|
+
if privateKey == nil {
|
|
120
|
+
privateKey = Curve25519.KeyAgreement.PrivateKey()
|
|
121
|
+
if let pk = privateKey {
|
|
122
|
+
saveKey(pk.rawRepresentation, forKey: "mesh.privateKey")
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if let savedSigningKey = loadKey(forKey: "mesh.signingKey") {
|
|
127
|
+
signingKey = try? Curve25519.Signing.PrivateKey(rawRepresentation: savedSigningKey)
|
|
128
|
+
}
|
|
129
|
+
if signingKey == nil {
|
|
130
|
+
signingKey = Curve25519.Signing.PrivateKey()
|
|
131
|
+
if let sk = signingKey {
|
|
132
|
+
saveKey(sk.rawRepresentation, forKey: "mesh.signingKey")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Generate peer ID from public key fingerprint (first 16 hex chars of SHA256)
|
|
137
|
+
if let pk = privateKey {
|
|
138
|
+
let fingerprint = SHA256.hash(data: pk.publicKey.rawRepresentation)
|
|
139
|
+
myPeerID = fingerprint.prefix(8).map { String(format: "%02x", $0) }.joined()
|
|
140
|
+
myPeerIDData = Data(fingerprint.prefix(8))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private func loadKey(forKey key: String) -> Data? {
|
|
145
|
+
let query: [String: Any] = [
|
|
146
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
147
|
+
kSecAttrService as String: "com.blemesh",
|
|
148
|
+
kSecAttrAccount as String: key,
|
|
149
|
+
kSecReturnData as String: true
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
var result: AnyObject?
|
|
153
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
154
|
+
|
|
155
|
+
if status == errSecSuccess {
|
|
156
|
+
return result as? Data
|
|
157
|
+
}
|
|
158
|
+
return nil
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private func saveKey(_ data: Data, forKey key: String) {
|
|
162
|
+
let query: [String: Any] = [
|
|
163
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
164
|
+
kSecAttrService as String: "com.blemesh",
|
|
165
|
+
kSecAttrAccount as String: key,
|
|
166
|
+
kSecValueData as String: data
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
SecItemDelete(query as CFDictionary)
|
|
170
|
+
SecItemAdd(query as CFDictionary, nil)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// MARK: - React Native API
|
|
174
|
+
|
|
175
|
+
@objc(requestPermissions:rejecter:)
|
|
176
|
+
func requestPermissions(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
177
|
+
// On iOS, BLE permissions are requested when CBCentralManager is initialized
|
|
178
|
+
// We need to check the current state
|
|
179
|
+
let tempManager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false])
|
|
180
|
+
|
|
181
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
182
|
+
let bluetoothGranted = tempManager.state != .unauthorized
|
|
183
|
+
resolve([
|
|
184
|
+
"bluetooth": bluetoothGranted,
|
|
185
|
+
"location": true // iOS doesn't require location for BLE
|
|
186
|
+
])
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@objc(checkPermissions:rejecter:)
|
|
191
|
+
func checkPermissions(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
192
|
+
let state = centralManager?.state ?? .unknown
|
|
193
|
+
let bluetoothGranted = state != .unauthorized
|
|
194
|
+
|
|
195
|
+
resolve([
|
|
196
|
+
"bluetooth": bluetoothGranted,
|
|
197
|
+
"location": true
|
|
198
|
+
])
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@objc(start:resolver:rejecter:)
|
|
202
|
+
func start(_ nickname: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
203
|
+
myNickname = nickname
|
|
204
|
+
|
|
205
|
+
bleQueue.async { [weak self] in
|
|
206
|
+
guard let self = self else { return }
|
|
207
|
+
|
|
208
|
+
self.centralManager = CBCentralManager(delegate: self, queue: self.bleQueue)
|
|
209
|
+
self.peripheralManager = CBPeripheralManager(delegate: self, queue: self.bleQueue)
|
|
210
|
+
|
|
211
|
+
self.isRunning = true
|
|
212
|
+
|
|
213
|
+
DispatchQueue.main.async {
|
|
214
|
+
resolve(nil)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@objc(stop:rejecter:)
|
|
220
|
+
func stop(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
221
|
+
bleQueue.async { [weak self] in
|
|
222
|
+
guard let self = self else { return }
|
|
223
|
+
|
|
224
|
+
// Send leave announcement
|
|
225
|
+
self.sendLeaveAnnouncement()
|
|
226
|
+
|
|
227
|
+
// Stop scanning and advertising
|
|
228
|
+
self.centralManager?.stopScan()
|
|
229
|
+
self.peripheralManager?.stopAdvertising()
|
|
230
|
+
|
|
231
|
+
// Disconnect all peripherals
|
|
232
|
+
for (_, peripheral) in self.peripherals {
|
|
233
|
+
self.centralManager?.cancelPeripheralConnection(peripheral)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
self.isRunning = false
|
|
237
|
+
self.peers.removeAll()
|
|
238
|
+
self.peripherals.removeAll()
|
|
239
|
+
self.peripheralToPeer.removeAll()
|
|
240
|
+
self.subscribedCentrals.removeAll()
|
|
241
|
+
|
|
242
|
+
DispatchQueue.main.async {
|
|
243
|
+
resolve(nil)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@objc(setNickname:resolver:rejecter:)
|
|
249
|
+
func setNickname(_ nickname: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
250
|
+
myNickname = nickname
|
|
251
|
+
sendAnnounce()
|
|
252
|
+
resolve(nil)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
@objc(getMyPeerId:rejecter:)
|
|
256
|
+
func getMyPeerId(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
257
|
+
resolve(myPeerID)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@objc(getMyNickname:rejecter:)
|
|
261
|
+
func getMyNickname(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
262
|
+
resolve(myNickname)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@objc(getPeers:rejecter:)
|
|
266
|
+
func getPeers(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
267
|
+
let peerList = peers.values.map { peer in
|
|
268
|
+
return [
|
|
269
|
+
"peerId": peer.peerId,
|
|
270
|
+
"nickname": peer.nickname,
|
|
271
|
+
"isConnected": peer.isConnected,
|
|
272
|
+
"rssi": peer.rssi ?? NSNull(),
|
|
273
|
+
"lastSeen": Int(peer.lastSeen.timeIntervalSince1970 * 1000),
|
|
274
|
+
"isVerified": peer.isVerified
|
|
275
|
+
] as [String: Any]
|
|
276
|
+
}
|
|
277
|
+
resolve(peerList)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@objc(sendMessage:channel:resolver:rejecter:)
|
|
281
|
+
func sendMessage(_ content: String, channel: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
282
|
+
let messageId = UUID().uuidString
|
|
283
|
+
|
|
284
|
+
messageQueue.async { [weak self] in
|
|
285
|
+
guard let self = self else { return }
|
|
286
|
+
|
|
287
|
+
let packet = self.createPacket(
|
|
288
|
+
type: MessageType.message.rawValue,
|
|
289
|
+
payload: Data(content.utf8),
|
|
290
|
+
recipientID: nil
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
self.broadcastPacket(packet)
|
|
294
|
+
|
|
295
|
+
DispatchQueue.main.async {
|
|
296
|
+
resolve(messageId)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
@objc(sendPrivateMessage:recipientPeerId:resolver:rejecter:)
|
|
302
|
+
func sendPrivateMessage(_ content: String, recipientPeerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
303
|
+
let messageId = UUID().uuidString
|
|
304
|
+
|
|
305
|
+
messageQueue.async { [weak self] in
|
|
306
|
+
guard let self = self else { return }
|
|
307
|
+
|
|
308
|
+
// Check if we have an encryption session
|
|
309
|
+
if self.sessions[recipientPeerId] != nil {
|
|
310
|
+
// Encrypt the message
|
|
311
|
+
if let encrypted = self.encryptMessage(content, for: recipientPeerId) {
|
|
312
|
+
let packet = self.createPacket(
|
|
313
|
+
type: MessageType.noiseEncrypted.rawValue,
|
|
314
|
+
payload: encrypted,
|
|
315
|
+
recipientID: Data(hexString: recipientPeerId)
|
|
316
|
+
)
|
|
317
|
+
self.broadcastPacket(packet)
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
// Initiate handshake first
|
|
321
|
+
self.initiateHandshakeInternal(with: recipientPeerId)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
DispatchQueue.main.async {
|
|
325
|
+
resolve(messageId)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@objc(sendFile:recipientPeerId:channel:resolver:rejecter:)
|
|
331
|
+
func sendFile(_ filePath: String, recipientPeerId: String?, channel: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
332
|
+
let transferId = UUID().uuidString
|
|
333
|
+
|
|
334
|
+
guard let fileData = FileManager.default.contents(atPath: filePath) else {
|
|
335
|
+
reject("FILE_ERROR", "Could not read file at path: \(filePath)", nil)
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let fileName = (filePath as NSString).lastPathComponent
|
|
340
|
+
let mimeType = getMimeType(for: fileName)
|
|
341
|
+
let totalChunks = (fileData.count + fileFragmentSize - 1) / fileFragmentSize
|
|
342
|
+
|
|
343
|
+
messageQueue.async { [weak self] in
|
|
344
|
+
guard let self = self else { return }
|
|
345
|
+
|
|
346
|
+
// Build and send metadata packet
|
|
347
|
+
let metadataPayload = self.buildFileTransferMetadata(
|
|
348
|
+
transferId: transferId,
|
|
349
|
+
fileName: fileName,
|
|
350
|
+
fileSize: fileData.count,
|
|
351
|
+
mimeType: mimeType,
|
|
352
|
+
totalChunks: totalChunks
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
let metadataPacket = self.createPacket(
|
|
356
|
+
type: MessageType.fileTransfer.rawValue,
|
|
357
|
+
payload: metadataPayload,
|
|
358
|
+
recipientID: recipientPeerId != nil ? Data(hexString: recipientPeerId!) : nil
|
|
359
|
+
)
|
|
360
|
+
self.broadcastPacket(metadataPacket)
|
|
361
|
+
|
|
362
|
+
// Send file fragments
|
|
363
|
+
for chunkIndex in 0..<totalChunks {
|
|
364
|
+
let start = chunkIndex * self.fileFragmentSize
|
|
365
|
+
let end = min(start + self.fileFragmentSize, fileData.count)
|
|
366
|
+
let chunkData = fileData.subdata(in: start..<end)
|
|
367
|
+
|
|
368
|
+
let fragmentPayload = self.buildFileFragment(
|
|
369
|
+
transferId: transferId,
|
|
370
|
+
chunkIndex: chunkIndex,
|
|
371
|
+
totalChunks: totalChunks,
|
|
372
|
+
chunkData: chunkData
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
let fragmentPacket = self.createPacket(
|
|
376
|
+
type: MessageType.fragment.rawValue,
|
|
377
|
+
payload: fragmentPayload,
|
|
378
|
+
recipientID: recipientPeerId != nil ? Data(hexString: recipientPeerId!) : nil
|
|
379
|
+
)
|
|
380
|
+
self.broadcastPacket(fragmentPacket)
|
|
381
|
+
|
|
382
|
+
// Small delay to avoid overwhelming the BLE stack
|
|
383
|
+
Thread.sleep(forTimeInterval: 0.05)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
DispatchQueue.main.async {
|
|
387
|
+
resolve(transferId)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private func buildFileTransferMetadata(transferId: String, fileName: String, fileSize: Int, mimeType: String, totalChunks: Int) -> Data {
|
|
393
|
+
var payload = Data()
|
|
394
|
+
|
|
395
|
+
// Transfer ID TLV (tag 0x01)
|
|
396
|
+
let transferIdData = Data(transferId.utf8)
|
|
397
|
+
payload.append(0x01)
|
|
398
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(transferIdData.count).bigEndian) { Array($0) })
|
|
399
|
+
payload.append(transferIdData)
|
|
400
|
+
|
|
401
|
+
// File name TLV (tag 0x02)
|
|
402
|
+
let fileNameData = Data(fileName.utf8)
|
|
403
|
+
payload.append(0x02)
|
|
404
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(fileNameData.count).bigEndian) { Array($0) })
|
|
405
|
+
payload.append(fileNameData)
|
|
406
|
+
|
|
407
|
+
// File size TLV (tag 0x03) - 4 bytes
|
|
408
|
+
payload.append(0x03)
|
|
409
|
+
payload.append(0x00)
|
|
410
|
+
payload.append(0x04)
|
|
411
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt32(fileSize).bigEndian) { Array($0) })
|
|
412
|
+
|
|
413
|
+
// MIME type TLV (tag 0x04)
|
|
414
|
+
let mimeTypeData = Data(mimeType.utf8)
|
|
415
|
+
payload.append(0x04)
|
|
416
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(mimeTypeData.count).bigEndian) { Array($0) })
|
|
417
|
+
payload.append(mimeTypeData)
|
|
418
|
+
|
|
419
|
+
// Total chunks TLV (tag 0x05) - 4 bytes
|
|
420
|
+
payload.append(0x05)
|
|
421
|
+
payload.append(0x00)
|
|
422
|
+
payload.append(0x04)
|
|
423
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalChunks).bigEndian) { Array($0) })
|
|
424
|
+
|
|
425
|
+
return payload
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private func buildFileFragment(transferId: String, chunkIndex: Int, totalChunks: Int, chunkData: Data) -> Data {
|
|
429
|
+
var payload = Data()
|
|
430
|
+
|
|
431
|
+
// Transfer ID TLV (tag 0x01)
|
|
432
|
+
let transferIdData = Data(transferId.utf8)
|
|
433
|
+
payload.append(0x01)
|
|
434
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(transferIdData.count).bigEndian) { Array($0) })
|
|
435
|
+
payload.append(transferIdData)
|
|
436
|
+
|
|
437
|
+
// Chunk index TLV (tag 0x02) - 4 bytes
|
|
438
|
+
payload.append(0x02)
|
|
439
|
+
payload.append(0x00)
|
|
440
|
+
payload.append(0x04)
|
|
441
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt32(chunkIndex).bigEndian) { Array($0) })
|
|
442
|
+
|
|
443
|
+
// Total chunks TLV (tag 0x03) - 4 bytes
|
|
444
|
+
payload.append(0x03)
|
|
445
|
+
payload.append(0x00)
|
|
446
|
+
payload.append(0x04)
|
|
447
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalChunks).bigEndian) { Array($0) })
|
|
448
|
+
|
|
449
|
+
// Chunk data TLV (tag 0x04)
|
|
450
|
+
payload.append(0x04)
|
|
451
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(chunkData.count).bigEndian) { Array($0) })
|
|
452
|
+
payload.append(chunkData)
|
|
453
|
+
|
|
454
|
+
return payload
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
@objc(sendTransaction:serializedTransaction:recipientPeerId:firstSignerPublicKey:secondSignerPublicKey:description:resolver:rejecter:)
|
|
458
|
+
func sendTransaction(
|
|
459
|
+
_ txId: String,
|
|
460
|
+
serializedTransaction: String,
|
|
461
|
+
recipientPeerId: String?,
|
|
462
|
+
firstSignerPublicKey: String,
|
|
463
|
+
secondSignerPublicKey: String?,
|
|
464
|
+
description: String?,
|
|
465
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
466
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
467
|
+
) {
|
|
468
|
+
messageQueue.async { [weak self] in
|
|
469
|
+
guard let self = self else { return }
|
|
470
|
+
|
|
471
|
+
// Build TLV payload for Solana transaction
|
|
472
|
+
let payload = self.buildSolanaTransactionPayload(
|
|
473
|
+
txId: txId,
|
|
474
|
+
serializedTransaction: serializedTransaction,
|
|
475
|
+
firstSignerPublicKey: firstSignerPublicKey,
|
|
476
|
+
secondSignerPublicKey: secondSignerPublicKey,
|
|
477
|
+
description: description
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
// Check if we need chunking (MTU limit is ~500 bytes for encrypted payload)
|
|
481
|
+
let maxPayloadSize = 450 // Conservative limit
|
|
482
|
+
|
|
483
|
+
if let targetPeerId = recipientPeerId {
|
|
484
|
+
// Send to specific peer
|
|
485
|
+
self.sendTransactionToPeer(txId: txId, payload: payload, recipientPeerId: targetPeerId, maxPayloadSize: maxPayloadSize)
|
|
486
|
+
} else {
|
|
487
|
+
// Broadcast to all connected peers with sessions
|
|
488
|
+
print("Broadcasting transaction \(txId) to all peers")
|
|
489
|
+
var sentCount = 0
|
|
490
|
+
for peerId in self.sessions.keys {
|
|
491
|
+
self.sendTransactionToPeer(txId: txId, payload: payload, recipientPeerId: peerId, maxPayloadSize: maxPayloadSize)
|
|
492
|
+
sentCount += 1
|
|
493
|
+
}
|
|
494
|
+
print("Transaction broadcast to \(sentCount) peers")
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
DispatchQueue.main.async {
|
|
498
|
+
resolve(txId)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private func sendTransactionToPeer(txId: String, payload: Data, recipientPeerId: String, maxPayloadSize: Int) {
|
|
504
|
+
guard self.sessions[recipientPeerId] != nil else {
|
|
505
|
+
// No session, initiate handshake first
|
|
506
|
+
self.initiateHandshakeInternal(with: recipientPeerId)
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
var encryptedPayload = Data([NoisePayloadType.solanaTransaction.rawValue])
|
|
511
|
+
encryptedPayload.append(payload)
|
|
512
|
+
|
|
513
|
+
if let encrypted = self.encryptPayload(encryptedPayload, for: recipientPeerId) {
|
|
514
|
+
if encrypted.count <= maxPayloadSize {
|
|
515
|
+
// Small enough for single packet
|
|
516
|
+
let packet = self.createPacket(
|
|
517
|
+
type: MessageType.noiseEncrypted.rawValue,
|
|
518
|
+
payload: encrypted,
|
|
519
|
+
recipientID: Data(hexString: recipientPeerId)
|
|
520
|
+
)
|
|
521
|
+
self.broadcastPacket(packet)
|
|
522
|
+
} else {
|
|
523
|
+
// Need to chunk large transaction
|
|
524
|
+
print("Transaction too large (\(encrypted.count) bytes), using chunking")
|
|
525
|
+
self.sendChunkedTransaction(txId: txId, encryptedData: encrypted, recipientPeerId: recipientPeerId)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private func sendChunkedTransaction(txId: String, encryptedData: Data, recipientPeerId: String) {
|
|
531
|
+
let chunkSize = 400 // Max chunk size
|
|
532
|
+
let totalChunks = (encryptedData.count + chunkSize - 1) / chunkSize
|
|
533
|
+
|
|
534
|
+
print("Sending chunked transaction \(txId): \(encryptedData.count) bytes in \(totalChunks) chunks")
|
|
535
|
+
|
|
536
|
+
// Send metadata packet first
|
|
537
|
+
let metadataPayload = self.buildTransactionChunkMetadata(txId: txId, totalSize: encryptedData.count, totalChunks: totalChunks)
|
|
538
|
+
let metadataPacket = self.createPacket(
|
|
539
|
+
type: MessageType.solanaTransaction.rawValue,
|
|
540
|
+
payload: metadataPayload,
|
|
541
|
+
recipientID: Data(hexString: recipientPeerId)
|
|
542
|
+
)
|
|
543
|
+
self.broadcastPacket(metadataPacket)
|
|
544
|
+
|
|
545
|
+
// Small delay to let receiver prepare
|
|
546
|
+
Thread.sleep(forTimeInterval: 0.1)
|
|
547
|
+
|
|
548
|
+
// Send chunks
|
|
549
|
+
for chunkIndex in 0..<totalChunks {
|
|
550
|
+
let start = chunkIndex * chunkSize
|
|
551
|
+
let end = min(start + chunkSize, encryptedData.count)
|
|
552
|
+
let chunkData = encryptedData.subdata(in: start..<end)
|
|
553
|
+
|
|
554
|
+
let chunkPayload = self.buildTransactionChunk(txId: txId, chunkIndex: chunkIndex, totalChunks: totalChunks, chunkData: chunkData)
|
|
555
|
+
let chunkPacket = self.createPacket(
|
|
556
|
+
type: MessageType.fragment.rawValue,
|
|
557
|
+
payload: chunkPayload,
|
|
558
|
+
recipientID: Data(hexString: recipientPeerId)
|
|
559
|
+
)
|
|
560
|
+
self.broadcastPacket(chunkPacket)
|
|
561
|
+
|
|
562
|
+
print("Sent chunk \(chunkIndex + 1)/\(totalChunks) for transaction \(txId)")
|
|
563
|
+
Thread.sleep(forTimeInterval: 0.05) // Small delay between chunks
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private func buildTransactionChunkMetadata(txId: String, totalSize: Int, totalChunks: Int) -> Data {
|
|
568
|
+
var payload = Data()
|
|
569
|
+
|
|
570
|
+
// Transaction ID TLV (tag 0x01)
|
|
571
|
+
let txIdData = Data(txId.utf8)
|
|
572
|
+
payload.append(0x01)
|
|
573
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(txIdData.count).bigEndian) { Array($0) })
|
|
574
|
+
payload.append(txIdData)
|
|
575
|
+
|
|
576
|
+
// Total size TLV (tag 0x02) - 4 bytes
|
|
577
|
+
payload.append(0x02)
|
|
578
|
+
payload.append(0x00)
|
|
579
|
+
payload.append(0x04)
|
|
580
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalSize).bigEndian) { Array($0) })
|
|
581
|
+
|
|
582
|
+
// Total chunks TLV (tag 0x03) - 4 bytes
|
|
583
|
+
payload.append(0x03)
|
|
584
|
+
payload.append(0x00)
|
|
585
|
+
payload.append(0x04)
|
|
586
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalChunks).bigEndian) { Array($0) })
|
|
587
|
+
|
|
588
|
+
return payload
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private func buildTransactionChunk(txId: String, chunkIndex: Int, totalChunks: Int, chunkData: Data) -> Data {
|
|
592
|
+
var payload = Data()
|
|
593
|
+
|
|
594
|
+
// Transaction ID TLV (tag 0x01)
|
|
595
|
+
let txIdData = Data(txId.utf8)
|
|
596
|
+
payload.append(0x01)
|
|
597
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(txIdData.count).bigEndian) { Array($0) })
|
|
598
|
+
payload.append(txIdData)
|
|
599
|
+
|
|
600
|
+
// Chunk index TLV (tag 0x02) - 4 bytes
|
|
601
|
+
payload.append(0x02)
|
|
602
|
+
payload.append(0x00)
|
|
603
|
+
payload.append(0x04)
|
|
604
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt32(chunkIndex).bigEndian) { Array($0) })
|
|
605
|
+
|
|
606
|
+
// Total chunks TLV (tag 0x03) - 4 bytes
|
|
607
|
+
payload.append(0x03)
|
|
608
|
+
payload.append(0x00)
|
|
609
|
+
payload.append(0x04)
|
|
610
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt32(totalChunks).bigEndian) { Array($0) })
|
|
611
|
+
|
|
612
|
+
// Chunk data TLV (tag 0x04)
|
|
613
|
+
payload.append(0x04)
|
|
614
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(chunkData.count).bigEndian) { Array($0) })
|
|
615
|
+
payload.append(chunkData)
|
|
616
|
+
|
|
617
|
+
return payload
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private func buildSolanaTransactionPayload(
|
|
621
|
+
txId: String,
|
|
622
|
+
serializedTransaction: String,
|
|
623
|
+
firstSignerPublicKey: String,
|
|
624
|
+
secondSignerPublicKey: String?,
|
|
625
|
+
description: String?
|
|
626
|
+
) -> Data {
|
|
627
|
+
var payload = Data()
|
|
628
|
+
|
|
629
|
+
// Transaction ID TLV (tag 0x01)
|
|
630
|
+
let txIdData = Data(txId.utf8)
|
|
631
|
+
payload.append(0x01)
|
|
632
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(txIdData.count).bigEndian) { Array($0) })
|
|
633
|
+
payload.append(txIdData)
|
|
634
|
+
|
|
635
|
+
// Serialized transaction TLV (tag 0x02)
|
|
636
|
+
let txData = Data(serializedTransaction.utf8)
|
|
637
|
+
payload.append(0x02)
|
|
638
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(txData.count).bigEndian) { Array($0) })
|
|
639
|
+
payload.append(txData)
|
|
640
|
+
|
|
641
|
+
// First signer public key TLV (tag 0x03)
|
|
642
|
+
let firstSignerData = Data(firstSignerPublicKey.utf8)
|
|
643
|
+
payload.append(0x03)
|
|
644
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(firstSignerData.count).bigEndian) { Array($0) })
|
|
645
|
+
payload.append(firstSignerData)
|
|
646
|
+
|
|
647
|
+
// Second signer public key TLV (tag 0x04) - optional
|
|
648
|
+
if let secondSigner = secondSignerPublicKey, !secondSigner.isEmpty {
|
|
649
|
+
let secondSignerData = Data(secondSigner.utf8)
|
|
650
|
+
payload.append(0x04)
|
|
651
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(secondSignerData.count).bigEndian) { Array($0) })
|
|
652
|
+
payload.append(secondSignerData)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Description TLV (tag 0x05) - optional
|
|
656
|
+
if let desc = description, !desc.isEmpty {
|
|
657
|
+
let descData = Data(desc.utf8)
|
|
658
|
+
payload.append(0x05)
|
|
659
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(descData.count).bigEndian) { Array($0) })
|
|
660
|
+
payload.append(descData)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return payload
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
@objc(respondToTransaction:recipientPeerId:signedTransaction:error:resolver:rejecter:)
|
|
667
|
+
func respondToTransaction(
|
|
668
|
+
_ transactionId: String,
|
|
669
|
+
recipientPeerId: String,
|
|
670
|
+
signedTransaction: String?,
|
|
671
|
+
error: String?,
|
|
672
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
673
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
674
|
+
) {
|
|
675
|
+
messageQueue.async { [weak self] in
|
|
676
|
+
guard let self = self else { return }
|
|
677
|
+
|
|
678
|
+
// Build TLV payload for response
|
|
679
|
+
var payload = Data()
|
|
680
|
+
|
|
681
|
+
// Transaction ID TLV (tag 0x01)
|
|
682
|
+
let txIdData = Data(transactionId.utf8)
|
|
683
|
+
payload.append(0x01)
|
|
684
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(txIdData.count).bigEndian) { Array($0) })
|
|
685
|
+
payload.append(txIdData)
|
|
686
|
+
|
|
687
|
+
// Signed transaction TLV (tag 0x02) - optional
|
|
688
|
+
if let signedTx = signedTransaction, !signedTx.isEmpty {
|
|
689
|
+
let signedTxData = Data(signedTx.utf8)
|
|
690
|
+
payload.append(0x02)
|
|
691
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(signedTxData.count).bigEndian) { Array($0) })
|
|
692
|
+
payload.append(signedTxData)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Error TLV (tag 0x03) - optional
|
|
696
|
+
if let err = error, !err.isEmpty {
|
|
697
|
+
let errData = Data(err.utf8)
|
|
698
|
+
payload.append(0x03)
|
|
699
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(errData.count).bigEndian) { Array($0) })
|
|
700
|
+
payload.append(errData)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
var encryptedPayload = Data([NoisePayloadType.transactionResponse.rawValue])
|
|
704
|
+
encryptedPayload.append(payload)
|
|
705
|
+
|
|
706
|
+
// Send encrypted response
|
|
707
|
+
if let encrypted = self.encryptPayload(encryptedPayload, for: recipientPeerId) {
|
|
708
|
+
let packet = self.createPacket(
|
|
709
|
+
type: MessageType.noiseEncrypted.rawValue,
|
|
710
|
+
payload: encrypted,
|
|
711
|
+
recipientID: Data(hexString: recipientPeerId)
|
|
712
|
+
)
|
|
713
|
+
self.broadcastPacket(packet)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
DispatchQueue.main.async {
|
|
717
|
+
resolve(nil)
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
@objc(sendReadReceipt:recipientPeerId:resolver:rejecter:)
|
|
723
|
+
func sendReadReceipt(_ messageId: String, recipientPeerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
724
|
+
messageQueue.async { [weak self] in
|
|
725
|
+
guard let self = self else { return }
|
|
726
|
+
|
|
727
|
+
// Create read receipt payload
|
|
728
|
+
var payload = Data([NoisePayloadType.readReceipt.rawValue])
|
|
729
|
+
payload.append(contentsOf: messageId.utf8)
|
|
730
|
+
|
|
731
|
+
if let encrypted = self.encryptPayload(payload, for: recipientPeerId) {
|
|
732
|
+
let packet = self.createPacket(
|
|
733
|
+
type: MessageType.noiseEncrypted.rawValue,
|
|
734
|
+
payload: encrypted,
|
|
735
|
+
recipientID: Data(hexString: recipientPeerId)
|
|
736
|
+
)
|
|
737
|
+
self.broadcastPacket(packet)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
DispatchQueue.main.async {
|
|
741
|
+
resolve(nil)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
@objc(hasEncryptedSession:resolver:rejecter:)
|
|
747
|
+
func hasEncryptedSession(_ peerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
748
|
+
resolve(sessions[peerId] != nil)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
@objc(initiateHandshake:resolver:rejecter:)
|
|
752
|
+
func initiateHandshake(_ peerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
753
|
+
initiateHandshakeInternal(with: peerId)
|
|
754
|
+
resolve(nil)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
@objc(getIdentityFingerprint:rejecter:)
|
|
758
|
+
func getIdentityFingerprint(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
759
|
+
if let pk = privateKey {
|
|
760
|
+
let fingerprint = SHA256.hash(data: pk.publicKey.rawRepresentation)
|
|
761
|
+
let fingerprintHex = fingerprint.map { String(format: "%02x", $0) }.joined()
|
|
762
|
+
resolve(fingerprintHex)
|
|
763
|
+
} else {
|
|
764
|
+
resolve(nil)
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
@objc(getPeerFingerprint:resolver:rejecter:)
|
|
769
|
+
func getPeerFingerprint(_ peerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
770
|
+
if let peer = peers[peerId], let publicKey = peer.noisePublicKey {
|
|
771
|
+
let fingerprint = SHA256.hash(data: publicKey)
|
|
772
|
+
let fingerprintHex = fingerprint.map { String(format: "%02x", $0) }.joined()
|
|
773
|
+
resolve(fingerprintHex)
|
|
774
|
+
} else {
|
|
775
|
+
resolve(nil)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
@objc(broadcastAnnounce:rejecter:)
|
|
780
|
+
func broadcastAnnounce(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
781
|
+
sendAnnounce()
|
|
782
|
+
resolve(nil)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// MARK: - Private Methods
|
|
786
|
+
|
|
787
|
+
private func sendAnnounce() {
|
|
788
|
+
guard let publicKey = privateKey?.publicKey.rawRepresentation,
|
|
789
|
+
let signingPublicKey = signingKey?.publicKey.rawRepresentation else { return }
|
|
790
|
+
|
|
791
|
+
// TLV-encoded announcement
|
|
792
|
+
var payload = Data()
|
|
793
|
+
|
|
794
|
+
// Nickname TLV (tag 0x01)
|
|
795
|
+
let nicknameData = Data(myNickname.utf8)
|
|
796
|
+
payload.append(0x01)
|
|
797
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(nicknameData.count).bigEndian) { Array($0) })
|
|
798
|
+
payload.append(nicknameData)
|
|
799
|
+
|
|
800
|
+
// Noise public key TLV (tag 0x02)
|
|
801
|
+
payload.append(0x02)
|
|
802
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(publicKey.count).bigEndian) { Array($0) })
|
|
803
|
+
payload.append(publicKey)
|
|
804
|
+
|
|
805
|
+
// Signing public key TLV (tag 0x03)
|
|
806
|
+
payload.append(0x03)
|
|
807
|
+
payload.append(contentsOf: withUnsafeBytes(of: UInt16(signingPublicKey.count).bigEndian) { Array($0) })
|
|
808
|
+
payload.append(signingPublicKey)
|
|
809
|
+
|
|
810
|
+
let packet = createPacket(type: MessageType.announce.rawValue, payload: payload, recipientID: nil)
|
|
811
|
+
broadcastPacket(packet)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
private func sendLeaveAnnouncement() {
|
|
815
|
+
let packet = createPacket(type: MessageType.leave.rawValue, payload: Data(), recipientID: nil)
|
|
816
|
+
broadcastPacket(packet)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private func initiateHandshakeInternal(with peerId: String) {
|
|
820
|
+
guard let publicKey = privateKey?.publicKey.rawRepresentation else { return }
|
|
821
|
+
|
|
822
|
+
let packet = createPacket(
|
|
823
|
+
type: MessageType.noiseHandshake.rawValue,
|
|
824
|
+
payload: publicKey,
|
|
825
|
+
recipientID: Data(hexString: peerId)
|
|
826
|
+
)
|
|
827
|
+
broadcastPacket(packet)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private func createPacket(type: UInt8, payload: Data, recipientID: Data?) -> BitchatPacket {
|
|
831
|
+
var packet = BitchatPacket(
|
|
832
|
+
version: 1,
|
|
833
|
+
type: type,
|
|
834
|
+
senderID: myPeerIDData,
|
|
835
|
+
recipientID: recipientID,
|
|
836
|
+
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
|
837
|
+
payload: payload,
|
|
838
|
+
signature: nil,
|
|
839
|
+
ttl: messageTTL
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
// Sign the packet
|
|
843
|
+
if let sk = signingKey {
|
|
844
|
+
let dataToSign = packet.dataForSigning()
|
|
845
|
+
if let signature = try? sk.signature(for: dataToSign) {
|
|
846
|
+
packet.signature = signature
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return packet
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private func broadcastPacket(_ packet: BitchatPacket) {
|
|
854
|
+
guard let data = packet.encode() else { return }
|
|
855
|
+
|
|
856
|
+
// Send to all connected peripherals
|
|
857
|
+
for (_, peripheral) in peripherals {
|
|
858
|
+
if let services = peripheral.services,
|
|
859
|
+
let service = services.first(where: { $0.uuid == BleMesh.serviceUUID }),
|
|
860
|
+
let char = service.characteristics?.first(where: { $0.uuid == BleMesh.characteristicUUID }) {
|
|
861
|
+
peripheral.writeValue(data, for: char, type: .withoutResponse)
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Notify all subscribed centrals
|
|
866
|
+
if let char = characteristic {
|
|
867
|
+
peripheralManager?.updateValue(data, for: char, onSubscribedCentrals: nil)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private func handleReceivedPacket(_ data: Data, from peripheralUUID: String?) {
|
|
872
|
+
guard let packet = BitchatPacket.decode(from: data) else { return }
|
|
873
|
+
|
|
874
|
+
let senderID = packet.senderID.map { String(format: "%02x", $0) }.joined()
|
|
875
|
+
|
|
876
|
+
// Skip our own packets
|
|
877
|
+
if senderID == myPeerID { return }
|
|
878
|
+
|
|
879
|
+
// Deduplication
|
|
880
|
+
let messageID = "\(senderID)-\(packet.timestamp)-\(packet.type)"
|
|
881
|
+
if processedMessages.contains(messageID) { return }
|
|
882
|
+
processedMessages.insert(messageID)
|
|
883
|
+
|
|
884
|
+
// Handle by type
|
|
885
|
+
switch MessageType(rawValue: packet.type) {
|
|
886
|
+
case .announce:
|
|
887
|
+
handleAnnounce(packet, from: senderID)
|
|
888
|
+
case .message:
|
|
889
|
+
handleMessage(packet, from: senderID)
|
|
890
|
+
case .noiseHandshake:
|
|
891
|
+
handleNoiseHandshake(packet, from: senderID)
|
|
892
|
+
case .noiseEncrypted:
|
|
893
|
+
handleNoiseEncrypted(packet, from: senderID)
|
|
894
|
+
case .leave:
|
|
895
|
+
handleLeave(from: senderID)
|
|
896
|
+
case .fileTransfer:
|
|
897
|
+
handleFileTransfer(packet, from: senderID)
|
|
898
|
+
case .fragment:
|
|
899
|
+
handleFragment(packet, from: senderID)
|
|
900
|
+
case .solanaTransaction:
|
|
901
|
+
handleTransactionMetadata(packet, from: senderID)
|
|
902
|
+
default:
|
|
903
|
+
break
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Relay if TTL > 0
|
|
907
|
+
if packet.ttl > 0 {
|
|
908
|
+
var relayPacket = packet
|
|
909
|
+
relayPacket.ttl -= 1
|
|
910
|
+
if let data = relayPacket.encode() {
|
|
911
|
+
bleQueue.asyncAfter(deadline: .now() + Double.random(in: 0.01...0.1)) { [weak self] in
|
|
912
|
+
// Relay to all except source
|
|
913
|
+
self?.relayPacket(data, excluding: peripheralUUID)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private func relayPacket(_ data: Data, excluding peripheralUUID: String?) {
|
|
920
|
+
for (uuid, peripheral) in peripherals where uuid != peripheralUUID {
|
|
921
|
+
if let services = peripheral.services,
|
|
922
|
+
let service = services.first(where: { $0.uuid == BleMesh.serviceUUID }),
|
|
923
|
+
let char = service.characteristics?.first(where: { $0.uuid == BleMesh.characteristicUUID }) {
|
|
924
|
+
peripheral.writeValue(data, for: char, type: .withoutResponse)
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if let char = characteristic {
|
|
929
|
+
peripheralManager?.updateValue(data, for: char, onSubscribedCentrals: nil)
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
private func handleAnnounce(_ packet: BitchatPacket, from senderID: String) {
|
|
934
|
+
// Parse TLV payload
|
|
935
|
+
var nickname = senderID
|
|
936
|
+
var noisePublicKey: Data?
|
|
937
|
+
var signingPublicKey: Data?
|
|
938
|
+
|
|
939
|
+
var offset = 0
|
|
940
|
+
while offset < packet.payload.count {
|
|
941
|
+
guard offset + 3 <= packet.payload.count else { break }
|
|
942
|
+
|
|
943
|
+
let tag = packet.payload[offset]
|
|
944
|
+
let length = Int(packet.payload[offset + 1]) << 8 | Int(packet.payload[offset + 2])
|
|
945
|
+
offset += 3
|
|
946
|
+
|
|
947
|
+
guard offset + length <= packet.payload.count else { break }
|
|
948
|
+
let value = packet.payload.subdata(in: offset..<(offset + length))
|
|
949
|
+
offset += length
|
|
950
|
+
|
|
951
|
+
switch tag {
|
|
952
|
+
case 0x01:
|
|
953
|
+
nickname = String(data: value, encoding: .utf8) ?? senderID
|
|
954
|
+
case 0x02:
|
|
955
|
+
noisePublicKey = value
|
|
956
|
+
case 0x03:
|
|
957
|
+
signingPublicKey = value
|
|
958
|
+
default:
|
|
959
|
+
break
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Update peer info
|
|
964
|
+
peers[senderID] = PeerInfo(
|
|
965
|
+
peerId: senderID,
|
|
966
|
+
nickname: nickname,
|
|
967
|
+
isConnected: true,
|
|
968
|
+
rssi: nil,
|
|
969
|
+
lastSeen: Date(),
|
|
970
|
+
noisePublicKey: noisePublicKey,
|
|
971
|
+
isVerified: false
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
notifyPeerListUpdated()
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
private func handleMessage(_ packet: BitchatPacket, from senderID: String) {
|
|
978
|
+
guard let content = String(data: packet.payload, encoding: .utf8) else { return }
|
|
979
|
+
|
|
980
|
+
let nickname = peers[senderID]?.nickname ?? senderID
|
|
981
|
+
|
|
982
|
+
let message: [String: Any] = [
|
|
983
|
+
"id": UUID().uuidString,
|
|
984
|
+
"content": content,
|
|
985
|
+
"senderPeerId": senderID,
|
|
986
|
+
"senderNickname": nickname,
|
|
987
|
+
"timestamp": Int(packet.timestamp),
|
|
988
|
+
"isPrivate": false
|
|
989
|
+
]
|
|
990
|
+
|
|
991
|
+
sendEvent(withName: "onMessageReceived", body: ["message": message])
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
private func handleNoiseHandshake(_ packet: BitchatPacket, from senderID: String) {
|
|
995
|
+
// Store peer's public key and derive shared secret
|
|
996
|
+
guard packet.payload.count == 32,
|
|
997
|
+
let peerPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: packet.payload),
|
|
998
|
+
let pk = privateKey else { return }
|
|
999
|
+
|
|
1000
|
+
do {
|
|
1001
|
+
let sharedSecret = try pk.sharedSecretFromKeyAgreement(with: peerPublicKey)
|
|
1002
|
+
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
|
|
1003
|
+
using: SHA256.self,
|
|
1004
|
+
salt: Data(),
|
|
1005
|
+
sharedInfo: Data("mesh-encryption".utf8),
|
|
1006
|
+
outputByteCount: 32
|
|
1007
|
+
)
|
|
1008
|
+
sessions[senderID] = symmetricKey.withUnsafeBytes { Data($0) }
|
|
1009
|
+
|
|
1010
|
+
// Update peer's noise public key
|
|
1011
|
+
if var peer = peers[senderID] {
|
|
1012
|
+
peer.noisePublicKey = packet.payload
|
|
1013
|
+
peers[senderID] = peer
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Send response handshake if this is an incoming request
|
|
1017
|
+
if packet.recipientID == nil || packet.recipientID == myPeerIDData {
|
|
1018
|
+
initiateHandshakeInternal(with: senderID)
|
|
1019
|
+
}
|
|
1020
|
+
} catch {
|
|
1021
|
+
sendEvent(withName: "onError", body: ["code": "HANDSHAKE_ERROR", "message": error.localizedDescription])
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
private func handleNoiseEncrypted(_ packet: BitchatPacket, from senderID: String) {
|
|
1026
|
+
// Check if message is for us
|
|
1027
|
+
if let recipientID = packet.recipientID,
|
|
1028
|
+
recipientID != myPeerIDData {
|
|
1029
|
+
return
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
guard let sessionKey = sessions[senderID] else { return }
|
|
1033
|
+
|
|
1034
|
+
// Decrypt
|
|
1035
|
+
guard let decrypted = decryptPayload(packet.payload, with: sessionKey) else { return }
|
|
1036
|
+
|
|
1037
|
+
// Parse payload type
|
|
1038
|
+
guard decrypted.count > 0 else { return }
|
|
1039
|
+
let payloadType = decrypted[0]
|
|
1040
|
+
let payloadData = decrypted.dropFirst()
|
|
1041
|
+
|
|
1042
|
+
switch NoisePayloadType(rawValue: payloadType) {
|
|
1043
|
+
case .privateMessage:
|
|
1044
|
+
handlePrivateMessage(Data(payloadData), from: senderID)
|
|
1045
|
+
case .readReceipt:
|
|
1046
|
+
if let messageId = String(data: payloadData, encoding: .utf8) {
|
|
1047
|
+
sendEvent(withName: "onReadReceipt", body: ["messageId": messageId, "fromPeerId": senderID])
|
|
1048
|
+
}
|
|
1049
|
+
case .deliveryAck:
|
|
1050
|
+
if let messageId = String(data: payloadData, encoding: .utf8) {
|
|
1051
|
+
sendEvent(withName: "onDeliveryAck", body: ["messageId": messageId, "fromPeerId": senderID])
|
|
1052
|
+
}
|
|
1053
|
+
case .solanaTransaction:
|
|
1054
|
+
handleSolanaTransaction(Data(payloadData), from: senderID)
|
|
1055
|
+
case .transactionResponse:
|
|
1056
|
+
handleTransactionResponse(Data(payloadData), from: senderID)
|
|
1057
|
+
default:
|
|
1058
|
+
break
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private func handlePrivateMessage(_ data: Data, from senderID: String) {
|
|
1063
|
+
// Parse TLV private message
|
|
1064
|
+
var messageId: String?
|
|
1065
|
+
var content: String?
|
|
1066
|
+
|
|
1067
|
+
var offset = 0
|
|
1068
|
+
while offset < data.count {
|
|
1069
|
+
guard offset + 3 <= data.count else { break }
|
|
1070
|
+
let tag = data[offset]
|
|
1071
|
+
let length = Int(data[offset + 1]) << 8 | Int(data[offset + 2])
|
|
1072
|
+
offset += 3
|
|
1073
|
+
|
|
1074
|
+
guard offset + length <= data.count else { break }
|
|
1075
|
+
let value = data.subdata(in: offset..<(offset + length))
|
|
1076
|
+
offset += length
|
|
1077
|
+
|
|
1078
|
+
switch tag {
|
|
1079
|
+
case 0x01:
|
|
1080
|
+
messageId = String(data: value, encoding: .utf8)
|
|
1081
|
+
case 0x02:
|
|
1082
|
+
content = String(data: value, encoding: .utf8)
|
|
1083
|
+
default:
|
|
1084
|
+
break
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
guard let id = messageId, let text = content else { return }
|
|
1089
|
+
let nickname = peers[senderID]?.nickname ?? senderID
|
|
1090
|
+
|
|
1091
|
+
let message: [String: Any] = [
|
|
1092
|
+
"id": id,
|
|
1093
|
+
"content": text,
|
|
1094
|
+
"senderPeerId": senderID,
|
|
1095
|
+
"senderNickname": nickname,
|
|
1096
|
+
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
1097
|
+
"isPrivate": true
|
|
1098
|
+
]
|
|
1099
|
+
|
|
1100
|
+
sendEvent(withName: "onMessageReceived", body: ["message": message])
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
private func handleSolanaTransaction(_ data: Data, from senderID: String) {
|
|
1104
|
+
// Parse TLV Solana transaction
|
|
1105
|
+
var txId: String?
|
|
1106
|
+
var serializedTransaction: String?
|
|
1107
|
+
var firstSignerPublicKey: String?
|
|
1108
|
+
var secondSignerPublicKey: String?
|
|
1109
|
+
var description: String?
|
|
1110
|
+
|
|
1111
|
+
var offset = 0
|
|
1112
|
+
while offset < data.count {
|
|
1113
|
+
guard offset + 3 <= data.count else { break }
|
|
1114
|
+
|
|
1115
|
+
let tag = data[offset]
|
|
1116
|
+
let length = Int(data[offset + 1]) << 8 | Int(data[offset + 2])
|
|
1117
|
+
offset += 3
|
|
1118
|
+
|
|
1119
|
+
guard offset + length <= data.count else { break }
|
|
1120
|
+
let value = data.subdata(in: offset..<(offset + length))
|
|
1121
|
+
offset += length
|
|
1122
|
+
|
|
1123
|
+
switch tag {
|
|
1124
|
+
case 0x01:
|
|
1125
|
+
txId = String(data: value, encoding: .utf8)
|
|
1126
|
+
case 0x02:
|
|
1127
|
+
serializedTransaction = String(data: value, encoding: .utf8)
|
|
1128
|
+
case 0x03:
|
|
1129
|
+
firstSignerPublicKey = String(data: value, encoding: .utf8)
|
|
1130
|
+
case 0x04:
|
|
1131
|
+
secondSignerPublicKey = String(data: value, encoding: .utf8)
|
|
1132
|
+
case 0x05:
|
|
1133
|
+
description = String(data: value, encoding: .utf8)
|
|
1134
|
+
default:
|
|
1135
|
+
break
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
guard let id = txId, let tx = serializedTransaction, let firstSigner = firstSignerPublicKey else {
|
|
1140
|
+
print("Invalid Solana transaction data")
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
var txDict: [String: Any] = [
|
|
1145
|
+
"id": id,
|
|
1146
|
+
"serializedTransaction": tx,
|
|
1147
|
+
"senderPeerId": senderID,
|
|
1148
|
+
"firstSignerPublicKey": firstSigner,
|
|
1149
|
+
"timestamp": Int(Date().timeIntervalSince1970 * 1000),
|
|
1150
|
+
"requiresSecondSigner": true,
|
|
1151
|
+
"openForAnySigner": secondSignerPublicKey == nil
|
|
1152
|
+
]
|
|
1153
|
+
|
|
1154
|
+
// Second signer is optional - any peer can sign
|
|
1155
|
+
if let secondSigner = secondSignerPublicKey {
|
|
1156
|
+
txDict["secondSignerPublicKey"] = secondSigner
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if let desc = description {
|
|
1160
|
+
txDict["description"] = desc
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
sendEvent(withName: "onTransactionReceived", body: ["transaction": txDict])
|
|
1164
|
+
print("Received Solana transaction \(id) from \(senderID) (openForAnySigner=\(secondSignerPublicKey == nil))")
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
private func handleTransactionResponse(_ data: Data, from senderID: String) {
|
|
1168
|
+
// Parse TLV transaction response
|
|
1169
|
+
var txId: String?
|
|
1170
|
+
var signedTransaction: String?
|
|
1171
|
+
var error: String?
|
|
1172
|
+
|
|
1173
|
+
var offset = 0
|
|
1174
|
+
while offset < data.count {
|
|
1175
|
+
guard offset + 3 <= data.count else { break }
|
|
1176
|
+
|
|
1177
|
+
let tag = data[offset]
|
|
1178
|
+
let length = Int(data[offset + 1]) << 8 | Int(data[offset + 2])
|
|
1179
|
+
offset += 3
|
|
1180
|
+
|
|
1181
|
+
guard offset + length <= data.count else { break }
|
|
1182
|
+
let value = data.subdata(in: offset..<(offset + length))
|
|
1183
|
+
offset += length
|
|
1184
|
+
|
|
1185
|
+
switch tag {
|
|
1186
|
+
case 0x01:
|
|
1187
|
+
txId = String(data: value, encoding: .utf8)
|
|
1188
|
+
case 0x02:
|
|
1189
|
+
signedTransaction = String(data: value, encoding: .utf8)
|
|
1190
|
+
case 0x03:
|
|
1191
|
+
error = String(data: value, encoding: .utf8)
|
|
1192
|
+
default:
|
|
1193
|
+
break
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
guard let id = txId else {
|
|
1198
|
+
print("Invalid transaction response")
|
|
1199
|
+
return
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
var responseDict: [String: Any] = [
|
|
1203
|
+
"id": id,
|
|
1204
|
+
"responderPeerId": senderID,
|
|
1205
|
+
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
|
|
1206
|
+
]
|
|
1207
|
+
|
|
1208
|
+
if let signedTx = signedTransaction {
|
|
1209
|
+
responseDict["signedTransaction"] = signedTx
|
|
1210
|
+
}
|
|
1211
|
+
if let err = error {
|
|
1212
|
+
responseDict["error"] = err
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
sendEvent(withName: "onTransactionResponse", body: ["response": responseDict])
|
|
1216
|
+
print("Received transaction response for \(id) from \(senderID)")
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private func handleLeave(from senderID: String) {
|
|
1220
|
+
peers.removeValue(forKey: senderID)
|
|
1221
|
+
sessions.removeValue(forKey: senderID)
|
|
1222
|
+
notifyPeerListUpdated()
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
private func encryptMessage(_ content: String, for peerId: String) -> Data? {
|
|
1226
|
+
// Create private message TLV
|
|
1227
|
+
let messageId = UUID().uuidString
|
|
1228
|
+
var tlvData = Data()
|
|
1229
|
+
|
|
1230
|
+
// Message ID TLV (tag 0x01)
|
|
1231
|
+
let idData = Data(messageId.utf8)
|
|
1232
|
+
tlvData.append(0x01)
|
|
1233
|
+
tlvData.append(contentsOf: withUnsafeBytes(of: UInt16(idData.count).bigEndian) { Array($0) })
|
|
1234
|
+
tlvData.append(idData)
|
|
1235
|
+
|
|
1236
|
+
// Content TLV (tag 0x02)
|
|
1237
|
+
let contentData = Data(content.utf8)
|
|
1238
|
+
tlvData.append(0x02)
|
|
1239
|
+
tlvData.append(contentsOf: withUnsafeBytes(of: UInt16(contentData.count).bigEndian) { Array($0) })
|
|
1240
|
+
tlvData.append(contentData)
|
|
1241
|
+
|
|
1242
|
+
// Add payload type prefix
|
|
1243
|
+
var payload = Data([NoisePayloadType.privateMessage.rawValue])
|
|
1244
|
+
payload.append(tlvData)
|
|
1245
|
+
|
|
1246
|
+
return encryptPayload(payload, for: peerId)
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
private func encryptPayload(_ payload: Data, for peerId: String) -> Data? {
|
|
1250
|
+
guard let sessionKey = sessions[peerId] else { return nil }
|
|
1251
|
+
|
|
1252
|
+
do {
|
|
1253
|
+
let key = SymmetricKey(data: sessionKey)
|
|
1254
|
+
let nonce = AES.GCM.Nonce()
|
|
1255
|
+
let sealedBox = try AES.GCM.seal(payload, using: key, nonce: nonce)
|
|
1256
|
+
return sealedBox.combined
|
|
1257
|
+
} catch {
|
|
1258
|
+
return nil
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
private func decryptPayload(_ encrypted: Data, with sessionKey: Data) -> Data? {
|
|
1263
|
+
do {
|
|
1264
|
+
let key = SymmetricKey(data: sessionKey)
|
|
1265
|
+
let sealedBox = try AES.GCM.SealedBox(combined: encrypted)
|
|
1266
|
+
return try AES.GCM.open(sealedBox, using: key)
|
|
1267
|
+
} catch {
|
|
1268
|
+
return nil
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
private func notifyPeerListUpdated() {
|
|
1273
|
+
let peerList = peers.values.map { peer in
|
|
1274
|
+
return [
|
|
1275
|
+
"peerId": peer.peerId,
|
|
1276
|
+
"nickname": peer.nickname,
|
|
1277
|
+
"isConnected": peer.isConnected,
|
|
1278
|
+
"rssi": peer.rssi ?? NSNull(),
|
|
1279
|
+
"lastSeen": Int(peer.lastSeen.timeIntervalSince1970 * 1000),
|
|
1280
|
+
"isVerified": peer.isVerified
|
|
1281
|
+
] as [String: Any]
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
sendEvent(withName: "onPeerListUpdated", body: ["peers": peerList])
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
private func getMimeType(for fileName: String) -> String {
|
|
1288
|
+
let ext = (fileName as NSString).pathExtension.lowercased()
|
|
1289
|
+
switch ext {
|
|
1290
|
+
case "jpg", "jpeg": return "image/jpeg"
|
|
1291
|
+
case "png": return "image/png"
|
|
1292
|
+
case "gif": return "image/gif"
|
|
1293
|
+
case "pdf": return "application/pdf"
|
|
1294
|
+
case "txt": return "text/plain"
|
|
1295
|
+
case "mp4": return "video/mp4"
|
|
1296
|
+
case "mp3": return "audio/mpeg"
|
|
1297
|
+
default: return "application/octet-stream"
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
private func handleFileTransfer(_ packet: BitchatPacket, from senderID: String) {
|
|
1302
|
+
// Parse file transfer metadata
|
|
1303
|
+
var transferId: String?
|
|
1304
|
+
var fileName: String?
|
|
1305
|
+
var fileSize: Int?
|
|
1306
|
+
var mimeType: String?
|
|
1307
|
+
var totalChunks: Int?
|
|
1308
|
+
|
|
1309
|
+
var offset = 0
|
|
1310
|
+
while offset < packet.payload.count {
|
|
1311
|
+
guard offset + 3 <= packet.payload.count else { break }
|
|
1312
|
+
|
|
1313
|
+
let tag = packet.payload[offset]
|
|
1314
|
+
let length = Int(packet.payload[offset + 1]) << 8 | Int(packet.payload[offset + 2])
|
|
1315
|
+
offset += 3
|
|
1316
|
+
|
|
1317
|
+
guard offset + length <= packet.payload.count else { break }
|
|
1318
|
+
let value = packet.payload.subdata(in: offset..<(offset + length))
|
|
1319
|
+
offset += length
|
|
1320
|
+
|
|
1321
|
+
switch tag {
|
|
1322
|
+
case 0x01:
|
|
1323
|
+
transferId = String(data: value, encoding: .utf8)
|
|
1324
|
+
case 0x02:
|
|
1325
|
+
fileName = String(data: value, encoding: .utf8)
|
|
1326
|
+
case 0x03 where value.count == 4:
|
|
1327
|
+
fileSize = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
|
|
1328
|
+
case 0x04:
|
|
1329
|
+
mimeType = String(data: value, encoding: .utf8)
|
|
1330
|
+
case 0x05 where value.count == 4:
|
|
1331
|
+
totalChunks = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
|
|
1332
|
+
default:
|
|
1333
|
+
break
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
guard let id = transferId, let name = fileName, let size = fileSize, let mime = mimeType, let chunks = totalChunks else {
|
|
1338
|
+
print("Invalid file transfer metadata")
|
|
1339
|
+
return
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Store transfer info
|
|
1343
|
+
activeFileTransfers[id] = FileTransfer(
|
|
1344
|
+
id: id,
|
|
1345
|
+
fileName: name,
|
|
1346
|
+
fileSize: size,
|
|
1347
|
+
mimeType: mime,
|
|
1348
|
+
senderPeerId: senderID,
|
|
1349
|
+
totalChunks: chunks,
|
|
1350
|
+
receivedChunks: []
|
|
1351
|
+
)
|
|
1352
|
+
fileTransferFragments[id] = [:]
|
|
1353
|
+
|
|
1354
|
+
print("Started receiving file: \(name) (\(size) bytes, \(chunks) chunks)")
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
private func handleFragment(_ packet: BitchatPacket, from senderID: String) {
|
|
1358
|
+
// Parse fragment - could be file or transaction chunk
|
|
1359
|
+
var id: String?
|
|
1360
|
+
var chunkIndex: Int?
|
|
1361
|
+
var totalChunks: Int?
|
|
1362
|
+
var chunkData: Data?
|
|
1363
|
+
|
|
1364
|
+
var offset = 0
|
|
1365
|
+
while offset < packet.payload.count {
|
|
1366
|
+
guard offset + 3 <= packet.payload.count else { break }
|
|
1367
|
+
|
|
1368
|
+
let tag = packet.payload[offset]
|
|
1369
|
+
let length = Int(packet.payload[offset + 1]) << 8 | Int(packet.payload[offset + 2])
|
|
1370
|
+
offset += 3
|
|
1371
|
+
|
|
1372
|
+
guard offset + length <= packet.payload.count else { break }
|
|
1373
|
+
let value = packet.payload.subdata(in: offset..<(offset + length))
|
|
1374
|
+
offset += length
|
|
1375
|
+
|
|
1376
|
+
switch tag {
|
|
1377
|
+
case 0x01:
|
|
1378
|
+
id = String(data: value, encoding: .utf8)
|
|
1379
|
+
case 0x02 where value.count == 4:
|
|
1380
|
+
chunkIndex = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
|
|
1381
|
+
case 0x03 where value.count == 4:
|
|
1382
|
+
totalChunks = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
|
|
1383
|
+
case 0x04:
|
|
1384
|
+
chunkData = value
|
|
1385
|
+
default:
|
|
1386
|
+
break
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
guard let fragmentId = id, let index = chunkIndex, let chunks = totalChunks, let data = chunkData else {
|
|
1391
|
+
print("Invalid fragment")
|
|
1392
|
+
return
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// Check if this is a file fragment or transaction chunk
|
|
1396
|
+
if activeFileTransfers[fragmentId] != nil {
|
|
1397
|
+
handleFileFragment(id: fragmentId, chunkIndex: index, totalChunks: chunks, chunkData: data)
|
|
1398
|
+
} else if pendingTransactionChunks[fragmentId] != nil {
|
|
1399
|
+
handleTransactionChunk(id: fragmentId, chunkIndex: index, totalChunks: chunks, chunkData: data)
|
|
1400
|
+
} else {
|
|
1401
|
+
print("Received fragment for unknown transfer/transaction: \(fragmentId)")
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
private func handleFileFragment(id: String, chunkIndex: Int, totalChunks: Int, chunkData: Data) {
|
|
1406
|
+
guard var transfer = activeFileTransfers[id] else { return }
|
|
1407
|
+
|
|
1408
|
+
// Store the fragment
|
|
1409
|
+
fileTransferFragments[id]?[chunkIndex] = chunkData
|
|
1410
|
+
transfer.receivedChunks.insert(chunkIndex)
|
|
1411
|
+
activeFileTransfers[id] = transfer
|
|
1412
|
+
|
|
1413
|
+
print("Received file chunk \(chunkIndex)/\(totalChunks) for transfer \(id)")
|
|
1414
|
+
|
|
1415
|
+
// Check if we have all chunks
|
|
1416
|
+
if transfer.receivedChunks.count == transfer.totalChunks {
|
|
1417
|
+
// Reassemble file
|
|
1418
|
+
var reassembledData = Data()
|
|
1419
|
+
for i in 0..<transfer.totalChunks {
|
|
1420
|
+
if let chunk = fileTransferFragments[id]?[i] {
|
|
1421
|
+
reassembledData.append(chunk)
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Convert to base64
|
|
1426
|
+
let base64Data = reassembledData.base64EncodedString()
|
|
1427
|
+
|
|
1428
|
+
// Emit file received event
|
|
1429
|
+
let fileDict: [String: Any] = [
|
|
1430
|
+
"id": transfer.id,
|
|
1431
|
+
"fileName": transfer.fileName,
|
|
1432
|
+
"fileSize": transfer.fileSize,
|
|
1433
|
+
"mimeType": transfer.mimeType,
|
|
1434
|
+
"data": base64Data,
|
|
1435
|
+
"senderPeerId": transfer.senderPeerId,
|
|
1436
|
+
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
|
|
1437
|
+
]
|
|
1438
|
+
|
|
1439
|
+
sendEvent(withName: "onFileReceived", body: ["file": fileDict])
|
|
1440
|
+
|
|
1441
|
+
// Clean up
|
|
1442
|
+
activeFileTransfers.removeValue(forKey: id)
|
|
1443
|
+
fileTransferFragments.removeValue(forKey: id)
|
|
1444
|
+
|
|
1445
|
+
print("File transfer complete: \(transfer.fileName)")
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
private func handleTransactionChunk(id: String, chunkIndex: Int, totalChunks: Int, chunkData: Data) {
|
|
1450
|
+
guard var pendingTx = pendingTransactionChunks[id] else { return }
|
|
1451
|
+
|
|
1452
|
+
// Store the chunk
|
|
1453
|
+
pendingTx.chunks[chunkIndex] = chunkData
|
|
1454
|
+
pendingTransactionChunks[id] = pendingTx
|
|
1455
|
+
|
|
1456
|
+
print("Received transaction chunk \(chunkIndex)/\(totalChunks) for tx \(id)")
|
|
1457
|
+
|
|
1458
|
+
// Check if we have all chunks
|
|
1459
|
+
if pendingTx.chunks.count == pendingTx.totalChunks {
|
|
1460
|
+
// Reassemble encrypted data
|
|
1461
|
+
var reassembledData = Data()
|
|
1462
|
+
for i in 0..<pendingTx.totalChunks {
|
|
1463
|
+
if let chunk = pendingTx.chunks[i] {
|
|
1464
|
+
reassembledData.append(chunk)
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Decrypt and process
|
|
1469
|
+
if let sessionKey = sessions[pendingTx.senderId] {
|
|
1470
|
+
if let decrypted = decryptPayload(reassembledData, with: sessionKey),
|
|
1471
|
+
decrypted.count > 0 {
|
|
1472
|
+
// Skip the payload type byte and process as Solana transaction
|
|
1473
|
+
let payloadData = decrypted.subdata(in: 1..<decrypted.count)
|
|
1474
|
+
handleSolanaTransaction(payloadData, from: pendingTx.senderId)
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Clean up
|
|
1479
|
+
pendingTransactionChunks.removeValue(forKey: id)
|
|
1480
|
+
|
|
1481
|
+
print("Transaction receive complete: \(id)")
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
private func handleTransactionMetadata(_ packet: BitchatPacket, from senderID: String) {
|
|
1486
|
+
// Parse transaction chunk metadata
|
|
1487
|
+
var txId: String?
|
|
1488
|
+
var totalSize: Int?
|
|
1489
|
+
var totalChunks: Int?
|
|
1490
|
+
|
|
1491
|
+
var offset = 0
|
|
1492
|
+
while offset < packet.payload.count {
|
|
1493
|
+
guard offset + 3 <= packet.payload.count else { break }
|
|
1494
|
+
|
|
1495
|
+
let tag = packet.payload[offset]
|
|
1496
|
+
let length = Int(packet.payload[offset + 1]) << 8 | Int(packet.payload[offset + 2])
|
|
1497
|
+
offset += 3
|
|
1498
|
+
|
|
1499
|
+
guard offset + length <= packet.payload.count else { break }
|
|
1500
|
+
let value = packet.payload.subdata(in: offset..<(offset + length))
|
|
1501
|
+
offset += length
|
|
1502
|
+
|
|
1503
|
+
switch tag {
|
|
1504
|
+
case 0x01:
|
|
1505
|
+
txId = String(data: value, encoding: .utf8)
|
|
1506
|
+
case 0x02 where value.count == 4:
|
|
1507
|
+
totalSize = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
|
|
1508
|
+
case 0x03 where value.count == 4:
|
|
1509
|
+
totalChunks = value.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
|
|
1510
|
+
default:
|
|
1511
|
+
break
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
guard let id = txId, let size = totalSize, let chunks = totalChunks else {
|
|
1516
|
+
print("Invalid transaction metadata")
|
|
1517
|
+
return
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Store pending transaction chunks info
|
|
1521
|
+
pendingTransactionChunks[id] = TransactionChunks(
|
|
1522
|
+
txId: id,
|
|
1523
|
+
senderId: senderID,
|
|
1524
|
+
totalSize: size,
|
|
1525
|
+
totalChunks: chunks
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
print("Started receiving chunked transaction: \(id) (\(size) bytes, \(chunks) chunks)")
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// MARK: - CBCentralManagerDelegate
|
|
1533
|
+
|
|
1534
|
+
extension BleMesh: CBCentralManagerDelegate {
|
|
1535
|
+
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
1536
|
+
if central.state == .poweredOn && isRunning {
|
|
1537
|
+
central.scanForPeripherals(
|
|
1538
|
+
withServices: [BleMesh.serviceUUID],
|
|
1539
|
+
options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
|
|
1540
|
+
)
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
let state: String
|
|
1544
|
+
switch central.state {
|
|
1545
|
+
case .poweredOn: state = "connected"
|
|
1546
|
+
case .poweredOff, .unauthorized: state = "disconnected"
|
|
1547
|
+
default: state = "connecting"
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
sendEvent(withName: "onConnectionStateChanged", body: [
|
|
1551
|
+
"state": state,
|
|
1552
|
+
"peerCount": peers.count
|
|
1553
|
+
])
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
|
1557
|
+
let uuid = peripheral.identifier.uuidString
|
|
1558
|
+
|
|
1559
|
+
if peripherals[uuid] == nil {
|
|
1560
|
+
peripherals[uuid] = peripheral
|
|
1561
|
+
peripheral.delegate = self
|
|
1562
|
+
central.connect(peripheral, options: nil)
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
1567
|
+
peripheral.discoverServices([BleMesh.serviceUUID])
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
1571
|
+
let uuid = peripheral.identifier.uuidString
|
|
1572
|
+
|
|
1573
|
+
if let peerId = peripheralToPeer[uuid] {
|
|
1574
|
+
if var peer = peers[peerId] {
|
|
1575
|
+
peer.isConnected = false
|
|
1576
|
+
peers[peerId] = peer
|
|
1577
|
+
}
|
|
1578
|
+
notifyPeerListUpdated()
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// Reconnect
|
|
1582
|
+
if isRunning {
|
|
1583
|
+
central.connect(peripheral, options: nil)
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// MARK: - CBPeripheralDelegate
|
|
1589
|
+
|
|
1590
|
+
extension BleMesh: CBPeripheralDelegate {
|
|
1591
|
+
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
1592
|
+
guard let services = peripheral.services else { return }
|
|
1593
|
+
|
|
1594
|
+
for service in services where service.uuid == BleMesh.serviceUUID {
|
|
1595
|
+
peripheral.discoverCharacteristics([BleMesh.characteristicUUID], for: service)
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
1600
|
+
guard let characteristics = service.characteristics else { return }
|
|
1601
|
+
|
|
1602
|
+
for char in characteristics where char.uuid == BleMesh.characteristicUUID {
|
|
1603
|
+
peripheral.setNotifyValue(true, for: char)
|
|
1604
|
+
|
|
1605
|
+
// Send announce to new peer
|
|
1606
|
+
sendAnnounce()
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
1611
|
+
guard let data = characteristic.value else { return }
|
|
1612
|
+
handleReceivedPacket(data, from: peripheral.identifier.uuidString)
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// MARK: - CBPeripheralManagerDelegate
|
|
1617
|
+
|
|
1618
|
+
extension BleMesh: CBPeripheralManagerDelegate {
|
|
1619
|
+
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
|
1620
|
+
if peripheral.state == .poweredOn && isRunning {
|
|
1621
|
+
setupPeripheralService()
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
private func setupPeripheralService() {
|
|
1626
|
+
let char = CBMutableCharacteristic(
|
|
1627
|
+
type: BleMesh.characteristicUUID,
|
|
1628
|
+
properties: [.read, .write, .writeWithoutResponse, .notify],
|
|
1629
|
+
value: nil,
|
|
1630
|
+
permissions: [.readable, .writeable]
|
|
1631
|
+
)
|
|
1632
|
+
|
|
1633
|
+
let service = CBMutableService(type: BleMesh.serviceUUID, primary: true)
|
|
1634
|
+
service.characteristics = [char]
|
|
1635
|
+
|
|
1636
|
+
peripheralManager?.add(service)
|
|
1637
|
+
characteristic = char
|
|
1638
|
+
|
|
1639
|
+
peripheralManager?.startAdvertising([
|
|
1640
|
+
CBAdvertisementDataServiceUUIDsKey: [BleMesh.serviceUUID]
|
|
1641
|
+
])
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
|
1645
|
+
subscribedCentrals.append(central)
|
|
1646
|
+
sendAnnounce()
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
|
|
1650
|
+
subscribedCentrals.removeAll { $0.identifier == central.identifier }
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
|
1654
|
+
for request in requests {
|
|
1655
|
+
if let data = request.value {
|
|
1656
|
+
handleReceivedPacket(data, from: nil)
|
|
1657
|
+
}
|
|
1658
|
+
peripheral.respond(to: request, withResult: .success)
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// MARK: - Supporting Types
|
|
1664
|
+
|
|
1665
|
+
enum MessageType: UInt8 {
|
|
1666
|
+
case announce = 0x01
|
|
1667
|
+
case message = 0x02
|
|
1668
|
+
case leave = 0x03
|
|
1669
|
+
case noiseHandshake = 0x04
|
|
1670
|
+
case noiseEncrypted = 0x05
|
|
1671
|
+
case fileTransfer = 0x06
|
|
1672
|
+
case fragment = 0x07
|
|
1673
|
+
case requestSync = 0x08
|
|
1674
|
+
case solanaTransaction = 0x09
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
enum NoisePayloadType: UInt8 {
|
|
1678
|
+
case privateMessage = 0x01
|
|
1679
|
+
case readReceipt = 0x02
|
|
1680
|
+
case deliveryAck = 0x03
|
|
1681
|
+
case fileTransfer = 0x04
|
|
1682
|
+
case verifyChallenge = 0x05
|
|
1683
|
+
case verifyResponse = 0x06
|
|
1684
|
+
case solanaTransaction = 0x07
|
|
1685
|
+
case transactionResponse = 0x08
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
struct BitchatPacket {
|
|
1689
|
+
var version: UInt8 = 1
|
|
1690
|
+
var type: UInt8
|
|
1691
|
+
var senderID: Data
|
|
1692
|
+
var recipientID: Data?
|
|
1693
|
+
var timestamp: UInt64
|
|
1694
|
+
var payload: Data
|
|
1695
|
+
var signature: Data?
|
|
1696
|
+
var ttl: UInt8
|
|
1697
|
+
|
|
1698
|
+
func dataForSigning() -> Data {
|
|
1699
|
+
var data = Data()
|
|
1700
|
+
data.append(version)
|
|
1701
|
+
data.append(type)
|
|
1702
|
+
data.append(senderID)
|
|
1703
|
+
if let recipientID = recipientID {
|
|
1704
|
+
data.append(recipientID)
|
|
1705
|
+
}
|
|
1706
|
+
data.append(contentsOf: withUnsafeBytes(of: timestamp.bigEndian) { Array($0) })
|
|
1707
|
+
data.append(payload)
|
|
1708
|
+
data.append(ttl)
|
|
1709
|
+
return data
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
func encode() -> Data? {
|
|
1713
|
+
var data = Data()
|
|
1714
|
+
|
|
1715
|
+
// Version
|
|
1716
|
+
data.append(version)
|
|
1717
|
+
|
|
1718
|
+
// Type
|
|
1719
|
+
data.append(type)
|
|
1720
|
+
|
|
1721
|
+
// TTL
|
|
1722
|
+
data.append(ttl)
|
|
1723
|
+
|
|
1724
|
+
// Sender ID (8 bytes)
|
|
1725
|
+
data.append(senderID.prefix(8))
|
|
1726
|
+
if senderID.count < 8 {
|
|
1727
|
+
data.append(Data(count: 8 - senderID.count))
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// Recipient ID (8 bytes, or zeros for broadcast)
|
|
1731
|
+
if let recipientID = recipientID {
|
|
1732
|
+
data.append(recipientID.prefix(8))
|
|
1733
|
+
if recipientID.count < 8 {
|
|
1734
|
+
data.append(Data(count: 8 - recipientID.count))
|
|
1735
|
+
}
|
|
1736
|
+
} else {
|
|
1737
|
+
data.append(Data(count: 8))
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// Timestamp (8 bytes)
|
|
1741
|
+
data.append(contentsOf: withUnsafeBytes(of: timestamp.bigEndian) { Array($0) })
|
|
1742
|
+
|
|
1743
|
+
// Payload length (2 bytes)
|
|
1744
|
+
let payloadLength = UInt16(payload.count)
|
|
1745
|
+
data.append(contentsOf: withUnsafeBytes(of: payloadLength.bigEndian) { Array($0) })
|
|
1746
|
+
|
|
1747
|
+
// Payload
|
|
1748
|
+
data.append(payload)
|
|
1749
|
+
|
|
1750
|
+
// Signature (64 bytes if present)
|
|
1751
|
+
if let signature = signature {
|
|
1752
|
+
data.append(signature)
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
return data
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
static func decode(from data: Data) -> BitchatPacket? {
|
|
1759
|
+
guard data.count >= 29 else { return nil } // Minimum packet size
|
|
1760
|
+
|
|
1761
|
+
var offset = 0
|
|
1762
|
+
|
|
1763
|
+
// Version
|
|
1764
|
+
let version = data[offset]
|
|
1765
|
+
offset += 1
|
|
1766
|
+
|
|
1767
|
+
// Type
|
|
1768
|
+
let type = data[offset]
|
|
1769
|
+
offset += 1
|
|
1770
|
+
|
|
1771
|
+
// TTL
|
|
1772
|
+
let ttl = data[offset]
|
|
1773
|
+
offset += 1
|
|
1774
|
+
|
|
1775
|
+
// Sender ID (8 bytes)
|
|
1776
|
+
let senderID = data.subdata(in: offset..<(offset + 8))
|
|
1777
|
+
offset += 8
|
|
1778
|
+
|
|
1779
|
+
// Recipient ID (8 bytes)
|
|
1780
|
+
let recipientIDData = data.subdata(in: offset..<(offset + 8))
|
|
1781
|
+
let recipientID = recipientIDData.allSatisfy({ $0 == 0 }) ? nil : recipientIDData
|
|
1782
|
+
offset += 8
|
|
1783
|
+
|
|
1784
|
+
// Timestamp (8 bytes)
|
|
1785
|
+
let timestampData = data.subdata(in: offset..<(offset + 8))
|
|
1786
|
+
let timestamp = timestampData.withUnsafeBytes { $0.load(as: UInt64.self).bigEndian }
|
|
1787
|
+
offset += 8
|
|
1788
|
+
|
|
1789
|
+
// Payload length (2 bytes)
|
|
1790
|
+
let lengthData = data.subdata(in: offset..<(offset + 2))
|
|
1791
|
+
let payloadLength = Int(lengthData.withUnsafeBytes { $0.load(as: UInt16.self).bigEndian })
|
|
1792
|
+
offset += 2
|
|
1793
|
+
|
|
1794
|
+
guard offset + payloadLength <= data.count else { return nil }
|
|
1795
|
+
|
|
1796
|
+
// Payload
|
|
1797
|
+
let payload = data.subdata(in: offset..<(offset + payloadLength))
|
|
1798
|
+
offset += payloadLength
|
|
1799
|
+
|
|
1800
|
+
// Signature (64 bytes if present)
|
|
1801
|
+
var signature: Data?
|
|
1802
|
+
if offset + 64 <= data.count {
|
|
1803
|
+
signature = data.subdata(in: offset..<(offset + 64))
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
return BitchatPacket(
|
|
1807
|
+
version: version,
|
|
1808
|
+
type: type,
|
|
1809
|
+
senderID: senderID,
|
|
1810
|
+
recipientID: recipientID,
|
|
1811
|
+
timestamp: timestamp,
|
|
1812
|
+
payload: payload,
|
|
1813
|
+
signature: signature,
|
|
1814
|
+
ttl: ttl
|
|
1815
|
+
)
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// MARK: - Data Extension
|
|
1820
|
+
|
|
1821
|
+
extension Data {
|
|
1822
|
+
init?(hexString: String?) {
|
|
1823
|
+
guard let hexString = hexString else { return nil }
|
|
1824
|
+
|
|
1825
|
+
let len = hexString.count / 2
|
|
1826
|
+
var data = Data(capacity: len)
|
|
1827
|
+
var index = hexString.startIndex
|
|
1828
|
+
|
|
1829
|
+
for _ in 0..<len {
|
|
1830
|
+
let nextIndex = hexString.index(index, offsetBy: 2)
|
|
1831
|
+
guard let byte = UInt8(hexString[index..<nextIndex], radix: 16) else { return nil }
|
|
1832
|
+
data.append(byte)
|
|
1833
|
+
index = nextIndex
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
self = data
|
|
1837
|
+
}
|
|
1838
|
+
}
|