From 9bac75ef76cb78afab574d501c6d8b9e6d901b22 Mon Sep 17 00:00:00 2001 From: jack Date: Fri, 25 Jul 2025 15:04:12 +0200 Subject: [PATCH 1/5] Add encryption export compliance to Info.plist Set ITSAppUsesNonExemptEncryption to false for TestFlight submission --- bitchat/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bitchat/Info.plist b/bitchat/Info.plist index fd87f8b5..ea6ef9d8 100644 --- a/bitchat/Info.plist +++ b/bitchat/Info.plist @@ -4,6 +4,8 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + ITSAppUsesNonExemptEncryption + CFBundleDisplayName bitchat CFBundleExecutable From 21562049acb5c94a16e3babcf9933afecd5fff2e Mon Sep 17 00:00:00 2001 From: jack Date: Fri, 25 Jul 2025 15:06:20 +0200 Subject: [PATCH 2/5] Revert "Add encryption export compliance to Info.plist" This reverts commit 2cfbb1679d80e9b1cdff7a3008ba6228870e36e6. --- bitchat/Info.plist | 2 -- 1 file changed, 2 deletions(-) diff --git a/bitchat/Info.plist b/bitchat/Info.plist index ea6ef9d8..fd87f8b5 100644 --- a/bitchat/Info.plist +++ b/bitchat/Info.plist @@ -4,8 +4,6 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) - ITSAppUsesNonExemptEncryption - CFBundleDisplayName bitchat CFBundleExecutable From 087d3b29aeccf009695b659d87f35210f08f7df4 Mon Sep 17 00:00:00 2001 From: jack Date: Fri, 25 Jul 2025 16:02:25 +0200 Subject: [PATCH 3/5] 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) --- bitchat.xcodeproj/project.pbxproj | 16 ++++ bitchat/Services/BluetoothMeshService.swift | 98 ++++++++++++++++----- bitchat/ViewModels/ChatViewModel.swift | 13 ++- 3 files changed, 101 insertions(+), 26 deletions(-) diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index a598a611..0c33af40 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -737,14 +737,20 @@ CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = bitchatShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = bitchat; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; 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; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -760,6 +766,7 @@ CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = bitchat; @@ -769,6 +776,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; @@ -814,6 +822,7 @@ CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = bitchat; @@ -823,6 +832,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; @@ -1025,14 +1035,20 @@ CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = bitchatShareExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = bitchat; IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); + MARKETING_VERSION = 1.1.0; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.ShareExtension; 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; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 63adbae2..11ec050e 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -709,12 +709,10 @@ class BluetoothMeshService: NSObject { self.peerNicknames.removeValue(forKey: existingPeerID) } - // Update active peers set - if self.activePeers.contains(existingPeerID) { - self.activePeers.remove(existingPeerID) - // Don't pre-insert the new peer ID - let the announce packet handle it - // This ensures the connect message logic works properly - } + // Update active peers set - always remove old peer ID + self.activePeers.remove(existingPeerID) + // Don't pre-insert the new peer ID - let the announce packet handle it + // This ensures the connect message logic works properly // Transfer any connected peripherals if let peripheral = self.connectedPeripherals[existingPeerID] { @@ -733,6 +731,14 @@ class BluetoothMeshService: NSObject { self.lastHeardFromPeer.removeValue(forKey: existingPeerID) 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 @@ -1161,8 +1167,15 @@ class BluetoothMeshService: NSObject { // Setup battery optimizer setupBatteryOptimizer() - // Start cover traffic for privacy - startCoverTraffic() + // Start cover traffic for privacy (disabled by default for now) + // 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() { @@ -4759,22 +4772,32 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } private func sendDummyMessage() { - // Only send dummy messages if we have connected peers - let peers = getAllConnectedPeerIDs() - guard !peers.isEmpty else { return } + // Only send dummy messages if we have connected peers with established sessions + let peersWithSessions = getAllConnectedPeerIDs().filter { peerID in + return noiseService.hasEstablishedSession(with: peerID) + } + + guard !peersWithSessions.isEmpty else { + SecureLogger.log("Cover traffic: No peers with established sessions, skipping dummy message", + category: SecureLogger.security, level: .debug) + return + } // Skip if battery is low if currentBatteryLevel < 0.2 { + SecureLogger.log("Cover traffic: Battery low, skipping dummy message", + category: SecureLogger.security, level: .debug) return } - // Pick a random peer to send to - guard let randomPeer = peers.randomElement() else { return } + // Pick a random peer with an established session to send to + guard let randomPeer = peersWithSessions.randomElement() else { return } // Generate random dummy content 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 let recipientNickname = collectionsQueue.sync { @@ -4892,18 +4915,42 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { private func sendKeepAlivePings() { let connectedPeers = collectionsQueue.sync { 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 } } + SecureLogger.log("Keep-alive timer: checking \(connectedPeers.count) peers for pings", + category: SecureLogger.session, level: .debug) + for peerID in connectedPeers { // Don't spam if we recently heard from them if let lastHeard = lastHeardFromPeer[peerID], 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 } + SecureLogger.log("Sending keepalive ping to \(peerID)", + category: SecureLogger.session, level: .debug) validateNoiseSession(with: peerID) } } @@ -6103,6 +6150,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // Send private message using Noise Protocol 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 let encryptionQueue = getEncryptionQueue(for: recipientPeerID) @@ -6117,16 +6167,16 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // 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) - // Increase session validity to 24 hours - sessions should persist across temporary disconnects - if sessionAge > 86400.0 { // More than 24 hours 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) + let lastSuccess = lastSuccessfulMessageTime[recipientPeerID] ?? Date.distantPast + let sessionAge = Date().timeIntervalSince(lastSuccess) + // Increase session validity to 24 hours - sessions should persist across temporary disconnects + if sessionAge > 86400.0 { // More than 24 hours 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 !hasSession || sessionIsStale { if sessionIsStale { // Clear stale session first cleanupPeerCryptoState(recipientPeerID) diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 05a1100a..c00468aa 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -610,8 +610,17 @@ class ChatViewModel: ObservableObject { if let peerID = selectedPrivateChatPeer { // In private chat - send to the other person if let peerNickname = meshService.getPeerNicknames()[peerID] { - // Send the message directly without going through sendPrivateMessage to avoid local echo - meshService.sendPrivateMessage(screenshotMessage, to: peerID, recipientNickname: peerNickname) + // Only send screenshot notification if we have an established session + // 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 From 97c239337052035377f6d84cc2b48eaba1c58f71 Mon Sep 17 00:00:00 2001 From: jack Date: Fri, 25 Jul 2025 16:12:36 +0200 Subject: [PATCH 4/5] Fix lazy handshake behavior and re-enable cover traffic - Re-enable cover traffic for privacy protection - Start handshake immediately when opening PM conversation - Remove handshake request system notifications --- bitchat/Services/BluetoothMeshService.swift | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 11ec050e..f14b56a8 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -1169,12 +1169,10 @@ class BluetoothMeshService: NSObject { // Start cover traffic for privacy (disabled by default for now) // TODO: Make this configurable in settings - let coverTrafficEnabled = false + let coverTrafficEnabled = true if coverTrafficEnabled { SecureLogger.log("Cover traffic enabled", category: SecureLogger.security, level: .info) startCoverTraffic() - } else { - SecureLogger.log("Cover traffic disabled", category: SecureLogger.security, level: .info) } } @@ -1637,14 +1635,9 @@ class BluetoothMeshService: NSObject { noiseSessionStates[peerID] = .handshakeQueued } - // Apply tie-breaker logic for handshake initiation - if myPeerID < peerID { - // We have lower ID, initiate handshake - initiateNoiseHandshake(with: peerID) - } else { - // We have higher ID, send targeted identity announce to prompt them to initiate - sendNoiseIdentityAnnounce(to: peerID) - } + // Always initiate handshake when triggered by UI + // This ensures immediate handshake when opening PM + initiateNoiseHandshake(with: peerID) } func getPeerRSSI() -> [String: NSNumber] { @@ -5753,7 +5746,9 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { SecureLogger.log("Received handshake request from \(request.requesterID) (\(request.requesterNickname)) with \(request.pendingMessageCount) pending messages", category: SecureLogger.noise, level: .info) - // Notify UI about pending messages + // Don't show handshake request notification in UI + // User requested to remove this notification + /* DispatchQueue.main.async { [weak self] in if let chatVM = self?.delegate as? ChatViewModel { // Notify the UI that someone wants to send messages @@ -5762,6 +5757,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { pendingCount: request.pendingMessageCount) } } + */ // Check if we already have a session if noiseService.hasEstablishedSession(with: peerID) { From 6afef8466a61215b7cab409908397f8df734f4c6 Mon Sep 17 00:00:00 2001 From: jack Date: Fri, 25 Jul 2025 18:09:55 +0200 Subject: [PATCH 5/5] Fix read receipt to use same efficient pattern as delivery ACK - Modified read receipt to use type marker + binary payload pattern - Added read receipt type marker handling in noise decryption - Reduces message size and processing overhead - Ensures consistency between delivery ACK and read receipt implementations --- bitchat.xcodeproj/project.pbxproj | 4 +- bitchat/Services/BluetoothMeshService.swift | 69 ++++++++++++--------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index 0c33af40..0c04f530 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -785,7 +785,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; @@ -841,7 +841,7 @@ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index f14b56a8..d5a20bb0 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -1502,41 +1502,35 @@ class BluetoothMeshService: NSObject { // Check if we have a Noise session with this peer // Use noiseService directly if self.noiseService.hasEstablishedSession(with: recipientID) { - // Use Noise encryption - send as Noise encrypted message + // Use Noise encryption - encrypt only the receipt payload directly do { - // Create inner read receipt packet - let innerPacket = BitchatPacket( - type: MessageType.readReceipt.rawValue, + // Create a special payload that indicates this is a read receipt + // Format: [1 byte type marker] + [receipt binary data] + var receiptPayload = Data() + receiptPayload.append(MessageType.readReceipt.rawValue) // Type marker + receiptPayload.append(receiptData) // Receipt binary data + + // Encrypt only the payload (not a full packet) + let encryptedPayload = try noiseService.encrypt(receiptPayload, for: recipientID) + + // Create outer Noise packet with the encrypted payload + let outerPacket = BitchatPacket( + type: MessageType.noiseEncrypted.rawValue, senderID: Data(hexString: self.myPeerID) ?? Data(), recipientID: Data(hexString: recipientID) ?? Data(), timestamp: UInt64(Date().timeIntervalSince1970 * 1000), - payload: receiptData, + payload: encryptedPayload, signature: nil, ttl: 3 ) - // Encrypt the entire inner packet - if let innerData = innerPacket.toBinaryData() { - let encryptedInnerData = try noiseService.encrypt(innerData, for: recipientID) - - // Create outer Noise packet - let outerPacket = BitchatPacket( - type: MessageType.noiseEncrypted.rawValue, - senderID: Data(hexString: self.myPeerID) ?? Data(), - recipientID: Data(hexString: recipientID) ?? Data(), - timestamp: UInt64(Date().timeIntervalSince1970 * 1000), - payload: encryptedInnerData, - signature: nil, - ttl: 3 ) - - SecureLogger.log("Sending encrypted read receipt for message \(receipt.originalMessageID) to \(recipientID)", category: SecureLogger.noise, level: .info) - - // Try direct delivery first for read receipts - if !self.sendDirectToRecipient(outerPacket, recipientPeerID: recipientID) { - // Recipient not directly connected, use selective relay - SecureLogger.log("Recipient \(recipientID) not directly connected for read receipt, using relay", - category: SecureLogger.session, level: .info) - self.sendViaSelectiveRelay(outerPacket, recipientPeerID: recipientID) - } + SecureLogger.log("Sending encrypted read receipt for message \(receipt.originalMessageID) to \(recipientID)", category: SecureLogger.noise, level: .info) + + // Try direct delivery first for read receipts + if !self.sendDirectToRecipient(outerPacket, recipientPeerID: recipientID) { + // Recipient not directly connected, use selective relay + SecureLogger.log("Recipient \(recipientID) not directly connected for read receipt, using relay", + category: SecureLogger.session, level: .info) + self.sendViaSelectiveRelay(outerPacket, recipientPeerID: recipientID) } } catch { SecureLogger.logError(error, context: "Failed to encrypt read receipt via Noise for \(recipientID)", category: SecureLogger.encryption) @@ -5358,6 +5352,25 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { SecureLogger.log("Failed to decode delivery ACK via Noise - data size: \(ackData.count)", category: SecureLogger.session, level: .warning) } } + + // Check if this is a read receipt with the new format + else if typeMarker == MessageType.readReceipt.rawValue { + // Extract the receipt binary data (skip the type marker) + let receiptData = decryptedData.dropFirst() + + // Decode the read receipt from binary + if let receipt = ReadReceipt.fromBinaryData(receiptData) { + SecureLogger.log("Received binary read receipt via Noise: \(receipt.originalMessageID) from \(receipt.readerNickname)", category: SecureLogger.session, level: .debug) + + // Process the read receipt + DispatchQueue.main.async { + self.delegate?.didReceiveReadReceipt(receipt) + } + return + } else { + SecureLogger.log("Failed to decode read receipt via Noise - data size: \(receiptData.count)", category: SecureLogger.session, level: .warning) + } + } } // Try to parse as a full inner packet (for backward compatibility and other message types)