fix: prevent spurious handshake requests from rotated peers and cover traffic

- Clean up connection states and pending messages during peer ID rotation
- Filter out rotated peer IDs in keep-alive timer to prevent validation attempts
- Add session check before sending cover traffic dummy messages
- Disable cover traffic by default (was sending without user knowledge)
- Only send screenshot notifications when session exists
- Add logging to track automatic message triggers
- Fix compilation errors (SecureLogger.privacy -> security)
This commit is contained in:
jack
2025-07-25 16:15:12 +02:00
parent 21562049ac
commit 087d3b29ae
3 changed files with 101 additions and 26 deletions
+16
View File
@@ -737,14 +737,20 @@
CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = bitchatShareExtension/Info.plist; INFOPLIST_FILE = bitchatShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
@@ -760,6 +766,7 @@
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat; INFOPLIST_KEY_CFBundleDisplayName = bitchat;
@@ -769,6 +776,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
PRODUCT_NAME = bitchat; PRODUCT_NAME = bitchat;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -814,6 +822,7 @@
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat; INFOPLIST_KEY_CFBundleDisplayName = bitchat;
@@ -823,6 +832,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
PRODUCT_NAME = bitchat; PRODUCT_NAME = bitchat;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1025,14 +1035,20 @@
CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = bitchatShareExtension/Info.plist; INFOPLIST_FILE = bitchatShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
}; };
+75 -25
View File
@@ -709,12 +709,10 @@ class BluetoothMeshService: NSObject {
self.peerNicknames.removeValue(forKey: existingPeerID) self.peerNicknames.removeValue(forKey: existingPeerID)
} }
// Update active peers set // Update active peers set - always remove old peer ID
if self.activePeers.contains(existingPeerID) { self.activePeers.remove(existingPeerID)
self.activePeers.remove(existingPeerID) // Don't pre-insert the new peer ID - let the announce packet handle it
// Don't pre-insert the new peer ID - let the announce packet handle it // This ensures the connect message logic works properly
// This ensures the connect message logic works properly
}
// Transfer any connected peripherals // Transfer any connected peripherals
if let peripheral = self.connectedPeripherals[existingPeerID] { if let peripheral = self.connectedPeripherals[existingPeerID] {
@@ -733,6 +731,14 @@ class BluetoothMeshService: NSObject {
self.lastHeardFromPeer.removeValue(forKey: existingPeerID) self.lastHeardFromPeer.removeValue(forKey: existingPeerID)
self.lastHeardFromPeer[newPeerID] = lastHeard self.lastHeardFromPeer[newPeerID] = lastHeard
} }
// Clean up connection state for old peer ID
self.peerConnectionStates.removeValue(forKey: existingPeerID)
// Don't transfer connection state - let it be re-established naturally
// Clean up any pending messages for old peer ID
self.pendingPrivateMessages.removeValue(forKey: existingPeerID)
// Don't transfer pending messages - they would be stale anyway
} }
// Add new mapping // Add new mapping
@@ -1161,8 +1167,15 @@ class BluetoothMeshService: NSObject {
// Setup battery optimizer // Setup battery optimizer
setupBatteryOptimizer() setupBatteryOptimizer()
// Start cover traffic for privacy // Start cover traffic for privacy (disabled by default for now)
startCoverTraffic() // TODO: Make this configurable in settings
let coverTrafficEnabled = false
if coverTrafficEnabled {
SecureLogger.log("Cover traffic enabled", category: SecureLogger.security, level: .info)
startCoverTraffic()
} else {
SecureLogger.log("Cover traffic disabled", category: SecureLogger.security, level: .info)
}
} }
func sendBroadcastAnnounce() { func sendBroadcastAnnounce() {
@@ -4759,22 +4772,32 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
} }
private func sendDummyMessage() { private func sendDummyMessage() {
// Only send dummy messages if we have connected peers // Only send dummy messages if we have connected peers with established sessions
let peers = getAllConnectedPeerIDs() let peersWithSessions = getAllConnectedPeerIDs().filter { peerID in
guard !peers.isEmpty else { return } return noiseService.hasEstablishedSession(with: peerID)
}
// Skip if battery is low guard !peersWithSessions.isEmpty else {
if currentBatteryLevel < 0.2 { SecureLogger.log("Cover traffic: No peers with established sessions, skipping dummy message",
category: SecureLogger.security, level: .debug)
return return
} }
// Pick a random peer to send to // Skip if battery is low
guard let randomPeer = peers.randomElement() else { return } if currentBatteryLevel < 0.2 {
SecureLogger.log("Cover traffic: Battery low, skipping dummy message",
category: SecureLogger.security, level: .debug)
return
}
// Pick a random peer with an established session to send to
guard let randomPeer = peersWithSessions.randomElement() else { return }
// Generate random dummy content // Generate random dummy content
let dummyContent = generateDummyContent() let dummyContent = generateDummyContent()
// Sending cover traffic SecureLogger.log("Cover traffic: Sending dummy message to \(randomPeer)",
category: SecureLogger.security, level: .info)
// Send as a private message so it's encrypted // Send as a private message so it's encrypted
let recipientNickname = collectionsQueue.sync { let recipientNickname = collectionsQueue.sync {
@@ -4892,18 +4915,42 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
private func sendKeepAlivePings() { private func sendKeepAlivePings() {
let connectedPeers = collectionsQueue.sync { let connectedPeers = collectionsQueue.sync {
return Array(activePeers).filter { peerID in return Array(activePeers).filter { peerID in
// Only send keepalive to authenticated peers // Only send keepalive to authenticated peers that still exist
// Check if this peer ID is still current (not rotated)
guard let fingerprint = peerIDToFingerprint[peerID],
let currentPeerID = fingerprintToPeerID[fingerprint],
currentPeerID == peerID else {
// This peer ID has rotated, skip it
SecureLogger.log("Skipping keepalive for rotated peer ID: \(peerID)",
category: SecureLogger.session, level: .debug)
return false
}
// Check if we actually have a Noise session with this peer
guard noiseService.hasEstablishedSession(with: peerID) else {
SecureLogger.log("Skipping keepalive for \(peerID) - no established session",
category: SecureLogger.session, level: .debug)
return false
}
return peerConnectionStates[peerID] == .authenticated return peerConnectionStates[peerID] == .authenticated
} }
} }
SecureLogger.log("Keep-alive timer: checking \(connectedPeers.count) peers for pings",
category: SecureLogger.session, level: .debug)
for peerID in connectedPeers { for peerID in connectedPeers {
// Don't spam if we recently heard from them // Don't spam if we recently heard from them
if let lastHeard = lastHeardFromPeer[peerID], if let lastHeard = lastHeardFromPeer[peerID],
Date().timeIntervalSince(lastHeard) < keepAliveInterval / 2 { Date().timeIntervalSince(lastHeard) < keepAliveInterval / 2 {
SecureLogger.log("Skipping keepalive for \(peerID) - heard recently",
category: SecureLogger.session, level: .debug)
continue // Skip if we heard from them in the last 10 seconds continue // Skip if we heard from them in the last 10 seconds
} }
SecureLogger.log("Sending keepalive ping to \(peerID)",
category: SecureLogger.session, level: .debug)
validateNoiseSession(with: peerID) validateNoiseSession(with: peerID)
} }
} }
@@ -6103,6 +6150,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
// Send private message using Noise Protocol // Send private message using Noise Protocol
private func sendPrivateMessageViaNoise(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) { private func sendPrivateMessageViaNoise(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
SecureLogger.log("sendPrivateMessageViaNoise called - content: '\(content.prefix(50))...', to: \(recipientPeerID), messageID: \(messageID ?? "nil")",
category: SecureLogger.noise, level: .info)
// Use per-peer encryption queue to prevent nonce desynchronization // Use per-peer encryption queue to prevent nonce desynchronization
let encryptionQueue = getEncryptionQueue(for: recipientPeerID) let encryptionQueue = getEncryptionQueue(for: recipientPeerID)
@@ -6117,16 +6167,16 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
// Check if session is stale (no successful communication for a while) // Check if session is stale (no successful communication for a while)
var sessionIsStale = false var sessionIsStale = false
if hasSession { if hasSession {
let lastSuccess = lastSuccessfulMessageTime[recipientPeerID] ?? Date.distantPast let lastSuccess = lastSuccessfulMessageTime[recipientPeerID] ?? Date.distantPast
let sessionAge = Date().timeIntervalSince(lastSuccess) let sessionAge = Date().timeIntervalSince(lastSuccess)
// Increase session validity to 24 hours - sessions should persist across temporary disconnects // Increase session validity to 24 hours - sessions should persist across temporary disconnects
if sessionAge > 86400.0 { // More than 24 hours since last successful message if sessionAge > 86400.0 { // More than 24 hours since last successful message
sessionIsStale = true sessionIsStale = true
SecureLogger.log("Session with \(recipientPeerID) is stale (last success: \(Int(sessionAge))s ago), will re-establish", category: SecureLogger.noise, level: .info) 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 !hasSession || sessionIsStale {
if sessionIsStale { if sessionIsStale {
// Clear stale session first // Clear stale session first
cleanupPeerCryptoState(recipientPeerID) cleanupPeerCryptoState(recipientPeerID)
+11 -2
View File
@@ -610,8 +610,17 @@ class ChatViewModel: ObservableObject {
if let peerID = selectedPrivateChatPeer { if let peerID = selectedPrivateChatPeer {
// In private chat - send to the other person // In private chat - send to the other person
if let peerNickname = meshService.getPeerNicknames()[peerID] { if let peerNickname = meshService.getPeerNicknames()[peerID] {
// Send the message directly without going through sendPrivateMessage to avoid local echo // Only send screenshot notification if we have an established session
meshService.sendPrivateMessage(screenshotMessage, to: peerID, recipientNickname: peerNickname) // This prevents triggering handshake requests for screenshot notifications
let sessionState = meshService.getNoiseSessionState(for: peerID)
switch sessionState {
case .established:
// Send the message directly without going through sendPrivateMessage to avoid local echo
meshService.sendPrivateMessage(screenshotMessage, to: peerID, recipientNickname: peerNickname)
default:
// Don't send screenshot notification if no session exists
SecureLogger.log("Skipping screenshot notification to \(peerID) - no established session", category: SecureLogger.security, level: .debug)
}
} }
// Show local notification immediately as system message // Show local notification immediately as system message