Fix handshake deadlock after decryption failure

Always accept handshake initiations even with valid sessions, as the peer must have cleared their session for a good reason (e.g., decryption failure). This prevents deadlock where one peer has no session while the other maintains an invalid one.
This commit is contained in:
jack
2025-07-23 18:22:39 +02:00
parent d1d4434b66
commit a84d6f22ef
5 changed files with 973 additions and 69 deletions
+35 -1
View File
@@ -57,10 +57,24 @@ class NoiseHandshakeCoordinator {
} }
/// Check if we should initiate handshake with a peer /// Check if we should initiate handshake with a peer
func shouldInitiateHandshake(myPeerID: String, remotePeerID: String) -> Bool { func shouldInitiateHandshake(myPeerID: String, remotePeerID: String, forceIfStale: Bool = false) -> Bool {
return handshakeQueue.sync { return handshakeQueue.sync {
// Check if we're already in an active handshake // Check if we're already in an active handshake
if let state = handshakeStates[remotePeerID], state.isActive { if let state = handshakeStates[remotePeerID], state.isActive {
// Check if the handshake is stale and we should force a new one
if forceIfStale {
switch state {
case .initiating(_, let lastAttempt):
if Date().timeIntervalSince(lastAttempt) > handshakeTimeout {
SecureLogger.log("Forcing new handshake with \(remotePeerID) - previous stuck in initiating",
category: SecureLogger.handshake, level: .warning)
return true
}
default:
break
}
}
SecureLogger.log("Already in active handshake with \(remotePeerID), state: \(state)", SecureLogger.log("Already in active handshake with \(remotePeerID), state: \(state)",
category: SecureLogger.handshake, level: .debug) category: SecureLogger.handshake, level: .debug)
return false return false
@@ -255,6 +269,26 @@ class NoiseHandshakeCoordinator {
} }
} }
/// Get current retry count for a peer
func getRetryCount(for peerID: String) -> Int {
return handshakeQueue.sync {
switch handshakeStates[peerID] {
case .initiating(let attempt, _):
return attempt - 1 // Attempts start at 1, retries start at 0
default:
return 0
}
}
}
/// Increment retry count for a peer
func incrementRetryCount(for peerID: String) {
handshakeQueue.async(flags: .barrier) {
let currentAttempt = self.getCurrentAttempt(for: peerID)
self.handshakeStates[peerID] = .initiating(attempt: currentAttempt + 1, lastAttempt: Date())
}
}
// MARK: - Private Helpers // MARK: - Private Helpers
private func getCurrentAttempt(for peerID: String) -> Int { private func getCurrentAttempt(for peerID: String) -> Int {
+181
View File
@@ -96,6 +96,11 @@ enum MessageType: UInt8 {
case versionHello = 0x20 // Initial version announcement case versionHello = 0x20 // Initial version announcement
case versionAck = 0x21 // Version acknowledgment case versionAck = 0x21 // Version acknowledgment
// Protocol-level acknowledgments
case protocolAck = 0x22 // Generic protocol acknowledgment
case protocolNack = 0x23 // Negative acknowledgment (failure)
case systemValidation = 0x24 // Session validation ping
var description: String { var description: String {
switch self { switch self {
case .announce: return "announce" case .announce: return "announce"
@@ -113,6 +118,9 @@ enum MessageType: UInt8 {
case .noiseIdentityAnnounce: return "noiseIdentityAnnounce" case .noiseIdentityAnnounce: return "noiseIdentityAnnounce"
case .versionHello: return "versionHello" case .versionHello: return "versionHello"
case .versionAck: return "versionAck" case .versionAck: return "versionAck"
case .protocolAck: return "protocolAck"
case .protocolNack: return "protocolNack"
case .systemValidation: return "systemValidation"
} }
} }
} }
@@ -352,6 +360,172 @@ struct ReadReceipt: Codable {
} }
} }
// MARK: - Protocol Acknowledgments
// Protocol-level acknowledgment for reliable delivery
struct ProtocolAck: Codable {
let originalPacketID: String // ID of the packet being acknowledged
let ackID: String // Unique ID for this ACK
let senderID: String // Who sent the original packet
let receiverID: String // Who received and is acknowledging
let packetType: UInt8 // Type of packet being acknowledged
let timestamp: Date // When ACK was generated
let hopCount: UInt8 // Hops taken to reach receiver
init(originalPacketID: String, senderID: String, receiverID: String, packetType: UInt8, hopCount: UInt8) {
self.originalPacketID = originalPacketID
self.ackID = UUID().uuidString
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.timestamp = Date()
self.hopCount = hopCount
}
// Private init for binary decoding
private init(originalPacketID: String, ackID: String, senderID: String, receiverID: String,
packetType: UInt8, timestamp: Date, hopCount: UInt8) {
self.originalPacketID = originalPacketID
self.ackID = ackID
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.timestamp = timestamp
self.hopCount = hopCount
}
func toBinaryData() -> Data {
var data = Data()
data.appendUUID(originalPacketID)
data.appendUUID(ackID)
// Sender and receiver IDs as 8-byte hex strings
data.append(Data(hexString: senderID) ?? Data(repeating: 0, count: 8))
data.append(Data(hexString: receiverID) ?? Data(repeating: 0, count: 8))
data.appendUInt8(packetType)
data.appendUInt8(hopCount)
data.appendDate(timestamp)
return data
}
static func fromBinaryData(_ data: Data) -> ProtocolAck? {
let dataCopy = Data(data)
guard dataCopy.count >= 50 else { return nil } // 2 UUIDs + 2 IDs + type + hop + timestamp
var offset = 0
guard let originalPacketID = dataCopy.readUUID(at: &offset),
let ackID = dataCopy.readUUID(at: &offset),
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
let packetType = dataCopy.readUInt8(at: &offset),
let hopCount = dataCopy.readUInt8(at: &offset),
let timestamp = dataCopy.readDate(at: &offset) else { return nil }
let senderID = senderIDData.hexEncodedString()
let receiverID = receiverIDData.hexEncodedString()
return ProtocolAck(originalPacketID: originalPacketID,
ackID: ackID,
senderID: senderID,
receiverID: receiverID,
packetType: packetType,
timestamp: timestamp,
hopCount: hopCount)
}
}
// Protocol-level negative acknowledgment
struct ProtocolNack: Codable {
let originalPacketID: String // ID of the packet that failed
let nackID: String // Unique ID for this NACK
let senderID: String // Who sent the original packet
let receiverID: String // Who is reporting the failure
let packetType: UInt8 // Type of packet that failed
let timestamp: Date // When NACK was generated
let reason: String // Reason for failure
let errorCode: UInt8 // Numeric error code
// Error codes
enum ErrorCode: UInt8 {
case unknown = 0
case checksumFailed = 1
case decryptionFailed = 2
case malformedPacket = 3
case unsupportedVersion = 4
case resourceExhausted = 5
case routingFailed = 6
case sessionExpired = 7
}
init(originalPacketID: String, senderID: String, receiverID: String,
packetType: UInt8, reason: String, errorCode: ErrorCode = .unknown) {
self.originalPacketID = originalPacketID
self.nackID = UUID().uuidString
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.timestamp = Date()
self.reason = reason
self.errorCode = errorCode.rawValue
}
// Private init for binary decoding
private init(originalPacketID: String, nackID: String, senderID: String, receiverID: String,
packetType: UInt8, timestamp: Date, reason: String, errorCode: UInt8) {
self.originalPacketID = originalPacketID
self.nackID = nackID
self.senderID = senderID
self.receiverID = receiverID
self.packetType = packetType
self.timestamp = timestamp
self.reason = reason
self.errorCode = errorCode
}
func toBinaryData() -> Data {
var data = Data()
data.appendUUID(originalPacketID)
data.appendUUID(nackID)
// Sender and receiver IDs as 8-byte hex strings
data.append(Data(hexString: senderID) ?? Data(repeating: 0, count: 8))
data.append(Data(hexString: receiverID) ?? Data(repeating: 0, count: 8))
data.appendUInt8(packetType)
data.appendUInt8(errorCode)
data.appendDate(timestamp)
data.appendString(reason)
return data
}
static func fromBinaryData(_ data: Data) -> ProtocolNack? {
let dataCopy = Data(data)
guard dataCopy.count >= 52 else { return nil } // Minimum size
var offset = 0
guard let originalPacketID = dataCopy.readUUID(at: &offset),
let nackID = dataCopy.readUUID(at: &offset),
let senderIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
let receiverIDData = dataCopy.readFixedBytes(at: &offset, count: 8),
let packetType = dataCopy.readUInt8(at: &offset),
let errorCode = dataCopy.readUInt8(at: &offset),
let timestamp = dataCopy.readDate(at: &offset),
let reason = dataCopy.readString(at: &offset) else { return nil }
let senderID = senderIDData.hexEncodedString()
let receiverID = receiverIDData.hexEncodedString()
return ProtocolNack(originalPacketID: originalPacketID,
nackID: nackID,
senderID: senderID,
receiverID: receiverID,
packetType: packetType,
timestamp: timestamp,
reason: reason,
errorCode: errorCode)
}
}
// MARK: - Peer Identity Rotation // MARK: - Peer Identity Rotation
@@ -796,6 +970,9 @@ protocol BitchatDelegate: AnyObject {
func didReceiveDeliveryAck(_ ack: DeliveryAck) func didReceiveDeliveryAck(_ ack: DeliveryAck)
func didReceiveReadReceipt(_ receipt: ReadReceipt) func didReceiveReadReceipt(_ receipt: ReadReceipt)
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
// Peer availability tracking
func peerAvailabilityChanged(_ peerID: String, available: Bool)
} }
// Provide default implementation to make it effectively optional // Provide default implementation to make it effectively optional
@@ -815,4 +992,8 @@ extension BitchatDelegate {
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) { func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
// Default empty implementation // Default empty implementation
} }
func peerAvailabilityChanged(_ peerID: String, available: Bool) {
// Default empty implementation
}
} }
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -859,9 +859,11 @@ class ChatViewModel: ObservableObject {
return primaryColor return primaryColor
} else if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender), } else if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender),
let rssi = meshService.getPeerRSSI()[peerID] { let rssi = meshService.getPeerRSSI()[peerID] {
// Use actual RSSI value
return getRSSIColor(rssi: rssi.intValue, colorScheme: colorScheme) return getRSSIColor(rssi: rssi.intValue, colorScheme: colorScheme)
} else { } else {
return primaryColor.opacity(0.9) // No RSSI data available - use a neutral color
return primaryColor.opacity(0.7)
} }
} }
@@ -960,9 +962,11 @@ class ChatViewModel: ObservableObject {
senderColor = primaryColor senderColor = primaryColor
} else if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender), } else if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender),
let rssi = meshService.getPeerRSSI()[peerID] { let rssi = meshService.getPeerRSSI()[peerID] {
// Use actual RSSI value
senderColor = getRSSIColor(rssi: rssi.intValue, colorScheme: colorScheme) senderColor = getRSSIColor(rssi: rssi.intValue, colorScheme: colorScheme)
} else { } else {
senderColor = primaryColor.opacity(0.9) // No RSSI data available - use a neutral color
senderColor = primaryColor.opacity(0.7)
} }
senderStyle.foregroundColor = senderColor senderStyle.foregroundColor = senderColor
@@ -1103,9 +1107,11 @@ class ChatViewModel: ObservableObject {
senderColor = primaryColor senderColor = primaryColor
} else if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender), } else if let peerID = message.senderPeerID ?? getPeerIDForNickname(message.sender),
let rssi = meshService.getPeerRSSI()[peerID] { let rssi = meshService.getPeerRSSI()[peerID] {
// Use actual RSSI value
senderColor = getRSSIColor(rssi: rssi.intValue, colorScheme: colorScheme) senderColor = getRSSIColor(rssi: rssi.intValue, colorScheme: colorScheme)
} else { } else {
senderColor = primaryColor.opacity(0.9) // No RSSI data available - use a neutral color
senderColor = primaryColor.opacity(0.7)
} }
senderStyle.foregroundColor = senderColor senderStyle.foregroundColor = senderColor
+8 -2
View File
@@ -495,7 +495,7 @@ struct ContentView: View {
ForEach(sortedPeers, id: \.self) { peerID in ForEach(sortedPeers, id: \.self) { peerID in
let displayName = peerID == myPeerID ? viewModel.nickname : (peerNicknames[peerID] ?? "anon\(peerID.prefix(4))") let displayName = peerID == myPeerID ? viewModel.nickname : (peerNicknames[peerID] ?? "anon\(peerID.prefix(4))")
let rssi = peerRSSI[peerID]?.intValue ?? -100 let rssi = peerRSSI[peerID]?.intValue
let isFavorite = viewModel.isFavorite(peerID: peerID) let isFavorite = viewModel.isFavorite(peerID: peerID)
let isMe = peerID == myPeerID let isMe = peerID == myPeerID
@@ -511,11 +511,17 @@ struct ContentView: View {
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundColor(Color.orange) .foregroundColor(Color.orange)
.accessibilityLabel("Unread message from \(displayName)") .accessibilityLabel("Unread message from \(displayName)")
} else { } else if let rssi = rssi {
Image(systemName: "circle.fill") Image(systemName: "circle.fill")
.font(.system(size: 8)) .font(.system(size: 8))
.foregroundColor(viewModel.getRSSIColor(rssi: rssi, colorScheme: colorScheme)) .foregroundColor(viewModel.getRSSIColor(rssi: rssi, colorScheme: colorScheme))
.accessibilityLabel("Signal strength: \(rssi > -60 ? "excellent" : rssi > -70 ? "good" : rssi > -80 ? "fair" : "poor")") .accessibilityLabel("Signal strength: \(rssi > -60 ? "excellent" : rssi > -70 ? "good" : rssi > -80 ? "fair" : "poor")")
} else {
// No RSSI data available
Image(systemName: "circle")
.font(.system(size: 8))
.foregroundColor(Color.secondary.opacity(0.5))
.accessibilityLabel("Signal strength: unknown")
} }
// Peer name // Peer name