kard-network-ble-mesh 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1075 @@
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
+ // Queues
56
+ private let bleQueue = DispatchQueue(label: "mesh.bluetooth", qos: .userInitiated)
57
+ private let messageQueue = DispatchQueue(label: "mesh.message", attributes: .concurrent)
58
+
59
+ // MARK: - RCTEventEmitter
60
+
61
+ override init() {
62
+ super.init()
63
+ generateIdentity()
64
+ }
65
+
66
+ override static func moduleName() -> String! {
67
+ return "BleMesh"
68
+ }
69
+
70
+ override func supportedEvents() -> [String]! {
71
+ return [
72
+ "onPeerListUpdated",
73
+ "onMessageReceived",
74
+ "onFileReceived",
75
+ "onConnectionStateChanged",
76
+ "onReadReceipt",
77
+ "onDeliveryAck",
78
+ "onError"
79
+ ]
80
+ }
81
+
82
+ override static func requiresMainQueueSetup() -> Bool {
83
+ return false
84
+ }
85
+
86
+ // MARK: - Identity
87
+
88
+ private func generateIdentity() {
89
+ // Generate or load keys
90
+ if let savedPrivateKey = loadKey(forKey: "mesh.privateKey") {
91
+ privateKey = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: savedPrivateKey)
92
+ }
93
+ if privateKey == nil {
94
+ privateKey = Curve25519.KeyAgreement.PrivateKey()
95
+ if let pk = privateKey {
96
+ saveKey(pk.rawRepresentation, forKey: "mesh.privateKey")
97
+ }
98
+ }
99
+
100
+ if let savedSigningKey = loadKey(forKey: "mesh.signingKey") {
101
+ signingKey = try? Curve25519.Signing.PrivateKey(rawRepresentation: savedSigningKey)
102
+ }
103
+ if signingKey == nil {
104
+ signingKey = Curve25519.Signing.PrivateKey()
105
+ if let sk = signingKey {
106
+ saveKey(sk.rawRepresentation, forKey: "mesh.signingKey")
107
+ }
108
+ }
109
+
110
+ // Generate peer ID from public key fingerprint (first 16 hex chars of SHA256)
111
+ if let pk = privateKey {
112
+ let fingerprint = SHA256.hash(data: pk.publicKey.rawRepresentation)
113
+ myPeerID = fingerprint.prefix(8).map { String(format: "%02x", $0) }.joined()
114
+ myPeerIDData = Data(fingerprint.prefix(8))
115
+ }
116
+ }
117
+
118
+ private func loadKey(forKey key: String) -> Data? {
119
+ let query: [String: Any] = [
120
+ kSecClass as String: kSecClassGenericPassword,
121
+ kSecAttrService as String: "com.blemesh",
122
+ kSecAttrAccount as String: key,
123
+ kSecReturnData as String: true
124
+ ]
125
+
126
+ var result: AnyObject?
127
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
128
+
129
+ if status == errSecSuccess {
130
+ return result as? Data
131
+ }
132
+ return nil
133
+ }
134
+
135
+ private func saveKey(_ data: Data, forKey key: String) {
136
+ let query: [String: Any] = [
137
+ kSecClass as String: kSecClassGenericPassword,
138
+ kSecAttrService as String: "com.blemesh",
139
+ kSecAttrAccount as String: key,
140
+ kSecValueData as String: data
141
+ ]
142
+
143
+ SecItemDelete(query as CFDictionary)
144
+ SecItemAdd(query as CFDictionary, nil)
145
+ }
146
+
147
+ // MARK: - React Native API
148
+
149
+ @objc(requestPermissions:rejecter:)
150
+ func requestPermissions(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
151
+ // On iOS, BLE permissions are requested when CBCentralManager is initialized
152
+ // We need to check the current state
153
+ let tempManager = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: false])
154
+
155
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
156
+ let bluetoothGranted = tempManager.state != .unauthorized
157
+ resolve([
158
+ "bluetooth": bluetoothGranted,
159
+ "location": true // iOS doesn't require location for BLE
160
+ ])
161
+ }
162
+ }
163
+
164
+ @objc(checkPermissions:rejecter:)
165
+ func checkPermissions(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
166
+ let state = centralManager?.state ?? .unknown
167
+ let bluetoothGranted = state != .unauthorized
168
+
169
+ resolve([
170
+ "bluetooth": bluetoothGranted,
171
+ "location": true
172
+ ])
173
+ }
174
+
175
+ @objc(start:resolver:rejecter:)
176
+ func start(_ nickname: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
177
+ myNickname = nickname
178
+
179
+ bleQueue.async { [weak self] in
180
+ guard let self = self else { return }
181
+
182
+ self.centralManager = CBCentralManager(delegate: self, queue: self.bleQueue)
183
+ self.peripheralManager = CBPeripheralManager(delegate: self, queue: self.bleQueue)
184
+
185
+ self.isRunning = true
186
+
187
+ DispatchQueue.main.async {
188
+ resolve(nil)
189
+ }
190
+ }
191
+ }
192
+
193
+ @objc(stop:rejecter:)
194
+ func stop(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
195
+ bleQueue.async { [weak self] in
196
+ guard let self = self else { return }
197
+
198
+ // Send leave announcement
199
+ self.sendLeaveAnnouncement()
200
+
201
+ // Stop scanning and advertising
202
+ self.centralManager?.stopScan()
203
+ self.peripheralManager?.stopAdvertising()
204
+
205
+ // Disconnect all peripherals
206
+ for (_, peripheral) in self.peripherals {
207
+ self.centralManager?.cancelPeripheralConnection(peripheral)
208
+ }
209
+
210
+ self.isRunning = false
211
+ self.peers.removeAll()
212
+ self.peripherals.removeAll()
213
+ self.peripheralToPeer.removeAll()
214
+ self.subscribedCentrals.removeAll()
215
+
216
+ DispatchQueue.main.async {
217
+ resolve(nil)
218
+ }
219
+ }
220
+ }
221
+
222
+ @objc(setNickname:resolver:rejecter:)
223
+ func setNickname(_ nickname: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
224
+ myNickname = nickname
225
+ sendAnnounce()
226
+ resolve(nil)
227
+ }
228
+
229
+ @objc(getMyPeerId:rejecter:)
230
+ func getMyPeerId(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
231
+ resolve(myPeerID)
232
+ }
233
+
234
+ @objc(getMyNickname:rejecter:)
235
+ func getMyNickname(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
236
+ resolve(myNickname)
237
+ }
238
+
239
+ @objc(getPeers:rejecter:)
240
+ func getPeers(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
241
+ let peerList = peers.values.map { peer in
242
+ return [
243
+ "peerId": peer.peerId,
244
+ "nickname": peer.nickname,
245
+ "isConnected": peer.isConnected,
246
+ "rssi": peer.rssi ?? NSNull(),
247
+ "lastSeen": Int(peer.lastSeen.timeIntervalSince1970 * 1000),
248
+ "isVerified": peer.isVerified
249
+ ] as [String: Any]
250
+ }
251
+ resolve(peerList)
252
+ }
253
+
254
+ @objc(sendMessage:channel:resolver:rejecter:)
255
+ func sendMessage(_ content: String, channel: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
256
+ let messageId = UUID().uuidString
257
+
258
+ messageQueue.async { [weak self] in
259
+ guard let self = self else { return }
260
+
261
+ let packet = self.createPacket(
262
+ type: MessageType.message.rawValue,
263
+ payload: Data(content.utf8),
264
+ recipientID: nil
265
+ )
266
+
267
+ self.broadcastPacket(packet)
268
+
269
+ DispatchQueue.main.async {
270
+ resolve(messageId)
271
+ }
272
+ }
273
+ }
274
+
275
+ @objc(sendPrivateMessage:recipientPeerId:resolver:rejecter:)
276
+ func sendPrivateMessage(_ content: String, recipientPeerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
277
+ let messageId = UUID().uuidString
278
+
279
+ messageQueue.async { [weak self] in
280
+ guard let self = self else { return }
281
+
282
+ // Check if we have an encryption session
283
+ if self.sessions[recipientPeerId] != nil {
284
+ // Encrypt the message
285
+ if let encrypted = self.encryptMessage(content, for: recipientPeerId) {
286
+ let packet = self.createPacket(
287
+ type: MessageType.noiseEncrypted.rawValue,
288
+ payload: encrypted,
289
+ recipientID: Data(hexString: recipientPeerId)
290
+ )
291
+ self.broadcastPacket(packet)
292
+ }
293
+ } else {
294
+ // Initiate handshake first
295
+ self.initiateHandshakeInternal(with: recipientPeerId)
296
+ }
297
+
298
+ DispatchQueue.main.async {
299
+ resolve(messageId)
300
+ }
301
+ }
302
+ }
303
+
304
+ @objc(sendFile:recipientPeerId:channel:resolver:rejecter:)
305
+ func sendFile(_ filePath: String, recipientPeerId: String?, channel: String?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
306
+ let transferId = UUID().uuidString
307
+
308
+ guard let fileData = FileManager.default.contents(atPath: filePath) else {
309
+ reject("FILE_ERROR", "Could not read file at path: \(filePath)", nil)
310
+ return
311
+ }
312
+
313
+ let fileName = (filePath as NSString).lastPathComponent
314
+ let mimeType = getMimeType(for: filePath)
315
+
316
+ // TODO: Implement file transfer
317
+ resolve(transferId)
318
+ }
319
+
320
+ @objc(sendReadReceipt:recipientPeerId:resolver:rejecter:)
321
+ func sendReadReceipt(_ messageId: String, recipientPeerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
322
+ messageQueue.async { [weak self] in
323
+ guard let self = self else { return }
324
+
325
+ // Create read receipt payload
326
+ var payload = Data([NoisePayloadType.readReceipt.rawValue])
327
+ payload.append(contentsOf: messageId.utf8)
328
+
329
+ if let encrypted = self.encryptPayload(payload, for: recipientPeerId) {
330
+ let packet = self.createPacket(
331
+ type: MessageType.noiseEncrypted.rawValue,
332
+ payload: encrypted,
333
+ recipientID: Data(hexString: recipientPeerId)
334
+ )
335
+ self.broadcastPacket(packet)
336
+ }
337
+
338
+ DispatchQueue.main.async {
339
+ resolve(nil)
340
+ }
341
+ }
342
+ }
343
+
344
+ @objc(hasEncryptedSession:resolver:rejecter:)
345
+ func hasEncryptedSession(_ peerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
346
+ resolve(sessions[peerId] != nil)
347
+ }
348
+
349
+ @objc(initiateHandshake:resolver:rejecter:)
350
+ func initiateHandshake(_ peerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
351
+ initiateHandshakeInternal(with: peerId)
352
+ resolve(nil)
353
+ }
354
+
355
+ @objc(getIdentityFingerprint:rejecter:)
356
+ func getIdentityFingerprint(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
357
+ if let pk = privateKey {
358
+ let fingerprint = SHA256.hash(data: pk.publicKey.rawRepresentation)
359
+ let fingerprintHex = fingerprint.map { String(format: "%02x", $0) }.joined()
360
+ resolve(fingerprintHex)
361
+ } else {
362
+ resolve(nil)
363
+ }
364
+ }
365
+
366
+ @objc(getPeerFingerprint:resolver:rejecter:)
367
+ func getPeerFingerprint(_ peerId: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
368
+ if let peer = peers[peerId], let publicKey = peer.noisePublicKey {
369
+ let fingerprint = SHA256.hash(data: publicKey)
370
+ let fingerprintHex = fingerprint.map { String(format: "%02x", $0) }.joined()
371
+ resolve(fingerprintHex)
372
+ } else {
373
+ resolve(nil)
374
+ }
375
+ }
376
+
377
+ @objc(broadcastAnnounce:rejecter:)
378
+ func broadcastAnnounce(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
379
+ sendAnnounce()
380
+ resolve(nil)
381
+ }
382
+
383
+ // MARK: - Private Methods
384
+
385
+ private func sendAnnounce() {
386
+ guard let publicKey = privateKey?.publicKey.rawRepresentation,
387
+ let signingPublicKey = signingKey?.publicKey.rawRepresentation else { return }
388
+
389
+ // TLV-encoded announcement
390
+ var payload = Data()
391
+
392
+ // Nickname TLV (tag 0x01)
393
+ let nicknameData = Data(myNickname.utf8)
394
+ payload.append(0x01)
395
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(nicknameData.count).bigEndian) { Array($0) })
396
+ payload.append(nicknameData)
397
+
398
+ // Noise public key TLV (tag 0x02)
399
+ payload.append(0x02)
400
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(publicKey.count).bigEndian) { Array($0) })
401
+ payload.append(publicKey)
402
+
403
+ // Signing public key TLV (tag 0x03)
404
+ payload.append(0x03)
405
+ payload.append(contentsOf: withUnsafeBytes(of: UInt16(signingPublicKey.count).bigEndian) { Array($0) })
406
+ payload.append(signingPublicKey)
407
+
408
+ let packet = createPacket(type: MessageType.announce.rawValue, payload: payload, recipientID: nil)
409
+ broadcastPacket(packet)
410
+ }
411
+
412
+ private func sendLeaveAnnouncement() {
413
+ let packet = createPacket(type: MessageType.leave.rawValue, payload: Data(), recipientID: nil)
414
+ broadcastPacket(packet)
415
+ }
416
+
417
+ private func initiateHandshakeInternal(with peerId: String) {
418
+ guard let publicKey = privateKey?.publicKey.rawRepresentation else { return }
419
+
420
+ let packet = createPacket(
421
+ type: MessageType.noiseHandshake.rawValue,
422
+ payload: publicKey,
423
+ recipientID: Data(hexString: peerId)
424
+ )
425
+ broadcastPacket(packet)
426
+ }
427
+
428
+ private func createPacket(type: UInt8, payload: Data, recipientID: Data?) -> BitchatPacket {
429
+ var packet = BitchatPacket(
430
+ version: 1,
431
+ type: type,
432
+ senderID: myPeerIDData,
433
+ recipientID: recipientID,
434
+ timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
435
+ payload: payload,
436
+ signature: nil,
437
+ ttl: messageTTL
438
+ )
439
+
440
+ // Sign the packet
441
+ if let sk = signingKey {
442
+ let dataToSign = packet.dataForSigning()
443
+ if let signature = try? sk.signature(for: dataToSign) {
444
+ packet.signature = signature
445
+ }
446
+ }
447
+
448
+ return packet
449
+ }
450
+
451
+ private func broadcastPacket(_ packet: BitchatPacket) {
452
+ guard let data = packet.encode() else { return }
453
+
454
+ // Send to all connected peripherals
455
+ for (_, peripheral) in peripherals {
456
+ if let services = peripheral.services,
457
+ let service = services.first(where: { $0.uuid == BleMesh.serviceUUID }),
458
+ let char = service.characteristics?.first(where: { $0.uuid == BleMesh.characteristicUUID }) {
459
+ peripheral.writeValue(data, for: char, type: .withoutResponse)
460
+ }
461
+ }
462
+
463
+ // Notify all subscribed centrals
464
+ if let char = characteristic {
465
+ peripheralManager?.updateValue(data, for: char, onSubscribedCentrals: nil)
466
+ }
467
+ }
468
+
469
+ private func handleReceivedPacket(_ data: Data, from peripheralUUID: String?) {
470
+ guard let packet = BitchatPacket.decode(from: data) else { return }
471
+
472
+ let senderID = packet.senderID.map { String(format: "%02x", $0) }.joined()
473
+
474
+ // Skip our own packets
475
+ if senderID == myPeerID { return }
476
+
477
+ // Deduplication
478
+ let messageID = "\(senderID)-\(packet.timestamp)-\(packet.type)"
479
+ if processedMessages.contains(messageID) { return }
480
+ processedMessages.insert(messageID)
481
+
482
+ // Handle by type
483
+ switch MessageType(rawValue: packet.type) {
484
+ case .announce:
485
+ handleAnnounce(packet, from: senderID)
486
+ case .message:
487
+ handleMessage(packet, from: senderID)
488
+ case .noiseHandshake:
489
+ handleNoiseHandshake(packet, from: senderID)
490
+ case .noiseEncrypted:
491
+ handleNoiseEncrypted(packet, from: senderID)
492
+ case .leave:
493
+ handleLeave(from: senderID)
494
+ default:
495
+ break
496
+ }
497
+
498
+ // Relay if TTL > 0
499
+ if packet.ttl > 0 {
500
+ var relayPacket = packet
501
+ relayPacket.ttl -= 1
502
+ if let data = relayPacket.encode() {
503
+ bleQueue.asyncAfter(deadline: .now() + Double.random(in: 0.01...0.1)) { [weak self] in
504
+ // Relay to all except source
505
+ self?.relayPacket(data, excluding: peripheralUUID)
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ private func relayPacket(_ data: Data, excluding peripheralUUID: String?) {
512
+ for (uuid, peripheral) in peripherals where uuid != peripheralUUID {
513
+ if let services = peripheral.services,
514
+ let service = services.first(where: { $0.uuid == BleMesh.serviceUUID }),
515
+ let char = service.characteristics?.first(where: { $0.uuid == BleMesh.characteristicUUID }) {
516
+ peripheral.writeValue(data, for: char, type: .withoutResponse)
517
+ }
518
+ }
519
+
520
+ if let char = characteristic {
521
+ peripheralManager?.updateValue(data, for: char, onSubscribedCentrals: nil)
522
+ }
523
+ }
524
+
525
+ private func handleAnnounce(_ packet: BitchatPacket, from senderID: String) {
526
+ // Parse TLV payload
527
+ var nickname = senderID
528
+ var noisePublicKey: Data?
529
+ var signingPublicKey: Data?
530
+
531
+ var offset = 0
532
+ while offset < packet.payload.count {
533
+ guard offset + 3 <= packet.payload.count else { break }
534
+
535
+ let tag = packet.payload[offset]
536
+ let length = Int(packet.payload[offset + 1]) << 8 | Int(packet.payload[offset + 2])
537
+ offset += 3
538
+
539
+ guard offset + length <= packet.payload.count else { break }
540
+ let value = packet.payload.subdata(in: offset..<(offset + length))
541
+ offset += length
542
+
543
+ switch tag {
544
+ case 0x01:
545
+ nickname = String(data: value, encoding: .utf8) ?? senderID
546
+ case 0x02:
547
+ noisePublicKey = value
548
+ case 0x03:
549
+ signingPublicKey = value
550
+ default:
551
+ break
552
+ }
553
+ }
554
+
555
+ // Update peer info
556
+ peers[senderID] = PeerInfo(
557
+ peerId: senderID,
558
+ nickname: nickname,
559
+ isConnected: true,
560
+ rssi: nil,
561
+ lastSeen: Date(),
562
+ noisePublicKey: noisePublicKey,
563
+ isVerified: false
564
+ )
565
+
566
+ notifyPeerListUpdated()
567
+ }
568
+
569
+ private func handleMessage(_ packet: BitchatPacket, from senderID: String) {
570
+ guard let content = String(data: packet.payload, encoding: .utf8) else { return }
571
+
572
+ let nickname = peers[senderID]?.nickname ?? senderID
573
+
574
+ let message: [String: Any] = [
575
+ "id": UUID().uuidString,
576
+ "content": content,
577
+ "senderPeerId": senderID,
578
+ "senderNickname": nickname,
579
+ "timestamp": Int(packet.timestamp),
580
+ "isPrivate": false
581
+ ]
582
+
583
+ sendEvent(withName: "onMessageReceived", body: ["message": message])
584
+ }
585
+
586
+ private func handleNoiseHandshake(_ packet: BitchatPacket, from senderID: String) {
587
+ // Store peer's public key and derive shared secret
588
+ guard packet.payload.count == 32,
589
+ let peerPublicKey = try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: packet.payload),
590
+ let pk = privateKey else { return }
591
+
592
+ do {
593
+ let sharedSecret = try pk.sharedSecretFromKeyAgreement(with: peerPublicKey)
594
+ let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
595
+ using: SHA256.self,
596
+ salt: Data(),
597
+ sharedInfo: Data("mesh-encryption".utf8),
598
+ outputByteCount: 32
599
+ )
600
+ sessions[senderID] = symmetricKey.withUnsafeBytes { Data($0) }
601
+
602
+ // Update peer's noise public key
603
+ if var peer = peers[senderID] {
604
+ peer.noisePublicKey = packet.payload
605
+ peers[senderID] = peer
606
+ }
607
+
608
+ // Send response handshake if this is an incoming request
609
+ if packet.recipientID == nil || packet.recipientID == myPeerIDData {
610
+ initiateHandshakeInternal(with: senderID)
611
+ }
612
+ } catch {
613
+ sendEvent(withName: "onError", body: ["code": "HANDSHAKE_ERROR", "message": error.localizedDescription])
614
+ }
615
+ }
616
+
617
+ private func handleNoiseEncrypted(_ packet: BitchatPacket, from senderID: String) {
618
+ // Check if message is for us
619
+ if let recipientID = packet.recipientID,
620
+ recipientID != myPeerIDData {
621
+ return
622
+ }
623
+
624
+ guard let sessionKey = sessions[senderID] else { return }
625
+
626
+ // Decrypt
627
+ guard let decrypted = decryptPayload(packet.payload, with: sessionKey) else { return }
628
+
629
+ // Parse payload type
630
+ guard decrypted.count > 0 else { return }
631
+ let payloadType = decrypted[0]
632
+ let payloadData = decrypted.dropFirst()
633
+
634
+ switch NoisePayloadType(rawValue: payloadType) {
635
+ case .privateMessage:
636
+ handlePrivateMessage(Data(payloadData), from: senderID)
637
+ case .readReceipt:
638
+ if let messageId = String(data: payloadData, encoding: .utf8) {
639
+ sendEvent(withName: "onReadReceipt", body: ["messageId": messageId, "fromPeerId": senderID])
640
+ }
641
+ case .deliveryAck:
642
+ if let messageId = String(data: payloadData, encoding: .utf8) {
643
+ sendEvent(withName: "onDeliveryAck", body: ["messageId": messageId, "fromPeerId": senderID])
644
+ }
645
+ default:
646
+ break
647
+ }
648
+ }
649
+
650
+ private func handlePrivateMessage(_ data: Data, from senderID: String) {
651
+ // Parse TLV private message
652
+ var messageId: String?
653
+ var content: String?
654
+
655
+ var offset = 0
656
+ while offset < data.count {
657
+ guard offset + 3 <= data.count else { break }
658
+ let tag = data[offset]
659
+ let length = Int(data[offset + 1]) << 8 | Int(data[offset + 2])
660
+ offset += 3
661
+
662
+ guard offset + length <= data.count else { break }
663
+ let value = data.subdata(in: offset..<(offset + length))
664
+ offset += length
665
+
666
+ switch tag {
667
+ case 0x01:
668
+ messageId = String(data: value, encoding: .utf8)
669
+ case 0x02:
670
+ content = String(data: value, encoding: .utf8)
671
+ default:
672
+ break
673
+ }
674
+ }
675
+
676
+ guard let id = messageId, let text = content else { return }
677
+ let nickname = peers[senderID]?.nickname ?? senderID
678
+
679
+ let message: [String: Any] = [
680
+ "id": id,
681
+ "content": text,
682
+ "senderPeerId": senderID,
683
+ "senderNickname": nickname,
684
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000),
685
+ "isPrivate": true
686
+ ]
687
+
688
+ sendEvent(withName: "onMessageReceived", body: ["message": message])
689
+ }
690
+
691
+ private func handleLeave(from senderID: String) {
692
+ peers.removeValue(forKey: senderID)
693
+ sessions.removeValue(forKey: senderID)
694
+ notifyPeerListUpdated()
695
+ }
696
+
697
+ private func encryptMessage(_ content: String, for peerId: String) -> Data? {
698
+ // Create private message TLV
699
+ let messageId = UUID().uuidString
700
+ var tlvData = Data()
701
+
702
+ // Message ID TLV (tag 0x01)
703
+ let idData = Data(messageId.utf8)
704
+ tlvData.append(0x01)
705
+ tlvData.append(contentsOf: withUnsafeBytes(of: UInt16(idData.count).bigEndian) { Array($0) })
706
+ tlvData.append(idData)
707
+
708
+ // Content TLV (tag 0x02)
709
+ let contentData = Data(content.utf8)
710
+ tlvData.append(0x02)
711
+ tlvData.append(contentsOf: withUnsafeBytes(of: UInt16(contentData.count).bigEndian) { Array($0) })
712
+ tlvData.append(contentData)
713
+
714
+ // Add payload type prefix
715
+ var payload = Data([NoisePayloadType.privateMessage.rawValue])
716
+ payload.append(tlvData)
717
+
718
+ return encryptPayload(payload, for: peerId)
719
+ }
720
+
721
+ private func encryptPayload(_ payload: Data, for peerId: String) -> Data? {
722
+ guard let sessionKey = sessions[peerId] else { return nil }
723
+
724
+ do {
725
+ let key = SymmetricKey(data: sessionKey)
726
+ let nonce = AES.GCM.Nonce()
727
+ let sealedBox = try AES.GCM.seal(payload, using: key, nonce: nonce)
728
+ return sealedBox.combined
729
+ } catch {
730
+ return nil
731
+ }
732
+ }
733
+
734
+ private func decryptPayload(_ encrypted: Data, with sessionKey: Data) -> Data? {
735
+ do {
736
+ let key = SymmetricKey(data: sessionKey)
737
+ let sealedBox = try AES.GCM.SealedBox(combined: encrypted)
738
+ return try AES.GCM.open(sealedBox, using: key)
739
+ } catch {
740
+ return nil
741
+ }
742
+ }
743
+
744
+ private func notifyPeerListUpdated() {
745
+ let peerList = peers.values.map { peer in
746
+ return [
747
+ "peerId": peer.peerId,
748
+ "nickname": peer.nickname,
749
+ "isConnected": peer.isConnected,
750
+ "rssi": peer.rssi ?? NSNull(),
751
+ "lastSeen": Int(peer.lastSeen.timeIntervalSince1970 * 1000),
752
+ "isVerified": peer.isVerified
753
+ ] as [String: Any]
754
+ }
755
+
756
+ sendEvent(withName: "onPeerListUpdated", body: ["peers": peerList])
757
+ }
758
+
759
+ private func getMimeType(for path: String) -> String {
760
+ let ext = (path as NSString).pathExtension.lowercased()
761
+ switch ext {
762
+ case "jpg", "jpeg": return "image/jpeg"
763
+ case "png": return "image/png"
764
+ case "gif": return "image/gif"
765
+ case "pdf": return "application/pdf"
766
+ case "txt": return "text/plain"
767
+ default: return "application/octet-stream"
768
+ }
769
+ }
770
+ }
771
+
772
+ // MARK: - CBCentralManagerDelegate
773
+
774
+ extension BleMesh: CBCentralManagerDelegate {
775
+ func centralManagerDidUpdateState(_ central: CBCentralManager) {
776
+ if central.state == .poweredOn && isRunning {
777
+ central.scanForPeripherals(
778
+ withServices: [BleMesh.serviceUUID],
779
+ options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
780
+ )
781
+ }
782
+
783
+ let state: String
784
+ switch central.state {
785
+ case .poweredOn: state = "connected"
786
+ case .poweredOff, .unauthorized: state = "disconnected"
787
+ default: state = "connecting"
788
+ }
789
+
790
+ sendEvent(withName: "onConnectionStateChanged", body: [
791
+ "state": state,
792
+ "peerCount": peers.count
793
+ ])
794
+ }
795
+
796
+ func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
797
+ let uuid = peripheral.identifier.uuidString
798
+
799
+ if peripherals[uuid] == nil {
800
+ peripherals[uuid] = peripheral
801
+ peripheral.delegate = self
802
+ central.connect(peripheral, options: nil)
803
+ }
804
+ }
805
+
806
+ func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
807
+ peripheral.discoverServices([BleMesh.serviceUUID])
808
+ }
809
+
810
+ func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
811
+ let uuid = peripheral.identifier.uuidString
812
+
813
+ if let peerId = peripheralToPeer[uuid] {
814
+ if var peer = peers[peerId] {
815
+ peer.isConnected = false
816
+ peers[peerId] = peer
817
+ }
818
+ notifyPeerListUpdated()
819
+ }
820
+
821
+ // Reconnect
822
+ if isRunning {
823
+ central.connect(peripheral, options: nil)
824
+ }
825
+ }
826
+ }
827
+
828
+ // MARK: - CBPeripheralDelegate
829
+
830
+ extension BleMesh: CBPeripheralDelegate {
831
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
832
+ guard let services = peripheral.services else { return }
833
+
834
+ for service in services where service.uuid == BleMesh.serviceUUID {
835
+ peripheral.discoverCharacteristics([BleMesh.characteristicUUID], for: service)
836
+ }
837
+ }
838
+
839
+ func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
840
+ guard let characteristics = service.characteristics else { return }
841
+
842
+ for char in characteristics where char.uuid == BleMesh.characteristicUUID {
843
+ peripheral.setNotifyValue(true, for: char)
844
+
845
+ // Send announce to new peer
846
+ sendAnnounce()
847
+ }
848
+ }
849
+
850
+ func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
851
+ guard let data = characteristic.value else { return }
852
+ handleReceivedPacket(data, from: peripheral.identifier.uuidString)
853
+ }
854
+ }
855
+
856
+ // MARK: - CBPeripheralManagerDelegate
857
+
858
+ extension BleMesh: CBPeripheralManagerDelegate {
859
+ func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
860
+ if peripheral.state == .poweredOn && isRunning {
861
+ setupPeripheralService()
862
+ }
863
+ }
864
+
865
+ private func setupPeripheralService() {
866
+ let char = CBMutableCharacteristic(
867
+ type: BleMesh.characteristicUUID,
868
+ properties: [.read, .write, .writeWithoutResponse, .notify],
869
+ value: nil,
870
+ permissions: [.readable, .writeable]
871
+ )
872
+
873
+ let service = CBMutableService(type: BleMesh.serviceUUID, primary: true)
874
+ service.characteristics = [char]
875
+
876
+ peripheralManager?.add(service)
877
+ characteristic = char
878
+
879
+ peripheralManager?.startAdvertising([
880
+ CBAdvertisementDataServiceUUIDsKey: [BleMesh.serviceUUID]
881
+ ])
882
+ }
883
+
884
+ func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
885
+ subscribedCentrals.append(central)
886
+ sendAnnounce()
887
+ }
888
+
889
+ func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
890
+ subscribedCentrals.removeAll { $0.identifier == central.identifier }
891
+ }
892
+
893
+ func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
894
+ for request in requests {
895
+ if let data = request.value {
896
+ handleReceivedPacket(data, from: nil)
897
+ }
898
+ peripheral.respond(to: request, withResult: .success)
899
+ }
900
+ }
901
+ }
902
+
903
+ // MARK: - Supporting Types
904
+
905
+ enum MessageType: UInt8 {
906
+ case announce = 0x01
907
+ case message = 0x02
908
+ case leave = 0x03
909
+ case noiseHandshake = 0x04
910
+ case noiseEncrypted = 0x05
911
+ case fileTransfer = 0x06
912
+ case fragment = 0x07
913
+ case requestSync = 0x08
914
+ }
915
+
916
+ enum NoisePayloadType: UInt8 {
917
+ case privateMessage = 0x01
918
+ case readReceipt = 0x02
919
+ case deliveryAck = 0x03
920
+ case fileTransfer = 0x04
921
+ case verifyChallenge = 0x05
922
+ case verifyResponse = 0x06
923
+ }
924
+
925
+ struct BitchatPacket {
926
+ var version: UInt8 = 1
927
+ var type: UInt8
928
+ var senderID: Data
929
+ var recipientID: Data?
930
+ var timestamp: UInt64
931
+ var payload: Data
932
+ var signature: Data?
933
+ var ttl: UInt8
934
+
935
+ func dataForSigning() -> Data {
936
+ var data = Data()
937
+ data.append(version)
938
+ data.append(type)
939
+ data.append(senderID)
940
+ if let recipientID = recipientID {
941
+ data.append(recipientID)
942
+ }
943
+ data.append(contentsOf: withUnsafeBytes(of: timestamp.bigEndian) { Array($0) })
944
+ data.append(payload)
945
+ data.append(ttl)
946
+ return data
947
+ }
948
+
949
+ func encode() -> Data? {
950
+ var data = Data()
951
+
952
+ // Version
953
+ data.append(version)
954
+
955
+ // Type
956
+ data.append(type)
957
+
958
+ // TTL
959
+ data.append(ttl)
960
+
961
+ // Sender ID (8 bytes)
962
+ data.append(senderID.prefix(8))
963
+ if senderID.count < 8 {
964
+ data.append(Data(count: 8 - senderID.count))
965
+ }
966
+
967
+ // Recipient ID (8 bytes, or zeros for broadcast)
968
+ if let recipientID = recipientID {
969
+ data.append(recipientID.prefix(8))
970
+ if recipientID.count < 8 {
971
+ data.append(Data(count: 8 - recipientID.count))
972
+ }
973
+ } else {
974
+ data.append(Data(count: 8))
975
+ }
976
+
977
+ // Timestamp (8 bytes)
978
+ data.append(contentsOf: withUnsafeBytes(of: timestamp.bigEndian) { Array($0) })
979
+
980
+ // Payload length (2 bytes)
981
+ let payloadLength = UInt16(payload.count)
982
+ data.append(contentsOf: withUnsafeBytes(of: payloadLength.bigEndian) { Array($0) })
983
+
984
+ // Payload
985
+ data.append(payload)
986
+
987
+ // Signature (64 bytes if present)
988
+ if let signature = signature {
989
+ data.append(signature)
990
+ }
991
+
992
+ return data
993
+ }
994
+
995
+ static func decode(from data: Data) -> BitchatPacket? {
996
+ guard data.count >= 29 else { return nil } // Minimum packet size
997
+
998
+ var offset = 0
999
+
1000
+ // Version
1001
+ let version = data[offset]
1002
+ offset += 1
1003
+
1004
+ // Type
1005
+ let type = data[offset]
1006
+ offset += 1
1007
+
1008
+ // TTL
1009
+ let ttl = data[offset]
1010
+ offset += 1
1011
+
1012
+ // Sender ID (8 bytes)
1013
+ let senderID = data.subdata(in: offset..<(offset + 8))
1014
+ offset += 8
1015
+
1016
+ // Recipient ID (8 bytes)
1017
+ let recipientIDData = data.subdata(in: offset..<(offset + 8))
1018
+ let recipientID = recipientIDData.allSatisfy({ $0 == 0 }) ? nil : recipientIDData
1019
+ offset += 8
1020
+
1021
+ // Timestamp (8 bytes)
1022
+ let timestampData = data.subdata(in: offset..<(offset + 8))
1023
+ let timestamp = timestampData.withUnsafeBytes { $0.load(as: UInt64.self).bigEndian }
1024
+ offset += 8
1025
+
1026
+ // Payload length (2 bytes)
1027
+ let lengthData = data.subdata(in: offset..<(offset + 2))
1028
+ let payloadLength = Int(lengthData.withUnsafeBytes { $0.load(as: UInt16.self).bigEndian })
1029
+ offset += 2
1030
+
1031
+ guard offset + payloadLength <= data.count else { return nil }
1032
+
1033
+ // Payload
1034
+ let payload = data.subdata(in: offset..<(offset + payloadLength))
1035
+ offset += payloadLength
1036
+
1037
+ // Signature (64 bytes if present)
1038
+ var signature: Data?
1039
+ if offset + 64 <= data.count {
1040
+ signature = data.subdata(in: offset..<(offset + 64))
1041
+ }
1042
+
1043
+ return BitchatPacket(
1044
+ version: version,
1045
+ type: type,
1046
+ senderID: senderID,
1047
+ recipientID: recipientID,
1048
+ timestamp: timestamp,
1049
+ payload: payload,
1050
+ signature: signature,
1051
+ ttl: ttl
1052
+ )
1053
+ }
1054
+ }
1055
+
1056
+ // MARK: - Data Extension
1057
+
1058
+ extension Data {
1059
+ init?(hexString: String?) {
1060
+ guard let hexString = hexString else { return nil }
1061
+
1062
+ let len = hexString.count / 2
1063
+ var data = Data(capacity: len)
1064
+ var index = hexString.startIndex
1065
+
1066
+ for _ in 0..<len {
1067
+ let nextIndex = hexString.index(index, offsetBy: 2)
1068
+ guard let byte = UInt8(hexString[index..<nextIndex], radix: 16) else { return nil }
1069
+ data.append(byte)
1070
+ index = nextIndex
1071
+ }
1072
+
1073
+ self = data
1074
+ }
1075
+ }