mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 18:45:20 +00:00
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:
@@ -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";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
// TODO: Make this configurable in settings
|
||||||
|
let coverTrafficEnabled = false
|
||||||
|
if coverTrafficEnabled {
|
||||||
|
SecureLogger.log("Cover traffic enabled", category: SecureLogger.security, level: .info)
|
||||||
startCoverTraffic()
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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] {
|
||||||
|
// 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
|
// Send the message directly without going through sendPrivateMessage to avoid local echo
|
||||||
meshService.sendPrivateMessage(screenshotMessage, to: peerID, recipientNickname: peerNickname)
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user