You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-07-31 15:24:23 +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": {
|
||||
"@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",
|
||||
"bs58": "^5.0.0",
|
||||
"content-type": "^1.0.4",
|
||||
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
import { MockResponse } from "fetch-mock";
|
||||
import "fake-indexeddb/auto";
|
||||
|
||||
import { createClient, CryptoEvent, MatrixClient } from "../../../src";
|
||||
import {
|
||||
@ -41,6 +42,7 @@ import {
|
||||
} from "../../test-utils/test-data";
|
||||
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
|
||||
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
|
||||
// to ensure that we don't end up with dangling timeouts.
|
||||
@ -48,7 +50,7 @@ jest.useFakeTimers();
|
||||
|
||||
let previousCrypto: Crypto | undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
// Stub out 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
|
||||
@ -105,6 +110,9 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
||||
/** an object which intercepts `/keys/query` requests from {@link #aliceClient} */
|
||||
let e2eKeyResponder: E2EKeyResponder;
|
||||
|
||||
/** an object which intercepts `/keys/upload` requests from {@link #aliceClient} */
|
||||
let e2eKeyReceiver: E2EKeyReceiver;
|
||||
|
||||
beforeEach(async () => {
|
||||
// anything that we don't have a specific matcher for silently returns a 404
|
||||
fetchMock.catch(404);
|
||||
@ -121,7 +129,10 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
||||
|
||||
await initCrypto(aliceClient);
|
||||
|
||||
e2eKeyReceiver = new E2EKeyReceiver(aliceClient.getHomeserverUrl());
|
||||
e2eKeyResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
|
||||
e2eKeyResponder.addKeyReceiver(TEST_USER_ID, e2eKeyReceiver);
|
||||
|
||||
syncResponder = new SyncResponder(aliceClient.getHomeserverUrl());
|
||||
mockInitialApiRequests(aliceClient.getHomeserverUrl());
|
||||
await aliceClient.startClient();
|
||||
@ -129,6 +140,10 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
||||
|
||||
afterEach(async () => {
|
||||
await aliceClient.stopClient();
|
||||
|
||||
// Allow in-flight things to complete before we tear down the test
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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
|
||||
let [requestBody, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
@ -189,22 +206,29 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
||||
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");
|
||||
// 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.otherPartySupportsMethod("m.sas.v1")).toBe(true);
|
||||
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 verifier: Verifier = request.verifier!;
|
||||
expect(verifier).toBeDefined();
|
||||
expect(verifier.getShowSasCallbacks()).toBeNull();
|
||||
|
||||
resolve(verifier);
|
||||
}
|
||||
request.once(VerificationRequestEvent.Change, onChange);
|
||||
});
|
||||
|
||||
// start off the verification process: alice will send an `accept`
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.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");
|
||||
requestBody = await sendToDevicePromise;
|
||||
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"]);
|
||||
@ -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 () => {
|
||||
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
|
||||
// user will be one).
|
||||
syncResponder.sendOrQueueSyncResponse({});
|
||||
// DeviceList has a sleep(5) which we need to make happen
|
||||
await jest.advanceTimersByTimeAsync(10);
|
||||
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
|
||||
await waitForDeviceList();
|
||||
expect(aliceClient.getStoredCrossSigningForUser(TEST_USER_ID)).toBeTruthy();
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
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
|
||||
const [, request] = await Promise.all([
|
||||
expectSendToDeviceMessage("m.key.verification.request"),
|
||||
@ -419,12 +439,13 @@ function runTests(backend: string, initCrypto: InitCrypto, methods: string[] | u
|
||||
expect(verifier.hasBeenCancelled).toBe(false);
|
||||
|
||||
// start off the verification process: alice will send an `accept`
|
||||
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.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");
|
||||
await sendToDevicePromise;
|
||||
|
||||
// now we unceremoniously cancel
|
||||
// now we unceremoniously cancel. We expect the verificatationPromise to reject.
|
||||
const requestPromise = expectSendToDeviceMessage("m.key.verification.cancel");
|
||||
verifier.cancel(new Error("blah"));
|
||||
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 {
|
||||
ev.sender ??= TEST_USER_ID;
|
||||
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
|
||||
|
@ -145,6 +145,13 @@ export class E2EKeyReceiver implements IE2EKeyReceiver {
|
||||
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,
|
||||
* 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 { IDownloadKeyResult } from "../../src";
|
||||
import { IDeviceKeys } from "../../src/@types/crypto";
|
||||
import { E2EKeyReceiver } from "./E2EKeyReceiver";
|
||||
|
||||
/**
|
||||
* An object which intercepts `/keys/query` fetches via fetch-mock.
|
||||
*/
|
||||
export class E2EKeyResponder {
|
||||
private deviceKeysByUserByDevice = new MapWithDefault<string, Map<string, any>>(() => new Map());
|
||||
private e2eKeyReceiversByUser = new Map<string, E2EKeyReceiver>();
|
||||
private masterKeysByUser: Record<string, any> = {};
|
||||
private selfSigningKeysByUser: Record<string, any> = {};
|
||||
private userSigningKeysByUser: Record<string, any> = {};
|
||||
@ -61,6 +63,16 @@ export class E2EKeyResponder {
|
||||
if (userKeys !== undefined) {
|
||||
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)) {
|
||||
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.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);
|
||||
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
|
||||
|
@ -2238,6 +2238,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
|
||||
this.secretStorage,
|
||||
this.cryptoCallbacks,
|
||||
);
|
||||
rustCrypto.supportedVerificationMethods = this.verificationMethods;
|
||||
|
||||
this.cryptoBackend = rustCrypto;
|
||||
|
||||
// attach the event listeners needed by RustCrypto
|
||||
|
@ -51,6 +51,7 @@ import { secretStorageContainsCrossSigningKeys } from "./secret-storage";
|
||||
import { keyFromPassphrase } from "../crypto/key_passphrase";
|
||||
import { encodeRecoveryKey } from "../crypto/recoverykey";
|
||||
import { crypto } from "../crypto/crypto";
|
||||
import { RustVerificationRequest, verificationMethodIdentifierToMethod } from "./verification";
|
||||
|
||||
/**
|
||||
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
|
||||
@ -562,6 +563,13 @@ export class RustCrypto implements CryptoBackend {
|
||||
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.
|
||||
*
|
||||
@ -571,7 +579,7 @@ export class RustCrypto implements CryptoBackend {
|
||||
*
|
||||
* @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");
|
||||
}
|
||||
|
||||
@ -580,15 +588,29 @@ export class RustCrypto implements CryptoBackend {
|
||||
*
|
||||
* If a verification is already in flight, returns it. Otherwise, initiates a new one.
|
||||
*
|
||||
* Implementation of {@link CryptoApi#requestDeviceVerification }.
|
||||
* Implementation of {@link CryptoApi#requestDeviceVerification}.
|
||||
*
|
||||
* @param userId - ID of the owner of the device to verify
|
||||
* @param deviceId - ID of the device to verify
|
||||
*
|
||||
* @returns a VerificationRequest when the request has been sent to the other party.
|
||||
*/
|
||||
public requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest> {
|
||||
throw new Error("not implemented");
|
||||
public async requestDeviceVerification(userId: string, deviceId: string): Promise<VerificationRequest> {
|
||||
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:
|
||||
lodash "^4.17.21"
|
||||
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.10":
|
||||
version "0.1.0-alpha.10"
|
||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.10.tgz#b6a6395cffd3197ae2e0a88f4eeae8b315571fd2"
|
||||
integrity sha512-8V2NKuzGOFzEZeZVgF2is7gmuopdRbMZ064tzPDE0vN34iX6s3O8A4oxIT7SA3qtymwm3t1yEvTnT+0gfbmh4g==
|
||||
"@matrix-org/matrix-sdk-crypto-js@^0.1.0-alpha.11":
|
||||
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.11.tgz#24d705318c3159ef7dbe43bca464ac2bdd11e45d"
|
||||
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":
|
||||
version "3.2.14"
|
||||
|
Reference in New Issue
Block a user