mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 17:05:19 +00:00
Improve BLE connection stability and message reliability (#318)
* Remove sequence numbers from protocol - Remove sequenceNumber field from BitchatPacket struct - Update BinaryProtocol to not encode/decode sequence numbers (header size reduced from 17 to 13 bytes) - Replace sequence-based duplicate detection with content-based using packet ID hash - Update packet ID generation to use SHA256(senderID + timestamp + type + payload prefix) - Remove all sequence tracking variables and methods - Simplify duplicate detection to rely on timestamp and content hashing * Fix connection stability issues - Increase peer availability check interval from 5 to 15 seconds - Fix availability logic to not mark connected peers as unavailable - Add BLE connection keepalive timer (20s) to prevent iOS timeouts - Fix missing delivery ACKs by passing peripheral context through Noise decryption - Reduce identity announce frequency from 2 to 10 seconds minimum - Remove unnecessary identity announces on connection - Debounce identity cache keychain saves (2 second delay) - Add message retry notification handler in ChatViewModel - Fix version negotiation redundancy by checking existing negotiations - Keep Noise sessions for already-connected peers * Fix build errors in message retry handler - Fix reference to 'displayedMessages' - should be 'messages' - Fix sendMessage call signature to use individual parameters instead of message object - Both iOS and macOS builds now succeed * Fix remaining connection stability issues - Fix duplicate identity announces with content-based deduplication - Simplify peripheral mapping with cleaner temp ID to peer ID transitions - Improve graceful leave detection across peer ID rotations - Track previousPeerID from announcements to maintain state - Add time-based cleanup for gracefully left peers * Fix connection stability issues - Add special duplicate detection for identity announces - Simplify peripheral mapping with dedicated structure - Improve graceful leave detection with peer ID rotation handling - Track graceful leave timestamps for cleanup - Transfer states properly during peer ID rotation * Improve BLE connection stability and message reliability - Increase peer availability timeout from 5s to 15s to prevent flapping - Add BLE keepalive timer with 30s interval to maintain connections - Fix missing delivery ACKs by passing peripheral context through decryption - Reduce identity announce frequency from 2s to 10s minimum interval - Add keychain save debouncing with 2s delay to prevent excessive writes - Implement message retry system for failed deliveries to favorites - Fix version negotiation redundancy by checking existing state - Add special duplicate detection for identity announcements - Implement graceful leave detection with peer ID rotation support - Simplify peripheral mapping to reduce complexity - Fix switch statement structure issues causing build errors These changes significantly improve connection stability, eliminate peer availability flapping, and ensure reliable message delivery. --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -27,6 +27,11 @@ class SecureIdentityStateManager {
|
|||||||
// Thread safety
|
// Thread safety
|
||||||
private let queue = DispatchQueue(label: "bitchat.identity.state", attributes: .concurrent)
|
private let queue = DispatchQueue(label: "bitchat.identity.state", attributes: .concurrent)
|
||||||
|
|
||||||
|
// Debouncing for keychain saves
|
||||||
|
private var saveTimer: Timer?
|
||||||
|
private let saveDebounceInterval: TimeInterval = 2.0 // Save at most once every 2 seconds
|
||||||
|
private var pendingSave = false
|
||||||
|
|
||||||
// Encryption key
|
// Encryption key
|
||||||
private let encryptionKey: SymmetricKey
|
private let encryptionKey: SymmetricKey
|
||||||
|
|
||||||
@@ -72,16 +77,48 @@ class SecureIdentityStateManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
// Force save any pending changes
|
||||||
|
forceSave()
|
||||||
|
}
|
||||||
|
|
||||||
func saveIdentityCache() {
|
func saveIdentityCache() {
|
||||||
|
// Mark that we need to save
|
||||||
|
pendingSave = true
|
||||||
|
|
||||||
|
// Cancel any existing timer
|
||||||
|
saveTimer?.invalidate()
|
||||||
|
|
||||||
|
// Schedule a new save after the debounce interval
|
||||||
|
saveTimer = Timer.scheduledTimer(withTimeInterval: saveDebounceInterval, repeats: false) { [weak self] _ in
|
||||||
|
self?.performSave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func performSave() {
|
||||||
|
guard pendingSave else { return }
|
||||||
|
pendingSave = false
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let data = try JSONEncoder().encode(cache)
|
let data = try JSONEncoder().encode(cache)
|
||||||
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
|
let sealedBox = try AES.GCM.seal(data, using: encryptionKey)
|
||||||
_ = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey)
|
let saved = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey)
|
||||||
|
if saved {
|
||||||
|
SecureLogger.log("Identity cache saved to keychain", category: SecureLogger.security, level: .debug)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
SecureLogger.logError(error, context: "Failed to save identity cache", category: SecureLogger.security)
|
SecureLogger.logError(error, context: "Failed to save identity cache", category: SecureLogger.security)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force immediate save (for app termination)
|
||||||
|
func forceSave() {
|
||||||
|
saveTimer?.invalidate()
|
||||||
|
if pendingSave {
|
||||||
|
performSave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Identity Resolution
|
// MARK: - Identity Resolution
|
||||||
|
|
||||||
func resolveIdentity(peerID: String, claimedNickname: String) -> IdentityHint {
|
func resolveIdentity(peerID: String, claimedNickname: String) -> IdentityHint {
|
||||||
|
|||||||
@@ -19,12 +19,11 @@ extension Data {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Binary Protocol Format:
|
// Binary Protocol Format:
|
||||||
// Header (Fixed 17 bytes):
|
// Header (Fixed 13 bytes):
|
||||||
// - Version: 1 byte
|
// - Version: 1 byte
|
||||||
// - Type: 1 byte
|
// - Type: 1 byte
|
||||||
// - TTL: 1 byte
|
// - TTL: 1 byte
|
||||||
// - Timestamp: 8 bytes (UInt64)
|
// - Timestamp: 8 bytes (UInt64)
|
||||||
// - SequenceNumber: 4 bytes (UInt32)
|
|
||||||
// - Flags: 1 byte (bit 0: hasRecipient, bit 1: hasSignature)
|
// - Flags: 1 byte (bit 0: hasRecipient, bit 1: hasSignature)
|
||||||
// - PayloadLength: 2 bytes (UInt16)
|
// - PayloadLength: 2 bytes (UInt16)
|
||||||
//
|
//
|
||||||
@@ -35,7 +34,7 @@ extension Data {
|
|||||||
// - Signature: 64 bytes (if hasSignature flag set)
|
// - Signature: 64 bytes (if hasSignature flag set)
|
||||||
|
|
||||||
struct BinaryProtocol {
|
struct BinaryProtocol {
|
||||||
static let headerSize = 17
|
static let headerSize = 13
|
||||||
static let senderIDSize = 8
|
static let senderIDSize = 8
|
||||||
static let recipientIDSize = 8
|
static let recipientIDSize = 8
|
||||||
static let signatureSize = 64
|
static let signatureSize = 64
|
||||||
@@ -78,11 +77,6 @@ struct BinaryProtocol {
|
|||||||
data.append(UInt8((packet.timestamp >> (i * 8)) & 0xFF))
|
data.append(UInt8((packet.timestamp >> (i * 8)) & 0xFF))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sequence number (4 bytes, big-endian)
|
|
||||||
for i in (0..<4).reversed() {
|
|
||||||
data.append(UInt8((packet.sequenceNumber >> (i * 8)) & 0xFF))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
var flags: UInt8 = 0
|
var flags: UInt8 = 0
|
||||||
if packet.recipientID != nil {
|
if packet.recipientID != nil {
|
||||||
@@ -171,13 +165,6 @@ struct BinaryProtocol {
|
|||||||
}
|
}
|
||||||
offset += 8
|
offset += 8
|
||||||
|
|
||||||
// Sequence number
|
|
||||||
let sequenceData = unpaddedData[offset..<offset+4]
|
|
||||||
let sequenceNumber = sequenceData.reduce(0) { result, byte in
|
|
||||||
(result << 8) | UInt32(byte)
|
|
||||||
}
|
|
||||||
offset += 4
|
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
let flags = unpaddedData[offset]; offset += 1
|
let flags = unpaddedData[offset]; offset += 1
|
||||||
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
let hasRecipient = (flags & Flags.hasRecipient) != 0
|
||||||
@@ -253,8 +240,7 @@ struct BinaryProtocol {
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
payload: payload,
|
payload: payload,
|
||||||
signature: signature,
|
signature: signature,
|
||||||
ttl: ttl,
|
ttl: ttl
|
||||||
sequenceNumber: sequenceNumber
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,9 +139,8 @@ struct BitchatPacket: Codable {
|
|||||||
let payload: Data
|
let payload: Data
|
||||||
let signature: Data?
|
let signature: Data?
|
||||||
var ttl: UInt8
|
var ttl: UInt8
|
||||||
let sequenceNumber: UInt32 // New field for duplicate detection
|
|
||||||
|
|
||||||
init(type: UInt8, senderID: Data, recipientID: Data?, timestamp: UInt64, payload: Data, signature: Data?, ttl: UInt8, sequenceNumber: UInt32 = 0) {
|
init(type: UInt8, senderID: Data, recipientID: Data?, timestamp: UInt64, payload: Data, signature: Data?, ttl: UInt8) {
|
||||||
self.version = 1
|
self.version = 1
|
||||||
self.type = type
|
self.type = type
|
||||||
self.senderID = senderID
|
self.senderID = senderID
|
||||||
@@ -150,11 +149,10 @@ struct BitchatPacket: Codable {
|
|||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.signature = signature
|
self.signature = signature
|
||||||
self.ttl = ttl
|
self.ttl = ttl
|
||||||
self.sequenceNumber = sequenceNumber
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convenience initializer for new binary format
|
// Convenience initializer for new binary format
|
||||||
init(type: UInt8, ttl: UInt8, senderID: String, payload: Data, sequenceNumber: UInt32 = 0) {
|
init(type: UInt8, ttl: UInt8, senderID: String, payload: Data) {
|
||||||
self.version = 1
|
self.version = 1
|
||||||
self.type = type
|
self.type = type
|
||||||
// Convert hex string peer ID to binary data (8 bytes)
|
// Convert hex string peer ID to binary data (8 bytes)
|
||||||
@@ -173,7 +171,6 @@ struct BitchatPacket: Codable {
|
|||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.signature = nil
|
self.signature = nil
|
||||||
self.ttl = ttl
|
self.ttl = ttl
|
||||||
self.sequenceNumber = sequenceNumber
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var data: Data? {
|
var data: Data? {
|
||||||
|
|||||||
@@ -334,6 +334,10 @@ class BluetoothMeshService: NSObject {
|
|||||||
private let processedMessagesLock = NSLock()
|
private let processedMessagesLock = NSLock()
|
||||||
private let maxProcessedMessages = 10000
|
private let maxProcessedMessages = 10000
|
||||||
|
|
||||||
|
// Special handling for identity announces to prevent duplicates
|
||||||
|
private var recentIdentityAnnounces = [String: Date]() // peerID -> last seen time
|
||||||
|
private let identityAnnounceDuplicateWindow: TimeInterval = 30.0 // 30 second window
|
||||||
|
|
||||||
private let maxTTL: UInt8 = 7 // Maximum hops for long-distance delivery
|
private let maxTTL: UInt8 = 7 // Maximum hops for long-distance delivery
|
||||||
private var announcedToPeers = Set<String>() // Track which peers we've announced to
|
private var announcedToPeers = Set<String>() // Track which peers we've announced to
|
||||||
private var announcedPeers = Set<String>() // Track peers who have already been announced
|
private var announcedPeers = Set<String>() // Track peers who have already been announced
|
||||||
@@ -344,6 +348,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
private let networkEmptyResetDelay: TimeInterval = 60 // 1 minute before resetting notification flag
|
private let networkEmptyResetDelay: TimeInterval = 60 // 1 minute before resetting notification flag
|
||||||
private var intentionalDisconnects = Set<String>() // Track peripherals we're disconnecting intentionally
|
private var intentionalDisconnects = Set<String>() // Track peripherals we're disconnecting intentionally
|
||||||
private var gracefullyLeftPeers = Set<String>() // Track peers that sent leave messages
|
private var gracefullyLeftPeers = Set<String>() // Track peers that sent leave messages
|
||||||
|
private var gracefulLeaveTimestamps: [String: Date] = [:] // Track when peers left
|
||||||
|
private let gracefulLeaveExpirationTime: TimeInterval = 300.0 // 5 minutes
|
||||||
private var peerLastSeenTimestamps = LRUCache<String, Date>(maxSize: 100) // Bounded cache for peer timestamps
|
private var peerLastSeenTimestamps = LRUCache<String, Date>(maxSize: 100) // Bounded cache for peer timestamps
|
||||||
private var cleanupTimer: Timer? // Timer to clean up stale peers
|
private var cleanupTimer: Timer? // Timer to clean up stale peers
|
||||||
|
|
||||||
@@ -385,7 +391,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Track when we last sent identity announcements to prevent flooding
|
// Track when we last sent identity announcements to prevent flooding
|
||||||
private var lastIdentityAnnounceTimes: [String: Date] = [:]
|
private var lastIdentityAnnounceTimes: [String: Date] = [:]
|
||||||
private let identityAnnounceMinInterval: TimeInterval = 2.0 // Minimum 2 seconds between announcements per peer
|
private let identityAnnounceMinInterval: TimeInterval = 10.0 // Minimum 10 seconds between announcements per peer
|
||||||
|
|
||||||
// Track handshake attempts to handle timeouts
|
// Track handshake attempts to handle timeouts
|
||||||
private var handshakeAttemptTimes: [String: Date] = [:]
|
private var handshakeAttemptTimes: [String: Date] = [:]
|
||||||
@@ -409,6 +415,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
private let maxAckRetries = 3
|
private let maxAckRetries = 3
|
||||||
private var ackTimer: Timer?
|
private var ackTimer: Timer?
|
||||||
private var advertisingTimer: Timer? // Timer for interval-based advertising
|
private var advertisingTimer: Timer? // Timer for interval-based advertising
|
||||||
|
private var connectionKeepAliveTimer: Timer? // Timer to send keepalive pings
|
||||||
|
private let keepAliveInterval: TimeInterval = 20.0 // Send keepalive every 20 seconds
|
||||||
|
|
||||||
// Timing randomization for privacy (now with exponential distribution)
|
// Timing randomization for privacy (now with exponential distribution)
|
||||||
private let minMessageDelay: TimeInterval = 0.01 // 10ms minimum for faster sync
|
private let minMessageDelay: TimeInterval = 0.01 // 10ms minimum for faster sync
|
||||||
@@ -446,6 +454,72 @@ class BluetoothMeshService: NSObject {
|
|||||||
private let maxConnectionAttempts = 3
|
private let maxConnectionAttempts = 3
|
||||||
private let baseBackoffInterval: TimeInterval = 1.0
|
private let baseBackoffInterval: TimeInterval = 1.0
|
||||||
|
|
||||||
|
// Simplified peripheral mapping system
|
||||||
|
private struct PeripheralMapping {
|
||||||
|
let peripheral: CBPeripheral
|
||||||
|
var peerID: String? // nil until we receive announce
|
||||||
|
var rssi: NSNumber?
|
||||||
|
var lastActivity: Date
|
||||||
|
|
||||||
|
var isIdentified: Bool { peerID != nil }
|
||||||
|
}
|
||||||
|
private var peripheralMappings: [String: PeripheralMapping] = [:] // peripheralID -> mapping
|
||||||
|
|
||||||
|
// Helper methods for peripheral mapping
|
||||||
|
private func registerPeripheral(_ peripheral: CBPeripheral) {
|
||||||
|
let peripheralID = peripheral.identifier.uuidString
|
||||||
|
peripheralMappings[peripheralID] = PeripheralMapping(
|
||||||
|
peripheral: peripheral,
|
||||||
|
peerID: nil,
|
||||||
|
rssi: nil,
|
||||||
|
lastActivity: Date()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePeripheralMapping(peripheralID: String, peerID: String) {
|
||||||
|
guard var mapping = peripheralMappings[peripheralID] else { return }
|
||||||
|
|
||||||
|
// Remove old temp mapping if it exists
|
||||||
|
let tempID = peripheralID
|
||||||
|
if connectedPeripherals[tempID] != nil {
|
||||||
|
connectedPeripherals.removeValue(forKey: tempID)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping.peerID = peerID
|
||||||
|
mapping.lastActivity = Date()
|
||||||
|
peripheralMappings[peripheralID] = mapping
|
||||||
|
|
||||||
|
// Update legacy mappings
|
||||||
|
peerIDByPeripheralID[peripheralID] = peerID
|
||||||
|
connectedPeripherals[peerID] = mapping.peripheral
|
||||||
|
|
||||||
|
// Transfer RSSI
|
||||||
|
if let rssi = mapping.rssi {
|
||||||
|
peerRSSI[peerID] = rssi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePeripheralRSSI(peripheralID: String, rssi: NSNumber) {
|
||||||
|
guard var mapping = peripheralMappings[peripheralID] else { return }
|
||||||
|
mapping.rssi = rssi
|
||||||
|
mapping.lastActivity = Date()
|
||||||
|
peripheralMappings[peripheralID] = mapping
|
||||||
|
|
||||||
|
// Update peer RSSI if we have the peer ID
|
||||||
|
if let peerID = mapping.peerID {
|
||||||
|
peerRSSI[peerID] = rssi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findPeripheralForPeerID(_ peerID: String) -> CBPeripheral? {
|
||||||
|
for (_, mapping) in peripheralMappings {
|
||||||
|
if mapping.peerID == peerID {
|
||||||
|
return mapping.peripheral
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Probabilistic flooding
|
// Probabilistic flooding
|
||||||
private var relayProbability: Double = 1.0 // Start at 100%, decrease with peer count
|
private var relayProbability: Double = 1.0 // Start at 100%, decrease with peer count
|
||||||
private let minRelayProbability: Double = 0.4 // Minimum 40% relay chance - ensures coverage
|
private let minRelayProbability: Double = 0.4 // Minimum 40% relay chance - ensures coverage
|
||||||
@@ -465,15 +539,6 @@ class BluetoothMeshService: NSObject {
|
|||||||
return max(activePeers.count, connectedPeripherals.count)
|
return max(activePeers.count, connectedPeripherals.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sequence number tracking for duplicate detection
|
|
||||||
private var currentSequenceNumber: UInt32 = 0
|
|
||||||
private var peerSequenceNumbers: [String: UInt32] = [:] // Track last seen sequence per peer
|
|
||||||
|
|
||||||
private func getNextSequenceNumber() -> UInt32 {
|
|
||||||
currentSequenceNumber += 1
|
|
||||||
return currentSequenceNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adaptive parameters based on network size
|
// Adaptive parameters based on network size
|
||||||
private var adaptiveTTL: UInt8 {
|
private var adaptiveTTL: UInt8 {
|
||||||
// Keep TTL high enough for messages to travel far
|
// Keep TTL high enough for messages to travel far
|
||||||
@@ -625,6 +690,14 @@ class BluetoothMeshService: NSObject {
|
|||||||
oldPeerID = existingPeerID
|
oldPeerID = existingPeerID
|
||||||
SecureLogger.log("Peer ID rotation detected: \(existingPeerID) -> \(newPeerID) for fingerprint \(fingerprint)", category: SecureLogger.security, level: .info)
|
SecureLogger.log("Peer ID rotation detected: \(existingPeerID) -> \(newPeerID) for fingerprint \(fingerprint)", category: SecureLogger.security, level: .info)
|
||||||
|
|
||||||
|
// Transfer gracefullyLeftPeers state from old to new peer ID
|
||||||
|
if self.gracefullyLeftPeers.contains(existingPeerID) {
|
||||||
|
self.gracefullyLeftPeers.remove(existingPeerID)
|
||||||
|
self.gracefullyLeftPeers.insert(newPeerID)
|
||||||
|
SecureLogger.log("Transferred gracefullyLeft state from \(existingPeerID) to \(newPeerID)",
|
||||||
|
category: SecureLogger.session, level: .debug)
|
||||||
|
}
|
||||||
|
|
||||||
self.peerIDToFingerprint.removeValue(forKey: existingPeerID)
|
self.peerIDToFingerprint.removeValue(forKey: existingPeerID)
|
||||||
|
|
||||||
// Transfer nickname if known
|
// Transfer nickname if known
|
||||||
@@ -912,8 +985,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
self?.checkAckTimeouts()
|
self?.checkAckTimeouts()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start peer availability checking timer (every 5 seconds)
|
// Start peer availability checking timer (every 15 seconds)
|
||||||
availabilityCheckTimer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
|
availabilityCheckTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: true) { [weak self] _ in
|
||||||
self?.checkPeerAvailability()
|
self?.checkPeerAvailability()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -935,6 +1008,11 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Start memory cleanup timer (every minute)
|
// Start memory cleanup timer (every minute)
|
||||||
startMemoryCleanupTimer()
|
startMemoryCleanupTimer()
|
||||||
|
|
||||||
|
// Start connection keep-alive timer to prevent iOS BLE timeouts
|
||||||
|
connectionKeepAliveTimer = Timer.scheduledTimer(withTimeInterval: keepAliveInterval, repeats: true) { [weak self] _ in
|
||||||
|
self?.sendKeepAlivePings()
|
||||||
|
}
|
||||||
|
|
||||||
// Log handshake states periodically for debugging and clean up stale states
|
// Log handshake states periodically for debugging and clean up stale states
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
|
Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
|
||||||
@@ -1005,6 +1083,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
cleanupTimer?.invalidate()
|
cleanupTimer?.invalidate()
|
||||||
rotationTimer?.invalidate()
|
rotationTimer?.invalidate()
|
||||||
memoryCleanupTimer?.invalidate()
|
memoryCleanupTimer?.invalidate()
|
||||||
|
connectionKeepAliveTimer?.invalidate()
|
||||||
|
availabilityCheckTimer?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func appWillTerminate() {
|
@objc private func appWillTerminate() {
|
||||||
@@ -1092,9 +1172,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
type: MessageType.announce.rawValue,
|
type: MessageType.announce.rawValue,
|
||||||
ttl: 3, // Increase TTL so announce reaches all peers
|
ttl: 3, // Increase TTL so announce reaches all peers
|
||||||
senderID: myPeerID,
|
senderID: myPeerID,
|
||||||
payload: Data(vm.nickname.utf8),
|
payload: Data(vm.nickname.utf8) )
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
// Single send with smart collision avoidance
|
// Single send with smart collision avoidance
|
||||||
@@ -1102,8 +1180,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + initialDelay) { [weak self] in
|
||||||
self?.broadcastPacket(announcePacket)
|
self?.broadcastPacket(announcePacket)
|
||||||
|
|
||||||
// Also send Noise identity announcement
|
// Don't automatically send identity announcement on startup
|
||||||
self?.sendNoiseIdentityAnnounce()
|
// Let it happen naturally when peers connect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1225,9 +1303,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000), // milliseconds
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000), // milliseconds
|
||||||
payload: messageData,
|
payload: messageData,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: self.adaptiveTTL,
|
ttl: self.adaptiveTTL )
|
||||||
sequenceNumber: self.getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Track this message to prevent duplicate sends
|
// Track this message to prevent duplicate sends
|
||||||
let msgID = "\(packet.timestamp)-\(self.myPeerID)-\(packet.payload.prefix(32).hashValue)"
|
let msgID = "\(packet.timestamp)-\(self.myPeerID)-\(packet.payload.prefix(32).hashValue)"
|
||||||
@@ -1354,9 +1430,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encryptedPayload,
|
payload: encryptedPayload,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 3,
|
ttl: 3 )
|
||||||
sequenceNumber: self.getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.broadcastPacket(outerPacket)
|
self.broadcastPacket(outerPacket)
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1380,8 +1454,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encryptedPayload,
|
payload: encryptedPayload,
|
||||||
signature: nil, // ACKs don't need signatures
|
signature: nil, // ACKs don't need signatures
|
||||||
ttl: 3, // Limited TTL for ACKs
|
ttl: 3 // Limited TTL for ACKs
|
||||||
sequenceNumber: self.getNextSequenceNumber()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send immediately without delay (ACKs should be fast)
|
// Send immediately without delay (ACKs should be fast)
|
||||||
@@ -1440,9 +1513,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: receiptData,
|
payload: receiptData,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 3,
|
ttl: 3 )
|
||||||
sequenceNumber: self.getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Encrypt the entire inner packet
|
// Encrypt the entire inner packet
|
||||||
if let innerData = innerPacket.toBinaryData() {
|
if let innerData = innerPacket.toBinaryData() {
|
||||||
@@ -1456,9 +1527,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encryptedInnerData,
|
payload: encryptedInnerData,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 3,
|
ttl: 3 )
|
||||||
sequenceNumber: self.getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
SecureLogger.log("Sending encrypted read receipt for message \(receipt.originalMessageID) to \(recipientID)", category: SecureLogger.noise, level: .info)
|
SecureLogger.log("Sending encrypted read receipt for message \(receipt.originalMessageID) to \(recipientID)", category: SecureLogger.noise, level: .info)
|
||||||
self.broadcastPacket(outerPacket)
|
self.broadcastPacket(outerPacket)
|
||||||
@@ -1511,9 +1580,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
type: MessageType.announce.rawValue,
|
type: MessageType.announce.rawValue,
|
||||||
ttl: 3, // Allow relay for better reach
|
ttl: 3, // Allow relay for better reach
|
||||||
senderID: myPeerID,
|
senderID: myPeerID,
|
||||||
payload: Data(vm.nickname.utf8),
|
payload: Data(vm.nickname.utf8) )
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
if let data = packet.toBinaryData() {
|
if let data = packet.toBinaryData() {
|
||||||
// Try both broadcast and targeted send
|
// Try both broadcast and targeted send
|
||||||
@@ -1539,9 +1606,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
type: MessageType.leave.rawValue,
|
type: MessageType.leave.rawValue,
|
||||||
ttl: 1, // Don't relay leave messages
|
ttl: 1, // Don't relay leave messages
|
||||||
senderID: myPeerID,
|
senderID: myPeerID,
|
||||||
payload: Data(vm.nickname.utf8),
|
payload: Data(vm.nickname.utf8) )
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
}
|
}
|
||||||
@@ -1730,6 +1795,18 @@ class BluetoothMeshService: NSObject {
|
|||||||
let staleThreshold: TimeInterval = 180.0 // 3 minutes - increased for better stability
|
let staleThreshold: TimeInterval = 180.0 // 3 minutes - increased for better stability
|
||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
|
// Clean up expired gracefully left peers
|
||||||
|
let expiredGracefulPeers = gracefulLeaveTimestamps.filter { (_, timestamp) in
|
||||||
|
now.timeIntervalSince(timestamp) > gracefulLeaveExpirationTime
|
||||||
|
}.map { $0.key }
|
||||||
|
|
||||||
|
for peerID in expiredGracefulPeers {
|
||||||
|
gracefullyLeftPeers.remove(peerID)
|
||||||
|
gracefulLeaveTimestamps.removeValue(forKey: peerID)
|
||||||
|
SecureLogger.log("Cleaned up expired gracefullyLeft entry for \(peerID)",
|
||||||
|
category: SecureLogger.session, level: .debug)
|
||||||
|
}
|
||||||
|
|
||||||
let peersToRemove = collectionsQueue.sync(flags: .barrier) {
|
let peersToRemove = collectionsQueue.sync(flags: .barrier) {
|
||||||
let toRemove = activePeers.filter { peerID in
|
let toRemove = activePeers.filter { peerID in
|
||||||
if let lastSeen = peerLastSeenTimestamps.get(peerID) {
|
if let lastSeen = peerLastSeenTimestamps.get(peerID) {
|
||||||
@@ -2213,7 +2290,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
if senderID != self.myPeerID && isRateLimited(peerID: senderID, messageType: packet.type) {
|
if senderID != self.myPeerID && isRateLimited(peerID: senderID, messageType: packet.type) {
|
||||||
if !isHighPriority {
|
if !isHighPriority {
|
||||||
SecureLogger.log("RATE_LIMITED: Dropped \(messageTypeName) from \(senderID) (seq#\(packet.sequenceNumber))",
|
SecureLogger.log("RATE_LIMITED: Dropped \(messageTypeName) from \(senderID)",
|
||||||
category: SecureLogger.security, level: .warning)
|
category: SecureLogger.security, level: .warning)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
@@ -2227,10 +2304,10 @@ class BluetoothMeshService: NSObject {
|
|||||||
recordMessage(from: senderID, messageType: packet.type)
|
recordMessage(from: senderID, messageType: packet.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sequence-based duplicate detection for more reliability
|
// Content-based duplicate detection using packet ID
|
||||||
let messageID = "\(senderID)-\(packet.sequenceNumber)"
|
let messageID = generatePacketID(for: packet)
|
||||||
|
|
||||||
// Check if we've seen this exact message before (sequence + sender)
|
// Check if we've seen this exact message before
|
||||||
processedMessagesLock.lock()
|
processedMessagesLock.lock()
|
||||||
if let existingState = processedMessages[messageID] {
|
if let existingState = processedMessages[messageID] {
|
||||||
// Update the state
|
// Update the state
|
||||||
@@ -2239,22 +2316,13 @@ class BluetoothMeshService: NSObject {
|
|||||||
processedMessages[messageID] = updatedState
|
processedMessages[messageID] = updatedState
|
||||||
processedMessagesLock.unlock()
|
processedMessagesLock.unlock()
|
||||||
|
|
||||||
SecureLogger.log("Dropped duplicate message seq#\(packet.sequenceNumber) from \(senderID) (seen \(updatedState.seenCount) times)", category: SecureLogger.security, level: .debug)
|
SecureLogger.log("Dropped duplicate message from \(senderID) (seen \(updatedState.seenCount) times)", category: SecureLogger.security, level: .debug)
|
||||||
// Cancel any pending relay for this message
|
// Cancel any pending relay for this message
|
||||||
cancelPendingRelay(messageID: messageID)
|
cancelPendingRelay(messageID: messageID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
processedMessagesLock.unlock()
|
processedMessagesLock.unlock()
|
||||||
|
|
||||||
// Check if this is an old message (sequence number rollback)
|
|
||||||
if let lastSeenSeq = peerSequenceNumbers[senderID] {
|
|
||||||
// Allow some out-of-order tolerance (up to 100 messages)
|
|
||||||
if packet.sequenceNumber < lastSeenSeq && (lastSeenSeq - packet.sequenceNumber) > 100 {
|
|
||||||
SecureLogger.log("Dropped old message seq#\(packet.sequenceNumber) from \(senderID), last seen: \(lastSeenSeq)", category: SecureLogger.security, level: .debug)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use bloom filter for efficient duplicate detection
|
// Use bloom filter for efficient duplicate detection
|
||||||
if messageBloomFilter.contains(messageID) {
|
if messageBloomFilter.contains(messageID) {
|
||||||
// Double check with exact set (bloom filter can have false positives)
|
// Double check with exact set (bloom filter can have false positives)
|
||||||
@@ -2288,8 +2356,6 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
processedMessagesLock.unlock()
|
processedMessagesLock.unlock()
|
||||||
|
|
||||||
// Update last seen sequence number for this peer
|
|
||||||
peerSequenceNumbers[senderID] = max(packet.sequenceNumber, peerSequenceNumbers[senderID] ?? 0)
|
|
||||||
|
|
||||||
// Log statistics periodically
|
// Log statistics periodically
|
||||||
if messageBloomFilter.insertCount % 100 == 0 {
|
if messageBloomFilter.insertCount % 100 == 0 {
|
||||||
@@ -2629,39 +2695,25 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let peripheral = peripheralToUpdate {
|
if let peripheral = peripheralToUpdate {
|
||||||
SecureLogger.log("handleReceivedPacket: Updating peripheral mapping for announce from \(senderID)", category: SecureLogger.session, level: .debug)
|
let peripheralID = peripheral.identifier.uuidString
|
||||||
SecureLogger.log("handleReceivedPacket: Current connectedPeripherals: \(self.connectedPeripherals.keys.joined(separator: ", "))", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("Updating peripheral \(peripheralID) mapping to peer ID \(senderID)",
|
||||||
|
category: SecureLogger.session, level: .info)
|
||||||
|
|
||||||
|
// Update simplified mapping
|
||||||
|
updatePeripheralMapping(peripheralID: peripheralID, peerID: senderID)
|
||||||
|
|
||||||
// Find and remove any temp ID mapping for this peripheral
|
// Find and remove any temp ID mapping for this peripheral
|
||||||
var tempIDToRemove: String? = nil
|
var tempIDToRemove: String? = nil
|
||||||
for (id, per) in self.connectedPeripherals {
|
for (id, per) in self.connectedPeripherals {
|
||||||
if per == peripheral && id != senderID {
|
if per == peripheral && id != senderID && id == peripheralID {
|
||||||
SecureLogger.log("handleReceivedPacket: Found temp ID \(id) for peripheral that announced as \(senderID)", category: SecureLogger.session, level: .debug)
|
|
||||||
tempIDToRemove = id
|
tempIDToRemove = id
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let tempID = tempIDToRemove {
|
if let tempID = tempIDToRemove {
|
||||||
SecureLogger.log("handleReceivedPacket: Transferring from temp ID \(tempID) to real peer ID \(senderID)", category: SecureLogger.session, level: .debug)
|
// RSSI transfer is handled by updatePeripheralMapping
|
||||||
|
|
||||||
// Remove temp mapping
|
|
||||||
self.connectedPeripherals.removeValue(forKey: tempID)
|
|
||||||
// Add real peer ID mapping
|
|
||||||
self.connectedPeripherals[senderID] = peripheral
|
|
||||||
// Update peripheral ID to peer ID mapping
|
|
||||||
self.peerIDByPeripheralID[peripheral.identifier.uuidString] = senderID
|
|
||||||
|
|
||||||
// Transfer RSSI from temp ID to real peer ID
|
|
||||||
if let tempRSSI = self.peripheralRSSI[tempID] {
|
|
||||||
self.peerRSSI[senderID] = tempRSSI
|
|
||||||
self.peripheralRSSI.removeValue(forKey: tempID)
|
self.peripheralRSSI.removeValue(forKey: tempID)
|
||||||
}
|
|
||||||
// Also check if RSSI is stored by peripheral UUID
|
|
||||||
let peripheralUUID = peripheral.identifier.uuidString
|
|
||||||
if let peripheralStoredRSSI = self.peripheralRSSI[peripheralUUID] {
|
|
||||||
self.peerRSSI[senderID] = peripheralStoredRSSI
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: Remove old peer ID from activePeers to prevent duplicates
|
// IMPORTANT: Remove old peer ID from activePeers to prevent duplicates
|
||||||
collectionsQueue.sync(flags: .barrier) {
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
@@ -2782,7 +2834,6 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Just update the peer list
|
// Just update the peer list
|
||||||
self.notifyPeerListUpdate()
|
self.notifyPeerListUpdate()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relay announce if TTL > 0
|
// Relay announce if TTL > 0
|
||||||
@@ -2794,7 +2845,6 @@ class BluetoothMeshService: NSObject {
|
|||||||
let delay = Double.random(in: 0.1...0.3)
|
let delay = Double.random(in: 0.1...0.3)
|
||||||
self.scheduleRelay(relayPacket, messageID: messageID, delay: delay)
|
self.scheduleRelay(relayPacket, messageID: messageID, delay: delay)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case .leave:
|
case .leave:
|
||||||
@@ -2809,6 +2859,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
if wasRemoved {
|
if wasRemoved {
|
||||||
// Mark as gracefully left to prevent duplicate disconnect message
|
// Mark as gracefully left to prevent duplicate disconnect message
|
||||||
self.gracefullyLeftPeers.insert(senderID)
|
self.gracefullyLeftPeers.insert(senderID)
|
||||||
|
self.gracefulLeaveTimestamps[senderID] = Date()
|
||||||
|
|
||||||
SecureLogger.log("📴 Peer left network: \(senderID) (\(nickname)) - marked as gracefully left", category: SecureLogger.session, level: .info)
|
SecureLogger.log("📴 Peer left network: \(senderID) (\(nickname)) - marked as gracefully left", category: SecureLogger.session, level: .info)
|
||||||
}
|
}
|
||||||
@@ -2973,6 +3024,20 @@ class BluetoothMeshService: NSObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special duplicate detection for identity announces
|
||||||
|
processedMessagesLock.lock()
|
||||||
|
if let lastSeenTime = recentIdentityAnnounces[senderID] {
|
||||||
|
let timeSince = Date().timeIntervalSince(lastSeenTime)
|
||||||
|
if timeSince < identityAnnounceDuplicateWindow {
|
||||||
|
processedMessagesLock.unlock()
|
||||||
|
SecureLogger.log("Dropped duplicate identity announce from \(senderID) (last seen \(timeSince)s ago)",
|
||||||
|
category: SecureLogger.security, level: .debug)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recentIdentityAnnounces[senderID] = Date()
|
||||||
|
processedMessagesLock.unlock()
|
||||||
|
|
||||||
if senderID != myPeerID && !isPeerIDOurs(senderID) {
|
if senderID != myPeerID && !isPeerIDOurs(senderID) {
|
||||||
// Create defensive copy and validate
|
// Create defensive copy and validate
|
||||||
let payloadCopy = Data(packet.payload)
|
let payloadCopy = Data(packet.payload)
|
||||||
@@ -3024,6 +3089,27 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
SecureLogger.log("Creating identity binding for \(announcement.peerID) -> \(fingerprint)", category: SecureLogger.security, level: .info)
|
SecureLogger.log("Creating identity binding for \(announcement.peerID) -> \(fingerprint)", category: SecureLogger.security, level: .info)
|
||||||
|
|
||||||
|
// Handle peer ID rotation if previousPeerID is provided
|
||||||
|
if let previousID = announcement.previousPeerID, previousID != announcement.peerID {
|
||||||
|
SecureLogger.log("Peer announced rotation from \(previousID) to \(announcement.peerID)",
|
||||||
|
category: SecureLogger.security, level: .info)
|
||||||
|
|
||||||
|
// Transfer states from previous ID
|
||||||
|
collectionsQueue.sync(flags: .barrier) {
|
||||||
|
// Transfer gracefullyLeft state
|
||||||
|
if self.gracefullyLeftPeers.contains(previousID) {
|
||||||
|
self.gracefullyLeftPeers.remove(previousID)
|
||||||
|
self.gracefullyLeftPeers.insert(announcement.peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the old peer ID from active peers
|
||||||
|
if self.activePeers.contains(previousID) {
|
||||||
|
self.activePeers.remove(previousID)
|
||||||
|
// Don't add new ID yet - let normal flow handle it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update our mappings
|
// Update our mappings
|
||||||
updatePeerBinding(announcement.peerID, fingerprint: fingerprint, binding: binding)
|
updatePeerBinding(announcement.peerID, fingerprint: fingerprint, binding: binding)
|
||||||
|
|
||||||
@@ -3189,7 +3275,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
let senderID = packet.senderID.hexEncodedString()
|
let senderID = packet.senderID.hexEncodedString()
|
||||||
if !isPeerIDOurs(senderID) {
|
if !isPeerIDOurs(senderID) {
|
||||||
_ = packet.recipientID?.hexEncodedString()
|
_ = packet.recipientID?.hexEncodedString()
|
||||||
handleNoiseEncryptedMessage(from: senderID, encryptedData: packet.payload, originalPacket: packet)
|
handleNoiseEncryptedMessage(from: senderID, encryptedData: packet.payload, originalPacket: packet, peripheral: peripheral)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .versionHello:
|
case .versionHello:
|
||||||
@@ -3297,9 +3383,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
timestamp: packet.timestamp, // Use original timestamp
|
timestamp: packet.timestamp, // Use original timestamp
|
||||||
payload: fragmentPayload,
|
payload: fragmentPayload,
|
||||||
signature: nil, // Fragments don't need signatures
|
signature: nil, // Fragments don't need signatures
|
||||||
ttl: packet.ttl,
|
ttl: packet.ttl )
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Send fragments with linear delay
|
// Send fragments with linear delay
|
||||||
let totalDelay = Double(index) * delayBetweenFragments
|
let totalDelay = Double(index) * delayBetweenFragments
|
||||||
@@ -3465,7 +3549,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} // End of BluetoothMeshService class
|
||||||
|
|
||||||
extension BluetoothMeshService: CBCentralManagerDelegate {
|
extension BluetoothMeshService: CBCentralManagerDelegate {
|
||||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||||
@@ -3608,19 +3692,19 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
|
||||||
let tempID = peripheral.identifier.uuidString
|
let peripheralID = peripheral.identifier.uuidString
|
||||||
// Peripheral connected
|
|
||||||
|
|
||||||
// Current mapping state for debugging
|
|
||||||
SecureLogger.log("Current peerIDByPeripheralID mappings:",
|
|
||||||
category: SecureLogger.session, level: .debug)
|
|
||||||
|
|
||||||
peripheral.delegate = self
|
peripheral.delegate = self
|
||||||
peripheral.discoverServices([BluetoothMeshService.serviceUUID])
|
peripheral.discoverServices([BluetoothMeshService.serviceUUID])
|
||||||
|
|
||||||
// Store peripheral by its system ID temporarily until we get the real peer ID
|
// Register peripheral in simplified mapping system
|
||||||
connectedPeripherals[tempID] = peripheral
|
registerPeripheral(peripheral)
|
||||||
SecureLogger.log("didConnect: Stored peripheral with temp ID \(tempID)", category: SecureLogger.session, level: .debug)
|
|
||||||
|
// Store peripheral temporarily until we get the real peer ID
|
||||||
|
connectedPeripherals[peripheralID] = peripheral
|
||||||
|
|
||||||
|
SecureLogger.log("Connected to peripheral \(peripheralID) - awaiting peer ID",
|
||||||
|
category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
// Update connection state to connected (but not authenticated yet)
|
// Update connection state to connected (but not authenticated yet)
|
||||||
// We don't know the real peer ID yet, so we can't update the state
|
// We don't know the real peer ID yet, so we can't update the state
|
||||||
@@ -3659,16 +3743,20 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
|||||||
SecureLogger.log("Peripheral disconnected normally: \(peripheralID)", category: SecureLogger.session, level: .info)
|
SecureLogger.log("Peripheral disconnected normally: \(peripheralID)", category: SecureLogger.session, level: .info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the real peer ID for this peripheral
|
// Find the real peer ID using simplified mapping
|
||||||
var realPeerID: String? = nil
|
var realPeerID: String? = nil
|
||||||
|
|
||||||
// First check our peripheral ID to peer ID mapping
|
if let mapping = peripheralMappings[peripheralID], let peerID = mapping.peerID {
|
||||||
if let peerID = peerIDByPeripheralID[peripheralID] {
|
|
||||||
realPeerID = peerID
|
realPeerID = peerID
|
||||||
SecureLogger.log("Found peer ID \(peerID) from peerIDByPeripheralID for peripheral \(peripheralID)",
|
SecureLogger.log("Found peer ID \(peerID) for peripheral \(peripheralID)",
|
||||||
|
category: SecureLogger.session, level: .debug)
|
||||||
|
} else if let peerID = peerIDByPeripheralID[peripheralID] {
|
||||||
|
// Fallback to legacy mapping
|
||||||
|
realPeerID = peerID
|
||||||
|
SecureLogger.log("Found peer ID \(peerID) from legacy mapping for peripheral \(peripheralID)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("No mapping in peerIDByPeripheralID for peripheral \(peripheralID), checking connectedPeripherals",
|
SecureLogger.log("No peer ID mapping found for peripheral \(peripheralID)",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
|
|
||||||
// Fallback: check if we have a direct mapping from peripheral to peer ID
|
// Fallback: check if we have a direct mapping from peripheral to peer ID
|
||||||
@@ -3738,6 +3826,7 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean up peripheral tracking
|
// Clean up peripheral tracking
|
||||||
|
peripheralMappings.removeValue(forKey: peripheralID)
|
||||||
peerIDByPeripheralID.removeValue(forKey: peripheralID)
|
peerIDByPeripheralID.removeValue(forKey: peripheralID)
|
||||||
lastActivityByPeripheralID.removeValue(forKey: peripheralID)
|
lastActivityByPeripheralID.removeValue(forKey: peripheralID)
|
||||||
|
|
||||||
@@ -3781,6 +3870,7 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
|||||||
} else {
|
} else {
|
||||||
// Peer gracefully left, just clean up the tracking
|
// Peer gracefully left, just clean up the tracking
|
||||||
self.gracefullyLeftPeers.remove(peerID)
|
self.gracefullyLeftPeers.remove(peerID)
|
||||||
|
self.gracefulLeaveTimestamps.removeValue(forKey: peerID)
|
||||||
SecureLogger.log("Cleaning up gracefullyLeftPeers for \(peerID)", category: SecureLogger.session, level: .debug)
|
SecureLogger.log("Cleaning up gracefullyLeftPeers for \(peerID)", category: SecureLogger.session, level: .debug)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3874,9 +3964,7 @@ extension BluetoothMeshService: CBPeripheralDelegate {
|
|||||||
type: MessageType.announce.rawValue,
|
type: MessageType.announce.rawValue,
|
||||||
ttl: 3,
|
ttl: 3,
|
||||||
senderID: self.myPeerID,
|
senderID: self.myPeerID,
|
||||||
payload: Data(vm.nickname.utf8),
|
payload: Data(vm.nickname.utf8) )
|
||||||
sequenceNumber: self.getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
self.broadcastPacket(announcePacket)
|
self.broadcastPacket(announcePacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3891,9 +3979,7 @@ extension BluetoothMeshService: CBPeripheralDelegate {
|
|||||||
type: MessageType.announce.rawValue,
|
type: MessageType.announce.rawValue,
|
||||||
ttl: 3,
|
ttl: 3,
|
||||||
senderID: self.myPeerID,
|
senderID: self.myPeerID,
|
||||||
payload: Data(vm.nickname.utf8),
|
payload: Data(vm.nickname.utf8) )
|
||||||
sequenceNumber: self.getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
if let data = announcePacket.toBinaryData() {
|
if let data = announcePacket.toBinaryData() {
|
||||||
self.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: nil)
|
self.writeToPeripheral(data, peripheral: peripheral, characteristic: characteristic, peerID: nil)
|
||||||
}
|
}
|
||||||
@@ -3981,29 +4067,24 @@ extension BluetoothMeshService: CBPeripheralDelegate {
|
|||||||
// Valid RSSI received, reset retry count
|
// Valid RSSI received, reset retry count
|
||||||
rssiRetryCount.removeValue(forKey: peripheralID)
|
rssiRetryCount.removeValue(forKey: peripheralID)
|
||||||
|
|
||||||
// Store RSSI by peripheral ID for fallback lookup
|
// Update RSSI in simplified mapping
|
||||||
|
updatePeripheralRSSI(peripheralID: peripheralID, rssi: RSSI)
|
||||||
|
|
||||||
|
// Also store in legacy mapping for compatibility
|
||||||
peripheralRSSI[peripheralID] = RSSI
|
peripheralRSSI[peripheralID] = RSSI
|
||||||
|
|
||||||
// Find the peer ID for this peripheral
|
// If we have a peer ID, update peer RSSI (handled in updatePeripheralRSSI)
|
||||||
if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key {
|
if let mapping = peripheralMappings[peripheralID], mapping.isIdentified {
|
||||||
// Handle both temp IDs and real peer IDs
|
// Force UI update when we have a real peer ID
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
self?.notifyPeerListUpdate()
|
||||||
|
}
|
||||||
if peerID.count != 16 {
|
} else {
|
||||||
// It's a temp ID, store RSSI temporarily
|
|
||||||
self.peripheralRSSI[peerID] = RSSI
|
|
||||||
// Keep trying to read RSSI until we get real peer ID
|
// Keep trying to read RSSI until we get real peer ID
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak peripheral] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak peripheral] in
|
||||||
guard let peripheral = peripheral, peripheral.state == .connected else { return }
|
guard let peripheral = peripheral, peripheral.state == .connected else { return }
|
||||||
peripheral.readRSSI()
|
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.notifyPeerListUpdate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Periodically update RSSI
|
// Periodically update RSSI
|
||||||
@@ -4011,8 +4092,6 @@ extension BluetoothMeshService: CBPeripheralDelegate {
|
|||||||
guard let peripheral = peripheral, peripheral.state == .connected else { return }
|
guard let peripheral = peripheral, peripheral.state == .connected else { return }
|
||||||
peripheral.readRSSI()
|
peripheral.readRSSI()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4112,8 +4191,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
if !subscribedCentrals.contains(central) {
|
if !subscribedCentrals.contains(central) {
|
||||||
subscribedCentrals.append(central)
|
subscribedCentrals.append(central)
|
||||||
|
|
||||||
// Send Noise identity announcement to newly connected central
|
// Only send identity announcement if we haven't recently
|
||||||
sendNoiseIdentityAnnounce()
|
// This reduces spam when multiple centrals connect quickly
|
||||||
|
// sendNoiseIdentityAnnounce() will check rate limits internally
|
||||||
|
|
||||||
// Update peer list to show we're connected (even without peer ID yet)
|
// Update peer list to show we're connected (even without peer ID yet)
|
||||||
self.notifyPeerListUpdate()
|
self.notifyPeerListUpdate()
|
||||||
@@ -4581,6 +4661,12 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Clean global timestamps
|
// Clean global timestamps
|
||||||
totalMessageTimestamps.removeAll { $0 < cutoff }
|
totalMessageTimestamps.removeAll { $0 < cutoff }
|
||||||
|
|
||||||
|
// Clean up old identity announce tracking
|
||||||
|
processedMessagesLock.lock()
|
||||||
|
let identityCutoff = Date().addingTimeInterval(-identityAnnounceDuplicateWindow * 2)
|
||||||
|
recentIdentityAnnounces = recentIdentityAnnounces.filter { $0.value > identityCutoff }
|
||||||
|
processedMessagesLock.unlock()
|
||||||
|
|
||||||
// Clean per-peer chat message timestamps
|
// Clean per-peer chat message timestamps
|
||||||
for (peerID, timestamps) in messageRateLimiter {
|
for (peerID, timestamps) in messageRateLimiter {
|
||||||
let filtered = timestamps.filter { $0 >= cutoff }
|
let filtered = timestamps.filter { $0 >= cutoff }
|
||||||
@@ -4753,6 +4839,26 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
initiateNoiseHandshake(with: peerID)
|
initiateNoiseHandshake(with: peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send keep-alive pings to all connected peers to prevent iOS BLE timeouts
|
||||||
|
private func sendKeepAlivePings() {
|
||||||
|
let connectedPeers = collectionsQueue.sync {
|
||||||
|
return Array(activePeers).filter { peerID in
|
||||||
|
// Only send keepalive to authenticated peers
|
||||||
|
return peerConnectionStates[peerID] == .authenticated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for peerID in connectedPeers {
|
||||||
|
// Don't spam if we recently heard from them
|
||||||
|
if let lastHeard = lastHeardFromPeer[peerID],
|
||||||
|
Date().timeIntervalSince(lastHeard) < keepAliveInterval / 2 {
|
||||||
|
continue // Skip if we heard from them in the last 10 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
validateNoiseSession(with: peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate an existing Noise session by sending an encrypted ping
|
// Validate an existing Noise session by sending an encrypted ping
|
||||||
private func validateNoiseSession(with peerID: String) {
|
private func validateNoiseSession(with peerID: String) {
|
||||||
let encryptionQueue = getEncryptionQueue(for: peerID)
|
let encryptionQueue = getEncryptionQueue(for: peerID)
|
||||||
@@ -4775,9 +4881,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encrypted,
|
payload: encrypted,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 1,
|
ttl: 1 )
|
||||||
sequenceNumber: self.getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.broadcastPacket(packet)
|
self.broadcastPacket(packet)
|
||||||
|
|
||||||
@@ -4896,8 +5000,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: handshakeData,
|
payload: handshakeData,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 6, // Increased TTL for better delivery on startup
|
ttl: 6 // Increased TTL for better delivery on startup
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Track packet for ACK
|
// Track packet for ACK
|
||||||
@@ -4970,8 +5073,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: response,
|
payload: response,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 6, // Increased TTL for better delivery on startup
|
ttl: 6 // Increased TTL for better delivery on startup
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Track packet for ACK
|
// Track packet for ACK
|
||||||
@@ -5043,7 +5145,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleNoiseEncryptedMessage(from peerID: String, encryptedData: Data, originalPacket: BitchatPacket) {
|
private func handleNoiseEncryptedMessage(from peerID: String, encryptedData: Data, originalPacket: BitchatPacket, peripheral: CBPeripheral? = nil) {
|
||||||
// Use noiseService directly
|
// Use noiseService directly
|
||||||
|
|
||||||
// For Noise encrypted messages, we need to decrypt first to check the inner packet
|
// For Noise encrypted messages, we need to decrypt first to check the inner packet
|
||||||
@@ -5137,7 +5239,8 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Process the decrypted inner packet
|
// Process the decrypted inner packet
|
||||||
// The packet will be handled according to its recipient ID
|
// The packet will be handled according to its recipient ID
|
||||||
// If it's for us, it won't be relayed
|
// If it's for us, it won't be relayed
|
||||||
handleReceivedPacket(innerPacket, from: peerID)
|
// Pass the peripheral context for proper ACK routing
|
||||||
|
handleReceivedPacket(innerPacket, from: peerID, peripheral: peripheral)
|
||||||
} else {
|
} else {
|
||||||
SecureLogger.log("Failed to parse inner packet from decrypted data", category: SecureLogger.encryption, level: .warning)
|
SecureLogger.log("Failed to parse inner packet from decrypted data", category: SecureLogger.encryption, level: .warning)
|
||||||
}
|
}
|
||||||
@@ -5200,6 +5303,17 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Update last connection time
|
// Update last connection time
|
||||||
lastConnectionTime[peerID] = Date()
|
lastConnectionTime[peerID] = Date()
|
||||||
|
|
||||||
|
// Check if we've already negotiated version with this peer
|
||||||
|
if let existingVersion = negotiatedVersions[peerID] {
|
||||||
|
SecureLogger.log("Already negotiated version \(existingVersion) with \(peerID), skipping re-negotiation",
|
||||||
|
category: SecureLogger.session, level: .debug)
|
||||||
|
// If we have a session, validate it
|
||||||
|
if noiseService.hasEstablishedSession(with: peerID) {
|
||||||
|
validateNoiseSession(with: peerID)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Try JSON first if it looks like JSON
|
// Try JSON first if it looks like JSON
|
||||||
let hello: VersionHello?
|
let hello: VersionHello?
|
||||||
if let firstByte = dataCopy.first, firstByte == 0x7B { // '{' character
|
if let firstByte = dataCopy.first, firstByte == 0x7B { // '{' character
|
||||||
@@ -5344,9 +5458,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
type: MessageType.versionHello.rawValue,
|
type: MessageType.versionHello.rawValue,
|
||||||
ttl: 1, // Version negotiation is direct, no relay
|
ttl: 1, // Version negotiation is direct, no relay
|
||||||
senderID: myPeerID,
|
senderID: myPeerID,
|
||||||
payload: helloData,
|
payload: helloData )
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Mark that we initiated version negotiation
|
// Mark that we initiated version negotiation
|
||||||
// We don't know the peer ID yet from peripheral, so we'll track it when we get the response
|
// We don't know the peer ID yet from peripheral, so we'll track it when we get the response
|
||||||
@@ -5373,8 +5485,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: ackData,
|
payload: ackData,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 1, // Direct response, no relay
|
ttl: 1 // Direct response, no relay
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
broadcastPacket(packet)
|
broadcastPacket(packet)
|
||||||
@@ -5522,8 +5633,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: ack.toBinaryData(),
|
payload: ack.toBinaryData(),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 3, // ACKs don't need to travel far
|
ttl: 3 // ACKs don't need to travel far
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
broadcastPacket(ackPacket)
|
broadcastPacket(ackPacket)
|
||||||
@@ -5549,8 +5659,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: nack.toBinaryData(),
|
payload: nack.toBinaryData(),
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: 3, // NACKs don't need to travel far
|
ttl: 3 // NACKs don't need to travel far
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
broadcastPacket(nackPacket)
|
broadcastPacket(nackPacket)
|
||||||
@@ -5564,9 +5673,10 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
// Create a deterministic ID using SHA256 of immutable fields
|
// Create a deterministic ID using SHA256 of immutable fields
|
||||||
var data = Data()
|
var data = Data()
|
||||||
data.append(packet.senderID)
|
data.append(packet.senderID)
|
||||||
data.append(contentsOf: withUnsafeBytes(of: packet.sequenceNumber) { Array($0) })
|
|
||||||
data.append(contentsOf: withUnsafeBytes(of: packet.timestamp) { Array($0) })
|
data.append(contentsOf: withUnsafeBytes(of: packet.timestamp) { Array($0) })
|
||||||
data.append(packet.type)
|
data.append(packet.type)
|
||||||
|
// Add first 32 bytes of payload for uniqueness
|
||||||
|
data.append(packet.payload.prefix(32))
|
||||||
|
|
||||||
let hash = SHA256.hash(data: data)
|
let hash = SHA256.hash(data: data)
|
||||||
let hashData = Data(hash)
|
let hashData = Data(hash)
|
||||||
@@ -5653,7 +5763,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
retries: pending.retries + 1)
|
retries: pending.retries + 1)
|
||||||
|
|
||||||
// Resend the packet
|
// Resend the packet
|
||||||
SecureLogger.log("Resending packet seq#\(pending.packet.sequenceNumber) due to ACK timeout (retry \(pending.retries + 1))",
|
SecureLogger.log("Resending packet due to ACK timeout (retry \(pending.retries + 1))",
|
||||||
category: SecureLogger.session, level: .debug)
|
category: SecureLogger.session, level: .debug)
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.broadcastPacket(pending.packet)
|
self.broadcastPacket(pending.packet)
|
||||||
@@ -5722,9 +5832,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
let hasConnection = connectionState == .connected || connectionState == .authenticating || connectionState == .authenticated
|
let hasConnection = connectionState == .connected || connectionState == .authenticating || connectionState == .authenticated
|
||||||
|
|
||||||
// Peer is available if:
|
// Peer is available if:
|
||||||
// 1. We have an active connection AND heard from them recently, OR
|
// 1. We have an active connection (regardless of last heard time), OR
|
||||||
// 2. We're authenticated and heard from them within timeout period
|
// 2. We're authenticated and heard from them within timeout period
|
||||||
let isAvailable = (hasConnection && timeSinceLastHeard < 60.0) ||
|
let isAvailable = hasConnection ||
|
||||||
(connectionState == .authenticated && timeSinceLastHeard < peerAvailabilityTimeout)
|
(connectionState == .authenticated && timeSinceLastHeard < peerAvailabilityTimeout)
|
||||||
|
|
||||||
if wasAvailable != isAvailable {
|
if wasAvailable != isAvailable {
|
||||||
@@ -5834,9 +5944,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: announcementData,
|
payload: announcementData,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: adaptiveTTL,
|
ttl: adaptiveTTL )
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
if let targetPeer = specificPeerID {
|
if let targetPeer = specificPeerID {
|
||||||
SecureLogger.log("Sending targeted identity announce to \(targetPeer)",
|
SecureLogger.log("Sending targeted identity announce to \(targetPeer)",
|
||||||
@@ -5958,8 +6066,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: messageData,
|
payload: messageData,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: self.adaptiveTTL, // Inner packet needs valid TTL for processing after decryption
|
ttl: self.adaptiveTTL // Inner packet needs valid TTL for processing after decryption
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
guard let innerData = innerPacket.toBinaryData() else { return }
|
guard let innerData = innerPacket.toBinaryData() else { return }
|
||||||
@@ -5981,9 +6088,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
|||||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
payload: encryptedData,
|
payload: encryptedData,
|
||||||
signature: nil,
|
signature: nil,
|
||||||
ttl: adaptiveTTL,
|
ttl: adaptiveTTL )
|
||||||
sequenceNumber: getNextSequenceNumber()
|
|
||||||
)
|
|
||||||
|
|
||||||
SecureLogger.log("Broadcasting encrypted private message \(msgID) to \(recipientPeerID)", category: SecureLogger.session, level: .info)
|
SecureLogger.log("Broadcasting encrypted private message \(msgID) to \(recipientPeerID)", category: SecureLogger.session, level: .info)
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,14 @@ class ChatViewModel: ObservableObject {
|
|||||||
self?.updateMessageDeliveryStatus(messageID, status: status)
|
self?.updateMessageDeliveryStatus(messageID, status: status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for retry notifications
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleRetryMessage),
|
||||||
|
name: Notification.Name("bitchat.retryMessage"),
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
// When app becomes active, send read receipts for visible messages
|
// When app becomes active, send read receipts for visible messages
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
@@ -549,6 +557,30 @@ class ChatViewModel: ObservableObject {
|
|||||||
selectedPrivateChatFingerprint = nil
|
selectedPrivateChatFingerprint = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func handleRetryMessage(_ notification: Notification) {
|
||||||
|
guard let messageID = notification.userInfo?["messageID"] as? String else { return }
|
||||||
|
|
||||||
|
// Find the message to retry
|
||||||
|
if let message = messages.first(where: { $0.id == messageID }) {
|
||||||
|
SecureLogger.log("Retrying message \(messageID) to \(message.recipientNickname ?? "unknown")",
|
||||||
|
category: SecureLogger.session, level: .info)
|
||||||
|
|
||||||
|
// Resend the message through mesh service
|
||||||
|
if message.isPrivate,
|
||||||
|
let peerID = getPeerIDForNickname(message.recipientNickname ?? "") {
|
||||||
|
// Update status to sending
|
||||||
|
updateMessageDeliveryStatus(messageID, status: .sending)
|
||||||
|
|
||||||
|
// Resend via mesh service
|
||||||
|
meshService.sendMessage(message.content,
|
||||||
|
mentions: message.mentions ?? [],
|
||||||
|
to: peerID,
|
||||||
|
messageID: messageID,
|
||||||
|
timestamp: message.timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func appDidBecomeActive() {
|
@objc private func appDidBecomeActive() {
|
||||||
// When app becomes active, send read receipts for visible private chat
|
// When app becomes active, send read receipts for visible private chat
|
||||||
if let peerID = selectedPrivateChatPeer {
|
if let peerID = selectedPrivateChatPeer {
|
||||||
|
|||||||
@@ -23,17 +23,17 @@ struct AppInfoView: View {
|
|||||||
|
|
||||||
enum Features {
|
enum Features {
|
||||||
static let title = "FEATURES"
|
static let title = "FEATURES"
|
||||||
static let offlineComm = ("wifi.slash", "offline communication", "works without internet using Bluetooth mesh networking")
|
static let offlineComm = ("wifi.slash", "offline communication", "works without internet using Bluetooth low energy")
|
||||||
static let encryption = ("lock.shield", "end-to-end encryption", "private messages encrypted with noise protocol")
|
static let encryption = ("lock.shield", "end-to-end encryption", "private messages encrypted with noise protocol")
|
||||||
static let extendedRange = ("antenna.radiowaves.left.and.right", "extended range", "messages relay through peers, increasing the distance")
|
static let extendedRange = ("antenna.radiowaves.left.and.right", "extended range", "messages relay through peers, going the distance")
|
||||||
static let favorites = ("star.fill", "favorites", "store-and-forward messages for favorite people")
|
static let favorites = ("star.fill", "favorites", "get notified when your favorite people join")
|
||||||
static let mentions = ("at", "mentions", "use @nickname to notify specific people")
|
static let mentions = ("at", "mentions", "use @nickname to notify specific people")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Privacy {
|
enum Privacy {
|
||||||
static let title = "PRIVACY"
|
static let title = "PRIVACY"
|
||||||
static let noTracking = ("eye.slash", "no tracking", "no servers, accounts, or data collection")
|
static let noTracking = ("eye.slash", "no tracking", "no servers, accounts, or data collection")
|
||||||
static let ephemeral = ("shuffle", "ephemeral identity", "new peer ID generated each session")
|
static let ephemeral = ("shuffle", "ephemeral identity", "new peer ID generated regularly")
|
||||||
static let panic = ("hand.raised.fill", "panic mode", "triple-tap logo to instantly clear all data")
|
static let panic = ("hand.raised.fill", "panic mode", "triple-tap logo to instantly clear all data")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +44,7 @@ struct AppInfoView: View {
|
|||||||
"• swipe left for sidebar",
|
"• swipe left for sidebar",
|
||||||
"• tap a peer to start a private chat",
|
"• tap a peer to start a private chat",
|
||||||
"• use @nickname to mention someone",
|
"• use @nickname to mention someone",
|
||||||
"• triple-tap \"bitchat\" for panic mode",
|
"• triple-tap chat to clear"
|
||||||
"• triple-tap chat messages to clear current chat"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user