From b3ec5eeda0c879637dde698430dc767dd0a8d88a Mon Sep 17 00:00:00 2001 From: Islam <2553451+qalandarov@users.noreply.github.com> Date: Tue, 14 Oct 2025 23:32:44 +0100 Subject: [PATCH] Refactor Noise: Extract files and remove dead code (#806) * Extract each type to a separate file * NoiseSessionManager: Remove unused functions --------- Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com> --- bitchat/Noise/NoiseRateLimiter.swift | 95 ++++++++ .../Noise/NoiseSecurityConsiderations.swift | 227 ------------------ bitchat/Noise/NoiseSecurityConstants.swift | 37 +++ bitchat/Noise/NoiseSecurityError.swift | 18 ++ bitchat/Noise/NoiseSecurityValidator.swift | 22 ++ bitchat/Noise/NoiseSession.swift | 6 - bitchat/Noise/NoiseSessionManager.swift | 32 +-- bitchat/Noise/SecureNoiseSession.swift | 81 +++++++ bitchatTests/Noise/NoiseProtocolTests.swift | 8 +- 9 files changed, 259 insertions(+), 267 deletions(-) create mode 100644 bitchat/Noise/NoiseRateLimiter.swift delete mode 100644 bitchat/Noise/NoiseSecurityConsiderations.swift create mode 100644 bitchat/Noise/NoiseSecurityConstants.swift create mode 100644 bitchat/Noise/NoiseSecurityError.swift create mode 100644 bitchat/Noise/NoiseSecurityValidator.swift create mode 100644 bitchat/Noise/SecureNoiseSession.swift diff --git a/bitchat/Noise/NoiseRateLimiter.swift b/bitchat/Noise/NoiseRateLimiter.swift new file mode 100644 index 00000000..cfdd0543 --- /dev/null +++ b/bitchat/Noise/NoiseRateLimiter.swift @@ -0,0 +1,95 @@ +// +// NoiseRateLimiter.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import BitLogger +import Foundation + +final class NoiseRateLimiter { + private var handshakeTimestamps: [PeerID: [Date]] = [:] + private var messageTimestamps: [PeerID: [Date]] = [:] + + // Global rate limiting + private var globalHandshakeTimestamps: [Date] = [] + private var globalMessageTimestamps: [Date] = [] + + private let queue = DispatchQueue(label: "chat.bitchat.noise.ratelimit", attributes: .concurrent) + + func allowHandshake(from peerID: PeerID) -> Bool { + return queue.sync(flags: .barrier) { + let now = Date() + let oneMinuteAgo = now.addingTimeInterval(-60) + + // Check global rate limit first + globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo } + if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute { + SecureLogger.warning("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: .security) + return false + } + + // Check per-peer rate limit + var timestamps = handshakeTimestamps[peerID] ?? [] + timestamps = timestamps.filter { $0 > oneMinuteAgo } + + if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute { + SecureLogger.warning("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: .security) + return false + } + + // Record new handshake + timestamps.append(now) + handshakeTimestamps[peerID] = timestamps + globalHandshakeTimestamps.append(now) + return true + } + } + + func allowMessage(from peerID: PeerID) -> Bool { + return queue.sync(flags: .barrier) { + let now = Date() + let oneSecondAgo = now.addingTimeInterval(-1) + + // Check global rate limit first + globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo } + if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond { + SecureLogger.warning("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: .security) + return false + } + + // Check per-peer rate limit + var timestamps = messageTimestamps[peerID] ?? [] + timestamps = timestamps.filter { $0 > oneSecondAgo } + + if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond { + SecureLogger.warning("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: .security) + return false + } + + // Record new message + timestamps.append(now) + messageTimestamps[peerID] = timestamps + globalMessageTimestamps.append(now) + return true + } + } + + func reset(for peerID: PeerID) { + queue.async(flags: .barrier) { + self.handshakeTimestamps.removeValue(forKey: peerID) + self.messageTimestamps.removeValue(forKey: peerID) + } + } + + func resetAll() { + queue.async(flags: .barrier) { + self.handshakeTimestamps.removeAll() + self.messageTimestamps.removeAll() + self.globalHandshakeTimestamps.removeAll() + self.globalMessageTimestamps.removeAll() + } + } +} diff --git a/bitchat/Noise/NoiseSecurityConsiderations.swift b/bitchat/Noise/NoiseSecurityConsiderations.swift deleted file mode 100644 index 02245a5d..00000000 --- a/bitchat/Noise/NoiseSecurityConsiderations.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// NoiseSecurityConsiderations.swift -// bitchat -// -// This is free and unencumbered software released into the public domain. -// For more information, see -// - -import BitLogger -import Foundation - -// MARK: - Security Constants - -enum NoiseSecurityConstants { - // Maximum message size to prevent memory exhaustion - static let maxMessageSize = 65535 // 64KB as per Noise spec - - // Maximum handshake message size - static let maxHandshakeMessageSize = 2048 // 2KB to accommodate XX pattern - - // Session timeout - sessions older than this should be renegotiated - static let sessionTimeout: TimeInterval = 86400 // 24 hours - - // Maximum number of messages before rekey (2^64 - 1 is the nonce limit) - static let maxMessagesPerSession: UInt64 = 1_000_000_000 // 1 billion messages - - // Handshake timeout - abandon incomplete handshakes - static let handshakeTimeout: TimeInterval = 60 // 1 minute - - // Maximum concurrent sessions per peer - static let maxSessionsPerPeer = 3 - - // Rate limiting - static let maxHandshakesPerMinute = 10 - static let maxMessagesPerSecond = 100 - - // Global rate limiting (across all peers) - static let maxGlobalHandshakesPerMinute = 30 - static let maxGlobalMessagesPerSecond = 500 -} - -// MARK: - Security Validations - -struct NoiseSecurityValidator { - - /// Validate message size - static func validateMessageSize(_ data: Data) -> Bool { - return data.count <= NoiseSecurityConstants.maxMessageSize - } - - /// Validate handshake message size - static func validateHandshakeMessageSize(_ data: Data) -> Bool { - return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize - } -} - -// MARK: - Enhanced Noise Session with Security - -final class SecureNoiseSession: NoiseSession { - private(set) var messageCount: UInt64 = 0 - private let sessionStartTime = Date() - private(set) var lastActivityTime = Date() - - override func encrypt(_ plaintext: Data) throws -> Data { - // Check session age - if Date().timeIntervalSince(sessionStartTime) > NoiseSecurityConstants.sessionTimeout { - throw NoiseSecurityError.sessionExpired - } - - // Check message count - if messageCount >= NoiseSecurityConstants.maxMessagesPerSession { - throw NoiseSecurityError.sessionExhausted - } - - // Validate message size - guard NoiseSecurityValidator.validateMessageSize(plaintext) else { - throw NoiseSecurityError.messageTooLarge - } - - let encrypted = try super.encrypt(plaintext) - messageCount += 1 - lastActivityTime = Date() - - return encrypted - } - - override func decrypt(_ ciphertext: Data) throws -> Data { - // Check session age - if Date().timeIntervalSince(sessionStartTime) > NoiseSecurityConstants.sessionTimeout { - throw NoiseSecurityError.sessionExpired - } - - // Validate message size - guard NoiseSecurityValidator.validateMessageSize(ciphertext) else { - throw NoiseSecurityError.messageTooLarge - } - - let decrypted = try super.decrypt(ciphertext) - lastActivityTime = Date() - - return decrypted - } - - func needsRenegotiation() -> Bool { - // Check if we've used more than 90% of message limit - let messageThreshold = UInt64(Double(NoiseSecurityConstants.maxMessagesPerSession) * 0.9) - if messageCount >= messageThreshold { - return true - } - - // Check if last activity was more than 30 minutes ago - if Date().timeIntervalSince(lastActivityTime) > NoiseSecurityConstants.sessionTimeout { - return true - } - - return false - } - - // MARK: - Testing Support - #if DEBUG - func setLastActivityTimeForTesting(_ date: Date) { - lastActivityTime = date - } - - func setMessageCountForTesting(_ count: UInt64) { - messageCount = count - } - #endif -} - -// MARK: - Rate Limiter - -final class NoiseRateLimiter { - private var handshakeTimestamps: [PeerID: [Date]] = [:] - private var messageTimestamps: [PeerID: [Date]] = [:] - - // Global rate limiting - private var globalHandshakeTimestamps: [Date] = [] - private var globalMessageTimestamps: [Date] = [] - - private let queue = DispatchQueue(label: "chat.bitchat.noise.ratelimit", attributes: .concurrent) - - func allowHandshake(from peerID: PeerID) -> Bool { - return queue.sync(flags: .barrier) { - let now = Date() - let oneMinuteAgo = now.addingTimeInterval(-60) - - // Check global rate limit first - globalHandshakeTimestamps = globalHandshakeTimestamps.filter { $0 > oneMinuteAgo } - if globalHandshakeTimestamps.count >= NoiseSecurityConstants.maxGlobalHandshakesPerMinute { - SecureLogger.warning("Global handshake rate limit exceeded: \(globalHandshakeTimestamps.count)/\(NoiseSecurityConstants.maxGlobalHandshakesPerMinute) per minute", category: .security) - return false - } - - // Check per-peer rate limit - var timestamps = handshakeTimestamps[peerID] ?? [] - timestamps = timestamps.filter { $0 > oneMinuteAgo } - - if timestamps.count >= NoiseSecurityConstants.maxHandshakesPerMinute { - SecureLogger.warning("Per-peer handshake rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxHandshakesPerMinute) per minute", category: .security) - return false - } - - // Record new handshake - timestamps.append(now) - handshakeTimestamps[peerID] = timestamps - globalHandshakeTimestamps.append(now) - return true - } - } - - func allowMessage(from peerID: PeerID) -> Bool { - return queue.sync(flags: .barrier) { - let now = Date() - let oneSecondAgo = now.addingTimeInterval(-1) - - // Check global rate limit first - globalMessageTimestamps = globalMessageTimestamps.filter { $0 > oneSecondAgo } - if globalMessageTimestamps.count >= NoiseSecurityConstants.maxGlobalMessagesPerSecond { - SecureLogger.warning("Global message rate limit exceeded: \(globalMessageTimestamps.count)/\(NoiseSecurityConstants.maxGlobalMessagesPerSecond) per second", category: .security) - return false - } - - // Check per-peer rate limit - var timestamps = messageTimestamps[peerID] ?? [] - timestamps = timestamps.filter { $0 > oneSecondAgo } - - if timestamps.count >= NoiseSecurityConstants.maxMessagesPerSecond { - SecureLogger.warning("Per-peer message rate limit exceeded for \(peerID): \(timestamps.count)/\(NoiseSecurityConstants.maxMessagesPerSecond) per second", category: .security) - return false - } - - // Record new message - timestamps.append(now) - messageTimestamps[peerID] = timestamps - globalMessageTimestamps.append(now) - return true - } - } - - func reset(for peerID: PeerID) { - queue.async(flags: .barrier) { - self.handshakeTimestamps.removeValue(forKey: peerID) - self.messageTimestamps.removeValue(forKey: peerID) - } - } - - func resetAll() { - queue.async(flags: .barrier) { - self.handshakeTimestamps.removeAll() - self.messageTimestamps.removeAll() - self.globalHandshakeTimestamps.removeAll() - self.globalMessageTimestamps.removeAll() - } - } -} - -// MARK: - Security Errors - -enum NoiseSecurityError: Error { - case sessionExpired - case sessionExhausted - case messageTooLarge - case invalidPeerID - case rateLimitExceeded - case handshakeTimeout -} diff --git a/bitchat/Noise/NoiseSecurityConstants.swift b/bitchat/Noise/NoiseSecurityConstants.swift new file mode 100644 index 00000000..17781352 --- /dev/null +++ b/bitchat/Noise/NoiseSecurityConstants.swift @@ -0,0 +1,37 @@ +// +// NoiseSecurityConstants.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation + +enum NoiseSecurityConstants { + // Maximum message size to prevent memory exhaustion + static let maxMessageSize = 65535 // 64KB as per Noise spec + + // Maximum handshake message size + static let maxHandshakeMessageSize = 2048 // 2KB to accommodate XX pattern + + // Session timeout - sessions older than this should be renegotiated + static let sessionTimeout: TimeInterval = 86400 // 24 hours + + // Maximum number of messages before rekey (2^64 - 1 is the nonce limit) + static let maxMessagesPerSession: UInt64 = 1_000_000_000 // 1 billion messages + + // Handshake timeout - abandon incomplete handshakes + static let handshakeTimeout: TimeInterval = 60 // 1 minute + + // Maximum concurrent sessions per peer + static let maxSessionsPerPeer = 3 + + // Rate limiting + static let maxHandshakesPerMinute = 10 + static let maxMessagesPerSecond = 100 + + // Global rate limiting (across all peers) + static let maxGlobalHandshakesPerMinute = 30 + static let maxGlobalMessagesPerSecond = 500 +} diff --git a/bitchat/Noise/NoiseSecurityError.swift b/bitchat/Noise/NoiseSecurityError.swift new file mode 100644 index 00000000..aff73c8b --- /dev/null +++ b/bitchat/Noise/NoiseSecurityError.swift @@ -0,0 +1,18 @@ +// +// NoiseSecurityError.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation + +enum NoiseSecurityError: Error { + case sessionExpired + case sessionExhausted + case messageTooLarge + case invalidPeerID + case rateLimitExceeded + case handshakeTimeout +} diff --git a/bitchat/Noise/NoiseSecurityValidator.swift b/bitchat/Noise/NoiseSecurityValidator.swift new file mode 100644 index 00000000..355d8fd7 --- /dev/null +++ b/bitchat/Noise/NoiseSecurityValidator.swift @@ -0,0 +1,22 @@ +// +// NoiseSecurityValidator.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation + +struct NoiseSecurityValidator { + + /// Validate message size + static func validateMessageSize(_ data: Data) -> Bool { + return data.count <= NoiseSecurityConstants.maxMessageSize + } + + /// Validate handshake message size + static func validateHandshakeMessageSize(_ data: Data) -> Bool { + return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize + } +} diff --git a/bitchat/Noise/NoiseSession.swift b/bitchat/Noise/NoiseSession.swift index fd417548..52e3dedd 100644 --- a/bitchat/Noise/NoiseSession.swift +++ b/bitchat/Noise/NoiseSession.swift @@ -196,12 +196,6 @@ class NoiseSession { } } - func getHandshakeHash() -> Data? { - return sessionQueue.sync { - return handshakeHash - } - } - func reset() { sessionQueue.sync(flags: .barrier) { let wasEstablished = state == .established diff --git a/bitchat/Noise/NoiseSessionManager.swift b/bitchat/Noise/NoiseSessionManager.swift index cd4ad425..92ccf9d6 100644 --- a/bitchat/Noise/NoiseSessionManager.swift +++ b/bitchat/Noise/NoiseSessionManager.swift @@ -27,19 +27,6 @@ final class NoiseSessionManager { // MARK: - Session Management - func createSession(for peerID: PeerID, role: NoiseRole) -> NoiseSession { - return managerQueue.sync(flags: .barrier) { - let session = SecureNoiseSession( - peerID: peerID, - role: role, - keychain: keychain, - localStaticKey: localStaticKey - ) - sessions[peerID] = session - return session - } - } - func getSession(for peerID: PeerID) -> NoiseSession? { return managerQueue.sync { return sessions[peerID] @@ -48,14 +35,9 @@ final class NoiseSessionManager { func removeSession(for peerID: PeerID) { managerQueue.sync(flags: .barrier) { - if let session = sessions[peerID] { - if session.isEstablished() { - SecureLogger.info(.sessionExpired(peerID: peerID.id)) - } - // Clear sensitive data before removing - session.reset() + if let session = sessions.removeValue(forKey: peerID) { + session.reset() // Clear sensitive data before removing } - _ = sessions.removeValue(forKey: peerID) } } @@ -68,12 +50,6 @@ final class NoiseSessionManager { } } - func getEstablishedSessions() -> [PeerID: NoiseSession] { - return managerQueue.sync { - return sessions.filter { $0.value.isEstablished() } - } - } - // MARK: - Handshake Helpers func initiateHandshake(with peerID: PeerID) throws -> Data { @@ -207,10 +183,6 @@ final class NoiseSessionManager { return getSession(for: peerID)?.getRemoteStaticPublicKey() } - func getHandshakeHash(for peerID: PeerID) -> Data? { - return getSession(for: peerID)?.getHandshakeHash() - } - // MARK: - Session Rekeying func getSessionsNeedingRekey() -> [(peerID: PeerID, needsRekey: Bool)] { diff --git a/bitchat/Noise/SecureNoiseSession.swift b/bitchat/Noise/SecureNoiseSession.swift new file mode 100644 index 00000000..c42354fe --- /dev/null +++ b/bitchat/Noise/SecureNoiseSession.swift @@ -0,0 +1,81 @@ +// +// SecureNoiseSession.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation + +final class SecureNoiseSession: NoiseSession { + private(set) var messageCount: UInt64 = 0 + private let sessionStartTime = Date() + private(set) var lastActivityTime = Date() + + override func encrypt(_ plaintext: Data) throws -> Data { + // Check session age + if Date().timeIntervalSince(sessionStartTime) > NoiseSecurityConstants.sessionTimeout { + throw NoiseSecurityError.sessionExpired + } + + // Check message count + if messageCount >= NoiseSecurityConstants.maxMessagesPerSession { + throw NoiseSecurityError.sessionExhausted + } + + // Validate message size + guard NoiseSecurityValidator.validateMessageSize(plaintext) else { + throw NoiseSecurityError.messageTooLarge + } + + let encrypted = try super.encrypt(plaintext) + messageCount += 1 + lastActivityTime = Date() + + return encrypted + } + + override func decrypt(_ ciphertext: Data) throws -> Data { + // Check session age + if Date().timeIntervalSince(sessionStartTime) > NoiseSecurityConstants.sessionTimeout { + throw NoiseSecurityError.sessionExpired + } + + // Validate message size + guard NoiseSecurityValidator.validateMessageSize(ciphertext) else { + throw NoiseSecurityError.messageTooLarge + } + + let decrypted = try super.decrypt(ciphertext) + lastActivityTime = Date() + + return decrypted + } + + func needsRenegotiation() -> Bool { + // Check if we've used more than 90% of message limit + let messageThreshold = UInt64(Double(NoiseSecurityConstants.maxMessagesPerSession) * 0.9) + if messageCount >= messageThreshold { + return true + } + + // Check if last activity was more than 30 minutes ago + if Date().timeIntervalSince(lastActivityTime) > NoiseSecurityConstants.sessionTimeout { + return true + } + + return false + } + + // MARK: - Testing Support + #if DEBUG + func setLastActivityTimeForTesting(_ date: Date) { + lastActivityTime = date + } + + func setMessageCountForTesting(_ count: UInt64) { + messageCount = count + } + #endif +} diff --git a/bitchatTests/Noise/NoiseProtocolTests.swift b/bitchatTests/Noise/NoiseProtocolTests.swift index 0b7ad2db..4a22588b 100644 --- a/bitchatTests/Noise/NoiseProtocolTests.swift +++ b/bitchatTests/Noise/NoiseProtocolTests.swift @@ -166,14 +166,14 @@ final class NoiseProtocolTests: XCTestCase { func testSessionManagerBasicOperations() throws { let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) - // Create session - let session = manager.createSession(for: TestConstants.testPeerID2, role: .initiator) - XCTAssertNotNil(session) + XCTAssertNil(manager.getSession(for: TestConstants.testPeerID2)) + + _ = try manager.initiateHandshake(with: TestConstants.testPeerID2) + XCTAssertNotNil(manager.getSession(for: TestConstants.testPeerID2)) // Get session let retrieved = manager.getSession(for: TestConstants.testPeerID2) XCTAssertNotNil(retrieved) - XCTAssertTrue(session === retrieved) // Remove session manager.removeSession(for: TestConstants.testPeerID2)