diff --git a/Package.swift b/Package.swift index 1cbc526a..5510094f 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,14 @@ let package = Package( targets: [ .executableTarget( name: "bitchat", - path: "bitchat" + path: "bitchat", + exclude: [ + "Info.plist", + "Assets.xcassets", + "bitchat.entitlements", + "bitchat-macOS.entitlements", + "LaunchScreen.storyboard" + ] ), ] ) \ No newline at end of file diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index d372e4a2..a0a66a6d 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 04636BBB2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */; }; 04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */; }; 04636BBD2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */; }; + 04636BBF2E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */; }; + 04636BC02E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */; }; 04891CA92E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; }; 04891CAA2E22971E0064A111 /* LRUCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04891CA82E22971E0064A111 /* LRUCache.swift */; }; 04AD0B4E2E25B9580002A40A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; }; @@ -61,9 +63,7 @@ 04B6BA752E21601B0090FE39 /* KeychainIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA602E21601B0090FE39 /* KeychainIntegrationTests.swift */; }; 04B6BA762E21601B0090FE39 /* NoiseChannelEncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA612E21601B0090FE39 /* NoiseChannelEncryptionTests.swift */; }; 04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */; }; - 04B6BA7A2E2166A50090FE39 /* SecurityLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA782E2166A50090FE39 /* SecurityLogger.swift */; }; 04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */; }; - 04B6BA7C2E2166A50090FE39 /* SecurityLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B6BA782E2166A50090FE39 /* SecurityLogger.swift */; }; 0DAFF1DDE9BA83FF648D5AB3 /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; }; 0FBC81FF78CF4711B78E092A /* IdentityModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2446380E7A44E49A35B664 /* IdentityModels.swift */; }; 10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; }; @@ -155,6 +155,7 @@ 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 04636BB82E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryEncodingUtils.swift; sourceTree = ""; }; 04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryMessageHandler.swift; sourceTree = ""; }; + 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = ""; }; 04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = ""; }; 04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolVersionTests.swift; sourceTree = ""; }; 04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolVersionNegotiationTests.swift; sourceTree = ""; }; @@ -179,7 +180,6 @@ 04B6BA652E21601B0090FE39 /* NoiseSecurityValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityValidatorTests.swift; sourceTree = ""; }; 04B6BA662E21601B0090FE39 /* SecureNoiseSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureNoiseSessionTests.swift; sourceTree = ""; }; 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseTestingHelper.swift; sourceTree = ""; }; - 04B6BA782E2166A50090FE39 /* SecurityLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityLogger.swift; sourceTree = ""; }; 12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryTracker.swift; sourceTree = ""; }; 136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = ""; }; 1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomFilterTests.swift; sourceTree = ""; }; @@ -279,9 +279,9 @@ 9A78348821A7D3374607D4E3 /* Utils */ = { isa = PBXGroup; children = ( + 04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */, 04891CA82E22971E0064A111 /* LRUCache.swift */, 04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */, - 04B6BA782E2166A50090FE39 /* SecurityLogger.swift */, ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */, 32F149C43D1915831B60FE09 /* CompressionUtil.swift */, CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */, @@ -572,7 +572,6 @@ 04B6BA4F2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */, 8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */, 04B6BA792E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */, - 04B6BA7A2E2166A50090FE39 /* SecurityLogger.swift in Sources */, FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */, 04636BBA2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */, 04636BBB2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */, @@ -583,6 +582,7 @@ 04B6BA582E203D6C0090FE39 /* NoiseTestingView.swift in Sources */, C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */, 749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */, + 04636BBF2E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */, 0245710AEAA58AD0A1425234 /* OptimizedBloomFilter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -609,7 +609,6 @@ 04B6BA4E2E2038A70090FE39 /* NoiseEncryptionService.swift in Sources */, CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */, 04B6BA7B2E2166A50090FE39 /* NoiseTestingHelper.swift in Sources */, - 04B6BA7C2E2166A50090FE39 /* SecurityLogger.swift in Sources */, 8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */, 04636BBC2E2FAA1700FBCFA8 /* BinaryEncodingUtils.swift in Sources */, 04636BBD2E2FAA1700FBCFA8 /* BinaryMessageHandler.swift in Sources */, @@ -620,6 +619,7 @@ 04B6BA562E203D6C0090FE39 /* NoiseTestingView.swift in Sources */, CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */, 61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */, + 04636BC02E2FCA8A00FBCFA8 /* SecureLogger.swift in Sources */, 1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/bitchat/Identity/SecureIdentityStateManager.swift b/bitchat/Identity/SecureIdentityStateManager.swift index 730c8e00..56cb5680 100644 --- a/bitchat/Identity/SecureIdentityStateManager.swift +++ b/bitchat/Identity/SecureIdentityStateManager.swift @@ -37,13 +37,15 @@ class SecureIdentityStateManager { // Try to load from keychain if let keyData = keychain.getIdentityKey(forKey: encryptionKeyName) { loadedKey = SymmetricKey(data: keyData) + SecureLogger.logKeyOperation("load", keyType: "identity cache encryption key", success: true) } // Generate new key if needed else { loadedKey = SymmetricKey(size: .bits256) let keyData = loadedKey.withUnsafeBytes { Data($0) } // Save to keychain - _ = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName) + let saved = keychain.saveIdentityKey(keyData, forKey: encryptionKeyName) + SecureLogger.logKeyOperation("generate", keyType: "identity cache encryption key", success: saved) } self.encryptionKey = loadedKey @@ -66,7 +68,7 @@ class SecureIdentityStateManager { cache = try JSONDecoder().decode(IdentityCache.self, from: decryptedData) } catch { // Log error but continue with empty cache - SecurityLogger.log("Failed to load identity cache", category: SecurityLogger.security, level: .error) + SecureLogger.logError(error, context: "Failed to load identity cache", category: SecureLogger.security) } } @@ -76,7 +78,7 @@ class SecureIdentityStateManager { let sealedBox = try AES.GCM.seal(data, using: encryptionKey) _ = keychain.saveIdentityKey(sealedBox.combined!, forKey: cacheKey) } catch { - SecurityLogger.log("Failed to save identity cache", category: SecurityLogger.security, level: .error) + SecureLogger.logError(error, context: "Failed to save identity cache", category: SecureLogger.security) } } @@ -184,6 +186,8 @@ class SecureIdentityStateManager { } func setBlocked(_ fingerprint: String, isBlocked: Bool) { + SecureLogger.log("User \(isBlocked ? "blocked" : "unblocked"): \(fingerprint)", category: SecureLogger.security, level: .info) + queue.async(flags: .barrier) { if var identity = self.cache.socialIdentities[fingerprint] { identity.isBlocked = isBlocked @@ -282,6 +286,8 @@ class SecureIdentityStateManager { // MARK: - Cleanup func clearAllIdentityData() { + SecureLogger.logSecurityEvent(.invalidKey(reason: "Clearing all identity data"), level: .warning) + queue.async(flags: .barrier) { self.cache = IdentityCache() self.ephemeralSessions.removeAll() @@ -289,7 +295,8 @@ class SecureIdentityStateManager { self.pendingActions.removeAll() // Delete from keychain - _ = self.keychain.deleteIdentityKey(forKey: self.cacheKey) + let deleted = self.keychain.deleteIdentityKey(forKey: self.cacheKey) + SecureLogger.logKeyOperation("delete", keyType: "identity cache", success: deleted) } } @@ -303,6 +310,8 @@ class SecureIdentityStateManager { // MARK: - Verification func setVerified(fingerprint: String, verified: Bool) { + SecureLogger.log("Fingerprint \(verified ? "verified" : "unverified"): \(fingerprint)", category: SecureLogger.security, level: .info) + queue.async(flags: .barrier) { if verified { self.cache.verifiedFingerprints.insert(fingerprint) diff --git a/bitchat/Noise/NoiseChannelEncryption.swift b/bitchat/Noise/NoiseChannelEncryption.swift index 7cb150c7..71d71ffd 100644 --- a/bitchat/Noise/NoiseChannelEncryption.swift +++ b/bitchat/Noise/NoiseChannelEncryption.swift @@ -58,7 +58,10 @@ class NoiseChannelEncryption { } // Store in keychain - _ = KeychainManager.shared.saveChannelPassword(password, for: channel) + let saved = KeychainManager.shared.saveChannelPassword(password, for: channel) + if saved { + SecureLogger.logKeyOperation("set", keyType: "channel key for \(channel)", success: true) + } } /// Get channel key @@ -84,7 +87,10 @@ class NoiseChannelEncryption { self.channelKeys.removeValue(forKey: channel) } - _ = KeychainManager.shared.deleteChannelPassword(for: channel) + let deleted = KeychainManager.shared.deleteChannelPassword(for: channel) + if deleted { + SecureLogger.logKeyOperation("remove", keyType: "channel key for \(channel)", success: true) + } } // MARK: - Replay Protection @@ -119,6 +125,7 @@ class NoiseChannelEncryption { /// Encrypt message for a channel func encryptChannelMessage(_ message: String, for channel: String) throws -> Data { guard let key = getChannelKey(for: channel) else { + SecureLogger.log("Channel encryption failed - no key for channel: \(channel)", category: SecureLogger.encryption, level: .error) throw NoiseChannelError.noChannelKey } @@ -137,6 +144,7 @@ class NoiseChannelEncryption { /// Decrypt channel message func decryptChannelMessage(_ encryptedData: Data, for channel: String) throws -> String { guard let key = getChannelKey(for: channel) else { + SecureLogger.log("Channel decryption failed - no key for channel: \(channel)", category: SecureLogger.encryption, level: .error) throw NoiseChannelError.noChannelKey } @@ -157,6 +165,7 @@ class NoiseChannelEncryption { let decryptedData = try ChaChaPoly.open(sealedBox, using: key) guard let message = String(data: decryptedData, encoding: .utf8) else { + SecureLogger.log("Channel decryption failed - invalid UTF8 for channel: \(channel)", category: SecureLogger.encryption, level: .error) throw NoiseChannelError.decryptionFailed } @@ -192,12 +201,15 @@ class NoiseChannelEncryption { // Verify timestamp is recent (within 5 minutes) let age = Date().timeIntervalSince(packet.timestamp) - guard age < 300 else { return nil } + guard age < 300 else { + SecureLogger.log("Expired channel key packet for channel: \(packet.channel), age: \(age)s", category: SecureLogger.security, level: .warning) + return nil + } return keyQueue.sync(flags: .barrier) { // Check for replay attack if receivedNonces.contains(packet.nonce) { - SecurityLogger.logSecurityEvent(.replayAttackDetected(channel: packet.channel), level: .warning) + SecureLogger.logSecurityEvent(.replayAttackDetected(channel: packet.channel), level: .warning) return nil // This nonce was already processed } diff --git a/bitchat/Noise/NoiseProtocol.swift b/bitchat/Noise/NoiseProtocol.swift index 20be4170..05775aea 100644 --- a/bitchat/Noise/NoiseProtocol.swift +++ b/bitchat/Noise/NoiseProtocol.swift @@ -87,7 +87,8 @@ class NoiseCipherState { nonce += 1 // Log high nonce values that might indicate issues - if currentNonce > 100 { + if currentNonce > 1000000 { + SecureLogger.log("High nonce value detected: \(currentNonce) - consider rekeying", category: SecureLogger.encryption, level: .warning) } return sealedBox.ciphertext + sealedBox.tag @@ -122,7 +123,8 @@ class NoiseCipherState { ) // Log high nonce values that might indicate issues - if currentNonce > 100 { + if currentNonce > 1000000 { + SecureLogger.log("High nonce value detected: \(currentNonce) - consider rekeying", category: SecureLogger.encryption, level: .warning) } do { @@ -131,6 +133,7 @@ class NoiseCipherState { return plaintext } catch { // Log authentication failures with nonce info + SecureLogger.log("Decryption failed at nonce \(currentNonce)", category: SecureLogger.encryption, level: .error) throw error } } @@ -404,6 +407,7 @@ class NoiseHandshakeState { do { remoteEphemeralPublic = try NoiseHandshakeState.validatePublicKey(ephemeralData) } catch { + SecureLogger.log("Invalid ephemeral public key received", category: SecureLogger.security, level: .warning) throw NoiseError.invalidMessage } symmetricState.mixHash(ephemeralData) @@ -420,6 +424,7 @@ class NoiseHandshakeState { let decrypted = try symmetricState.decryptAndHash(staticData) remoteStaticPublic = try NoiseHandshakeState.validatePublicKey(decrypted) } catch { + SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Unknown - handshake"), level: .error) throw NoiseError.authenticationFailure } @@ -604,7 +609,7 @@ extension NoiseHandshakeState { // Check against known bad points if lowOrderPoints.contains(keyData) { - SecurityLogger.logSecurityEvent(.invalidKey(reason: "Low-order point detected"), level: .warning) + SecureLogger.logSecurityEvent(.invalidKey(reason: "Low-order point detected"), level: .warning) throw NoiseError.invalidPublicKey } @@ -614,6 +619,7 @@ extension NoiseHandshakeState { return publicKey } catch { // If CryptoKit rejects it, it's invalid + SecureLogger.logSecurityEvent(.invalidKey(reason: "CryptoKit validation failed"), level: .warning) throw NoiseError.invalidPublicKey } } diff --git a/bitchat/Noise/NoiseSecurityConsiderations.swift b/bitchat/Noise/NoiseSecurityConsiderations.swift index 71371bdd..227f8fbf 100644 --- a/bitchat/Noise/NoiseSecurityConsiderations.swift +++ b/bitchat/Noise/NoiseSecurityConsiderations.swift @@ -167,6 +167,7 @@ class NoiseRateLimiter { // Check global rate limit first globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo } if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute { + SecureLogger.log("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: SecureLogger.security, level: .warning) return false } @@ -175,6 +176,7 @@ class NoiseRateLimiter { timestamps = timestamps.filter { $0 > oneMinuteAgo } if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute { + SecureLogger.log("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: SecureLogger.security, level: .warning) return false } @@ -194,6 +196,7 @@ class NoiseRateLimiter { // Check global rate limit first globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo } if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond { + SecureLogger.log("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: SecureLogger.security, level: .warning) return false } @@ -202,6 +205,7 @@ class NoiseRateLimiter { timestamps = timestamps.filter { $0 > oneSecondAgo } if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond { + SecureLogger.log("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: SecureLogger.security, level: .warning) return false } diff --git a/bitchat/Noise/NoiseSession.swift b/bitchat/Noise/NoiseSession.swift index 1558b3eb..f8636cf3 100644 --- a/bitchat/Noise/NoiseSession.swift +++ b/bitchat/Noise/NoiseSession.swift @@ -127,6 +127,8 @@ class NoiseSession { state = .established handshakeState = nil // Clear handshake state + SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID)) + return nil } else { // Generate response @@ -148,6 +150,8 @@ class NoiseSession { state = .established handshakeState = nil // Clear handshake state + + SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID)) } return response @@ -208,12 +212,17 @@ class NoiseSession { func reset() { sessionQueue.sync(flags: .barrier) { + let wasEstablished = state == .established state = .uninitialized handshakeState = nil sendCipher = nil receiveCipher = nil sentHandshakeMessages.removeAll() handshakeHash = nil + + if wasEstablished { + SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID)) + } } } } @@ -255,6 +264,9 @@ class NoiseSessionManager { func removeSession(for peerID: String) { managerQueue.sync(flags: .barrier) { + if let session = sessions[peerID], session.isEstablished() { + SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID)) + } _ = sessions.removeValue(forKey: peerID) } } @@ -267,7 +279,7 @@ class NoiseSessionManager { sessions[newPeerID] = session _ = sessions.removeValue(forKey: oldPeerID) - SecurityLogger.log("Migrated Noise session from \(oldPeerID) to \(newPeerID)", category: SecurityLogger.noise, level: .info) + SecureLogger.log("Migrated Noise session from \(oldPeerID) to \(newPeerID)", category: SecureLogger.noise, level: .info) } } } @@ -307,6 +319,7 @@ class NoiseSessionManager { } catch { // Clean up failed session _ = sessions.removeValue(forKey: peerID) + SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription), level: .error) throw error } } @@ -323,6 +336,7 @@ class NoiseSessionManager { if existing.isEstablished() { // Don't destroy our working session just because the other side is confused // They should detect the established session through successful message exchange + SecureLogger.log("Rejecting handshake attempt - session already established with \(peerID)", category: SecureLogger.session, level: .debug) throw NoiseSessionError.alreadyEstablished } else { // If we're in the middle of a handshake and receive a new initiation, @@ -376,6 +390,7 @@ class NoiseSessionManager { self?.onSessionFailed?(peerID, error) } + SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: error.localizedDescription), level: .error) throw error } } @@ -428,6 +443,8 @@ class NoiseSessionManager { } func initiateRekey(for peerID: String) throws { + SecureLogger.logSecurityEvent(.keyRotation(channel: peerID)) + // Remove old session removeSession(for: peerID) diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index f26072c2..f798cd4f 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -299,6 +299,7 @@ class BluetoothMeshService: NSObject { // Remove old peer ID mapping if exists if let existingPeerID = self.fingerprintToPeerID[fingerprint], existingPeerID != newPeerID { oldPeerID = existingPeerID + SecureLogger.log("Peer ID rotation detected: \(existingPeerID) -> \(newPeerID) for fingerprint \(fingerprint)", category: SecureLogger.security, level: .info) self.peerIDToFingerprint.removeValue(forKey: existingPeerID) @@ -421,6 +422,8 @@ class BluetoothMeshService: NSObject { // Generate new peer ID self.myPeerID = self.generateNewPeerID() + SecureLogger.log("Peer ID rotated from \(oldID) to \(self.myPeerID)", category: SecureLogger.security, level: .info) + // Update advertising with new peer ID DispatchQueue.main.async { [weak self] in self?.updateAdvertisement() @@ -904,7 +907,6 @@ class BluetoothMeshService: NSObject { // Encode the ACK let ackData = ack.toBinaryData() - print("📤 Sending delivery ACK for message \(ack.originalMessageID) to \(recipientID) - binary size: \(ackData.count)") // Check if we have a Noise session with this peer // Use noiseService directly @@ -933,8 +935,7 @@ class BluetoothMeshService: NSObject { self.broadcastPacket(outerPacket) } catch { - SecurityLogger.log("Failed to encrypt delivery ACK via Noise for \(recipientID): \(error)", - category: SecurityLogger.encryption, level: .error) + SecureLogger.logError(error, context: "Failed to encrypt delivery ACK via Noise for \(recipientID)", category: SecureLogger.encryption) } } else { // Fall back to legacy encryption @@ -942,8 +943,7 @@ class BluetoothMeshService: NSObject { do { encryptedPayload = try self.noiseService.encrypt(ackData, for: recipientID) } catch { - SecurityLogger.log("Failed to encrypt delivery ACK for \(recipientID): \(error)", - category: SecurityLogger.encryption, level: .error) + SecureLogger.logError(error, context: "Failed to encrypt delivery ACK for \(recipientID)", category: SecureLogger.encryption) return } @@ -1005,8 +1005,7 @@ class BluetoothMeshService: NSObject { self.broadcastPacket(outerPacket) } } catch { - SecurityLogger.log("Failed to encrypt read receipt via Noise for \(recipientID): \(error)", - category: SecurityLogger.encryption, level: .error) + SecureLogger.logError(error, context: "Failed to encrypt read receipt via Noise for \(recipientID)", category: SecureLogger.encryption) } } else { // Fall back to legacy encryption @@ -1203,6 +1202,8 @@ class BluetoothMeshService: NSObject { // Emergency disconnect for panic situations func emergencyDisconnectAll() { + SecureLogger.logSecurityEvent(.invalidKey(reason: "Emergency disconnect triggered"), level: .warning) + // Stop advertising immediately if peripheralManager?.isAdvertising == true { peripheralManager?.stopAdvertising() @@ -1214,7 +1215,8 @@ class BluetoothMeshService: NSObject { scanDutyCycleTimer = nil // Disconnect all peripherals - for (_, peripheral) in connectedPeripherals { + for (peerID, peripheral) in connectedPeripherals { + SecureLogger.log("Emergency disconnect peer: \(peerID)", category: SecureLogger.session, level: .warning) centralManager?.cancelPeripheralConnection(peripheral) } @@ -1662,6 +1664,8 @@ class BluetoothMeshService: NSObject { let currentTime = UInt64(Date().timeIntervalSince1970 * 1000) // milliseconds let timeDiff = abs(Int64(currentTime) - Int64(packet.timestamp)) if timeDiff > 300000 { // 5 minutes in milliseconds + SecureLogger.logSecurityEvent(.replayAttackDetected(channel: "timestamp-\(senderID)"), level: .warning) + SecureLogger.log("Dropped message with stale timestamp. Age: \(timeDiff/1000)s from \(senderID)", category: SecureLogger.security, level: .warning) return } @@ -1681,9 +1685,11 @@ class BluetoothMeshService: NSObject { if messageBloomFilter.contains(messageID) { // Also check exact set for accuracy (bloom filter can have false positives) if processedMessages.contains(messageID) { + SecureLogger.log("Dropped duplicate message: \(messageID.prefix(20))... from \(senderID)", category: SecureLogger.security, level: .debug) return } else { // False positive from Bloom filter + SecureLogger.log("Bloom filter false positive for message: \(messageID.prefix(20))...", category: SecureLogger.security, level: .debug) } } @@ -1800,11 +1806,14 @@ class BluetoothMeshService: NSObject { Double.random(in: 0...1) < relayProb if shouldRelay { + SecureLogger.log("Relaying broadcast from \(senderID), TTL: \(relayPacket.ttl), peers: \(self.activePeers.count)", category: SecureLogger.noise, level: .debug) // Add random delay to prevent collision storms let delay = Double.random(in: minMessageDelay...maxMessageDelay) DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in self?.broadcastPacket(relayPacket) } + } else { + SecureLogger.log("Dropped broadcast relay from \(senderID), TTL: \(relayPacket.ttl), prob: \(relayProb)", category: SecureLogger.noise, level: .debug) } } @@ -1875,12 +1884,10 @@ class BluetoothMeshService: NSObject { myNickname: myNickname, hopCount: UInt8(self.maxTTL - packet.ttl) ) { - print("💬 Generating delivery ACK for private message \(messageWithPeerID.id) from \(senderID)") self.sendDeliveryAck(ack, to: senderID) } } else { - SecurityLogger.log("Failed to parse private message from binary payload, payload size: \(decryptedPayload.count)", - category: SecurityLogger.encryption, level: .error) + SecureLogger.log("Failed to parse private message from binary, size: \(decryptedPayload.count)", category: SecureLogger.encryption, level: .error) } } else if packet.ttl > 0 { @@ -1918,8 +1925,7 @@ class BluetoothMeshService: NSObject { } } else { // No recipient ID - this shouldn't happen for messages - SecurityLogger.log("Message packet with no recipient ID from \(senderID)", - category: SecurityLogger.encryption, level: .warning) + SecureLogger.log("Message packet with no recipient ID from \(senderID)", category: SecureLogger.security, level: .warning) } // Note: 0x02 was legacy keyExchange - removed @@ -2052,7 +2058,7 @@ class BluetoothMeshService: NSObject { let wasInserted = collectionsQueue.sync(flags: .barrier) { // Final safety check if senderID == self.myPeerID { - SecurityLogger.log("Blocked self from being added to activePeers", category: SecurityLogger.noise, level: .error) + SecureLogger.log("Blocked self from being added to activePeers", category: SecureLogger.noise, level: .error) return false } let result = self.activePeers.insert(senderID).inserted @@ -2236,8 +2242,8 @@ class BluetoothMeshService: NSObject { } } } catch { - SecurityLogger.log("Failed to decrypt delivery ACK from \(senderID): \(error)", - category: SecurityLogger.encryption, level: .error) + SecureLogger.log("Failed to decrypt delivery ACK from \(senderID): \(error)", + category: SecureLogger.encryption, level: .error) } } } else if packet.ttl > 0 { @@ -2304,7 +2310,7 @@ class BluetoothMeshService: NSObject { let payloadCopy = Data(packet.payload) guard !payloadCopy.isEmpty else { - SecurityLogger.log("Received empty NoiseIdentityAnnouncement from \(senderID)", category: SecurityLogger.noise, level: .error) + SecureLogger.log("Received empty NoiseIdentityAnnouncement from \(senderID)", category: SecureLogger.noise, level: .error) return } @@ -2317,7 +2323,7 @@ class BluetoothMeshService: NSObject { } guard let announcement = announcement else { - SecurityLogger.log("Failed to decode NoiseIdentityAnnouncement from \(senderID), size: \(payloadCopy.count)", category: SecurityLogger.noise, level: .error) + SecureLogger.log("Failed to decode NoiseIdentityAnnouncement from \(senderID), size: \(payloadCopy.count)", category: SecureLogger.noise, level: .error) return } @@ -2325,7 +2331,7 @@ class BluetoothMeshService: NSObject { let timestampData = String(Int64(announcement.timestamp.timeIntervalSince1970 * 1000)).data(using: .utf8)! let bindingData = announcement.peerID.data(using: .utf8)! + announcement.publicKey + timestampData if !noiseService.verifySignature(announcement.signature, for: bindingData, publicKey: announcement.signingPublicKey) { - SecurityLogger.log("Signature verification failed for \(senderID)", category: SecurityLogger.noise, level: .warning) + SecureLogger.log("Signature verification failed for \(senderID)", category: SecureLogger.noise, level: .warning) return // Reject announcements with invalid signatures } @@ -2344,6 +2350,8 @@ class BluetoothMeshService: NSObject { signature: announcement.signature ) + SecureLogger.log("Creating identity binding for \(announcement.peerID) -> \(fingerprint)", category: SecureLogger.security, level: .info) + // Update our mappings updatePeerBinding(announcement.peerID, fingerprint: fingerprint, binding: binding) @@ -2379,7 +2387,7 @@ class BluetoothMeshService: NSObject { case .noiseHandshakeInit: // Handle incoming Noise handshake initiation let senderID = packet.senderID.hexEncodedString() - print("🤝 Received Noise handshake init from \(senderID), payload size: \(packet.payload.count)") + SecureLogger.logHandshake("initiation received", peerID: senderID, success: true) // Check if this handshake is for us or broadcast if let recipientID = packet.recipientID, @@ -2403,8 +2411,8 @@ class BluetoothMeshService: NSObject { // Check if we've completed version negotiation with this peer if negotiatedVersions[senderID] == nil { // Legacy peer - assume version 1 for backward compatibility - SecurityLogger.log("Received Noise handshake from \(senderID) without version negotiation, assuming v1", - category: SecurityLogger.session, level: .debug) + SecureLogger.log("Received Noise handshake from \(senderID) without version negotiation, assuming v1", + category: SecureLogger.session, level: .debug) negotiatedVersions[senderID] = 1 versionNegotiationState[senderID] = .ackReceived(version: 1) } @@ -2414,7 +2422,7 @@ class BluetoothMeshService: NSObject { case .noiseHandshakeResp: // Handle Noise handshake response let senderID = packet.senderID.hexEncodedString() - print("🤝 Received Noise handshake response from \(senderID)") + SecureLogger.logHandshake("response received", peerID: senderID, success: true) // Check if this handshake response is for us if let recipientID = packet.recipientID { @@ -2769,7 +2777,7 @@ extension BluetoothMeshService: CBCentralManagerDelegate { peerRSSI[peerID] = RSSI // Discovered potential peer - SecurityLogger.log("Discovered peer with ID: \(peerID), self ID: \(myPeerID)", category: SecurityLogger.noise, level: .debug) + SecureLogger.log("Discovered peer with ID: \(peerID), self ID: \(myPeerID)", category: SecureLogger.noise, level: .debug) } // Connection pooling with exponential backoff @@ -2821,12 +2829,13 @@ extension BluetoothMeshService: CBCentralManagerDelegate { } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + let tempID = peripheral.identifier.uuidString + SecureLogger.log("Peripheral connected: \(tempID)", category: SecureLogger.session, level: .info) peripheral.delegate = self peripheral.discoverServices([BluetoothMeshService.serviceUUID]) // Store peripheral by its system ID temporarily until we get the real peer ID - let tempID = peripheral.identifier.uuidString connectedPeripherals[tempID] = peripheral // Connected to peripheral @@ -2850,10 +2859,18 @@ extension BluetoothMeshService: CBCentralManagerDelegate { // Check if this was an intentional disconnect if intentionalDisconnects.contains(peripheralID) { intentionalDisconnects.remove(peripheralID) + SecureLogger.log("Intentional disconnect: \(peripheralID)", category: SecureLogger.session, level: .debug) // Don't process this disconnect further return } + // Log disconnect with error if present + if let error = error { + SecureLogger.logError(error, context: "Peripheral disconnected: \(peripheralID)", category: SecureLogger.session) + } else { + SecureLogger.log("Peripheral disconnected normally: \(peripheralID)", category: SecureLogger.session, level: .info) + } + // Implement exponential backoff for failed connections if error != nil { let attempts = connectionAttempts[peripheralID] ?? 0 @@ -2930,8 +2947,8 @@ extension BluetoothMeshService: CBCentralManagerDelegate { extension BluetoothMeshService: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error = error { - SecurityLogger.log("Error discovering services: \(error)", - category: SecurityLogger.encryption, level: .error) + SecureLogger.log("Error discovering services: \(error)", + category: SecureLogger.encryption, level: .error) return } @@ -2945,8 +2962,8 @@ extension BluetoothMeshService: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { - SecurityLogger.log("Error discovering characteristics: \(error)", - category: SecurityLogger.encryption, level: .error) + SecureLogger.log("Error discovering characteristics: \(error)", + category: SecureLogger.encryption, level: .error) return } @@ -3146,7 +3163,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { if peerID != "unknown" && peerID != myPeerID { // Double-check we're not adding ourselves if peerID == self.myPeerID { - SecurityLogger.log("Preventing self from being added as peer (peripheral manager)", category: SecurityLogger.noise, level: .warning) + SecureLogger.log("Preventing self from being added as peer (peripheral manager)", category: SecureLogger.noise, level: .warning) peripheral.respond(to: request, withResult: .success) return } @@ -3411,11 +3428,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { private func initiateNoiseHandshake(with peerID: String) { // Use noiseService directly - SecurityLogger.log("Initiating Noise handshake with \(peerID)", category: SecurityLogger.noise, level: .info) + SecureLogger.log("Initiating Noise handshake with \(peerID)", category: SecureLogger.noise, level: .info) // Check if we already have an established session if noiseService.hasEstablishedSession(with: peerID) { - SecurityLogger.log("Already have established session with \(peerID)", category: SecurityLogger.noise, level: .debug) + SecureLogger.log("Already have established session with \(peerID)", category: SecureLogger.noise, level: .debug) // Clear any lingering handshake attempt time handshakeAttemptTimes.removeValue(forKey: peerID) return @@ -3424,7 +3441,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // Check if we've recently tried to handshake with this peer if let lastAttempt = handshakeAttemptTimes[peerID], Date().timeIntervalSince(lastAttempt) < handshakeTimeout { - SecurityLogger.log("Skipping handshake with \(peerID) - too recent", category: SecurityLogger.noise, level: .debug) + SecureLogger.log("Skipping handshake with \(peerID) - too recent", category: SecureLogger.noise, level: .debug) return } @@ -3434,6 +3451,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { do { // Generate handshake initiation message let handshakeData = try noiseService.initiateHandshake(with: peerID) + SecureLogger.logHandshake("initiated", peerID: peerID, success: true) // Send handshake initiation let packet = BitchatPacket( @@ -3458,12 +3476,11 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { private func handleNoiseHandshakeMessage(from peerID: String, message: Data, isInitiation: Bool) { // Use noiseService directly - print("🤝 handleNoiseHandshakeMessage from \(peerID), isInitiation: \(isInitiation), messageSize: \(message.count)") + SecureLogger.logHandshake("processing \(isInitiation ? "init" : "response")", peerID: peerID, success: true) do { // Process handshake message if let response = try noiseService.processHandshakeMessage(from: peerID, message: message) { - print("🤝 Got response from processHandshakeMessage, size: \(response.count)") // Always send responses as handshake response type let packet = BitchatPacket( type: MessageType.noiseHandshakeResp.rawValue, @@ -3483,7 +3500,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // Check if handshake is complete if noiseService.hasEstablishedSession(with: peerID) { - print("🎉 Handshake completed with peer: \(peerID)") + SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID)) // Unlock rotation now that handshake is complete unlockRotation() @@ -3665,32 +3682,33 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // Safety check for empty data guard !dataCopy.isEmpty else { - SecurityLogger.log("Received empty version hello data from \(peerID)", category: SecurityLogger.session, level: .error) + SecureLogger.log("Received empty version hello data from \(peerID)", category: SecureLogger.session, level: .error) return } // Try JSON first if it looks like JSON let hello: VersionHello? if let firstByte = dataCopy.first, firstByte == 0x7B { // '{' character - SecurityLogger.log("Version hello from \(peerID) appears to be JSON (size: \(dataCopy.count))", category: SecurityLogger.session, level: .debug) + SecureLogger.log("Version hello from \(peerID) appears to be JSON (size: \(dataCopy.count))", category: SecureLogger.session, level: .debug) hello = VersionHello.decode(from: dataCopy) ?? VersionHello.fromBinaryData(dataCopy) } else { - SecurityLogger.log("Version hello from \(peerID) appears to be binary (size: \(dataCopy.count), first byte: \(dataCopy.first?.description ?? "nil"))", category: SecurityLogger.session, level: .debug) + SecureLogger.log("Version hello from \(peerID) appears to be binary (size: \(dataCopy.count), first byte: \(dataCopy.first?.description ?? "nil"))", category: SecureLogger.session, level: .debug) hello = VersionHello.fromBinaryData(dataCopy) ?? VersionHello.decode(from: dataCopy) } guard let hello = hello else { - SecurityLogger.log("Failed to decode version hello from \(peerID)", category: SecurityLogger.session, level: .error) + SecureLogger.log("Failed to decode version hello from \(peerID)", category: SecureLogger.session, level: .error) return } - SecurityLogger.log("Received version hello from \(peerID): supported versions \(hello.supportedVersions), preferred \(hello.preferredVersion)", - category: SecurityLogger.session, level: .debug) + SecureLogger.log("Received version hello from \(peerID): supported versions \(hello.supportedVersions), preferred \(hello.preferredVersion)", + category: SecureLogger.session, level: .debug) // Find the best common version let ourVersions = Array(ProtocolVersion.supportedVersions) if let agreedVersion = ProtocolVersion.negotiateVersion(clientVersions: hello.supportedVersions, serverVersions: ourVersions) { // We can communicate! Send ACK + SecureLogger.log("Version negotiation agreed with \(peerID): v\(agreedVersion) (client: \(hello.clientVersion), platform: \(hello.platform))", category: SecureLogger.session, level: .info) negotiatedVersions[peerID] = agreedVersion versionNegotiationState[peerID] = .ackReceived(version: agreedVersion) @@ -3709,6 +3727,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } } else { // No compatible version + SecureLogger.log("Version negotiation failed with \(peerID): No compatible version (client supports: \(hello.supportedVersions))", category: SecureLogger.session, level: .warning) versionNegotiationState[peerID] = .failed(reason: "No compatible protocol version") let ack = VersionAck( @@ -3736,7 +3755,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // Safety check for empty data guard !dataCopy.isEmpty else { - SecurityLogger.log("Received empty version ack data from \(peerID)", category: SecurityLogger.session, level: .error) + SecureLogger.log("Received empty version ack data from \(peerID)", category: SecureLogger.session, level: .error) return } @@ -3749,13 +3768,13 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { } guard let ack = ack else { - SecurityLogger.log("Failed to decode version ack from \(peerID)", category: SecurityLogger.session, level: .error) + SecureLogger.log("Failed to decode version ack from \(peerID)", category: SecureLogger.session, level: .error) return } if ack.rejected { - SecurityLogger.log("Version negotiation rejected by \(peerID): \(ack.reason ?? "Unknown reason")", - category: SecurityLogger.session, level: .error) + SecureLogger.log("Version negotiation rejected by \(peerID): \(ack.reason ?? "Unknown reason")", + category: SecureLogger.session, level: .error) versionNegotiationState[peerID] = .failed(reason: ack.reason ?? "Version rejected") // Clean up state for incompatible peer @@ -3773,8 +3792,8 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { self?.delegate?.didDisconnectFromPeer(peerID) } } else { - SecurityLogger.log("Version negotiation successful with \(peerID): agreed on version \(ack.agreedVersion)", - category: SecurityLogger.session, level: .debug) + SecureLogger.log("Version negotiation successful with \(peerID): agreed on v\(ack.agreedVersion) (server: \(ack.serverVersion), platform: \(ack.platform))", + category: SecureLogger.session, level: .info) negotiatedVersions[peerID] = ack.agreedVersion versionNegotiationState[peerID] = .ackReceived(version: ack.agreedVersion) @@ -3987,7 +4006,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { // Check if we have a Noise session with this peer if !noiseService.hasEstablishedSession(with: recipientPeerID) { - SecurityLogger.log("No Noise session with \(recipientPeerID), initiating handshake", category: SecurityLogger.noise, level: .info) + SecureLogger.log("No Noise session with \(recipientPeerID), initiating handshake", category: SecureLogger.noise, level: .info) // Apply tie-breaker logic for handshake initiation if myPeerID < recipientPeerID { @@ -4005,7 +4024,7 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate { self.pendingPrivateMessages[recipientPeerID] = [] } self.pendingPrivateMessages[recipientPeerID]?.append((content, recipientNickname, messageID ?? UUID().uuidString)) - SecurityLogger.log("Queued private message for \(recipientPeerID), \(self.pendingPrivateMessages[recipientPeerID]?.count ?? 0) messages pending", category: SecurityLogger.noise, level: .info) + SecureLogger.log("Queued private message for \(recipientPeerID), \(self.pendingPrivateMessages[recipientPeerID]?.count ?? 0) messages pending", category: SecureLogger.noise, level: .info) } return } diff --git a/bitchat/Services/KeychainManager.swift b/bitchat/Services/KeychainManager.swift index f0144677..f77d92af 100644 --- a/bitchat/Services/KeychainManager.swift +++ b/bitchat/Services/KeychainManager.swift @@ -58,6 +58,7 @@ class KeychainManager { // Save to new service if saveIdentityKey(data, forKey: "noiseStaticKey") { migratedItems += 1 + SecureLogger.logKeyOperation("migrate", keyType: "noiseStaticKey", success: true) } // Delete from old service let deleteQuery: [String: Any] = [ @@ -98,7 +99,9 @@ class KeychainManager { func saveChannelPassword(_ password: String, for channel: String) -> Bool { let key = "channel_\(channel)" - return save(password, forKey: key) + let result = save(password, forKey: key) + SecureLogger.logKeyOperation("save", keyType: "channel password for \(channel)", success: result) + return result } func getChannelPassword(for channel: String) -> String? { @@ -108,7 +111,9 @@ class KeychainManager { func deleteChannelPassword(for channel: String) -> Bool { let key = "channel_\(channel)" - return delete(forKey: key) + let result = delete(forKey: key) + SecureLogger.logKeyOperation("delete", keyType: "channel password for \(channel)", success: result) + return result } func getAllChannelPasswords() -> [String: String] { @@ -150,7 +155,9 @@ class KeychainManager { func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool { let fullKey = "identity_\(key)" - return saveData(keyData, forKey: fullKey) + let result = saveData(keyData, forKey: fullKey) + SecureLogger.logKeyOperation("save", keyType: key, success: result) + return result } func getIdentityKey(forKey key: String) -> Data? { @@ -159,7 +166,9 @@ class KeychainManager { } func deleteIdentityKey(forKey key: String) -> Bool { - return delete(forKey: "identity_\(key)") + let result = delete(forKey: "identity_\(key)") + SecureLogger.logKeyOperation("delete", keyType: key, success: result) + return result } // MARK: - Generic Operations @@ -201,9 +210,9 @@ class KeychainManager { if status == errSecSuccess { return true } else if status == -34018 { - SecurityLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecurityLogger.keychain) + SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecureLogger.keychain) } else if status != errSecDuplicateItem { - SecurityLogger.logError(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: SecurityLogger.keychain) + SecureLogger.logError(NSError(domain: "Keychain", code: Int(status)), context: "Error saving to keychain", category: SecureLogger.keychain) } return false @@ -234,7 +243,7 @@ class KeychainManager { if status == errSecSuccess { return result as? Data } else if status == -34018 { - SecurityLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecurityLogger.keychain) + SecureLogger.logError(NSError(domain: "Keychain", code: -34018), context: "Missing keychain entitlement", category: SecureLogger.keychain) } return nil @@ -281,6 +290,7 @@ class KeychainManager { // Delete ALL keychain data for panic mode func deleteAllKeychainData() -> Bool { + SecureLogger.logSecurityEvent(.invalidKey(reason: "Panic mode - deleting all keychain data"), level: .warning) var totalDeleted = 0 @@ -328,6 +338,7 @@ class KeychainManager { let deleteStatus = SecItemDelete(deleteQuery as CFDictionary) if deleteStatus == errSecSuccess { totalDeleted += 1 + SecureLogger.log("Deleted keychain item: \(account) from \(service)", category: SecureLogger.keychain, level: .info) } } } @@ -357,6 +368,7 @@ class KeychainManager { } } + SecureLogger.log("Panic mode cleanup completed. Total items deleted: \(totalDeleted)", category: SecureLogger.keychain, level: .warning) return totalDeleted > 0 } diff --git a/bitchat/Services/NoiseEncryptionService.swift b/bitchat/Services/NoiseEncryptionService.swift index d000a190..5fe6a524 100644 --- a/bitchat/Services/NoiseEncryptionService.swift +++ b/bitchat/Services/NoiseEncryptionService.swift @@ -53,6 +53,7 @@ class NoiseEncryptionService { if let identityData = KeychainManager.shared.getIdentityKey(forKey: "noiseStaticKey"), let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: identityData) { loadedKey = key + SecureLogger.logKeyOperation("load", keyType: "noiseStaticKey", success: true) } // If no identity exists, create new one else { @@ -60,7 +61,8 @@ class NoiseEncryptionService { let keyData = loadedKey.rawRepresentation // Save to keychain - _ = KeychainManager.shared.saveIdentityKey(keyData, forKey: "noiseStaticKey") + let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "noiseStaticKey") + SecureLogger.logKeyOperation("create", keyType: "noiseStaticKey", success: saved) } // Now assign the final value @@ -74,6 +76,7 @@ class NoiseEncryptionService { if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"), let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) { loadedSigningKey = key + SecureLogger.logKeyOperation("load", keyType: "ed25519SigningKey", success: true) } // If no signing key exists, create new one else { @@ -81,7 +84,8 @@ class NoiseEncryptionService { let keyData = loadedSigningKey.rawRepresentation // Save to keychain - _ = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey") + let saved = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey") + SecureLogger.logKeyOperation("create", keyType: "ed25519SigningKey", success: saved) } // Now assign the signing keys @@ -126,8 +130,10 @@ class NoiseEncryptionService { /// Clear persistent identity (for panic mode) func clearPersistentIdentity() { // Clear from keychain - _ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey") - _ = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey") + let deletedStatic = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey") + let deletedSigning = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey") + SecureLogger.logKeyOperation("delete", keyType: "identity keys", success: deletedStatic && deletedSigning) + SecureLogger.logSecurityEvent(.invalidKey(reason: "Panic mode activated - identity cleared"), level: .warning) // Stop rekey timer stopRekeyTimer() } @@ -138,7 +144,7 @@ class NoiseEncryptionService { let signature = try signingKey.signature(for: data) return signature } catch { - SecurityLogger.logError(error, context: "Failed to sign data", category: SecurityLogger.noise) + SecureLogger.logError(error, context: "Failed to sign data", category: SecureLogger.noise) return nil } } @@ -149,7 +155,7 @@ class NoiseEncryptionService { let signingPublicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey) return signingPublicKey.isValidSignature(signature, for: data) } catch { - SecurityLogger.logError(error, context: "Failed to verify signature", category: SecurityLogger.noise) + SecureLogger.logError(error, context: "Failed to verify signature", category: SecureLogger.noise) return false } } @@ -161,14 +167,18 @@ class NoiseEncryptionService { // Validate peer ID guard NoiseSecurityValidator.validatePeerID(peerID) else { + SecureLogger.logSecurityEvent(.authenticationFailed(peerID: peerID), level: .warning) throw NoiseSecurityError.invalidPeerID } // Check rate limit guard rateLimiter.allowHandshake(from: peerID) else { + SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Rate limited: \(peerID)"), level: .warning) throw NoiseSecurityError.rateLimitExceeded } + SecureLogger.logSecurityEvent(.handshakeStarted(peerID: peerID)) + // Return raw handshake data without wrapper // The Noise protocol handles its own message format let handshakeData = try sessionManager.initiateHandshake(with: peerID) @@ -180,16 +190,19 @@ class NoiseEncryptionService { // Validate peer ID guard NoiseSecurityValidator.validatePeerID(peerID) else { + SecureLogger.logSecurityEvent(.authenticationFailed(peerID: peerID), level: .warning) throw NoiseSecurityError.invalidPeerID } // Validate message size guard NoiseSecurityValidator.validateHandshakeMessageSize(message) else { + SecureLogger.logSecurityEvent(.handshakeFailed(peerID: peerID, error: "Message too large"), level: .warning) throw NoiseSecurityError.messageTooLarge } // Check rate limit guard rateLimiter.allowHandshake(from: peerID) else { + SecureLogger.logSecurityEvent(.authenticationFailed(peerID: "Rate limited: \(peerID)"), level: .warning) throw NoiseSecurityError.rateLimitExceeded } @@ -282,6 +295,8 @@ class NoiseEncryptionService { } peerFingerprints.removeValue(forKey: peerID) } + + SecureLogger.logSecurityEvent(.sessionExpired(peerID: peerID)) } /// Migrate session when peer ID changes @@ -315,7 +330,7 @@ class NoiseEncryptionService { } // Log security event - SecurityLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID)) + SecureLogger.logSecurityEvent(.handshakeCompleted(peerID: peerID)) // Notify about authentication onPeerAuthenticated?(peerID, fingerprint) @@ -332,15 +347,18 @@ class NoiseEncryptionService { func setChannelPassword(_ password: String, for channel: String) { // Validate channel name guard NoiseSecurityValidator.validateChannelName(channel) else { + SecureLogger.log("Invalid channel name for password", category: SecureLogger.security, level: .warning) return } // Validate password is not empty guard !password.isEmpty else { + SecureLogger.log("Empty password rejected for channel", category: SecureLogger.security, level: .warning) return } channelEncryption.setChannelPassword(password, for: channel) + SecureLogger.logKeyOperation("set", keyType: "channel password", success: true) } /// Load channel password from keychain @@ -406,11 +424,12 @@ class NoiseEncryptionService { // Attempt to rekey the session do { try sessionManager.initiateRekey(for: peerID) + SecureLogger.logSecurityEvent(.keyRotation(channel: peerID)) // Signal that handshake is needed onHandshakeRequired?(peerID) } catch { - SecurityLogger.logError(error, context: "Failed to initiate rekey for peer", category: SecurityLogger.session) + SecureLogger.logError(error, context: "Failed to initiate rekey for peer: \(peerID)", category: SecureLogger.session) } } } diff --git a/bitchat/Utils/SecurityLogger.swift b/bitchat/Utils/SecureLogger.swift similarity index 63% rename from bitchat/Utils/SecurityLogger.swift rename to bitchat/Utils/SecureLogger.swift index f501e0b6..7864020c 100644 --- a/bitchat/Utils/SecurityLogger.swift +++ b/bitchat/Utils/SecureLogger.swift @@ -1,5 +1,5 @@ // -// SecurityLogger.swift +// SecureLogger.swift // bitchat // // This is free and unencumbered software released into the public domain. @@ -11,7 +11,7 @@ import os.log /// Centralized security-aware logging framework /// Provides safe logging that filters sensitive data and security events -class SecurityLogger { +class SecureLogger { // MARK: - Log Categories @@ -23,6 +23,22 @@ class SecurityLogger { static let session = OSLog(subsystem: subsystem, category: "session") static let security = OSLog(subsystem: subsystem, category: "security") + // MARK: - Cached Regex Patterns + + private static let fingerprintPattern = #/[a-fA-F0-9]{64}/# + private static let base64Pattern = #/[A-Za-z0-9+/]{40,}={0,2}/# + private static let passwordPattern = #/password["\s:=]+["']?[^"'\s]+["']?/# + private static let peerIDPattern = #/peerID: ([a-zA-Z0-9]{8})[a-zA-Z0-9]+/# + + // MARK: - Sanitization Cache + + private static let sanitizationCache: NSCache = { + let cache = NSCache() + cache.countLimit = 100 // Keep last 100 sanitized strings + return cache + }() + private static let cacheQueue = DispatchQueue(label: "chat.bitchat.securelogger.cache", attributes: .concurrent) + // MARK: - Log Levels enum LogLevel { @@ -80,18 +96,24 @@ class SecurityLogger { // MARK: - Public Logging Methods /// Log a security event - static func logSecurityEvent(_ event: SecurityEvent, level: LogLevel = .info) { + static func logSecurityEvent(_ event: SecurityEvent, level: LogLevel = .info, + file: String = #file, line: Int = #line, function: String = #function) { + let location = formatLocation(file: file, line: line, function: function) + let message = "\(location) \(event.message)" + #if DEBUG - os_log("%{public}@", log: security, type: level.osLogType, event.message) + os_log("%{public}@", log: security, type: level.osLogType, message) #else // In release, use private logging to prevent sensitive data exposure - os_log("%{private}@", log: security, type: level.osLogType, event.message) + os_log("%{private}@", log: security, type: level.osLogType, message) #endif } /// Log general messages with automatic sensitive data filtering - static func log(_ message: String, category: OSLog = noise, level: LogLevel = .debug) { - let sanitized = sanitize(message) + static func log(_ message: String, category: OSLog = noise, level: LogLevel = .debug, + file: String = #file, line: Int = #line, function: String = #function) { + let location = formatLocation(file: file, line: line, function: function) + let sanitized = sanitize("\(location) \(message)") #if DEBUG os_log("%{public}@", log: category, type: level.osLogType, sanitized) @@ -104,48 +126,70 @@ class SecurityLogger { } /// Log errors with context - static func logError(_ error: Error, context: String, category: OSLog = noise) { + static func logError(_ error: Error, context: String, category: OSLog = noise, + file: String = #file, line: Int = #line, function: String = #function) { + let location = formatLocation(file: file, line: line, function: function) let sanitized = sanitize(context) let errorDesc = sanitize(error.localizedDescription) #if DEBUG - os_log("Error in %{public}@: %{public}@", log: category, type: .error, sanitized, errorDesc) + os_log("%{public}@ Error in %{public}@: %{public}@", log: category, type: .error, location, sanitized, errorDesc) #else - os_log("Error in %{private}@: %{private}@", log: category, type: .error, sanitized, errorDesc) + os_log("%{private}@ Error in %{private}@: %{private}@", log: category, type: .error, location, sanitized, errorDesc) #endif } // MARK: - Private Helpers + /// Format location information for logging + private static func formatLocation(file: String, line: Int, function: String) -> String { + let fileName = (file as NSString).lastPathComponent + return "[\(fileName):\(line) \(function)]" + } + /// Sanitize strings to remove potentially sensitive data private static func sanitize(_ input: String) -> String { + let key = input as NSString + + // Check cache first + var cachedValue: String? + cacheQueue.sync { + cachedValue = sanitizationCache.object(forKey: key) as String? + } + + if let cached = cachedValue { + return cached + } + + // Perform sanitization var sanitized = input // Remove full fingerprints (keep first 8 chars for debugging) - let fingerprintPattern = #/[a-fA-F0-9]{64}/# sanitized = sanitized.replacing(fingerprintPattern) { match in let fingerprint = String(match.output) return String(fingerprint.prefix(8)) + "..." } // Remove base64 encoded data that might be keys - let base64Pattern = #/[A-Za-z0-9+/]{40,}={0,2}/# sanitized = sanitized.replacing(base64Pattern) { _ in "" } // Remove potential passwords (assuming they're in quotes or after "password:") - let passwordPattern = #/password["\s:=]+["']?[^"'\s]+["']?/# sanitized = sanitized.replacing(passwordPattern) { _ in "password: " } // Truncate peer IDs to first 8 characters - let peerIDPattern = #/peerID: ([a-zA-Z0-9]{8})[a-zA-Z0-9]+/# sanitized = sanitized.replacing(peerIDPattern) { match in "peerID: \(match.1)..." } + // Cache the result + cacheQueue.async(flags: .barrier) { + sanitizationCache.setObject(sanitized as NSString, forKey: key) + } + return sanitized } @@ -158,39 +202,45 @@ class SecurityLogger { // MARK: - Convenience Extensions -extension SecurityLogger { +extension SecureLogger { /// Log handshake events - static func logHandshake(_ phase: String, peerID: String, success: Bool = true) { + static func logHandshake(_ phase: String, peerID: String, success: Bool = true, + file: String = #file, line: Int = #line, function: String = #function) { if success { - log("Handshake \(phase) with peer: \(peerID)", category: session, level: .info) + log("Handshake \(phase) with peer: \(peerID)", category: session, level: .info, + file: file, line: line, function: function) } else { - log("Handshake \(phase) failed with peer: \(peerID)", category: session, level: .warning) + log("Handshake \(phase) failed with peer: \(peerID)", category: session, level: .warning, + file: file, line: line, function: function) } } /// Log encryption operations - static func logEncryption(_ operation: String, success: Bool = true) { + static func logEncryption(_ operation: String, success: Bool = true, + file: String = #file, line: Int = #line, function: String = #function) { let level: LogLevel = success ? .debug : .error log("Encryption operation '\(operation)' \(success ? "succeeded" : "failed")", - category: encryption, level: level) + category: encryption, level: level, file: file, line: line, function: function) } /// Log key management operations - static func logKeyOperation(_ operation: String, keyType: String, success: Bool = true) { + static func logKeyOperation(_ operation: String, keyType: String, success: Bool = true, + file: String = #file, line: Int = #line, function: String = #function) { let level: LogLevel = success ? .info : .error log("Key operation '\(operation)' for \(keyType) \(success ? "succeeded" : "failed")", - category: keychain, level: level) + category: keychain, level: level, file: file, line: line, function: function) } } // MARK: - Migration Helper -/// Helper to migrate from print statements to SecurityLogger +/// Helper to migrate from print statements to SecureLogger /// Usage: Replace print(...) with secureLog(...) -func secureLog(_ items: Any..., separator: String = " ", terminator: String = "\n") { +func secureLog(_ items: Any..., separator: String = " ", terminator: String = "\n", + file: String = #file, line: Int = #line, function: String = #function) { #if DEBUG let message = items.map { String(describing: $0) }.joined(separator: separator) - SecurityLogger.log(message, level: .debug) + SecureLogger.log(message, level: .debug, file: file, line: line, function: function) #endif } \ No newline at end of file diff --git a/bitchat/bitchat.entitlements b/bitchat/bitchat.entitlements index 80566616..610b08cb 100644 --- a/bitchat/bitchat.entitlements +++ b/bitchat/bitchat.entitlements @@ -8,5 +8,7 @@ group.chat.bitchat + com.apple.security.device.bluetooth + \ No newline at end of file diff --git a/bitchatTests/VersionNegotiationScenarioTests.swift b/bitchatTests/VersionNegotiationScenarioTests.swift index 7a35fc93..2dbd3ee6 100644 --- a/bitchatTests/VersionNegotiationScenarioTests.swift +++ b/bitchatTests/VersionNegotiationScenarioTests.swift @@ -41,7 +41,7 @@ class VersionNegotiationScenarioTests: XCTestCase { func testMixedVersionNetwork() { // Scenario: Network with mixed client versions - let clients = [ + let clients: [[UInt8]] = [ [1], // Old client [1, 2], // Mid-version client [1, 2, 3] // New client @@ -181,13 +181,13 @@ class VersionNegotiationScenarioTests: XCTestCase { let clientCapabilities = ["noise", "compression", "multipath"] let serverCapabilities = ["noise", "compression", "federation"] - let hello = VersionHello( + _ = VersionHello( clientVersion: "1.0.0", platform: "iOS", capabilities: clientCapabilities ) - let ack = VersionAck( + _ = VersionAck( agreedVersion: 1, serverVersion: "1.0.0", platform: "macOS",