mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 05:05:19 +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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user