mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-24 23:45:18 +00:00
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:
+8
-1
@@ -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"
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user