diff --git a/Configs/Release.xcconfig b/Configs/Release.xcconfig index a1c0d2ec..a93d93df 100644 --- a/Configs/Release.xcconfig +++ b/Configs/Release.xcconfig @@ -1,4 +1,4 @@ -MARKETING_VERSION = 1.4.0 +MARKETING_VERSION = 1.4.1 CURRENT_PROJECT_VERSION = 1 IPHONEOS_DEPLOYMENT_TARGET = 16.0 diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index cda979db..07441c6f 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -481,7 +481,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = "$(MARKETING_VERSION)"; + MARKETING_VERSION = 1.4.1; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ShareExtension"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -514,7 +514,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = "$(MARKETING_VERSION)"; + MARKETING_VERSION = 1.4.1; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; @@ -574,7 +574,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = "$(MARKETING_VERSION)"; + MARKETING_VERSION = 1.4.1; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = bitchat; SDKROOT = iphoneos; @@ -609,7 +609,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)"; - MARKETING_VERSION = "$(MARKETING_VERSION)"; + MARKETING_VERSION = 1.4.1; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = bitchat; REGISTER_APP_GROUPS = YES; @@ -700,7 +700,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)"; - MARKETING_VERSION = "$(MARKETING_VERSION)"; + MARKETING_VERSION = 1.4.1; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = bitchat; REGISTER_APP_GROUPS = YES; @@ -795,7 +795,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = "$(MARKETING_VERSION)"; + MARKETING_VERSION = 1.4.1; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ShareExtension"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/bitchat/Noise/NoiseSecurityConsiderations.swift b/bitchat/Noise/NoiseSecurityConsiderations.swift index 67402f1c..a9e69216 100644 --- a/bitchat/Noise/NoiseSecurityConsiderations.swift +++ b/bitchat/Noise/NoiseSecurityConsiderations.swift @@ -210,6 +210,15 @@ final class NoiseRateLimiter { self.messageTimestamps.removeValue(forKey: peerID) } } + + func resetAll() { + queue.async(flags: .barrier) { + self.handshakeTimestamps.removeAll() + self.messageTimestamps.removeAll() + self.globalHandshakeTimestamps.removeAll() + self.globalMessageTimestamps.removeAll() + } + } } // MARK: - Security Errors diff --git a/bitchat/Noise/NoiseSession.swift b/bitchat/Noise/NoiseSession.swift index 0c9f31b9..e159021d 100644 --- a/bitchat/Noise/NoiseSession.swift +++ b/bitchat/Noise/NoiseSession.swift @@ -308,6 +308,15 @@ final class NoiseSessionManager { _ = sessions.removeValue(forKey: peerID) } } + + func removeAllSessions() { + managerQueue.sync(flags: .barrier) { + for (_, session) in sessions { + session.reset() + } + sessions.removeAll() + } + } func getEstablishedSessions() -> [String: NoiseSession] { return managerQueue.sync { diff --git a/bitchat/Nostr/NostrIdentity.swift b/bitchat/Nostr/NostrIdentity.swift index da789c31..9630b403 100644 --- a/bitchat/Nostr/NostrIdentity.swift +++ b/bitchat/Nostr/NostrIdentity.swift @@ -150,13 +150,31 @@ struct NostrIdentityBridge { /// Clear all Nostr identity associations and current identity static func clearAllAssociations() { - // Delete current Nostr identity - KeychainHelper.delete(key: currentIdentityKey, service: keychainService) - KeychainHelper.delete(key: deviceSeedKey, service: keychainService) - - // Note: We can't efficiently delete all noise-nostr associations - // without tracking them, but they'll be orphaned and eventually cleaned up - // The important part is deleting the current identity so a new one is generated + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: keychainService, + kSecMatchLimit as String: kSecMatchLimitAll, + kSecReturnAttributes as String: true + ] + + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + if status == errSecSuccess, let items = result as? [[String: Any]] { + for item in items { + var deleteQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: keychainService + ] + if let account = item[kSecAttrAccount as String] as? String { + deleteQuery[kSecAttrAccount as String] = account + } + SecItemDelete(deleteQuery as CFDictionary) + } + } else if status == errSecItemNotFound { + // nothing persisted; no action needed + } + + deviceSeedCache = nil } // MARK: - Per-Geohash Identities (Location Channels) diff --git a/bitchat/Services/BLEService.swift b/bitchat/Services/BLEService.swift index 17787715..3d15a24a 100644 --- a/bitchat/Services/BLEService.swift +++ b/bitchat/Services/BLEService.swift @@ -89,8 +89,9 @@ final class BLEService: NSObject { var myPeerID: String = "" var myNickname: String = "anon" - private let noiseService: NoiseEncryptionService + private var noiseService: NoiseEncryptionService private let identityManager: SecureIdentityStateManagerProtocol + private let keychain: KeychainManagerProtocol private var myPeerIDData: Data = Data() // MARK: - Advertising Privacy @@ -330,33 +331,44 @@ final class BLEService: NSObject { } } - init(keychain: KeychainManagerProtocol, identityManager: SecureIdentityStateManagerProtocol) { - noiseService = NoiseEncryptionService(keychain: keychain) - self.identityManager = identityManager - super.init() - - // Derive stable peer ID from Noise static public key fingerprint (first 8 bytes → 16 hex chars) - let fingerprint = noiseService.getIdentityFingerprint() // 64 hex chars - self.myPeerID = String(fingerprint.prefix(16)) - self.myPeerIDData = Data(hexString: myPeerID) ?? Data() - - // Set queue key for identification - messageQueue.setSpecific(key: messageQueueKey, value: ()) - - // Set up Noise session establishment callback - // This ensures we send pending messages only when session is truly established - noiseService.onPeerAuthenticated = { [weak self] peerID, fingerprint in + private func configureNoiseServiceCallbacks(for service: NoiseEncryptionService) { + service.onPeerAuthenticated = { [weak self] peerID, fingerprint in SecureLogger.debug("🔐 Noise session authenticated with \(peerID), fingerprint: \(fingerprint.prefix(16))...") - // Send any messages that were queued during handshake self?.messageQueue.async { [weak self] in self?.sendPendingMessagesAfterHandshake(for: peerID) self?.sendPendingNoisePayloadsAfterHandshake(for: peerID) } - // Proactive presence nudge: announce immediately after handshake self?.messageQueue.async { [weak self] in self?.sendAnnounce(forceSend: true) } } + } + + private func refreshPeerIdentity() { + let fingerprint = noiseService.getIdentityFingerprint() + myPeerID = String(fingerprint.prefix(16)) + myPeerIDData = Data(hexString: myPeerID) ?? Data() + } + + private func restartGossipManager() { + gossipSyncManager?.stop() + let sync = GossipSyncManager(myPeerID: myPeerID) + sync.delegate = self + sync.start() + gossipSyncManager = sync + } + + init(keychain: KeychainManagerProtocol, identityManager: SecureIdentityStateManagerProtocol) { + self.keychain = keychain + noiseService = NoiseEncryptionService(keychain: keychain) + self.identityManager = identityManager + super.init() + + configureNoiseServiceCallbacks(for: noiseService) + refreshPeerIdentity() + + // Set queue key for identification + messageQueue.setSpecific(key: messageQueueKey, value: ()) // Set up application state tracking (iOS only) #if os(iOS) @@ -407,10 +419,7 @@ final class BLEService: NSObject { requestPeerDataPublish() // Initialize gossip sync manager - let sync = GossipSyncManager(myPeerID: myPeerID) - sync.delegate = self - sync.start() - self.gossipSyncManager = sync + restartGossipManager() } func setNickname(_ nickname: String) { @@ -738,6 +747,46 @@ final class BLEService: NSObject { subscribedCentrals.removeAll() centralToPeerID.removeAll() } + + func resetIdentityForPanic(currentNickname: String) { + messageQueue.sync(flags: .barrier) { + pendingMessagesAfterHandshake.removeAll() + pendingNoisePayloadsAfterHandshake.removeAll() + } + + collectionsQueue.sync(flags: .barrier) { + recentAnnounceBySender.removeAll() + recentAnnounceOrder.removeAll() + pendingPeripheralWrites.removeAll() + pendingNotifications.removeAll() + pendingDirectedRelays.removeAll() + ingressByMessageID.removeAll() + recentPacketTimestamps.removeAll() + scheduledRelays.values.forEach { $0.cancel() } + scheduledRelays.removeAll() + } + + bleQueue.sync { + pendingWriteBuffers.removeAll() + recentConnectTimeouts.removeAll() + } + recentDisconnectNotifies.removeAll() + + noiseService.clearEphemeralStateForPanic() + noiseService.clearPersistentIdentity() + + let newNoise = NoiseEncryptionService(keychain: keychain) + noiseService = newNoise + configureNoiseServiceCallbacks(for: newNoise) + refreshPeerIdentity() + restartGossipManager() + + setNickname(currentNickname) + + messageDeduplicator.reset() + requestPeerDataPublish() + startServices() + } func getNoiseService() -> NoiseEncryptionService { return noiseService diff --git a/bitchat/Services/KeychainManager.swift b/bitchat/Services/KeychainManager.swift index 5cd70a7f..cbc379f4 100644 --- a/bitchat/Services/KeychainManager.swift +++ b/bitchat/Services/KeychainManager.swift @@ -282,6 +282,7 @@ final class KeychainManager: KeychainManagerProtocol { "com.bitchat.deviceidentity", "com.bitchat.noise.identity", "chat.bitchat.passwords", + "chat.bitchat.nostr", "bitchat.keychain", "bitchat", "com.bitchat" diff --git a/bitchat/Services/NoiseEncryptionService.swift b/bitchat/Services/NoiseEncryptionService.swift index 0163f2c7..7d55421d 100644 --- a/bitchat/Services/NoiseEncryptionService.swift +++ b/bitchat/Services/NoiseEncryptionService.swift @@ -526,6 +526,15 @@ final class NoiseEncryptionService { SecureLogger.info(.sessionExpired(peerID: peerID)) } + + func clearEphemeralStateForPanic() { + sessionManager.removeAllSessions() + serviceQueue.sync(flags: .barrier) { + peerFingerprints.removeAll() + fingerprintToPeerID.removeAll() + } + rateLimiter.resetAll() + } // MARK: - Private Helpers diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 15636afa..6451a9b9 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -3088,8 +3088,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Clear persistent favorites from keychain FavoritesPersistenceService.shared.clearAllFavorites() - // Clear identity data from secure storage - identityManager.clearAllIdentityData() + // Identity manager has cleared persisted identity data above // Clear autocomplete state autocompleteSuggestions.removeAll() @@ -3119,6 +3118,9 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { // Disconnect from all peers and clear persistent identity // This will force creation of a new identity (new fingerprint) on next launch meshService.emergencyDisconnectAll() + if let bleService = meshService as? BLEService { + bleService.resetIdentityForPanic(currentNickname: nickname) + } // No need to force UserDefaults synchronization