import XCTest @testable import Rosetta /// Tests that iOS crypto primitives produce byte-identical output /// to Android/Desktop for call E2EE. final class CallCryptoParityTests: XCTestCase { // MARK: - HSalsa20 (nacl.box.before parity) func testHSalsa20ProducesConsistentOutput() throws { // HSalsa20 with zero nonce should produce deterministic output from any key let key = Data(repeating: 0x42, count: 32) let result1 = NativeCryptoBridge.naclSharedKey(fromRawDH: key) let result2 = NativeCryptoBridge.naclSharedKey(fromRawDH: key) XCTAssertNotNil(result1) XCTAssertNotNil(result2) XCTAssertEqual(result1!.count, 32) XCTAssertEqual(result1, result2, "HSalsa20 must be deterministic") } func testHSalsa20OutputDiffersForDifferentKeys() throws { let key1 = Data(repeating: 0x01, count: 32) let key2 = Data(repeating: 0x02, count: 32) let result1 = NativeCryptoBridge.naclSharedKey(fromRawDH: key1) let result2 = NativeCryptoBridge.naclSharedKey(fromRawDH: key2) XCTAssertNotNil(result1) XCTAssertNotNil(result2) XCTAssertNotEqual(result1, result2, "Different keys must produce different shared secrets") } func testHSalsa20RejectsWrongKeyLength() { let shortKey = Data(repeating: 0x00, count: 16) let result = NativeCryptoBridge.naclSharedKey(fromRawDH: shortKey) XCTAssertNil(result, "HSalsa20 must reject keys != 32 bytes") } // MARK: - XChaCha20 stream cipher func testXChaCha20EncryptDecryptRoundTrip() throws { let key = Data(repeating: 0xAB, count: 32) let nonce = Data(repeating: 0xCD, count: 24) let plaintext = Data("Hello Rosetta E2EE call frame!".utf8) let encrypted = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce) XCTAssertNotNil(encrypted) XCTAssertEqual(encrypted!.count, plaintext.count, "Encrypted size must match plaintext size") XCTAssertNotEqual(encrypted, plaintext, "Encrypted data must differ from plaintext") // XOR cipher: encrypt again = decrypt let decrypted = NativeCryptoBridge.xChaCha20Xor(encrypted!, key: key, nonce: nonce) XCTAssertNotNil(decrypted) XCTAssertEqual(decrypted, plaintext, "XChaCha20 XOR decrypt must recover original plaintext") } func testXChaCha20DifferentNonceProducesDifferentCiphertext() throws { let key = Data(repeating: 0x11, count: 32) let nonce1 = Data(repeating: 0x01, count: 24) let nonce2 = Data(repeating: 0x02, count: 24) let plaintext = Data(repeating: 0xFF, count: 64) let enc1 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce1) let enc2 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce2) XCTAssertNotNil(enc1) XCTAssertNotNil(enc2) XCTAssertNotEqual(enc1, enc2, "Different nonces must produce different ciphertext") } func testXChaCha20DifferentKeyProducesDifferentCiphertext() throws { let key1 = Data(repeating: 0x11, count: 32) let key2 = Data(repeating: 0x22, count: 32) let nonce = Data(repeating: 0x00, count: 24) let plaintext = Data(repeating: 0xFF, count: 64) let enc1 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key1, nonce: nonce) let enc2 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key2, nonce: nonce) XCTAssertNotNil(enc1) XCTAssertNotNil(enc2) XCTAssertNotEqual(enc1, enc2, "Different keys must produce different ciphertext") } func testXChaCha20OutputSizeMatchesInput() throws { let key = Data(repeating: 0x33, count: 32) let nonce = Data(repeating: 0x44, count: 24) for size in [0, 1, 16, 63, 64, 65, 128, 960, 1024] { let plaintext = Data(repeating: 0xAA, count: size) let encrypted = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce) XCTAssertNotNil(encrypted, "XChaCha20 must handle size \(size)") XCTAssertEqual(encrypted!.count, size, "Output size must equal input size for \(size) bytes") } } func testXChaCha20RejectsWrongKeyOrNonceLength() { let plaintext = Data(repeating: 0x00, count: 16) let badKey = NativeCryptoBridge.xChaCha20Xor(plaintext, key: Data(repeating: 0, count: 16), nonce: Data(repeating: 0, count: 24)) XCTAssertNil(badKey, "Must reject key != 32 bytes") let badNonce = NativeCryptoBridge.xChaCha20Xor(plaintext, key: Data(repeating: 0, count: 32), nonce: Data(repeating: 0, count: 12)) XCTAssertNil(badNonce, "Must reject nonce != 24 bytes") } // MARK: - Nonce construction parity (Desktop BigInt encoding) /// Verifies that a nonce built from an 8-byte big-endian timestamp /// matches the Desktop audioE2EE.ts fillNonceFromTimestamp output. func testNonceFromTimestampMatchesDesktopFormat() throws { let key = Data(repeating: 0x55, count: 32) let plaintext = Data(repeating: 0xEE, count: 48) // Desktop encodes timestamp 12345 as big-endian 8 bytes: // [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x39] // Then nonce[8:24] = zeros let timestamp: UInt64 = 12345 var nonce = Data(repeating: 0, count: 24) nonce[0] = UInt8((timestamp >> 56) & 0xFF) nonce[1] = UInt8((timestamp >> 48) & 0xFF) nonce[2] = UInt8((timestamp >> 40) & 0xFF) nonce[3] = UInt8((timestamp >> 32) & 0xFF) nonce[4] = UInt8((timestamp >> 24) & 0xFF) nonce[5] = UInt8((timestamp >> 16) & 0xFF) nonce[6] = UInt8((timestamp >> 8) & 0xFF) nonce[7] = UInt8(timestamp & 0xFF) // Encrypt with manually constructed nonce let enc1 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce) XCTAssertNotNil(enc1) // Same nonce built differently (raw bytes = big-endian 12345) var nonce2 = Data(repeating: 0, count: 24) var tsBE = timestamp.bigEndian nonce2.replaceSubrange(0..<8, with: Data(bytes: &tsBE, count: 8)) let enc2 = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonce2) XCTAssertNotNil(enc2) XCTAssertEqual(enc1, enc2, "Both nonce construction methods must produce identical ciphertext") } /// Verifies that 32-bit RTP timestamp at nonce[4:8] matches /// the 64-bit encoding when value fits in 32 bits. func testRtpTimestamp32MatchesDesktop64ForSmallValues() throws { let key = Data(repeating: 0x66, count: 32) let plaintext = Data(repeating: 0xDD, count: 32) let rtpTs: UInt32 = 48000 // 1 second at 48kHz // Desktop-style: 64-bit big-endian in nonce[0:8] var nonceDesktop = Data(repeating: 0, count: 24) let ts64 = UInt64(rtpTs) nonceDesktop[0] = UInt8((ts64 >> 56) & 0xFF) // 0 nonceDesktop[1] = UInt8((ts64 >> 48) & 0xFF) // 0 nonceDesktop[2] = UInt8((ts64 >> 40) & 0xFF) // 0 nonceDesktop[3] = UInt8((ts64 >> 32) & 0xFF) // 0 nonceDesktop[4] = UInt8((ts64 >> 24) & 0xFF) // 0 nonceDesktop[5] = UInt8((ts64 >> 16) & 0xFF) // 0 nonceDesktop[6] = UInt8((ts64 >> 8) & 0xFF) // 0xBB nonceDesktop[7] = UInt8(ts64 & 0xFF) // 0x80 // RTP-style: 32-bit big-endian at nonce[4:8] var nonceRtp = Data(repeating: 0, count: 24) nonceRtp[4] = UInt8((rtpTs >> 24) & 0xFF) // 0 nonceRtp[5] = UInt8((rtpTs >> 16) & 0xFF) // 0 nonceRtp[6] = UInt8((rtpTs >> 8) & 0xFF) // 0xBB nonceRtp[7] = UInt8(rtpTs & 0xFF) // 0x80 let encDesktop = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonceDesktop) let encRtp = NativeCryptoBridge.xChaCha20Xor(plaintext, key: key, nonce: nonceRtp) XCTAssertNotNil(encDesktop) XCTAssertNotNil(encRtp) XCTAssertEqual(encDesktop, encRtp, "32-bit RTP timestamp at nonce[4:8] must match 64-bit encoding for values < 2^32") } // MARK: - Full E2EE round-trip (simulate iOS encrypt → Desktop decrypt) func testFullE2EERoundTripWithDesktopNonce() throws { // Simulate: iOS encrypts a frame, Desktop decrypts with same key and nonce let sharedKey = Data(repeating: 0x77, count: 32) // Simulated Opus frame (TOC byte 0xFC = CELT, 20ms, 1 frame) var opusFrame = Data([0xFC]) // TOC: config=31, code=0 opusFrame.append(Data(repeating: 0x42, count: 80)) // payload // Desktop-style timestamp nonce let timestamp: UInt64 = 96000 // 2 seconds at 48kHz var nonce = Data(repeating: 0, count: 24) for i in 0..<8 { nonce[i] = UInt8((timestamp >> UInt64((7 - i) * 8)) & 0xFF) } // Encrypt (iOS side) let encrypted = NativeCryptoBridge.xChaCha20Xor(opusFrame, key: sharedKey, nonce: nonce) XCTAssertNotNil(encrypted) XCTAssertEqual(encrypted!.count, opusFrame.count) XCTAssertNotEqual(encrypted, opusFrame) // Decrypt (simulating Desktop side with same key+nonce) let decrypted = NativeCryptoBridge.xChaCha20Xor(encrypted!, key: sharedKey, nonce: nonce) XCTAssertNotNil(decrypted) XCTAssertEqual(decrypted, opusFrame, "Desktop must recover exact Opus frame") } // MARK: - HSalsa20 + XChaCha20 full chain func testFullKeyDerivationAndEncryptionChain() throws { // Simulate two peers with fixed "DH results" let rawDH = Data((0..<32).map { UInt8($0) }) let sharedKey = NativeCryptoBridge.naclSharedKey(fromRawDH: rawDH) XCTAssertNotNil(sharedKey) XCTAssertEqual(sharedKey!.count, 32) let plaintext = Data(repeating: 0xAA, count: 160) // typical Opus frame let nonce = Data(repeating: 0, count: 24) let encrypted = NativeCryptoBridge.xChaCha20Xor(plaintext, key: sharedKey!, nonce: nonce) XCTAssertNotNil(encrypted) let decrypted = NativeCryptoBridge.xChaCha20Xor(encrypted!, key: sharedKey!, nonce: nonce) XCTAssertEqual(decrypted, plaintext, "Full chain: HSalsa20 key derivation + XChaCha20 must round-trip") } }