diff --git a/spec/integ/crypto/verification.spec.ts b/spec/integ/crypto/verification.spec.ts index 2d6800201a3..8cb2a9ea287 100644 --- a/spec/integ/crypto/verification.spec.ts +++ b/spec/integ/crypto/verification.spec.ts @@ -16,9 +16,11 @@ limitations under the License. import "fake-indexeddb/auto"; +import anotherjson from "another-json"; import { MockResponse } from "fetch-mock"; import fetchMock from "fetch-mock-jest"; import { IDBFactory } from "fake-indexeddb"; +import { createHash } from "crypto"; import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src"; import { @@ -235,13 +237,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st // The dummy device makes up a curve25519 keypair and sends the public bit back in an `m.key.verification.key' // We use the Curve25519, HMAC and HKDF implementations in libolm, for now const olmSAS = new global.Olm.SAS(); - returnToDeviceMessageFromSync({ - type: "m.key.verification.key", - content: { - transaction_id: transactionId, - key: olmSAS.get_pubkey(), - }, - }); + returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey())); // alice responds with a 'key' ... requestBody = await expectSendToDeviceMessage("m.key.verification.key"); @@ -265,32 +261,15 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st expect(toDeviceMessage.transaction_id).toEqual(transactionId); // the dummy device also confirms that the emoji match, and sends a mac - const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${TEST_USER_ID}${aliceClient.deviceId}${transactionId}`; - returnToDeviceMessageFromSync({ - type: "m.key.verification.mac", - content: { - keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`), - transaction_id: transactionId, - mac: { - [`ed25519:${TEST_DEVICE_ID}`]: calculateMAC( - olmSAS, - TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64, - `${macInfoBase}ed25519:${TEST_DEVICE_ID}`, - ), - }, - }, - }); + returnToDeviceMessageFromSync( + buildSasMacMessage(transactionId, olmSAS, TEST_USER_ID, aliceClient.deviceId!), + ); // that should satisfy Alice, who should reply with a 'done' await expectSendToDeviceMessage("m.key.verification.done"); // the dummy device also confirms done-ness - returnToDeviceMessageFromSync({ - type: "m.key.verification.done", - content: { - transaction_id: transactionId, - }, - }); + returnToDeviceMessageFromSync(buildDoneMessage(transactionId)); // ... and the whole thing should be done! await verificationPromise; @@ -304,6 +283,102 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st olmSAS.free(); }); + it("can initiate SAS verification ourselves", async () => { + aliceClient = await startTestClient(); + await waitForDeviceList(); + + // Alice sends a m.key.verification.request + const [, request] = await Promise.all([ + expectSendToDeviceMessage("m.key.verification.request"), + aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID), + ]); + const transactionId = request.transactionId!; + + // The dummy device replies with an m.key.verification.ready + returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"])); + await waitForVerificationRequestChanged(request); + expect(request.phase).toEqual(VerificationPhase.Ready); + expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true); + + // advance the clock, because the devicelist likes to sleep for 5ms during key downloads + await jest.advanceTimersByTimeAsync(10); + + // And now Alice starts a SAS verification + let sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start"); + await request.startVerification("m.sas.v1"); + let requestBody = await sendToDevicePromise; + + let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; + expect(toDeviceMessage).toEqual({ + from_device: aliceClient.deviceId, + method: "m.sas.v1", + transaction_id: transactionId, + hashes: ["sha256"], + key_agreement_protocols: expect.arrayContaining(["curve25519-hkdf-sha256"]), + message_authentication_codes: expect.arrayContaining(["hkdf-hmac-sha256.v2"]), + short_authentication_string: ["decimal", "emoji"], + }); + + expect(request.chosenMethod).toEqual("m.sas.v1"); + + // There should now be a `verifier` + const verifier: Verifier = request.verifier!; + expect(verifier).toBeDefined(); + expect(verifier.getShowSasCallbacks()).toBeNull(); + const verificationPromise = verifier.verify(); + + // The dummy device makes up a curve25519 keypair and uses the hash in an 'm.key.verification.accept' + // We use the Curve25519, HMAC and HKDF implementations in libolm, for now + const olmSAS = new global.Olm.SAS(); + const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(toDeviceMessage); + + sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.key"); + returnToDeviceMessageFromSync(buildSasAcceptMessage(transactionId, commitmentStr)); + + // alice responds with a 'key' ... + requestBody = await sendToDevicePromise; + + toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; + expect(toDeviceMessage.transaction_id).toEqual(transactionId); + const aliceDevicePubKeyBase64 = toDeviceMessage.key; + olmSAS.set_their_key(aliceDevicePubKeyBase64); + + // ... and the dummy device also sends a 'key' + returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey())); + + // ... and the client is notified to show the emoji + const showSas = await new Promise((resolve) => { + verifier.once(VerifierEvent.ShowSas, resolve); + }); + + // `getShowSasCallbacks` is an alternative way to get the callbacks + expect(verifier.getShowSasCallbacks()).toBe(showSas); + expect(verifier.getReciprocateQrCodeCallbacks()).toBeNull(); + + // user confirms that the emoji match, and alice sends a 'mac' + [requestBody] = await Promise.all([expectSendToDeviceMessage("m.key.verification.mac"), showSas.confirm()]); + toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID]; + expect(toDeviceMessage.transaction_id).toEqual(transactionId); + + // the dummy device also confirms that the emoji match, and sends a mac + returnToDeviceMessageFromSync( + buildSasMacMessage(transactionId, olmSAS, TEST_USER_ID, aliceClient.deviceId!), + ); + + // that should satisfy Alice, who should reply with a 'done' + await expectSendToDeviceMessage("m.key.verification.done"); + + // the dummy device also confirms done-ness + returnToDeviceMessageFromSync(buildDoneMessage(transactionId)); + + // ... and the whole thing should be done! + await verificationPromise; + expect(request.phase).toEqual(VerificationPhase.Done); + + // we're done with the temporary keypair + olmSAS.free(); + }); + it("Can make a verification request to *all* devices", async () => { aliceClient = await startTestClient(); // we need an existing cross-signing key for this @@ -610,6 +685,11 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string { return mac; } +/** Calculate the sha256 hash of a string, encoding as unpadded base64 */ +function sha256(commitmentStr: string): string { + return encodeUnpaddedBase64(createHash("sha256").update(commitmentStr, "utf8").digest()); +} + function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string { return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, ""); } @@ -642,3 +722,64 @@ function buildSasStartMessage(transactionId: string): { type: string; content: o }, }; } + +/** build an m.key.verification.accept to-device message suitable for the SAS flow */ +function buildSasAcceptMessage(transactionId: string, commitmentStr: string) { + return { + type: "m.key.verification.accept", + content: { + transaction_id: transactionId, + commitment: sha256(commitmentStr), + hash: "sha256", + key_agreement_protocol: "curve25519-hkdf-sha256", + short_authentication_string: ["decimal", "emoji"], + message_authentication_code: "hkdf-hmac-sha256.v2", + }, + }; +} + +/** build an m.key.verification.key to-device message suitable for the SAS flow */ +function buildSasKeyMessage(transactionId: string, key: string): { type: string; content: object } { + return { + type: "m.key.verification.key", + content: { + transaction_id: transactionId, + key: key, + }, + }; +} + +/** build an m.key.verification.mac to-device message suitable for the SAS flow, originating from the dummy device */ +function buildSasMacMessage( + transactionId: string, + olmSAS: Olm.SAS, + recipientUserId: string, + recipientDeviceId: string, +): { type: string; content: object } { + const macInfoBase = `MATRIX_KEY_VERIFICATION_MAC${TEST_USER_ID}${TEST_DEVICE_ID}${recipientUserId}${recipientDeviceId}${transactionId}`; + + return { + type: "m.key.verification.mac", + content: { + keys: calculateMAC(olmSAS, `ed25519:${TEST_DEVICE_ID}`, `${macInfoBase}KEY_IDS`), + transaction_id: transactionId, + mac: { + [`ed25519:${TEST_DEVICE_ID}`]: calculateMAC( + olmSAS, + TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64, + `${macInfoBase}ed25519:${TEST_DEVICE_ID}`, + ), + }, + }, + }; +} + +/** build an m.key.verification.done to-device message */ +function buildDoneMessage(transactionId: string) { + return { + type: "m.key.verification.done", + content: { + transaction_id: transactionId, + }, + }; +} diff --git a/spec/unit/rust-crypto/verification.spec.ts b/spec/unit/rust-crypto/verification.spec.ts new file mode 100644 index 00000000000..cb92fd5778e --- /dev/null +++ b/spec/unit/rust-crypto/verification.spec.ts @@ -0,0 +1,50 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js"; +import { Mocked } from "jest-mock"; + +import { RustVerificationRequest } from "../../../src/rust-crypto/verification"; +import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor"; + +describe("VerificationRequest", () => { + describe("startVerification", () => { + let mockedInner: Mocked; + let mockedOutgoingRequestProcessor: Mocked; + let request: RustVerificationRequest; + + beforeEach(() => { + mockedInner = { + registerChangesCallback: jest.fn(), + startSas: jest.fn(), + } as unknown as Mocked; + mockedOutgoingRequestProcessor = {} as Mocked; + request = new RustVerificationRequest(mockedInner, mockedOutgoingRequestProcessor); + }); + + it("does not permit methods other than SAS", async () => { + await expect(request.startVerification("m.reciprocate.v1")).rejects.toThrow( + "Unsupported verification method", + ); + }); + + it("raises an error if starting verification does not produce a verifier", async () => { + await expect(request.startVerification("m.sas.v1")).rejects.toThrow( + "Still no verifier after startSas() call", + ); + }); + }); +}); diff --git a/src/crypto-api/verification.ts b/src/crypto-api/verification.ts index fc5c75bc7a0..1d079b8b3a2 100644 --- a/src/crypto-api/verification.ts +++ b/src/crypto-api/verification.ts @@ -128,9 +128,20 @@ export interface VerificationRequest * @param targetDevice - details of where to send the request to. * * @returns The verifier which will do the actual verification. + * + * @deprecated Use {@link VerificationRequest#startVerification} instead. */ beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier; + /** + * Send an `m.key.verification.start` event to start verification via a particular method. + * + * @param method - the name of the verification method to use. + * + * @returns The verifier which will do the actual verification. + */ + startVerification(method: string): Promise; + /** * The verifier which is doing the actual verification, once the method has been established. * Only defined when the `phase` is Started. diff --git a/src/crypto/verification/request/VerificationRequest.ts b/src/crypto/verification/request/VerificationRequest.ts index c5ffba21419..7a1a999cf74 100644 --- a/src/crypto/verification/request/VerificationRequest.ts +++ b/src/crypto/verification/request/VerificationRequest.ts @@ -30,6 +30,7 @@ import { VerificationRequest as IVerificationRequest, VerificationRequestEvent, VerificationRequestEventHandlerMap, + Verifier, } from "../../../crypto-api/verification"; // backwards-compatibility exports @@ -458,6 +459,13 @@ export class VerificationRequest { + const verifier = this.beginKeyVerification(method); + // kick off the verification in the background, but *don't* wait for to complete: we need to return the `Verifier`. + verifier.verify(); + return verifier; + } + /** * sends the initial .request event. * @returns resolves when the event has been sent. diff --git a/src/rust-crypto/verification.ts b/src/rust-crypto/verification.ts index 7fb314c78d9..9d36a45b641 100644 --- a/src/rust-crypto/verification.ts +++ b/src/rust-crypto/verification.ts @@ -236,6 +236,35 @@ export class RustVerificationRequest throw new Error("not implemented"); } + /** + * Send an `m.key.verification.start` event to start verification via a particular method. + * + * Implementation of {@link Crypto.VerificationRequest#startVerification}. + * + * @param method - the name of the verification method to use. + */ + public async startVerification(method: string): Promise { + if (method !== "m.sas.v1") { + throw new Error(`Unsupported verification method ${method}`); + } + + const res: + | [RustSdkCryptoJs.Sas, RustSdkCryptoJs.RoomMessageRequest | RustSdkCryptoJs.ToDeviceRequest] + | undefined = await this.inner.startSas(); + + if (res) { + const [, req] = res; + await this.outgoingRequestProcessor.makeOutgoingRequest(req); + } + + // this should have triggered the onChange callback, and we should now have a verifier + if (!this._verifier) { + throw new Error("Still no verifier after startSas() call"); + } + + return this._verifier; + } + /** * The verifier which is doing the actual verification, once the method has been established. * Only defined when the `phase` is Started.