From e8fb4a63331376393de8713d88e59e5c61e899ea Mon Sep 17 00:00:00 2001 From: islam <2553451+qalandarov@users.noreply.github.com> Date: Sat, 4 Apr 2026 00:32:05 +0100 Subject: [PATCH] Extract Noise into a dedicated module --- .github/workflows/swift-tests.yml | 2 + Package.swift | 5 ++- bitchat.xcodeproj/project.pbxproj | 20 +++++++++ bitchat/Services/KeychainManager.swift | 3 +- bitchat/Services/NoiseEncryptionService.swift | 1 + .../EndToEnd/PrivateChatE2ETests.swift | 1 + .../Integration/IntegrationTests.swift | 1 + .../Integration/TestNetworkHelper.swift | 1 + localPackages/Noise/Package.swift | 38 +++++++++++++++++ .../Noise/Sources}/Noise/NoiseProtocol.swift | 7 ++-- .../Sources}/Noise/NoiseRateLimiter.swift | 12 +++--- .../Noise/NoiseSecurityConstants.swift | 10 ++--- .../Sources}/Noise/NoiseSecurityError.swift | 2 +- .../Noise/NoiseSecurityValidator.swift | 6 +-- .../Noise/Sources}/Noise/NoiseSession.swift | 12 +++--- .../Sources}/Noise/NoiseSessionError.swift | 2 +- .../Sources}/Noise/NoiseSessionManager.swift | 30 ++++++------- .../Sources}/Noise/NoiseSessionState.swift | 0 .../Sources/Noise/SecureMemoryCleaner.swift | 14 +++++++ .../Sources}/Noise/SecureNoiseSession.swift | 0 .../Noise/Tests/NoiseTests/MockKeychain.swift | 34 +++++++++++++++ .../NoiseTests}/NoiseCoverageTests.swift | 3 +- .../NoiseTests}/NoiseProtocolTests.swift | 14 +++---- .../NoiseTests}/NoiseRateLimiterTests.swift | 2 +- .../Tests/NoiseTests}/NoiseTestVectors.json | 0 .../Noise/Tests/NoiseTests/TestHelpers.swift | 42 +++++++++++++++++++ 26 files changed, 209 insertions(+), 53 deletions(-) create mode 100644 localPackages/Noise/Package.swift rename {bitchat => localPackages/Noise/Sources}/Noise/NoiseProtocol.swift (99%) rename {bitchat => localPackages/Noise/Sources}/Noise/NoiseRateLimiter.swift (94%) rename {bitchat => localPackages/Noise/Sources}/Noise/NoiseSecurityConstants.swift (80%) rename {bitchat => localPackages/Noise/Sources}/Noise/NoiseSecurityError.swift (89%) rename {bitchat => localPackages/Noise/Sources}/Noise/NoiseSecurityValidator.swift (70%) rename {bitchat => localPackages/Noise/Sources}/Noise/NoiseSession.swift (97%) rename {bitchat => localPackages/Noise/Sources}/Noise/NoiseSessionError.swift (84%) rename {bitchat => localPackages/Noise/Sources}/Noise/NoiseSessionManager.swift (87%) rename {bitchat => localPackages/Noise/Sources}/Noise/NoiseSessionState.swift (100%) create mode 100644 localPackages/Noise/Sources/Noise/SecureMemoryCleaner.swift rename {bitchat => localPackages/Noise/Sources}/Noise/SecureNoiseSession.swift (100%) create mode 100644 localPackages/Noise/Tests/NoiseTests/MockKeychain.swift rename {bitchatTests/Noise => localPackages/Noise/Tests/NoiseTests}/NoiseCoverageTests.swift (99%) rename {bitchatTests/Noise => localPackages/Noise/Tests/NoiseTests}/NoiseProtocolTests.swift (99%) rename {bitchatTests/Noise => localPackages/Noise/Tests/NoiseTests}/NoiseRateLimiterTests.swift (98%) rename {bitchatTests/Noise => localPackages/Noise/Tests/NoiseTests}/NoiseTestVectors.json (100%) create mode 100644 localPackages/Noise/Tests/NoiseTests/TestHelpers.swift diff --git a/.github/workflows/swift-tests.yml b/.github/workflows/swift-tests.yml index a56b9030..54c121e5 100644 --- a/.github/workflows/swift-tests.yml +++ b/.github/workflows/swift-tests.yml @@ -21,6 +21,8 @@ jobs: path: localPackages/BitLogger - name: BitFoundation path: localPackages/BitFoundation + - name: Noise + path: localPackages/Noise steps: - name: Checkout code diff --git a/Package.swift b/Package.swift index d7bc2cb6..9eb6a935 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ let package = Package( ], dependencies:[ .package(path: "localPackages/Arti"), + .package(path: "localPackages/Noise"), .package(path: "localPackages/BitFoundation"), .package(path: "localPackages/BitLogger"), .package(url: "https://github.com/21-DOT-DEV/swift-secp256k1", exact: "0.21.1") @@ -28,6 +29,7 @@ let package = Package( .product(name: "P256K", package: "swift-secp256k1"), .product(name: "BitFoundation", package: "BitFoundation"), .product(name: "BitLogger", package: "BitLogger"), + .product(name: "Noise", package: "Noise"), .product(name: "Tor", package: "Arti") ], path: "bitchat", @@ -56,8 +58,7 @@ let package = Package( "README.md" ], resources: [ - .process("Localization"), - .process("Noise") + .process("Localization") ] ) ] diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index a648a09f..6f04ce99 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; }; 885BBED78092484A5B069461 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 4EB6BA1B8464F1EA38F4E286 /* P256K */; }; + A63163B62F80CB2500B8B128 /* Noise in Frameworks */ = {isa = PBXBuildFile; productRef = A63163B52F80CB2500B8B128 /* Noise */; }; + A63163B82F80CB2D00B8B128 /* Noise in Frameworks */ = {isa = PBXBuildFile; productRef = A63163B72F80CB2D00B8B128 /* Noise */; }; A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = A6BCF9472F80953E001CF9B9 /* BitFoundation */; }; A6BCF94A2F809550001CF9B9 /* BitFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = A6BCF9492F809550001CF9B9 /* BitFoundation */; }; A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E56F2E77036A0032EA8A /* BitLogger */; }; @@ -156,6 +158,7 @@ isa = PBXFrameworksBuildPhase; files = ( A6E3E5722E7703760032EA8A /* BitLogger in Frameworks */, + A63163B82F80CB2D00B8B128 /* Noise in Frameworks */, 3EE336D150427F736F32B56C /* P256K in Frameworks */, A6E3EA812E7706A80032EA8A /* Tor in Frameworks */, A6BCF94A2F809550001CF9B9 /* BitFoundation in Frameworks */, @@ -165,6 +168,7 @@ isa = PBXFrameworksBuildPhase; files = ( A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */, + A63163B62F80CB2500B8B128 /* Noise in Frameworks */, 885BBED78092484A5B069461 /* P256K in Frameworks */, A6E3EA7F2E7706720032EA8A /* Tor in Frameworks */, A6BCF9482F80953E001CF9B9 /* BitFoundation in Frameworks */, @@ -228,6 +232,7 @@ A6E3E5712E7703760032EA8A /* BitLogger */, A6E3EA802E7706A80032EA8A /* Tor */, A6BCF9492F809550001CF9B9 /* BitFoundation */, + A63163B72F80CB2D00B8B128 /* Noise */, ); productName = bitchat_macOS; productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */; @@ -309,6 +314,7 @@ A6E3E56F2E77036A0032EA8A /* BitLogger */, A6E3EA7E2E7706720032EA8A /* Tor */, A6BCF9472F80953E001CF9B9 /* BitFoundation */, + A63163B52F80CB2500B8B128 /* Noise */, ); productName = bitchat_iOS; productReference = 96D0D41CA19EE5A772AA8434 /* bitchat.app */; @@ -351,6 +357,7 @@ A6E3E56E2E77036A0032EA8A /* XCLocalSwiftPackageReference "localPackages/BitLogger" */, A6E3EA7D2E7706720032EA8A /* XCLocalSwiftPackageReference "localPackages/Arti" */, A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */, + A63163B42F80CB2500B8B128 /* XCLocalSwiftPackageReference "localPackages/Noise" */, ); preferredProjectObjectVersion = 90; projectDirPath = ""; @@ -916,6 +923,10 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ + A63163B42F80CB2500B8B128 /* XCLocalSwiftPackageReference "localPackages/Noise" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = localPackages/Noise; + }; A6BCF9462F80953E001CF9B9 /* XCLocalSwiftPackageReference "localPackages/BitFoundation" */ = { isa = XCLocalSwiftPackageReference; relativePath = localPackages/BitFoundation; @@ -947,6 +958,15 @@ package = B8C407587481BBB190741C93 /* XCRemoteSwiftPackageReference "swift-secp256k1" */; productName = P256K; }; + A63163B52F80CB2500B8B128 /* Noise */ = { + isa = XCSwiftPackageProductDependency; + productName = Noise; + }; + A63163B72F80CB2D00B8B128 /* Noise */ = { + isa = XCSwiftPackageProductDependency; + package = A63163B42F80CB2500B8B128 /* XCLocalSwiftPackageReference "localPackages/Noise" */; + productName = Noise; + }; A6BCF9472F80953E001CF9B9 /* BitFoundation */ = { isa = XCSwiftPackageProductDependency; productName = BitFoundation; diff --git a/bitchat/Services/KeychainManager.swift b/bitchat/Services/KeychainManager.swift index 574bdf9f..2ed3cf25 100644 --- a/bitchat/Services/KeychainManager.swift +++ b/bitchat/Services/KeychainManager.swift @@ -7,6 +7,7 @@ // import BitLogger +import protocol Noise.SecureMemoryCleaner import Foundation import Security @@ -51,7 +52,7 @@ enum KeychainSaveResult { } } -protocol KeychainManagerProtocol { +protocol KeychainManagerProtocol: SecureMemoryCleaner { func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool func getIdentityKey(forKey key: String) -> Data? func deleteIdentityKey(forKey key: String) -> Bool diff --git a/bitchat/Services/NoiseEncryptionService.swift b/bitchat/Services/NoiseEncryptionService.swift index 70e43c0d..740f8206 100644 --- a/bitchat/Services/NoiseEncryptionService.swift +++ b/bitchat/Services/NoiseEncryptionService.swift @@ -84,6 +84,7 @@ import BitLogger import BitFoundation +import Noise import Foundation import CryptoKit diff --git a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift index 6f5c7891..4e69d349 100644 --- a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift +++ b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift @@ -10,6 +10,7 @@ import Testing import CryptoKit import struct Foundation.UUID import BitFoundation +import Noise @testable import bitchat struct PrivateChatE2ETests { diff --git a/bitchatTests/Integration/IntegrationTests.swift b/bitchatTests/Integration/IntegrationTests.swift index 1907cf33..4199da64 100644 --- a/bitchatTests/Integration/IntegrationTests.swift +++ b/bitchatTests/Integration/IntegrationTests.swift @@ -10,6 +10,7 @@ import Foundation import CryptoKit import Testing import BitFoundation +import Noise @testable import bitchat struct IntegrationTests { diff --git a/bitchatTests/Integration/TestNetworkHelper.swift b/bitchatTests/Integration/TestNetworkHelper.swift index 3b633e54..b9fc5a35 100644 --- a/bitchatTests/Integration/TestNetworkHelper.swift +++ b/bitchatTests/Integration/TestNetworkHelper.swift @@ -9,6 +9,7 @@ import Foundation import CryptoKit import BitFoundation +import Noise @testable import bitchat final class TestNetworkHelper { diff --git a/localPackages/Noise/Package.swift b/localPackages/Noise/Package.swift new file mode 100644 index 00000000..805ad9e4 --- /dev/null +++ b/localPackages/Noise/Package.swift @@ -0,0 +1,38 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "Noise", + platforms: [ + .iOS(.v16), + .macOS(.v13) + ], + products: [ + .library( + name: "Noise", + targets: ["Noise"] + ), + ], + dependencies: [ + .package(path: "../BitLogger"), + .package(path: "../BitFoundation"), + ], + targets: [ + .target( + name: "Noise", + dependencies: [ + .product(name: "BitLogger", package: "BitLogger"), + .product(name: "BitFoundation", package: "BitFoundation"), + ], + path: "Sources" + ), + .testTarget( + name: "NoiseTests", + dependencies: ["Noise"], + resources: [ + .process("NoiseTestVectors.json") + ] + ), + ] +) diff --git a/bitchat/Noise/NoiseProtocol.swift b/localPackages/Noise/Sources/Noise/NoiseProtocol.swift similarity index 99% rename from bitchat/Noise/NoiseProtocol.swift rename to localPackages/Noise/Sources/Noise/NoiseProtocol.swift index faa18f31..41c6447a 100644 --- a/bitchat/Noise/NoiseProtocol.swift +++ b/localPackages/Noise/Sources/Noise/NoiseProtocol.swift @@ -78,6 +78,7 @@ /// import BitLogger +import BitFoundation import Foundation import CryptoKit @@ -532,7 +533,7 @@ final class NoiseSymmetricState { final class NoiseHandshakeState { private let role: NoiseRole private let pattern: NoisePattern - private let keychain: KeychainManagerProtocol + private let keychain: SecureMemoryCleaner private var symmetricState: NoiseSymmetricState // Keys @@ -555,7 +556,7 @@ final class NoiseHandshakeState { init( role: NoiseRole, pattern: NoisePattern, - keychain: KeychainManagerProtocol, + keychain: SecureMemoryCleaner, localStaticKey: Curve25519.KeyAgreement.PrivateKey? = nil, remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil, prologue: Data = Data(), @@ -923,7 +924,7 @@ extension NoisePattern { // MARK: - Errors -enum NoiseError: Error { +public enum NoiseError: Error { case uninitializedCipher case invalidCiphertext case handshakeComplete diff --git a/bitchat/Noise/NoiseRateLimiter.swift b/localPackages/Noise/Sources/Noise/NoiseRateLimiter.swift similarity index 94% rename from bitchat/Noise/NoiseRateLimiter.swift rename to localPackages/Noise/Sources/Noise/NoiseRateLimiter.swift index 7581244f..fccb6000 100644 --- a/bitchat/Noise/NoiseRateLimiter.swift +++ b/localPackages/Noise/Sources/Noise/NoiseRateLimiter.swift @@ -10,7 +10,7 @@ import BitLogger import BitFoundation import Foundation -final class NoiseRateLimiter { +public final class NoiseRateLimiter { private var handshakeTimestamps: [PeerID: [Date]] = [:] private var messageTimestamps: [PeerID: [Date]] = [:] @@ -19,8 +19,10 @@ final class NoiseRateLimiter { private var globalMessageTimestamps: [Date] = [] private let queue = DispatchQueue(label: "chat.bitchat.noise.ratelimit", attributes: .concurrent) - - func allowHandshake(from peerID: PeerID) -> Bool { + + public init() {} + + public func allowHandshake(from peerID: PeerID) -> Bool { return queue.sync(flags: .barrier) { let now = Date() let oneMinuteAgo = now.addingTimeInterval(-60) @@ -49,7 +51,7 @@ final class NoiseRateLimiter { } } - func allowMessage(from peerID: PeerID) -> Bool { + public func allowMessage(from peerID: PeerID) -> Bool { return queue.sync(flags: .barrier) { let now = Date() let oneSecondAgo = now.addingTimeInterval(-1) @@ -85,7 +87,7 @@ final class NoiseRateLimiter { } } - func resetAll() { + public func resetAll() { queue.async(flags: .barrier) { self.handshakeTimestamps.removeAll() self.messageTimestamps.removeAll() diff --git a/bitchat/Noise/NoiseSecurityConstants.swift b/localPackages/Noise/Sources/Noise/NoiseSecurityConstants.swift similarity index 80% rename from bitchat/Noise/NoiseSecurityConstants.swift rename to localPackages/Noise/Sources/Noise/NoiseSecurityConstants.swift index 17781352..8b30299c 100644 --- a/bitchat/Noise/NoiseSecurityConstants.swift +++ b/localPackages/Noise/Sources/Noise/NoiseSecurityConstants.swift @@ -8,7 +8,7 @@ import Foundation -enum NoiseSecurityConstants { +public enum NoiseSecurityConstants { // Maximum message size to prevent memory exhaustion static let maxMessageSize = 65535 // 64KB as per Noise spec @@ -28,10 +28,10 @@ enum NoiseSecurityConstants { static let maxSessionsPerPeer = 3 // Rate limiting - static let maxHandshakesPerMinute = 10 - static let maxMessagesPerSecond = 100 + public static let maxHandshakesPerMinute = 10 + public static let maxMessagesPerSecond = 100 // Global rate limiting (across all peers) - static let maxGlobalHandshakesPerMinute = 30 - static let maxGlobalMessagesPerSecond = 500 + public static let maxGlobalHandshakesPerMinute = 30 + public static let maxGlobalMessagesPerSecond = 500 } diff --git a/bitchat/Noise/NoiseSecurityError.swift b/localPackages/Noise/Sources/Noise/NoiseSecurityError.swift similarity index 89% rename from bitchat/Noise/NoiseSecurityError.swift rename to localPackages/Noise/Sources/Noise/NoiseSecurityError.swift index aff73c8b..5663af0a 100644 --- a/bitchat/Noise/NoiseSecurityError.swift +++ b/localPackages/Noise/Sources/Noise/NoiseSecurityError.swift @@ -8,7 +8,7 @@ import Foundation -enum NoiseSecurityError: Error { +public enum NoiseSecurityError: Error { case sessionExpired case sessionExhausted case messageTooLarge diff --git a/bitchat/Noise/NoiseSecurityValidator.swift b/localPackages/Noise/Sources/Noise/NoiseSecurityValidator.swift similarity index 70% rename from bitchat/Noise/NoiseSecurityValidator.swift rename to localPackages/Noise/Sources/Noise/NoiseSecurityValidator.swift index 355d8fd7..f2a02e00 100644 --- a/bitchat/Noise/NoiseSecurityValidator.swift +++ b/localPackages/Noise/Sources/Noise/NoiseSecurityValidator.swift @@ -8,15 +8,15 @@ import Foundation -struct NoiseSecurityValidator { +public struct NoiseSecurityValidator { /// Validate message size - static func validateMessageSize(_ data: Data) -> Bool { + public static func validateMessageSize(_ data: Data) -> Bool { return data.count <= NoiseSecurityConstants.maxMessageSize } /// Validate handshake message size - static func validateHandshakeMessageSize(_ data: Data) -> Bool { + public static func validateHandshakeMessageSize(_ data: Data) -> Bool { return data.count <= NoiseSecurityConstants.maxHandshakeMessageSize } } diff --git a/bitchat/Noise/NoiseSession.swift b/localPackages/Noise/Sources/Noise/NoiseSession.swift similarity index 97% rename from bitchat/Noise/NoiseSession.swift rename to localPackages/Noise/Sources/Noise/NoiseSession.swift index 1df2f0ba..b36a6d90 100644 --- a/bitchat/Noise/NoiseSession.swift +++ b/localPackages/Noise/Sources/Noise/NoiseSession.swift @@ -11,10 +11,10 @@ import Foundation import CryptoKit import BitFoundation -class NoiseSession { +public class NoiseSession { let peerID: PeerID let role: NoiseRole - private let keychain: KeychainManagerProtocol + private let keychain: SecureMemoryCleaner private var state: NoiseSessionState = .uninitialized private var handshakeState: NoiseHandshakeState? private var sendCipher: NoiseCipherState? @@ -34,7 +34,7 @@ class NoiseSession { init( peerID: PeerID, role: NoiseRole, - keychain: KeychainManagerProtocol, + keychain: SecureMemoryCleaner, localStaticKey: Curve25519.KeyAgreement.PrivateKey, remoteStaticKey: Curve25519.KeyAgreement.PublicKey? = nil ) { @@ -182,7 +182,7 @@ class NoiseSession { } } - func isEstablished() -> Bool { + public func isEstablished() -> Bool { return sessionQueue.sync { if case .established = state { return true @@ -217,8 +217,8 @@ class NoiseSession { sentHandshakeMessages.removeAll() // Clear handshake hash - if var hash = handshakeHash { - keychain.secureClear(&hash) + if handshakeHash != nil { + keychain.secureClear(&handshakeHash!) } handshakeHash = nil diff --git a/bitchat/Noise/NoiseSessionError.swift b/localPackages/Noise/Sources/Noise/NoiseSessionError.swift similarity index 84% rename from bitchat/Noise/NoiseSessionError.swift rename to localPackages/Noise/Sources/Noise/NoiseSessionError.swift index 24e172e7..0cda921c 100644 --- a/bitchat/Noise/NoiseSessionError.swift +++ b/localPackages/Noise/Sources/Noise/NoiseSessionError.swift @@ -6,7 +6,7 @@ // For more information, see // -enum NoiseSessionError: Error, Equatable { +public enum NoiseSessionError: Error, Equatable { case invalidState case notEstablished case sessionNotFound diff --git a/bitchat/Noise/NoiseSessionManager.swift b/localPackages/Noise/Sources/Noise/NoiseSessionManager.swift similarity index 87% rename from bitchat/Noise/NoiseSessionManager.swift rename to localPackages/Noise/Sources/Noise/NoiseSessionManager.swift index 9029b228..9c9c129d 100644 --- a/bitchat/Noise/NoiseSessionManager.swift +++ b/localPackages/Noise/Sources/Noise/NoiseSessionManager.swift @@ -11,18 +11,18 @@ import CryptoKit import Foundation import BitFoundation -final class NoiseSessionManager { +public final class NoiseSessionManager { private var sessions: [PeerID: NoiseSession] = [:] private let localStaticKey: Curve25519.KeyAgreement.PrivateKey - private let keychain: KeychainManagerProtocol + private let keychain: SecureMemoryCleaner private let sessionFactory: (PeerID, NoiseRole) -> NoiseSession private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent) // Callbacks - var onSessionEstablished: ((PeerID, Curve25519.KeyAgreement.PublicKey) -> Void)? + public var onSessionEstablished: ((PeerID, Curve25519.KeyAgreement.PublicKey) -> Void)? var onSessionFailed: ((PeerID, Error) -> Void)? - init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) { + public init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: SecureMemoryCleaner) { self.localStaticKey = localStaticKey self.keychain = keychain self.sessionFactory = { peerID, role in @@ -38,7 +38,7 @@ final class NoiseSessionManager { #if DEBUG init( localStaticKey: Curve25519.KeyAgreement.PrivateKey, - keychain: KeychainManagerProtocol, + keychain: SecureMemoryCleaner, sessionFactory: @escaping (PeerID, NoiseRole) -> NoiseSession ) { self.localStaticKey = localStaticKey @@ -49,13 +49,13 @@ final class NoiseSessionManager { // MARK: - Session Management - func getSession(for peerID: PeerID) -> NoiseSession? { + public func getSession(for peerID: PeerID) -> NoiseSession? { return managerQueue.sync { return sessions[peerID] } } - func removeSession(for peerID: PeerID) { + public func removeSession(for peerID: PeerID) { managerQueue.sync(flags: .barrier) { if let session = sessions.removeValue(forKey: peerID) { session.reset() // Clear sensitive data before removing @@ -63,7 +63,7 @@ final class NoiseSessionManager { } } - func removeAllSessions() { + public func removeAllSessions() { managerQueue.sync(flags: .barrier) { for (_, session) in sessions { session.reset() @@ -74,7 +74,7 @@ final class NoiseSessionManager { // MARK: - Handshake Helpers - func initiateHandshake(with peerID: PeerID) throws -> Data { + public func initiateHandshake(with peerID: PeerID) throws -> Data { return try managerQueue.sync(flags: .barrier) { // Check if we already have an established session if let existingSession = sessions[peerID], existingSession.isEstablished() { @@ -103,7 +103,7 @@ final class NoiseSessionManager { } } - func handleIncomingHandshake(from peerID: PeerID, message: Data) throws -> Data? { + public func handleIncomingHandshake(from peerID: PeerID, message: Data) throws -> Data? { // Process everything within the synchronized block to prevent race conditions return try managerQueue.sync(flags: .barrier) { var shouldCreateNew = false @@ -173,7 +173,7 @@ final class NoiseSessionManager { // MARK: - Encryption/Decryption - func encrypt(_ plaintext: Data, for peerID: PeerID) throws -> Data { + public func encrypt(_ plaintext: Data, for peerID: PeerID) throws -> Data { guard let session = getSession(for: peerID) else { throw NoiseSessionError.sessionNotFound } @@ -181,7 +181,7 @@ final class NoiseSessionManager { return try session.encrypt(plaintext) } - func decrypt(_ ciphertext: Data, from peerID: PeerID) throws -> Data { + public func decrypt(_ ciphertext: Data, from peerID: PeerID) throws -> Data { guard let session = getSession(for: peerID) else { throw NoiseSessionError.sessionNotFound } @@ -191,13 +191,13 @@ final class NoiseSessionManager { // MARK: - Key Management - func getRemoteStaticKey(for peerID: PeerID) -> Curve25519.KeyAgreement.PublicKey? { + public func getRemoteStaticKey(for peerID: PeerID) -> Curve25519.KeyAgreement.PublicKey? { return getSession(for: peerID)?.getRemoteStaticPublicKey() } // MARK: - Session Rekeying - func getSessionsNeedingRekey() -> [(peerID: PeerID, needsRekey: Bool)] { + public func getSessionsNeedingRekey() -> [(peerID: PeerID, needsRekey: Bool)] { return managerQueue.sync { var needingRekey: [(peerID: PeerID, needsRekey: Bool)] = [] @@ -213,7 +213,7 @@ final class NoiseSessionManager { } } - func initiateRekey(for peerID: PeerID) throws { + public func initiateRekey(for peerID: PeerID) throws { // Remove old session removeSession(for: peerID) diff --git a/bitchat/Noise/NoiseSessionState.swift b/localPackages/Noise/Sources/Noise/NoiseSessionState.swift similarity index 100% rename from bitchat/Noise/NoiseSessionState.swift rename to localPackages/Noise/Sources/Noise/NoiseSessionState.swift diff --git a/localPackages/Noise/Sources/Noise/SecureMemoryCleaner.swift b/localPackages/Noise/Sources/Noise/SecureMemoryCleaner.swift new file mode 100644 index 00000000..37da7010 --- /dev/null +++ b/localPackages/Noise/Sources/Noise/SecureMemoryCleaner.swift @@ -0,0 +1,14 @@ +// +// SecureMemoryCleaner.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import struct Foundation.Data + +public protocol SecureMemoryCleaner { + func secureClear(_ data: inout Data) + func secureClear(_ string: inout String) +} diff --git a/bitchat/Noise/SecureNoiseSession.swift b/localPackages/Noise/Sources/Noise/SecureNoiseSession.swift similarity index 100% rename from bitchat/Noise/SecureNoiseSession.swift rename to localPackages/Noise/Sources/Noise/SecureNoiseSession.swift diff --git a/localPackages/Noise/Tests/NoiseTests/MockKeychain.swift b/localPackages/Noise/Tests/NoiseTests/MockKeychain.swift new file mode 100644 index 00000000..3cd6c723 --- /dev/null +++ b/localPackages/Noise/Tests/NoiseTests/MockKeychain.swift @@ -0,0 +1,34 @@ +// +// MockKeychain.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation +@testable import Noise + +// TODO: Combine with the one from the main target +final class MockKeychain: SecureMemoryCleaner { + /// Thread-safe counter for secureClear calls + private let lock = NSLock() + private var _secureClearDataCallCount = 0 + + var secureClearDataCallCount: Int { + lock.lock() + defer { lock.unlock() } + return _secureClearDataCallCount + } + + func secureClear(_ data: inout Data) { + lock.lock() + _secureClearDataCallCount += 1 + lock.unlock() + data = Data() + } + + func secureClear(_ string: inout String) { + string = "" + } +} diff --git a/bitchatTests/Noise/NoiseCoverageTests.swift b/localPackages/Noise/Tests/NoiseTests/NoiseCoverageTests.swift similarity index 99% rename from bitchatTests/Noise/NoiseCoverageTests.swift rename to localPackages/Noise/Tests/NoiseTests/NoiseCoverageTests.swift index 601b7061..04f4bb27 100644 --- a/bitchatTests/Noise/NoiseCoverageTests.swift +++ b/localPackages/Noise/Tests/NoiseTests/NoiseCoverageTests.swift @@ -2,8 +2,7 @@ import CryptoKit import Foundation import Testing import BitFoundation - -@testable import bitchat +@testable import Noise @Suite("Noise Coverage Tests") struct NoiseCoverageTests { diff --git a/bitchatTests/Noise/NoiseProtocolTests.swift b/localPackages/Noise/Tests/NoiseTests/NoiseProtocolTests.swift similarity index 99% rename from bitchatTests/Noise/NoiseProtocolTests.swift rename to localPackages/Noise/Tests/NoiseTests/NoiseProtocolTests.swift index 6ba8dbfa..5cfe185c 100644 --- a/bitchatTests/Noise/NoiseProtocolTests.swift +++ b/localPackages/Noise/Tests/NoiseTests/NoiseProtocolTests.swift @@ -10,7 +10,7 @@ import CryptoKit import Foundation import Testing import BitFoundation -@testable import bitchat +@testable import Noise // MARK: - Test Vector Support @@ -613,9 +613,7 @@ struct NoiseProtocolTests { private func loadTestVectors() throws -> [NoiseTestVector] { // Try to load from test bundle - let testBundle = Bundle(for: MockKeychain.self) - guard let url = testBundle.url(forResource: "NoiseTestVectors", withExtension: "json") - else { + guard let url = Bundle.module.url(forResource: "NoiseTestVectors", withExtension: "json") else { throw NSError( domain: "NoiseTests", code: 1, userInfo: [ @@ -794,7 +792,7 @@ struct NoiseProtocolTests { @Test func secureClearCalledDuringHandshake() throws { // Use TrackingMockKeychain to verify secureClear is called - let trackingKeychain = TrackingMockKeychain() + let trackingKeychain = MockKeychain() let aliceKey = Curve25519.KeyAgreement.PrivateKey() let bobKey = Curve25519.KeyAgreement.PrivateKey() @@ -843,7 +841,7 @@ struct NoiseProtocolTests { @Test func encryptionWorksAfterSecureClear() throws { // Verify that encryption/decryption still works correctly after adding secureClear - let trackingKeychain = TrackingMockKeychain() + let trackingKeychain = MockKeychain() let aliceKey = Curve25519.KeyAgreement.PrivateKey() let bobKey = Curve25519.KeyAgreement.PrivateKey() @@ -900,8 +898,8 @@ struct NoiseProtocolTests { // Verify secureClear is called in both writeMessage and readMessage paths // We do this by checking the count increases at each step - let aliceKeychain = TrackingMockKeychain() - let bobKeychain = TrackingMockKeychain() + let aliceKeychain = MockKeychain() + let bobKeychain = MockKeychain() let aliceKey = Curve25519.KeyAgreement.PrivateKey() let bobKey = Curve25519.KeyAgreement.PrivateKey() diff --git a/bitchatTests/Noise/NoiseRateLimiterTests.swift b/localPackages/Noise/Tests/NoiseTests/NoiseRateLimiterTests.swift similarity index 98% rename from bitchatTests/Noise/NoiseRateLimiterTests.swift rename to localPackages/Noise/Tests/NoiseTests/NoiseRateLimiterTests.swift index 623fe22e..74bac77c 100644 --- a/bitchatTests/Noise/NoiseRateLimiterTests.swift +++ b/localPackages/Noise/Tests/NoiseTests/NoiseRateLimiterTests.swift @@ -1,6 +1,6 @@ import XCTest import BitFoundation -@testable import bitchat +@testable import Noise final class NoiseRateLimiterTests: XCTestCase { func test_allowHandshake_blocksAfterPerPeerLimit() { diff --git a/bitchatTests/Noise/NoiseTestVectors.json b/localPackages/Noise/Tests/NoiseTests/NoiseTestVectors.json similarity index 100% rename from bitchatTests/Noise/NoiseTestVectors.json rename to localPackages/Noise/Tests/NoiseTests/NoiseTestVectors.json diff --git a/localPackages/Noise/Tests/NoiseTests/TestHelpers.swift b/localPackages/Noise/Tests/NoiseTests/TestHelpers.swift new file mode 100644 index 00000000..f96a91bf --- /dev/null +++ b/localPackages/Noise/Tests/NoiseTests/TestHelpers.swift @@ -0,0 +1,42 @@ +// +// TestHelpers.swift +// bitchatTests +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import Foundation +import CryptoKit +import BitFoundation + +// TODO: Combine with the one from the main target +final class TestHelpers { + static func generateRandomData(length: Int) -> Data { + var data = Data(count: length) + _ = data.withUnsafeMutableBytes { bytes in + SecRandomCopyBytes(kSecRandomDefault, length, bytes.baseAddress!) + } + return data + } + + @MainActor + static func waitUntil( + _ condition: @escaping () -> Bool, + timeout: TimeInterval = 5, + pollInterval: TimeInterval = 0.01 + ) async -> Bool { + let start = Date() + while !condition() { + if Date().timeIntervalSince(start) > timeout { + return condition() + } + try? await sleep(pollInterval) + } + return true + } +} + +func sleep(_ seconds: TimeInterval) async throws { + try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) +}