// // NoiseProtocolTests.swift // bitchatTests // // This is free and unencumbered software released into the public domain. // For more information, see // import CryptoKit import Foundation import Testing @testable import bitchat // 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 } } 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() } } 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") } } }