1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Improve integration test for interactive verification (#3495)

* Tweaks to the integ test to conform to the spec

Rust is a bit more insistent than legacy crypto...

* Improve documentation on request*Verification

* Check more things in the integration test

* Create an E2EKeyResponder

* Test verification with custom method list

* Add a test for SAS cancellation

* Update spec/integ/crypto/verification.spec.ts
This commit is contained in:
Richard van der Hoff
2023-06-23 15:38:38 +01:00
committed by GitHub
parent 3c59476cf7
commit f884c78579
4 changed files with 377 additions and 180 deletions

View File

@ -40,6 +40,7 @@ import {
TEST_USER_ID,
} from "../../test-utils/test-data";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
// to ensure that we don't end up with dangling timeouts.
@ -79,7 +80,18 @@ afterAll(() => {
* These tests work by intercepting HTTP requests via fetch-mock rather than mocking out bits of the client, so as
* to provide the most effective integration tests possible.
*/
// 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;
@ -90,6 +102,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
/** an object which intercepts `/sync` requests from {@link #aliceClient} */
let syncResponder: SyncResponder;
/** an object which intercepts `/keys/query` requests from {@link #aliceClient} */
let e2eKeyResponder: E2EKeyResponder;
beforeEach(async () => {
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
@ -101,9 +116,15 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
userId: TEST_USER_ID,
accessToken: "akjgkrgjs",
deviceId: "device_under_test",
verificationMethods: methods,
});
await initCrypto(aliceClient);
e2eKeyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
mockInitialApiRequests(aliceClient.getHomeserverUrl());
await aliceClient.startClient();
});
afterEach(async () => {
@ -111,156 +132,157 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
fetchMock.mockReset();
});
beforeEach(() => {
syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
mockInitialApiRequests(aliceClient.getHomeserverUrl());
aliceClient.startClient();
});
describe("Outgoing verification requests for another device", () => {
beforeEach(async () => {
// pretend that we have another device, which we will verify
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
});
oldBackendOnly("Outgoing verification: can verify another device via SAS", async () => {
// expect requests to download our own keys
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
device_keys: {
[TEST_USER_ID]: {
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
oldBackendOnly("can verify via SAS", async () => {
// have alice initiate a verification. She should send a m.key.verification.request
let [requestBody, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId;
expect(transactionId).toBeDefined();
expect(request.phase).toEqual(VerificationPhase.Requested);
expect(request.roomId).toBeUndefined();
expect(request.isSelfVerification).toBe(true);
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(false); // no reply yet
expect(request.chosenMethod).toBe(null); // nothing chosen yet
expect(request.initiatedByMe).toBe(true);
expect(request.otherUserId).toEqual(TEST_USER_ID);
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
if (methods !== undefined) {
// eslint-disable-next-line jest/no-conditional-expect
expect(new Set(toDeviceMessage.methods)).toEqual(new Set(methods));
}
// 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,
},
},
});
});
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
expect(request.otherDeviceId).toEqual(TEST_DEVICE_ID);
// have alice initiate a verification. She should send a m.key.verification.request
let [requestBody, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId;
expect(transactionId).toBeDefined();
expect(request.phase).toEqual(VerificationPhase.Requested);
expect(request.roomId).toBeUndefined();
let toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.methods).toContain("m.sas.v1");
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(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,
},
});
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"],
message_authentication_codes: ["hkdf-hmac-sha256.v2"],
short_authentication_string: ["emoji"],
},
});
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Started);
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();
// start off the verification process: alice will send an `accept`
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
requestBody = await expectSendToDeviceMessage("m.key.verification.accept");
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519");
expect(toDeviceMessage.short_authentication_string).toEqual(["emoji"]);
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
// 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(),
},
});
// alice responds with a 'key' ...
requestBody = await expectSendToDeviceMessage("m.key.verification.key");
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 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
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}`,
),
// ... 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"],
},
},
});
});
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Started);
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
expect(request.chosenMethod).toEqual("m.sas.v1");
// that should satisfy Alice, who should reply with a 'done'
await expectSendToDeviceMessage("m.key.verification.done");
// there should now be a verifier
const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined();
expect(verifier.getShowSasCallbacks()).toBeNull();
// ... and the whole thing should be done!
await verificationPromise;
expect(request.phase).toEqual(VerificationPhase.Done);
// start off the verification process: alice will send an `accept`
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
// we're done with the temporary keypair
olmSAS.free();
});
requestBody = await expectSendToDeviceMessage("m.key.verification.accept");
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519-hkdf-sha256");
expect(toDeviceMessage.short_authentication_string).toEqual(["decimal", "emoji"]);
const macMethod = toDeviceMessage.message_authentication_code;
expect(macMethod).toEqual("hkdf-hmac-sha256.v2");
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
oldBackendOnly(
"Outgoing verification: can verify another device via QR code with an untrusted cross-signing key",
async () => {
// expect requests to download our own keys
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
device_keys: {
[TEST_USER_ID]: {
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
// 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(),
},
});
// alice responds with a 'key' ...
requestBody = await expectSendToDeviceMessage("m.key.verification.key");
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 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
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}`,
),
},
},
...SIGNED_CROSS_SIGNING_KEYS_DATA,
});
// 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,
},
});
// ... and the whole thing should be done!
await verificationPromise;
expect(request.phase).toEqual(VerificationPhase.Done);
// we're done with the temporary keypair
olmSAS.free();
});
oldBackendOnly("can verify another via QR code with an untrusted cross-signing key", async () => {
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
//
// Completing the initial sync will make the device list download outdated device lists (of which our own
@ -279,8 +301,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.methods).toContain("m.qr_code.show.v1");
expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1");
expect(toDeviceMessage.methods).toContain("m.reciprocate.v1");
if (methods === undefined) {
expect(toDeviceMessage.methods).toContain("m.qr_code.scan.v1");
}
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(transactionId);
@ -351,57 +375,115 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
// ... and the whole thing should be done!
await verificationPromise;
expect(request.phase).toEqual(VerificationPhase.Done);
},
);
});
oldBackendOnly("Incoming verification: can accept", async () => {
// expect requests to download our own keys
fetchMock.post(new RegExp("/_matrix/client/(r0|v3)/keys/query"), {
device_keys: {
[TEST_USER_ID]: {
[TEST_DEVICE_ID]: SIGNED_TEST_DEVICE_DATA,
oldBackendOnly("can cancel during the SAS phase", async () => {
// have alice initiate a verification. She should send 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({
type: "m.key.verification.ready",
content: {
from_device: TEST_DEVICE_ID,
methods: ["m.sas.v1"],
transaction_id: transactionId,
},
},
});
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"],
},
});
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Started);
// there should now be a verifier...
const verifier: Verifier = request.verifier!;
expect(verifier).toBeDefined();
expect(verifier.hasBeenCancelled).toBe(false);
// start off the verification process: alice will send an `accept`
const verificationPromise = verifier.verify();
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
jest.advanceTimersByTime(10);
await expectSendToDeviceMessage("m.key.verification.accept");
// now we unceremoniously cancel
const requestPromise = expectSendToDeviceMessage("m.key.verification.cancel");
verifier.cancel(new Error("blah"));
await requestPromise;
// ... which should cancel the verifier
await expect(verificationPromise).rejects.toThrow();
expect(request.phase).toEqual(VerificationPhase.Cancelled);
expect(verifier.hasBeenCancelled).toBe(true);
});
});
describe("Incoming verification from another device", () => {
beforeEach(() => {
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
});
const TRANSACTION_ID = "abcd";
oldBackendOnly("Incoming verification: can accept", async () => {
const TRANSACTION_ID = "abcd";
// Initiate the request by sending a to-device message
returnToDeviceMessageFromSync({
type: "m.key.verification.request",
content: {
from_device: TEST_DEVICE_ID,
methods: ["m.sas.v1"],
transaction_id: TRANSACTION_ID,
timestamp: Date.now() - 1000,
},
// Initiate the request by sending a to-device message
returnToDeviceMessageFromSync({
type: "m.key.verification.request",
content: {
from_device: TEST_DEVICE_ID,
methods: ["m.sas.v1"],
transaction_id: TRANSACTION_ID,
timestamp: Date.now() - 1000,
},
});
const request: VerificationRequest = await emitPromise(aliceClient, CryptoEvent.VerificationRequest);
expect(request.transactionId).toEqual(TRANSACTION_ID);
expect(request.phase).toEqual(VerificationPhase.Requested);
expect(request.roomId).toBeUndefined();
expect(request.initiatedByMe).toBe(false);
expect(request.otherUserId).toEqual(TEST_USER_ID);
expect(request.chosenMethod).toBe(null); // nothing chosen yet
expect(canAcceptVerificationRequest(request)).toBe(true);
// Alice accepts, by sending a to-device message
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.ready");
const acceptPromise = request.accept();
expect(canAcceptVerificationRequest(request)).toBe(false);
expect(request.phase).toEqual(VerificationPhase.Requested);
await acceptPromise;
const requestBody = await sendToDevicePromise;
expect(request.phase).toEqual(VerificationPhase.Ready);
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.methods).toContain("m.sas.v1");
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(TRANSACTION_ID);
});
const request: VerificationRequest = await emitPromise(aliceClient, CryptoEvent.VerificationRequest);
expect(request.transactionId).toEqual(TRANSACTION_ID);
expect(request.phase).toEqual(VerificationPhase.Requested);
expect(request.roomId).toBeUndefined();
expect(canAcceptVerificationRequest(request)).toBe(true);
// Alice accepts, by sending a to-device message
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.ready");
const acceptPromise = request.accept();
expect(canAcceptVerificationRequest(request)).toBe(false);
expect(request.phase).toEqual(VerificationPhase.Requested);
await acceptPromise;
const requestBody = await sendToDevicePromise;
expect(request.phase).toEqual(VerificationPhase.Ready);
const toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
expect(toDeviceMessage.methods).toContain("m.sas.v1");
expect(toDeviceMessage.from_device).toEqual(aliceClient.deviceId);
expect(toDeviceMessage.transaction_id).toEqual(TRANSACTION_ID);
});
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
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.

View File

@ -0,0 +1,99 @@
/*
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 fetchMock from "fetch-mock-jest";
import { MapWithDefault } from "../../src/utils";
import { IDownloadKeyResult } from "../../src";
import { IDeviceKeys } from "../../src/@types/crypto";
/**
* An object which intercepts `/keys/query` fetches via fetch-mock.
*/
export class E2EKeyResponder {
private deviceKeysByUserByDevice = new MapWithDefault<string, Map<string, any>>(() => new Map());
private masterKeysByUser: Record<string, any> = {};
private selfSigningKeysByUser: Record<string, any> = {};
private userSigningKeysByUser: Record<string, any> = {};
/**
* Construct a new E2EKeyResponder.
*
* It will immediately register an intercept of `/keys/query` requests for the given homeserverUrl.
* Only /query requests made to this server will be intercepted: this allows a single test to use more than one
* client and have the keys collected separately.
*
* @param homeserverUrl - the Homeserver Url of the client under test.
*/
public constructor(homeserverUrl: string) {
// set up a listener for /keys/query.
const listener = (url: string, options: RequestInit) => this.onKeyQueryRequest(options);
// catch both r0 and v3 variants
fetchMock.post(new URL("/_matrix/client/r0/keys/query", homeserverUrl).toString(), listener);
fetchMock.post(new URL("/_matrix/client/v3/keys/query", homeserverUrl).toString(), listener);
}
private onKeyQueryRequest(options: RequestInit) {
const content = JSON.parse(options.body as string);
const usersToReturn = Object.keys(content["device_keys"]);
const response = {
device_keys: {} as { [userId: string]: any },
master_keys: {} as { [userId: string]: any },
self_signing_keys: {} as { [userId: string]: any },
user_signing_keys: {} as { [userId: string]: any },
failures: {} as { [serverName: string]: any },
};
for (const user of usersToReturn) {
const userKeys = this.deviceKeysByUserByDevice.get(user);
if (userKeys !== undefined) {
response.device_keys[user] = Object.fromEntries(userKeys.entries());
}
if (this.masterKeysByUser.hasOwnProperty(user)) {
response.master_keys[user] = this.masterKeysByUser[user];
}
if (this.selfSigningKeysByUser.hasOwnProperty(user)) {
response.self_signing_keys[user] = this.selfSigningKeysByUser[user];
}
if (this.userSigningKeysByUser.hasOwnProperty(user)) {
response.user_signing_keys[user] = this.userSigningKeysByUser[user];
}
}
return response;
}
/**
* Add a set of device keys for return by a future `/keys/query`, as if they had been `/upload`ed
*
* @param userId - user the keys belong to
* @param deviceId - device the keys belong to
* @param keys - device keys for this device.
*/
public addDeviceKeys(userId: string, deviceId: string, keys: IDeviceKeys) {
this.deviceKeysByUserByDevice.getOrCreate(userId).set(deviceId, keys);
}
/** Add a set of cross-signing keys for return by a future `/keys/query`, as if they had been `/keys/device_signing/upload`ed
*
* @param data cross-signing data
*/
public addCrossSigningData(
data: Pick<IDownloadKeyResult, "master_keys" | "self_signing_keys" | "user_signing_keys">,
) {
Object.assign(this.masterKeysByUser, data.master_keys);
Object.assign(this.selfSigningKeysByUser, data.self_signing_keys);
Object.assign(this.userSigningKeysByUser, data.user_signing_keys);
}
}

View File

@ -336,6 +336,11 @@ export interface ICreateClientOpts {
*/
pickleKey?: string;
/**
* Verification methods we should offer to the other side when performing an interactive verification.
* If unset, we will offer all known methods. Currently these are: showing a QR code, scanning a QR code, and SAS
* (aka "emojis").
*/
verificationMethods?: Array<VerificationMethod>;
/**

View File

@ -256,7 +256,12 @@ export interface CryptoApi {
/**
* Send a verification request to our other devices.
*
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
* This is normally used when the current device is new, and we want to ask another of our devices to cross-sign.
*
* If an all-devices verification is already in flight, returns it. Otherwise, initiates a new one.
*
* To control the methods offered, set {@link ICreateClientOpts.verificationMethods} when creating the
* MatrixClient.
*
* @returns a VerificationRequest when the request has been sent to the other party.
*/
@ -265,7 +270,13 @@ export interface CryptoApi {
/**
* Request an interactive verification with the given device.
*
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
* This is normally used on one of our own devices, when the current device is already cross-signed, and we want to
* validate another device.
*
* If a verification for this user/device is already in flight, returns it. Otherwise, initiates a new one.
*
* To control the methods offered, set {@link ICreateClientOpts.verificationMethods} when creating the
* MatrixClient.
*
* @param userId - ID of the owner of the device to verify
* @param deviceId - ID of the device to verify