import XCTest import CommonCrypto import P256K @testable import Rosetta /// Cross-platform crypto parity tests: iOS ↔ Desktop ↔ Android. /// Verifies that all crypto operations produce compatible output /// and that messages encrypted on any platform can be decrypted on iOS. @MainActor final class CryptoParityTests: XCTestCase { // MARK: - XChaCha20-Poly1305 Round-Trip func testXChaCha20Poly1305_encryptDecryptRoundTrip() throws { let plaintext = "Привет, мир! Hello, world! 🔐" let key = try CryptoPrimitives.randomBytes(count: 32) let nonce = try CryptoPrimitives.randomBytes(count: 24) let keyAndNonce = key + nonce let ciphertextWithTag = try XChaCha20Engine.encrypt( plaintext: Data(plaintext.utf8), key: key, nonce: nonce ) let decrypted = try MessageCrypto.decryptIncomingWithPlainKey( ciphertext: ciphertextWithTag.hexString, plainKeyAndNonce: keyAndNonce ) XCTAssertEqual(decrypted, plaintext, "Round-trip must preserve plaintext") } func testXChaCha20Poly1305_wrongKeyFails() throws { let plaintext = "secret message" let key = try CryptoPrimitives.randomBytes(count: 32) let nonce = try CryptoPrimitives.randomBytes(count: 24) let wrongKey = try CryptoPrimitives.randomBytes(count: 32) let ciphertext = try XChaCha20Engine.encrypt( plaintext: Data(plaintext.utf8), key: key, nonce: nonce ) XCTAssertThrowsError( try XChaCha20Engine.decrypt( ciphertextWithTag: ciphertext, key: wrongKey, nonce: nonce ), "Decryption with wrong key must fail (Poly1305 tag mismatch)" ) } func testXChaCha20Poly1305_emptyPlaintext() throws { let key = try CryptoPrimitives.randomBytes(count: 32) let nonce = try CryptoPrimitives.randomBytes(count: 24) let ciphertext = try XChaCha20Engine.encrypt( plaintext: Data(), key: key, nonce: nonce ) // Ciphertext should be exactly 16 bytes (Poly1305 tag only) XCTAssertEqual(ciphertext.count, 16, "Empty plaintext produces 16-byte tag only") let decrypted = try XChaCha20Engine.decrypt( ciphertextWithTag: ciphertext, key: key, nonce: nonce ) XCTAssertEqual(decrypted.count, 0, "Decrypted empty plaintext must be empty") } func testXChaCha20Poly1305_largePlaintext() throws { let plaintext = Data(repeating: 0x42, count: 100_000) let key = try CryptoPrimitives.randomBytes(count: 32) let nonce = try CryptoPrimitives.randomBytes(count: 24) let ciphertext = try XChaCha20Engine.encrypt( plaintext: plaintext, key: key, nonce: nonce ) XCTAssertEqual(ciphertext.count, plaintext.count + 16) let decrypted = try XChaCha20Engine.decrypt( ciphertextWithTag: ciphertext, key: key, nonce: nonce ) XCTAssertEqual(decrypted, plaintext) } // MARK: - ECDH Encrypt → Decrypt Round-Trip func testECDH_encryptDecryptRoundTrip() throws { let plaintext = "Test ECDH round-trip" // Generate recipient key pair let recipientPrivKey = try P256K.KeyAgreement.PrivateKey() let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString // Encrypt let encrypted = try MessageCrypto.encryptOutgoing( plaintext: plaintext, recipientPublicKeyHex: recipientPubKeyHex ) XCTAssertFalse(encrypted.content.isEmpty, "Content must not be empty") XCTAssertFalse(encrypted.chachaKey.isEmpty, "chachaKey must not be empty") XCTAssertEqual(encrypted.plainKeyAndNonce.count, 56, "Key+nonce must be 56 bytes") // Decrypt let decrypted = try MessageCrypto.decryptIncoming( ciphertext: encrypted.content, encryptedKey: encrypted.chachaKey, myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString ) XCTAssertEqual(decrypted, plaintext, "ECDH round-trip must preserve plaintext") } func testECDH_decryptReturnsCorrectKeyAndNonce() throws { let plaintext = "Key verification" let recipientPrivKey = try P256K.KeyAgreement.PrivateKey() let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString let encrypted = try MessageCrypto.encryptOutgoing( plaintext: plaintext, recipientPublicKeyHex: recipientPubKeyHex ) let (decryptedText, recoveredKeyAndNonce) = try MessageCrypto.decryptIncomingFull( ciphertext: encrypted.content, encryptedKey: encrypted.chachaKey, myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString ) XCTAssertEqual(decryptedText, plaintext) XCTAssertEqual(recoveredKeyAndNonce, encrypted.plainKeyAndNonce, "Recovered key+nonce must match original") } func testECDH_wrongPrivateKeyFails() throws { let recipientPrivKey = try P256K.KeyAgreement.PrivateKey() let wrongPrivKey = try P256K.KeyAgreement.PrivateKey() let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString let encrypted = try MessageCrypto.encryptOutgoing( plaintext: "secret", recipientPublicKeyHex: recipientPubKeyHex ) XCTAssertThrowsError( try MessageCrypto.decryptIncoming( ciphertext: encrypted.content, encryptedKey: encrypted.chachaKey, myPrivateKeyHex: wrongPrivKey.rawRepresentation.hexString ), "Decryption with wrong private key must fail" ) } func testECDH_multipleEncryptions_differentCiphertext() throws { let plaintext = "same message" let recipientPrivKey = try P256K.KeyAgreement.PrivateKey() let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString let enc1 = try MessageCrypto.encryptOutgoing(plaintext: plaintext, recipientPublicKeyHex: recipientPubKeyHex) let enc2 = try MessageCrypto.encryptOutgoing(plaintext: plaintext, recipientPublicKeyHex: recipientPubKeyHex) XCTAssertNotEqual(enc1.content, enc2.content, "Each encryption must use different random key") XCTAssertNotEqual(enc1.chachaKey, enc2.chachaKey, "Each encryption must use different ephemeral key") // Both must decrypt correctly let dec1 = try MessageCrypto.decryptIncoming(ciphertext: enc1.content, encryptedKey: enc1.chachaKey, myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString) let dec2 = try MessageCrypto.decryptIncoming(ciphertext: enc2.content, encryptedKey: enc2.chachaKey, myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString) XCTAssertEqual(dec1, plaintext) XCTAssertEqual(dec2, plaintext) } // MARK: - aesChachaKey (Sync Path) Round-Trip func testAesChachaKey_encryptDecryptRoundTrip() throws { let plaintext = "Sync path message" let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString // Encrypt with XChaCha20 let recipientPrivKey = try P256K.KeyAgreement.PrivateKey() let encrypted = try MessageCrypto.encryptOutgoing( plaintext: plaintext, recipientPublicKeyHex: recipientPrivKey.publicKey.dataRepresentation.hexString ) // Build aesChachaKey (same logic as makeOutgoingPacket) guard let latin1String = String(data: encrypted.plainKeyAndNonce, encoding: .isoLatin1) else { XCTFail("Latin-1 encoding must succeed for any 56-byte sequence") return } let aesChachaPayload = Data(latin1String.utf8) let aesChachaKey = try CryptoManager.shared.encryptWithPasswordDesktopCompat( aesChachaPayload, password: privateKeyHex ) // Decrypt aesChachaKey (same logic as decryptIncomingMessage sync path) let decryptedPayload = try CryptoManager.shared.decryptWithPassword( aesChachaKey, password: privateKeyHex ) let recoveredKeyAndNonce = MessageCrypto.androidUtf8BytesToLatin1Bytes(decryptedPayload) XCTAssertEqual(recoveredKeyAndNonce.count, 56, "Recovered key+nonce must be 56 bytes") // Decrypt message content with recovered key let decryptedText = try MessageCrypto.decryptIncomingWithPlainKey( ciphertext: encrypted.content, plainKeyAndNonce: recoveredKeyAndNonce ) XCTAssertEqual(decryptedText, plaintext, "aesChachaKey round-trip must recover original text") } func testAesChachaKey_latin1Utf8RoundTrip_allByteValues() throws { // Verify that Latin-1 → UTF-8 → Latin-1 round-trip preserves ALL byte values 0-255. // This is critical: key bytes can be ANY value. var allBytes = Data(count: 256) for i in 0..<256 { allBytes[i] = UInt8(i) } guard let latin1String = String(data: allBytes, encoding: .isoLatin1) else { XCTFail("Latin-1 must encode all 256 byte values") return } let utf8Bytes = Data(latin1String.utf8) let recovered = MessageCrypto.androidUtf8BytesToLatin1Bytes(utf8Bytes) XCTAssertEqual(recovered, allBytes, "Latin-1 → UTF-8 → Latin-1 must be lossless for all byte values 0-255") } func testAesChachaKey_wrongPasswordFails() throws { let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString let wrongKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString let data = try CryptoPrimitives.randomBytes(count: 56) guard let latin1 = String(data: data, encoding: .isoLatin1) else { return } let plaintext = Data(latin1.utf8) let encrypted = try CryptoManager.shared.encryptWithPasswordDesktopCompat( plaintext, password: privateKeyHex ) do { let decrypted = try CryptoManager.shared.decryptWithPassword( encrypted, password: wrongKeyHex, requireCompression: true ) XCTAssertNotEqual( decrypted, plaintext, "Wrong password must never recover original plaintext" ) } catch { // Expected path for the majority of wrong-password attempts. } } // MARK: - Attachment Password Candidates func testAttachmentPasswordCandidates_rawkeyFormat() { // 56 random bytes as hex let keyData = Data([ 0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD, 0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD, 0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD, 0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD, 0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD, 0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD, 0x00, 0x01, 0x7F, 0x80, 0xFF, 0xFE, 0xAB, 0xCD, ]) let stored = "rawkey:" + keyData.hexString let candidates = MessageCrypto.attachmentPasswordCandidates(from: stored) // Must have at least HEX + Android + WHATWG XCTAssertGreaterThanOrEqual(candidates.count, 3, "Must generate ≥3 candidates") // HEX must be first (Desktop commit 61e83bd parity) XCTAssertEqual(candidates[0], keyData.hexString, "First candidate must be HEX (Desktop parity)") // All candidates must be unique XCTAssertEqual(candidates.count, Set(candidates).count, "All candidates must be unique (deduplicated)") } func testAttachmentPasswordCandidates_legacyFormat() { let stored = "some_legacy_password_string" let candidates = MessageCrypto.attachmentPasswordCandidates(from: stored) XCTAssertEqual(candidates.count, 2, "Legacy format returns hex+plain candidates") XCTAssertEqual(candidates[0], Data(stored.utf8).map { String(format: "%02x", $0) }.joined()) XCTAssertEqual(candidates[1], stored, "Legacy plain candidate must be preserved") } func testAttachmentPasswordCandidates_hexMatchesDesktop() { // Desktop: key.toString('hex') = lowercase hex of raw 56 bytes let keyBytes = Data([ 0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, ]) let stored = "rawkey:" + keyBytes.hexString let candidates = MessageCrypto.attachmentPasswordCandidates(from: stored) // Desktop: Buffer.from(keyBytes).toString('hex') let expectedDesktopPassword = "deadbeefcafebabe0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30" // Verify hex format matches (lowercase, no separators) XCTAssertTrue(candidates[0].allSatisfy { "0123456789abcdef".contains($0) }, "HEX candidate must be lowercase hex") // Verify exact match with expected Desktop output // Note: the hex is based on keyBytes.hexString which should be lowercase XCTAssertEqual(candidates[0], keyBytes.hexString) XCTAssertEqual(candidates[0], expectedDesktopPassword) } // MARK: - PBKDF2 Parity func testPBKDF2_consistentOutput() throws { let password = "test_password_hex_string" let key1 = CryptoPrimitives.pbkdf2( password: password, salt: "rosetta", iterations: 1000, keyLength: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256) ) let key2 = CryptoPrimitives.pbkdf2( password: password, salt: "rosetta", iterations: 1000, keyLength: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256) ) XCTAssertNotNil(key1) XCTAssertNotNil(key2) XCTAssertEqual(key1, key2, "PBKDF2 must be deterministic") XCTAssertEqual(key1.count, 32, "PBKDF2 key must be 32 bytes") } func testPBKDF2_differentPasswordsDifferentKeys() throws { let key1 = CryptoPrimitives.pbkdf2( password: "password_a", salt: "rosetta", iterations: 1000, keyLength: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256) ) let key2 = CryptoPrimitives.pbkdf2( password: "password_b", salt: "rosetta", iterations: 1000, keyLength: 32, prf: CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256) ) XCTAssertNotEqual(key1, key2, "Different passwords must produce different keys") } // MARK: - encryptWithPassword / decryptWithPassword Parity func testEncryptWithPasswordDesktopCompat_roundTrip() throws { let plaintext = "Cross-platform encrypted message content" let password = "my_private_key_hex" let encrypted = try CryptoManager.shared.encryptWithPasswordDesktopCompat( Data(plaintext.utf8), password: password ) // Must be ivBase64:ctBase64 format let parts = encrypted.components(separatedBy: ":") XCTAssertEqual(parts.count, 2, "Format must be ivBase64:ctBase64") let decrypted = try CryptoManager.shared.decryptWithPassword( encrypted, password: password, requireCompression: true ) let decryptedText = String(data: decrypted, encoding: .utf8) XCTAssertEqual(decryptedText, plaintext, "Desktop-compat round-trip must preserve plaintext") } func testEncryptWithPassword_iOSOnly_roundTrip() throws { let plaintext = "iOS-only storage encryption" let password = "local_private_key" let encrypted = try CryptoManager.shared.encryptWithPassword( Data(plaintext.utf8), password: password ) let decrypted = try CryptoManager.shared.decryptWithPassword( encrypted, password: password, requireCompression: true ) let decryptedText = String(data: decrypted, encoding: .utf8) XCTAssertEqual(decryptedText, plaintext, "iOS-only round-trip must preserve plaintext") } func testDecryptWithPassword_wrongPassword_withCompression_fails() throws { let encrypted = try CryptoManager.shared.encryptWithPasswordDesktopCompat( Data("secret".utf8), password: "correct_password" ) do { let decrypted = try CryptoManager.shared.decryptWithPassword( encrypted, password: "wrong_password", requireCompression: true ) let decryptedText = String(data: decrypted, encoding: .utf8) XCTAssertNotEqual( decryptedText, "secret", "Wrong password must never recover original plaintext" ) } catch { // Expected path for most wrong-password attempts. } } // MARK: - UTF-8 Decoder Parity (Android ↔ iOS) func testAndroidUtf8Decoder_validAscii() { let bytes = Data("Hello".utf8) let result = MessageCrypto.bytesToAndroidUtf8String(bytes) XCTAssertEqual(result, "Hello", "ASCII must decode identically") } func testAndroidUtf8Decoder_validMultibyte() { // Use BMP-only multibyte text here; four-byte emoji sequences are // covered by malformed/compatibility behavior in separate tests. let bytes = Data("Привет мир".utf8) let result = MessageCrypto.bytesToAndroidUtf8String(bytes) XCTAssertEqual(result, "Привет мир", "Valid UTF-8 must decode identically") } func testAndroidUtf8Decoder_matchesWhatWG_onValidUtf8() { // For valid UTF-8, both decoders must produce identical results for _ in 0..<100 { let randomBytes = (0..<56).map { _ in UInt8.random(in: 0...127) } let data = Data(randomBytes) let android = MessageCrypto.bytesToAndroidUtf8String(data) let whatwg = String(decoding: data, as: UTF8.self) XCTAssertEqual(android, whatwg, "For ASCII bytes, Android and WHATWG decoders must match") } } func testLatin1RoundTrip_withBothDecoders_onValidUtf8() { // When input is valid UTF-8 (from CryptoJS Latin-1→UTF-8), both decoders match // This is the ACTUAL scenario for Desktop→iOS messages var allLatin1 = Data(count: 256) for i in 0..<256 { allLatin1[i] = UInt8(i) } // Simulate Desktop: Latin-1 string → UTF-8 bytes guard let latin1String = String(data: allLatin1, encoding: .isoLatin1) else { XCTFail("Latin-1 encoding must work") return } let utf8Bytes = Data(latin1String.utf8) // iOS recovery path 1: WHATWG let recoveredWhatWG = MessageCrypto.androidUtf8BytesToLatin1Bytes(utf8Bytes) // iOS recovery path 2: Android polyfill let recoveredAndroid = MessageCrypto.androidUtf8BytesToLatin1BytesAlt(utf8Bytes) XCTAssertEqual(recoveredWhatWG, allLatin1, "WHATWG decoder must recover all 256 Latin-1 bytes from valid UTF-8") XCTAssertEqual(recoveredAndroid, allLatin1, "Android decoder must recover all 256 Latin-1 bytes from valid UTF-8") XCTAssertEqual(recoveredWhatWG, recoveredAndroid, "Both decoders must produce identical results for valid UTF-8 input") } // MARK: - Defense Layers func testIsProbablyEncryptedPayload_base64Colon() { // ivBase64:ctBase64 (both ≥16 chars) let encrypted = "YWJjZGVmZ2hpamtsbQ==:eHl6MTIzNDU2Nzg5MA==" XCTAssertTrue(MessageRepository.testIsProbablyEncrypted(encrypted), "ivBase64:ctBase64 must be detected") } func testIsProbablyEncryptedPayload_hexString() { // Pure hex ≥40 chars let hex = String(repeating: "ab", count: 30) // 60 chars XCTAssertTrue(MessageRepository.testIsProbablyEncrypted(hex), "Pure hex ≥40 chars must be detected") } func testIsProbablyEncryptedPayload_chunked() { XCTAssertTrue(MessageRepository.testIsProbablyEncrypted("CHNK:data:here"), "CHNK: format must be detected") } func testIsProbablyEncryptedPayload_normalText() { XCTAssertFalse(MessageRepository.testIsProbablyEncrypted("Hello, world!"), "Normal text must NOT be flagged") XCTAssertFalse(MessageRepository.testIsProbablyEncrypted("Привет"), "Cyrillic text must NOT be flagged") } func testIsProbablyEncryptedPayload_shortHex() { // Short hex (<40 chars) must NOT be flagged (could be normal text) XCTAssertFalse(MessageRepository.testIsProbablyEncrypted("abcdef1234"), "Short hex must NOT be flagged") } func testIsProbablyEncryptedPayload_shortBase64Parts() { // Both parts <16 chars must NOT be flagged XCTAssertFalse(MessageRepository.testIsProbablyEncrypted("abc:def"), "Short base64:base64 must NOT be flagged") } func testIsProbablyEncryptedPayload_emptyString() { // Empty string is not encrypted XCTAssertFalse(MessageRepository.testIsProbablyEncrypted(""), "Empty string must NOT be flagged as encrypted (it's just empty)") } // MARK: - Compression Parity (zlib) func testZlibDeflate_inflate_roundTrip() throws { let original = Data("Test compression for cross-platform parity".utf8) let compressed = try CryptoPrimitives.zlibDeflate(original) XCTAssertTrue(compressed.count > 0, "Compressed data must not be empty") XCTAssertTrue(compressed[0] == 0x78, "zlib deflate must start with 0x78 header") let decompressed = try CryptoPrimitives.rawInflate(compressed) XCTAssertEqual(decompressed, original, "Inflate must recover original data") } func testRawDeflate_inflate_roundTrip() throws { let original = Data("Raw deflate for iOS-only storage".utf8) let compressed = try CryptoPrimitives.rawDeflate(original) XCTAssertTrue(compressed.count > 0) let decompressed = try CryptoPrimitives.rawInflate(compressed) XCTAssertEqual(decompressed, original, "Raw inflate must recover original data") } // MARK: - Full Message Flow (Simulate Desktop → iOS) func testFullMessageFlow_desktopToiOS() throws { // Simulate: Desktop sends message to iOS user let senderPrivKey = try P256K.KeyAgreement.PrivateKey() let recipientPrivKey = try P256K.KeyAgreement.PrivateKey() let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString let originalMessage = "Message from Desktop to iOS 🚀" // Step 1: Desktop encrypts (simulated on iOS since crypto is identical) let encrypted = try MessageCrypto.encryptOutgoing( plaintext: originalMessage, recipientPublicKeyHex: recipientPubKeyHex ) // Step 2: Build aesChachaKey for sync (Desktop logic) let senderPrivKeyHex = senderPrivKey.rawRepresentation.hexString guard let latin1 = String(data: encrypted.plainKeyAndNonce, encoding: .isoLatin1) else { XCTFail("Latin-1 encoding failed") return } let aesChachaKey = try CryptoManager.shared.encryptWithPasswordDesktopCompat( Data(latin1.utf8), password: senderPrivKeyHex ) // Step 3: iOS receives and decrypts via ECDH path let (ecdhText, ecdhKeyAndNonce) = try MessageCrypto.decryptIncomingFull( ciphertext: encrypted.content, encryptedKey: encrypted.chachaKey, myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString ) XCTAssertEqual(ecdhText, originalMessage, "ECDH path must decrypt correctly") // Step 4: iOS receives own message via aesChachaKey sync path let syncDecrypted = try CryptoManager.shared.decryptWithPassword( aesChachaKey, password: senderPrivKeyHex ) let syncKeyAndNonce = MessageCrypto.androidUtf8BytesToLatin1Bytes(syncDecrypted) let syncText = try MessageCrypto.decryptIncomingWithPlainKey( ciphertext: encrypted.content, plainKeyAndNonce: syncKeyAndNonce ) XCTAssertEqual(syncText, originalMessage, "aesChachaKey sync path must decrypt correctly") // Step 5: Verify key+nonce matches across both paths XCTAssertEqual(ecdhKeyAndNonce, syncKeyAndNonce, "ECDH and sync paths must recover the same key+nonce") // Step 6: Verify attachment password derivation matches let ecdhPassword = "rawkey:" + ecdhKeyAndNonce.hexString let syncPassword = "rawkey:" + syncKeyAndNonce.hexString let ecdhCandidates = MessageCrypto.attachmentPasswordCandidates(from: ecdhPassword) let syncCandidates = MessageCrypto.attachmentPasswordCandidates(from: syncPassword) XCTAssertEqual(ecdhCandidates, syncCandidates, "Attachment password candidates must be identical across both paths") } func testDecryptIncomingMessage_allowsAttachmentOnlyEmptyContent() throws { let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString var packet = PacketMessage() packet.fromPublicKey = "02peer_attachment_only" packet.toPublicKey = "02my_attachment_only" packet.content = "" packet.chachaKey = "" packet.attachments = [ MessageAttachment( id: "att-1", preview: "preview", blob: "", type: .image, transportTag: "tag-1", transportServer: "cdn.rosetta.im" ), ] let result = SessionManager.testDecryptIncomingMessage( packet: packet, myPublicKey: "02my_attachment_only", privateKeyHex: privateKeyHex, groupKey: nil ) XCTAssertNotNil(result) XCTAssertEqual(result?.text, "") } func testDecryptIncomingMessage_rejectsEmptyContentWithoutAttachments() throws { let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString var packet = PacketMessage() packet.fromPublicKey = "02peer_invalid" packet.toPublicKey = "02my_invalid" packet.content = "" packet.chachaKey = "" packet.attachments = [] let result = SessionManager.testDecryptIncomingMessage( packet: packet, myPublicKey: "02my_invalid", privateKeyHex: privateKeyHex, groupKey: nil ) XCTAssertNil(result) } func testRecoverRetryPlaintext_rejectsCiphertextFallback() throws { let privateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString let wrongPrivateKeyHex = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString let encrypted = try CryptoManager.shared.encryptWithPassword( Data("retry-text".utf8), password: privateKeyHex ) let recoveredWithWrongKey = SessionManager.testRecoverRetryPlaintext( storedText: encrypted, privateKeyHex: wrongPrivateKeyHex ) XCTAssertNotEqual( recoveredWithWrongKey, encrypted, "Retry recovery must never return encrypted wire payload as plaintext" ) XCTAssertNotEqual( recoveredWithWrongKey, "retry-text", "Wrong key must never recover original plaintext" ) let recoveredPlainLegacy = SessionManager.testRecoverRetryPlaintext( storedText: "legacy plain text", privateKeyHex: privateKeyHex ) XCTAssertEqual(recoveredPlainLegacy, "legacy plain text") } func testRawKeyAndNonceParser_requiresStrictRawKeyFormat() throws { let raw = try CryptoPrimitives.randomBytes(count: 56) let validStored = "rawkey:" + raw.hexString let decodedValid = SessionManager.testRawKeyAndNonceFromStoredAttachmentPassword(validStored) XCTAssertEqual(decodedValid, raw) XCTAssertNil( SessionManager.testRawKeyAndNonceFromStoredAttachmentPassword(raw.hexString), "Missing rawkey prefix must be rejected" ) XCTAssertNil( SessionManager.testRawKeyAndNonceFromStoredAttachmentPassword("rawkey:abc"), "Odd-length hex must be rejected" ) XCTAssertNil( SessionManager.testRawKeyAndNonceFromStoredAttachmentPassword("rawkey:zz"), "Non-hex symbols must be rejected" ) } func testDataStrictHexString_rejectsInvalidInput() { XCTAssertNil(Data(strictHexString: "abc")) XCTAssertNil(Data(strictHexString: "0g")) XCTAssertEqual(Data(strictHexString: "0A0b"), Data([0x0A, 0x0B])) } // MARK: - Stress Test: Random Key Bytes func testECDH_100RandomKeys_allDecryptSuccessfully() throws { for i in 0..<100 { let recipientPrivKey = try P256K.KeyAgreement.PrivateKey() let recipientPubKeyHex = recipientPrivKey.publicKey.dataRepresentation.hexString let encrypted = try MessageCrypto.encryptOutgoing( plaintext: "Message #\(i)", recipientPublicKeyHex: recipientPubKeyHex ) let decrypted = try MessageCrypto.decryptIncoming( ciphertext: encrypted.content, encryptedKey: encrypted.chachaKey, myPrivateKeyHex: recipientPrivKey.rawRepresentation.hexString ) XCTAssertEqual(decrypted, "Message #\(i)", "Message #\(i) must decrypt correctly") } } func testAesChachaKey_100RandomKeys_allRoundTrip() throws { for i in 0..<100 { let keyAndNonce = try CryptoPrimitives.randomBytes(count: 56) let password = try P256K.KeyAgreement.PrivateKey().rawRepresentation.hexString guard let latin1 = String(data: keyAndNonce, encoding: .isoLatin1) else { XCTFail("Latin-1 must work for key #\(i)") continue } let encrypted = try CryptoManager.shared.encryptWithPasswordDesktopCompat( Data(latin1.utf8), password: password ) let decrypted = try CryptoManager.shared.decryptWithPassword( encrypted, password: password ) let recovered = MessageCrypto.androidUtf8BytesToLatin1Bytes(decrypted) XCTAssertEqual(recovered, keyAndNonce, "aesChachaKey round-trip #\(i) must recover original 56 bytes") } } }