- Swift: BLFieldEncrypt.swift + BLFieldEncryptTests.swift (22 tests)
- CryptoKit AES-256-GCM, BLEncryptedField Codable struct
- encrypt/decrypt, AAD support, generateKey, keyFromHex, isEncrypted
- Data hex helpers (hexString, init?(hexString:))
- Kotlin: BLFieldEncrypt.kt + BLFieldEncryptTest.kt (21 tests)
- javax.crypto AES-256-GCM, BLEncryptedField data class
- encrypt/decrypt, AAD support, generateKey, keyFromHex, isEncrypted
- ByteArray/String hex extension functions
- Wire-compatible: same EncryptedField JSON structure as @bytelyst/field-encrypt (TS)
- { __encrypted: true, v: 1, alg: 'aes-256-gcm', ct, iv, tag, dekId }
- All hex-encoded, 12-byte IV, 16-byte auth tag
- Fix: ByteLystPlatform.kt getString() → read() (pre-existing compile error)
187 lines
6.7 KiB
Swift
187 lines
6.7 KiB
Swift
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
|
|
}
|
|
}
|