Enhance logging framework and fix build issues

- Rename SecurityLogger to SecureLogger for better clarity
- Add file:line:function tracking to all log entries
- Optimize logging with pre-compiled regex patterns and NSCache
- Add comprehensive logging for critical protocol flow points
- Fix iOS entitlements to include Bluetooth permission
- Fix VersionHello field name and optional chaining issues
- Fix Package.swift resource warnings
- Fix test compilation errors with proper type annotations
This commit is contained in:
jack
2025-07-22 15:40:13 +02:00
parent 49daa995cc
commit aaa7e2bf28
13 changed files with 269 additions and 112 deletions
+8 -1
View File
@@ -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"
]
),
]
)
+6 -6
View File
@@ -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 = "<group>"; };
04636BB92E2FAA1700FBCFA8 /* BinaryMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryMessageHandler.swift; sourceTree = "<group>"; };
04636BBE2E2FCA8A00FBCFA8 /* SecureLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureLogger.swift; sourceTree = "<group>"; };
04891CA82E22971E0064A111 /* LRUCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
04AD0B502E2678220002A40A /* BinaryProtocolVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolVersionTests.swift; sourceTree = "<group>"; };
04AD0B512E2678220002A40A /* ProtocolVersionNegotiationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolVersionNegotiationTests.swift; sourceTree = "<group>"; };
@@ -179,7 +180,6 @@
04B6BA652E21601B0090FE39 /* NoiseSecurityValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseSecurityValidatorTests.swift; sourceTree = "<group>"; };
04B6BA662E21601B0090FE39 /* SecureNoiseSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureNoiseSessionTests.swift; sourceTree = "<group>"; };
04B6BA772E2166A50090FE39 /* NoiseTestingHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoiseTestingHelper.swift; sourceTree = "<group>"; };
04B6BA782E2166A50090FE39 /* SecurityLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityLogger.swift; sourceTree = "<group>"; };
12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryTracker.swift; sourceTree = "<group>"; };
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomFilterTests.swift; sourceTree = "<group>"; };
@@ -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;
@@ -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)
+16 -4
View File
@@ -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
}
+9 -3
View File
@@ -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
}
}
@@ -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
}
+18 -1
View File
@@ -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)
+69 -50
View File
@@ -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
}
+19 -7
View File
@@ -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
}
+27 -8
View File
@@ -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)
}
}
}
@@ -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<NSString, NSString> = {
let cache = NSCache<NSString, NSString>()
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
"<base64-data>"
}
// Remove potential passwords (assuming they're in quotes or after "password:")
let passwordPattern = #/password["\s:=]+["']?[^"'\s]+["']?/#
sanitized = sanitized.replacing(passwordPattern) { _ in
"password: <redacted>"
}
// 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
}
+2
View File
@@ -8,5 +8,7 @@
<array>
<string>group.chat.bitchat</string>
</array>
<key>com.apple.security.device.bluetooth</key>
<true/>
</dict>
</plist>
@@ -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",