mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 00:45:21 +00:00
Fix Noise handshake stability and session synchronization issues
- Add peer presence tracking with lastHeardFromPeer to detect reconnections - Automatically send identity announcement when detecting peer reconnection after 30s - Clear stale sessions when receiving handshake from recently-seen peer (likely restart) - Add peers to activePeers when successfully decrypting their messages - Fix thread safety in handshake coordinator with concurrent collections - Extend message delivery timeouts (30s→120s private, 60s→180s room, 300s→600s favorite) - Add per-peer encryption queues to prevent nonce desynchronization - Make NoiseSession encrypt/decrypt operations thread-safe with barrier flag - Initialize lastSuccessfulMessageTime when handshake completes - Send identity announcement when decryption fails to prompt session reset - Improve handshake state logging and debugging This fixes: - Peers stuck in "establishing encryption" after restart - Nonce desynchronization causing "Decryption failed at nonce N" - Asymmetric peer visibility (one peer sees the other but not vice versa) - Thread safety issues causing crashes with pendingPrivateMessages - Sessions marked as stale immediately after establishment
This commit is contained in:
@@ -17,7 +17,7 @@ class NoiseHandshakeCoordinator {
|
||||
case idle
|
||||
case waitingToInitiate(since: Date)
|
||||
case initiating(attempt: Int, lastAttempt: Date)
|
||||
case responding
|
||||
case responding(since: Date)
|
||||
case waitingForResponse(messagesSent: [Data], timeout: Date)
|
||||
case established(since: Date)
|
||||
case failed(reason: String, canRetry: Bool, lastAttempt: Date)
|
||||
@@ -101,7 +101,7 @@ class NoiseHandshakeCoordinator {
|
||||
/// Record that we're responding to a handshake
|
||||
func recordHandshakeResponse(peerID: String) {
|
||||
handshakeQueue.async(flags: .barrier) {
|
||||
self.handshakeStates[peerID] = .responding
|
||||
self.handshakeStates[peerID] = .responding(since: Date())
|
||||
SecureLogger.log("Recording handshake response to \(peerID)",
|
||||
category: SecureLogger.handshake, level: .info)
|
||||
}
|
||||
@@ -206,6 +206,48 @@ class NoiseHandshakeCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up stale handshake states
|
||||
func cleanupStaleHandshakes(staleTimeout: TimeInterval = 30.0) -> [String] {
|
||||
return handshakeQueue.sync {
|
||||
let now = Date()
|
||||
var stalePeerIDs: [String] = []
|
||||
|
||||
for (peerID, state) in handshakeStates {
|
||||
var isStale = false
|
||||
|
||||
switch state {
|
||||
case .initiating(_, let lastAttempt):
|
||||
if now.timeIntervalSince(lastAttempt) > staleTimeout {
|
||||
isStale = true
|
||||
}
|
||||
case .responding(let since):
|
||||
if now.timeIntervalSince(since) > staleTimeout {
|
||||
isStale = true
|
||||
}
|
||||
case .waitingForResponse(_, let timeout):
|
||||
if now > timeout {
|
||||
isStale = true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if isStale {
|
||||
stalePeerIDs.append(peerID)
|
||||
SecureLogger.log("Found stale handshake state for \(peerID): \(state)",
|
||||
category: SecureLogger.handshake, level: .warning)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up stale states
|
||||
for peerID in stalePeerIDs {
|
||||
handshakeStates.removeValue(forKey: peerID)
|
||||
}
|
||||
|
||||
return stalePeerIDs
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current handshake state
|
||||
func getHandshakeState(for peerID: String) -> HandshakeState {
|
||||
return handshakeQueue.sync {
|
||||
@@ -240,8 +282,8 @@ class NoiseHandshakeCoordinator {
|
||||
stateDesc = "waiting to initiate (since \(since))"
|
||||
case .initiating(let attempt, let lastAttempt):
|
||||
stateDesc = "initiating (attempt \(attempt), last: \(lastAttempt))"
|
||||
case .responding:
|
||||
stateDesc = "responding"
|
||||
case .responding(let since):
|
||||
stateDesc = "responding (since: \(since))"
|
||||
case .waitingForResponse(let messages, let timeout):
|
||||
stateDesc = "waiting for response (\(messages.count) messages, timeout: \(timeout))"
|
||||
case .established(let since):
|
||||
|
||||
@@ -299,6 +299,7 @@ class NoiseHandshakeState {
|
||||
throw NoiseError.handshakeComplete
|
||||
}
|
||||
|
||||
SecureLogger.log("NoiseHandshake[\(role)]: Writing message \(currentPattern + 1)/\(messagePatterns.count)", category: SecureLogger.noise, level: .info)
|
||||
|
||||
var messageBuffer = Data()
|
||||
let patterns = messagePatterns[currentPattern]
|
||||
@@ -390,6 +391,7 @@ class NoiseHandshakeState {
|
||||
throw NoiseError.handshakeComplete
|
||||
}
|
||||
|
||||
SecureLogger.log("NoiseHandshake[\(role)]: Reading message \(currentPattern + 1)/\(messagePatterns.count)", category: SecureLogger.noise, level: .info)
|
||||
|
||||
var buffer = message
|
||||
let patterns = messagePatterns[currentPattern]
|
||||
|
||||
@@ -92,6 +92,7 @@ class NoiseSession {
|
||||
|
||||
func processHandshakeMessage(_ message: Data) throws -> Data? {
|
||||
return try sessionQueue.sync(flags: .barrier) {
|
||||
SecureLogger.log("NoiseSession[\(peerID)]: Processing handshake message, current state: \(state), role: \(role)", category: SecureLogger.noise, level: .info)
|
||||
|
||||
// Initialize handshake state if needed (for responders)
|
||||
if state == .uninitialized && role == .responder {
|
||||
@@ -102,6 +103,7 @@ class NoiseSession {
|
||||
remoteStaticKey: nil
|
||||
)
|
||||
state = .handshaking
|
||||
SecureLogger.log("NoiseSession[\(peerID)]: Initialized handshake state for responder", category: SecureLogger.noise, level: .info)
|
||||
}
|
||||
|
||||
guard case .handshaking = state, let handshake = handshakeState else {
|
||||
@@ -110,6 +112,7 @@ class NoiseSession {
|
||||
|
||||
// Process incoming message
|
||||
_ = try handshake.readMessage(message)
|
||||
SecureLogger.log("NoiseSession[\(peerID)]: Read handshake message, checking if complete", category: SecureLogger.noise, level: .info)
|
||||
|
||||
// Check if handshake is complete
|
||||
if handshake.isHandshakeComplete() {
|
||||
@@ -127,6 +130,7 @@ class NoiseSession {
|
||||
state = .established
|
||||
handshakeState = nil // Clear handshake state
|
||||
|
||||
SecureLogger.log("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established", category: SecureLogger.noise, level: .info)
|
||||
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
||||
|
||||
return nil
|
||||
@@ -134,6 +138,7 @@ class NoiseSession {
|
||||
// Generate response
|
||||
let response = try handshake.writeMessage()
|
||||
sentHandshakeMessages.append(response)
|
||||
SecureLogger.log("NoiseSession[\(peerID)]: Generated handshake response of size \(response.count)", category: SecureLogger.noise, level: .info)
|
||||
|
||||
// Check if handshake is complete after writing
|
||||
if handshake.isHandshakeComplete() {
|
||||
@@ -151,6 +156,7 @@ class NoiseSession {
|
||||
state = .established
|
||||
handshakeState = nil // Clear handshake state
|
||||
|
||||
SecureLogger.log("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established", category: SecureLogger.noise, level: .info)
|
||||
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
||||
}
|
||||
|
||||
@@ -162,7 +168,7 @@ class NoiseSession {
|
||||
// MARK: - Transport
|
||||
|
||||
func encrypt(_ plaintext: Data) throws -> Data {
|
||||
return try sessionQueue.sync {
|
||||
return try sessionQueue.sync(flags: .barrier) {
|
||||
guard case .established = state, let cipher = sendCipher else {
|
||||
throw NoiseSessionError.notEstablished
|
||||
}
|
||||
@@ -172,7 +178,7 @@ class NoiseSession {
|
||||
}
|
||||
|
||||
func decrypt(_ ciphertext: Data) throws -> Data {
|
||||
return try sessionQueue.sync {
|
||||
return try sessionQueue.sync(flags: .barrier) {
|
||||
guard case .established = state, let cipher = receiveCipher else {
|
||||
throw NoiseSessionError.notEstablished
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ class BluetoothMeshService: NSObject {
|
||||
private var discoveredPeripherals: [CBPeripheral] = []
|
||||
private var connectedPeripherals: [String: CBPeripheral] = [:]
|
||||
private var peripheralCharacteristics: [CBPeripheral: CBCharacteristic] = [:]
|
||||
private var lastConnectionTime: [String: Date] = [:] // Track when peers last connected
|
||||
private var lastSuccessfulMessageTime: [String: Date] = [:] // Track last successful message exchange
|
||||
private var lastHeardFromPeer: [String: Date] = [:] // Track last time we received ANY packet from peer
|
||||
private var characteristic: CBMutableCharacteristic?
|
||||
private var subscribedCentrals: [CBCentral] = []
|
||||
// Thread-safe collections using concurrent queues
|
||||
@@ -55,6 +58,10 @@ class BluetoothMeshService: NSObject {
|
||||
private var peerRSSI: [String: NSNumber] = [:] // Track RSSI values for peers
|
||||
private var peripheralRSSI: [String: NSNumber] = [:] // Track RSSI by peripheral ID during discovery
|
||||
|
||||
// Per-peer encryption queues to prevent nonce desynchronization
|
||||
private var peerEncryptionQueues: [String: DispatchQueue] = [:]
|
||||
private let encryptionQueuesLock = NSLock()
|
||||
|
||||
// MARK: - Peer Identity Rotation
|
||||
// Mappings between ephemeral peer IDs and permanent fingerprints
|
||||
private var peerIDToFingerprint: [String: String] = [:] // PeerID -> Fingerprint
|
||||
@@ -333,6 +340,12 @@ class BluetoothMeshService: NSObject {
|
||||
self.peerRSSI.removeValue(forKey: existingPeerID)
|
||||
self.peerRSSI[newPeerID] = rssi
|
||||
}
|
||||
|
||||
// Transfer lastHeardFromPeer tracking
|
||||
if let lastHeard = self.lastHeardFromPeer[existingPeerID] {
|
||||
self.lastHeardFromPeer.removeValue(forKey: existingPeerID)
|
||||
self.lastHeardFromPeer[newPeerID] = lastHeard
|
||||
}
|
||||
}
|
||||
|
||||
// Add new mapping
|
||||
@@ -347,10 +360,7 @@ class BluetoothMeshService: NSObject {
|
||||
if let oldID = oldPeerID {
|
||||
// Clear the old session instead of migrating it
|
||||
// This ensures both peers do a fresh handshake after ID rotation
|
||||
self.noiseService.removePeer(oldID)
|
||||
|
||||
// Reset handshake state for both old and new peer IDs
|
||||
self.handshakeCoordinator.resetHandshakeState(for: oldID)
|
||||
self.cleanupPeerCryptoState(oldID)
|
||||
self.handshakeCoordinator.resetHandshakeState(for: newPeerID)
|
||||
|
||||
// Log the peer ID rotation
|
||||
@@ -534,10 +544,22 @@ class BluetoothMeshService: NSObject {
|
||||
self?.cleanupStalePeers()
|
||||
}
|
||||
|
||||
// Log handshake states periodically for debugging
|
||||
// Log handshake states periodically for debugging and clean up stale states
|
||||
#if DEBUG
|
||||
Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in
|
||||
self?.handshakeCoordinator.logHandshakeStates()
|
||||
guard let self = self else { return }
|
||||
|
||||
// Clean up stale handshakes
|
||||
let stalePeerIDs = self.handshakeCoordinator.cleanupStaleHandshakes()
|
||||
if !stalePeerIDs.isEmpty {
|
||||
for peerID in stalePeerIDs {
|
||||
// Also remove from noise service
|
||||
self.cleanupPeerCryptoState(peerID)
|
||||
SecureLogger.log("Cleaned up stale handshake for \(peerID)", category: SecureLogger.handshake, level: .info)
|
||||
}
|
||||
}
|
||||
|
||||
self.handshakeCoordinator.logHandshakeStates()
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -552,8 +574,8 @@ class BluetoothMeshService: NSObject {
|
||||
DispatchQueue.main.async {
|
||||
(self?.delegate as? ChatViewModel)?.registerPeerPublicKey(peerID: peerID, publicKeyData: publicKeyData)
|
||||
|
||||
// Force UI to update encryption status after authentication
|
||||
(self?.delegate as? ChatViewModel)?.updateEncryptionStatusForPeers()
|
||||
// Force UI to update encryption status for this specific peer
|
||||
(self?.delegate as? ChatViewModel)?.updateEncryptionStatusForPeer(peerID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,6 +659,14 @@ class BluetoothMeshService: NSObject {
|
||||
|
||||
// Clear last seen timestamps
|
||||
peerLastSeenTimestamps.removeAll()
|
||||
|
||||
// Clear all encryption queues
|
||||
encryptionQueuesLock.lock()
|
||||
peerEncryptionQueues.removeAll()
|
||||
encryptionQueuesLock.unlock()
|
||||
|
||||
// Clear peer tracking
|
||||
lastHeardFromPeer.removeAll()
|
||||
}
|
||||
|
||||
func startServices() {
|
||||
@@ -915,7 +945,10 @@ class BluetoothMeshService: NSObject {
|
||||
|
||||
|
||||
func sendDeliveryAck(_ ack: DeliveryAck, to recipientID: String) {
|
||||
messageQueue.async { [weak self] in
|
||||
// Use per-peer encryption queue to prevent nonce desynchronization
|
||||
let encryptionQueue = getEncryptionQueue(for: recipientID)
|
||||
|
||||
encryptionQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Encode the ACK
|
||||
@@ -977,8 +1010,38 @@ class BluetoothMeshService: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func getEncryptionQueue(for peerID: String) -> DispatchQueue {
|
||||
encryptionQueuesLock.lock()
|
||||
defer { encryptionQueuesLock.unlock() }
|
||||
|
||||
if let queue = peerEncryptionQueues[peerID] {
|
||||
return queue
|
||||
}
|
||||
|
||||
let queue = DispatchQueue(label: "bitchat.encryption.\(peerID)", qos: .userInitiated)
|
||||
peerEncryptionQueues[peerID] = queue
|
||||
return queue
|
||||
}
|
||||
|
||||
private func removeEncryptionQueue(for peerID: String) {
|
||||
encryptionQueuesLock.lock()
|
||||
defer { encryptionQueuesLock.unlock() }
|
||||
|
||||
peerEncryptionQueues.removeValue(forKey: peerID)
|
||||
}
|
||||
|
||||
// Centralized cleanup for peer crypto state
|
||||
private func cleanupPeerCryptoState(_ peerID: String) {
|
||||
noiseService.removePeer(peerID)
|
||||
handshakeCoordinator.resetHandshakeState(for: peerID)
|
||||
removeEncryptionQueue(for: peerID)
|
||||
}
|
||||
|
||||
func sendReadReceipt(_ receipt: ReadReceipt, to recipientID: String) {
|
||||
messageQueue.async { [weak self] in
|
||||
// Use per-peer encryption queue to prevent nonce desynchronization
|
||||
let encryptionQueue = getEncryptionQueue(for: recipientID)
|
||||
|
||||
encryptionQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Encode the receipt
|
||||
@@ -1015,33 +1078,38 @@ class BluetoothMeshService: NSObject {
|
||||
ttl: 3
|
||||
)
|
||||
|
||||
SecureLogger.log("Sending encrypted read receipt for message \(receipt.originalMessageID) to \(recipientID)", category: SecureLogger.noise, level: .info)
|
||||
self.broadcastPacket(outerPacket)
|
||||
}
|
||||
} catch {
|
||||
SecureLogger.logError(error, context: "Failed to encrypt read receipt via Noise for \(recipientID)", category: SecureLogger.encryption)
|
||||
}
|
||||
} else {
|
||||
// Fall back to legacy encryption
|
||||
let encryptedPayload: Data
|
||||
do {
|
||||
encryptedPayload = try self.noiseService.encrypt(receiptData, for: recipientID)
|
||||
} catch {
|
||||
return
|
||||
// No session - initiate handshake and queue the read receipt
|
||||
SecureLogger.log("No Noise session with \(recipientID) for read receipt, initiating handshake", category: SecureLogger.noise, level: .info)
|
||||
|
||||
// Initiate handshake regardless of our role if we need to send data
|
||||
self.initiateNoiseHandshake(with: recipientID)
|
||||
|
||||
// Queue the read receipt as a pending message
|
||||
// Create a synthetic message ID for the read receipt
|
||||
let readReceiptMessageID = "READ_RECEIPT_\(receipt.originalMessageID)"
|
||||
|
||||
collectionsQueue.sync(flags: .barrier) {
|
||||
if self.pendingPrivateMessages[recipientID] == nil {
|
||||
self.pendingPrivateMessages[recipientID] = []
|
||||
}
|
||||
|
||||
// Store the read receipt data as a pending "message"
|
||||
self.pendingPrivateMessages[recipientID]?.append((
|
||||
content: "READ_RECEIPT:\(receipt.originalMessageID)",
|
||||
recipientNickname: receipt.readerNickname,
|
||||
messageID: readReceiptMessageID
|
||||
))
|
||||
|
||||
let count = self.pendingPrivateMessages[recipientID]?.count ?? 0
|
||||
SecureLogger.log("Queued read receipt for \(recipientID), pending messages: \(count)", category: SecureLogger.noise, level: .info)
|
||||
}
|
||||
|
||||
// Create read receipt packet with direct routing to original sender
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.readReceipt.rawValue,
|
||||
senderID: Data(hexString: self.myPeerID) ?? Data(),
|
||||
recipientID: Data(hexString: recipientID) ?? Data(),
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: encryptedPayload,
|
||||
signature: nil, // Read receipts don't need signatures
|
||||
ttl: 3 // Limited TTL for receipts
|
||||
)
|
||||
|
||||
// Send immediately without delay
|
||||
self.broadcastPacket(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1154,8 +1222,16 @@ class BluetoothMeshService: NSObject {
|
||||
lastNetworkNotificationTime = nil
|
||||
processedMessages.removeAll()
|
||||
incomingFragments.removeAll()
|
||||
|
||||
// Clear all encryption queues
|
||||
encryptionQueuesLock.lock()
|
||||
peerEncryptionQueues.removeAll()
|
||||
encryptionQueuesLock.unlock()
|
||||
fragmentMetadata.removeAll()
|
||||
|
||||
// Clear peer tracking
|
||||
lastHeardFromPeer.removeAll()
|
||||
|
||||
// Clear persistent identity
|
||||
noiseService.clearPersistentIdentity()
|
||||
|
||||
@@ -1253,6 +1329,7 @@ class BluetoothMeshService: NSObject {
|
||||
announcedPeers.remove(peerID)
|
||||
announcedToPeers.remove(peerID)
|
||||
peerNicknames.removeValue(forKey: peerID)
|
||||
lastHeardFromPeer.removeValue(forKey: peerID)
|
||||
|
||||
actuallyRemoved.append(peerID)
|
||||
// Removed stale peer
|
||||
@@ -1582,6 +1659,30 @@ class BluetoothMeshService: NSObject {
|
||||
messageQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Track that we heard from this peer
|
||||
let senderID = packet.senderID.hexEncodedString()
|
||||
if !senderID.isEmpty && senderID != self.myPeerID {
|
||||
// Check if this is a reconnection after a long silence
|
||||
let wasReconnection: Bool
|
||||
if let lastHeard = self.lastHeardFromPeer[senderID] {
|
||||
let timeSinceLastHeard = Date().timeIntervalSince(lastHeard)
|
||||
wasReconnection = timeSinceLastHeard > 30.0
|
||||
} else {
|
||||
// First time hearing from this peer
|
||||
wasReconnection = true
|
||||
}
|
||||
|
||||
self.lastHeardFromPeer[senderID] = Date()
|
||||
|
||||
// If this is a reconnection, send our identity announcement
|
||||
if wasReconnection && packet.type != MessageType.noiseIdentityAnnounce.rawValue {
|
||||
SecureLogger.log("Detected reconnection from \(senderID) after silence, sending identity announcement", category: SecureLogger.noise, level: .info)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.sendNoiseIdentityAnnounce(to: senderID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Log specific Noise packet types
|
||||
|
||||
@@ -1595,7 +1696,6 @@ class BluetoothMeshService: NSObject {
|
||||
}
|
||||
|
||||
// Update last seen timestamp for this peer
|
||||
let senderID = packet.senderID.hexEncodedString()
|
||||
if senderID != "unknown" && senderID != self.myPeerID {
|
||||
peerLastSeenTimestamps.set(senderID, value: Date())
|
||||
}
|
||||
@@ -1881,6 +1981,9 @@ class BluetoothMeshService: NSObject {
|
||||
self.announcedPeers.remove(stalePeerID)
|
||||
self.announcedToPeers.remove(stalePeerID)
|
||||
|
||||
// Clear tracking data
|
||||
self.lastHeardFromPeer.removeValue(forKey: stalePeerID)
|
||||
|
||||
// Disconnect any peripherals associated with stale ID
|
||||
if let peripheral = self.connectedPeripherals[stalePeerID] {
|
||||
self.intentionalDisconnects.insert(peripheral.identifier.uuidString)
|
||||
@@ -2163,14 +2266,17 @@ class BluetoothMeshService: NSObject {
|
||||
isPeerIDOurs(recipientIDData.hexEncodedString()) {
|
||||
// This read receipt is for us
|
||||
let senderID = packet.senderID.hexEncodedString()
|
||||
SecureLogger.log("Received read receipt from \(senderID)", category: SecureLogger.session, level: .info)
|
||||
// Check if payload is already decrypted (came through Noise)
|
||||
if let receipt = ReadReceipt.fromBinaryData(packet.payload) {
|
||||
// Already decrypted - process directly
|
||||
SecureLogger.log("Processing read receipt for message \(receipt.originalMessageID) from \(receipt.readerID)", category: SecureLogger.session, level: .info)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.didReceiveReadReceipt(receipt)
|
||||
}
|
||||
} else if let receipt = ReadReceipt.decode(from: packet.payload) {
|
||||
// Fallback to JSON for backward compatibility
|
||||
SecureLogger.log("Processing read receipt (JSON) for message \(receipt.originalMessageID) from \(receipt.readerID)", category: SecureLogger.session, level: .info)
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.didReceiveReadReceipt(receipt)
|
||||
}
|
||||
@@ -2266,7 +2372,19 @@ class BluetoothMeshService: NSObject {
|
||||
// Use lexicographic comparison as tie-breaker to prevent simultaneous handshakes
|
||||
// Only the peer with the "lower" ID initiates
|
||||
if myPeerID < announcement.peerID {
|
||||
initiateNoiseHandshake(with: announcement.peerID)
|
||||
// Add small delay on fresh startup to let connections stabilize
|
||||
let lastConnection = lastConnectionTime[announcement.peerID] ?? Date.distantPast
|
||||
let timeSinceConnection = Date().timeIntervalSince(lastConnection)
|
||||
|
||||
if timeSinceConnection > 60.0 { // Fresh connection
|
||||
// Delay handshake initiation slightly for connection stability
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.initiateNoiseHandshake(with: announcement.peerID)
|
||||
}
|
||||
} else {
|
||||
// Quick reconnection, initiate immediately
|
||||
initiateNoiseHandshake(with: announcement.peerID)
|
||||
}
|
||||
} else {
|
||||
// Send our identity back so they know we're ready
|
||||
sendNoiseIdentityAnnounce(to: announcement.peerID)
|
||||
@@ -2300,10 +2418,37 @@ class BluetoothMeshService: NSObject {
|
||||
return
|
||||
}
|
||||
if !isPeerIDOurs(senderID) {
|
||||
// Check if we already have a session (established or handshaking)
|
||||
if noiseService.hasSession(with: senderID) {
|
||||
SecureLogger.log("Received handshake init from \(senderID) but already have session/handshaking - ignoring duplicate", category: SecureLogger.noise, level: .warning)
|
||||
return
|
||||
// Check if we already have an established session
|
||||
if noiseService.hasEstablishedSession(with: senderID) {
|
||||
// Determine who should be initiator based on peer ID comparison
|
||||
let shouldBeInitiator = myPeerID < senderID
|
||||
|
||||
if shouldBeInitiator {
|
||||
// We should be initiator but peer is initiating - likely they had a session failure
|
||||
SecureLogger.log("Received handshake init from \(senderID) who should be responder - likely session mismatch, clearing and accepting", category: SecureLogger.noise, level: .warning)
|
||||
cleanupPeerCryptoState(senderID)
|
||||
} else {
|
||||
// Check if we've heard from this peer recently
|
||||
let lastHeard = lastHeardFromPeer[senderID] ?? Date.distantPast
|
||||
let timeSinceLastHeard = Date().timeIntervalSince(lastHeard)
|
||||
|
||||
// If we haven't heard from the peer in 30 seconds, they likely disconnected and reconnected
|
||||
if timeSinceLastHeard > 30.0 {
|
||||
SecureLogger.log("Received handshake init from \(senderID) after \(Int(timeSinceLastHeard))s silence - likely reconnected, clearing old session", category: SecureLogger.noise, level: .info)
|
||||
cleanupPeerCryptoState(senderID)
|
||||
} else {
|
||||
// We've heard from them recently but they're initiating a new handshake
|
||||
// This likely means they restarted and lost their session
|
||||
SecureLogger.log("Received handshake init from \(senderID) despite recent communication - peer likely restarted, clearing old session", category: SecureLogger.noise, level: .info)
|
||||
cleanupPeerCryptoState(senderID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a handshaking session, reset it to allow new handshake
|
||||
if noiseService.hasSession(with: senderID) && !noiseService.hasEstablishedSession(with: senderID) {
|
||||
SecureLogger.log("Received handshake init from \(senderID) while already handshaking - resetting to allow new handshake", category: SecureLogger.noise, level: .info)
|
||||
cleanupPeerCryptoState(senderID)
|
||||
}
|
||||
|
||||
// Check if we've completed version negotiation with this peer
|
||||
@@ -2339,7 +2484,11 @@ class BluetoothMeshService: NSObject {
|
||||
}
|
||||
|
||||
if !isPeerIDOurs(senderID) {
|
||||
SecureLogger.log("Processing handshake response from \(senderID)", category: SecureLogger.noise, level: .info)
|
||||
// Check our current handshake state
|
||||
let currentState = handshakeCoordinator.getHandshakeState(for: senderID)
|
||||
SecureLogger.log("Processing handshake response from \(senderID), current state: \(currentState)", category: SecureLogger.noise, level: .info)
|
||||
|
||||
// Process the response - this could be message 2 or message 3 in the XX pattern
|
||||
handleNoiseHandshakeMessage(from: senderID, message: packet.payload, isInitiation: false)
|
||||
}
|
||||
|
||||
@@ -2768,6 +2917,19 @@ extension BluetoothMeshService: CBCentralManagerDelegate {
|
||||
connectedPeripherals.removeValue(forKey: peerID)
|
||||
peripheralCharacteristics.removeValue(forKey: peripheral)
|
||||
|
||||
// Don't clear Noise session on disconnect - sessions should survive disconnects
|
||||
// The Noise protocol is designed to maintain sessions across network interruptions
|
||||
// Only clear sessions on authentication failure
|
||||
if peerID.count == 16 { // Real peer ID
|
||||
// Clear connection time and last heard tracking on disconnect to properly detect stale sessions
|
||||
lastConnectionTime.removeValue(forKey: peerID)
|
||||
lastHeardFromPeer.removeValue(forKey: peerID)
|
||||
// Keep lastSuccessfulMessageTime to validate session on reconnect
|
||||
let lastSuccess = lastSuccessfulMessageTime[peerID] ?? Date.distantPast
|
||||
let sessionAge = Date().timeIntervalSince(lastSuccess)
|
||||
SecureLogger.log("Peer disconnected: \(peerID), keeping Noise session (age: \(Int(sessionAge))s)", category: SecureLogger.noise, level: .info)
|
||||
}
|
||||
|
||||
// Only remove from active peers if it's not a temp ID
|
||||
// Temp IDs shouldn't be in activePeers anyway
|
||||
let (removed, _) = collectionsQueue.sync(flags: .barrier) {
|
||||
@@ -3280,20 +3442,48 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
|
||||
private func sendPendingPrivateMessages(to peerID: String) {
|
||||
messageQueue.async(flags: .barrier) { [weak self] in
|
||||
guard let self = self,
|
||||
let pendingMessages = self.pendingPrivateMessages[peerID] else { return }
|
||||
guard let self = self else { return }
|
||||
|
||||
SecureLogger.log("Sending \(pendingMessages.count) pending private messages to \(peerID)", category: SecureLogger.session, level: .info)
|
||||
// Get pending messages with proper queue synchronization
|
||||
let pendingMessages = self.collectionsQueue.sync {
|
||||
return self.pendingPrivateMessages[peerID]
|
||||
}
|
||||
|
||||
guard let messages = pendingMessages else { return }
|
||||
|
||||
SecureLogger.log("Sending \(messages.count) pending private messages to \(peerID)", category: SecureLogger.session, level: .info)
|
||||
|
||||
// Clear pending messages for this peer
|
||||
self.pendingPrivateMessages.removeValue(forKey: peerID)
|
||||
self.collectionsQueue.sync(flags: .barrier) {
|
||||
_ = self.pendingPrivateMessages.removeValue(forKey: peerID)
|
||||
}
|
||||
|
||||
// Send each pending message
|
||||
for (content, recipientNickname, messageID) in pendingMessages {
|
||||
SecureLogger.log("Sending pending message \(messageID) to \(peerID)", category: SecureLogger.session, level: .debug)
|
||||
// Use async to avoid blocking the queue
|
||||
DispatchQueue.global().async { [weak self] in
|
||||
self?.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
||||
for (content, recipientNickname, messageID) in messages {
|
||||
// Check if this is a read receipt
|
||||
if content.hasPrefix("READ_RECEIPT:") {
|
||||
// Extract the original message ID
|
||||
let originalMessageID = String(content.dropFirst("READ_RECEIPT:".count))
|
||||
SecureLogger.log("Sending queued read receipt for message \(originalMessageID) to \(peerID)", category: SecureLogger.session, level: .debug)
|
||||
|
||||
// Create and send the actual read receipt
|
||||
let receipt = ReadReceipt(
|
||||
originalMessageID: originalMessageID,
|
||||
readerID: self.myPeerID,
|
||||
readerNickname: recipientNickname // This is actually the reader's nickname
|
||||
)
|
||||
|
||||
// Send the read receipt using the normal method
|
||||
DispatchQueue.global().async { [weak self] in
|
||||
self?.sendReadReceipt(receipt, to: peerID)
|
||||
}
|
||||
} else {
|
||||
// Regular message
|
||||
SecureLogger.log("Sending pending message \(messageID) to \(peerID)", category: SecureLogger.session, level: .debug)
|
||||
// Use async to avoid blocking the queue
|
||||
DispatchQueue.global().async { [weak self] in
|
||||
self?.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: messageID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3324,7 +3514,17 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
// Check with coordinator if we should initiate
|
||||
if !handshakeCoordinator.shouldInitiateHandshake(myPeerID: myPeerID, remotePeerID: peerID) {
|
||||
SecureLogger.log("Coordinator says we should not initiate handshake with \(peerID)", category: SecureLogger.handshake, level: .debug)
|
||||
return
|
||||
// Exception: If we have pending messages to send, override and initiate anyway
|
||||
let hasPendingMessages = collectionsQueue.sync {
|
||||
return pendingPrivateMessages[peerID]?.isEmpty == false
|
||||
}
|
||||
if !hasPendingMessages {
|
||||
return
|
||||
}
|
||||
let pendingCount = collectionsQueue.sync {
|
||||
return pendingPrivateMessages[peerID]?.count ?? 0
|
||||
}
|
||||
SecureLogger.log("Overriding handshake role due to \(pendingCount) pending messages for \(peerID)", category: SecureLogger.handshake, level: .warning)
|
||||
}
|
||||
|
||||
// Check if there's a retry delay
|
||||
@@ -3354,12 +3554,25 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: handshakeData,
|
||||
signature: nil,
|
||||
ttl: 3 // Moderate TTL for handshakes
|
||||
ttl: 6 // Increased TTL for better delivery on startup
|
||||
)
|
||||
|
||||
// Use broadcastPacket instead of sendPacket to ensure it goes through the mesh
|
||||
broadcastPacket(packet)
|
||||
|
||||
// Schedule a retry check after 5 seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// Check if handshake completed
|
||||
if !self.noiseService.hasEstablishedSession(with: peerID) {
|
||||
let state = self.handshakeCoordinator.getHandshakeState(for: peerID)
|
||||
if case .initiating = state {
|
||||
SecureLogger.log("Handshake with \(peerID) not completed after 5s, will retry", category: SecureLogger.handshake, level: .warning)
|
||||
// The handshake coordinator will handle retry logic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch NoiseSessionError.alreadyEstablished {
|
||||
// Session already established, no need to handshake
|
||||
handshakeCoordinator.recordHandshakeSuccess(peerID: peerID)
|
||||
@@ -3374,6 +3587,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
// Use noiseService directly
|
||||
SecureLogger.logHandshake("processing \(isInitiation ? "init" : "response")", peerID: peerID, success: true)
|
||||
|
||||
// Get current handshake state before processing
|
||||
let currentState = handshakeCoordinator.getHandshakeState(for: peerID)
|
||||
let hasEstablishedSession = noiseService.hasEstablishedSession(with: peerID)
|
||||
SecureLogger.log("Current handshake state for \(peerID): \(currentState), hasEstablishedSession: \(hasEstablishedSession)", category: SecureLogger.noise, level: .info)
|
||||
|
||||
// Check for duplicate handshake messages
|
||||
if handshakeCoordinator.isDuplicateHandshakeMessage(message) {
|
||||
SecureLogger.log("Duplicate handshake message from \(peerID), ignoring", category: SecureLogger.handshake, level: .debug)
|
||||
@@ -3393,6 +3611,8 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
do {
|
||||
// Process handshake message
|
||||
if let response = try noiseService.processHandshakeMessage(from: peerID, message: message) {
|
||||
SecureLogger.log("Handshake processing returned response of size \(response.count), sending back to \(peerID)", category: SecureLogger.noise, level: .info)
|
||||
|
||||
// Always send responses as handshake response type
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.noiseHandshakeResp.rawValue,
|
||||
@@ -3401,17 +3621,21 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: response,
|
||||
signature: nil,
|
||||
ttl: 3
|
||||
ttl: 6 // Increased TTL for better delivery on startup
|
||||
)
|
||||
|
||||
// Use broadcastPacket instead of sendPacket to ensure it goes through the mesh
|
||||
broadcastPacket(packet)
|
||||
} else {
|
||||
SecureLogger.log("No response needed from processHandshakeMessage", category: SecureLogger.noise, level: .debug)
|
||||
SecureLogger.log("No response needed from processHandshakeMessage (isInitiation: \(isInitiation))", category: SecureLogger.noise, level: .debug)
|
||||
}
|
||||
|
||||
// Check if handshake is complete
|
||||
if noiseService.hasEstablishedSession(with: peerID) {
|
||||
let sessionEstablished = noiseService.hasEstablishedSession(with: peerID)
|
||||
let newState = handshakeCoordinator.getHandshakeState(for: peerID)
|
||||
SecureLogger.log("After processing handshake message - sessionEstablished: \(sessionEstablished), newState: \(newState)", category: SecureLogger.noise, level: .info)
|
||||
|
||||
if sessionEstablished {
|
||||
SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID))
|
||||
// Unlock rotation now that handshake is complete
|
||||
unlockRotation()
|
||||
@@ -3422,6 +3646,10 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
// Clear handshake attempt time on success
|
||||
handshakeAttemptTimes.removeValue(forKey: peerID)
|
||||
|
||||
// Initialize last successful message time
|
||||
lastSuccessfulMessageTime[peerID] = Date()
|
||||
SecureLogger.log("Initialized lastSuccessfulMessageTime for \(peerID)", category: SecureLogger.noise, level: .debug)
|
||||
|
||||
// Send identity announcement to this specific peer
|
||||
sendNoiseIdentityAnnounce(to: peerID)
|
||||
|
||||
@@ -3450,6 +3678,12 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
handshakeCoordinator.recordHandshakeFailure(peerID: peerID, reason: error.localizedDescription)
|
||||
SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription))
|
||||
SecureLogger.log("Handshake failed with \(peerID): \(error)", category: SecureLogger.noise, level: .error)
|
||||
|
||||
// If handshake failed due to authentication error, clear the session to allow retry
|
||||
if case NoiseError.authenticationFailure = error {
|
||||
SecureLogger.log("Handshake failed with \(peerID): authenticationFailure - clearing session", category: SecureLogger.noise, level: .warning)
|
||||
cleanupPeerCryptoState(peerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3482,6 +3716,23 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
let decryptedData = try noiseService.decrypt(encryptedData, from: peerID)
|
||||
SecureLogger.log("Successfully decrypted message from \(peerID), decrypted size: \(decryptedData.count)", category: SecureLogger.encryption, level: .debug)
|
||||
|
||||
// Update last successful message time
|
||||
lastSuccessfulMessageTime[peerID] = Date()
|
||||
|
||||
// If we can decrypt messages from this peer, they should be in activePeers
|
||||
let wasAdded = collectionsQueue.sync(flags: .barrier) {
|
||||
if !self.activePeers.contains(peerID) {
|
||||
SecureLogger.log("Adding \(peerID) to activePeers after successful decryption", category: SecureLogger.noise, level: .info)
|
||||
return self.activePeers.insert(peerID).inserted
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if wasAdded {
|
||||
// Notify about peer list update
|
||||
self.notifyPeerListUpdate()
|
||||
}
|
||||
|
||||
// Check if this is a special format message (type marker + payload)
|
||||
if decryptedData.count > 1 {
|
||||
let typeMarker = decryptedData[0]
|
||||
@@ -3539,6 +3790,24 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
initiateNoiseHandshake(with: peerID)
|
||||
} else {
|
||||
SecureLogger.log("Have session with \(peerID) but decryption failed", category: SecureLogger.encryption, level: .warning)
|
||||
|
||||
// Session is corrupted - clear it and re-initiate handshake
|
||||
cleanupPeerCryptoState(peerID)
|
||||
|
||||
// Send identity announcement to prompt peer to initiate handshake if needed
|
||||
sendNoiseIdentityAnnounce(to: peerID)
|
||||
|
||||
// Update UI to show encryption is broken
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
if let chatVM = self?.delegate as? ChatViewModel {
|
||||
chatVM.updateEncryptionStatusForPeer(peerID)
|
||||
}
|
||||
}
|
||||
|
||||
// Initiate fresh handshake after a short delay to avoid collision
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
self?.initiateNoiseHandshake(with: peerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3556,6 +3825,21 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this peer is reconnecting after disconnect
|
||||
if let lastConnected = lastConnectionTime[peerID] {
|
||||
let timeSinceLastConnection = Date().timeIntervalSince(lastConnected)
|
||||
if timeSinceLastConnection > 5.0 { // More than 5 seconds since last connection
|
||||
// Clear any stale Noise session
|
||||
if noiseService.hasEstablishedSession(with: peerID) {
|
||||
SecureLogger.log("Peer \(peerID) reconnecting after \(Int(timeSinceLastConnection))s - clearing stale session", category: SecureLogger.noise, level: .info)
|
||||
cleanupPeerCryptoState(peerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update last connection time
|
||||
lastConnectionTime[peerID] = Date()
|
||||
|
||||
// Try JSON first if it looks like JSON
|
||||
let hello: VersionHello?
|
||||
if let firstByte = dataCopy.first, firstByte == 0x7B { // '{' character
|
||||
@@ -3651,12 +3935,12 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
collectionsQueue.sync(flags: .barrier) {
|
||||
_ = self.activePeers.remove(peerID)
|
||||
_ = self.peerNicknames.removeValue(forKey: peerID)
|
||||
_ = self.lastHeardFromPeer.removeValue(forKey: peerID)
|
||||
}
|
||||
announcedPeers.remove(peerID)
|
||||
|
||||
// Clean up any Noise session
|
||||
noiseService.removePeer(peerID)
|
||||
handshakeCoordinator.resetHandshakeState(for: peerID)
|
||||
cleanupPeerCryptoState(peerID)
|
||||
|
||||
// Notify delegate about incompatible peer disconnection
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
@@ -3800,11 +4084,34 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
|
||||
// Send private message using Noise Protocol
|
||||
private func sendPrivateMessageViaNoise(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
|
||||
// Use noiseService directly
|
||||
// Use per-peer encryption queue to prevent nonce desynchronization
|
||||
let encryptionQueue = getEncryptionQueue(for: recipientPeerID)
|
||||
|
||||
// Check if we have a Noise session with this peer
|
||||
if !noiseService.hasEstablishedSession(with: recipientPeerID) {
|
||||
SecureLogger.log("No Noise session with \(recipientPeerID), initiating handshake", category: SecureLogger.noise, level: .info)
|
||||
encryptionQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Use noiseService directly
|
||||
|
||||
// Check if we have a Noise session with this peer
|
||||
let hasSession = self.noiseService.hasEstablishedSession(with: recipientPeerID)
|
||||
|
||||
// Check if session is stale (no successful communication for a while)
|
||||
var sessionIsStale = false
|
||||
if hasSession {
|
||||
let lastSuccess = lastSuccessfulMessageTime[recipientPeerID] ?? Date.distantPast
|
||||
let sessionAge = Date().timeIntervalSince(lastSuccess)
|
||||
if sessionAge > 600.0 { // More than 10 minutes since last successful message
|
||||
sessionIsStale = true
|
||||
SecureLogger.log("Session with \(recipientPeerID) is stale (last success: \(Int(sessionAge))s ago), will re-establish", category: SecureLogger.noise, level: .info)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSession || sessionIsStale {
|
||||
if sessionIsStale {
|
||||
// Clear stale session first
|
||||
cleanupPeerCryptoState(recipientPeerID)
|
||||
}
|
||||
SecureLogger.log("No valid Noise session with \(recipientPeerID), initiating handshake", category: SecureLogger.noise, level: .info)
|
||||
|
||||
// Apply tie-breaker logic for handshake initiation
|
||||
if myPeerID < recipientPeerID {
|
||||
@@ -3822,7 +4129,8 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
self.pendingPrivateMessages[recipientPeerID] = []
|
||||
}
|
||||
self.pendingPrivateMessages[recipientPeerID]?.append((content, recipientNickname, messageID ?? UUID().uuidString))
|
||||
SecureLogger.log("Queued private message for \(recipientPeerID), \(self.pendingPrivateMessages[recipientPeerID]?.count ?? 0) messages pending", category: SecureLogger.noise, level: .info)
|
||||
let count = self.pendingPrivateMessages[recipientPeerID]?.count ?? 0
|
||||
SecureLogger.log("Queued private message for \(recipientPeerID), \(count) messages pending", category: SecureLogger.noise, level: .info)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -3832,11 +4140,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
|
||||
// Check if we're already processing this message
|
||||
let sendKey = "\(msgID)-\(recipientPeerID)"
|
||||
let alreadySending = collectionsQueue.sync(flags: .barrier) {
|
||||
if recentlySentMessages.contains(sendKey) {
|
||||
let alreadySending = self.collectionsQueue.sync(flags: .barrier) {
|
||||
if self.recentlySentMessages.contains(sendKey) {
|
||||
return true
|
||||
}
|
||||
recentlySentMessages.insert(sendKey)
|
||||
self.recentlySentMessages.insert(sendKey)
|
||||
// Clean up old entries after 10 seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { [weak self] in
|
||||
self?.collectionsQueue.sync(flags: .barrier) {
|
||||
@@ -3891,6 +4199,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
let encryptedData = try noiseService.encrypt(innerData, for: recipientPeerID)
|
||||
SecureLogger.log("Successfully encrypted message, size: \(encryptedData.count)", category: SecureLogger.encryption, level: .debug)
|
||||
|
||||
// Update last successful message time
|
||||
lastSuccessfulMessageTime[recipientPeerID] = Date()
|
||||
|
||||
// Send as Noise encrypted message
|
||||
let outerPacket = BitchatPacket(
|
||||
type: MessageType.noiseEncrypted.rawValue,
|
||||
@@ -3908,5 +4219,6 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
|
||||
// Failed to encrypt message
|
||||
SecureLogger.log("Failed to encrypt private message \(msgID) for \(recipientPeerID): \(error)", category: SecureLogger.encryption, level: .error)
|
||||
}
|
||||
} // End of encryptionQueue.async
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ class DeliveryTracker {
|
||||
private var sentAckIDs = Set<String>()
|
||||
|
||||
// Timeout configuration
|
||||
private let privateMessageTimeout: TimeInterval = 30 // 30 seconds
|
||||
private let roomMessageTimeout: TimeInterval = 60 // 1 minute
|
||||
private let favoriteTimeout: TimeInterval = 300 // 5 minutes for favorites
|
||||
private let privateMessageTimeout: TimeInterval = 120 // 2 minutes
|
||||
private let roomMessageTimeout: TimeInterval = 180 // 3 minutes
|
||||
private let favoriteTimeout: TimeInterval = 600 // 10 minutes for favorites
|
||||
|
||||
// Retry configuration
|
||||
private let maxRetries = 3
|
||||
|
||||
@@ -25,9 +25,9 @@ enum EncryptionStatus: Equatable {
|
||||
case .noiseHandshaking:
|
||||
return "lock.rotation"
|
||||
case .noiseSecured:
|
||||
return "lock"
|
||||
return "lock.fill" // Changed from "lock" to "lock.fill" for filled lock
|
||||
case .noiseVerified:
|
||||
return "lock.shield"
|
||||
return "lock.shield.fill" // Changed to filled version for consistency
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1172,21 +1172,33 @@ class ChatViewModel: ObservableObject {
|
||||
// MARK: - Noise Protocol Support
|
||||
|
||||
func updateEncryptionStatusForPeers() {
|
||||
for peerID in connectedPeers {
|
||||
updateEncryptionStatusForPeer(peerID)
|
||||
}
|
||||
}
|
||||
|
||||
func updateEncryptionStatusForPeer(_ peerID: String) {
|
||||
let noiseService = meshService.getNoiseService()
|
||||
|
||||
for peerID in connectedPeers {
|
||||
if noiseService.hasEstablishedSession(with: peerID) {
|
||||
// Check if fingerprint is verified using our persisted data
|
||||
if let fingerprint = noiseService.getPeerFingerprint(peerID),
|
||||
verifiedFingerprints.contains(fingerprint) {
|
||||
peerEncryptionStatus[peerID] = .noiseVerified
|
||||
} else {
|
||||
peerEncryptionStatus[peerID] = .noiseSecured
|
||||
}
|
||||
if noiseService.hasEstablishedSession(with: peerID) {
|
||||
// Check if fingerprint is verified using our persisted data
|
||||
if let fingerprint = getFingerprint(for: peerID),
|
||||
verifiedFingerprints.contains(fingerprint) {
|
||||
peerEncryptionStatus[peerID] = .noiseVerified
|
||||
} else {
|
||||
// Always use Noise - no legacy encryption
|
||||
peerEncryptionStatus[peerID] = .noiseHandshaking
|
||||
peerEncryptionStatus[peerID] = .noiseSecured
|
||||
}
|
||||
} else if noiseService.hasSession(with: peerID) {
|
||||
// Session exists but not established - handshaking
|
||||
peerEncryptionStatus[peerID] = .noiseHandshaking
|
||||
} else {
|
||||
// No session at all
|
||||
peerEncryptionStatus[peerID] = Optional.none
|
||||
}
|
||||
|
||||
// Force UI update
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1194,31 +1206,64 @@ class ChatViewModel: ObservableObject {
|
||||
// This must be a pure function - no state mutations allowed
|
||||
// to avoid SwiftUI update loops
|
||||
|
||||
// Check if we have a fingerprint for this peer
|
||||
if let fingerprint = getFingerprint(for: peerID) {
|
||||
// Check if this fingerprint is verified
|
||||
if verifiedFingerprints.contains(fingerprint) {
|
||||
// Return verified if we have a Noise session
|
||||
if meshService.getNoiseService().hasEstablishedSession(with: peerID) {
|
||||
let hasEstablished = meshService.getNoiseService().hasEstablishedSession(with: peerID)
|
||||
let hasSession = meshService.getNoiseService().hasSession(with: peerID)
|
||||
let storedStatus = peerEncryptionStatus[peerID]
|
||||
|
||||
// First check if we have an established session
|
||||
if hasEstablished {
|
||||
// We have encryption, now check if it's verified
|
||||
if let fingerprint = getFingerprint(for: peerID) {
|
||||
if verifiedFingerprints.contains(fingerprint) {
|
||||
return .noiseVerified
|
||||
} else {
|
||||
return .noiseSecured
|
||||
}
|
||||
} else if meshService.getNoiseService().hasEstablishedSession(with: peerID) {
|
||||
return .noiseSecured
|
||||
}
|
||||
// We have a session but no fingerprint yet - still secured
|
||||
return .noiseSecured
|
||||
}
|
||||
|
||||
// Check if handshaking
|
||||
if hasSession {
|
||||
return .noiseHandshaking
|
||||
}
|
||||
|
||||
// Fall back to stored status
|
||||
return peerEncryptionStatus[peerID] ?? .none
|
||||
let finalStatus = storedStatus ?? .none
|
||||
|
||||
// Only log occasionally to avoid spam
|
||||
if Int.random(in: 0..<100) == 0 {
|
||||
SecureLogger.log("getEncryptionStatus for \(peerID): hasEstablished=\(hasEstablished), hasSession=\(hasSession), stored=\(String(describing: storedStatus)), final=\(finalStatus)", category: SecureLogger.security, level: .debug)
|
||||
}
|
||||
|
||||
return finalStatus
|
||||
}
|
||||
|
||||
// Update encryption status in appropriate places, not during view updates
|
||||
private func updateEncryptionStatus(for peerID: String) {
|
||||
if let fingerprint = getFingerprint(for: peerID) {
|
||||
if verifiedFingerprints.contains(fingerprint) && meshService.getNoiseService().hasEstablishedSession(with: peerID) {
|
||||
peerEncryptionStatus[peerID] = .noiseVerified
|
||||
} else if meshService.getNoiseService().hasEstablishedSession(with: peerID) {
|
||||
let noiseService = meshService.getNoiseService()
|
||||
|
||||
if noiseService.hasEstablishedSession(with: peerID) {
|
||||
if let fingerprint = getFingerprint(for: peerID) {
|
||||
if verifiedFingerprints.contains(fingerprint) {
|
||||
peerEncryptionStatus[peerID] = .noiseVerified
|
||||
} else {
|
||||
peerEncryptionStatus[peerID] = .noiseSecured
|
||||
}
|
||||
} else {
|
||||
// Session established but no fingerprint yet
|
||||
peerEncryptionStatus[peerID] = .noiseSecured
|
||||
}
|
||||
} else if noiseService.hasSession(with: peerID) {
|
||||
peerEncryptionStatus[peerID] = .noiseHandshaking
|
||||
} else {
|
||||
peerEncryptionStatus[peerID] = Optional.none
|
||||
}
|
||||
|
||||
// Trigger UI update
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1322,12 +1367,21 @@ class ChatViewModel: ObservableObject {
|
||||
// Set up authentication callback
|
||||
noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in
|
||||
DispatchQueue.main.async {
|
||||
guard let self = self else { return }
|
||||
|
||||
SecureLogger.log("ChatViewModel: Peer authenticated - \(peerID), fingerprint: \(fingerprint)", category: SecureLogger.security, level: .info)
|
||||
|
||||
// Update encryption status
|
||||
if self?.verifiedFingerprints.contains(fingerprint) == true {
|
||||
self?.peerEncryptionStatus[peerID] = .noiseVerified
|
||||
if self.verifiedFingerprints.contains(fingerprint) {
|
||||
self.peerEncryptionStatus[peerID] = .noiseVerified
|
||||
SecureLogger.log("ChatViewModel: Setting encryption status to noiseVerified for \(peerID)", category: SecureLogger.security, level: .info)
|
||||
} else {
|
||||
self?.peerEncryptionStatus[peerID] = .noiseSecured
|
||||
self.peerEncryptionStatus[peerID] = .noiseSecured
|
||||
SecureLogger.log("ChatViewModel: Setting encryption status to noiseSecured for \(peerID)", category: SecureLogger.security, level: .info)
|
||||
}
|
||||
|
||||
// Force UI update
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2024,6 +2078,17 @@ extension ChatViewModel: BitchatDelegate {
|
||||
// Remove ephemeral session from identity manager
|
||||
SecureIdentityStateManager.shared.removeEphemeralSession(peerID: peerID)
|
||||
|
||||
// Clear sent read receipts for this peer since they'll need to be resent after reconnection
|
||||
// Only clear receipts for messages from this specific peer
|
||||
if let messages = privateChats[peerID] {
|
||||
for message in messages {
|
||||
// Remove read receipts for messages FROM this peer (not TO this peer)
|
||||
if message.senderPeerID == peerID {
|
||||
sentReadReceipts.remove(message.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve nickname using helper
|
||||
let displayName = resolveNickname(for: peerID)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user