import XCTest import CryptoKit @testable import ByteLystPlatformSDK final class BLFieldEncryptTests: XCTestCase { private var key: SymmetricKey! private let dekId = "dek_user1_test" override func setUp() { super.setUp() key = BLFieldEncrypt.generateKey() } // MARK: - Encrypt / Decrypt Roundtrip func testEncryptDecryptRoundtrip() throws { let plaintext = "Hello, World!" let encrypted = try BLFieldEncrypt.encrypt(plaintext, key: key, dekId: dekId) let decrypted = try BLFieldEncrypt.decrypt(encrypted, key: key) XCTAssertEqual(decrypted, plaintext) } func testEncryptDecryptEmptyString() throws { let plaintext = "" let encrypted = try BLFieldEncrypt.encrypt(plaintext, key: key, dekId: dekId) let decrypted = try BLFieldEncrypt.decrypt(encrypted, key: key) XCTAssertEqual(decrypted, plaintext) } func testEncryptDecryptUnicode() throws { let plaintext = "こんにちは世界 🌍 مرحبا Ñoño" let encrypted = try BLFieldEncrypt.encrypt(plaintext, key: key, dekId: dekId) let decrypted = try BLFieldEncrypt.decrypt(encrypted, key: key) XCTAssertEqual(decrypted, plaintext) } func testEncryptDecryptLargePayload() throws { let plaintext = String(repeating: "A", count: 100_000) let encrypted = try BLFieldEncrypt.encrypt(plaintext, key: key, dekId: dekId) let decrypted = try BLFieldEncrypt.decrypt(encrypted, key: key) XCTAssertEqual(decrypted, plaintext) } // MARK: - EncryptedField Structure func testEncryptedFieldHasCorrectSentinel() throws { let encrypted = try BLFieldEncrypt.encrypt("test", key: key, dekId: dekId) XCTAssertTrue(encrypted.__encrypted) XCTAssertEqual(encrypted.v, 1) XCTAssertEqual(encrypted.alg, "aes-256-gcm") XCTAssertEqual(encrypted.dekId, dekId) } func testEncryptedFieldHasCorrectHexLengths() throws { let encrypted = try BLFieldEncrypt.encrypt("test", key: key, dekId: dekId) // IV: 12 bytes = 24 hex chars XCTAssertEqual(encrypted.iv.count, 24) // Tag: 16 bytes = 32 hex chars XCTAssertEqual(encrypted.tag.count, 32) // ct should be non-empty hex XCTAssertFalse(encrypted.ct.isEmpty) } func testEachEncryptionProducesUniqueIV() throws { let a = try BLFieldEncrypt.encrypt("same", key: key, dekId: dekId) let b = try BLFieldEncrypt.encrypt("same", key: key, dekId: dekId) XCTAssertNotEqual(a.iv, b.iv, "Each encryption should use a unique IV") XCTAssertNotEqual(a.ct, b.ct, "Ciphertext should differ with different IVs") } // MARK: - AAD (Additional Authenticated Data) func testEncryptDecryptWithAAD() throws { let plaintext = "secret data" let aad = "user123:notes" let encrypted = try BLFieldEncrypt.encrypt(plaintext, key: key, dekId: dekId, aad: aad) let decrypted = try BLFieldEncrypt.decrypt(encrypted, key: key, aad: aad) XCTAssertEqual(decrypted, plaintext) } func testDecryptWithWrongAADFails() throws { let encrypted = try BLFieldEncrypt.encrypt("secret", key: key, dekId: dekId, aad: "correct") XCTAssertThrowsError(try BLFieldEncrypt.decrypt(encrypted, key: key, aad: "wrong")) } func testDecryptWithMissingAADFails() throws { let encrypted = try BLFieldEncrypt.encrypt("secret", key: key, dekId: dekId, aad: "some-aad") XCTAssertThrowsError(try BLFieldEncrypt.decrypt(encrypted, key: key)) } // MARK: - Wrong Key func testDecryptWithWrongKeyFails() throws { let encrypted = try BLFieldEncrypt.encrypt("secret", key: key, dekId: dekId) let wrongKey = BLFieldEncrypt.generateKey() XCTAssertThrowsError(try BLFieldEncrypt.decrypt(encrypted, key: wrongKey)) } // MARK: - Key Size Validation func testEncryptRejectsShortKey() { let shortKey = SymmetricKey(size: .bits128) XCTAssertThrowsError(try BLFieldEncrypt.encrypt("test", key: shortKey, dekId: dekId)) { error in guard let encError = error as? BLFieldEncryptError, case .invalidKeySize = encError else { XCTFail("Expected invalidKeySize error") return } } } // MARK: - Key from Hex func testKeyFromHex() throws { let hex = String(repeating: "ab", count: 32) // 64 hex chars = 32 bytes let key = try BLFieldEncrypt.keyFromHex(hex) let encrypted = try BLFieldEncrypt.encrypt("test", key: key, dekId: dekId) let decrypted = try BLFieldEncrypt.decrypt(encrypted, key: key) XCTAssertEqual(decrypted, "test") } func testKeyFromHexRejectsInvalidLength() { XCTAssertThrowsError(try BLFieldEncrypt.keyFromHex("aabb")) } // MARK: - isEncrypted Type Guard func testIsEncryptedWithValidField() throws { let encrypted = try BLFieldEncrypt.encrypt("test", key: key, dekId: dekId) XCTAssertTrue(BLFieldEncrypt.isEncrypted(encrypted)) } func testIsEncryptedWithNil() { XCTAssertFalse(BLFieldEncrypt.isEncrypted(nil as BLEncryptedField?)) } func testIsEncryptedWithDictionary() { let dict: [String: Any] = [ "__encrypted": true, "v": 1, "alg": "aes-256-gcm", "ct": "abcd", "iv": "1234", "tag": "5678", "dekId": "dek_test", ] XCTAssertTrue(BLFieldEncrypt.isEncrypted(dict)) } func testIsEncryptedWithPlainString() { XCTAssertFalse(BLFieldEncrypt.isEncrypted("just a string" as Any)) } func testIsEncryptedWithIncompleteDict() { let dict: [String: Any] = ["__encrypted": true, "v": 1] XCTAssertFalse(BLFieldEncrypt.isEncrypted(dict)) } // MARK: - JSON Codable Roundtrip func testEncryptedFieldCodableRoundtrip() throws { let encrypted = try BLFieldEncrypt.encrypt("codable test", key: key, dekId: dekId) let encoder = JSONEncoder() let data = try encoder.encode(encrypted) let decoder = JSONDecoder() let decoded = try decoder.decode(BLEncryptedField.self, from: data) let decrypted = try BLFieldEncrypt.decrypt(decoded, key: key) XCTAssertEqual(decrypted, "codable test") } // MARK: - Hex Helpers func testDataHexRoundtrip() { let original = Data([0x00, 0x0f, 0xff, 0xab, 0xcd]) let hex = original.hexString XCTAssertEqual(hex, "000fffabcd") let restored = Data(hexString: hex) XCTAssertEqual(restored, original) } func testDataHexInvalidString() { XCTAssertNil(Data(hexString: "xyz")) XCTAssertNil(Data(hexString: "a")) // odd length } }