From 109b7d0e03d1c8c0fec1adee6db48a950cd61902 Mon Sep 17 00:00:00 2001 From: Nadim Kobeissi Date: Tue, 18 Nov 2025 19:15:54 +0200 Subject: [PATCH] Move new Noise test vectors into XCTests (WIP) --- bitchat.xcodeproj/project.pbxproj | 34 + bitchat/Noise/NoiseProtocol.swift | 2 - bitchat/Noise/NoiseTestRunner.swift | 293 ----- bitchat/Noise/NoiseTestVectors.json | 71 -- bitchatTests/Noise/NoiseProtocolTests.swift | 1250 +++++++++++-------- bitchatTests/Noise/NoiseTestVectors.json | 71 ++ 6 files changed, 838 insertions(+), 883 deletions(-) delete mode 100644 bitchat/Noise/NoiseTestRunner.swift delete mode 100644 bitchat/Noise/NoiseTestVectors.json create mode 100644 bitchatTests/Noise/NoiseTestVectors.json diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index cbc5e765..e062000b 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -95,6 +95,24 @@ ); target = 57CA17A36A2532A6CFF367BB /* bitchatShareExtension */; }; + C5E027A52ECCDFD700BD6012 /* Exceptions for "bitchatTests" folder in "bitchatTests_macOS" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + Localization/PrimaryLocalizationKeys.json, + README.md, + ); + target = 47FF23248747DD7CB666CB91 /* bitchatTests_macOS */; + }; + C5E027A82ECCDFE200BD6012 /* Exceptions for "bitchatTests" folder in "bitchatTests_iOS" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + Localization/PrimaryLocalizationKeys.json, + README.md, + ); + target = 6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -118,6 +136,10 @@ }; A6E32D412E762EAE0032EA8A /* bitchatTests */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + C5E027A82ECCDFE200BD6012 /* Exceptions for "bitchatTests" folder in "bitchatTests_iOS" target */, + C5E027A52ECCDFD700BD6012 /* Exceptions for "bitchatTests" folder in "bitchatTests_macOS" target */, + ); path = bitchatTests; sourceTree = ""; }; @@ -213,6 +235,7 @@ buildConfigurationList = 1C27B5BA3DB46DDF0DBFEF62 /* Build configuration list for PBXNativeTarget "bitchatTests_macOS" */; buildPhases = ( 5C22AA7B9ACC5A861445C769 /* Sources */, + C5E027A42ECCDFD700BD6012 /* Resources */, ); buildRules = ( ); @@ -245,6 +268,7 @@ buildConfigurationList = 38C4AF6313E5037F25CEF30B /* Build configuration list for PBXNativeTarget "bitchatTests_iOS" */; buildPhases = ( 865C8403EF02C089369A9FCB /* Sources */, + C5E027A72ECCDFE200BD6012 /* Resources */, ); buildRules = ( ); @@ -343,6 +367,16 @@ E0A1B2C3D4E5F6012345678D /* relays/online_relays_gps.csv in Resources */, ); }; + C5E027A42ECCDFD700BD6012 /* Resources */ = { + isa = PBXResourcesBuildPhase; + files = ( + ); + }; + C5E027A72ECCDFE200BD6012 /* Resources */ = { + isa = PBXResourcesBuildPhase; + files = ( + ); + }; CD6E8F32BC38357473954F97 /* Resources */ = { isa = PBXResourcesBuildPhase; files = ( diff --git a/bitchat/Noise/NoiseProtocol.swift b/bitchat/Noise/NoiseProtocol.swift index 1a8d0c6a..465ddae0 100644 --- a/bitchat/Noise/NoiseProtocol.swift +++ b/bitchat/Noise/NoiseProtocol.swift @@ -77,9 +77,7 @@ /// - Noise Specification: http://www.noiseprotocol.org/noise.html /// -#if !NOISE_TESTS import BitLogger -#endif import Foundation import CryptoKit diff --git a/bitchat/Noise/NoiseTestRunner.swift b/bitchat/Noise/NoiseTestRunner.swift deleted file mode 100644 index da8a05e7..00000000 --- a/bitchat/Noise/NoiseTestRunner.swift +++ /dev/null @@ -1,293 +0,0 @@ -import CryptoKit -import Foundation - -// MARK: - Minimal Mocks for Dependencies - -protocol KeychainManagerProtocol { - func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool - func getIdentityKey(forKey key: String) -> Data? - func deleteIdentityKey(forKey key: String) -> Bool - func deleteAllKeychainData() -> Bool - func secureClear(_ data: inout Data) - func secureClear(_ string: inout String) - func verifyIdentityKeyExists() -> Bool -} - -struct MockKeychain: KeychainManagerProtocol { - func saveIdentityKey(_ keyData: Data, forKey key: String) -> Bool { true } - func getIdentityKey(forKey key: String) -> Data? { nil } - func deleteIdentityKey(forKey key: String) -> Bool { true } - func deleteAllKeychainData() -> Bool { true } - func secureClear(_ data: inout Data) { data.resetBytes(in: 0.. Bool { true } -} - -enum SecureLogCategory { case encryption, security } - -struct SecureLogger { - static func debug(_ message: String, category: SecureLogCategory? = nil) {} - static func info(_ message: Any) {} - static func warning(_ message: String, category: SecureLogCategory) {} - static func error(_ message: Any, category: SecureLogCategory? = nil) {} - static func logKeyOperation(_ op: String, keyType: String, success: Bool) {} -} - -enum SecureLogEvent { - case handshakeCompleted(peerID: String) - case sessionExpired(peerID: String) - case authenticationFailed(peerID: String) -} - -extension SecureLogger { - static func error(_ event: SecureLogEvent) {} - static func info(_ event: SecureLogEvent) {} -} - -// MARK: - Test Vector Structure - -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 - } -} - -// MARK: - Helper Extensions - -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() - } -} - -// MARK: - Test Runner - -func runNoiseTests() { - print("=== Noise Protocol Test Vector Runner ===\n") - - // Load test vectors - guard let testData = try? Data(contentsOf: URL(fileURLWithPath: "NoiseTestVectors.json")), - let testVectors = try? JSONDecoder().decode([NoiseTestVector].self, from: testData) - else { - print("❌ Failed to load test vectors") - exit(1) - } - - print("Found \(testVectors.count) test vector(s)\n") - - for (index, testVector) in testVectors.enumerated() { - print("=== Test Vector \(index + 1) ===") - print("Protocol: \(testVector.protocol_name)") - runSingleTest(testVector) - print("") - } - - print("=== All Test Vectors Passed! ===") -} - -func runSingleTest(_ testVector: NoiseTestVector) { - - // 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 { - print("❌ Failed to parse test vector hex strings") - exit(1) - } - - 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 { - print("❌ Failed to create keys from test vectors") - exit(1) - } - - 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 - ) - - print("\n--- Handshake Phase ---") - - // Message 1: Initiator -> Responder (e) - guard let msg1 = try? initiatorHandshake.writeMessage() else { - print("❌ Failed to write message 1") - exit(1) - } - print("✓ Message 1: Initiator sent ephemeral (\(msg1.count) bytes)") - - guard (try? responderHandshake.readMessage(msg1)) != nil else { - print("❌ Failed to read message 1") - exit(1) - } - print("✓ Message 1: Responder received") - - // Message 2: Responder -> Initiator (e, ee, s, es) - guard let msg2 = try? responderHandshake.writeMessage() else { - print("❌ Failed to write message 2") - exit(1) - } - print("✓ Message 2: Responder sent (\(msg2.count) bytes)") - - guard (try? initiatorHandshake.readMessage(msg2)) != nil else { - print("❌ Failed to read message 2") - exit(1) - } - print("✓ Message 2: Initiator received") - - // Message 3: Initiator -> Responder (s, se) - guard let msg3 = try? initiatorHandshake.writeMessage() else { - print("❌ Failed to write message 3") - exit(1) - } - print("✓ Message 3: Initiator sent (\(msg3.count) bytes)") - - guard (try? responderHandshake.readMessage(msg3)) != nil else { - print("❌ Failed to read message 3") - exit(1) - } - print("✓ Message 3: Responder received") - - // Verify handshake hash - let initiatorHash = initiatorHandshake.getHandshakeHash() - let responderHash = responderHandshake.getHandshakeHash() - - if initiatorHash != responderHash { - print("❌ Initiator and responder hashes don't match!") - exit(1) - } - - if let expectedHash = expectedHash { - if initiatorHash == expectedHash { - print("✓ Handshake hash verified") - } else { - print("⚠️ Handshake hash differs from test vector (may be implementation-specific)") - } - } else { - print("✓ Handshake complete") - } - - // Get transport ciphers - guard let (initSend, initRecv) = try? initiatorHandshake.getTransportCiphers(), - let (respSend, respRecv) = try? responderHandshake.getTransportCiphers() - else { - print("❌ Failed to split to transport ciphers") - exit(1) - } - - print("\n--- Transport Phase ---") - - // Test transport messages - var passedMessages = 0 - for (index, testMsg) in testVector.messages.enumerated() { - guard let payload = Data(hex: testMsg.payload) else { - print("❌ Message \(index + 1): Failed to parse payload hex") - exit(1) - } - - // Alternate between initiator and responder sending - let (sender, receiver): (NoiseCipherState, NoiseCipherState) - let direction: String - if index % 2 == 0 { - sender = initSend - receiver = respRecv - direction = "Initiator → Responder" - } else { - sender = respSend - receiver = initRecv - direction = "Responder → Initiator" - } - - // Encrypt - guard let ciphertext = try? sender.encrypt(plaintext: payload) else { - print("❌ Message \(index + 1): Encryption failed") - exit(1) - } - - // Decrypt - guard let decrypted = try? receiver.decrypt(ciphertext: ciphertext) else { - print("❌ Message \(index + 1): Decryption failed") - print(" Ciphertext: \(ciphertext.hexString())") - exit(1) - } - - if decrypted == payload { - print( - "✓ Message \(index + 1) (\(direction)): Encrypt/decrypt successful (\(payload.count) bytes)" - ) - passedMessages += 1 - } else { - print("❌ Message \(index + 1): Decrypted payload mismatch!") - print(" Expected: \(payload.hexString())") - print(" Got: \(decrypted.hexString())") - exit(1) - } - } - - print("✓ Test Passed!") - print(" Handshake: ✓") - print(" Transport Messages: \(passedMessages)/\(testVector.messages.count) ✓") -} - -// MARK: - Main Entry Point - -@main -struct NoiseTestRunner { - static func main() { - runNoiseTests() - } -} diff --git a/bitchat/Noise/NoiseTestVectors.json b/bitchat/Noise/NoiseTestVectors.json deleted file mode 100644 index 1ced9298..00000000 --- a/bitchat/Noise/NoiseTestVectors.json +++ /dev/null @@ -1,71 +0,0 @@ -[ - { - "protocol_name": "Noise_XX_25519_ChaChaPoly_SHA256", - "init_prologue": "4a6f686e2047616c74", - "init_static": "e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1", - "init_ephemeral": "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a", - "resp_prologue": "4a6f686e2047616c74", - "resp_static": "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893", - "resp_ephemeral": "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b", - "handshake_hash": "c8e5f64e846193be2a834104c2a009868d6c9f3bd3c186299888b488b2f1f58e", - "messages": [ - { - "payload": "4c756477696720766f6e204d69736573", - "ciphertext": "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c79444c756477696720766f6e204d69736573" - }, - { - "payload": "4d757272617920526f746862617264", - "ciphertext": "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f14480884381cbad1f276e038c48378ffce2b65285e08d6b68aaa3629a5a8639392490e5b9bd5269c2f1e4f488ed8831161f19b7815528f8982ffe09be9b5c412f8a0db50f8814c7194e83f23dbd8d162c9326ad" - }, - { - "payload": "462e20412e20486179656b", - "ciphertext": "c7195ffacac1307ff99046f219750fc47693e23c3cb08b89c2af808b444850a80ae475b9df0f169ae80a89be0865b57f58c9fea0d4ec82a286427402f113e4b6ae769a1d95941d49b25030" - }, - { - "payload": "4361726c204d656e676572", - "ciphertext": "96763ed773f8e47bb3712f0e29b3060ffc956ffc146cee53d5e1df" - }, - { - "payload": "4a65616e2d426170746973746520536179", - "ciphertext": "3e40f15f6f3a46ae446b253bf8b1d9ffb6ed9b174d272328ff91a7e2e5c79c07f5" - }, - { - "payload": "457567656e2042f6686d20766f6e2042617765726b", - "ciphertext": "eb3f3515110702e047a6c9da4478b6ead94873c11c0f2d710ddb3f09fce024b3a58502ae3f" - } - ] - }, - { - "protocol_name": "Noise_XX_25519_ChaChaPoly_SHA256", - "init_prologue": "5468657265206973206e6f20726967687420616e642077726f6e672e2054686572652773206f6e6c792066756e20616e6420626f72696e672e", - "init_psks": [], - "init_static": "7dec208517a3b81a2861d7a71266d5d6dc944c5a8816634a86fe63198a0148ee", - "init_ephemeral": "a32daf21e93c0131495ce1d903181fde81cc46937daaeb990bae7c992709421e", - "resp_prologue": "5468657265206973206e6f20726967687420616e642077726f6e672e2054686572652773206f6e6c792066756e20616e6420626f72696e672e", - "resp_psks": [], - "resp_static": "4d0aed5098e3b4ef20357e9f686ce66204c792b358da2e475017d6c485304881", - "resp_ephemeral": "4eece0f195d026db035ff987597c429d3ad3bcc2944df37d649528951b2a27c5", - "messages": [ - { - "payload": "d03c489139e645d0711a3c9e810d776b46a84912463fafa87b884eebf242dc34", - "ciphertext": "f9fa868ba97ab8a2686deccfaad5a484ee10a5bb85e3d1dce015a84797f92818d03c489139e645d0711a3c9e810d776b46a84912463fafa87b884eebf242dc34" - }, - { - "payload": "d8190a92f7dc0c93dbea9118ba8055751fb7c6590c416ffbd419964132b99a85", - "ciphertext": "8c4e6fdb7d09d501a86f7eca5c234522751706ed409182c05cdf5f827d4dae47b81c6c5f43b025692c24391eefee725c17d8cb0fbe3e4abb8aedf42c4fd2592d4ea48ac08989d6ae8b4adae08b2c34087c808c7aa55a63c02b0fab9e930612336bd43eaea04d3c670a0a146691aa9cc9d357872320dc735dbc48580cffb553db" - }, - { - "payload": "77891b19dcb92ef7c055b672c4a5aa7fdf1c84146b8b303459022729473ce254", - "ciphertext": "933ca6b5ed60df3df66121f0ab49a09e49efa45c613a86a3cecbf4c535cef2f83f72b42837b18e3572f2fdc2b74c331e2368a545cef54bdca081678ab0e9dd5348122459e0c034c851984d88ce610963d43cde6cfe73a67fbd5a63e8bfca96d0" - }, - { - "payload": "d7efdf988072881941db045a42882433817555128fbf5663e56081712ec7d212", - "ciphertext": "54ef0ff0629e1aaa7685a2806ab111cba76b52331f2642276736f415868eacb69ab2577f3bda0cbf72f879685f6ed25f" - }, - { - "payload": "dd7bf01a588bafb52c6cfba952e5d8fe35cc2b3f92b4730ae2474615157345ce", - "ciphertext": "356be70f110306d5c699bb834bb9d58d909e325924dfbec972e406e6f294dc63e1daebefe8a62a334facc8048ab4ad66" - } - ] - } -] diff --git a/bitchatTests/Noise/NoiseProtocolTests.swift b/bitchatTests/Noise/NoiseProtocolTests.swift index be4b15a3..a6652093 100644 --- a/bitchatTests/Noise/NoiseProtocolTests.swift +++ b/bitchatTests/Noise/NoiseProtocolTests.swift @@ -6,527 +6,743 @@ // For more information, see // -import Testing import CryptoKit import Foundation +import Testing + @testable import bitchat -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) +// MARK: - Test Vector Support - #expect(manager.getSession(for: alicePeerID) == nil) +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] - _ = 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.. 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") + } + } } diff --git a/bitchatTests/Noise/NoiseTestVectors.json b/bitchatTests/Noise/NoiseTestVectors.json new file mode 100644 index 00000000..4c595465 --- /dev/null +++ b/bitchatTests/Noise/NoiseTestVectors.json @@ -0,0 +1,71 @@ +[ + { + "protocol_name": "Noise_XX_25519_ChaChaPoly_SHA256", + "init_prologue": "4a6f686e2047616c74", + "init_static": "e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1", + "init_ephemeral": "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a", + "resp_prologue": "4a6f686e2047616c74", + "resp_static": "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893", + "resp_ephemeral": "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b", + "handshake_hash": "c8e5f64e846193be2a834104c2a009868d6c9f3bd3c186299888b488b2f1f58e", + "messages": [ + { + "payload": "4c756477696720766f6e204d69736573", + "ciphertext": "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c79444c756477696720766f6e204d69736573" + }, + { + "payload": "4d757272617920526f746862617264", + "ciphertext": "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f14480884381cbad1f276e038c48378ffce2b65285e08d6b68aaa3629a5a8639392490e5b9bd5269c2f1e4f488ed8831161f19b7815528f8982ffe09be9b5c412f8a0db50f8814c7194e83f23dbd8d162c9326ad" + }, + { + "payload": "462e20412e20486179656b", + "ciphertext": "c7195ffacac1307ff99046f219750fc47693e23c3cb08b89c2af808b444850a80ae475b9df0f169ae80a89be0865b57f58c9fea0d4ec82a286427402f113e4b6ae769a1d95941d49b25030" + }, + { + "payload": "4361726c204d656e676572", + "ciphertext": "96763ed773f8e47bb3712f0e29b3060ffc956ffc146cee53d5e1df" + }, + { + "payload": "4a65616e2d426170746973746520536179", + "ciphertext": "3e40f15f6f3a46ae446b253bf8b1d9ffb6ed9b174d272328ff91a7e2e5c79c07f5" + }, + { + "payload": "457567656e2042f6686d20766f6e2042617765726b", + "ciphertext": "eb3f3515110702e047a6c9da4478b6ead94873c11c0f2d710ddb3f09fce024b3a58502ae3f" + } + ] + }, + { + "protocol_name": "Noise_XX_25519_ChaChaPoly_SHA256", + "init_prologue": "5468657265206973206e6f20726967687420616e642077726f6e672e2054686572652773206f6e6c792066756e20616e6420626f72696e672e", + "init_psks": [], + "init_static": "7dec208517a3b81a2861d7a71266d5d6dc944c5a8816634a86fe63198a0148ee", + "init_ephemeral": "a32daf21e93c0131495ce1d903181fde81cc46937daaeb990bae7c992709421e", + "resp_prologue": "5468657265206973206e6f20726967687420616e642077726f6e672e2054686572652773206f6e6c792066756e20616e6420626f72696e672e", + "resp_psks": [], + "resp_static": "4d0aed5098e3b4ef20357e9f686ce66204c792b358da2e475017d6c485304881", + "resp_ephemeral": "4eece0f195d026db035ff987597c429d3ad3bcc2944df37d649528951b2a27c5", + "messages": [ + { + "payload": "d03c489139e645d0711a3c9e810d776b46a84912463fafa87b884eebf242dc34", + "ciphertext": "f9fa868ba97ab8a2686deccfaad5a484ee10a5bb85e3d1dce015a84797f92818d03c489139e645d0711a3c9e810d776b46a84912463fafa87b884eebf242dc34" + }, + { + "payload": "d8190a92f7dc0c93dbea9118ba8055751fb7c6590c416ffbd419964132b99a85", + "ciphertext": "8c4e6fdb7d09d501a86f7eca5c234522751706ed409182c05cdf5f827d4dae47b81c6c5f43b025692c24391eefee725c17d8cb0fbe3e4abb8aedf42c4fd2592d4ea48ac08989d6ae8b4adae08b2c34087c808c7aa55a63c02b0fab9e930612336bd43eaea04d3c670a0a146691aa9cc9d357872320dc735dbc48580cffb553db" + }, + { + "payload": "77891b19dcb92ef7c055b672c4a5aa7fdf1c84146b8b303459022729473ce254", + "ciphertext": "933ca6b5ed60df3df66121f0ab49a09e49efa45c613a86a3cecbf4c535cef2f83f72b42837b18e3572f2fdc2b74c331e2368a545cef54bdca081678ab0e9dd5348122459e0c034c851984d88ce610963d43cde6cfe73a67fbd5a63e8bfca96d0" + }, + { + "payload": "d7efdf988072881941db045a42882433817555128fbf5663e56081712ec7d212", + "ciphertext": "54ef0ff0629e1aaa7685a2806ab111cba76b52331f2642276736f415868eacb69ab2577f3bda0cbf72f879685f6ed25f" + }, + { + "payload": "dd7bf01a588bafb52c6cfba952e5d8fe35cc2b3f92b4730ae2474615157345ce", + "ciphertext": "356be70f110306d5c699bb834bb9d58d909e325924dfbec972e406e6f294dc63e1daebefe8a62a334facc8048ab4ad66" + } + ] + } +]