From cf528b0dafa123928e4cc28d8495d3a724d3a118 Mon Sep 17 00:00:00 2001 From: Nadim Kobeissi Date: Tue, 18 Nov 2025 19:22:19 +0200 Subject: [PATCH] Move new Noise test vectors into XCTests (WIP) --- bitchat.xcodeproj/project.pbxproj | 19 +- bitchatTests/Noise/NoiseProtocolTests.swift | 1448 +++++++++---------- 2 files changed, 736 insertions(+), 731 deletions(-) diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index e062000b..8a5b6b1a 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -581,10 +581,11 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGNING_ALLOWED = YES; CODE_SIGNING_REQUIRED = YES; - CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = N762Z3YT9R; INFOPLIST_FILE = bitchatTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -593,6 +594,7 @@ ); MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)"; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).tests"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = macosx; SWIFT_VERSION = "$(SWIFT_VERSION)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bitchat.app/Contents/MacOS/bitchat"; @@ -643,10 +645,10 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGNING_REQUIRED = YES; CODE_SIGN_ENTITLEMENTS = "bitchat/bitchat-macOS.entitlements"; - CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)"; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = bitchat; @@ -659,6 +661,7 @@ MARKETING_VERSION = "$(MARKETING_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = bitchat; + PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; SDKROOT = macosx; SWIFT_VERSION = "$(SWIFT_VERSION)"; @@ -735,10 +738,11 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGNING_REQUIRED = YES; CODE_SIGN_ENTITLEMENTS = "bitchat/bitchat-macOS.entitlements"; - CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)"; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)"; + DEVELOPMENT_TEAM = N762Z3YT9R; ENABLE_PREVIEWS = NO; INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = bitchat; @@ -749,8 +753,9 @@ ); MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)"; MARKETING_VERSION = "$(MARKETING_VERSION)"; - PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)"; + PRODUCT_BUNDLE_IDENTIFIER = nadimkobeissi.wowie.zowie; PRODUCT_NAME = bitchat; + PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; SDKROOT = macosx; SWIFT_VERSION = "$(SWIFT_VERSION)"; diff --git a/bitchatTests/Noise/NoiseProtocolTests.swift b/bitchatTests/Noise/NoiseProtocolTests.swift index a6652093..3b790642 100644 --- a/bitchatTests/Noise/NoiseProtocolTests.swift +++ b/bitchatTests/Noise/NoiseProtocolTests.swift @@ -15,734 +15,734 @@ import Testing // MARK: - Test Vector Support struct NoiseTestVector: Codable { - let protocol_name: String - let init_prologue: String - let init_static: String - let init_ephemeral: String - let init_psks: [String]? - let resp_prologue: String - let resp_static: String - let resp_ephemeral: String - let resp_psks: [String]? - let handshake_hash: String? - let messages: [TestMessage] - - struct TestMessage: Codable { - let payload: String - let ciphertext: String - } + let protocol_name: String + let init_prologue: String + let init_static: String + let init_ephemeral: String + let init_psks: [String]? + let resp_prologue: String + let resp_static: String + let resp_ephemeral: String + let resp_psks: [String]? + let handshake_hash: String? + let messages: [TestMessage] + + struct TestMessage: Codable { + let payload: String + let ciphertext: String + } } extension Data { - init?(hex: String) { - let cleaned = hex.replacingOccurrences(of: " ", with: "") - guard cleaned.count % 2 == 0 else { return nil } - var data = Data(capacity: cleaned.count / 2) - var index = cleaned.startIndex - while index < cleaned.endIndex { - let nextIndex = cleaned.index(index, offsetBy: 2) - guard let byte = UInt8(cleaned[index.. String { - map { String(format: "%02x", $0) }.joined() - } + init?(hex: String) { + let cleaned = hex.replacingOccurrences(of: " ", with: "") + guard cleaned.count % 2 == 0 else { return nil } + var data = Data(capacity: cleaned.count / 2) + var index = cleaned.startIndex + while index < cleaned.endIndex { + let nextIndex = cleaned.index(index, offsetBy: 2) + guard let byte = UInt8(cleaned[index.. String { + map { String(format: "%02x", $0) }.joined() + } } struct NoiseProtocolTests { - - private let aliceKey = Curve25519.KeyAgreement.PrivateKey() - private let bobKey = Curve25519.KeyAgreement.PrivateKey() - private let mockKeychain = MockKeychain() - - private let alicePeerID = PeerID(str: UUID().uuidString) - private let bobPeerID = PeerID(str: UUID().uuidString) - - private let aliceSession: NoiseSession - private let bobSession: NoiseSession - - init() { - aliceSession = NoiseSession( - peerID: alicePeerID, - role: .initiator, - keychain: mockKeychain, - localStaticKey: aliceKey - ) - - bobSession = NoiseSession( - peerID: bobPeerID, - role: .responder, - keychain: mockKeychain, - localStaticKey: bobKey - ) - } - - // MARK: - Basic Handshake Tests - - @Test func xxPatternHandshake() throws { - // Alice starts handshake (message 1) - let message1 = try aliceSession.startHandshake() - #expect(!message1.isEmpty) - #expect(aliceSession.getState() == .handshaking) - - // Bob processes message 1 and creates message 2 - let message2 = try bobSession.processHandshakeMessage(message1) - #expect(message2 != nil) - #expect(!message2!.isEmpty) - #expect(bobSession.getState() == .handshaking) - - // Alice processes message 2 and creates message 3 - let message3 = try aliceSession.processHandshakeMessage(message2!) - #expect(message3 != nil) - #expect(!message3!.isEmpty) - #expect(aliceSession.getState() == .established) - - // Bob processes message 3 and completes handshake - let finalMessage = try bobSession.processHandshakeMessage(message3!) - #expect(finalMessage == nil) // No more messages needed - #expect(bobSession.getState() == .established) - - // Verify both sessions are established - #expect(aliceSession.isEstablished()) - #expect(bobSession.isEstablished()) - - // Verify they have each other's static keys - #expect( - aliceSession.getRemoteStaticPublicKey()?.rawRepresentation - == bobKey.publicKey.rawRepresentation) - #expect( - bobSession.getRemoteStaticPublicKey()?.rawRepresentation - == aliceKey.publicKey.rawRepresentation) - } - - @Test func handshakeStateValidation() throws { - // Cannot process message before starting handshake - #expect(throws: NoiseSessionError.invalidState) { - try aliceSession.processHandshakeMessage(Data()) - } - - // Start handshake - _ = try aliceSession.startHandshake() - - // Cannot start handshake twice - #expect(throws: NoiseSessionError.invalidState) { - try aliceSession.startHandshake() - } - } - - // MARK: - Encryption/Decryption Tests - - @Test func basicEncryptionDecryption() throws { - try performHandshake(initiator: aliceSession, responder: bobSession) - - let plaintext = "Hello, Bob!".data(using: .utf8)! - - // Alice encrypts - let ciphertext = try aliceSession.encrypt(plaintext) - #expect(ciphertext != plaintext) - #expect(ciphertext.count > plaintext.count) // Should have overhead - - // Bob decrypts - let decrypted = try bobSession.decrypt(ciphertext) - #expect(decrypted == plaintext) - } - - @Test func bidirectionalEncryption() throws { - try performHandshake(initiator: aliceSession, responder: bobSession) - - // Alice -> Bob - let aliceMessage = "Hello from Alice".data(using: .utf8)! - let aliceCiphertext = try aliceSession.encrypt(aliceMessage) - let bobReceived = try bobSession.decrypt(aliceCiphertext) - #expect(bobReceived == aliceMessage) - - // Bob -> Alice - let bobMessage = "Hello from Bob".data(using: .utf8)! - let bobCiphertext = try bobSession.encrypt(bobMessage) - let aliceReceived = try aliceSession.decrypt(bobCiphertext) - #expect(aliceReceived == bobMessage) - } - - @Test func largeMessageEncryption() throws { - try performHandshake(initiator: aliceSession, responder: bobSession) - - // Create a large message - let largeMessage = TestHelpers.generateRandomData(length: 100_000) - - // Encrypt and decrypt - let ciphertext = try aliceSession.encrypt(largeMessage) - let decrypted = try bobSession.decrypt(ciphertext) - - #expect(decrypted == largeMessage) - } - - @Test func encryptionBeforeHandshake() { - let plaintext = "test".data(using: .utf8)! - - #expect(throws: NoiseSessionError.notEstablished) { - try aliceSession.encrypt(plaintext) - } - - #expect(throws: NoiseSessionError.notEstablished) { - try aliceSession.decrypt(plaintext) - } - } - - // MARK: - Session Manager Tests - - @Test func sessionManagerBasicOperations() throws { - let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) - - #expect(manager.getSession(for: alicePeerID) == nil) - - _ = try manager.initiateHandshake(with: alicePeerID) - #expect(manager.getSession(for: alicePeerID) != nil) - - // Get session - let retrieved = manager.getSession(for: alicePeerID) - #expect(retrieved != nil) - - // Remove session - manager.removeSession(for: alicePeerID) - #expect(manager.getSession(for: alicePeerID) == nil) - } - - @Test func sessionManagerHandshakeInitiation() throws { - let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) - - // Initiate handshake - let handshakeData = try manager.initiateHandshake(with: alicePeerID) - #expect(!handshakeData.isEmpty) - - // Session should exist - let session = manager.getSession(for: alicePeerID) - #expect(session != nil) - #expect(session?.getState() == .handshaking) - } - - @Test func sessionManagerIncomingHandshake() throws { - let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) - let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) - - // Alice initiates - let message1 = try aliceManager.initiateHandshake(with: alicePeerID) - - // Bob responds - let message2 = try bobManager.handleIncomingHandshake(from: bobPeerID, message: message1) - #expect(message2 != nil) - - // Continue handshake - let message3 = try aliceManager.handleIncomingHandshake( - from: alicePeerID, message: message2!) - #expect(message3 != nil) - - // Complete handshake - let finalMessage = try bobManager.handleIncomingHandshake( - from: bobPeerID, message: message3!) - #expect(finalMessage == nil) - - // Both should have established sessions - #expect(aliceManager.getSession(for: alicePeerID)?.isEstablished() == true) - #expect(bobManager.getSession(for: bobPeerID)?.isEstablished() == true) - } - - @Test func sessionManagerEncryptionDecryption() throws { - let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) - let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) - - // Establish sessions - try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) - - // Encrypt with manager - let plaintext = "Test message".data(using: .utf8)! - let ciphertext = try aliceManager.encrypt(plaintext, for: alicePeerID) - - // Decrypt with manager - let decrypted = try bobManager.decrypt(ciphertext, from: bobPeerID) - #expect(decrypted == plaintext) - } - - // MARK: - Security Tests - - @Test func tamperedCiphertextDetection() throws { - try performHandshake(initiator: aliceSession, responder: bobSession) - - let plaintext = "Secret message".data(using: .utf8)! - var ciphertext = try aliceSession.encrypt(plaintext) - - // Tamper with ciphertext - ciphertext[ciphertext.count / 2] ^= 0xFF - - // Decryption should fail - if #available(macOS 14.4, iOS 17.4, *) { - #expect(throws: CryptoKitError.authenticationFailure) { - try bobSession.decrypt(ciphertext) - } - } else { - #expect(throws: (any Error).self) { - try bobSession.decrypt(ciphertext) - } - } - } - - @Test func replayPrevention() throws { - try performHandshake(initiator: aliceSession, responder: bobSession) - - let plaintext = "Test message".data(using: .utf8)! - let ciphertext = try aliceSession.encrypt(plaintext) - - // First decryption should succeed - _ = try bobSession.decrypt(ciphertext) - - // Replaying the same ciphertext should fail - #expect(throws: NoiseError.replayDetected) { - try bobSession.decrypt(ciphertext) - } - } - - @Test func sessionIsolation() throws { - // Create two separate session pairs - let aliceSession1 = NoiseSession( - peerID: PeerID(str: "peer1"), role: .initiator, keychain: mockKeychain, - localStaticKey: aliceKey) - let bobSession1 = NoiseSession( - peerID: PeerID(str: "alice1"), role: .responder, keychain: mockKeychain, - localStaticKey: bobKey) - - let aliceSession2 = NoiseSession( - peerID: PeerID(str: "peer2"), role: .initiator, keychain: mockKeychain, - localStaticKey: aliceKey) - let bobSession2 = NoiseSession( - peerID: PeerID(str: "alice2"), role: .responder, keychain: mockKeychain, - localStaticKey: bobKey) - - // Establish both pairs - try performHandshake(initiator: aliceSession1, responder: bobSession1) - try performHandshake(initiator: aliceSession2, responder: bobSession2) - - // Encrypt with session 1 - let plaintext = "Secret".data(using: .utf8)! - let ciphertext1 = try aliceSession1.encrypt(plaintext) - - // Should not be able to decrypt with session 2 - if #available(macOS 14.4, iOS 17.4, *) { - #expect(throws: CryptoKitError.authenticationFailure) { - try bobSession2.decrypt(ciphertext1) - } - } else { - #expect(throws: (any Error).self) { - try bobSession2.decrypt(ciphertext1) - } - } - - // But should work with correct session - let decrypted = try bobSession1.decrypt(ciphertext1) - #expect(decrypted == plaintext) - } - - // MARK: - Session Recovery Tests - - @Test func peerRestartDetection() throws { - // Establish initial sessions - let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) - let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) - - try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) - - // Exchange some messages to establish nonce state - let message1 = try aliceManager.encrypt("Hello".data(using: .utf8)!, for: alicePeerID) - _ = try bobManager.decrypt(message1, from: bobPeerID) - - let message2 = try bobManager.encrypt("World".data(using: .utf8)!, for: bobPeerID) - _ = try aliceManager.decrypt(message2, from: alicePeerID) - - // Simulate Bob restart by creating new manager with same key - let bobManagerRestarted = NoiseSessionManager( - localStaticKey: bobKey, keychain: mockKeychain) - - // Bob initiates new handshake after restart - let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: bobPeerID) - - // Alice should accept the new handshake (clearing old session) - let newHandshake2 = try aliceManager.handleIncomingHandshake( - from: alicePeerID, message: newHandshake1) - #expect(newHandshake2 != nil) - - // Complete the new handshake - let newHandshake3 = try bobManagerRestarted.handleIncomingHandshake( - from: bobPeerID, message: newHandshake2!) - #expect(newHandshake3 != nil) - _ = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: newHandshake3!) - - // Should be able to exchange messages with new sessions - let testMessage = "After restart".data(using: .utf8)! - let encrypted = try bobManagerRestarted.encrypt(testMessage, for: bobPeerID) - let decrypted = try aliceManager.decrypt(encrypted, from: alicePeerID) - #expect(decrypted == testMessage) - } - - @Test func nonceDesynchronizationRecovery() throws { - // Create two sessions - let aliceSession = NoiseSession( - peerID: alicePeerID, role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) - let bobSession = NoiseSession( - peerID: bobPeerID, role: .responder, keychain: mockKeychain, localStaticKey: bobKey) - - // Establish sessions - try performHandshake(initiator: aliceSession, responder: bobSession) - - // Exchange messages to advance nonces - for i in 0..<5 { - let msg = try aliceSession.encrypt("Message \(i)".data(using: .utf8)!) - _ = try bobSession.decrypt(msg) - } - - // Simulate desynchronization by encrypting but not decrypting - for i in 0..<3 { - _ = try aliceSession.encrypt("Lost message \(i)".data(using: .utf8)!) - } - - // With per-packet nonce carried, decryption should not throw here - let desyncMessage = try aliceSession.encrypt("This now succeeds".data(using: .utf8)!) - #expect(throws: Never.self) { - try bobSession.decrypt(desyncMessage) - } - } - - @Test func concurrentEncryption() async throws { - // Test thread safety of encryption operations - let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) - let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) - - try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) - - let messageCount = 100 - - try await confirmation("All messages encrypted and decrypted", expectedCount: messageCount) - { completion in - var encryptedMessages: [Int: Data] = [:] - // Encrypt messages sequentially to avoid nonce races in manager - for i in 0.. [NoiseTestVector] { - // Try to load from test bundle - let testBundle = Bundle(for: MockKeychain.self) - guard let url = testBundle.url(forResource: "NoiseTestVectors", withExtension: "json") - else { - throw NSError( - domain: "NoiseTests", code: 1, - userInfo: [ - NSLocalizedDescriptionKey: "Could not find NoiseTestVectors.json in test bundle" - ]) - } - - let data = try Data(contentsOf: url) - return try JSONDecoder().decode([NoiseTestVector].self, from: data) - } - - private func runTestVector(_ testVector: NoiseTestVector) throws { - // Parse test inputs - guard let initStatic = Data(hex: testVector.init_static), - let initEphemeral = Data(hex: testVector.init_ephemeral), - let respStatic = Data(hex: testVector.resp_static), - let respEphemeral = Data(hex: testVector.resp_ephemeral), - let prologue = Data(hex: testVector.init_prologue) - else { - throw NSError( - domain: "NoiseTests", code: 2, - userInfo: [NSLocalizedDescriptionKey: "Failed to parse test vector hex strings"]) - } - - let expectedHash = testVector.handshake_hash.flatMap { Data(hex: $0) } - - // Create keys - guard - let initStaticKey = try? Curve25519.KeyAgreement.PrivateKey( - rawRepresentation: initStatic), - let initEphemeralKey = try? Curve25519.KeyAgreement.PrivateKey( - rawRepresentation: initEphemeral), - let respStaticKey = try? Curve25519.KeyAgreement.PrivateKey( - rawRepresentation: respStatic), - let respEphemeralKey = try? Curve25519.KeyAgreement.PrivateKey( - rawRepresentation: respEphemeral) - else { - throw NSError( - domain: "NoiseTests", code: 3, - userInfo: [NSLocalizedDescriptionKey: "Failed to create keys from test vectors"]) - } - - let keychain = MockKeychain() - - // Create handshake states - let initiatorHandshake = NoiseHandshakeState( - role: .initiator, - pattern: .XX, - keychain: keychain, - localStaticKey: initStaticKey, - prologue: prologue, - predeterminedEphemeralKey: initEphemeralKey - ) - - let responderHandshake = NoiseHandshakeState( - role: .responder, - pattern: .XX, - keychain: keychain, - localStaticKey: respStaticKey, - prologue: prologue, - predeterminedEphemeralKey: respEphemeralKey - ) - - // Message 1: Initiator -> Responder (e) - let msg1 = try initiatorHandshake.writeMessage() - #expect(!msg1.isEmpty, "Message 1 should not be empty") - - _ = try responderHandshake.readMessage(msg1) - - // Message 2: Responder -> Initiator (e, ee, s, es) - let msg2 = try responderHandshake.writeMessage() - #expect(!msg2.isEmpty, "Message 2 should not be empty") - - _ = try initiatorHandshake.readMessage(msg2) - - // Message 3: Initiator -> Responder (s, se) - let msg3 = try initiatorHandshake.writeMessage() - #expect(!msg3.isEmpty, "Message 3 should not be empty") - - _ = try responderHandshake.readMessage(msg3) - - // Verify handshake hash - let initiatorHash = initiatorHandshake.getHandshakeHash() - let responderHash = responderHandshake.getHandshakeHash() - - #expect(initiatorHash == responderHash, "Initiator and responder hashes should match") - - if let expectedHash = expectedHash { - #expect( - initiatorHash == expectedHash, - "Handshake hash should match expected value from test vector") - } - - // Get transport ciphers - let (initSend, initRecv) = try initiatorHandshake.getTransportCiphers() - let (respSend, respRecv) = try responderHandshake.getTransportCiphers() - - // Test transport messages - for (index, testMsg) in testVector.messages.enumerated() { - guard let payload = Data(hex: testMsg.payload) else { - throw NSError( - domain: "NoiseTests", code: 4, - userInfo: [ - NSLocalizedDescriptionKey: - "Message \(index + 1): Failed to parse payload hex" - ]) - } - - // Alternate between initiator and responder sending - let (sender, receiver): (NoiseCipherState, NoiseCipherState) - if index % 2 == 0 { - sender = initSend - receiver = respRecv - } else { - sender = respSend - receiver = initRecv - } - - // Encrypt - let ciphertext = try sender.encrypt(plaintext: payload) - - // Decrypt - let decrypted = try receiver.decrypt(ciphertext: ciphertext) - - #expect( - decrypted == payload, - "Message \(index + 1): Decrypted payload should match original") - } - } + + private let aliceKey = Curve25519.KeyAgreement.PrivateKey() + private let bobKey = Curve25519.KeyAgreement.PrivateKey() + private let mockKeychain = MockKeychain() + + private let alicePeerID = PeerID(str: UUID().uuidString) + private let bobPeerID = PeerID(str: UUID().uuidString) + + private let aliceSession: NoiseSession + private let bobSession: NoiseSession + + init() { + aliceSession = NoiseSession( + peerID: alicePeerID, + role: .initiator, + keychain: mockKeychain, + localStaticKey: aliceKey + ) + + bobSession = NoiseSession( + peerID: bobPeerID, + role: .responder, + keychain: mockKeychain, + localStaticKey: bobKey + ) + } + + // MARK: - Basic Handshake Tests + + @Test func xxPatternHandshake() throws { + // Alice starts handshake (message 1) + let message1 = try aliceSession.startHandshake() + #expect(!message1.isEmpty) + #expect(aliceSession.getState() == .handshaking) + + // Bob processes message 1 and creates message 2 + let message2 = try bobSession.processHandshakeMessage(message1) + #expect(message2 != nil) + #expect(!message2!.isEmpty) + #expect(bobSession.getState() == .handshaking) + + // Alice processes message 2 and creates message 3 + let message3 = try aliceSession.processHandshakeMessage(message2!) + #expect(message3 != nil) + #expect(!message3!.isEmpty) + #expect(aliceSession.getState() == .established) + + // Bob processes message 3 and completes handshake + let finalMessage = try bobSession.processHandshakeMessage(message3!) + #expect(finalMessage == nil) // No more messages needed + #expect(bobSession.getState() == .established) + + // Verify both sessions are established + #expect(aliceSession.isEstablished()) + #expect(bobSession.isEstablished()) + + // Verify they have each other's static keys + #expect( + aliceSession.getRemoteStaticPublicKey()?.rawRepresentation + == bobKey.publicKey.rawRepresentation) + #expect( + bobSession.getRemoteStaticPublicKey()?.rawRepresentation + == aliceKey.publicKey.rawRepresentation) + } + + @Test func handshakeStateValidation() throws { + // Cannot process message before starting handshake + #expect(throws: NoiseSessionError.invalidState) { + try aliceSession.processHandshakeMessage(Data()) + } + + // Start handshake + _ = try aliceSession.startHandshake() + + // Cannot start handshake twice + #expect(throws: NoiseSessionError.invalidState) { + try aliceSession.startHandshake() + } + } + + // MARK: - Encryption/Decryption Tests + + @Test func basicEncryptionDecryption() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) + + let plaintext = "Hello, Bob!".data(using: .utf8)! + + // Alice encrypts + let ciphertext = try aliceSession.encrypt(plaintext) + #expect(ciphertext != plaintext) + #expect(ciphertext.count > plaintext.count) // Should have overhead + + // Bob decrypts + let decrypted = try bobSession.decrypt(ciphertext) + #expect(decrypted == plaintext) + } + + @Test func bidirectionalEncryption() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) + + // Alice -> Bob + let aliceMessage = "Hello from Alice".data(using: .utf8)! + let aliceCiphertext = try aliceSession.encrypt(aliceMessage) + let bobReceived = try bobSession.decrypt(aliceCiphertext) + #expect(bobReceived == aliceMessage) + + // Bob -> Alice + let bobMessage = "Hello from Bob".data(using: .utf8)! + let bobCiphertext = try bobSession.encrypt(bobMessage) + let aliceReceived = try aliceSession.decrypt(bobCiphertext) + #expect(aliceReceived == bobMessage) + } + + @Test func largeMessageEncryption() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) + + // Create a large message + let largeMessage = TestHelpers.generateRandomData(length: 100_000) + + // Encrypt and decrypt + let ciphertext = try aliceSession.encrypt(largeMessage) + let decrypted = try bobSession.decrypt(ciphertext) + + #expect(decrypted == largeMessage) + } + + @Test func encryptionBeforeHandshake() { + let plaintext = "test".data(using: .utf8)! + + #expect(throws: NoiseSessionError.notEstablished) { + try aliceSession.encrypt(plaintext) + } + + #expect(throws: NoiseSessionError.notEstablished) { + try aliceSession.decrypt(plaintext) + } + } + + // MARK: - Session Manager Tests + + @Test func sessionManagerBasicOperations() throws { + let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) + + #expect(manager.getSession(for: alicePeerID) == nil) + + _ = try manager.initiateHandshake(with: alicePeerID) + #expect(manager.getSession(for: alicePeerID) != nil) + + // Get session + let retrieved = manager.getSession(for: alicePeerID) + #expect(retrieved != nil) + + // Remove session + manager.removeSession(for: alicePeerID) + #expect(manager.getSession(for: alicePeerID) == nil) + } + + @Test func sessionManagerHandshakeInitiation() throws { + let manager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) + + // Initiate handshake + let handshakeData = try manager.initiateHandshake(with: alicePeerID) + #expect(!handshakeData.isEmpty) + + // Session should exist + let session = manager.getSession(for: alicePeerID) + #expect(session != nil) + #expect(session?.getState() == .handshaking) + } + + @Test func sessionManagerIncomingHandshake() throws { + let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) + let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) + + // Alice initiates + let message1 = try aliceManager.initiateHandshake(with: alicePeerID) + + // Bob responds + let message2 = try bobManager.handleIncomingHandshake(from: bobPeerID, message: message1) + #expect(message2 != nil) + + // Continue handshake + let message3 = try aliceManager.handleIncomingHandshake( + from: alicePeerID, message: message2!) + #expect(message3 != nil) + + // Complete handshake + let finalMessage = try bobManager.handleIncomingHandshake( + from: bobPeerID, message: message3!) + #expect(finalMessage == nil) + + // Both should have established sessions + #expect(aliceManager.getSession(for: alicePeerID)?.isEstablished() == true) + #expect(bobManager.getSession(for: bobPeerID)?.isEstablished() == true) + } + + @Test func sessionManagerEncryptionDecryption() throws { + let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) + let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) + + // Establish sessions + try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) + + // Encrypt with manager + let plaintext = "Test message".data(using: .utf8)! + let ciphertext = try aliceManager.encrypt(plaintext, for: alicePeerID) + + // Decrypt with manager + let decrypted = try bobManager.decrypt(ciphertext, from: bobPeerID) + #expect(decrypted == plaintext) + } + + // MARK: - Security Tests + + @Test func tamperedCiphertextDetection() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) + + let plaintext = "Secret message".data(using: .utf8)! + var ciphertext = try aliceSession.encrypt(plaintext) + + // Tamper with ciphertext + ciphertext[ciphertext.count / 2] ^= 0xFF + + // Decryption should fail + if #available(macOS 14.4, iOS 17.4, *) { + #expect(throws: CryptoKitError.authenticationFailure) { + try bobSession.decrypt(ciphertext) + } + } else { + #expect(throws: (any Error).self) { + try bobSession.decrypt(ciphertext) + } + } + } + + @Test func replayPrevention() throws { + try performHandshake(initiator: aliceSession, responder: bobSession) + + let plaintext = "Test message".data(using: .utf8)! + let ciphertext = try aliceSession.encrypt(plaintext) + + // First decryption should succeed + _ = try bobSession.decrypt(ciphertext) + + // Replaying the same ciphertext should fail + #expect(throws: NoiseError.replayDetected) { + try bobSession.decrypt(ciphertext) + } + } + + @Test func sessionIsolation() throws { + // Create two separate session pairs + let aliceSession1 = NoiseSession( + peerID: PeerID(str: "peer1"), role: .initiator, keychain: mockKeychain, + localStaticKey: aliceKey) + let bobSession1 = NoiseSession( + peerID: PeerID(str: "alice1"), role: .responder, keychain: mockKeychain, + localStaticKey: bobKey) + + let aliceSession2 = NoiseSession( + peerID: PeerID(str: "peer2"), role: .initiator, keychain: mockKeychain, + localStaticKey: aliceKey) + let bobSession2 = NoiseSession( + peerID: PeerID(str: "alice2"), role: .responder, keychain: mockKeychain, + localStaticKey: bobKey) + + // Establish both pairs + try performHandshake(initiator: aliceSession1, responder: bobSession1) + try performHandshake(initiator: aliceSession2, responder: bobSession2) + + // Encrypt with session 1 + let plaintext = "Secret".data(using: .utf8)! + let ciphertext1 = try aliceSession1.encrypt(plaintext) + + // Should not be able to decrypt with session 2 + if #available(macOS 14.4, iOS 17.4, *) { + #expect(throws: CryptoKitError.authenticationFailure) { + try bobSession2.decrypt(ciphertext1) + } + } else { + #expect(throws: (any Error).self) { + try bobSession2.decrypt(ciphertext1) + } + } + + // But should work with correct session + let decrypted = try bobSession1.decrypt(ciphertext1) + #expect(decrypted == plaintext) + } + + // MARK: - Session Recovery Tests + + @Test func peerRestartDetection() throws { + // Establish initial sessions + let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) + let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) + + try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) + + // Exchange some messages to establish nonce state + let message1 = try aliceManager.encrypt("Hello".data(using: .utf8)!, for: alicePeerID) + _ = try bobManager.decrypt(message1, from: bobPeerID) + + let message2 = try bobManager.encrypt("World".data(using: .utf8)!, for: bobPeerID) + _ = try aliceManager.decrypt(message2, from: alicePeerID) + + // Simulate Bob restart by creating new manager with same key + let bobManagerRestarted = NoiseSessionManager( + localStaticKey: bobKey, keychain: mockKeychain) + + // Bob initiates new handshake after restart + let newHandshake1 = try bobManagerRestarted.initiateHandshake(with: bobPeerID) + + // Alice should accept the new handshake (clearing old session) + let newHandshake2 = try aliceManager.handleIncomingHandshake( + from: alicePeerID, message: newHandshake1) + #expect(newHandshake2 != nil) + + // Complete the new handshake + let newHandshake3 = try bobManagerRestarted.handleIncomingHandshake( + from: bobPeerID, message: newHandshake2!) + #expect(newHandshake3 != nil) + _ = try aliceManager.handleIncomingHandshake(from: alicePeerID, message: newHandshake3!) + + // Should be able to exchange messages with new sessions + let testMessage = "After restart".data(using: .utf8)! + let encrypted = try bobManagerRestarted.encrypt(testMessage, for: bobPeerID) + let decrypted = try aliceManager.decrypt(encrypted, from: alicePeerID) + #expect(decrypted == testMessage) + } + + @Test func nonceDesynchronizationRecovery() throws { + // Create two sessions + let aliceSession = NoiseSession( + peerID: alicePeerID, role: .initiator, keychain: mockKeychain, localStaticKey: aliceKey) + let bobSession = NoiseSession( + peerID: bobPeerID, role: .responder, keychain: mockKeychain, localStaticKey: bobKey) + + // Establish sessions + try performHandshake(initiator: aliceSession, responder: bobSession) + + // Exchange messages to advance nonces + for i in 0..<5 { + let msg = try aliceSession.encrypt("Message \(i)".data(using: .utf8)!) + _ = try bobSession.decrypt(msg) + } + + // Simulate desynchronization by encrypting but not decrypting + for i in 0..<3 { + _ = try aliceSession.encrypt("Lost message \(i)".data(using: .utf8)!) + } + + // With per-packet nonce carried, decryption should not throw here + let desyncMessage = try aliceSession.encrypt("This now succeeds".data(using: .utf8)!) + #expect(throws: Never.self) { + try bobSession.decrypt(desyncMessage) + } + } + + @Test func concurrentEncryption() async throws { + // Test thread safety of encryption operations + let aliceManager = NoiseSessionManager(localStaticKey: aliceKey, keychain: mockKeychain) + let bobManager = NoiseSessionManager(localStaticKey: bobKey, keychain: mockKeychain) + + try establishManagerSessions(aliceManager: aliceManager, bobManager: bobManager) + + let messageCount = 100 + + try await confirmation("All messages encrypted and decrypted", expectedCount: messageCount) + { completion in + var encryptedMessages: [Int: Data] = [:] + // Encrypt messages sequentially to avoid nonce races in manager + for i in 0.. [NoiseTestVector] { + // Try to load from test bundle + let testBundle = Bundle(for: MockKeychain.self) + guard let url = testBundle.url(forResource: "NoiseTestVectors", withExtension: "json") + else { + throw NSError( + domain: "NoiseTests", code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "Could not find NoiseTestVectors.json in test bundle" + ]) + } + + let data = try Data(contentsOf: url) + return try JSONDecoder().decode([NoiseTestVector].self, from: data) + } + + private func runTestVector(_ testVector: NoiseTestVector) throws { + // Parse test inputs + guard let initStatic = Data(hex: testVector.init_static), + let initEphemeral = Data(hex: testVector.init_ephemeral), + let respStatic = Data(hex: testVector.resp_static), + let respEphemeral = Data(hex: testVector.resp_ephemeral), + let prologue = Data(hex: testVector.init_prologue) + else { + throw NSError( + domain: "NoiseTests", code: 2, + userInfo: [NSLocalizedDescriptionKey: "Failed to parse test vector hex strings"]) + } + + let expectedHash = testVector.handshake_hash.flatMap { Data(hex: $0) } + + // Create keys + guard + let initStaticKey = try? Curve25519.KeyAgreement.PrivateKey( + rawRepresentation: initStatic), + let initEphemeralKey = try? Curve25519.KeyAgreement.PrivateKey( + rawRepresentation: initEphemeral), + let respStaticKey = try? Curve25519.KeyAgreement.PrivateKey( + rawRepresentation: respStatic), + let respEphemeralKey = try? Curve25519.KeyAgreement.PrivateKey( + rawRepresentation: respEphemeral) + else { + throw NSError( + domain: "NoiseTests", code: 3, + userInfo: [NSLocalizedDescriptionKey: "Failed to create keys from test vectors"]) + } + + let keychain = MockKeychain() + + // Create handshake states + let initiatorHandshake = NoiseHandshakeState( + role: .initiator, + pattern: .XX, + keychain: keychain, + localStaticKey: initStaticKey, + prologue: prologue, + predeterminedEphemeralKey: initEphemeralKey + ) + + let responderHandshake = NoiseHandshakeState( + role: .responder, + pattern: .XX, + keychain: keychain, + localStaticKey: respStaticKey, + prologue: prologue, + predeterminedEphemeralKey: respEphemeralKey + ) + + // Message 1: Initiator -> Responder (e) + let msg1 = try initiatorHandshake.writeMessage() + #expect(!msg1.isEmpty, "Message 1 should not be empty") + + _ = try responderHandshake.readMessage(msg1) + + // Message 2: Responder -> Initiator (e, ee, s, es) + let msg2 = try responderHandshake.writeMessage() + #expect(!msg2.isEmpty, "Message 2 should not be empty") + + _ = try initiatorHandshake.readMessage(msg2) + + // Message 3: Initiator -> Responder (s, se) + let msg3 = try initiatorHandshake.writeMessage() + #expect(!msg3.isEmpty, "Message 3 should not be empty") + + _ = try responderHandshake.readMessage(msg3) + + // Verify handshake hash + let initiatorHash = initiatorHandshake.getHandshakeHash() + let responderHash = responderHandshake.getHandshakeHash() + + #expect(initiatorHash == responderHash, "Initiator and responder hashes should match") + + if let expectedHash = expectedHash { + #expect( + initiatorHash == expectedHash, + "Handshake hash should match expected value from test vector") + } + + // Get transport ciphers + let (initSend, initRecv) = try initiatorHandshake.getTransportCiphers() + let (respSend, respRecv) = try responderHandshake.getTransportCiphers() + + // Test transport messages + for (index, testMsg) in testVector.messages.enumerated() { + guard let payload = Data(hex: testMsg.payload) else { + throw NSError( + domain: "NoiseTests", code: 4, + userInfo: [ + NSLocalizedDescriptionKey: + "Message \(index + 1): Failed to parse payload hex" + ]) + } + + // Alternate between initiator and responder sending + let (sender, receiver): (NoiseCipherState, NoiseCipherState) + if index % 2 == 0 { + sender = initSend + receiver = respRecv + } else { + sender = respSend + receiver = initRecv + } + + // Encrypt + let ciphertext = try sender.encrypt(plaintext: payload) + + // Decrypt + let decrypted = try receiver.decrypt(ciphertext: ciphertext) + + #expect( + decrypted == payload, + "Message \(index + 1): Decrypted payload should match original") + } + } }