| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // The sealed-branding KDF must agree byte-for-byte with the web |
| 5 | // (crypto.js deriveBrandingKey) — the pinned vector here is the SAME one |
| 6 | // test/crypto.test.mjs pins. If this fails you changed the derivation; |
| 7 | // don't re-pin — sealed blobs already at rest would silently brick. |
| 8 | final class SealedBrandingTests: XCTestCase { |
| 9 | private let vectorB = "k3m7p2qx4f" |
| 10 | private let vectorSalt = "AAECAwQFBgcICQoLDA0ODw" // bytes 0x00..0x0f |
| 11 | private let vectorBlob = |
| 12 | "uu9h74mrboFkgWSzYhnd67QKxlD7k9t6nJ79flw9UKkdkTMIZRTtxOcBW-ivDIckbudRf9uB64AXiGB4qcPfOA" |
| 13 | |
| 14 | func testPinnedWebVectorDecrypts() throws { |
| 15 | struct Pin: Codable { let name: String; let emoji: String } |
| 16 | let key = try SealedBrandingCrypto.deriveKey(b: vectorB, saltB64url: vectorSalt) |
| 17 | let pin = try key.decryptJSON(Pin.self, blob: vectorBlob) |
| 18 | XCTAssertEqual(pin.name, "Vector Pin") |
| 19 | XCTAssertEqual(pin.emoji, "🔏") |
| 20 | } |
| 21 | |
| 22 | func testWrongKeyOrSaltFailsClosed() throws { |
| 23 | let wrongB = try SealedBrandingCrypto.deriveKey(b: "aaaaaaaaaa", saltB64url: vectorSalt) |
| 24 | XCTAssertThrowsError(try wrongB.decryptJSON(SealedBranding.self, blob: vectorBlob)) |
| 25 | let wrongSalt = try SealedBrandingCrypto.deriveKey(b: vectorB, saltB64url: "_w0ODQwLCgkIBwYFBAMCAQA") |
| 26 | XCTAssertThrowsError(try wrongSalt.decryptJSON(SealedBranding.self, blob: vectorBlob)) |
| 27 | } |
| 28 | |
| 29 | func testEmptyInputsThrow() { |
| 30 | XCTAssertThrowsError(try SealedBrandingCrypto.deriveKey(b: "", saltB64url: vectorSalt)) |
| 31 | XCTAssertThrowsError(try SealedBrandingCrypto.deriveKey(b: vectorB, saltB64url: "")) |
| 32 | } |
| 33 | |
| 34 | // The sealed avatar path: bytes encrypted under the derived key round-trip |
| 35 | // through the same iv||ct format the web's encryptBytes produces. |
| 36 | func testAvatarBytesRoundTrip() throws { |
| 37 | let key = try SealedBrandingCrypto.deriveKey(b: vectorB, saltB64url: vectorSalt) |
| 38 | let photo = Data([0xFF, 0xD8, 0xFF, 0xE0, 1, 2, 3, 4, 5]) |
| 39 | let sealed = try key.encryptBytes(photo) |
| 40 | XCTAssertEqual(try key.decryptBytes(sealed), photo) |
| 41 | } |
| 42 | |
| 43 | // The blob's field names are the contract with the web (sealBrandingNow's |
| 44 | // JSON) — decode a literal to keep them pinned. |
| 45 | func testBrandingBlobFieldNames() throws { |
| 46 | let json = """ |
| 47 | {"name":"Family","description":"Us","admin_name":"Paul", |
| 48 | "admin_email":"p@x.com","avatar_alt":"A sunflower","avatar_mime":"image/jpeg"} |
| 49 | """.data(using: .utf8)! |
| 50 | let b = try JSONDecoder().decode(SealedBranding.self, from: json) |
| 51 | XCTAssertEqual(b.name, "Family") |
| 52 | XCTAssertEqual(b.avatar_mime, "image/jpeg") |
| 53 | } |
| 54 | } |
| 55 | |