From 03c357f0480fa06f409cc27099090e40ea739216 Mon Sep 17 00:00:00 2001 From: Islam <2553451+qalandarov@users.noreply.github.com> Date: Sun, 5 Oct 2025 14:51:06 +0100 Subject: [PATCH] PeerID 11/n: `Noise` types use `PeerID` + create separate files (#750) * Noise types use PeerID * Fix tests * Extract `NoiseSessionManager` into a separate file * Extract `NoiseSessionState` into a separate file * Remove `failed` state from `NoiseSessionState` * Extract `NoiseSessionError` into a separate file --- bitchat/Noise/NoiseSession.swift | 275 +----------------- bitchat/Noise/NoiseSessionError.swift | 15 + bitchat/Noise/NoiseSessionManager.swift | 239 +++++++++++++++ bitchat/Noise/NoiseSessionState.swift | 13 + bitchat/Services/NoiseEncryptionService.swift | 20 +- bitchat/ViewModels/ChatViewModel.swift | 4 +- .../EndToEnd/PrivateChatE2ETests.swift | 4 +- .../EndToEnd/PublicChatE2ETests.swift | 8 +- .../Integration/IntegrationTests.swift | 19 +- bitchatTests/Mocks/MockBLEService.swift | 4 +- .../Protocol/BinaryProtocolTests.swift | 4 +- .../TestUtilities/TestConstants.swift | 9 +- bitchatTests/TestUtilities/TestHelpers.swift | 26 +- 13 files changed, 329 insertions(+), 311 deletions(-) create mode 100644 bitchat/Noise/NoiseSessionError.swift create mode 100644 bitchat/Noise/NoiseSessionManager.swift create mode 100644 bitchat/Noise/NoiseSessionState.swift diff --git a/bitchat/Noise/NoiseSession.swift b/bitchat/Noise/NoiseSession.swift index e159021d..fd417548 100644 --- a/bitchat/Noise/NoiseSession.swift +++ b/bitchat/Noise/NoiseSession.swift @@ -10,32 +10,8 @@ import BitLogger import Foundation import CryptoKit -// MARK: - Noise Session State - -enum NoiseSessionState: Equatable { - case uninitialized - case handshaking - case established - case failed(Error) - - static func == (lhs: NoiseSessionState, rhs: NoiseSessionState) -> Bool { - switch (lhs, rhs) { - case (.uninitialized, .uninitialized), - (.handshaking, .handshaking), - (.established, .established): - return true - case (.failed, .failed): - return true // We don't compare the errors - default: - return false - } - } -} - -// MARK: - Noise Session - class NoiseSession { - let peerID: String + let peerID: PeerID let role: NoiseRole private let keychain: KeychainManagerProtocol private var state: NoiseSessionState = .uninitialized @@ -55,7 +31,7 @@ class NoiseSession { private let sessionQueue = DispatchQueue(label: "chat.bitchat.noise.session", attributes: .concurrent) init( - peerID: String, + peerID: PeerID, role: NoiseRole, keychain: KeychainManagerProtocol, localStaticKey: Curve25519.KeyAgreement.PrivateKey, @@ -141,7 +117,7 @@ class NoiseSession { handshakeState = nil // Clear handshake state SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete (no response needed), transitioning to established") - SecureLogger.info(.handshakeCompleted(peerID: peerID)) + SecureLogger.info(.handshakeCompleted(peerID: peerID.id)) return nil } else { @@ -167,7 +143,7 @@ class NoiseSession { handshakeState = nil // Clear handshake state SecureLogger.debug("NoiseSession[\(peerID)]: Handshake complete after writing response, transitioning to established") - SecureLogger.info(.handshakeCompleted(peerID: peerID)) + SecureLogger.info(.handshakeCompleted(peerID: peerID.id)) } return response @@ -252,249 +228,8 @@ class NoiseSession { handshakeHash = nil if wasEstablished { - SecureLogger.info(.sessionExpired(peerID: peerID)) + SecureLogger.info(.sessionExpired(peerID: peerID.id)) } } } } - -// MARK: - Session Manager - -final class NoiseSessionManager { - private var sessions: [String: NoiseSession] = [:] - private let localStaticKey: Curve25519.KeyAgreement.PrivateKey - private let keychain: KeychainManagerProtocol - private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent) - - // Callbacks - var onSessionEstablished: ((String, Curve25519.KeyAgreement.PublicKey) -> Void)? - var onSessionFailed: ((String, Error) -> Void)? - - init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) { - self.localStaticKey = localStaticKey - self.keychain = keychain - } - - // MARK: - Session Management - - func createSession(for peerID: String, 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: String) -> NoiseSession? { - return managerQueue.sync { - return sessions[peerID] - } - } - - func removeSession(for peerID: String) { - managerQueue.sync(flags: .barrier) { - if let session = sessions[peerID] { - if session.isEstablished() { - SecureLogger.info(.sessionExpired(peerID: peerID)) - } - // Clear sensitive data before removing - session.reset() - } - _ = sessions.removeValue(forKey: peerID) - } - } - - func removeAllSessions() { - managerQueue.sync(flags: .barrier) { - for (_, session) in sessions { - session.reset() - } - sessions.removeAll() - } - } - - func getEstablishedSessions() -> [String: NoiseSession] { - return managerQueue.sync { - return sessions.filter { $0.value.isEstablished() } - } - } - - // MARK: - Handshake Helpers - - func initiateHandshake(with peerID: String) throws -> Data { - return try managerQueue.sync(flags: .barrier) { - // Check if we already have an established session - if let existingSession = sessions[peerID], existingSession.isEstablished() { - // Session already established, don't recreate - throw NoiseSessionError.alreadyEstablished - } - - // Remove any existing non-established session - if let existingSession = sessions[peerID], !existingSession.isEstablished() { - _ = sessions.removeValue(forKey: peerID) - } - - // Create new initiator session - let session = SecureNoiseSession( - peerID: peerID, - role: .initiator, - keychain: keychain, - localStaticKey: localStaticKey - ) - sessions[peerID] = session - - do { - let handshakeData = try session.startHandshake() - return handshakeData - } catch { - // Clean up failed session - _ = sessions.removeValue(forKey: peerID) - SecureLogger.error(.handshakeFailed(peerID: peerID, error: error.localizedDescription)) - throw error - } - } - } - - func handleIncomingHandshake(from peerID: String, message: Data) throws -> Data? { - // Process everything within the synchronized block to prevent race conditions - return try managerQueue.sync(flags: .barrier) { - var shouldCreateNew = false - var existingSession: NoiseSession? = nil - - if let existing = sessions[peerID] { - // If we have an established session, the peer must have cleared their session - // for a good reason (e.g., decryption failure, restart, etc.) - // We should accept the new handshake to re-establish encryption - if existing.isEstablished() { - SecureLogger.info("Accepting handshake from \(peerID) despite existing session - peer likely cleared their session", category: .session) - _ = sessions.removeValue(forKey: peerID) - shouldCreateNew = true - } else { - // If we're in the middle of a handshake and receive a new initiation, - // reset and start fresh (the other side may have restarted) - if existing.getState() == .handshaking && message.count == 32 { - _ = sessions.removeValue(forKey: peerID) - shouldCreateNew = true - } else { - existingSession = existing - } - } - } else { - shouldCreateNew = true - } - - // Get or create session - let session: NoiseSession - if shouldCreateNew { - let newSession = SecureNoiseSession( - peerID: peerID, - role: .responder, - keychain: keychain, - localStaticKey: localStaticKey - ) - sessions[peerID] = newSession - session = newSession - } else { - session = existingSession! - } - - // Process the handshake message within the synchronized block - do { - let response = try session.processHandshakeMessage(message) - - // Check if session is established after processing - if session.isEstablished() { - if let remoteKey = session.getRemoteStaticPublicKey() { - // Schedule callback outside the synchronized block to prevent deadlock - DispatchQueue.global().async { [weak self] in - self?.onSessionEstablished?(peerID, remoteKey) - } - } - } - - return response - } catch { - // Reset the session on handshake failure so next attempt can start fresh - _ = sessions.removeValue(forKey: peerID) - - // Schedule callback outside the synchronized block to prevent deadlock - DispatchQueue.global().async { [weak self] in - self?.onSessionFailed?(peerID, error) - } - - SecureLogger.error(.handshakeFailed(peerID: peerID, error: error.localizedDescription)) - throw error - } - } - } - - // MARK: - Encryption/Decryption - - func encrypt(_ plaintext: Data, for peerID: String) throws -> Data { - guard let session = getSession(for: peerID) else { - throw NoiseSessionError.sessionNotFound - } - - return try session.encrypt(plaintext) - } - - func decrypt(_ ciphertext: Data, from peerID: String) throws -> Data { - guard let session = getSession(for: peerID) else { - throw NoiseSessionError.sessionNotFound - } - - return try session.decrypt(ciphertext) - } - - // MARK: - Key Management - - func getRemoteStaticKey(for peerID: String) -> Curve25519.KeyAgreement.PublicKey? { - return getSession(for: peerID)?.getRemoteStaticPublicKey() - } - - func getHandshakeHash(for peerID: String) -> Data? { - return getSession(for: peerID)?.getHandshakeHash() - } - - // MARK: - Session Rekeying - - func getSessionsNeedingRekey() -> [(peerID: String, needsRekey: Bool)] { - return managerQueue.sync { - var needingRekey: [(peerID: String, needsRekey: Bool)] = [] - - for (peerID, session) in sessions { - if let secureSession = session as? SecureNoiseSession, - secureSession.isEstablished(), - secureSession.needsRenegotiation() { - needingRekey.append((peerID: peerID, needsRekey: true)) - } - } - - return needingRekey - } - } - - func initiateRekey(for peerID: String) throws { - // Remove old session - removeSession(for: peerID) - - // Initiate new handshake - _ = try initiateHandshake(with: peerID) - - } -} - -// MARK: - Errors - -enum NoiseSessionError: Error { - case invalidState - case notEstablished - case sessionNotFound - case handshakeFailed(Error) - case alreadyEstablished -} diff --git a/bitchat/Noise/NoiseSessionError.swift b/bitchat/Noise/NoiseSessionError.swift new file mode 100644 index 00000000..34a2a3f7 --- /dev/null +++ b/bitchat/Noise/NoiseSessionError.swift @@ -0,0 +1,15 @@ +// +// NoiseSessionError.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +enum NoiseSessionError: Error { + case invalidState + case notEstablished + case sessionNotFound + case handshakeFailed(Error) + case alreadyEstablished +} diff --git a/bitchat/Noise/NoiseSessionManager.swift b/bitchat/Noise/NoiseSessionManager.swift new file mode 100644 index 00000000..cd4ad425 --- /dev/null +++ b/bitchat/Noise/NoiseSessionManager.swift @@ -0,0 +1,239 @@ +// +// NoiseSessionManager.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import BitLogger +import CryptoKit +import Foundation + +final class NoiseSessionManager { + private var sessions: [PeerID: NoiseSession] = [:] + private let localStaticKey: Curve25519.KeyAgreement.PrivateKey + private let keychain: KeychainManagerProtocol + private let managerQueue = DispatchQueue(label: "chat.bitchat.noise.manager", attributes: .concurrent) + + // Callbacks + var onSessionEstablished: ((PeerID, Curve25519.KeyAgreement.PublicKey) -> Void)? + var onSessionFailed: ((PeerID, Error) -> Void)? + + init(localStaticKey: Curve25519.KeyAgreement.PrivateKey, keychain: KeychainManagerProtocol) { + self.localStaticKey = localStaticKey + self.keychain = keychain + } + + // 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] + } + } + + 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() + } + _ = sessions.removeValue(forKey: peerID) + } + } + + func removeAllSessions() { + managerQueue.sync(flags: .barrier) { + for (_, session) in sessions { + session.reset() + } + sessions.removeAll() + } + } + + func getEstablishedSessions() -> [PeerID: NoiseSession] { + return managerQueue.sync { + return sessions.filter { $0.value.isEstablished() } + } + } + + // MARK: - Handshake Helpers + + 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() { + // Session already established, don't recreate + throw NoiseSessionError.alreadyEstablished + } + + // Remove any existing non-established session + if let existingSession = sessions[peerID], !existingSession.isEstablished() { + _ = sessions.removeValue(forKey: peerID) + } + + // Create new initiator session + let session = SecureNoiseSession( + peerID: peerID, + role: .initiator, + keychain: keychain, + localStaticKey: localStaticKey + ) + sessions[peerID] = session + + do { + let handshakeData = try session.startHandshake() + return handshakeData + } catch { + // Clean up failed session + _ = sessions.removeValue(forKey: peerID) + SecureLogger.error(.handshakeFailed(peerID: peerID.id, error: error.localizedDescription)) + throw error + } + } + } + + 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 + var existingSession: NoiseSession? = nil + + if let existing = sessions[peerID] { + // If we have an established session, the peer must have cleared their session + // for a good reason (e.g., decryption failure, restart, etc.) + // We should accept the new handshake to re-establish encryption + if existing.isEstablished() { + SecureLogger.info("Accepting handshake from \(peerID) despite existing session - peer likely cleared their session", category: .session) + _ = sessions.removeValue(forKey: peerID) + shouldCreateNew = true + } else { + // If we're in the middle of a handshake and receive a new initiation, + // reset and start fresh (the other side may have restarted) + if existing.getState() == .handshaking && message.count == 32 { + _ = sessions.removeValue(forKey: peerID) + shouldCreateNew = true + } else { + existingSession = existing + } + } + } else { + shouldCreateNew = true + } + + // Get or create session + let session: NoiseSession + if shouldCreateNew { + let newSession = SecureNoiseSession( + peerID: peerID, + role: .responder, + keychain: keychain, + localStaticKey: localStaticKey + ) + sessions[peerID] = newSession + session = newSession + } else { + session = existingSession! + } + + // Process the handshake message within the synchronized block + do { + let response = try session.processHandshakeMessage(message) + + // Check if session is established after processing + if session.isEstablished() { + if let remoteKey = session.getRemoteStaticPublicKey() { + // Schedule callback outside the synchronized block to prevent deadlock + DispatchQueue.global().async { [weak self] in + self?.onSessionEstablished?(peerID, remoteKey) + } + } + } + + return response + } catch { + // Reset the session on handshake failure so next attempt can start fresh + _ = sessions.removeValue(forKey: peerID) + + // Schedule callback outside the synchronized block to prevent deadlock + DispatchQueue.global().async { [weak self] in + self?.onSessionFailed?(peerID, error) + } + + SecureLogger.error(.handshakeFailed(peerID: peerID.id, error: error.localizedDescription)) + throw error + } + } + } + + // MARK: - Encryption/Decryption + + func encrypt(_ plaintext: Data, for peerID: PeerID) throws -> Data { + guard let session = getSession(for: peerID) else { + throw NoiseSessionError.sessionNotFound + } + + return try session.encrypt(plaintext) + } + + func decrypt(_ ciphertext: Data, from peerID: PeerID) throws -> Data { + guard let session = getSession(for: peerID) else { + throw NoiseSessionError.sessionNotFound + } + + return try session.decrypt(ciphertext) + } + + // MARK: - Key Management + + func getRemoteStaticKey(for peerID: PeerID) -> Curve25519.KeyAgreement.PublicKey? { + 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)] { + return managerQueue.sync { + var needingRekey: [(peerID: PeerID, needsRekey: Bool)] = [] + + for (peerID, session) in sessions { + if let secureSession = session as? SecureNoiseSession, + secureSession.isEstablished(), + secureSession.needsRenegotiation() { + needingRekey.append((peerID: peerID, needsRekey: true)) + } + } + + return needingRekey + } + } + + func initiateRekey(for peerID: PeerID) throws { + // Remove old session + removeSession(for: peerID) + + // Initiate new handshake + _ = try initiateHandshake(with: peerID) + } +} diff --git a/bitchat/Noise/NoiseSessionState.swift b/bitchat/Noise/NoiseSessionState.swift new file mode 100644 index 00000000..161cd164 --- /dev/null +++ b/bitchat/Noise/NoiseSessionState.swift @@ -0,0 +1,13 @@ +// +// NoiseSessionState.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +enum NoiseSessionState: Equatable { + case uninitialized + case handshaking + case established +} diff --git a/bitchat/Services/NoiseEncryptionService.swift b/bitchat/Services/NoiseEncryptionService.swift index 98630a19..aa0d6ea6 100644 --- a/bitchat/Services/NoiseEncryptionService.swift +++ b/bitchat/Services/NoiseEncryptionService.swift @@ -178,7 +178,7 @@ final class NoiseEncryptionService { // Callbacks private var onPeerAuthenticatedHandlers: [((String, String) -> Void)] = [] // Array of handlers for peer authentication - var onHandshakeRequired: ((String) -> Void)? // peerID needs handshake + var onHandshakeRequired: ((PeerID) -> Void)? // peerID needs handshake // Add a handler for peer authentication func addOnPeerAuthenticatedHandler(_ handler: @escaping (String, String) -> Void) { @@ -251,7 +251,7 @@ final class NoiseEncryptionService { // Set up session callbacks sessionManager.onSessionEstablished = { [weak self] peerID, remoteStaticKey in - self?.handleSessionEstablished(peerID: PeerID(str: peerID), remoteStaticKey: remoteStaticKey) + self?.handleSessionEstablished(peerID: peerID, remoteStaticKey: remoteStaticKey) } // Start session maintenance timer @@ -277,7 +277,7 @@ final class NoiseEncryptionService { /// Get peer's public key data func getPeerPublicKeyData(_ peerID: PeerID) -> Data? { - return sessionManager.getRemoteStaticKey(for: peerID.id)?.rawRepresentation + return sessionManager.getRemoteStaticKey(for: peerID)?.rawRepresentation } /// Clear persistent identity (for panic mode) @@ -422,7 +422,7 @@ final class NoiseEncryptionService { // Return raw handshake data without wrapper // The Noise protocol handles its own message format - let handshakeData = try sessionManager.initiateHandshake(with: peerID.id) + let handshakeData = try sessionManager.initiateHandshake(with: peerID) return handshakeData } @@ -449,7 +449,7 @@ final class NoiseEncryptionService { // For handshakes, we process the raw data directly without NoiseMessage wrapper // The Noise protocol handles its own message format - let responsePayload = try sessionManager.handleIncomingHandshake(from: peerID.id, message: message) + let responsePayload = try sessionManager.handleIncomingHandshake(from: peerID, message: message) // Return raw response without wrapper @@ -458,12 +458,12 @@ final class NoiseEncryptionService { /// Check if we have an established session with a peer func hasEstablishedSession(with peerID: PeerID) -> Bool { - return sessionManager.getSession(for: peerID.id)?.isEstablished() ?? false + return sessionManager.getSession(for: peerID)?.isEstablished() ?? false } /// Check if we have a session (established or handshaking) with a peer func hasSession(with peerID: PeerID) -> Bool { - return sessionManager.getSession(for: peerID.id) != nil + return sessionManager.getSession(for: peerID) != nil } // MARK: - Encryption/Decryption @@ -483,11 +483,11 @@ final class NoiseEncryptionService { // Check if we have an established session guard hasEstablishedSession(with: peerID) else { // Signal that handshake is needed - onHandshakeRequired?(peerID.id) + onHandshakeRequired?(peerID) throw NoiseEncryptionError.handshakeRequired } - return try sessionManager.encrypt(data, for: peerID.id) + return try sessionManager.encrypt(data, for: peerID) } /// Decrypt data from a specific peer @@ -507,7 +507,7 @@ final class NoiseEncryptionService { throw NoiseEncryptionError.sessionNotEstablished } - return try sessionManager.decrypt(data, from: peerID.id) + return try sessionManager.decrypt(data, from: peerID) } // MARK: - Peer Management diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index c5428e2d..2ac5d18c 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -4380,10 +4380,10 @@ final class ChatViewModel: ObservableObject, BitchatDelegate { noiseService.onHandshakeRequired = { [weak self] peerID in DispatchQueue.main.async { guard let self = self else { return } - self.peerEncryptionStatus[peerID] = .noiseHandshaking + self.peerEncryptionStatus[peerID.id] = .noiseHandshaking // Invalidate cache when encryption status changes - self.invalidateEncryptionCache(for: peerID) + self.invalidateEncryptionCache(for: peerID.id) } } } diff --git a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift index c7ce1bae..0e04776d 100644 --- a/bitchatTests/EndToEnd/PrivateChatE2ETests.swift +++ b/bitchatTests/EndToEnd/PrivateChatE2ETests.swift @@ -301,9 +301,9 @@ final class PrivateChatE2ETests: XCTestCase { // MARK: - Helper Methods - private func createMockService(peerID: String, nickname: String) -> MockBluetoothMeshService { + private func createMockService(peerID: PeerID, nickname: String) -> MockBluetoothMeshService { let service = MockBluetoothMeshService() - service.myPeerID = peerID + service.myPeerID = peerID.id service.mockNickname = nickname return service } diff --git a/bitchatTests/EndToEnd/PublicChatE2ETests.swift b/bitchatTests/EndToEnd/PublicChatE2ETests.swift index 46b9ef73..c88edb4d 100644 --- a/bitchatTests/EndToEnd/PublicChatE2ETests.swift +++ b/bitchatTests/EndToEnd/PublicChatE2ETests.swift @@ -200,9 +200,9 @@ final class PublicChatE2ETests: XCTestCase { } // Inject at Bob with TTL=2 so Charlie sees it (TTL->1) and does not relay to David - let msg = TestHelpers.createTestMessage(content: TestConstants.testMessage1, sender: TestConstants.testNickname1, senderPeerID: alice.peerID) + let msg = TestHelpers.createTestMessage(content: TestConstants.testMessage1, sender: TestConstants.testNickname1, senderPeerID: PeerID(str: alice.peerID)) if let payload = msg.toBinaryPayload() { - let pkt = TestHelpers.createTestPacket(senderID: alice.peerID, payload: payload, ttl: 2) + let pkt = TestHelpers.createTestPacket(senderID: PeerID(str: alice.peerID), payload: payload, ttl: 2) bob.simulateIncomingPacket(pkt) } @@ -419,9 +419,9 @@ final class PublicChatE2ETests: XCTestCase { // MARK: - Helper Methods - private func createMockService(peerID: String, nickname: String) -> MockBluetoothMeshService { + private func createMockService(peerID: PeerID, nickname: String) -> MockBluetoothMeshService { let service = MockBluetoothMeshService() - service.myPeerID = peerID + service.myPeerID = peerID.id service.mockNickname = nickname return service } diff --git a/bitchatTests/Integration/IntegrationTests.swift b/bitchatTests/Integration/IntegrationTests.swift index 39c951fb..43a32bfe 100644 --- a/bitchatTests/Integration/IntegrationTests.swift +++ b/bitchatTests/Integration/IntegrationTests.swift @@ -484,10 +484,13 @@ final class IntegrationTests: XCTestCase { guard let aliceManager = noiseManagers["Alice"], let bobManager = noiseManagers["Bob"], - let alicePeerID = nodes["Alice"]?.peerID, - let bobPeerID = nodes["Bob"]?.peerID else { + let aliceStringPeerID = nodes["Alice"]?.peerID, + let bobStringPeerID = nodes["Bob"]?.peerID else { return XCTFail("Missing managers or peer IDs") } + + let alicePeerID = PeerID(str: aliceStringPeerID) + let bobPeerID = PeerID(str: bobStringPeerID) // Baseline: encrypt from Alice, decrypt at Bob let plaintext1 = Data("hello-secure".utf8) @@ -587,9 +590,9 @@ final class IntegrationTests: XCTestCase { // MARK: - Helper Methods - private func createNode(_ name: String, peerID: String) { + private func createNode(_ name: String, peerID: PeerID) { let node = MockBluetoothMeshService() - node.myPeerID = peerID + node.myPeerID = peerID.id node.mockNickname = name nodes[name] = node @@ -666,9 +669,9 @@ final class IntegrationTests: XCTestCase { let peer1ID = nodes[node1]?.peerID, let peer2ID = nodes[node2]?.peerID else { return } - let msg1 = try manager1.initiateHandshake(with: peer2ID) - let msg2 = try manager2.handleIncomingHandshake(from: peer1ID, message: msg1)! - let msg3 = try manager1.handleIncomingHandshake(from: peer2ID, message: msg2)! - _ = try manager2.handleIncomingHandshake(from: peer1ID, message: msg3) + let msg1 = try manager1.initiateHandshake(with: PeerID(str: peer2ID)) + let msg2 = try manager2.handleIncomingHandshake(from: PeerID(str: peer1ID), message: msg1)! + let msg3 = try manager1.handleIncomingHandshake(from: PeerID(str: peer2ID), message: msg2)! + _ = try manager2.handleIncomingHandshake(from: PeerID(str: peer1ID), message: msg3) } } diff --git a/bitchatTests/Mocks/MockBLEService.swift b/bitchatTests/Mocks/MockBLEService.swift index 23217c86..eddb178f 100644 --- a/bitchatTests/Mocks/MockBLEService.swift +++ b/bitchatTests/Mocks/MockBLEService.swift @@ -342,8 +342,8 @@ final class MockBLEService: NSObject { // MARK: - Compatibility methods for old tests - func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) { - sendPrivateMessage(content, to: recipientPeerID, recipientNickname: recipientNickname, messageID: messageID ?? UUID().uuidString) + func sendPrivateMessage(_ content: String, to recipientPeerID: PeerID, recipientNickname: String, messageID: String? = nil) { + sendPrivateMessage(content, to: recipientPeerID.id, recipientNickname: recipientNickname, messageID: messageID ?? UUID().uuidString) } } diff --git a/bitchatTests/Protocol/BinaryProtocolTests.swift b/bitchatTests/Protocol/BinaryProtocolTests.swift index 3ea6d163..084ead6b 100644 --- a/bitchatTests/Protocol/BinaryProtocolTests.swift +++ b/bitchatTests/Protocol/BinaryProtocolTests.swift @@ -54,7 +54,7 @@ final class BinaryProtocolTests: XCTestCase { // Verify recipient XCTAssertNotNil(decodedPacket.recipientID) let decodedRecipientID = decodedPacket.recipientID?.trimmingNullBytes() - XCTAssertEqual(String(data: decodedRecipientID!, encoding: .utf8), recipientID) + XCTAssertTrue(String(data: decodedRecipientID!, encoding: .utf8) == recipientID) } func testPacketWithSignature() throws { @@ -251,7 +251,7 @@ final class BinaryProtocolTests: XCTestCase { originalSender: TestConstants.testNickname3, isPrivate: false, recipientNickname: nil, - senderPeerID: TestConstants.testPeerID1, + senderPeerID: TestConstants.testPeerID1.id, mentions: nil ) diff --git a/bitchatTests/TestUtilities/TestConstants.swift b/bitchatTests/TestUtilities/TestConstants.swift index be4670ab..4ae436bb 100644 --- a/bitchatTests/TestUtilities/TestConstants.swift +++ b/bitchatTests/TestUtilities/TestConstants.swift @@ -7,16 +7,17 @@ // import Foundation +@testable import bitchat struct TestConstants { static let defaultTimeout: TimeInterval = 5.0 static let shortTimeout: TimeInterval = 1.0 static let longTimeout: TimeInterval = 10.0 - static let testPeerID1 = "PEER1234" - static let testPeerID2 = "PEER5678" - static let testPeerID3 = "PEER9012" - static let testPeerID4 = "PEER3456" + static let testPeerID1: PeerID = "PEER1234" + static let testPeerID2: PeerID = "PEER5678" + static let testPeerID3: PeerID = "PEER9012" + static let testPeerID4: PeerID = "PEER3456" static let testNickname1 = "Alice" static let testNickname2 = "Bob" diff --git a/bitchatTests/TestUtilities/TestHelpers.swift b/bitchatTests/TestUtilities/TestHelpers.swift index deaedd24..5c4e2377 100644 --- a/bitchatTests/TestUtilities/TestHelpers.swift +++ b/bitchatTests/TestUtilities/TestHelpers.swift @@ -30,7 +30,7 @@ final class TestHelpers { static func createTestMessage( content: String = TestConstants.testMessage1, sender: String = TestConstants.testNickname1, - senderPeerID: String = TestConstants.testPeerID1, + senderPeerID: PeerID = TestConstants.testPeerID1, isPrivate: Bool = false, recipientNickname: String? = nil, mentions: [String]? = nil @@ -44,23 +44,23 @@ final class TestHelpers { originalSender: nil, isPrivate: isPrivate, recipientNickname: recipientNickname, - senderPeerID: senderPeerID, + senderPeerID: senderPeerID.id, mentions: mentions ) } static func createTestPacket( type: UInt8 = 0x01, - senderID: String = TestConstants.testPeerID1, - recipientID: String? = nil, + senderID: PeerID = TestConstants.testPeerID1, + recipientID: PeerID? = nil, payload: Data = "test payload".data(using: .utf8)!, signature: Data? = nil, ttl: UInt8 = 3 ) -> BitchatPacket { return BitchatPacket( type: type, - senderID: senderID.data(using: .utf8)!, - recipientID: recipientID?.data(using: .utf8), + senderID: senderID.id.data(using: .utf8)!, + recipientID: recipientID?.id.data(using: .utf8), timestamp: UInt64(Date().timeIntervalSince1970 * 1000), payload: payload, signature: signature, @@ -119,4 +119,16 @@ enum TestError: Error { case timeout case unexpectedValue case testFailure(String) -} \ No newline at end of file +} + +// MARK: - PeerID String Helpers + +/// Raw String can be passed as PeerID +extension PeerID: @retroactive ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(str: value) + } +} + +/// Interpolated String can be passed as PeerID +extension PeerID: @retroactive ExpressibleByStringInterpolation {}