You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-06 12:02:40 +03:00
Element-R: Basic implementation of SAS verification (#3490)
* Return uploaded keys from `/keys/query` * Basic implementation of SAS verification in Rust * Update the `verifier` *before* emitting `erificationRequestEvent.Change` * remove dead code
This commit is contained in:
committed by
GitHub
parent
f16a6bc654
commit
48c4127035
@@ -55,7 +55,7 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.10",
|
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.11",
|
||||||
"another-json": "^0.2.0",
|
"another-json": "^0.2.0",
|
||||||
"bs58": "^5.0.0",
|
"bs58": "^5.0.0",
|
||||||
"content-type": "^1.0.4",
|
"content-type": "^1.0.4",
|
||||||
|
@@ -16,6 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import fetchMock from "fetch-mock-jest";
|
import fetchMock from "fetch-mock-jest";
|
||||||
import { MockResponse } from "fetch-mock";
|
import { MockResponse } from "fetch-mock";
|
||||||
|
import "fake-indexeddb/auto";
|
||||||
|
|
||||||
import { createClient, CryptoEvent, MatrixClient } from "../../../src";
|
import { createClient, CryptoEvent, MatrixClient } from "../../../src";
|
||||||
import {
|
import {
|
||||||
@@ -41,6 +42,7 @@ import {
|
|||||||
} from "../../test-utils/test-data";
|
} from "../../test-utils/test-data";
|
||||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||||
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
|
||||||
|
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
|
||||||
|
|
||||||
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
|
// 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.
|
// to ensure that we don't end up with dangling timeouts.
|
||||||
@@ -48,7 +50,7 @@ jest.useFakeTimers();
|
|||||||
|
|
||||||
let previousCrypto: Crypto | undefined;
|
let previousCrypto: Crypto | undefined;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(async () => {
|
||||||
// Stub out global.crypto
|
// Stub out global.crypto
|
||||||
previousCrypto = global["crypto"];
|
previousCrypto = global["crypto"];
|
||||||
|
|
||||||
@@ -60,6 +62,9 @@ beforeAll(() => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// we use the libolm primitives in the test, so init the Olm library
|
||||||
|
await global.Olm.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
// restore the original global.crypto
|
// restore the original global.crypto
|
||||||
@@ -105,6 +110,9 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
|||||||
/** an object which intercepts `/keys/query` requests from {@link #aliceClient} */
|
/** an object which intercepts `/keys/query` requests from {@link #aliceClient} */
|
||||||
let e2eKeyResponder: E2EKeyResponder;
|
let e2eKeyResponder: E2EKeyResponder;
|
||||||
|
|
||||||
|
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} */
|
||||||
|
let e2eKeyReceiver: E2EKeyReceiver;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// anything that we don't have a specific matcher for silently returns a 404
|
// anything that we don't have a specific matcher for silently returns a 404
|
||||||
fetchMock.catch(404);
|
fetchMock.catch(404);
|
||||||
@@ -121,7 +129,10 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
|||||||
|
|
||||||
await initCrypto(aliceClient);
|
await initCrypto(aliceClient);
|
||||||
|
|
||||||
|
e2eKeyReceiver = new E2EKeyReceiver(aliceClient.getHomeserverUrl());
|
||||||
e2eKeyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
e2eKeyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||||
|
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
|
||||||
|
|
||||||
syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
|
syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
|
||||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||||
await aliceClient.startClient();
|
await aliceClient.startClient();
|
||||||
@@ -129,6 +140,10 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await aliceClient.stopClient();
|
await aliceClient.stopClient();
|
||||||
|
|
||||||
|
// Allow in-flight things to complete before we tear down the test
|
||||||
|
await jest.runAllTimersAsync();
|
||||||
|
|
||||||
fetchMock.mockReset();
|
fetchMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,7 +153,9 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
|||||||
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
e2eKeyResponder.addDeviceKeys(TEST_USER_ID, TEST_DEVICE_ID, SIGNED_TEST_DEVICE_DATA);
|
||||||
});
|
});
|
||||||
|
|
||||||
oldBackendOnly("can verify via SAS", async () => {
|
it("can verify another device via SAS", async () => {
|
||||||
|
await waitForDeviceList();
|
||||||
|
|
||||||
// have alice initiate a verification. She should send a m.key.verification.request
|
// have alice initiate a verification. She should send a m.key.verification.request
|
||||||
let [requestBody, request] = await Promise.all([
|
let [requestBody, request] = await Promise.all([
|
||||||
expectSendToDeviceMessage("m.key.verification.request"),
|
expectSendToDeviceMessage("m.key.verification.request"),
|
||||||
@@ -189,22 +206,29 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
|||||||
short_authentication_string: ["decimal", "emoji"],
|
short_authentication_string: ["decimal", "emoji"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await waitForVerificationRequestChanged(request);
|
// as soon as the Changed event arrives, `verifier` should be defined
|
||||||
|
const verifier = await new Promise<Verifier>((resolve) => {
|
||||||
|
function onChange() {
|
||||||
expect(request.phase).toEqual(VerificationPhase.Started);
|
expect(request.phase).toEqual(VerificationPhase.Started);
|
||||||
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
expect(request.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||||
expect(request.chosenMethod).toEqual("m.sas.v1");
|
expect(request.chosenMethod).toEqual("m.sas.v1");
|
||||||
|
|
||||||
// there should now be a verifier
|
|
||||||
const verifier: Verifier = request.verifier!;
|
const verifier: Verifier = request.verifier!;
|
||||||
expect(verifier).toBeDefined();
|
expect(verifier).toBeDefined();
|
||||||
expect(verifier.getShowSasCallbacks()).toBeNull();
|
expect(verifier.getShowSasCallbacks()).toBeNull();
|
||||||
|
|
||||||
|
resolve(verifier);
|
||||||
|
}
|
||||||
|
request.once(VerificationRequestEvent.Change, onChange);
|
||||||
|
});
|
||||||
|
|
||||||
// start off the verification process: alice will send an `accept`
|
// start off the verification process: alice will send an `accept`
|
||||||
|
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||||
const verificationPromise = verifier.verify();
|
const verificationPromise = verifier.verify();
|
||||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||||
jest.advanceTimersByTime(10);
|
jest.advanceTimersByTime(10);
|
||||||
|
|
||||||
requestBody = await expectSendToDeviceMessage("m.key.verification.accept");
|
requestBody = await sendToDevicePromise;
|
||||||
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
toDeviceMessage = requestBody.messages[TEST_USER_ID][TEST_DEVICE_ID];
|
||||||
expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519-hkdf-sha256");
|
expect(toDeviceMessage.key_agreement_protocol).toEqual("curve25519-hkdf-sha256");
|
||||||
expect(toDeviceMessage.short_authentication_string).toEqual(["decimal", "emoji"]);
|
expect(toDeviceMessage.short_authentication_string).toEqual(["decimal", "emoji"]);
|
||||||
@@ -281,15 +305,9 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
|||||||
});
|
});
|
||||||
|
|
||||||
oldBackendOnly("can verify another via QR code with an untrusted cross-signing key", async () => {
|
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.
|
// 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);
|
||||||
// Completing the initial sync will make the device list download outdated device lists (of which our own
|
await waitForDeviceList();
|
||||||
// user will be one).
|
|
||||||
syncResponder.sendOrQueueSyncResponse({});
|
|
||||||
// DeviceList has a sleep(5) which we need to make happen
|
|
||||||
await jest.advanceTimersByTimeAsync(10);
|
|
||||||
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
|
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
|
||||||
|
|
||||||
// have alice initiate a verification. She should send a m.key.verification.request
|
// have alice initiate a verification. She should send a m.key.verification.request
|
||||||
@@ -377,7 +395,9 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
|||||||
expect(request.phase).toEqual(VerificationPhase.Done);
|
expect(request.phase).toEqual(VerificationPhase.Done);
|
||||||
});
|
});
|
||||||
|
|
||||||
oldBackendOnly("can cancel during the SAS phase", async () => {
|
it("can cancel during the SAS phase", async () => {
|
||||||
|
await waitForDeviceList();
|
||||||
|
|
||||||
// have alice initiate a verification. She should send a m.key.verification.request
|
// have alice initiate a verification. She should send a m.key.verification.request
|
||||||
const [, request] = await Promise.all([
|
const [, request] = await Promise.all([
|
||||||
expectSendToDeviceMessage("m.key.verification.request"),
|
expectSendToDeviceMessage("m.key.verification.request"),
|
||||||
@@ -419,12 +439,13 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
|||||||
expect(verifier.hasBeenCancelled).toBe(false);
|
expect(verifier.hasBeenCancelled).toBe(false);
|
||||||
|
|
||||||
// start off the verification process: alice will send an `accept`
|
// start off the verification process: alice will send an `accept`
|
||||||
|
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.accept");
|
||||||
const verificationPromise = verifier.verify();
|
const verificationPromise = verifier.verify();
|
||||||
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
|
||||||
jest.advanceTimersByTime(10);
|
jest.advanceTimersByTime(10);
|
||||||
await expectSendToDeviceMessage("m.key.verification.accept");
|
await sendToDevicePromise;
|
||||||
|
|
||||||
// now we unceremoniously cancel
|
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
|
||||||
const requestPromise = expectSendToDeviceMessage("m.key.verification.cancel");
|
const requestPromise = expectSendToDeviceMessage("m.key.verification.cancel");
|
||||||
verifier.cancel(new Error("blah"));
|
verifier.cancel(new Error("blah"));
|
||||||
await requestPromise;
|
await requestPromise;
|
||||||
@@ -479,6 +500,19 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** make sure that the client knows about the dummy device */
|
||||||
|
async function waitForDeviceList(): Promise<void> {
|
||||||
|
// Completing the initial sync will make the device list download outdated device lists (of which our own
|
||||||
|
// user will be one).
|
||||||
|
syncResponder.sendOrQueueSyncResponse({});
|
||||||
|
// DeviceList has a sleep(5) which we need to make happen
|
||||||
|
await jest.advanceTimersByTimeAsync(10);
|
||||||
|
|
||||||
|
// The client should now know about the dummy device
|
||||||
|
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
|
||||||
|
expect(devices.get(TEST_USER_ID)!.keys()).toContain(TEST_DEVICE_ID);
|
||||||
|
}
|
||||||
|
|
||||||
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
|
function returnToDeviceMessageFromSync(ev: { type: string; content: object; sender?: string }): void {
|
||||||
ev.sender ??= TEST_USER_ID;
|
ev.sender ??= TEST_USER_ID;
|
||||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
||||||
|
@@ -145,6 +145,13 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
|||||||
return this.deviceKeys.keys[keyIds[0]];
|
return this.deviceKeys.keys[keyIds[0]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the device keys have already been uploaded, return them. Else return null.
|
||||||
|
*/
|
||||||
|
public getUploadedDeviceKeys(): IDeviceKeys | null {
|
||||||
|
return this.deviceKeys;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||||
* set up an expectation that the keys will be uploaded, and wait for
|
* set up an expectation that the keys will be uploaded, and wait for
|
||||||
|
@@ -19,12 +19,14 @@ import fetchMock from "fetch-mock-jest";
|
|||||||
import { MapWithDefault } from "../../src/utils";
|
import { MapWithDefault } from "../../src/utils";
|
||||||
import { IDownloadKeyResult } from "../../src";
|
import { IDownloadKeyResult } from "../../src";
|
||||||
import { IDeviceKeys } from "../../src/@types/crypto";
|
import { IDeviceKeys } from "../../src/@types/crypto";
|
||||||
|
import { E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object which intercepts `/keys/query` fetches via fetch-mock.
|
* An object which intercepts `/keys/query` fetches via fetch-mock.
|
||||||
*/
|
*/
|
||||||
export class E2EKeyResponder {
|
export class E2EKeyResponder {
|
||||||
private deviceKeysByUserByDevice = new MapWithDefault<string, Map<string, any>>(() => new Map());
|
private deviceKeysByUserByDevice = new MapWithDefault<string, Map<string, any>>(() => new Map());
|
||||||
|
private e2eKeyReceiversByUser = new Map<string, E2EKeyReceiver>();
|
||||||
private masterKeysByUser: Record<string, any> = {};
|
private masterKeysByUser: Record<string, any> = {};
|
||||||
private selfSigningKeysByUser: Record<string, any> = {};
|
private selfSigningKeysByUser: Record<string, any> = {};
|
||||||
private userSigningKeysByUser: Record<string, any> = {};
|
private userSigningKeysByUser: Record<string, any> = {};
|
||||||
@@ -61,6 +63,16 @@ export class E2EKeyResponder {
|
|||||||
if (userKeys !== undefined) {
|
if (userKeys !== undefined) {
|
||||||
response.device_keys[user] = Object.fromEntries(userKeys.entries());
|
response.device_keys[user] = Object.fromEntries(userKeys.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const e2eKeyReceiver = this.e2eKeyReceiversByUser.get(user);
|
||||||
|
if (e2eKeyReceiver !== undefined) {
|
||||||
|
const deviceKeys = e2eKeyReceiver.getUploadedDeviceKeys();
|
||||||
|
if (deviceKeys !== null) {
|
||||||
|
response.device_keys[user] ??= {};
|
||||||
|
response.device_keys[user][deviceKeys.device_id] = deviceKeys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.masterKeysByUser.hasOwnProperty(user)) {
|
if (this.masterKeysByUser.hasOwnProperty(user)) {
|
||||||
response.master_keys[user] = this.masterKeysByUser[user];
|
response.master_keys[user] = this.masterKeysByUser[user];
|
||||||
}
|
}
|
||||||
@@ -96,4 +108,16 @@ export class E2EKeyResponder {
|
|||||||
Object.assign(this.selfSigningKeysByUser, data.self_signing_keys);
|
Object.assign(this.selfSigningKeysByUser, data.self_signing_keys);
|
||||||
Object.assign(this.userSigningKeysByUser, data.user_signing_keys);
|
Object.assign(this.userSigningKeysByUser, data.user_signing_keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an E2EKeyReceiver to poll for uploaded keys
|
||||||
|
*
|
||||||
|
* Any keys which have been uploaded to the given `E2EKeyReceiver` at the time of the `/keys/query` request will
|
||||||
|
* be added to the response.
|
||||||
|
*
|
||||||
|
* @param e2eKeyReceiver
|
||||||
|
*/
|
||||||
|
public addKeyReceiver(userId: string, e2eKeyReceiver: E2EKeyReceiver) {
|
||||||
|
this.e2eKeyReceiversByUser.set(userId, e2eKeyReceiver);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -494,6 +494,15 @@ describe("RustCrypto", () => {
|
|||||||
expect(deviceMap.has(testData.TEST_DEVICE_ID)).toBe(true);
|
expect(deviceMap.has(testData.TEST_DEVICE_ID)).toBe(true);
|
||||||
rustCrypto.stop();
|
rustCrypto.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("requestDeviceVerification", () => {
|
||||||
|
it("throws an error if the device is unknown", async () => {
|
||||||
|
const rustCrypto = await makeTestRustCrypto();
|
||||||
|
await expect(() => rustCrypto.requestDeviceVerification(TEST_USER, "unknown")).rejects.toThrow(
|
||||||
|
"Not a known device",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/** build a basic RustCrypto instance for testing
|
/** build a basic RustCrypto instance for testing
|
||||||
|
@@ -2238,6 +2238,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
|||||||
this.secretStorage,
|
this.secretStorage,
|
||||||
this.cryptoCallbacks,
|
this.cryptoCallbacks,
|
||||||
);
|
);
|
||||||
|
rustCrypto.supportedVerificationMethods = this.verificationMethods;
|
||||||
|
|
||||||
this.cryptoBackend = rustCrypto;
|
this.cryptoBackend = rustCrypto;
|
||||||
|
|
||||||
// attach the event listeners needed by RustCrypto
|
// attach the event listeners needed by RustCrypto
|
||||||
|
@@ -51,6 +51,7 @@ import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
|
|||||||
import { keyFromPassphrase } from "../crypto/key_passphrase";
|
import { keyFromPassphrase } from "../crypto/key_passphrase";
|
||||||
import { encodeRecoveryKey } from "../crypto/recoverykey";
|
import { encodeRecoveryKey } from "../crypto/recoverykey";
|
||||||
import { crypto } from "../crypto/crypto";
|
import { crypto } from "../crypto/crypto";
|
||||||
|
import { RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||||
@@ -562,6 +563,13 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The verification methods we offer to the other side during an interactive verification.
|
||||||
|
*
|
||||||
|
* If `undefined`, we will offer all the methods supported by the Rust SDK.
|
||||||
|
*/
|
||||||
|
public supportedVerificationMethods: string[] | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a verification request to our other devices.
|
* Send a verification request to our other devices.
|
||||||
*
|
*
|
||||||
@@ -571,7 +579,7 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
*
|
*
|
||||||
* @returns a VerificationRequest when the request has been sent to the other party.
|
* @returns a VerificationRequest when the request has been sent to the other party.
|
||||||
*/
|
*/
|
||||||
public requestOwnUserVerification(): Promise<VerificationRequest> {
|
public async requestOwnUserVerification(): Promise<VerificationRequest> {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,8 +595,22 @@ export class RustCrypto implements CryptoBackend {
|
|||||||
*
|
*
|
||||||
* @returns a VerificationRequest when the request has been sent to the other party.
|
* @returns a VerificationRequest when the request has been sent to the other party.
|
||||||
*/
|
*/
|
||||||
public requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest> {
|
public async requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest> {
|
||||||
throw new Error("not implemented");
|
const device: RustSdkCryptoJs.Device | undefined = await this.olmMachine.getDevice(
|
||||||
|
new RustSdkCryptoJs.UserId(userId),
|
||||||
|
new RustSdkCryptoJs.DeviceId(deviceId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
throw new Error("Not a known device");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [request, outgoingRequest]: [RustSdkCryptoJs.VerificationRequest, RustSdkCryptoJs.ToDeviceRequest] =
|
||||||
|
await device.requestVerification(
|
||||||
|
this.supportedVerificationMethods?.map(verificationMethodIdentifierToMethod),
|
||||||
|
);
|
||||||
|
await this.outgoingRequestProcessor.makeOutgoingRequest(outgoingRequest);
|
||||||
|
return new RustVerificationRequest(request, this.outgoingRequestProcessor);
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
429
src/rust-crypto/verification.ts
Normal file
429
src/rust-crypto/verification.ts
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
/*
|
||||||
|
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 { Emoji } from "@matrix-org/matrix-sdk-crypto-js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ShowQrCodeCallbacks,
|
||||||
|
ShowSasCallbacks,
|
||||||
|
VerificationPhase,
|
||||||
|
VerificationRequest,
|
||||||
|
VerificationRequestEvent,
|
||||||
|
VerificationRequestEventHandlerMap,
|
||||||
|
Verifier,
|
||||||
|
VerifierEvent,
|
||||||
|
VerifierEventHandlerMap,
|
||||||
|
} from "../crypto-api/verification";
|
||||||
|
import { TypedEventEmitter } from "../models/typed-event-emitter";
|
||||||
|
import { OutgoingRequest, OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An incoming, or outgoing, request to verify a user or a device via cross-signing.
|
||||||
|
*/
|
||||||
|
export class RustVerificationRequest
|
||||||
|
extends TypedEventEmitter<VerificationRequestEvent, VerificationRequestEventHandlerMap>
|
||||||
|
implements VerificationRequest
|
||||||
|
{
|
||||||
|
private _verifier: Verifier | undefined;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly inner: RustSdkCryptoJs.VerificationRequest,
|
||||||
|
outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const onChange = async (): Promise<void> => {
|
||||||
|
// if we now have a `Verification` where we lacked one before, wrap it.
|
||||||
|
// TODO: QR support
|
||||||
|
if (this._verifier === undefined) {
|
||||||
|
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
|
||||||
|
if (verification instanceof RustSdkCryptoJs.Sas) {
|
||||||
|
this._verifier = new RustSASVerifier(verification, this, outgoingRequestProcessor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit(VerificationRequestEvent.Change);
|
||||||
|
};
|
||||||
|
inner.registerChangesCallback(onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique ID for this verification request.
|
||||||
|
*
|
||||||
|
* An ID isn't assigned until the first message is sent, so this may be `undefined` in the early phases.
|
||||||
|
*/
|
||||||
|
public get transactionId(): string | undefined {
|
||||||
|
return this.inner.flowId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For an in-room verification, the ID of the room.
|
||||||
|
*
|
||||||
|
* For to-device verifications, `undefined`.
|
||||||
|
*/
|
||||||
|
public get roomId(): string | undefined {
|
||||||
|
return this.inner.roomId?.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if this request was initiated by the local client.
|
||||||
|
*
|
||||||
|
* For in-room verifications, the initiator is who sent the `m.key.verification.request` event.
|
||||||
|
* For to-device verifications, the initiator is who sent the `m.key.verification.start` event.
|
||||||
|
*/
|
||||||
|
public get initiatedByMe(): boolean {
|
||||||
|
return this.inner.weStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The user id of the other party in this request */
|
||||||
|
public get otherUserId(): string {
|
||||||
|
return this.inner.otherUserId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For verifications via to-device messages: the ID of the other device. Otherwise, undefined. */
|
||||||
|
public get otherDeviceId(): string | undefined {
|
||||||
|
return this.inner.otherDeviceId?.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if the other party in this request is one of this user's own devices. */
|
||||||
|
public get isSelfVerification(): boolean {
|
||||||
|
return this.inner.isSelfVerification();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** current phase of the request. */
|
||||||
|
public get phase(): VerificationPhase {
|
||||||
|
const phase = this.inner.phase();
|
||||||
|
|
||||||
|
switch (phase) {
|
||||||
|
case RustSdkCryptoJs.VerificationRequestPhase.Created:
|
||||||
|
case RustSdkCryptoJs.VerificationRequestPhase.Requested:
|
||||||
|
return VerificationPhase.Requested;
|
||||||
|
case RustSdkCryptoJs.VerificationRequestPhase.Ready:
|
||||||
|
return VerificationPhase.Ready;
|
||||||
|
case RustSdkCryptoJs.VerificationRequestPhase.Transitioned:
|
||||||
|
return VerificationPhase.Started;
|
||||||
|
case RustSdkCryptoJs.VerificationRequestPhase.Done:
|
||||||
|
return VerificationPhase.Done;
|
||||||
|
case RustSdkCryptoJs.VerificationRequestPhase.Cancelled:
|
||||||
|
return VerificationPhase.Cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown verification phase ${phase}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if the request has sent its initial event and needs more events to complete
|
||||||
|
* (ie it is in phase `Requested`, `Ready` or `Started`).
|
||||||
|
*/
|
||||||
|
public get pending(): boolean {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if we have started the process of sending an `m.key.verification.ready` (but have not necessarily received
|
||||||
|
* the remote echo which causes a transition to {@link VerificationPhase.Ready}.
|
||||||
|
*/
|
||||||
|
public get accepting(): boolean {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if we have started the process of sending an `m.key.verification.cancel` (but have not necessarily received
|
||||||
|
* the remote echo which causes a transition to {@link VerificationPhase.Cancelled}).
|
||||||
|
*/
|
||||||
|
public get declining(): boolean {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The remaining number of ms before the request will be automatically cancelled.
|
||||||
|
*
|
||||||
|
* `null` indicates that there is no timeout
|
||||||
|
*/
|
||||||
|
public get timeout(): number | null {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** once the phase is Started (and !initiatedByMe) or Ready: common methods supported by both sides */
|
||||||
|
public get methods(): string[] {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** the method picked in the .start event */
|
||||||
|
public get chosenMethod(): string | null {
|
||||||
|
const verification: RustSdkCryptoJs.Qr | RustSdkCryptoJs.Sas | undefined = this.inner.getVerification();
|
||||||
|
// TODO: this isn't quite right. The existence of a Verification doesn't prove that we have .started.
|
||||||
|
if (verification instanceof RustSdkCryptoJs.Sas) {
|
||||||
|
return "m.sas.v1";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the other party supports a given verification method.
|
||||||
|
* This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
|
||||||
|
* if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
|
||||||
|
* For methods that need to be supported by both ends, use the `methods` property.
|
||||||
|
*
|
||||||
|
* @param method - the method to check
|
||||||
|
* @returns true if the other party said they supported the method
|
||||||
|
*/
|
||||||
|
public otherPartySupportsMethod(method: string): boolean {
|
||||||
|
const theirMethods: RustSdkCryptoJs.VerificationMethod[] | undefined = this.inner.theirSupportedMethods;
|
||||||
|
if (theirMethods === undefined) {
|
||||||
|
// no message from the other side yet
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredMethod = verificationMethodsByIdentifier[method];
|
||||||
|
return theirMethods.some((m) => m === requiredMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accepts the request, sending a .ready event to the other party
|
||||||
|
*
|
||||||
|
* @returns Promise which resolves when the event has been sent.
|
||||||
|
*/
|
||||||
|
public accept(): Promise<void> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the request, sending a cancellation to the other party
|
||||||
|
*
|
||||||
|
* @param params - Details for the cancellation, including `reason` (defaults to "User declined"), and `code`
|
||||||
|
* (defaults to `m.user`).
|
||||||
|
*
|
||||||
|
* @returns Promise which resolves when the event has been sent.
|
||||||
|
*/
|
||||||
|
public cancel(params?: { reason?: string; code?: string }): Promise<void> {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link Verifier} to do this verification via a particular method.
|
||||||
|
*
|
||||||
|
* If a verifier has already been created for this request, returns that verifier.
|
||||||
|
*
|
||||||
|
* This does *not* send the `m.key.verification.start` event - to do so, call {@link Verifier#verifier} on the
|
||||||
|
* returned verifier.
|
||||||
|
*
|
||||||
|
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
|
||||||
|
*
|
||||||
|
* @param method - the name of the verification method to use.
|
||||||
|
* @param targetDevice - details of where to send the request to.
|
||||||
|
*
|
||||||
|
* @returns The verifier which will do the actual verification.
|
||||||
|
*/
|
||||||
|
public beginKeyVerification(method: string, targetDevice?: { userId?: string; deviceId?: string }): Verifier {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The verifier which is doing the actual verification, once the method has been established.
|
||||||
|
* Only defined when the `phase` is Started.
|
||||||
|
*/
|
||||||
|
public get verifier(): Verifier | undefined {
|
||||||
|
return this._verifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data for a QR code allowing the other device to verify this one, if it supports it.
|
||||||
|
*
|
||||||
|
* Only set after a .ready if the other party can scan a QR code, otherwise undefined.
|
||||||
|
*/
|
||||||
|
public getQRCodeBytes(): Buffer | undefined {
|
||||||
|
// TODO
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this request has been cancelled, the cancellation code (e.g `m.user`) which is responsible for cancelling
|
||||||
|
* this verification.
|
||||||
|
*/
|
||||||
|
public get cancellationCode(): string | null {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of the user that cancelled the request.
|
||||||
|
*
|
||||||
|
* Only defined when phase is Cancelled
|
||||||
|
*/
|
||||||
|
public get cancellingUserId(): string | undefined {
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RustSASVerifier extends TypedEventEmitter<VerifierEvent, VerifierEventHandlerMap> implements Verifier {
|
||||||
|
/** A promise which completes when the verification completes (or rejects when it is cancelled/fails) */
|
||||||
|
private readonly completionPromise: Promise<void>;
|
||||||
|
|
||||||
|
private callbacks: ShowSasCallbacks | null = null;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly inner: RustSdkCryptoJs.Sas,
|
||||||
|
_verificationRequest: RustVerificationRequest,
|
||||||
|
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.completionPromise = new Promise<void>((resolve, reject) => {
|
||||||
|
const onChange = async (): Promise<void> => {
|
||||||
|
this.updateCallbacks();
|
||||||
|
|
||||||
|
if (this.inner.isDone()) {
|
||||||
|
resolve(undefined);
|
||||||
|
} else if (this.inner.isCancelled()) {
|
||||||
|
const cancelInfo = this.inner.cancelInfo()!;
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Verification cancelled by ${
|
||||||
|
cancelInfo.cancelledbyUs() ? "us" : "them"
|
||||||
|
} with code ${cancelInfo.cancelCode()}: ${cancelInfo.reason()}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
inner.registerChangesCallback(onChange);
|
||||||
|
});
|
||||||
|
// stop the runtime complaining if nobody catches a failure
|
||||||
|
this.completionPromise.catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** if we can now show the callbacks, do so */
|
||||||
|
private updateCallbacks(): void {
|
||||||
|
if (this.callbacks === null) {
|
||||||
|
const emoji: Array<Emoji> | undefined = this.inner.emoji();
|
||||||
|
const decimal = this.inner.decimals() as [number, number, number] | undefined;
|
||||||
|
|
||||||
|
if (emoji === undefined && decimal === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbacks = {
|
||||||
|
sas: {
|
||||||
|
decimal: decimal,
|
||||||
|
emoji: emoji?.map((e) => [e.symbol, e.description]),
|
||||||
|
},
|
||||||
|
confirm: async (): Promise<void> => {
|
||||||
|
const requests: Array<OutgoingRequest> = await this.inner.confirm();
|
||||||
|
for (const m of requests) {
|
||||||
|
await this.outgoingRequestProcessor.makeOutgoingRequest(m);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mismatch: (): void => {
|
||||||
|
throw new Error("impl");
|
||||||
|
},
|
||||||
|
cancel: (): void => {
|
||||||
|
throw new Error("impl");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.emit(VerifierEvent.ShowSas, this.callbacks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the verification has been cancelled, either by us or the other side.
|
||||||
|
*/
|
||||||
|
public get hasBeenCancelled(): boolean {
|
||||||
|
return this.inner.isCancelled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the other user in the verification process.
|
||||||
|
*/
|
||||||
|
public get userId(): string {
|
||||||
|
return this.inner.otherUserId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the key verification, if it has not already been started.
|
||||||
|
*
|
||||||
|
* This means sending a `m.key.verification.start` if we are the first responder, or a `m.key.verification.accept`
|
||||||
|
* if the other side has already sent a start event.
|
||||||
|
*
|
||||||
|
* @returns Promise which resolves when the verification has completed, or rejects if the verification is cancelled
|
||||||
|
* or times out.
|
||||||
|
*/
|
||||||
|
public async verify(): Promise<void> {
|
||||||
|
const req: undefined | OutgoingRequest = this.inner.accept();
|
||||||
|
if (req) {
|
||||||
|
await this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||||
|
}
|
||||||
|
await this.completionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel a verification.
|
||||||
|
*
|
||||||
|
* We will send an `m.key.verification.cancel` if the verification is still in flight. The verification promise
|
||||||
|
* will reject, and a {@link Crypto.VerifierEvent#Cancel} will be emitted.
|
||||||
|
*
|
||||||
|
* @param e - the reason for the cancellation.
|
||||||
|
*/
|
||||||
|
public cancel(e: Error): void {
|
||||||
|
// TODO: something with `e`
|
||||||
|
const req: undefined | OutgoingRequest = this.inner.cancel();
|
||||||
|
if (req) {
|
||||||
|
this.outgoingRequestProcessor.makeOutgoingRequest(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the details for an SAS verification, if one is in progress
|
||||||
|
*
|
||||||
|
* Returns `null`, unless this verifier is for a SAS-based verification and we are waiting for the user to confirm
|
||||||
|
* the SAS matches.
|
||||||
|
*/
|
||||||
|
public getShowSasCallbacks(): ShowSasCallbacks | null {
|
||||||
|
return this.callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the details for reciprocating QR code verification, if one is in progress
|
||||||
|
*
|
||||||
|
* Returns `null`, unless this verifier is for reciprocating a QR-code-based verification (ie, the other user has
|
||||||
|
* already scanned our QR code), and we are waiting for the user to confirm.
|
||||||
|
*/
|
||||||
|
public getReciprocateQrCodeCallbacks(): ShowQrCodeCallbacks | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For each specced verification method, the rust-side `VerificationMethod` corresponding to it */
|
||||||
|
const verificationMethodsByIdentifier: Record<string, RustSdkCryptoJs.VerificationMethod> = {
|
||||||
|
"m.sas.v1": RustSdkCryptoJs.VerificationMethod.SasV1,
|
||||||
|
"m.qr_code.scan.v1": RustSdkCryptoJs.VerificationMethod.QrCodeScanV1,
|
||||||
|
"m.qr_code.show.v1": RustSdkCryptoJs.VerificationMethod.QrCodeShowV1,
|
||||||
|
"m.reciprocate.v1": RustSdkCryptoJs.VerificationMethod.ReciprocateV1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a specced verification method identifier into a rust-side `VerificationMethod`.
|
||||||
|
*
|
||||||
|
* @param method - specced method identifier, for example `m.sas.v1`.
|
||||||
|
* @returns Rust-side `VerificationMethod` corresponding to `method`.
|
||||||
|
* @throws An error if the method is unknown.
|
||||||
|
*/
|
||||||
|
export function verificationMethodIdentifierToMethod(method: string): RustSdkCryptoJs.VerificationMethod {
|
||||||
|
const meth = verificationMethodsByIdentifier[method];
|
||||||
|
if (meth === undefined) {
|
||||||
|
throw new Error(`Unknown verification method ${method}`);
|
||||||
|
}
|
||||||
|
return meth;
|
||||||
|
}
|
@@ -1426,10 +1426,10 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
|
|
||||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.10":
|
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.11":
|
||||||
version "0.1.0-alpha.10"
|
version "0.1.0-alpha.11"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.10.tgz#b6a6395cffd3197ae2e0a88f4eeae8b315571fd2"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.11.tgz#24d705318c3159ef7dbe43bca464ac2bdd11e45d"
|
||||||
integrity sha512-8V2NKuzGOFzEZeZVgF2is7gmuopdRbMZ064tzPDE0vN34iX6s3O8A4oxIT7SA3qtymwm3t1yEvTnT+0gfbmh4g==
|
integrity sha512-HD3rskPkqrUUSaKzGLg97k/bN+OZrkcX7ODB/pNBs/jqq+/A0wDKqsszJotzFwsQcDPpWn78BmMyvBo4tLxKjw==
|
||||||
|
|
||||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||||
version "3.2.14"
|
version "3.2.14"
|
||||||
|
Reference in New Issue
Block a user