diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index af553790418..11be866b85b 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -20,7 +20,7 @@ import { MockResponse } from "fetch-mock"; import fetchMock from "fetch-mock-jest"; import { IDBFactory } from "fake-indexeddb"; -import { createClient, CryptoEvent, MatrixClient } from "../../../src"; +import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src"; import { canAcceptVerificationRequest, ShowQrCodeCallbacks, @@ -88,6 +88,9 @@ afterAll(() => { } }); +/** The homeserver url that we give to the test client, and where we intercept /sync, /keys, etc requests. */ +const TEST_HOMESERVER_URL = "https://alice-server.com"; + /** * Integration tests for verification functionality. * @@ -96,16 +99,6 @@ afterAll(() => { */ // we test with both crypto stacks... describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: string, initCrypto: InitCrypto) => { - // and with (1) the default verification method list, (2) a custom verification method list. - describe.each([undefined, ["m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1"]])( - "supported methods=%s", - (methods) => { - runTests(backend, initCrypto, methods); - }, - ); -}); - -function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | undefined) { // oldBackendOnly is an alternative to `it` or `test` which will skip the test if we are running against the // Rust backend. Once we have full support in the rust sdk, it will go away. const oldBackendOnly = backend === "rust-sdk" ? test.skip : test; @@ -113,13 +106,13 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u /** the client under test */ let aliceClient: MatrixClient; - /** an object which intercepts `/sync` requests from {@link #aliceClient} */ + /** an object which intercepts `/sync` requests on the test homeserver */ let syncResponder: SyncResponder; - /** an object which intercepts `/keys/query` requests from {@link #aliceClient} */ + /** an object which intercepts `/keys/query` requests on the test homeserver */ let e2eKeyResponder: E2EKeyResponder; - /** an object which intercepts `/keys/upload` requests from {@link #aliceClient} */ + /** an object which intercepts `/keys/upload` requests on the test homeserver */ let e2eKeyReceiver: E2EKeyReceiver; beforeEach(async () => { @@ -127,28 +120,18 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u fetchMock.catch(404); fetchMock.config.warnOnFallback = false; - const homeserverUrl = "https://alice-server.com"; - aliceClient = createClient({ - baseUrl: homeserverUrl, - userId: TEST_USER_ID, - accessToken: "akjgkrgjs", - deviceId: "device_under_test", - verificationMethods: methods, - }); - - await initCrypto(aliceClient); - - e2eKeyReceiver = new E2EKeyReceiver(aliceClient.getHomeserverUrl()); - e2eKeyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl()); + e2eKeyReceiver = new E2EKeyReceiver(TEST_HOMESERVER_URL); + e2eKeyResponder = new E2EKeyResponder(TEST_HOMESERVER_URL); e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver); + syncResponder = new SyncResponder(TEST_HOMESERVER_URL); - syncResponder = new SyncResponder(aliceClient.getHomeserverUrl()); - mockInitialApiRequests(aliceClient.getHomeserverUrl()); - await aliceClient.startClient(); + mockInitialApiRequests(TEST_HOMESERVER_URL); }); afterEach(async () => { - await aliceClient.stopClient(); + if (aliceClient !== undefined) { + await aliceClient.stopClient(); + } // Allow in-flight things to complete before we tear down the test await jest.runAllTimersAsync(); @@ -162,7 +145,10 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA); }); - it("can verify another device via SAS", async () => { + // test with (1) the default verification method list, (2) a custom verification method list. + const TEST_METHODS = ["m.sas.v1", "m.qr_code.show.v1", "m.reciprocate.v1"]; + it.each([undefined, TEST_METHODS])("can verify via SAS (supported methods=%s)", async (methods) => { + aliceClient = await startTestClient({ verificationMethods: methods }); await waitForDeviceList(); // initially there should be no verifications in progress @@ -176,7 +162,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u expectSendToDeviceMessage("m.key.verification.request"), aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID), ]); - const transactionId = request.transactionId; + const transactionId = request.transactionId!; expect(transactionId).toBeDefined(); expect(request.phase).toEqual(VerificationPhase.Requested); expect(request.roomId).toBeUndefined(); @@ -202,32 +188,14 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u } // The dummy device replies with an m.key.verification.ready... - returnToDeviceMessageFromSync({ - type: "m.key.verification.ready", - content: { - from_device: TEST_DEVICE_ID, - methods: ["m.sas.v1"], - transaction_id: transactionId, - }, - }); + returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"])); await waitForVerificationRequestChanged(request); expect(request.phase).toEqual(VerificationPhase.Ready); expect(request.otherDeviceId).toEqual(TEST_DEVICE_ID); // ... and picks a method with m.key.verification.start - returnToDeviceMessageFromSync({ - type: "m.key.verification.start", - content: { - from_device: TEST_DEVICE_ID, - method: "m.sas.v1", - transaction_id: transactionId, - hashes: ["sha256"], - key_agreement_protocols: ["curve25519-hkdf-sha256"], - message_authentication_codes: ["hkdf-hmac-sha256.v2"], - // we have to include "decimal" per the spec. - short_authentication_string: ["decimal", "emoji"], - }, - }); + returnToDeviceMessageFromSync(buildSasStartMessage(transactionId)); + // as soon as the Changed event arrives, `verifier` should be defined const verifier = await new Promise((resolve) => { function onChange() { @@ -327,6 +295,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u }); it("Can make a verification request to *all* devices", async () => { + aliceClient = await startTestClient(); // we need an existing cross-signing key for this e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA); await waitForDeviceList(); @@ -356,6 +325,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u }); oldBackendOnly("can verify another via QR code with an untrusted cross-signing key", async () => { + aliceClient = await startTestClient(); // QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now. e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA); await waitForDeviceList(); @@ -366,26 +336,17 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u expectSendToDeviceMessage("m.key.verification.request"), aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID), ]); - const transactionId = request.transactionId; + const transactionId = request.transactionId!; const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; expect(toDeviceMessage.methods).toContain("m.qr_code.show.v1"); expect(toDeviceMessage.methods).toContain("m.reciprocate.v1"); - if (methods === undefined) { - expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1"); - } + expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1"); expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId); expect(toDeviceMessage.transaction_id).toEqual(transactionId); // The dummy device replies with an m.key.verification.ready, with an indication we can scan the QR code - returnToDeviceMessageFromSync({ - type: "m.key.verification.ready", - content: { - from_device: TEST_DEVICE_ID, - methods: ["m.qr_code.scan.v1"], - transaction_id: transactionId, - }, - }); + returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.qr_code.scan.v1"])); await waitForVerificationRequestChanged(request); expect(request.phase).toEqual(VerificationPhase.Ready); @@ -447,6 +408,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u }); it("can cancel during the SAS phase", async () => { + aliceClient = await startTestClient(); await waitForDeviceList(); // have alice initiate a verification. She should send a m.key.verification.request @@ -454,33 +416,14 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u expectSendToDeviceMessage("m.key.verification.request"), aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID), ]); - const transactionId = request.transactionId; + const transactionId = request.transactionId!; // The dummy device replies with an m.key.verification.ready... - returnToDeviceMessageFromSync({ - type: "m.key.verification.ready", - content: { - from_device: TEST_DEVICE_ID, - methods: ["m.sas.v1"], - transaction_id: transactionId, - }, - }); + returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"])); await waitForVerificationRequestChanged(request); // ... and picks a method with m.key.verification.start - returnToDeviceMessageFromSync({ - type: "m.key.verification.start", - content: { - from_device: TEST_DEVICE_ID, - method: "m.sas.v1", - transaction_id: transactionId, - hashes: ["sha256"], - key_agreement_protocols: ["curve25519-hkdf-sha256"], - message_authentication_codes: ["hkdf-hmac-sha256.v2"], - // we have to include "decimal" per the spec. - short_authentication_string: ["decimal", "emoji"], - }, - }); + returnToDeviceMessageFromSync(buildSasStartMessage(transactionId)); await waitForVerificationRequestChanged(request); expect(request.phase).toEqual(VerificationPhase.Started); @@ -514,6 +457,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u }); oldBackendOnly("Incoming verification: can accept", async () => { + aliceClient = await startTestClient(); const TRANSACTION_ID = "abcd"; // Initiate the request by sending a to-device message @@ -551,6 +495,19 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u }); }); + async function startTestClient(opts: Partial = {}): Promise { + const client = createClient({ + baseUrl: TEST_HOMESERVER_URL, + userId: TEST_USER_ID, + accessToken: "akjgkrgjs", + deviceId: "device_under_test", + ...opts, + }); + await initCrypto(client); + await client.startClient(); + return client; + } + /** make sure that the client knows about the dummy device */ async function waitForDeviceList(): Promise { // Completing the initial sync will make the device list download outdated device lists (of which our own @@ -568,7 +525,7 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u ev.sender ??= TEST_USER_ID; syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } }); } -} +}); /** * Wait for the client under test to send a to-device message of the given type. @@ -613,3 +570,32 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string { function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string { return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, ""); } + +/** build an m.key.verification.ready to-device message originating from the dummy device */ +function buildReadyMessage(transactionId: string, methods: string[]): { type: string; content: object } { + return { + type: "m.key.verification.ready", + content: { + from_device: TEST_DEVICE_ID, + methods: methods, + transaction_id: transactionId, + }, + }; +} + +/** build an m.key.verification.start to-device message suitable for the SAS flow, originating from the dummy device */ +function buildSasStartMessage(transactionId: string): { type: string; content: object } { + return { + type: "m.key.verification.start", + content: { + from_device: TEST_DEVICE_ID, + method: "m.sas.v1", + transaction_id: transactionId, + hashes: ["sha256"], + key_agreement_protocols: ["curve25519-hkdf-sha256"], + message_authentication_codes: ["hkdf-hmac-sha256.v2"], + // we have to include "decimal" per the spec. + short_authentication_string: ["decimal", "emoji"], + }, + }; +}