1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Element-R: support for starting a SAS verification (#3528)

* integ tests: factor out some utility methods

* Add `VerificationRequest.startVerification` to replace `beginKeyVerification`

The rust SDK ties together creating the verifier and sending the
`m.key.verification.start` message, so we need to combine
`.beginKeyVerification` and `.verify`.

* add some unit tests
This commit is contained in:
Richard van der Hoff
2023-06-29 17:34:49 +01:00
committed by GitHub
parent 1828826661
commit 5165899e82
5 changed files with 267 additions and 28 deletions

View File

@@ -16,9 +16,11 @@ limitations under the License.
import "fake-indexeddb/auto"; import "fake-indexeddb/auto";
import anotherjson from "another-json";
import { MockResponse } from "fetch-mock"; import { MockResponse } from "fetch-mock";
import fetchMock from "fetch-mock-jest"; import fetchMock from "fetch-mock-jest";
import { IDBFactory } from "fake-indexeddb"; import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto";
import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src"; import { createClient, CryptoEvent, ICreateClientOpts, MatrixClient } from "../../../src";
import { import {
@@ -209,13 +211,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' // 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 // We use the Curve25519, HMAC and HKDF implementations in libolm, for now
const olmSAS = new global.Olm.SAS(); const olmSAS = new global.Olm.SAS();
returnToDeviceMessageFromSync({ returnToDeviceMessageFromSync(buildSasKeyMessage(transactionId, olmSAS.get_pubkey()));
type: "m.key.verification.key",
content: {
transaction_id: transactionId,
key: olmSAS.get_pubkey(),
},
});
// alice responds with a 'key' ... // alice responds with a 'key' ...
requestBody = await expectSendToDeviceMessage("m.key.verification.key"); requestBody = await expectSendToDeviceMessage("m.key.verification.key");
@@ -239,32 +235,15 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
expect(toDeviceMessage.transaction_id).toEqual(transactionId); expect(toDeviceMessage.transaction_id).toEqual(transactionId);
// the dummy device also confirms that the emoji match, and sends a mac // 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(
returnToDeviceMessageFromSync({ buildSasMacMessage(transactionId, olmSAS, TEST_USER_ID, aliceClient.deviceId!),
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}`,
),
},
},
});
// that should satisfy Alice, who should reply with a 'done' // that should satisfy Alice, who should reply with a 'done'
await expectSendToDeviceMessage("m.key.verification.done"); await expectSendToDeviceMessage("m.key.verification.done");
// the dummy device also confirms done-ness // the dummy device also confirms done-ness
returnToDeviceMessageFromSync({ returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
type: "m.key.verification.done",
content: {
transaction_id: transactionId,
},
});
// ... and the whole thing should be done! // ... and the whole thing should be done!
await verificationPromise; await verificationPromise;
@@ -278,6 +257,102 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
olmSAS.free(); 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<ShowSasCallbacks>((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 () => { it("Can make a verification request to *all* devices", async () => {
aliceClient = await startTestClient(); aliceClient = await startTestClient();
// we need an existing cross-signing key for this // we need an existing cross-signing key for this
@@ -584,6 +659,11 @@ function calculateMAC(olmSAS: Olm.SAS, input: string, info: string): string {
return mac; 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 { function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, ""); return Buffer.from(uint8Array).toString("base64").replace(/=+$/g, "");
} }
@@ -616,3 +696,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,
},
};
}

View File

@@ -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<RustSdkCryptoJs.VerificationRequest>;
let mockedOutgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
let request: RustVerificationRequest;
beforeEach(() => {
mockedInner = {
registerChangesCallback: jest.fn(),
startSas: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.VerificationRequest>;
mockedOutgoingRequestProcessor = {} as Mocked<OutgoingRequestProcessor>;
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",
);
});
});
});

View File

@@ -128,9 +128,20 @@ export interface VerificationRequest
* @param targetDevice - details of where to send the request to. * @param targetDevice - details of where to send the request to.
* *
* @returns The verifier which will do the actual verification. * @returns The verifier which will do the actual verification.
*
* @deprecated Use {@link VerificationRequest#startVerification} instead.
*/ */
beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier; 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<Verifier>;
/** /**
* The verifier which is doing the actual verification, once the method has been established. * The verifier which is doing the actual verification, once the method has been established.
* Only defined when the `phase` is Started. * Only defined when the `phase` is Started.

View File

@@ -30,6 +30,7 @@ import {
VerificationRequest as IVerificationRequest, VerificationRequest as IVerificationRequest,
VerificationRequestEvent, VerificationRequestEvent,
VerificationRequestEventHandlerMap, VerificationRequestEventHandlerMap,
Verifier,
} from "../../../crypto-api/verification"; } from "../../../crypto-api/verification";
// backwards-compatibility exports // backwards-compatibility exports
@@ -458,6 +459,13 @@ export class VerificationRequest<C extends IVerificationChannel = IVerificationC
return this._verifier!; return this._verifier!;
} }
public async startVerification(method: string): Promise<Verifier> {
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. * sends the initial .request event.
* @returns resolves when the event has been sent. * @returns resolves when the event has been sent.

View File

@@ -236,6 +236,35 @@ export class RustVerificationRequest
throw new Error("not implemented"); 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<Verifier> {
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. * The verifier which is doing the actual verification, once the method has been established.
* Only defined when the `phase` is Started. * Only defined when the `phase` is Started.