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
+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)