mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 00:45:21 +00:00
- Fix NSInvalidArgumentException crash by adding thread-safe access to peerNicknames - Fix 'can only accept commands while connected' by checking peripheral state - Add safety checks for characteristic write properties - Clean up disconnected peripherals from tracking - Prevent type confusion in getAllConnectedPeerIDs These fixes address the crash when accessing peerNicknames dictionary which could be corrupted by concurrent access from multiple threads.
2115 lines
88 KiB
Swift
2115 lines
88 KiB
Swift
import Foundation
|
|
import CoreBluetooth
|
|
import Combine
|
|
import CryptoKit
|
|
#if os(macOS)
|
|
import AppKit
|
|
import IOKit.ps
|
|
#else
|
|
import UIKit
|
|
#endif
|
|
|
|
// Extension for hex encoding
|
|
extension Data {
|
|
func hexEncodedString() -> String {
|
|
if self.isEmpty {
|
|
return ""
|
|
}
|
|
return self.map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
}
|
|
|
|
class BluetoothMeshService: NSObject {
|
|
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C")
|
|
static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
|
|
|
|
private var centralManager: CBCentralManager!
|
|
private var peripheralManager: CBPeripheralManager!
|
|
private var discoveredPeripherals: [CBPeripheral] = []
|
|
private var connectedPeripherals: [String: CBPeripheral] = [:]
|
|
private var peripheralCharacteristics: [CBPeripheral: CBCharacteristic] = [:]
|
|
private var characteristic: CBMutableCharacteristic!
|
|
private var subscribedCentrals: [CBCentral] = []
|
|
private var _peerNicknames: [String: String] = [:]
|
|
private var peerNicknames: [String: String] {
|
|
get {
|
|
messageQueue.sync { _peerNicknames }
|
|
}
|
|
set {
|
|
messageQueue.async(flags: .barrier) { self._peerNicknames = newValue }
|
|
}
|
|
}
|
|
private var activePeers: Set<String> = [] // Track all active peers
|
|
private var peerRSSI: [String: NSNumber] = [:] // Track RSSI values for peers
|
|
private var peripheralRSSI: [String: NSNumber] = [:] // Track RSSI by peripheral ID during discovery
|
|
|
|
weak var delegate: BitchatDelegate?
|
|
private let encryptionService = EncryptionService()
|
|
private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent)
|
|
private var processedMessages = Set<String>()
|
|
private let maxTTL: UInt8 = 5 // Increased for better reach
|
|
private var announcedToPeers = Set<String>() // Track which peers we've announced to
|
|
private var announcedPeers = Set<String>() // Track peers who have already been announced
|
|
|
|
// Store-and-forward message cache
|
|
private struct StoredMessage {
|
|
let packet: BitchatPacket
|
|
let timestamp: Date
|
|
let messageID: String
|
|
let isForFavorite: Bool // Messages for favorites stored indefinitely
|
|
}
|
|
private var messageCache: [StoredMessage] = []
|
|
private let messageCacheTimeout: TimeInterval = 43200 // 12 hours for regular peers
|
|
private let maxCachedMessages = 100 // For regular peers
|
|
private let maxCachedMessagesForFavorites = 1000 // Much larger cache for favorites
|
|
private var favoriteMessageQueue: [String: [StoredMessage]] = [:] // Per-favorite message queues
|
|
|
|
// Battery and range optimizations
|
|
private var scanDutyCycleTimer: Timer?
|
|
private var isActivelyScanning = true
|
|
private var activeScanDuration: TimeInterval = 2.0 // Scan actively for 2 seconds - will be adjusted based on battery
|
|
private var scanPauseDuration: TimeInterval = 3.0 // Pause for 3 seconds - will be adjusted based on battery
|
|
private var lastRSSIUpdate: [String: Date] = [:] // Throttle RSSI updates
|
|
private var batteryMonitorTimer: Timer?
|
|
private var currentBatteryLevel: Float = 1.0 // Default to full battery
|
|
|
|
// Cover traffic for privacy
|
|
private var coverTrafficTimer: Timer?
|
|
private let coverTrafficPrefix = "☂DUMMY☂" // Prefix to identify dummy messages after decryption
|
|
private var lastCoverTrafficTime = Date()
|
|
|
|
// Timing randomization for privacy
|
|
private let minMessageDelay: TimeInterval = 0.05 // 50ms minimum
|
|
private let maxMessageDelay: TimeInterval = 0.5 // 500ms maximum
|
|
|
|
// Fragment handling
|
|
private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data]
|
|
private var fragmentMetadata: [String: (originalType: UInt8, totalFragments: Int, timestamp: Date)] = [:]
|
|
private let maxFragmentSize = 500 // Optimized for BLE 5.0 extended data length
|
|
|
|
let myPeerID: String
|
|
|
|
// ===== SCALING OPTIMIZATIONS =====
|
|
|
|
// Connection pooling
|
|
private var connectionPool: [String: CBPeripheral] = [:]
|
|
private var connectionAttempts: [String: Int] = [:]
|
|
private var connectionBackoff: [String: TimeInterval] = [:]
|
|
private let maxConnectionAttempts = 3
|
|
private let baseBackoffInterval: TimeInterval = 1.0
|
|
|
|
// Probabilistic flooding
|
|
private var relayProbability: Double = 1.0 // Start at 100%, decrease with peer count
|
|
private let minRelayProbability: Double = 0.3 // Minimum 30% relay chance
|
|
|
|
// Message aggregation
|
|
private var pendingMessages: [(message: BitchatPacket, destination: String?)] = []
|
|
private var aggregationTimer: Timer?
|
|
private let aggregationWindow: TimeInterval = 0.1 // 100ms window
|
|
private let maxAggregatedMessages = 5
|
|
|
|
// Bloom filter for efficient duplicate detection
|
|
private struct BloomFilter {
|
|
private var bitArray: [Bool]
|
|
private let size: Int = 4096 // 512 bytes
|
|
private let hashCount = 3
|
|
|
|
init() {
|
|
bitArray = Array(repeating: false, count: size)
|
|
}
|
|
|
|
mutating func insert(_ item: String) {
|
|
for i in 0..<hashCount {
|
|
let hash = item.hashValue &+ i.hashValue
|
|
let index = abs(hash) % size
|
|
bitArray[index] = true
|
|
}
|
|
}
|
|
|
|
func contains(_ item: String) -> Bool {
|
|
for i in 0..<hashCount {
|
|
let hash = item.hashValue &+ i.hashValue
|
|
let index = abs(hash) % size
|
|
if !bitArray[index] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
mutating func reset() {
|
|
bitArray = Array(repeating: false, count: size)
|
|
}
|
|
}
|
|
private var messageBloomFilter = BloomFilter()
|
|
private var bloomFilterResetTimer: Timer?
|
|
|
|
// Network size estimation
|
|
private var estimatedNetworkSize: Int {
|
|
return max(activePeers.count, connectedPeripherals.count)
|
|
}
|
|
|
|
// Adaptive parameters based on network size
|
|
private var adaptiveTTL: UInt8 {
|
|
// Reduce TTL for larger networks
|
|
let networkSize = estimatedNetworkSize
|
|
if networkSize <= 10 {
|
|
return 5
|
|
} else if networkSize <= 30 {
|
|
return 4
|
|
} else if networkSize <= 50 {
|
|
return 3
|
|
} else {
|
|
return 2
|
|
}
|
|
}
|
|
|
|
private var adaptiveRelayProbability: Double {
|
|
// Reduce relay probability as network grows
|
|
let networkSize = estimatedNetworkSize
|
|
if networkSize <= 5 {
|
|
return 1.0 // 100% for small networks
|
|
} else if networkSize <= 15 {
|
|
return 0.8 // 80%
|
|
} else if networkSize <= 30 {
|
|
return 0.6 // 60%
|
|
} else if networkSize <= 50 {
|
|
return 0.4 // 40%
|
|
} else {
|
|
return minRelayProbability // 30% minimum
|
|
}
|
|
}
|
|
|
|
// BLE advertisement for lightweight presence
|
|
private var advertisementData: [String: Any] = [:]
|
|
private var isAdvertising = false
|
|
|
|
// ===== MESSAGE AGGREGATION =====
|
|
|
|
private func startAggregationTimer() {
|
|
aggregationTimer?.invalidate()
|
|
aggregationTimer = Timer.scheduledTimer(withTimeInterval: aggregationWindow, repeats: false) { [weak self] _ in
|
|
self?.flushPendingMessages()
|
|
}
|
|
}
|
|
|
|
private func flushPendingMessages() {
|
|
guard !pendingMessages.isEmpty else { return }
|
|
|
|
messageQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
// Group messages by destination
|
|
var messagesByDestination: [String?: [BitchatPacket]] = [:]
|
|
|
|
for (message, destination) in self.pendingMessages {
|
|
if messagesByDestination[destination] == nil {
|
|
messagesByDestination[destination] = []
|
|
}
|
|
messagesByDestination[destination]?.append(message)
|
|
}
|
|
|
|
// Send aggregated messages
|
|
for (destination, messages) in messagesByDestination {
|
|
if messages.count == 1 {
|
|
// Single message, send normally
|
|
if destination == nil {
|
|
self.broadcastPacket(messages[0])
|
|
} else if let dest = destination,
|
|
let peripheral = self.connectedPeripherals[dest],
|
|
let characteristic = self.peripheralCharacteristics[peripheral] {
|
|
if let data = messages[0].toBinaryData() {
|
|
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
|
}
|
|
}
|
|
} else {
|
|
// Multiple messages - could aggregate into a single packet
|
|
// For now, send with minimal delay between them
|
|
for (index, message) in messages.enumerated() {
|
|
let delay = Double(index) * 0.02 // 20ms between messages
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
if destination == nil {
|
|
self?.broadcastPacket(message)
|
|
} else if let dest = destination,
|
|
let peripheral = self?.connectedPeripherals[dest],
|
|
let characteristic = self?.peripheralCharacteristics[peripheral] {
|
|
if let data = message.toBinaryData() {
|
|
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear pending messages
|
|
self.pendingMessages.removeAll()
|
|
}
|
|
}
|
|
|
|
// Helper method to get fingerprint from public key data
|
|
private func getPublicKeyFingerprint(_ publicKeyData: Data) -> String {
|
|
let fingerprint = SHA256.hash(data: publicKeyData)
|
|
.compactMap { String(format: "%02x", $0) }
|
|
.joined()
|
|
.prefix(16) // Use first 16 chars for brevity
|
|
.lowercased()
|
|
return String(fingerprint)
|
|
}
|
|
|
|
override init() {
|
|
// Generate ephemeral peer ID for each session to prevent tracking
|
|
// Use random bytes instead of UUID for better anonymity
|
|
var randomBytes = [UInt8](repeating: 0, count: 4)
|
|
_ = SecRandomCopyBytes(kSecRandomDefault, 4, &randomBytes)
|
|
self.myPeerID = randomBytes.map { String(format: "%02x", $0) }.joined()
|
|
|
|
super.init()
|
|
// Generated ephemeral peer ID
|
|
|
|
centralManager = CBCentralManager(delegate: self, queue: nil)
|
|
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
|
|
|
|
// Start bloom filter reset timer (reset every 5 minutes)
|
|
bloomFilterResetTimer = Timer.scheduledTimer(withTimeInterval: 300.0, repeats: true) { [weak self] _ in
|
|
self?.messageQueue.async(flags: .barrier) {
|
|
self?.messageBloomFilter.reset()
|
|
self?.processedMessages.removeAll()
|
|
}
|
|
}
|
|
|
|
// Register for app termination notifications
|
|
#if os(macOS)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(appWillTerminate),
|
|
name: NSApplication.willTerminateNotification,
|
|
object: nil
|
|
)
|
|
#else
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(appWillTerminate),
|
|
name: UIApplication.willTerminateNotification,
|
|
object: nil
|
|
)
|
|
#endif
|
|
}
|
|
|
|
deinit {
|
|
cleanup()
|
|
scanDutyCycleTimer?.invalidate()
|
|
batteryMonitorTimer?.invalidate()
|
|
coverTrafficTimer?.invalidate()
|
|
bloomFilterResetTimer?.invalidate()
|
|
aggregationTimer?.invalidate()
|
|
}
|
|
|
|
@objc private func appWillTerminate() {
|
|
cleanup()
|
|
}
|
|
|
|
private func cleanup() {
|
|
// Send leave announcement before disconnecting
|
|
sendLeaveAnnouncement()
|
|
|
|
// Give the leave message time to send
|
|
Thread.sleep(forTimeInterval: 0.2)
|
|
|
|
// First, disconnect all peripherals which will trigger disconnect delegates
|
|
for (_, peripheral) in connectedPeripherals {
|
|
centralManager.cancelPeripheralConnection(peripheral)
|
|
}
|
|
|
|
// Stop advertising
|
|
if peripheralManager.isAdvertising {
|
|
peripheralManager.stopAdvertising()
|
|
}
|
|
|
|
// Stop scanning
|
|
centralManager.stopScan()
|
|
|
|
// Remove all services - this will disconnect any connected centrals
|
|
if peripheralManager.state == .poweredOn {
|
|
peripheralManager.removeAllServices()
|
|
}
|
|
|
|
// Clear all tracking
|
|
connectedPeripherals.removeAll()
|
|
subscribedCentrals.removeAll()
|
|
activePeers.removeAll()
|
|
announcedPeers.removeAll()
|
|
|
|
// Clear announcement tracking
|
|
announcedToPeers.removeAll()
|
|
}
|
|
|
|
func startServices() {
|
|
// Start both central and peripheral services
|
|
if centralManager.state == .poweredOn {
|
|
startScanning()
|
|
}
|
|
if peripheralManager.state == .poweredOn {
|
|
setupPeripheral()
|
|
startAdvertising()
|
|
}
|
|
|
|
// Send initial announces after services are ready
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
|
self?.sendBroadcastAnnounce()
|
|
}
|
|
|
|
// Start battery monitoring
|
|
startBatteryMonitoring()
|
|
|
|
// Start cover traffic for privacy
|
|
startCoverTraffic()
|
|
}
|
|
|
|
func sendBroadcastAnnounce() {
|
|
guard let vm = delegate as? ChatViewModel else { return }
|
|
|
|
let announcePacket = BitchatPacket(
|
|
type: MessageType.announce.rawValue,
|
|
ttl: 1,
|
|
senderID: myPeerID,
|
|
payload: Data(vm.nickname.utf8)
|
|
)
|
|
|
|
// Sending proactive broadcast announce
|
|
|
|
// Initial send with random delay
|
|
let initialDelay = self.randomDelay()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) { [weak self] in
|
|
self?.broadcastPacket(announcePacket)
|
|
}
|
|
|
|
// Send multiple times for reliability with jittered delays
|
|
for baseDelay in [0.5, 1.0, 2.0] {
|
|
let jitteredDelay = baseDelay + self.randomDelay()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + jitteredDelay) { [weak self] in
|
|
guard let self = self else { return }
|
|
self.broadcastPacket(announcePacket)
|
|
// [ANNOUNCE] Re-sending broadcast announce with jitter
|
|
}
|
|
}
|
|
}
|
|
|
|
func startAdvertising() {
|
|
guard peripheralManager.state == .poweredOn else {
|
|
return
|
|
}
|
|
|
|
// Use generic advertising to avoid identification
|
|
// No identifying prefixes or app names for activist safety
|
|
|
|
// Only use allowed advertisement keys
|
|
advertisementData = [
|
|
CBAdvertisementDataServiceUUIDsKey: [BluetoothMeshService.serviceUUID],
|
|
// Use only peer ID without any identifying prefix
|
|
CBAdvertisementDataLocalNameKey: myPeerID
|
|
]
|
|
|
|
isAdvertising = true
|
|
peripheralManager.startAdvertising(advertisementData)
|
|
}
|
|
|
|
func startScanning() {
|
|
guard centralManager.state == .poweredOn else {
|
|
return
|
|
}
|
|
|
|
// [BLUETOOTH] Starting scan
|
|
// Enable duplicate detection for RSSI tracking
|
|
let scanOptions: [String: Any] = [
|
|
CBCentralManagerScanOptionAllowDuplicatesKey: true
|
|
]
|
|
|
|
centralManager.scanForPeripherals(
|
|
withServices: [BluetoothMeshService.serviceUUID],
|
|
options: scanOptions
|
|
)
|
|
|
|
// Update scan parameters based on battery before starting
|
|
updateScanParametersForBattery()
|
|
|
|
// Implement scan duty cycling for battery efficiency
|
|
scheduleScanDutyCycle()
|
|
}
|
|
|
|
private func scheduleScanDutyCycle() {
|
|
guard scanDutyCycleTimer == nil else { return }
|
|
|
|
// Start with active scanning
|
|
isActivelyScanning = true
|
|
|
|
scanDutyCycleTimer = Timer.scheduledTimer(withTimeInterval: activeScanDuration, repeats: true) { [weak self] _ in
|
|
guard let self = self else { return }
|
|
|
|
if self.isActivelyScanning {
|
|
// Pause scanning to save battery
|
|
self.centralManager.stopScan()
|
|
self.isActivelyScanning = false
|
|
// [BLUETOOTH] Pausing scan
|
|
|
|
// Schedule resume
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + self.scanPauseDuration) { [weak self] in
|
|
guard let self = self else { return }
|
|
if self.centralManager.state == .poweredOn {
|
|
self.centralManager.scanForPeripherals(
|
|
withServices: [BluetoothMeshService.serviceUUID],
|
|
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
|
)
|
|
self.isActivelyScanning = true
|
|
// [BLUETOOTH] Resuming scan
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setupPeripheral() {
|
|
let characteristic = CBMutableCharacteristic(
|
|
type: BluetoothMeshService.characteristicUUID,
|
|
properties: [.read, .write, .writeWithoutResponse, .notify],
|
|
value: nil,
|
|
permissions: [.readable, .writeable]
|
|
)
|
|
|
|
let service = CBMutableService(type: BluetoothMeshService.serviceUUID, primary: true)
|
|
service.characteristics = [characteristic]
|
|
|
|
peripheralManager.add(service)
|
|
self.characteristic = characteristic
|
|
}
|
|
|
|
func sendMessage(_ content: String, mentions: [String] = [], to recipientID: String? = nil) {
|
|
messageQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
let nickname = self.delegate as? ChatViewModel
|
|
let senderNick = nickname?.nickname ?? self.myPeerID
|
|
|
|
let message = BitchatMessage(
|
|
sender: senderNick,
|
|
content: content,
|
|
timestamp: Date(),
|
|
isRelay: false,
|
|
originalSender: nil,
|
|
mentions: mentions.isEmpty ? nil : mentions
|
|
)
|
|
|
|
if let messageData = message.toBinaryPayload() {
|
|
// Sign the message payload (no encryption for broadcasts)
|
|
let signature: Data?
|
|
do {
|
|
signature = try self.encryptionService.sign(messageData)
|
|
// Successfully signed broadcast message
|
|
} catch {
|
|
print("[CRYPTO] Failed to sign message: \(error)")
|
|
signature = nil
|
|
}
|
|
|
|
// Use unified message type with broadcast recipient
|
|
let packet = BitchatPacket(
|
|
type: MessageType.message.rawValue,
|
|
senderID: Data(self.myPeerID.utf8),
|
|
recipientID: SpecialRecipients.broadcast, // Special broadcast ID
|
|
timestamp: UInt64(Date().timeIntervalSince1970),
|
|
payload: messageData,
|
|
signature: signature,
|
|
ttl: self.adaptiveTTL
|
|
)
|
|
|
|
// Add random delay before initial send
|
|
let initialDelay = self.randomDelay()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) { [weak self] in
|
|
self?.broadcastPacket(packet)
|
|
// Sending message
|
|
}
|
|
|
|
// Retry with randomized delays for reliability
|
|
let baseDelays = [0.2, 0.5]
|
|
for baseDelay in baseDelays {
|
|
let jitteredDelay = baseDelay + self.randomDelay()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + jitteredDelay) { [weak self] in
|
|
self?.broadcastPacket(packet)
|
|
// Re-sending message with jitter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String) {
|
|
messageQueue.async { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
let nickname = self.delegate as? ChatViewModel
|
|
let senderNick = nickname?.nickname ?? self.myPeerID
|
|
|
|
let message = BitchatMessage(
|
|
sender: senderNick,
|
|
content: content,
|
|
timestamp: Date(),
|
|
isRelay: false,
|
|
originalSender: nil,
|
|
isPrivate: true,
|
|
recipientNickname: recipientNickname,
|
|
senderPeerID: self.myPeerID
|
|
)
|
|
|
|
if let messageData = message.toBinaryPayload() {
|
|
// Pad message to standard block size for privacy
|
|
let blockSize = MessagePadding.optimalBlockSize(for: messageData.count)
|
|
let paddedData = MessagePadding.pad(messageData, toSize: blockSize)
|
|
// Padded message for privacy
|
|
|
|
// Encrypt the padded message for the recipient
|
|
let encryptedPayload: Data
|
|
do {
|
|
encryptedPayload = try self.encryptionService.encrypt(paddedData, for: recipientPeerID)
|
|
// Successfully encrypted private message
|
|
} catch {
|
|
print("[CRYPTO] Failed to encrypt private message: \(error)")
|
|
// Don't send unencrypted private messages
|
|
return
|
|
}
|
|
|
|
// Sign the encrypted payload
|
|
let signature: Data?
|
|
do {
|
|
signature = try self.encryptionService.sign(encryptedPayload)
|
|
// Successfully signed private message
|
|
} catch {
|
|
print("[CRYPTO] Failed to sign private message: \(error)")
|
|
signature = nil
|
|
}
|
|
|
|
// Create packet with recipient ID for proper routing
|
|
let packet = BitchatPacket(
|
|
type: MessageType.message.rawValue,
|
|
senderID: Data(self.myPeerID.utf8),
|
|
recipientID: Data(recipientPeerID.utf8),
|
|
timestamp: UInt64(Date().timeIntervalSince1970),
|
|
payload: encryptedPayload,
|
|
signature: signature,
|
|
ttl: self.adaptiveTTL
|
|
)
|
|
|
|
// Sending encrypted private message
|
|
|
|
// Add random delay for timing obfuscation
|
|
let delay = self.randomDelay()
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
self?.broadcastPacket(packet)
|
|
// Private message sent with timing delay
|
|
}
|
|
|
|
// Don't call didReceiveMessage here - let the view model handle it directly
|
|
}
|
|
}
|
|
}
|
|
|
|
private func sendAnnouncementToPeer(_ peerID: String) {
|
|
guard let vm = delegate as? ChatViewModel else { return }
|
|
|
|
// Sending announce to peer
|
|
|
|
// Always send announce, don't check if already announced
|
|
// This ensures peers get our nickname even if they reconnect
|
|
|
|
let packet = BitchatPacket(
|
|
type: MessageType.announce.rawValue,
|
|
ttl: 1,
|
|
senderID: myPeerID,
|
|
payload: Data(vm.nickname.utf8)
|
|
)
|
|
|
|
if let data = packet.toBinaryData() {
|
|
// Broadcasting announce packet
|
|
// Try both broadcast and targeted send
|
|
broadcastPacket(packet)
|
|
|
|
// Also try targeted send if we have the peripheral
|
|
if let peripheral = connectedPeripherals[peerID],
|
|
let characteristic = peripheral.services?.first(where: { $0.uuid == BluetoothMeshService.serviceUUID })?.characteristics?.first(where: { $0.uuid == BluetoothMeshService.characteristicUUID }) {
|
|
// Also sending targeted announce
|
|
peripheral.writeValue(data, for: characteristic, type: .withResponse)
|
|
} else {
|
|
// No peripheral found for targeted send
|
|
}
|
|
} else {
|
|
// Failed to create binary data for announce packet
|
|
}
|
|
|
|
announcedToPeers.insert(peerID)
|
|
}
|
|
|
|
private func sendLeaveAnnouncement() {
|
|
guard let vm = delegate as? ChatViewModel else { return }
|
|
|
|
let packet = BitchatPacket(
|
|
type: MessageType.leave.rawValue,
|
|
ttl: 1, // Don't relay leave messages
|
|
senderID: myPeerID,
|
|
payload: Data(vm.nickname.utf8)
|
|
)
|
|
|
|
broadcastPacket(packet)
|
|
}
|
|
|
|
|
|
func getPeerNicknames() -> [String: String] {
|
|
return peerNicknames
|
|
}
|
|
|
|
func getPeerRSSI() -> [String: NSNumber] {
|
|
// Create a copy with default values for connected peers without RSSI
|
|
var rssiWithDefaults = peerRSSI
|
|
|
|
// For any active peer without RSSI, assume decent signal (-60)
|
|
// This handles centrals where we can't read RSSI
|
|
for peerID in activePeers {
|
|
if rssiWithDefaults[peerID] == nil {
|
|
rssiWithDefaults[peerID] = NSNumber(value: -60) // Good signal default
|
|
}
|
|
}
|
|
|
|
return rssiWithDefaults
|
|
}
|
|
|
|
// Emergency disconnect for panic situations
|
|
func emergencyDisconnectAll() {
|
|
// Stop advertising immediately
|
|
if peripheralManager.isAdvertising {
|
|
peripheralManager.stopAdvertising()
|
|
}
|
|
|
|
// Stop scanning
|
|
centralManager.stopScan()
|
|
scanDutyCycleTimer?.invalidate()
|
|
scanDutyCycleTimer = nil
|
|
|
|
// Disconnect all peripherals
|
|
for (_, peripheral) in connectedPeripherals {
|
|
centralManager.cancelPeripheralConnection(peripheral)
|
|
}
|
|
|
|
// Clear all peer data
|
|
connectedPeripherals.removeAll()
|
|
peripheralCharacteristics.removeAll()
|
|
discoveredPeripherals.removeAll()
|
|
subscribedCentrals.removeAll()
|
|
peerNicknames.removeAll()
|
|
activePeers.removeAll()
|
|
peerRSSI.removeAll()
|
|
peripheralRSSI.removeAll()
|
|
announcedToPeers.removeAll()
|
|
announcedPeers.removeAll()
|
|
processedMessages.removeAll()
|
|
incomingFragments.removeAll()
|
|
fragmentMetadata.removeAll()
|
|
|
|
// Clear persistent identity
|
|
encryptionService.clearPersistentIdentity()
|
|
|
|
print("[PANIC] Emergency disconnect completed")
|
|
}
|
|
|
|
private func getAllConnectedPeerIDs() -> [String] {
|
|
// Only return peers who have announced (have nicknames)
|
|
let announcedPeers = Set(activePeers.compactMap { peerID -> String? in
|
|
// Ensure peerID is valid and not nil
|
|
guard !peerID.isEmpty,
|
|
peerID != "unknown",
|
|
peerID != myPeerID,
|
|
peerID.count <= 8 else { // Filter out temp IDs
|
|
return nil
|
|
}
|
|
|
|
// Safely check if peer has a nickname (thread-safe)
|
|
let hasNickname = self._peerNicknames[peerID] != nil
|
|
if hasNickname {
|
|
return peerID
|
|
}
|
|
|
|
return nil
|
|
})
|
|
// Active peers: \(announcedPeers.count)
|
|
return Array(announcedPeers).sorted()
|
|
}
|
|
|
|
// MARK: - Store-and-Forward Methods
|
|
|
|
private func cacheMessage(_ packet: BitchatPacket, messageID: String) {
|
|
messageQueue.async(flags: .barrier) { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
// Don't cache certain message types
|
|
guard packet.type != MessageType.keyExchange.rawValue,
|
|
packet.type != MessageType.announce.rawValue,
|
|
packet.type != MessageType.leave.rawValue,
|
|
packet.type != MessageType.fragmentStart.rawValue,
|
|
packet.type != MessageType.fragmentContinue.rawValue,
|
|
packet.type != MessageType.fragmentEnd.rawValue else {
|
|
return
|
|
}
|
|
|
|
// Check if this is a private message for a favorite
|
|
var isForFavorite = false
|
|
if packet.type == MessageType.message.rawValue,
|
|
let recipientID = packet.recipientID,
|
|
let recipientPeerID = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) {
|
|
// Check if recipient is a favorite via their public key fingerprint
|
|
if let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientPeerID) {
|
|
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
|
isForFavorite = self.delegate?.isFavorite(fingerprint: fingerprint) ?? false
|
|
}
|
|
}
|
|
|
|
// Create stored message
|
|
let storedMessage = StoredMessage(
|
|
packet: packet,
|
|
timestamp: Date(),
|
|
messageID: messageID,
|
|
isForFavorite: isForFavorite
|
|
)
|
|
|
|
if isForFavorite {
|
|
// Store in favorite-specific queue
|
|
if let recipientID = packet.recipientID,
|
|
let recipientPeerID = String(data: recipientID.trimmingNullBytes(), encoding: .utf8) {
|
|
if self.favoriteMessageQueue[recipientPeerID] == nil {
|
|
self.favoriteMessageQueue[recipientPeerID] = []
|
|
}
|
|
self.favoriteMessageQueue[recipientPeerID]?.append(storedMessage)
|
|
|
|
// Limit favorite queue size
|
|
if let count = self.favoriteMessageQueue[recipientPeerID]?.count,
|
|
count > self.maxCachedMessagesForFavorites {
|
|
self.favoriteMessageQueue[recipientPeerID]?.removeFirst()
|
|
}
|
|
|
|
// Cached message for favorite
|
|
}
|
|
} else {
|
|
// Clean up old messages first (only for regular cache)
|
|
self.cleanupMessageCache()
|
|
|
|
// Add to regular cache
|
|
self.messageCache.append(storedMessage)
|
|
|
|
// Limit cache size
|
|
if self.messageCache.count > self.maxCachedMessages {
|
|
self.messageCache.removeFirst()
|
|
}
|
|
|
|
// Cached message
|
|
}
|
|
}
|
|
}
|
|
|
|
private func cleanupMessageCache() {
|
|
let cutoffTime = Date().addingTimeInterval(-messageCacheTimeout)
|
|
// Only remove non-favorite messages that are older than timeout
|
|
messageCache.removeAll { !$0.isForFavorite && $0.timestamp < cutoffTime }
|
|
}
|
|
|
|
private func sendCachedMessages(to peerID: String) {
|
|
messageQueue.async { [weak self] in
|
|
guard let self = self,
|
|
let peripheral = self.connectedPeripherals[peerID],
|
|
let characteristic = self.peripheralCharacteristics[peripheral] else {
|
|
return
|
|
}
|
|
|
|
// Clean up old messages first
|
|
self.cleanupMessageCache()
|
|
|
|
var messagesToSend: [StoredMessage] = []
|
|
|
|
// First, check if this peer has any favorite messages waiting
|
|
if let favoriteMessages = self.favoriteMessageQueue[peerID] {
|
|
messagesToSend.append(contentsOf: favoriteMessages)
|
|
// Clear the favorite queue after adding to send list
|
|
self.favoriteMessageQueue[peerID] = nil
|
|
// Found favorite messages
|
|
}
|
|
|
|
// Then add regular cached messages
|
|
messagesToSend.append(contentsOf: self.messageCache)
|
|
|
|
// Sending cached messages
|
|
|
|
// Send cached messages with slight delay between each
|
|
for (index, storedMessage) in messagesToSend.enumerated() {
|
|
let delay = Double(index) * 0.1 // 100ms between messages
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak peripheral] in
|
|
guard let peripheral = peripheral,
|
|
peripheral.state == .connected else { return }
|
|
|
|
// Create a new packet with fresh timestamp
|
|
let updatedPacket = BitchatPacket(
|
|
type: storedMessage.packet.type,
|
|
senderID: storedMessage.packet.senderID,
|
|
recipientID: storedMessage.packet.recipientID,
|
|
timestamp: UInt64(Date().timeIntervalSince1970),
|
|
payload: storedMessage.packet.payload,
|
|
signature: storedMessage.packet.signature,
|
|
ttl: storedMessage.packet.ttl
|
|
)
|
|
|
|
if let data = updatedPacket.toBinaryData(),
|
|
characteristic.properties.contains(.writeWithoutResponse) {
|
|
peripheral.writeValue(data, for: characteristic, type: .withoutResponse)
|
|
// Sent cached message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func estimateDistance(rssi: Int) -> Int {
|
|
// Rough distance estimation based on RSSI
|
|
// Using path loss formula: RSSI = TxPower - 10 * n * log10(distance)
|
|
// Assuming TxPower = -59 dBm at 1m, n = 2.0 (free space)
|
|
let txPower = -59.0
|
|
let pathLossExponent = 2.0
|
|
|
|
let ratio = (txPower - Double(rssi)) / (10.0 * pathLossExponent)
|
|
let distance = pow(10.0, ratio)
|
|
|
|
return Int(distance)
|
|
}
|
|
|
|
private func broadcastPacket(_ packet: BitchatPacket) {
|
|
guard let data = packet.toBinaryData() else {
|
|
print("[ERROR] Failed to convert packet to binary data")
|
|
return
|
|
}
|
|
|
|
// [BROADCAST] Type: \(packet.type), peripherals: \(connectedPeripherals.count), centrals: \(subscribedCentrals.count)
|
|
|
|
// Send to connected peripherals (as central)
|
|
var sentToPeripherals = 0
|
|
for (_, peripheral) in connectedPeripherals {
|
|
if let characteristic = peripheralCharacteristics[peripheral] {
|
|
// Check if peripheral is connected before writing
|
|
if peripheral.state == .connected {
|
|
// Use withoutResponse for faster transmission when possible
|
|
// Only use withResponse for critical messages or when MTU negotiation needed
|
|
let writeType: CBCharacteristicWriteType = data.count > 512 ? .withResponse : .withoutResponse
|
|
|
|
// Additional safety check for characteristic properties
|
|
if characteristic.properties.contains(.write) ||
|
|
characteristic.properties.contains(.writeWithoutResponse) {
|
|
peripheral.writeValue(data, for: characteristic, type: writeType)
|
|
sentToPeripherals += 1
|
|
}
|
|
} else {
|
|
// Peripheral not connected - remove from our tracking
|
|
if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key {
|
|
connectedPeripherals.removeValue(forKey: peerID)
|
|
peripheralCharacteristics.removeValue(forKey: peripheral)
|
|
}
|
|
}
|
|
} else {
|
|
// No characteristic for peripheral
|
|
}
|
|
}
|
|
// Sent to \(sentToPeripherals) peripherals
|
|
|
|
// Send to subscribed centrals (as peripheral)
|
|
if let char = characteristic, !subscribedCentrals.isEmpty {
|
|
// Send to all subscribed centrals
|
|
let success = peripheralManager.updateValue(data, for: char, onSubscribedCentrals: nil)
|
|
if success {
|
|
// Sent to centrals
|
|
} else {
|
|
// Failed to send to centrals - queue full
|
|
}
|
|
} else {
|
|
if characteristic == nil {
|
|
// No characteristic or centrals
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleReceivedPacket(_ packet: BitchatPacket, from peerID: String, peripheral: CBPeripheral? = nil) {
|
|
messageQueue.async(flags: .barrier) { [weak self] in
|
|
guard let self = self else { return }
|
|
guard packet.ttl > 0 else {
|
|
// Dropping packet with TTL 0
|
|
return
|
|
}
|
|
|
|
// Validate packet has payload
|
|
guard !packet.payload.isEmpty else {
|
|
// Dropping packet with empty payload
|
|
return
|
|
}
|
|
|
|
// Replay attack protection: Check timestamp is within reasonable window (5 minutes)
|
|
let currentTime = UInt64(Date().timeIntervalSince1970)
|
|
let timeDiff = abs(Int64(currentTime) - Int64(packet.timestamp))
|
|
if timeDiff > 300 { // 5 minutes
|
|
print("[SECURITY] Dropping packet with timestamp too far from current time: \(timeDiff) seconds")
|
|
return
|
|
}
|
|
|
|
// For fragments, include packet type in messageID to avoid dropping CONTINUE/END fragments
|
|
let messageID: String
|
|
if packet.type == MessageType.fragmentStart.rawValue ||
|
|
packet.type == MessageType.fragmentContinue.rawValue ||
|
|
packet.type == MessageType.fragmentEnd.rawValue {
|
|
// Include both type and payload hash for fragments to ensure uniqueness
|
|
messageID = "\(packet.timestamp)-\(String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "")-\(packet.type)-\(packet.payload.hashValue)"
|
|
} else {
|
|
messageID = "\(packet.timestamp)-\(String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "")"
|
|
}
|
|
|
|
// Use bloom filter for efficient duplicate detection
|
|
if messageBloomFilter.contains(messageID) {
|
|
// Also check exact set for accuracy (bloom filter can have false positives)
|
|
if processedMessages.contains(messageID) {
|
|
return
|
|
}
|
|
}
|
|
|
|
messageBloomFilter.insert(messageID)
|
|
processedMessages.insert(messageID)
|
|
|
|
// Reset bloom filter periodically to prevent saturation
|
|
if processedMessages.count > 1000 {
|
|
processedMessages.removeAll()
|
|
messageBloomFilter.reset()
|
|
}
|
|
|
|
// let _ = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "unknown"
|
|
|
|
// Received packet type: \(packet.type) from \(peerID)
|
|
|
|
// Note: We'll decode messages in the switch statement below, not here
|
|
|
|
switch MessageType(rawValue: packet.type) {
|
|
case .message:
|
|
// Unified message handler for both broadcast and private messages
|
|
guard let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) else {
|
|
return
|
|
}
|
|
|
|
// Ignore our own messages
|
|
if senderID == myPeerID {
|
|
return
|
|
}
|
|
|
|
// Check if this is a broadcast or private message
|
|
if let recipientID = packet.recipientID {
|
|
if recipientID == SpecialRecipients.broadcast {
|
|
// BROADCAST MESSAGE
|
|
// Received broadcast message
|
|
|
|
// Verify signature if present
|
|
if let signature = packet.signature {
|
|
do {
|
|
let isValid = try encryptionService.verify(signature, for: packet.payload, from: senderID)
|
|
if !isValid {
|
|
// Invalid signature, dropping message
|
|
return
|
|
}
|
|
} catch {
|
|
print("[CRYPTO] Failed to verify signature from \(senderID): \(error)")
|
|
}
|
|
}
|
|
|
|
// Parse broadcast message (not encrypted)
|
|
if let message = BitchatMessage.fromBinaryPayload(packet.payload) {
|
|
// Received broadcast message
|
|
|
|
// Store nickname mapping
|
|
peerNicknames[senderID] = message.sender
|
|
|
|
let messageWithPeerID = BitchatMessage(
|
|
sender: message.sender,
|
|
content: message.content,
|
|
timestamp: message.timestamp,
|
|
isRelay: message.isRelay,
|
|
originalSender: message.originalSender,
|
|
isPrivate: false,
|
|
recipientNickname: nil,
|
|
senderPeerID: senderID,
|
|
mentions: message.mentions
|
|
)
|
|
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didReceiveMessage(messageWithPeerID)
|
|
}
|
|
}
|
|
|
|
// Probabilistic relay based on network size
|
|
var relayPacket = packet
|
|
relayPacket.ttl -= 1
|
|
if relayPacket.ttl > 0 {
|
|
// Cache message for store-and-forward
|
|
self.cacheMessage(relayPacket, messageID: messageID)
|
|
|
|
// Probabilistic flooding: relay with probability based on network density
|
|
let relayProb = self.adaptiveRelayProbability
|
|
let shouldRelay = Double.random(in: 0...1) < relayProb
|
|
|
|
if shouldRelay {
|
|
// Add random delay to prevent collision storms
|
|
let delay = Double.random(in: minMessageDelay...maxMessageDelay)
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
self?.broadcastPacket(relayPacket)
|
|
}
|
|
}
|
|
}
|
|
|
|
} else if let recipientIDString = String(data: recipientID.trimmingNullBytes(), encoding: .utf8),
|
|
recipientIDString == myPeerID {
|
|
// PRIVATE MESSAGE FOR US
|
|
// Received private message
|
|
|
|
// Verify signature if present
|
|
if let signature = packet.signature {
|
|
do {
|
|
let isValid = try encryptionService.verify(signature, for: packet.payload, from: senderID)
|
|
if !isValid {
|
|
// Invalid signature on private message
|
|
return
|
|
}
|
|
} catch {
|
|
print("[CRYPTO] Failed to verify signature from \(senderID): \(error)")
|
|
}
|
|
}
|
|
|
|
// Decrypt the message
|
|
let decryptedPayload: Data
|
|
do {
|
|
let decryptedPadded = try encryptionService.decrypt(packet.payload, from: senderID)
|
|
// Successfully decrypted private message
|
|
|
|
// Remove padding
|
|
decryptedPayload = MessagePadding.unpad(decryptedPadded)
|
|
// Unpadded message
|
|
} catch {
|
|
print("[CRYPTO] Failed to decrypt private message from \(senderID): \(error)")
|
|
return
|
|
}
|
|
|
|
// Parse the decrypted message
|
|
if let message = BitchatMessage.fromBinaryPayload(decryptedPayload) {
|
|
// Check if this is a dummy message for cover traffic
|
|
if message.content.hasPrefix(self.coverTrafficPrefix) {
|
|
// Received and discarded cover traffic
|
|
return // Silently discard dummy messages
|
|
}
|
|
|
|
// Received private message
|
|
|
|
// Store nickname mapping if we don't have it
|
|
if peerNicknames[senderID] == nil {
|
|
peerNicknames[senderID] = message.sender
|
|
}
|
|
|
|
let messageWithPeerID = BitchatMessage(
|
|
sender: message.sender,
|
|
content: message.content,
|
|
timestamp: message.timestamp,
|
|
isRelay: message.isRelay,
|
|
originalSender: message.originalSender,
|
|
isPrivate: message.isPrivate,
|
|
recipientNickname: message.recipientNickname,
|
|
senderPeerID: senderID
|
|
)
|
|
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didReceiveMessage(messageWithPeerID)
|
|
}
|
|
}
|
|
|
|
} else if packet.ttl > 0 {
|
|
// RELAY PRIVATE MESSAGE (not for us)
|
|
// Relaying private message
|
|
var relayPacket = packet
|
|
relayPacket.ttl -= 1
|
|
|
|
// Check if this message is for an offline favorite and cache it
|
|
if let recipientIDString = String(data: recipientID.trimmingNullBytes(), encoding: .utf8),
|
|
let publicKeyData = self.encryptionService.getPeerIdentityKey(recipientIDString) {
|
|
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
|
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
|
// Caching message for offline favorite
|
|
self.cacheMessage(relayPacket, messageID: messageID)
|
|
}
|
|
}
|
|
|
|
// Probabilistic flooding for private message relay
|
|
let relayProb = self.adaptiveRelayProbability
|
|
let shouldRelay = Double.random(in: 0...1) < relayProb
|
|
|
|
if shouldRelay {
|
|
// Add random delay to prevent collision storms
|
|
let delay = Double.random(in: minMessageDelay...maxMessageDelay)
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
self?.broadcastPacket(relayPacket)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
case .keyExchange:
|
|
// Use senderID from packet for consistency
|
|
if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
|
|
if packet.payload.count > 0 {
|
|
let publicKeyData = packet.payload
|
|
do {
|
|
try encryptionService.addPeerPublicKey(senderID, publicKeyData: publicKeyData)
|
|
// Added public key
|
|
} catch {
|
|
print("[KEY_EXCHANGE] Failed to add public key for \(senderID): \(error)")
|
|
}
|
|
|
|
// Register identity key with view model for persistent favorites
|
|
if let viewModel = self.delegate as? ChatViewModel,
|
|
let identityKeyData = encryptionService.getPeerIdentityKey(senderID) {
|
|
viewModel.registerPeerPublicKey(peerID: senderID, publicKeyData: identityKeyData)
|
|
}
|
|
|
|
// If we have RSSI from discovery, apply it to this peer
|
|
if let peripheral = peripheral,
|
|
let tempRSSI = peripheralRSSI[peripheral.identifier.uuidString] {
|
|
peerRSSI[senderID] = tempRSSI
|
|
}
|
|
|
|
// Track this peer temporarily
|
|
if senderID != "unknown" && senderID != myPeerID {
|
|
// Check if we need to update peripheral mapping from the specific peripheral that sent this
|
|
if let peripheral = peripheral {
|
|
// Find if this peripheral is currently mapped with a temp ID
|
|
if let tempID = self.connectedPeripherals.first(where: { $0.value == peripheral })?.key,
|
|
tempID.count > 8 { // It's a temp ID
|
|
// Remove temp mapping and add real peer ID mapping
|
|
self.connectedPeripherals.removeValue(forKey: tempID)
|
|
self.connectedPeripherals[senderID] = peripheral
|
|
// Updated peripheral mapping
|
|
|
|
// Transfer RSSI from temp ID to peer ID
|
|
if let rssi = self.peripheralRSSI[tempID] {
|
|
self.peerRSSI[senderID] = rssi
|
|
self.peripheralRSSI.removeValue(forKey: tempID)
|
|
// Transferred RSSI to peer
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add to active peers immediately on key exchange
|
|
activePeers.insert(senderID)
|
|
let connectedPeerIDs = self.getAllConnectedPeerIDs()
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didUpdatePeerList(connectedPeerIDs)
|
|
}
|
|
}
|
|
|
|
// Send announce with our nickname immediately
|
|
// Sending announcement to peer
|
|
self.sendAnnouncementToPeer(senderID)
|
|
|
|
// Check if this peer has cached messages (especially for favorites)
|
|
self.sendCachedMessages(to: senderID)
|
|
}
|
|
}
|
|
|
|
case .announce:
|
|
// Processing announce packet
|
|
if let nickname = String(data: packet.payload, encoding: .utf8),
|
|
let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
|
|
// Received announce from \(senderID): \(nickname)
|
|
|
|
// Ignore if it's from ourselves
|
|
if senderID == myPeerID {
|
|
return
|
|
}
|
|
|
|
// Check if we've already announced this peer
|
|
let isFirstAnnounce = !announcedPeers.contains(senderID)
|
|
|
|
// Store the nickname
|
|
peerNicknames[senderID] = nickname
|
|
// Stored nickname
|
|
// Updated nicknames
|
|
|
|
// Note: We can't update peripheral mapping here since we don't have
|
|
// access to which peripheral sent this announce. The mapping will be
|
|
// updated when we receive key exchange packets where we do have the peripheral.
|
|
|
|
// Add to active peers if not already there
|
|
if senderID != "unknown" {
|
|
if !activePeers.contains(senderID) {
|
|
activePeers.insert(senderID)
|
|
}
|
|
|
|
// Show join message only for first announce
|
|
if isFirstAnnounce {
|
|
announcedPeers.insert(senderID)
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didConnectToPeer(nickname)
|
|
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
|
|
// Check if this is a favorite peer and send notification
|
|
// Note: This might not work immediately if key exchange hasn't happened yet
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
// Check if this is a favorite using their public key fingerprint
|
|
if let publicKeyData = self.encryptionService.getPeerIdentityKey(senderID) {
|
|
let fingerprint = self.getPublicKeyFingerprint(publicKeyData)
|
|
if self.delegate?.isFavorite(fingerprint: fingerprint) ?? false {
|
|
NotificationService.shared.sendFavoriteOnlineNotification(nickname: nickname)
|
|
|
|
// Send any cached messages for this favorite
|
|
// Favorite peer came online
|
|
self.sendCachedMessages(to: senderID)
|
|
}
|
|
} else if let viewModel = self.delegate as? ChatViewModel,
|
|
viewModel.isFavorite(peerID: senderID) {
|
|
// Fallback for backwards compatibility
|
|
NotificationService.shared.sendFavoriteOnlineNotification(nickname: nickname)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Just update the peer list
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
}
|
|
}
|
|
} else {
|
|
}
|
|
} else {
|
|
// Failed to decode announce packet
|
|
}
|
|
|
|
case .leave:
|
|
// Processing leave packet
|
|
if let nickname = String(data: packet.payload, encoding: .utf8),
|
|
let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
|
|
|
|
// Peer is leaving
|
|
|
|
// Remove from active peers
|
|
activePeers.remove(senderID)
|
|
announcedPeers.remove(senderID)
|
|
|
|
// Show leave message
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didDisconnectFromPeer(nickname)
|
|
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
}
|
|
|
|
// Clean up peer data
|
|
peerNicknames.removeValue(forKey: senderID)
|
|
} else {
|
|
// Failed to parse leave packet
|
|
}
|
|
|
|
case .fragmentStart, .fragmentContinue, .fragmentEnd:
|
|
// let fragmentTypeStr = packet.type == MessageType.fragmentStart.rawValue ? "START" :
|
|
// (packet.type == MessageType.fragmentContinue.rawValue ? "CONTINUE" : "END")
|
|
// Handling fragment
|
|
|
|
// Validate fragment has minimum required size
|
|
if packet.payload.count < 13 {
|
|
// Fragment payload too small
|
|
return
|
|
}
|
|
|
|
handleFragment(packet, from: peerID)
|
|
|
|
// Relay fragments if TTL > 0
|
|
var relayPacket = packet
|
|
relayPacket.ttl -= 1
|
|
if relayPacket.ttl > 0 {
|
|
// Relaying fragment
|
|
self.broadcastPacket(relayPacket)
|
|
}
|
|
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
private func sendFragmentedPacket(_ packet: BitchatPacket) {
|
|
guard let fullData = packet.toBinaryData() else { return }
|
|
|
|
// Generate a fixed 8-byte fragment ID
|
|
var fragmentID = Data(count: 8)
|
|
fragmentID.withUnsafeMutableBytes { bytes in
|
|
arc4random_buf(bytes.baseAddress, 8)
|
|
}
|
|
|
|
let fragments = stride(from: 0, to: fullData.count, by: maxFragmentSize).map { offset in
|
|
fullData[offset..<min(offset + maxFragmentSize, fullData.count)]
|
|
}
|
|
|
|
// Splitting into fragments
|
|
// Fragment ID generated
|
|
// Original packet size
|
|
|
|
// Optimize fragment transmission for speed
|
|
// Use minimal delay for BLE 5.0 which supports better throughput
|
|
let delayBetweenFragments: TimeInterval = 0.02 // 20ms between fragments for faster transmission
|
|
|
|
for (index, fragmentData) in fragments.enumerated() {
|
|
var fragmentPayload = Data()
|
|
|
|
// Fragment header: fragmentID (8) + index (2) + total (2) + originalType (1) + data
|
|
fragmentPayload.append(fragmentID)
|
|
fragmentPayload.append(UInt8((index >> 8) & 0xFF))
|
|
fragmentPayload.append(UInt8(index & 0xFF))
|
|
fragmentPayload.append(UInt8((fragments.count >> 8) & 0xFF))
|
|
fragmentPayload.append(UInt8(fragments.count & 0xFF))
|
|
fragmentPayload.append(packet.type)
|
|
fragmentPayload.append(fragmentData)
|
|
|
|
let fragmentType: MessageType
|
|
if index == 0 {
|
|
fragmentType = .fragmentStart
|
|
} else if index == fragments.count - 1 {
|
|
fragmentType = .fragmentEnd
|
|
} else {
|
|
fragmentType = .fragmentContinue
|
|
}
|
|
|
|
let fragmentPacket = BitchatPacket(
|
|
type: fragmentType.rawValue,
|
|
ttl: packet.ttl,
|
|
senderID: myPeerID,
|
|
payload: fragmentPayload
|
|
)
|
|
|
|
// Send fragments with linear delay
|
|
let totalDelay = Double(index) * delayBetweenFragments
|
|
|
|
// Send fragments on background queue with calculated delay
|
|
messageQueue.asyncAfter(deadline: .now() + totalDelay) { [weak self] in
|
|
self?.broadcastPacket(fragmentPacket)
|
|
// Sent fragment
|
|
}
|
|
}
|
|
|
|
let _ = Double(fragments.count - 1) * delayBetweenFragments
|
|
// Total fragment send time
|
|
}
|
|
|
|
private func handleFragment(_ packet: BitchatPacket, from peerID: String) {
|
|
// Handling fragment
|
|
|
|
guard packet.payload.count >= 13 else {
|
|
// Fragment payload too small
|
|
return
|
|
}
|
|
|
|
// Convert to array for safer access
|
|
let payloadArray = Array(packet.payload)
|
|
var offset = 0
|
|
|
|
// Extract fragment ID as binary data (8 bytes)
|
|
guard payloadArray.count >= 8 else {
|
|
// Payload too small for fragment ID
|
|
return
|
|
}
|
|
|
|
let fragmentIDData = Data(payloadArray[0..<8])
|
|
let fragmentID = fragmentIDData.hexEncodedString()
|
|
// Fragment ID
|
|
offset = 8
|
|
|
|
// Safely extract index
|
|
guard payloadArray.count >= offset + 2 else {
|
|
// Not enough data for index
|
|
return
|
|
}
|
|
let index = Int(payloadArray[offset]) << 8 | Int(payloadArray[offset + 1])
|
|
offset += 2
|
|
// Fragment index
|
|
|
|
// Safely extract total
|
|
guard payloadArray.count >= offset + 2 else {
|
|
// Not enough data for total
|
|
return
|
|
}
|
|
let total = Int(payloadArray[offset]) << 8 | Int(payloadArray[offset + 1])
|
|
offset += 2
|
|
// Total fragments
|
|
|
|
// Safely extract original type
|
|
guard payloadArray.count >= offset + 1 else {
|
|
// Not enough data for type
|
|
return
|
|
}
|
|
let originalType = payloadArray[offset]
|
|
offset += 1
|
|
// Original type
|
|
|
|
// Extract fragment data
|
|
let fragmentData: Data
|
|
if payloadArray.count > offset {
|
|
fragmentData = Data(payloadArray[offset...])
|
|
} else {
|
|
fragmentData = Data()
|
|
}
|
|
|
|
// Received fragment
|
|
|
|
// Initialize fragment collection if needed
|
|
if incomingFragments[fragmentID] == nil {
|
|
incomingFragments[fragmentID] = [:]
|
|
fragmentMetadata[fragmentID] = (originalType, total, Date())
|
|
// Started collecting fragments
|
|
}
|
|
|
|
// Store fragment
|
|
incomingFragments[fragmentID]?[index] = fragmentData
|
|
|
|
// Fragment collection progress
|
|
|
|
// Check if we have all fragments
|
|
if let fragments = incomingFragments[fragmentID],
|
|
fragments.count == total {
|
|
|
|
// Reassemble the original packet
|
|
var reassembledData = Data()
|
|
for i in 0..<total {
|
|
if let fragment = fragments[i] {
|
|
reassembledData.append(fragment)
|
|
} else {
|
|
// Missing fragment
|
|
return
|
|
}
|
|
}
|
|
|
|
// Successfully reassembled fragments
|
|
|
|
// Parse and handle the reassembled packet
|
|
if let reassembledPacket = BitchatPacket.from(reassembledData) {
|
|
// Clean up
|
|
incomingFragments.removeValue(forKey: fragmentID)
|
|
fragmentMetadata.removeValue(forKey: fragmentID)
|
|
|
|
// Handle the reassembled packet
|
|
handleReceivedPacket(reassembledPacket, from: peerID, peripheral: nil)
|
|
}
|
|
}
|
|
|
|
// Clean up old fragments (older than 30 seconds)
|
|
let cutoffTime = Date().addingTimeInterval(-30)
|
|
for (fragID, metadata) in fragmentMetadata {
|
|
if metadata.timestamp < cutoffTime {
|
|
incomingFragments.removeValue(forKey: fragID)
|
|
fragmentMetadata.removeValue(forKey: fragID)
|
|
// Cleaned up expired fragments
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension BluetoothMeshService: CBCentralManagerDelegate {
|
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
if central.state == .poweredOn {
|
|
startScanning()
|
|
|
|
// Send announces when central manager is ready
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
self?.sendBroadcastAnnounce()
|
|
}
|
|
}
|
|
}
|
|
|
|
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
|
|
// Optimize for 300m range - only connect to strong enough signals
|
|
let rssiValue = RSSI.intValue
|
|
|
|
// Filter out very weak signals (below -90 dBm) to save battery
|
|
guard rssiValue > -90 else { return }
|
|
|
|
// Throttle RSSI updates to save CPU
|
|
let peripheralID = peripheral.identifier.uuidString
|
|
if let lastUpdate = lastRSSIUpdate[peripheralID],
|
|
Date().timeIntervalSince(lastUpdate) < 1.0 {
|
|
return // Skip update if less than 1 second since last update
|
|
}
|
|
lastRSSIUpdate[peripheralID] = Date()
|
|
|
|
// Store RSSI by peripheral ID for later use
|
|
peripheralRSSI[peripheralID] = RSSI
|
|
|
|
// Extract peer ID from name (no prefix for stealth)
|
|
if let name = peripheral.name, name.count == 8 {
|
|
// Assume 8-character names are peer IDs
|
|
let peerID = name
|
|
peerRSSI[peerID] = RSSI
|
|
// Discovered potential peer
|
|
}
|
|
|
|
// Connection pooling with exponential backoff
|
|
// peripheralID already declared above
|
|
|
|
// Check if we should attempt connection (considering backoff)
|
|
if let backoffTime = connectionBackoff[peripheralID],
|
|
Date().timeIntervalSince1970 < backoffTime {
|
|
// Still in backoff period, skip connection
|
|
return
|
|
}
|
|
|
|
// Check if we already have this peripheral in our pool
|
|
if let pooledPeripheral = connectionPool[peripheralID] {
|
|
// Reuse existing peripheral from pool
|
|
if pooledPeripheral.state == CBPeripheralState.disconnected {
|
|
// Reconnect if disconnected
|
|
central.connect(pooledPeripheral, options: [
|
|
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
|
|
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
|
|
CBConnectPeripheralOptionNotifyOnNotificationKey: true
|
|
])
|
|
}
|
|
return
|
|
}
|
|
|
|
// New peripheral - add to pool and connect
|
|
if !discoveredPeripherals.contains(peripheral) {
|
|
discoveredPeripherals.append(peripheral)
|
|
peripheral.delegate = self
|
|
connectionPool[peripheralID] = peripheral
|
|
|
|
// Track connection attempts
|
|
let attempts = connectionAttempts[peripheralID] ?? 0
|
|
connectionAttempts[peripheralID] = attempts + 1
|
|
|
|
// Only attempt if under max attempts
|
|
if attempts < maxConnectionAttempts {
|
|
// Use optimized connection parameters for better range
|
|
let connectionOptions: [String: Any] = [
|
|
CBConnectPeripheralOptionNotifyOnConnectionKey: true,
|
|
CBConnectPeripheralOptionNotifyOnDisconnectionKey: true,
|
|
CBConnectPeripheralOptionNotifyOnNotificationKey: true
|
|
]
|
|
|
|
central.connect(peripheral, options: connectionOptions)
|
|
}
|
|
}
|
|
}
|
|
|
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
|
peripheral.delegate = self
|
|
peripheral.discoverServices([BluetoothMeshService.serviceUUID])
|
|
|
|
// Store peripheral by its system ID temporarily until we get the real peer ID
|
|
let tempID = peripheral.identifier.uuidString
|
|
connectedPeripherals[tempID] = peripheral
|
|
|
|
// Connected to peripheral
|
|
|
|
// Request RSSI reading
|
|
peripheral.readRSSI()
|
|
}
|
|
|
|
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
|
|
let peripheralID = peripheral.identifier.uuidString
|
|
|
|
// Implement exponential backoff for failed connections
|
|
if error != nil {
|
|
let attempts = connectionAttempts[peripheralID] ?? 0
|
|
if attempts >= maxConnectionAttempts {
|
|
// Max attempts reached, apply long backoff
|
|
let backoffDuration = baseBackoffInterval * pow(2.0, Double(attempts))
|
|
connectionBackoff[peripheralID] = Date().timeIntervalSince1970 + backoffDuration
|
|
}
|
|
} else {
|
|
// Clean disconnect, reset attempts
|
|
connectionAttempts[peripheralID] = 0
|
|
connectionBackoff.removeValue(forKey: peripheralID)
|
|
}
|
|
|
|
if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key {
|
|
connectedPeripherals.removeValue(forKey: peerID)
|
|
peripheralCharacteristics.removeValue(forKey: peripheral)
|
|
|
|
// Remove from active peers
|
|
activePeers.remove(peerID)
|
|
announcedPeers.remove(peerID)
|
|
announcedToPeers.remove(peerID)
|
|
|
|
// Only show disconnect if we have a resolved nickname
|
|
if let nickname = peerNicknames[peerID], nickname != peerID {
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didDisconnectFromPeer(nickname)
|
|
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
}
|
|
} else {
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep in pool but remove from discovered list
|
|
discoveredPeripherals.removeAll { $0 == peripheral }
|
|
|
|
// Continue scanning for reconnection
|
|
if centralManager.state == .poweredOn {
|
|
// Stop and restart to ensure clean state
|
|
centralManager.stopScan()
|
|
centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
|
|
}
|
|
}
|
|
}
|
|
|
|
extension BluetoothMeshService: CBPeripheralDelegate {
|
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
|
|
guard let services = peripheral.services else { return }
|
|
|
|
for service in services {
|
|
peripheral.discoverCharacteristics([BluetoothMeshService.characteristicUUID], for: service)
|
|
}
|
|
}
|
|
|
|
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
|
|
guard let characteristics = service.characteristics else { return }
|
|
|
|
for characteristic in characteristics {
|
|
if characteristic.uuid == BluetoothMeshService.characteristicUUID {
|
|
peripheral.setNotifyValue(true, for: characteristic)
|
|
peripheralCharacteristics[peripheral] = characteristic
|
|
|
|
// Request maximum MTU for faster data transfer
|
|
// iOS supports up to 512 bytes with BLE 5.0
|
|
peripheral.maximumWriteValueLength(for: .withoutResponse)
|
|
|
|
// Send key exchange and announce immediately without any delay
|
|
let publicKeyData = self.encryptionService.getCombinedPublicKeyData()
|
|
let packet = BitchatPacket(
|
|
type: MessageType.keyExchange.rawValue,
|
|
ttl: 1,
|
|
senderID: self.myPeerID,
|
|
payload: publicKeyData
|
|
)
|
|
|
|
if let data = packet.toBinaryData() {
|
|
peripheral.writeValue(data, for: characteristic, type: .withResponse)
|
|
// Sent key exchange
|
|
}
|
|
|
|
// Send announce packet immediately after key exchange
|
|
// Send multiple times for reliability
|
|
if let vm = self.delegate as? ChatViewModel {
|
|
// Send announces multiple times with delays
|
|
for delay in [0.1, 0.5, 1.0] {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
guard let self = self else { return }
|
|
let announcePacket = BitchatPacket(
|
|
type: MessageType.announce.rawValue,
|
|
ttl: 1,
|
|
senderID: self.myPeerID,
|
|
payload: Data(vm.nickname.utf8)
|
|
)
|
|
self.broadcastPacket(announcePacket)
|
|
// [KEY_EXCHANGE] Sent announce broadcast
|
|
}
|
|
}
|
|
|
|
// Also send targeted announce to this specific peripheral
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self, weak peripheral] in
|
|
guard let self = self,
|
|
let peripheral = peripheral,
|
|
let characteristic = peripheral.services?.first(where: { $0.uuid == BluetoothMeshService.serviceUUID })?.characteristics?.first(where: { $0.uuid == BluetoothMeshService.characteristicUUID }) else { return }
|
|
|
|
let announcePacket = BitchatPacket(
|
|
type: MessageType.announce.rawValue,
|
|
ttl: 1,
|
|
senderID: self.myPeerID,
|
|
payload: Data(vm.nickname.utf8)
|
|
)
|
|
if let data = announcePacket.toBinaryData() {
|
|
peripheral.writeValue(data, for: characteristic, type: .withResponse)
|
|
// Sent targeted announce
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
guard let data = characteristic.value else {
|
|
// No data in characteristic
|
|
return
|
|
}
|
|
|
|
guard let packet = BitchatPacket.from(data) else {
|
|
// Failed to parse packet
|
|
return
|
|
}
|
|
|
|
// Use the sender ID from the packet, not our local mapping which might still be a temp ID
|
|
let _ = connectedPeripherals.first(where: { $0.value == peripheral })?.key ?? "unknown"
|
|
let packetSenderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "unknown"
|
|
// Received data from peer
|
|
|
|
// Always handle received packets
|
|
handleReceivedPacket(packet, from: packetSenderID, peripheral: peripheral)
|
|
}
|
|
|
|
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
|
|
if let error = error {
|
|
print("[ERROR] Write failed: \(error)")
|
|
} else {
|
|
// Write completed
|
|
}
|
|
}
|
|
|
|
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
|
|
peripheral.discoverServices([BluetoothMeshService.serviceUUID])
|
|
}
|
|
|
|
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
|
|
// Handle notification state updates if needed
|
|
}
|
|
|
|
func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) {
|
|
guard error == nil else { return }
|
|
|
|
// Find the peer ID for this peripheral
|
|
if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key {
|
|
// Handle both temp IDs and real peer IDs
|
|
DispatchQueue.main.async { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
if peerID.count > 8 {
|
|
// It's a temp ID, store RSSI temporarily
|
|
self.peripheralRSSI[peerID] = RSSI
|
|
// Keep trying to read RSSI until we get real peer ID
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak peripheral] in
|
|
peripheral?.readRSSI()
|
|
}
|
|
} else {
|
|
// It's a real peer ID, store it
|
|
self.peerRSSI[peerID] = RSSI
|
|
// Force UI update when we have a real peer ID
|
|
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
}
|
|
}
|
|
|
|
// Periodically update RSSI
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak peripheral] in
|
|
peripheral?.readRSSI()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
|
switch peripheral.state {
|
|
case .poweredOn:
|
|
setupPeripheral()
|
|
startAdvertising()
|
|
|
|
// Send announces when peripheral manager is ready
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
self?.sendBroadcastAnnounce()
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
|
|
// Handle service addition if needed
|
|
}
|
|
|
|
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
|
for request in requests {
|
|
if let data = request.value,
|
|
let packet = BitchatPacket.from(data) {
|
|
// Try to identify peer from packet
|
|
let peerID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) ?? "unknown"
|
|
|
|
// Received write from peer
|
|
|
|
// Store the central for updates
|
|
if !subscribedCentrals.contains(request.central) {
|
|
subscribedCentrals.append(request.central)
|
|
}
|
|
|
|
// Track this peer as connected
|
|
if peerID != "unknown" && peerID != myPeerID {
|
|
// Send key exchange back if we haven't already
|
|
if packet.type == MessageType.keyExchange.rawValue {
|
|
let publicKeyData = self.encryptionService.getCombinedPublicKeyData()
|
|
let responsePacket = BitchatPacket(
|
|
type: MessageType.keyExchange.rawValue,
|
|
ttl: 1,
|
|
senderID: self.myPeerID,
|
|
payload: publicKeyData
|
|
)
|
|
if let data = responsePacket.toBinaryData() {
|
|
peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [request.central])
|
|
// Sent key exchange response
|
|
}
|
|
|
|
// Send announce immediately after key exchange
|
|
// Send multiple times for reliability
|
|
if let vm = self.delegate as? ChatViewModel {
|
|
for delay in [0.1, 0.5, 1.0] {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
guard let self = self else { return }
|
|
let announcePacket = BitchatPacket(
|
|
type: MessageType.announce.rawValue,
|
|
ttl: 1,
|
|
senderID: self.myPeerID,
|
|
payload: Data(vm.nickname.utf8)
|
|
)
|
|
if let data = announcePacket.toBinaryData() {
|
|
peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: nil)
|
|
// Sent announce
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
}
|
|
}
|
|
|
|
handleReceivedPacket(packet, from: peerID)
|
|
peripheral.respond(to: request, withResult: .success)
|
|
}
|
|
}
|
|
}
|
|
|
|
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
|
if !subscribedCentrals.contains(central) {
|
|
subscribedCentrals.append(central)
|
|
|
|
// Send our public key to the newly connected central
|
|
let publicKeyData = encryptionService.getCombinedPublicKeyData()
|
|
let keyPacket = BitchatPacket(
|
|
type: MessageType.keyExchange.rawValue,
|
|
ttl: 1,
|
|
senderID: myPeerID,
|
|
payload: publicKeyData
|
|
)
|
|
|
|
if let data = keyPacket.toBinaryData() {
|
|
peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [central])
|
|
// Sent initial key exchange
|
|
|
|
// We'll send announce when we receive their key exchange
|
|
}
|
|
|
|
// Update peer list to show we're connected (even without peer ID yet)
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
}
|
|
}
|
|
}
|
|
|
|
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
|
|
subscribedCentrals.removeAll { $0 == central }
|
|
|
|
// If no more centrals are subscribed, clear all central-connected peers
|
|
if subscribedCentrals.isEmpty {
|
|
// Find and remove peers that were connected as centrals only
|
|
let peersToRemove = activePeers.filter { peerID in
|
|
!connectedPeripherals.keys.contains(peerID)
|
|
}
|
|
|
|
for peerID in peersToRemove {
|
|
activePeers.remove(peerID)
|
|
announcedToPeers.remove(peerID)
|
|
if let nickname = peerNicknames[peerID] {
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didDisconnectFromPeer(nickname)
|
|
}
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
|
|
}
|
|
}
|
|
|
|
// Ensure advertising continues for reconnection
|
|
if peripheralManager.state == .poweredOn && !peripheralManager.isAdvertising {
|
|
startAdvertising()
|
|
}
|
|
}
|
|
|
|
// MARK: - Battery Monitoring
|
|
|
|
private func startBatteryMonitoring() {
|
|
// Update battery level immediately
|
|
updateBatteryLevel()
|
|
|
|
// Monitor battery level every 30 seconds
|
|
batteryMonitorTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
|
|
self?.updateBatteryLevel()
|
|
}
|
|
}
|
|
|
|
private func updateBatteryLevel() {
|
|
#if os(iOS)
|
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
|
currentBatteryLevel = UIDevice.current.batteryLevel
|
|
|
|
// Battery level is -1 when unknown (e.g., in simulator)
|
|
if currentBatteryLevel < 0 {
|
|
currentBatteryLevel = 1.0 // Assume full battery when unknown
|
|
}
|
|
#else
|
|
// macOS battery monitoring
|
|
if let batteryInfo = getMacOSBatteryInfo() {
|
|
currentBatteryLevel = batteryInfo
|
|
} else {
|
|
currentBatteryLevel = 1.0 // Assume full battery when unknown
|
|
}
|
|
#endif
|
|
|
|
// Battery level updated
|
|
updateScanParametersForBattery()
|
|
}
|
|
|
|
#if os(macOS)
|
|
private func getMacOSBatteryInfo() -> Float? {
|
|
let snapshot = IOPSCopyPowerSourcesInfo().takeRetainedValue()
|
|
let sources = IOPSCopyPowerSourcesList(snapshot).takeRetainedValue() as Array
|
|
|
|
for source in sources {
|
|
if let description = IOPSGetPowerSourceDescription(snapshot, source).takeUnretainedValue() as? [String: Any] {
|
|
if let currentCapacity = description[kIOPSCurrentCapacityKey] as? Int,
|
|
let maxCapacity = description[kIOPSMaxCapacityKey] as? Int {
|
|
return Float(currentCapacity) / Float(maxCapacity)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
#endif
|
|
|
|
private func updateScanParametersForBattery() {
|
|
// Adaptive scanning based on battery level
|
|
// High battery (80%+): Normal scanning
|
|
// Medium battery (40-80%): Moderate power saving
|
|
// Low battery (20-40%): Aggressive power saving
|
|
// Critical battery (<20%): Maximum power saving
|
|
|
|
if currentBatteryLevel > 0.8 {
|
|
// High battery: Normal operation
|
|
activeScanDuration = 2.0
|
|
scanPauseDuration = 3.0
|
|
// High battery mode
|
|
} else if currentBatteryLevel > 0.4 {
|
|
// Medium battery: Moderate power saving
|
|
activeScanDuration = 1.5
|
|
scanPauseDuration = 4.5
|
|
// Medium battery mode
|
|
} else if currentBatteryLevel > 0.2 {
|
|
// Low battery: Aggressive power saving
|
|
activeScanDuration = 1.0
|
|
scanPauseDuration = 8.0
|
|
// Low battery mode
|
|
} else {
|
|
// Critical battery: Maximum power saving
|
|
activeScanDuration = 0.5
|
|
scanPauseDuration = 15.0
|
|
// Critical battery mode
|
|
}
|
|
|
|
// If we're currently in a duty cycle, restart it with new parameters
|
|
if scanDutyCycleTimer != nil {
|
|
scanDutyCycleTimer?.invalidate()
|
|
scheduleScanDutyCycle()
|
|
}
|
|
}
|
|
|
|
// MARK: - Privacy Utilities
|
|
|
|
private func randomDelay() -> TimeInterval {
|
|
// Generate random delay between min and max for timing obfuscation
|
|
return TimeInterval.random(in: minMessageDelay...maxMessageDelay)
|
|
}
|
|
|
|
// MARK: - Cover Traffic
|
|
|
|
private func startCoverTraffic() {
|
|
// Start cover traffic with random interval
|
|
scheduleCoverTraffic()
|
|
}
|
|
|
|
private func scheduleCoverTraffic() {
|
|
// Random interval between 30-120 seconds
|
|
let interval = TimeInterval.random(in: 30...120)
|
|
|
|
coverTrafficTimer?.invalidate()
|
|
coverTrafficTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
|
|
self?.sendDummyMessage()
|
|
self?.scheduleCoverTraffic() // Schedule next dummy message
|
|
}
|
|
}
|
|
|
|
private func sendDummyMessage() {
|
|
// Only send dummy messages if we have connected peers
|
|
let peers = getAllConnectedPeerIDs()
|
|
guard !peers.isEmpty else { return }
|
|
|
|
// Skip if battery is low
|
|
if currentBatteryLevel < 0.2 {
|
|
// Skipping cover traffic due to low battery
|
|
return
|
|
}
|
|
|
|
// Pick a random peer to send to
|
|
guard let randomPeer = peers.randomElement() else { return }
|
|
|
|
// Generate random dummy content
|
|
let dummyContent = generateDummyContent()
|
|
|
|
// Sending cover traffic
|
|
|
|
// Send as a private message so it's encrypted
|
|
sendPrivateMessage(dummyContent, to: randomPeer, recipientNickname: peerNicknames[randomPeer] ?? "unknown")
|
|
}
|
|
|
|
private func generateDummyContent() -> String {
|
|
// Generate realistic-looking dummy messages
|
|
let templates = [
|
|
"hey",
|
|
"ok",
|
|
"got it",
|
|
"sure",
|
|
"sounds good",
|
|
"thanks",
|
|
"np",
|
|
"see you there",
|
|
"on my way",
|
|
"running late",
|
|
"be there soon",
|
|
"👍",
|
|
"✓",
|
|
"meeting at the usual spot",
|
|
"confirmed",
|
|
"roger that"
|
|
]
|
|
|
|
// Prefix with dummy marker (will be encrypted)
|
|
return coverTrafficPrefix + (templates.randomElement() ?? "ok")
|
|
}
|
|
} |