import CryptoKit import XCTest @testable import LChatCore // 🤖 Cross-implementation vector: the envelope below was sealed by the home // server's JS crypto (home/web/static/crypto.js) with a fixed PRF output and // DEK. If Swift opens it to the expected server list, the KEK derivation, // AES-GCM primitive layout, DEK unwrap, padding and envelope framing all match // the web byte-for-byte — so a backup written on the web restores on the phone. // Regenerate with `node home/test/gen-home-vector.mjs`. final class HomeCryptoTests: XCTestCase { // Base64url inputs from the JS generator. let prf = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8" let dekRaw = "AAcOFRwjKjE4P0ZNVFtiaXB3foWMk5qhqK-2vcTL0tk" let credId = "cred-abc" let wrapped = "LfDy9OjW1vWNWfVcpShcxANG7mcWIpsrDtS2yrlhic7KwdN1abiQIF8zOg7EdHF5p9oaI0f09LGg_Wvl" private func data(_ b64url: String) -> Data { Data(base64urlEncoded: b64url)! } func testKEKUnwrapsTheJSWrappedDEK() throws { let kek = HomeCrypto.kekFromPRF(data(prf)) let dek = try HomeCrypto.unwrapDEK(wrapped, kek: kek) XCTAssertEqual(dek.withUnsafeBytes { Data($0) }, data(dekRaw), "Swift KEK + unwrap must recover the JS-wrapped DEK") } func testOpensJSSealedEnvelope() throws { // The full JS-sealed envelope (truncated here is not allowed — it's the // exact bytes the generator printed). let envelope = data(HomeCryptoVectors.envelope) let dek = SymmetricKey(data: data(dekRaw)) let (objJSON, keys) = try HomeCrypto.openBlob(envelope, dek: dek) XCTAssertEqual(keys[credId], wrapped, "header preserves the wrapped-DEK map") struct Blob: Decodable { struct Server: Decodable { let url: String; let name: String } let v: Int let servers: [Server] } let blob = try JSONDecoder().decode(Blob.self, from: objJSON) XCTAssertEqual(blob.v, 1) XCTAssertEqual(blob.servers.first?.url, "https://test.example.com") XCTAssertEqual(blob.servers.first?.name, "Test") } func testSwiftRoundTrip() throws { let dek = HomeCrypto.generateDEK() let kek = HomeCrypto.kekFromPRF(Data("some-prf-output-32-bytes--------!".utf8)) let wrappedDEK = try HomeCrypto.wrapDEK(dek, kek: kek) let obj = Data(#"{"v":1,"servers":[{"url":"https://a.example","name":"A"}]}"#.utf8) let sealed = try HomeCrypto.sealBlob(objJSON: obj, dek: dek, keys: ["c1": wrappedDEK]) // Unwrap the DEK the way a real open would, then read it back. let reDEK = try HomeCrypto.unwrapDEK(wrappedDEK, kek: kek) let (out, keys) = try HomeCrypto.openBlob(sealed, dek: reDEK) XCTAssertEqual(out, obj) XCTAssertEqual(keys["c1"], wrappedDEK) } func testPaddingHidesLength() throws { let small = try HomeCrypto.pad(Data([1, 2, 3])) let bigger = try HomeCrypto.pad(Data(repeating: 9, count: 5000)) XCTAssertEqual(small.count, HomeCrypto.blobPad) XCTAssertEqual(bigger.count, HomeCrypto.blobPad) XCTAssertEqual(try HomeCrypto.unpad(small), Data([1, 2, 3])) XCTAssertEqual(try HomeCrypto.unpad(bigger), Data(repeating: 9, count: 5000)) } }