kard-network-ble-mesh 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -0
- package/android/build.gradle +94 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +18 -0
- package/android/src/main/AndroidManifestNew.xml +17 -0
- package/android/src/main/java/com/blemesh/BleMeshModule.kt +1143 -0
- package/android/src/main/java/com/blemesh/BleMeshPackage.kt +16 -0
- package/ios/BleMesh.m +45 -0
- package/ios/BleMesh.swift +1075 -0
- package/kard-network-ble-mesh.podspec +27 -0
- package/lib/commonjs/index.js +241 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/module/index.js +236 -0
- package/lib/module/index.js.map +1 -0
- package/lib/typescript/index.d.ts +103 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/package.json +90 -0
- package/src/index.ts +334 -0
|
@@ -0,0 +1,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
|
+
}
|