summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification
diff options
context:
space:
mode:
authorRaindropsSys <contact@minteck.org>2023-04-24 14:03:36 +0200
committerRaindropsSys <contact@minteck.org>2023-04-24 14:03:36 +0200
commit633c92eae865e957121e08de634aeee11a8b3992 (patch)
tree09d881bee1dae0b6eee49db1dfaf0f500240606c /includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification
parentc4657e4509733699c0f26a3c900bab47e915d5a0 (diff)
downloadpluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.gz
pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.bz2
pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.zip
Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts369
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts76
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts50
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts311
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts492
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts37
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts34
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts356
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts354
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts926
10 files changed, 3005 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts
new file mode 100644
index 0000000..89c700c
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Base.ts
@@ -0,0 +1,369 @@
+/*
+Copyright 2018 New Vector Ltd
+Copyright 2020 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.
+*/
+
+/**
+ * Base class for verification methods.
+ */
+
+import { MatrixEvent } from "../../models/event";
+import { EventType } from "../../@types/event";
+import { logger } from "../../logger";
+import { DeviceInfo } from "../deviceinfo";
+import { newTimeoutError } from "./Error";
+import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossSigning";
+import { IVerificationChannel } from "./request/Channel";
+import { MatrixClient } from "../../client";
+import { VerificationRequest } from "./request/VerificationRequest";
+import { ListenerMap, TypedEventEmitter } from "../../models/typed-event-emitter";
+
+const timeoutException = new Error("Verification timed out");
+
+export class SwitchStartEventError extends Error {
+ public constructor(public readonly startEvent: MatrixEvent | null) {
+ super();
+ }
+}
+
+export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
+
+export enum VerificationEvent {
+ Cancel = "cancel",
+}
+
+export type VerificationEventHandlerMap = {
+ [VerificationEvent.Cancel]: (e: Error | MatrixEvent) => void;
+};
+
+export class VerificationBase<
+ Events extends string,
+ Arguments extends ListenerMap<Events | VerificationEvent>,
+> extends TypedEventEmitter<Events | VerificationEvent, Arguments, VerificationEventHandlerMap> {
+ private cancelled = false;
+ private _done = false;
+ private promise: Promise<void> | null = null;
+ private transactionTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
+ protected expectedEvent?: string;
+ private resolve?: () => void;
+ private reject?: (e: Error | MatrixEvent) => void;
+ private resolveEvent?: (e: MatrixEvent) => void;
+ private rejectEvent?: (e: Error) => void;
+ private started?: boolean;
+
+ /**
+ * Base class for verification methods.
+ *
+ * <p>Once a verifier object is created, the verification can be started by
+ * calling the verify() method, which will return a promise that will
+ * resolve when the verification is completed, or reject if it could not
+ * complete.</p>
+ *
+ * <p>Subclasses must have a NAME class property.</p>
+ *
+ * @param channel - the verification channel to send verification messages over.
+ * TODO: Channel types
+ *
+ * @param baseApis - base matrix api interface
+ *
+ * @param userId - the user ID that is being verified
+ *
+ * @param deviceId - the device ID that is being verified
+ *
+ * @param startEvent - the m.key.verification.start event that
+ * initiated this verification, if any
+ *
+ * @param request - the key verification request object related to
+ * this verification, if any
+ */
+ public constructor(
+ public readonly channel: IVerificationChannel,
+ public readonly baseApis: MatrixClient,
+ public readonly userId: string,
+ public readonly deviceId: string,
+ public startEvent: MatrixEvent | null,
+ public readonly request: VerificationRequest,
+ ) {
+ super();
+ }
+
+ public get initiatedByMe(): boolean {
+ // if there is no start event yet,
+ // we probably want to send it,
+ // which happens if we initiate
+ if (!this.startEvent) {
+ return true;
+ }
+ const sender = this.startEvent.getSender();
+ const content = this.startEvent.getContent();
+ return sender === this.baseApis.getUserId() && content.from_device === this.baseApis.getDeviceId();
+ }
+
+ public get hasBeenCancelled(): boolean {
+ return this.cancelled;
+ }
+
+ private resetTimer(): void {
+ logger.info("Refreshing/starting the verification transaction timeout timer");
+ if (this.transactionTimeoutTimer !== null) {
+ clearTimeout(this.transactionTimeoutTimer);
+ }
+ this.transactionTimeoutTimer = setTimeout(() => {
+ if (!this._done && !this.cancelled) {
+ logger.info("Triggering verification timeout");
+ this.cancel(timeoutException);
+ }
+ }, 10 * 60 * 1000); // 10 minutes
+ }
+
+ private endTimer(): void {
+ if (this.transactionTimeoutTimer !== null) {
+ clearTimeout(this.transactionTimeoutTimer);
+ this.transactionTimeoutTimer = null;
+ }
+ }
+
+ protected send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
+ return this.channel.send(type, uncompletedContent);
+ }
+
+ protected waitForEvent(type: string): Promise<MatrixEvent> {
+ if (this._done) {
+ return Promise.reject(new Error("Verification is already done"));
+ }
+ const existingEvent = this.request.getEventFromOtherParty(type);
+ if (existingEvent) {
+ return Promise.resolve(existingEvent);
+ }
+
+ this.expectedEvent = type;
+ return new Promise((resolve, reject) => {
+ this.resolveEvent = resolve;
+ this.rejectEvent = reject;
+ });
+ }
+
+ public canSwitchStartEvent(event: MatrixEvent): boolean {
+ return false;
+ }
+
+ public switchStartEvent(event: MatrixEvent): void {
+ if (this.canSwitchStartEvent(event)) {
+ logger.log("Verification Base: switching verification start event", { restartingFlow: !!this.rejectEvent });
+ if (this.rejectEvent) {
+ const reject = this.rejectEvent;
+ this.rejectEvent = undefined;
+ reject(new SwitchStartEventError(event));
+ } else {
+ this.startEvent = event;
+ }
+ }
+ }
+
+ public handleEvent(e: MatrixEvent): void {
+ if (this._done) {
+ return;
+ } else if (e.getType() === this.expectedEvent) {
+ // if we receive an expected m.key.verification.done, then just
+ // ignore it, since we don't need to do anything about it
+ if (this.expectedEvent !== EventType.KeyVerificationDone) {
+ this.expectedEvent = undefined;
+ this.rejectEvent = undefined;
+ this.resetTimer();
+ this.resolveEvent?.(e);
+ }
+ } else if (e.getType() === EventType.KeyVerificationCancel) {
+ const reject = this.reject;
+ this.reject = undefined;
+ // there is only promise to reject if verify has been called
+ if (reject) {
+ const content = e.getContent();
+ const { reason, code } = content;
+ reject(new Error(`Other side cancelled verification ` + `because ${reason} (${code})`));
+ }
+ } else if (this.expectedEvent) {
+ // only cancel if there is an event expected.
+ // if there is no event expected, it means verify() wasn't called
+ // and we're just replaying the timeline events when syncing
+ // after a refresh when the events haven't been stored in the cache yet.
+ const exception = new Error(
+ "Unexpected message: expecting " + this.expectedEvent + " but got " + e.getType(),
+ );
+ this.expectedEvent = undefined;
+ if (this.rejectEvent) {
+ const reject = this.rejectEvent;
+ this.rejectEvent = undefined;
+ reject(exception);
+ }
+ this.cancel(exception);
+ }
+ }
+
+ public async done(): Promise<KeysDuringVerification | void> {
+ this.endTimer(); // always kill the activity timer
+ if (!this._done) {
+ this.request.onVerifierFinished();
+ this.resolve?.();
+ return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
+ }
+ }
+
+ public cancel(e: Error | MatrixEvent): void {
+ this.endTimer(); // always kill the activity timer
+ if (!this._done) {
+ this.cancelled = true;
+ this.request.onVerifierCancelled();
+ if (this.userId && this.deviceId) {
+ // send a cancellation to the other user (if it wasn't
+ // cancelled by the other user)
+ if (e === timeoutException) {
+ const timeoutEvent = newTimeoutError();
+ this.send(timeoutEvent.getType(), timeoutEvent.getContent());
+ } else if (e instanceof MatrixEvent) {
+ const sender = e.getSender();
+ if (sender !== this.userId) {
+ const content = e.getContent();
+ if (e.getType() === EventType.KeyVerificationCancel) {
+ content.code = content.code || "m.unknown";
+ content.reason = content.reason || content.body || "Unknown reason";
+ this.send(EventType.KeyVerificationCancel, content);
+ } else {
+ this.send(EventType.KeyVerificationCancel, {
+ code: "m.unknown",
+ reason: content.body || "Unknown reason",
+ });
+ }
+ }
+ } else {
+ this.send(EventType.KeyVerificationCancel, {
+ code: "m.unknown",
+ reason: e.toString(),
+ });
+ }
+ }
+ if (this.promise !== null) {
+ // when we cancel without a promise, we end up with a promise
+ // but no reject function. If cancel is called again, we'd error.
+ if (this.reject) this.reject(e);
+ } else {
+ // FIXME: this causes an "Uncaught promise" console message
+ // if nothing ends up chaining this promise.
+ this.promise = Promise.reject(e);
+ }
+ // Also emit a 'cancel' event that the app can listen for to detect cancellation
+ // before calling verify()
+ this.emit(VerificationEvent.Cancel, e);
+ }
+ }
+
+ /**
+ * Begin the key verification
+ *
+ * @returns Promise which resolves when the verification has
+ * completed.
+ */
+ public verify(): Promise<void> {
+ if (this.promise) return this.promise;
+
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = (...args): void => {
+ this._done = true;
+ this.endTimer();
+ resolve(...args);
+ };
+ this.reject = (e: Error | MatrixEvent): void => {
+ this._done = true;
+ this.endTimer();
+ reject(e);
+ };
+ });
+ if (this.doVerification && !this.started) {
+ this.started = true;
+ this.resetTimer(); // restart the timeout
+ new Promise<void>((resolve, reject) => {
+ const crossSignId = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(this.userId)?.getId();
+ if (crossSignId === this.deviceId) {
+ reject(new Error("Device ID is the same as the cross-signing ID"));
+ }
+ resolve();
+ })
+ .then(() => this.doVerification!())
+ .then(this.done.bind(this), this.cancel.bind(this));
+ }
+ return this.promise;
+ }
+
+ protected doVerification?: () => Promise<void>;
+
+ protected async verifyKeys(userId: string, keys: Record<string, string>, verifier: KeyVerifier): Promise<void> {
+ // we try to verify all the keys that we're told about, but we might
+ // not know about all of them, so keep track of the keys that we know
+ // about, and ignore the rest
+ const verifiedDevices: [string, string, string][] = [];
+
+ for (const [keyId, keyInfo] of Object.entries(keys)) {
+ const deviceId = keyId.split(":", 2)[1];
+ const device = this.baseApis.getStoredDevice(userId, deviceId);
+ if (device) {
+ verifier(keyId, device, keyInfo);
+ verifiedDevices.push([deviceId, keyId, device.keys[keyId]]);
+ } else {
+ const crossSigningInfo = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(userId);
+ if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
+ verifier(
+ keyId,
+ DeviceInfo.fromStorage(
+ {
+ keys: {
+ [keyId]: deviceId,
+ },
+ },
+ deviceId,
+ ),
+ keyInfo,
+ );
+ verifiedDevices.push([deviceId, keyId, deviceId]);
+ } else {
+ logger.warn(`verification: Could not find device ${deviceId} to verify`);
+ }
+ }
+ }
+
+ // if none of the keys could be verified, then error because the app
+ // should be informed about that
+ if (!verifiedDevices.length) {
+ throw new Error("No devices could be verified");
+ }
+
+ logger.info("Verification completed! Marking devices verified: ", verifiedDevices);
+ // TODO: There should probably be a batch version of this, otherwise it's going
+ // to upload each signature in a separate API call which is silly because the
+ // API supports as many signatures as you like.
+ for (const [deviceId, keyId, key] of verifiedDevices) {
+ await this.baseApis.crypto!.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key });
+ }
+
+ // if one of the user's own devices is being marked as verified / unverified,
+ // check the key backup status, since whether or not we use this depends on
+ // whether it has a signature from a verified device
+ if (userId == this.baseApis.credentials.userId) {
+ await this.baseApis.checkKeyBackup();
+ }
+ }
+
+ public get events(): string[] | undefined {
+ return undefined;
+ }
+}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts
new file mode 100644
index 0000000..da73ebb
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/Error.ts
@@ -0,0 +1,76 @@
+/*
+Copyright 2018 - 2021 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.
+*/
+
+/**
+ * Error messages.
+ */
+
+import { MatrixEvent } from "../../models/event";
+import { EventType } from "../../@types/event";
+
+export function newVerificationError(code: string, reason: string, extraData?: Record<string, any>): MatrixEvent {
+ const content = Object.assign({}, { code, reason }, extraData);
+ return new MatrixEvent({
+ type: EventType.KeyVerificationCancel,
+ content,
+ });
+}
+
+export function errorFactory(code: string, reason: string): (extraData?: Record<string, any>) => MatrixEvent {
+ return function (extraData?: Record<string, any>) {
+ return newVerificationError(code, reason, extraData);
+ };
+}
+
+/**
+ * The verification was cancelled by the user.
+ */
+export const newUserCancelledError = errorFactory("m.user", "Cancelled by user");
+
+/**
+ * The verification timed out.
+ */
+export const newTimeoutError = errorFactory("m.timeout", "Timed out");
+
+/**
+ * An unknown method was selected.
+ */
+export const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method");
+
+/**
+ * An unexpected message was sent.
+ */
+export const newUnexpectedMessageError = errorFactory("m.unexpected_message", "Unexpected message");
+
+/**
+ * The key does not match.
+ */
+export const newKeyMismatchError = errorFactory("m.key_mismatch", "Key mismatch");
+
+/**
+ * An invalid message was sent.
+ */
+export const newInvalidMessageError = errorFactory("m.invalid_message", "Invalid message");
+
+export function errorFromEvent(event: MatrixEvent): { code: string; reason: string } {
+ const content = event.getContent();
+ if (content) {
+ const { code, reason } = content;
+ return { code, reason };
+ } else {
+ return { code: "Unknown error", reason: "m.unknown" };
+ }
+}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts
new file mode 100644
index 0000000..c437e0c
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/IllegalMethod.ts
@@ -0,0 +1,50 @@
+/*
+Copyright 2020 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.
+*/
+
+/**
+ * Verification method that is illegal to have (cannot possibly
+ * do verification with this method).
+ */
+
+import { VerificationBase as Base, VerificationEvent, VerificationEventHandlerMap } from "./Base";
+import { IVerificationChannel } from "./request/Channel";
+import { MatrixClient } from "../../client";
+import { MatrixEvent } from "../../models/event";
+import { VerificationRequest } from "./request/VerificationRequest";
+
+export class IllegalMethod extends Base<VerificationEvent, VerificationEventHandlerMap> {
+ public static factory(
+ channel: IVerificationChannel,
+ baseApis: MatrixClient,
+ userId: string,
+ deviceId: string,
+ startEvent: MatrixEvent,
+ request: VerificationRequest,
+ ): IllegalMethod {
+ return new IllegalMethod(channel, baseApis, userId, deviceId, startEvent, request);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public static get NAME(): string {
+ // Typically the name will be something else, but to complete
+ // the contract we offer a default one here.
+ return "org.matrix.illegal_method";
+ }
+
+ protected doVerification = async (): Promise<void> => {
+ throw new Error("Verification is not possible with this method");
+ };
+}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts
new file mode 100644
index 0000000..bfb532e
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/QRCode.ts
@@ -0,0 +1,311 @@
+/*
+Copyright 2018 - 2021 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.
+*/
+
+/**
+ * QR code key verification.
+ */
+
+import { VerificationBase as Base, VerificationEventHandlerMap } from "./Base";
+import { newKeyMismatchError, newUserCancelledError } from "./Error";
+import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
+import { logger } from "../../logger";
+import { VerificationRequest } from "./request/VerificationRequest";
+import { MatrixClient } from "../../client";
+import { IVerificationChannel } from "./request/Channel";
+import { MatrixEvent } from "../../models/event";
+
+export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
+export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
+
+interface IReciprocateQr {
+ confirm(): void;
+ cancel(): void;
+}
+
+export enum QrCodeEvent {
+ ShowReciprocateQr = "show_reciprocate_qr",
+}
+
+type EventHandlerMap = {
+ [QrCodeEvent.ShowReciprocateQr]: (qr: IReciprocateQr) => void;
+} & VerificationEventHandlerMap;
+
+export class ReciprocateQRCode extends Base<QrCodeEvent, EventHandlerMap> {
+ public reciprocateQREvent?: IReciprocateQr;
+
+ public static factory(
+ channel: IVerificationChannel,
+ baseApis: MatrixClient,
+ userId: string,
+ deviceId: string,
+ startEvent: MatrixEvent,
+ request: VerificationRequest,
+ ): ReciprocateQRCode {
+ return new ReciprocateQRCode(channel, baseApis, userId, deviceId, startEvent, request);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public static get NAME(): string {
+ return "m.reciprocate.v1";
+ }
+
+ protected doVerification = async (): Promise<void> => {
+ if (!this.startEvent) {
+ // TODO: Support scanning QR codes
+ throw new Error("It is not currently possible to start verification" + "with this method yet.");
+ }
+
+ const { qrCodeData } = this.request;
+ // 1. check the secret
+ if (this.startEvent.getContent()["secret"] !== qrCodeData?.encodedSharedSecret) {
+ throw newKeyMismatchError();
+ }
+
+ // 2. ask if other user shows shield as well
+ await new Promise<void>((resolve, reject) => {
+ this.reciprocateQREvent = {
+ confirm: resolve,
+ cancel: () => reject(newUserCancelledError()),
+ };
+ this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
+ });
+
+ // 3. determine key to sign / mark as trusted
+ const keys: Record<string, string> = {};
+
+ switch (qrCodeData?.mode) {
+ case Mode.VerifyOtherUser: {
+ // add master key to keys to be signed, only if we're not doing self-verification
+ const masterKey = qrCodeData.otherUserMasterKey;
+ keys[`ed25519:${masterKey}`] = masterKey!;
+ break;
+ }
+ case Mode.VerifySelfTrusted: {
+ const deviceId = this.request.targetDevice.deviceId;
+ keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey!;
+ break;
+ }
+ case Mode.VerifySelfUntrusted: {
+ const masterKey = qrCodeData.myMasterKey;
+ keys[`ed25519:${masterKey}`] = masterKey!;
+ break;
+ }
+ }
+
+ // 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
+ await this.verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
+ // make sure the device has the expected keys
+ const targetKey = keys[keyId];
+ if (!targetKey) throw newKeyMismatchError();
+
+ if (keyInfo !== targetKey) {
+ logger.error("key ID from key info does not match");
+ throw newKeyMismatchError();
+ }
+ for (const deviceKeyId in device.keys) {
+ if (!deviceKeyId.startsWith("ed25519")) continue;
+ const deviceTargetKey = keys[deviceKeyId];
+ if (!deviceTargetKey) throw newKeyMismatchError();
+ if (device.keys[deviceKeyId] !== deviceTargetKey) {
+ logger.error("master key does not match");
+ throw newKeyMismatchError();
+ }
+ }
+ });
+ };
+}
+
+const CODE_VERSION = 0x02; // the version of binary QR codes we support
+const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
+
+enum Mode {
+ VerifyOtherUser = 0x00, // Verifying someone who isn't us
+ VerifySelfTrusted = 0x01, // We trust the master key
+ VerifySelfUntrusted = 0x02, // We do not trust the master key
+}
+
+interface IQrData {
+ prefix: string;
+ version: number;
+ mode: Mode;
+ transactionId?: string;
+ firstKeyB64: string;
+ secondKeyB64: string;
+ secretB64: string;
+}
+
+export class QRCodeData {
+ public constructor(
+ public readonly mode: Mode,
+ private readonly sharedSecret: string,
+ // only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
+ public readonly otherUserMasterKey: string | null,
+ // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
+ public readonly otherDeviceKey: string | null,
+ // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
+ public readonly myMasterKey: string | null,
+ private readonly buffer: Buffer,
+ ) {}
+
+ public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
+ const sharedSecret = QRCodeData.generateSharedSecret();
+ const mode = QRCodeData.determineMode(request, client);
+ let otherUserMasterKey: string | null = null;
+ let otherDeviceKey: string | null = null;
+ let myMasterKey: string | null = null;
+ if (mode === Mode.VerifyOtherUser) {
+ const otherUserCrossSigningInfo = client.getStoredCrossSigningForUser(request.otherUserId);
+ otherUserMasterKey = otherUserCrossSigningInfo!.getId("master");
+ } else if (mode === Mode.VerifySelfTrusted) {
+ otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
+ } else if (mode === Mode.VerifySelfUntrusted) {
+ const myUserId = client.getUserId()!;
+ const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
+ myMasterKey = myCrossSigningInfo!.getId("master");
+ }
+ const qrData = QRCodeData.generateQrData(
+ request,
+ client,
+ mode,
+ sharedSecret,
+ otherUserMasterKey!,
+ otherDeviceKey!,
+ myMasterKey!,
+ );
+ const buffer = QRCodeData.generateBuffer(qrData);
+ return new QRCodeData(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
+ }
+
+ /**
+ * The unpadded base64 encoded shared secret.
+ */
+ public get encodedSharedSecret(): string {
+ return this.sharedSecret;
+ }
+
+ public getBuffer(): Buffer {
+ return this.buffer;
+ }
+
+ private static generateSharedSecret(): string {
+ const secretBytes = new Uint8Array(11);
+ global.crypto.getRandomValues(secretBytes);
+ return encodeUnpaddedBase64(secretBytes);
+ }
+
+ private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
+ const myUserId = client.getUserId()!;
+ const otherDevice = request.targetDevice;
+ const device = otherDevice.deviceId ? client.getStoredDevice(myUserId, otherDevice.deviceId) : undefined;
+ if (!device) {
+ throw new Error("could not find device " + otherDevice?.deviceId);
+ }
+ return device.getFingerprint();
+ }
+
+ private static determineMode(request: VerificationRequest, client: MatrixClient): Mode {
+ const myUserId = client.getUserId();
+ const otherUserId = request.otherUserId;
+
+ let mode = Mode.VerifyOtherUser;
+ if (myUserId === otherUserId) {
+ // Mode changes depending on whether or not we trust the master cross signing key
+ const myTrust = client.checkUserTrust(myUserId);
+ if (myTrust.isCrossSigningVerified()) {
+ mode = Mode.VerifySelfTrusted;
+ } else {
+ mode = Mode.VerifySelfUntrusted;
+ }
+ }
+ return mode;
+ }
+
+ private static generateQrData(
+ request: VerificationRequest,
+ client: MatrixClient,
+ mode: Mode,
+ encodedSharedSecret: string,
+ otherUserMasterKey?: string,
+ otherDeviceKey?: string,
+ myMasterKey?: string,
+ ): IQrData {
+ const myUserId = client.getUserId()!;
+ const transactionId = request.channel.transactionId;
+ const qrData: IQrData = {
+ prefix: BINARY_PREFIX,
+ version: CODE_VERSION,
+ mode,
+ transactionId,
+ firstKeyB64: "", // worked out shortly
+ secondKeyB64: "", // worked out shortly
+ secretB64: encodedSharedSecret,
+ };
+
+ const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
+
+ if (mode === Mode.VerifyOtherUser) {
+ // First key is our master cross signing key
+ qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
+ // Second key is the other user's master cross signing key
+ qrData.secondKeyB64 = otherUserMasterKey!;
+ } else if (mode === Mode.VerifySelfTrusted) {
+ // First key is our master cross signing key
+ qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!;
+ qrData.secondKeyB64 = otherDeviceKey!;
+ } else if (mode === Mode.VerifySelfUntrusted) {
+ // First key is our device's key
+ qrData.firstKeyB64 = client.getDeviceEd25519Key()!;
+ // Second key is what we think our master cross signing key is
+ qrData.secondKeyB64 = myMasterKey!;
+ }
+ return qrData;
+ }
+
+ private static generateBuffer(qrData: IQrData): Buffer {
+ let buf = Buffer.alloc(0); // we'll concat our way through life
+
+ const appendByte = (b: number): void => {
+ const tmpBuf = Buffer.from([b]);
+ buf = Buffer.concat([buf, tmpBuf]);
+ };
+ const appendInt = (i: number): void => {
+ const tmpBuf = Buffer.alloc(2);
+ tmpBuf.writeInt16BE(i, 0);
+ buf = Buffer.concat([buf, tmpBuf]);
+ };
+ const appendStr = (s: string, enc: BufferEncoding, withLengthPrefix = true): void => {
+ const tmpBuf = Buffer.from(s, enc);
+ if (withLengthPrefix) appendInt(tmpBuf.byteLength);
+ buf = Buffer.concat([buf, tmpBuf]);
+ };
+ const appendEncBase64 = (b64: string): void => {
+ const b = decodeBase64(b64);
+ const tmpBuf = Buffer.from(b);
+ buf = Buffer.concat([buf, tmpBuf]);
+ };
+
+ // Actually build the buffer for the QR code
+ appendStr(qrData.prefix, "ascii", false);
+ appendByte(qrData.version);
+ appendByte(qrData.mode);
+ appendStr(qrData.transactionId!, "utf-8");
+ appendEncBase64(qrData.firstKeyB64);
+ appendEncBase64(qrData.secondKeyB64);
+ appendEncBase64(qrData.secretB64);
+
+ return buf;
+ }
+}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts
new file mode 100644
index 0000000..a8d237d
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SAS.ts
@@ -0,0 +1,492 @@
+/*
+Copyright 2018 - 2021 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.
+*/
+
+/**
+ * Short Authentication String (SAS) verification.
+ */
+
+import anotherjson from "another-json";
+import { Utility, SAS as OlmSAS } from "@matrix-org/olm";
+
+import { VerificationBase as Base, SwitchStartEventError, VerificationEventHandlerMap } from "./Base";
+import {
+ errorFactory,
+ newInvalidMessageError,
+ newKeyMismatchError,
+ newUnknownMethodError,
+ newUserCancelledError,
+} from "./Error";
+import { logger } from "../../logger";
+import { IContent, MatrixEvent } from "../../models/event";
+import { generateDecimalSas } from "./SASDecimal";
+import { EventType } from "../../@types/event";
+
+const START_TYPE = EventType.KeyVerificationStart;
+
+const EVENTS = [EventType.KeyVerificationAccept, EventType.KeyVerificationKey, EventType.KeyVerificationMac];
+
+let olmutil: Utility;
+
+const newMismatchedSASError = errorFactory("m.mismatched_sas", "Mismatched short authentication string");
+
+const newMismatchedCommitmentError = errorFactory("m.mismatched_commitment", "Mismatched commitment");
+
+type EmojiMapping = [emoji: string, name: string];
+
+const emojiMapping: EmojiMapping[] = [
+ ["🐶", "dog"], // 0
+ ["🐱", "cat"], // 1
+ ["🦁", "lion"], // 2
+ ["🐎", "horse"], // 3
+ ["🦄", "unicorn"], // 4
+ ["🐷", "pig"], // 5
+ ["🐘", "elephant"], // 6
+ ["🐰", "rabbit"], // 7
+ ["🐼", "panda"], // 8
+ ["🐓", "rooster"], // 9
+ ["🐧", "penguin"], // 10
+ ["🐢", "turtle"], // 11
+ ["🐟", "fish"], // 12
+ ["🐙", "octopus"], // 13
+ ["🦋", "butterfly"], // 14
+ ["🌷", "flower"], // 15
+ ["🌳", "tree"], // 16
+ ["🌵", "cactus"], // 17
+ ["🍄", "mushroom"], // 18
+ ["🌏", "globe"], // 19
+ ["🌙", "moon"], // 20
+ ["☁️", "cloud"], // 21
+ ["🔥", "fire"], // 22
+ ["🍌", "banana"], // 23
+ ["🍎", "apple"], // 24
+ ["🍓", "strawberry"], // 25
+ ["🌽", "corn"], // 26
+ ["🍕", "pizza"], // 27
+ ["🎂", "cake"], // 28
+ ["❤️", "heart"], // 29
+ ["🙂", "smiley"], // 30
+ ["🤖", "robot"], // 31
+ ["🎩", "hat"], // 32
+ ["👓", "glasses"], // 33
+ ["🔧", "spanner"], // 34
+ ["🎅", "santa"], // 35
+ ["👍", "thumbs up"], // 36
+ ["☂️", "umbrella"], // 37
+ ["⌛", "hourglass"], // 38
+ ["⏰", "clock"], // 39
+ ["🎁", "gift"], // 40
+ ["💡", "light bulb"], // 41
+ ["📕", "book"], // 42
+ ["✏️", "pencil"], // 43
+ ["📎", "paperclip"], // 44
+ ["✂️", "scissors"], // 45
+ ["🔒", "lock"], // 46
+ ["🔑", "key"], // 47
+ ["🔨", "hammer"], // 48
+ ["☎️", "telephone"], // 49
+ ["🏁", "flag"], // 50
+ ["🚂", "train"], // 51
+ ["🚲", "bicycle"], // 52
+ ["✈️", "aeroplane"], // 53
+ ["🚀", "rocket"], // 54
+ ["🏆", "trophy"], // 55
+ ["⚽", "ball"], // 56
+ ["🎸", "guitar"], // 57
+ ["🎺", "trumpet"], // 58
+ ["🔔", "bell"], // 59
+ ["⚓️", "anchor"], // 60
+ ["🎧", "headphones"], // 61
+ ["📁", "folder"], // 62
+ ["📌", "pin"], // 63
+];
+
+function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
+ const emojis = [
+ // just like base64 encoding
+ sasBytes[0] >> 2,
+ ((sasBytes[0] & 0x3) << 4) | (sasBytes[1] >> 4),
+ ((sasBytes[1] & 0xf) << 2) | (sasBytes[2] >> 6),
+ sasBytes[2] & 0x3f,
+ sasBytes[3] >> 2,
+ ((sasBytes[3] & 0x3) << 4) | (sasBytes[4] >> 4),
+ ((sasBytes[4] & 0xf) << 2) | (sasBytes[5] >> 6),
+ ];
+
+ return emojis.map((num) => emojiMapping[num]);
+}
+
+const sasGenerators = {
+ decimal: generateDecimalSas,
+ emoji: generateEmojiSas,
+} as const;
+
+export interface IGeneratedSas {
+ decimal?: [number, number, number];
+ emoji?: EmojiMapping[];
+}
+
+export interface ISasEvent {
+ sas: IGeneratedSas;
+ confirm(): Promise<void>;
+ cancel(): void;
+ mismatch(): void;
+}
+
+function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas {
+ const sas: IGeneratedSas = {};
+ for (const method of methods) {
+ if (method in sasGenerators) {
+ // @ts-ignore - ts doesn't like us mixing types like this
+ sas[method] = sasGenerators[method](Array.from(sasBytes));
+ }
+ }
+ return sas;
+}
+
+const macMethods = {
+ "hkdf-hmac-sha256": "calculate_mac",
+ "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64",
+ "hkdf-hmac-sha256.v2": "calculate_mac_fixed_base64",
+ "hmac-sha256": "calculate_mac_long_kdf",
+} as const;
+
+type MacMethod = keyof typeof macMethods;
+
+function calculateMAC(olmSAS: OlmSAS, method: MacMethod) {
+ return function (input: string, info: string): string {
+ const mac = olmSAS[macMethods[method]](input, info);
+ logger.log("SAS calculateMAC:", method, [input, info], mac);
+ return mac;
+ };
+}
+
+const calculateKeyAgreement = {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ "curve25519-hkdf-sha256": function (sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
+ const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|` + `${sas.ourSASPubKey}|`;
+ const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
+ const sasInfo =
+ "MATRIX_KEY_VERIFICATION_SAS|" +
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) +
+ sas.channel.transactionId;
+ return olmSAS.generate_bytes(sasInfo, bytes);
+ },
+ "curve25519": function (sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
+ const ourInfo = `${sas.baseApis.getUserId()}${sas.baseApis.deviceId}`;
+ const theirInfo = `${sas.userId}${sas.deviceId}`;
+ const sasInfo =
+ "MATRIX_KEY_VERIFICATION_SAS" +
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) +
+ sas.channel.transactionId;
+ return olmSAS.generate_bytes(sasInfo, bytes);
+ },
+} as const;
+
+type KeyAgreement = keyof typeof calculateKeyAgreement;
+
+/* lists of algorithms/methods that are supported. The key agreement, hashes,
+ * and MAC lists should be sorted in order of preference (most preferred
+ * first).
+ */
+const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"];
+const HASHES_LIST = ["sha256"];
+const MAC_LIST: MacMethod[] = [
+ "hkdf-hmac-sha256.v2",
+ "org.matrix.msc3783.hkdf-hmac-sha256",
+ "hkdf-hmac-sha256",
+ "hmac-sha256",
+];
+const SAS_LIST = Object.keys(sasGenerators);
+
+const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST);
+const HASHES_SET = new Set(HASHES_LIST);
+const MAC_SET = new Set(MAC_LIST);
+const SAS_SET = new Set(SAS_LIST);
+
+function intersection<T>(anArray: T[], aSet: Set<T>): T[] {
+ return Array.isArray(anArray) ? anArray.filter((x) => aSet.has(x)) : [];
+}
+
+export enum SasEvent {
+ ShowSas = "show_sas",
+}
+
+type EventHandlerMap = {
+ [SasEvent.ShowSas]: (sas: ISasEvent) => void;
+} & VerificationEventHandlerMap;
+
+export class SAS extends Base<SasEvent, EventHandlerMap> {
+ private waitingForAccept?: boolean;
+ public ourSASPubKey?: string;
+ public theirSASPubKey?: string;
+ public sasEvent?: ISasEvent;
+
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ public static get NAME(): string {
+ return "m.sas.v1";
+ }
+
+ public get events(): string[] {
+ return EVENTS;
+ }
+
+ protected doVerification = async (): Promise<void> => {
+ await global.Olm.init();
+ olmutil = olmutil || new global.Olm.Utility();
+
+ // make sure user's keys are downloaded
+ await this.baseApis.downloadKeys([this.userId]);
+
+ let retry = false;
+ do {
+ try {
+ if (this.initiatedByMe) {
+ return await this.doSendVerification();
+ } else {
+ return await this.doRespondVerification();
+ }
+ } catch (err) {
+ if (err instanceof SwitchStartEventError) {
+ // this changes what initiatedByMe returns
+ this.startEvent = err.startEvent;
+ retry = true;
+ } else {
+ throw err;
+ }
+ }
+ } while (retry);
+ };
+
+ public canSwitchStartEvent(event: MatrixEvent): boolean {
+ if (event.getType() !== START_TYPE) {
+ return false;
+ }
+ const content = event.getContent();
+ return content?.method === SAS.NAME && !!this.waitingForAccept;
+ }
+
+ private async sendStart(): Promise<Record<string, any>> {
+ const startContent = this.channel.completeContent(START_TYPE, {
+ method: SAS.NAME,
+ from_device: this.baseApis.deviceId,
+ key_agreement_protocols: KEY_AGREEMENT_LIST,
+ hashes: HASHES_LIST,
+ message_authentication_codes: MAC_LIST,
+ // FIXME: allow app to specify what SAS methods can be used
+ short_authentication_string: SAS_LIST,
+ });
+ await this.channel.sendCompleted(START_TYPE, startContent);
+ return startContent;
+ }
+
+ private async verifyAndCheckMAC(
+ keyAgreement: KeyAgreement,
+ sasMethods: string[],
+ olmSAS: OlmSAS,
+ macMethod: MacMethod,
+ ): Promise<void> {
+ const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
+ const verifySAS = new Promise<void>((resolve, reject) => {
+ this.sasEvent = {
+ sas: generateSas(sasBytes, sasMethods),
+ confirm: async (): Promise<void> => {
+ try {
+ await this.sendMAC(olmSAS, macMethod);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ },
+ cancel: () => reject(newUserCancelledError()),
+ mismatch: () => reject(newMismatchedSASError()),
+ };
+ this.emit(SasEvent.ShowSas, this.sasEvent);
+ });
+
+ const [e] = await Promise.all([
+ this.waitForEvent(EventType.KeyVerificationMac).then((e) => {
+ // we don't expect any more messages from the other
+ // party, and they may send a m.key.verification.done
+ // when they're done on their end
+ this.expectedEvent = EventType.KeyVerificationDone;
+ return e;
+ }),
+ verifySAS,
+ ]);
+ const content = e.getContent();
+ await this.checkMAC(olmSAS, content, macMethod);
+ }
+
+ private async doSendVerification(): Promise<void> {
+ this.waitingForAccept = true;
+ let startContent;
+ if (this.startEvent) {
+ startContent = this.channel.completedContentFromEvent(this.startEvent);
+ } else {
+ startContent = await this.sendStart();
+ }
+
+ // we might have switched to a different start event,
+ // but was we didn't call _waitForEvent there was no
+ // call that could throw yet. So check manually that
+ // we're still on the initiator side
+ if (!this.initiatedByMe) {
+ throw new SwitchStartEventError(this.startEvent);
+ }
+
+ let e: MatrixEvent;
+ try {
+ e = await this.waitForEvent(EventType.KeyVerificationAccept);
+ } finally {
+ this.waitingForAccept = false;
+ }
+ let content = e.getContent();
+ const sasMethods = intersection(content.short_authentication_string, SAS_SET);
+ if (
+ !(
+ KEY_AGREEMENT_SET.has(content.key_agreement_protocol) &&
+ HASHES_SET.has(content.hash) &&
+ MAC_SET.has(content.message_authentication_code) &&
+ sasMethods.length
+ )
+ ) {
+ throw newUnknownMethodError();
+ }
+ if (typeof content.commitment !== "string") {
+ throw newInvalidMessageError();
+ }
+ const keyAgreement = content.key_agreement_protocol;
+ const macMethod = content.message_authentication_code;
+ const hashCommitment = content.commitment;
+ const olmSAS = new global.Olm.SAS();
+ try {
+ this.ourSASPubKey = olmSAS.get_pubkey();
+ await this.send(EventType.KeyVerificationKey, {
+ key: this.ourSASPubKey,
+ });
+
+ e = await this.waitForEvent(EventType.KeyVerificationKey);
+ // FIXME: make sure event is properly formed
+ content = e.getContent();
+ const commitmentStr = content.key + anotherjson.stringify(startContent);
+ // TODO: use selected hash function (when we support multiple)
+ if (olmutil.sha256(commitmentStr) !== hashCommitment) {
+ throw newMismatchedCommitmentError();
+ }
+ this.theirSASPubKey = content.key;
+ olmSAS.set_their_key(content.key);
+
+ await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod);
+ } finally {
+ olmSAS.free();
+ }
+ }
+
+ private async doRespondVerification(): Promise<void> {
+ // as m.related_to is not included in the encrypted content in e2e rooms,
+ // we need to make sure it is added
+ let content = this.channel.completedContentFromEvent(this.startEvent!);
+
+ // Note: we intersect using our pre-made lists, rather than the sets,
+ // so that the result will be in our order of preference. Then
+ // fetching the first element from the array will give our preferred
+ // method out of the ones offered by the other party.
+ const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0];
+ const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0];
+ const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
+ // FIXME: allow app to specify what SAS methods can be used
+ const sasMethods = intersection(content.short_authentication_string, SAS_SET);
+ if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) {
+ throw newUnknownMethodError();
+ }
+
+ const olmSAS = new global.Olm.SAS();
+ try {
+ const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
+ await this.send(EventType.KeyVerificationAccept, {
+ key_agreement_protocol: keyAgreement,
+ hash: hashMethod,
+ message_authentication_code: macMethod,
+ short_authentication_string: sasMethods,
+ // TODO: use selected hash function (when we support multiple)
+ commitment: olmutil.sha256(commitmentStr),
+ });
+
+ const e = await this.waitForEvent(EventType.KeyVerificationKey);
+ // FIXME: make sure event is properly formed
+ content = e.getContent();
+ this.theirSASPubKey = content.key;
+ olmSAS.set_their_key(content.key);
+ this.ourSASPubKey = olmSAS.get_pubkey();
+ await this.send(EventType.KeyVerificationKey, {
+ key: this.ourSASPubKey,
+ });
+
+ await this.verifyAndCheckMAC(keyAgreement, sasMethods, olmSAS, macMethod);
+ } finally {
+ olmSAS.free();
+ }
+ }
+
+ private sendMAC(olmSAS: OlmSAS, method: MacMethod): Promise<void> {
+ const mac: Record<string, string> = {};
+ const keyList: string[] = [];
+ const baseInfo =
+ "MATRIX_KEY_VERIFICATION_MAC" +
+ this.baseApis.getUserId() +
+ this.baseApis.deviceId +
+ this.userId +
+ this.deviceId +
+ this.channel.transactionId;
+
+ const deviceKeyId = `ed25519:${this.baseApis.deviceId}`;
+ mac[deviceKeyId] = calculateMAC(olmSAS, method)(this.baseApis.getDeviceEd25519Key()!, baseInfo + deviceKeyId);
+ keyList.push(deviceKeyId);
+
+ const crossSigningId = this.baseApis.getCrossSigningId();
+ if (crossSigningId) {
+ const crossSigningKeyId = `ed25519:${crossSigningId}`;
+ mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(crossSigningId, baseInfo + crossSigningKeyId);
+ keyList.push(crossSigningKeyId);
+ }
+
+ const keys = calculateMAC(olmSAS, method)(keyList.sort().join(","), baseInfo + "KEY_IDS");
+ return this.send(EventType.KeyVerificationMac, { mac, keys });
+ }
+
+ private async checkMAC(olmSAS: OlmSAS, content: IContent, method: MacMethod): Promise<void> {
+ const baseInfo =
+ "MATRIX_KEY_VERIFICATION_MAC" +
+ this.userId +
+ this.deviceId +
+ this.baseApis.getUserId() +
+ this.baseApis.deviceId +
+ this.channel.transactionId;
+
+ if (
+ content.keys !==
+ calculateMAC(olmSAS, method)(Object.keys(content.mac).sort().join(","), baseInfo + "KEY_IDS")
+ ) {
+ throw newKeyMismatchError();
+ }
+
+ await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
+ if (keyInfo !== calculateMAC(olmSAS, method)(device.keys[keyId], baseInfo + keyId)) {
+ throw newKeyMismatchError();
+ }
+ });
+ }
+}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts
new file mode 100644
index 0000000..0cb4630
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/SASDecimal.ts
@@ -0,0 +1,37 @@
+/*
+Copyright 2018 - 2022 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.
+*/
+
+/**
+ * Implementation of decimal encoding of SAS as per:
+ * https://spec.matrix.org/v1.4/client-server-api/#sas-method-decimal
+ * @param sasBytes - the five bytes generated by HKDF
+ * @returns the derived three numbers between 1000 and 9191 inclusive
+ */
+export function generateDecimalSas(sasBytes: number[]): [number, number, number] {
+ /*
+ * +--------+--------+--------+--------+--------+
+ * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
+ * +--------+--------+--------+--------+--------+
+ * bits: 87654321 87654321 87654321 87654321 87654321
+ * \____________/\_____________/\____________/
+ * 1st number 2nd number 3rd number
+ */
+ return [
+ ((sasBytes[0] << 5) | (sasBytes[1] >> 3)) + 1000,
+ (((sasBytes[1] & 0x7) << 10) | (sasBytes[2] << 2) | (sasBytes[3] >> 6)) + 1000,
+ (((sasBytes[3] & 0x3f) << 7) | (sasBytes[4] >> 1)) + 1000,
+ ];
+}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts
new file mode 100644
index 0000000..48415f9
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/Channel.ts
@@ -0,0 +1,34 @@
+/*
+Copyright 2021 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 { MatrixEvent } from "../../../models/event";
+import { VerificationRequest } from "./VerificationRequest";
+
+export interface IVerificationChannel {
+ request?: VerificationRequest;
+ readonly userId?: string;
+ readonly roomId?: string;
+ readonly deviceId?: string;
+ readonly transactionId?: string;
+ readonly receiveStartFromOtherDevices?: boolean;
+ getTimestamp(event: MatrixEvent): number;
+ send(type: string, uncompletedContent: Record<string, any>): Promise<void>;
+ completeContent(type: string, content: Record<string, any>): Record<string, any>;
+ sendCompleted(type: string, content: Record<string, any>): Promise<void>;
+ completedContentFromEvent(event: MatrixEvent): Record<string, any>;
+ canCreateRequest(type: string): boolean;
+ handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent: boolean): Promise<void>;
+}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts
new file mode 100644
index 0000000..ff11bf1
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/InRoomChannel.ts
@@ -0,0 +1,356 @@
+/*
+Copyright 2018 New Vector Ltd
+Copyright 2019 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 { VerificationRequest, REQUEST_TYPE, READY_TYPE, START_TYPE } from "./VerificationRequest";
+import { logger } from "../../../logger";
+import { IVerificationChannel } from "./Channel";
+import { EventType } from "../../../@types/event";
+import { MatrixClient } from "../../../client";
+import { MatrixEvent } from "../../../models/event";
+import { IRequestsMap } from "../..";
+
+const MESSAGE_TYPE = EventType.RoomMessage;
+const M_REFERENCE = "m.reference";
+const M_RELATES_TO = "m.relates_to";
+
+/**
+ * A key verification channel that sends verification events in the timeline of a room.
+ * Uses the event id of the initial m.key.verification.request event as a transaction id.
+ */
+export class InRoomChannel implements IVerificationChannel {
+ private requestEventId?: string;
+
+ /**
+ * @param client - the matrix client, to send messages with and get current user & device from.
+ * @param roomId - id of the room where verification events should be posted in, should be a DM with the given user.
+ * @param userId - id of user that the verification request is directed at, should be present in the room.
+ */
+ public constructor(private readonly client: MatrixClient, public readonly roomId: string, public userId?: string) {}
+
+ public get receiveStartFromOtherDevices(): boolean {
+ return true;
+ }
+
+ /** The transaction id generated/used by this verification channel */
+ public get transactionId(): string | undefined {
+ return this.requestEventId;
+ }
+
+ public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string | undefined {
+ const type = InRoomChannel.getEventType(event);
+ if (type !== REQUEST_TYPE) {
+ return;
+ }
+ const ownUserId = client.getUserId();
+ const sender = event.getSender();
+ const content = event.getContent();
+ const receiver = content.to;
+
+ if (sender === ownUserId) {
+ return receiver;
+ } else if (receiver === ownUserId) {
+ return sender;
+ }
+ }
+
+ /**
+ * @param event - the event to get the timestamp of
+ * @returns the timestamp when the event was sent
+ */
+ public getTimestamp(event: MatrixEvent): number {
+ return event.getTs();
+ }
+
+ /**
+ * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
+ * @param type - the event type to check
+ * @returns boolean flag
+ */
+ public static canCreateRequest(type: string): boolean {
+ return type === REQUEST_TYPE;
+ }
+
+ public canCreateRequest(type: string): boolean {
+ return InRoomChannel.canCreateRequest(type);
+ }
+
+ /**
+ * Extract the transaction id used by a given key verification event, if any
+ * @param event - the event
+ * @returns the transaction id
+ */
+ public static getTransactionId(event: MatrixEvent): string | undefined {
+ if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
+ return event.getId();
+ } else {
+ const relation = event.getRelation();
+ if (relation?.rel_type === M_REFERENCE) {
+ return relation.event_id;
+ }
+ }
+ }
+
+ /**
+ * Checks whether this event is a well-formed key verification event.
+ * This only does checks that don't rely on the current state of a potentially already channel
+ * so we can prevent channels being created by invalid events.
+ * `handleEvent` can do more checks and choose to ignore invalid events.
+ * @param event - the event to validate
+ * @param client - the client to get the current user and device id from
+ * @returns whether the event is valid and should be passed to handleEvent
+ */
+ public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
+ const txnId = InRoomChannel.getTransactionId(event);
+ if (typeof txnId !== "string" || txnId.length === 0) {
+ return false;
+ }
+ const type = InRoomChannel.getEventType(event);
+ const content = event.getContent();
+
+ // from here on we're fairly sure that this is supposed to be
+ // part of a verification request, so be noisy when rejecting something
+ if (type === REQUEST_TYPE) {
+ if (!content || typeof content.to !== "string" || !content.to.length) {
+ logger.log("InRoomChannel: validateEvent: " + "no valid to " + (content && content.to));
+ return false;
+ }
+
+ // ignore requests that are not direct to or sent by the syncing user
+ if (!InRoomChannel.getOtherPartyUserId(event, client)) {
+ logger.log(
+ "InRoomChannel: validateEvent: " +
+ `not directed to or sent by me: ${event.getSender()}` +
+ `, ${content && content.to}`,
+ );
+ return false;
+ }
+ }
+
+ return VerificationRequest.validateEvent(type, event, client);
+ }
+
+ /**
+ * As m.key.verification.request events are as m.room.message events with the InRoomChannel
+ * to have a fallback message in non-supporting clients, we map the real event type
+ * to the symbolic one to keep things in unison with ToDeviceChannel
+ * @param event - the event to get the type of
+ * @returns the "symbolic" event type
+ */
+ public static getEventType(event: MatrixEvent): string {
+ const type = event.getType();
+ if (type === MESSAGE_TYPE) {
+ const content = event.getContent();
+ if (content) {
+ const { msgtype } = content;
+ if (msgtype === REQUEST_TYPE) {
+ return REQUEST_TYPE;
+ }
+ }
+ }
+ if (type && type !== REQUEST_TYPE) {
+ return type;
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Changes the state of the channel, request, and verifier in response to a key verification event.
+ * @param event - to handle
+ * @param request - the request to forward handling to
+ * @param isLiveEvent - whether this is an even received through sync or not
+ * @returns a promise that resolves when any requests as an answer to the passed-in event are sent.
+ */
+ public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise<void> {
+ // prevent processing the same event multiple times, as under
+ // some circumstances Room.timeline can get emitted twice for the same event
+ if (request.hasEventId(event.getId()!)) {
+ return;
+ }
+ const type = InRoomChannel.getEventType(event);
+ // do validations that need state (roomId, userId),
+ // ignore if invalid
+
+ if (event.getRoomId() !== this.roomId) {
+ return;
+ }
+ // set userId if not set already
+ if (!this.userId) {
+ const userId = InRoomChannel.getOtherPartyUserId(event, this.client);
+ if (userId) {
+ this.userId = userId;
+ }
+ }
+ // ignore events not sent by us or the other party
+ const ownUserId = this.client.getUserId();
+ const sender = event.getSender();
+ if (this.userId) {
+ if (sender !== ownUserId && sender !== this.userId) {
+ logger.log(`InRoomChannel: ignoring verification event from non-participating sender ${sender}`);
+ return;
+ }
+ }
+ if (!this.requestEventId) {
+ this.requestEventId = InRoomChannel.getTransactionId(event);
+ }
+
+ const isRemoteEcho = !!event.getUnsigned().transaction_id;
+ const isSentByUs = event.getSender() === this.client.getUserId();
+
+ return request.handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs);
+ }
+
+ /**
+ * Adds the transaction id (relation) back to a received event
+ * so it has the same format as returned by `completeContent` before sending.
+ * The relation can not appear on the event content because of encryption,
+ * relations are excluded from encryption.
+ * @param event - the received event
+ * @returns the content object with the relation added again
+ */
+ public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
+ // ensure m.related_to is included in e2ee rooms
+ // as the field is excluded from encryption
+ const content = Object.assign({}, event.getContent());
+ content[M_RELATES_TO] = event.getRelation()!;
+ return content;
+ }
+
+ /**
+ * Add all the fields to content needed for sending it over this channel.
+ * This is public so verification methods (SAS uses this) can get the exact
+ * content that will be sent independent of the used channel,
+ * as they need to calculate the hash of it.
+ * @param type - the event type
+ * @param content - the (incomplete) content
+ * @returns the complete content, as it will be sent.
+ */
+ public completeContent(type: string, content: Record<string, any>): Record<string, any> {
+ content = Object.assign({}, content);
+ if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
+ content.from_device = this.client.getDeviceId();
+ }
+ if (type === REQUEST_TYPE) {
+ // type is mapped to m.room.message in the send method
+ content = {
+ body:
+ this.client.getUserId() +
+ " is requesting to verify " +
+ "your key, but your client does not support in-chat key " +
+ "verification. You will need to use legacy key " +
+ "verification to verify keys.",
+ msgtype: REQUEST_TYPE,
+ to: this.userId,
+ from_device: content.from_device,
+ methods: content.methods,
+ };
+ } else {
+ content[M_RELATES_TO] = {
+ rel_type: M_REFERENCE,
+ event_id: this.transactionId,
+ };
+ }
+ return content;
+ }
+
+ /**
+ * Send an event over the channel with the content not having gone through `completeContent`.
+ * @param type - the event type
+ * @param uncompletedContent - the (incomplete) content
+ * @returns the promise of the request
+ */
+ public send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
+ const content = this.completeContent(type, uncompletedContent);
+ return this.sendCompleted(type, content);
+ }
+
+ /**
+ * Send an event over the channel with the content having gone through `completeContent` already.
+ * @param type - the event type
+ * @returns the promise of the request
+ */
+ public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
+ let sendType = type;
+ if (type === REQUEST_TYPE) {
+ sendType = MESSAGE_TYPE;
+ }
+ const response = await this.client.sendEvent(this.roomId, sendType, content);
+ if (type === REQUEST_TYPE) {
+ this.requestEventId = response.event_id;
+ }
+ }
+}
+
+export class InRoomRequests implements IRequestsMap {
+ private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
+
+ public getRequest(event: MatrixEvent): VerificationRequest | undefined {
+ const roomId = event.getRoomId()!;
+ const txnId = InRoomChannel.getTransactionId(event)!;
+ return this.getRequestByTxnId(roomId, txnId);
+ }
+
+ public getRequestByChannel(channel: InRoomChannel): VerificationRequest | undefined {
+ return this.getRequestByTxnId(channel.roomId, channel.transactionId!);
+ }
+
+ private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest | undefined {
+ const requestsByTxnId = this.requestsByRoomId.get(roomId);
+ if (requestsByTxnId) {
+ return requestsByTxnId.get(txnId);
+ }
+ }
+
+ public setRequest(event: MatrixEvent, request: VerificationRequest): void {
+ this.doSetRequest(event.getRoomId()!, InRoomChannel.getTransactionId(event)!, request);
+ }
+
+ public setRequestByChannel(channel: IVerificationChannel, request: VerificationRequest): void {
+ this.doSetRequest(channel.roomId!, channel.transactionId!, request);
+ }
+
+ private doSetRequest(roomId: string, txnId: string, request: VerificationRequest): void {
+ let requestsByTxnId = this.requestsByRoomId.get(roomId);
+ if (!requestsByTxnId) {
+ requestsByTxnId = new Map();
+ this.requestsByRoomId.set(roomId, requestsByTxnId);
+ }
+ requestsByTxnId.set(txnId, request);
+ }
+
+ public removeRequest(event: MatrixEvent): void {
+ const roomId = event.getRoomId()!;
+ const requestsByTxnId = this.requestsByRoomId.get(roomId);
+ if (requestsByTxnId) {
+ requestsByTxnId.delete(InRoomChannel.getTransactionId(event)!);
+ if (requestsByTxnId.size === 0) {
+ this.requestsByRoomId.delete(roomId);
+ }
+ }
+ }
+
+ public findRequestInProgress(roomId: string): VerificationRequest | undefined {
+ const requestsByTxnId = this.requestsByRoomId.get(roomId);
+ if (requestsByTxnId) {
+ for (const request of requestsByTxnId.values()) {
+ if (request.pending) {
+ return request;
+ }
+ }
+ }
+ }
+}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts
new file mode 100644
index 0000000..d51b85a
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel.ts
@@ -0,0 +1,354 @@
+/*
+Copyright 2018 New Vector Ltd
+Copyright 2019 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 { randomString } from "../../../randomstring";
+import { logger } from "../../../logger";
+import {
+ CANCEL_TYPE,
+ PHASE_STARTED,
+ PHASE_READY,
+ REQUEST_TYPE,
+ READY_TYPE,
+ START_TYPE,
+ VerificationRequest,
+} from "./VerificationRequest";
+import { errorFromEvent, newUnexpectedMessageError } from "../Error";
+import { MatrixEvent } from "../../../models/event";
+import { IVerificationChannel } from "./Channel";
+import { MatrixClient } from "../../../client";
+import { IRequestsMap } from "../..";
+
+export type Request = VerificationRequest<ToDeviceChannel>;
+
+/**
+ * A key verification channel that sends verification events over to_device messages.
+ * Generates its own transaction ids.
+ */
+export class ToDeviceChannel implements IVerificationChannel {
+ public request?: VerificationRequest;
+
+ // userId and devices of user we're about to verify
+ public constructor(
+ private readonly client: MatrixClient,
+ public readonly userId: string,
+ private readonly devices: string[],
+ public transactionId?: string,
+ public deviceId?: string,
+ ) {}
+
+ public isToDevices(devices: string[]): boolean {
+ if (devices.length === this.devices.length) {
+ for (const device of devices) {
+ if (!this.devices.includes(device)) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static getEventType(event: MatrixEvent): string {
+ return event.getType();
+ }
+
+ /**
+ * Extract the transaction id used by a given key verification event, if any
+ * @param event - the event
+ * @returns the transaction id
+ */
+ public static getTransactionId(event: MatrixEvent): string {
+ const content = event.getContent();
+ return content && content.transaction_id;
+ }
+
+ /**
+ * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
+ * @param type - the event type to check
+ * @returns boolean flag
+ */
+ public static canCreateRequest(type: string): boolean {
+ return type === REQUEST_TYPE || type === START_TYPE;
+ }
+
+ public canCreateRequest(type: string): boolean {
+ return ToDeviceChannel.canCreateRequest(type);
+ }
+
+ /**
+ * Checks whether this event is a well-formed key verification event.
+ * This only does checks that don't rely on the current state of a potentially already channel
+ * so we can prevent channels being created by invalid events.
+ * `handleEvent` can do more checks and choose to ignore invalid events.
+ * @param event - the event to validate
+ * @param client - the client to get the current user and device id from
+ * @returns whether the event is valid and should be passed to handleEvent
+ */
+ public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean {
+ if (event.isCancelled()) {
+ logger.warn("Ignoring flagged verification request from " + event.getSender());
+ return false;
+ }
+ const content = event.getContent();
+ if (!content) {
+ logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
+ return false;
+ }
+
+ if (!content.transaction_id) {
+ logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
+ return false;
+ }
+
+ const type = event.getType();
+
+ if (type === REQUEST_TYPE) {
+ if (!Number.isFinite(content.timestamp)) {
+ logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
+ return false;
+ }
+ if (event.getSender() === client.getUserId() && content.from_device == client.getDeviceId()) {
+ // ignore requests from ourselves, because it doesn't make sense for a
+ // device to verify itself
+ logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
+ return false;
+ }
+ }
+
+ return VerificationRequest.validateEvent(type, event, client);
+ }
+
+ /**
+ * @param event - the event to get the timestamp of
+ * @returns the timestamp when the event was sent
+ */
+ public getTimestamp(event: MatrixEvent): number {
+ const content = event.getContent();
+ return content && content.timestamp;
+ }
+
+ /**
+ * Changes the state of the channel, request, and verifier in response to a key verification event.
+ * @param event - to handle
+ * @param request - the request to forward handling to
+ * @param isLiveEvent - whether this is an even received through sync or not
+ * @returns a promise that resolves when any requests as an answer to the passed-in event are sent.
+ */
+ public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise<void> {
+ const type = event.getType();
+ const content = event.getContent();
+ if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
+ if (!this.transactionId) {
+ this.transactionId = content.transaction_id;
+ }
+ const deviceId = content.from_device;
+ // adopt deviceId if not set before and valid
+ if (!this.deviceId && this.devices.includes(deviceId)) {
+ this.deviceId = deviceId;
+ }
+ // if no device id or different from adopted one, cancel with sender
+ if (!this.deviceId || this.deviceId !== deviceId) {
+ // also check that message came from the device we sent the request to earlier on
+ // and do send a cancel message to that device
+ // (but don't cancel the request for the device we should be talking to)
+ const cancelContent = this.completeContent(CANCEL_TYPE, errorFromEvent(newUnexpectedMessageError()));
+ return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
+ }
+ }
+ const wasStarted = request.phase === PHASE_STARTED || request.phase === PHASE_READY;
+
+ await request.handleEvent(event.getType(), event, isLiveEvent, false, false);
+
+ const isStarted = request.phase === PHASE_STARTED || request.phase === PHASE_READY;
+
+ const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
+ // the request has picked a ready or start event, tell the other devices about it
+ if (isAcceptingEvent && !wasStarted && isStarted && this.deviceId) {
+ const nonChosenDevices = this.devices.filter((d) => d !== this.deviceId && d !== this.client.getDeviceId());
+ if (nonChosenDevices.length) {
+ const message = this.completeContent(CANCEL_TYPE, {
+ code: "m.accepted",
+ reason: "Verification request accepted by another device",
+ });
+ await this.sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
+ }
+ }
+ }
+
+ /**
+ * See {@link InRoomChannel#completedContentFromEvent} for why this is needed.
+ * @param event - the received event
+ * @returns the content object
+ */
+ public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
+ return event.getContent();
+ }
+
+ /**
+ * Add all the fields to content needed for sending it over this channel.
+ * This is public so verification methods (SAS uses this) can get the exact
+ * content that will be sent independent of the used channel,
+ * as they need to calculate the hash of it.
+ * @param type - the event type
+ * @param content - the (incomplete) content
+ * @returns the complete content, as it will be sent.
+ */
+ public completeContent(type: string, content: Record<string, any>): Record<string, any> {
+ // make a copy
+ content = Object.assign({}, content);
+ if (this.transactionId) {
+ content.transaction_id = this.transactionId;
+ }
+ if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
+ content.from_device = this.client.getDeviceId();
+ }
+ if (type === REQUEST_TYPE) {
+ content.timestamp = Date.now();
+ }
+ return content;
+ }
+
+ /**
+ * Send an event over the channel with the content not having gone through `completeContent`.
+ * @param type - the event type
+ * @param uncompletedContent - the (incomplete) content
+ * @returns the promise of the request
+ */
+ public send(type: string, uncompletedContent: Record<string, any> = {}): Promise<void> {
+ // create transaction id when sending request
+ if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) {
+ this.transactionId = ToDeviceChannel.makeTransactionId();
+ }
+ const content = this.completeContent(type, uncompletedContent);
+ return this.sendCompleted(type, content);
+ }
+
+ /**
+ * Send an event over the channel with the content having gone through `completeContent` already.
+ * @param type - the event type
+ * @returns the promise of the request
+ */
+ public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
+ let result;
+ if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.deviceId)) {
+ result = await this.sendToDevices(type, content, this.devices);
+ } else {
+ result = await this.sendToDevices(type, content, [this.deviceId!]);
+ }
+ // the VerificationRequest state machine requires remote echos of the event
+ // the client sends itself, so we fake this for to_device messages
+ const remoteEchoEvent = new MatrixEvent({
+ sender: this.client.getUserId()!,
+ content,
+ type,
+ });
+ await this.request!.handleEvent(
+ type,
+ remoteEchoEvent,
+ /*isLiveEvent=*/ true,
+ /*isRemoteEcho=*/ true,
+ /*isSentByUs=*/ true,
+ );
+ return result;
+ }
+
+ private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
+ if (devices.length) {
+ const deviceMessages: Map<string, Record<string, any>> = new Map();
+ for (const deviceId of devices) {
+ deviceMessages.set(deviceId, content);
+ }
+
+ await this.client.sendToDevice(type, new Map([[this.userId, deviceMessages]]));
+ }
+ }
+
+ /**
+ * Allow Crypto module to create and know the transaction id before the .start event gets sent.
+ * @returns the transaction id
+ */
+ public static makeTransactionId(): string {
+ return randomString(32);
+ }
+}
+
+export class ToDeviceRequests implements IRequestsMap {
+ private requestsByUserId = new Map<string, Map<string, Request>>();
+
+ public getRequest(event: MatrixEvent): Request | undefined {
+ return this.getRequestBySenderAndTxnId(event.getSender()!, ToDeviceChannel.getTransactionId(event));
+ }
+
+ public getRequestByChannel(channel: ToDeviceChannel): Request | undefined {
+ return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId!);
+ }
+
+ public getRequestBySenderAndTxnId(sender: string, txnId: string): Request | undefined {
+ const requestsByTxnId = this.requestsByUserId.get(sender);
+ if (requestsByTxnId) {
+ return requestsByTxnId.get(txnId);
+ }
+ }
+
+ public setRequest(event: MatrixEvent, request: Request): void {
+ this.setRequestBySenderAndTxnId(event.getSender()!, ToDeviceChannel.getTransactionId(event), request);
+ }
+
+ public setRequestByChannel(channel: ToDeviceChannel, request: Request): void {
+ this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId!, request);
+ }
+
+ public setRequestBySenderAndTxnId(sender: string, txnId: string, request: Request): void {
+ let requestsByTxnId = this.requestsByUserId.get(sender);
+ if (!requestsByTxnId) {
+ requestsByTxnId = new Map();
+ this.requestsByUserId.set(sender, requestsByTxnId);
+ }
+ requestsByTxnId.set(txnId, request);
+ }
+
+ public removeRequest(event: MatrixEvent): void {
+ const userId = event.getSender()!;
+ const requestsByTxnId = this.requestsByUserId.get(userId);
+ if (requestsByTxnId) {
+ requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
+ if (requestsByTxnId.size === 0) {
+ this.requestsByUserId.delete(userId);
+ }
+ }
+ }
+
+ public findRequestInProgress(userId: string, devices: string[]): Request | undefined {
+ const requestsByTxnId = this.requestsByUserId.get(userId);
+ if (requestsByTxnId) {
+ for (const request of requestsByTxnId.values()) {
+ if (request.pending && request.channel.isToDevices(devices)) {
+ return request;
+ }
+ }
+ }
+ }
+
+ public getRequestsInProgress(userId: string): Request[] {
+ const requestsByTxnId = this.requestsByUserId.get(userId);
+ if (requestsByTxnId) {
+ return Array.from(requestsByTxnId.values()).filter((r) => r.pending);
+ }
+ return [];
+ }
+}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts
new file mode 100644
index 0000000..617432e
--- /dev/null
+++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/crypto/verification/request/VerificationRequest.ts
@@ -0,0 +1,926 @@
+/*
+Copyright 2018 - 2021 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 { logger } from "../../../logger";
+import { errorFactory, errorFromEvent, newUnexpectedMessageError, newUnknownMethodError } from "../Error";
+import { QRCodeData, SCAN_QR_CODE_METHOD } from "../QRCode";
+import { IVerificationChannel } from "./Channel";
+import { MatrixClient } from "../../../client";
+import { MatrixEvent } from "../../../models/event";
+import { EventType } from "../../../@types/event";
+import { VerificationBase } from "../Base";
+import { VerificationMethod } from "../../index";
+import { TypedEventEmitter } from "../../../models/typed-event-emitter";
+
+// How long after the event's timestamp that the request times out
+const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
+
+// How long after we receive the event that the request times out
+const TIMEOUT_FROM_EVENT_RECEIPT = 2 * 60 * 1000; // 2 minutes
+
+// to avoid almost expired verification notifications
+// from showing a notification and almost immediately
+// disappearing, also ignore verification requests that
+// are this amount of time away from expiring.
+const VERIFICATION_REQUEST_MARGIN = 3 * 1000; // 3 seconds
+
+export const EVENT_PREFIX = "m.key.verification.";
+export const REQUEST_TYPE = EVENT_PREFIX + "request";
+export const START_TYPE = EVENT_PREFIX + "start";
+export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
+export const DONE_TYPE = EVENT_PREFIX + "done";
+export const READY_TYPE = EVENT_PREFIX + "ready";
+
+export enum Phase {
+ Unsent = 1,
+ Requested,
+ Ready,
+ Started,
+ Cancelled,
+ Done,
+}
+
+// Legacy export fields
+export const PHASE_UNSENT = Phase.Unsent;
+export const PHASE_REQUESTED = Phase.Requested;
+export const PHASE_READY = Phase.Ready;
+export const PHASE_STARTED = Phase.Started;
+export const PHASE_CANCELLED = Phase.Cancelled;
+export const PHASE_DONE = Phase.Done;
+
+interface ITargetDevice {
+ userId?: string;
+ deviceId?: string;
+}
+
+interface ITransition {
+ phase: Phase;
+ event?: MatrixEvent;
+}
+
+export enum VerificationRequestEvent {
+ Change = "change",
+}
+
+type EventHandlerMap = {
+ /**
+ * Fires whenever the state of the request object has changed.
+ */
+ [VerificationRequestEvent.Change]: () => void;
+};
+
+/**
+ * State machine for verification requests.
+ * Things that differ based on what channel is used to
+ * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
+ */
+export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends TypedEventEmitter<
+ VerificationRequestEvent,
+ EventHandlerMap
+> {
+ private eventsByUs = new Map<string, MatrixEvent>();
+ private eventsByThem = new Map<string, MatrixEvent>();
+ private _observeOnly = false;
+ private timeoutTimer: ReturnType<typeof setTimeout> | null = null;
+ private _accepting = false;
+ private _declining = false;
+ private verifierHasFinished = false;
+ private _cancelled = false;
+ private _chosenMethod: VerificationMethod | null = null;
+ // we keep a copy of the QR Code data (including other user master key) around
+ // for QR reciprocate verification, to protect against
+ // cross-signing identity reset between the .ready and .start event
+ // and signing the wrong key after .start
+ private _qrCodeData: QRCodeData | null = null;
+
+ // The timestamp when we received the request event from the other side
+ private requestReceivedAt: number | null = null;
+
+ private commonMethods: VerificationMethod[] = [];
+ private _phase!: Phase;
+ public _cancellingUserId?: string; // Used in tests only
+ private _verifier?: VerificationBase<any, any>;
+
+ public constructor(
+ public readonly channel: C,
+ private readonly verificationMethods: Map<VerificationMethod, typeof VerificationBase>,
+ private readonly client: MatrixClient,
+ ) {
+ super();
+ this.channel.request = this;
+ this.setPhase(PHASE_UNSENT, false);
+ }
+
+ /**
+ * Stateless validation logic not specific to the channel.
+ * Invoked by the same static method in either channel.
+ * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel.
+ * @param event - the event to validate. Don't call getType() on it but use the `type` parameter instead.
+ * @param client - the client to get the current user and device id from
+ * @returns whether the event is valid and should be passed to handleEvent
+ */
+ public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean {
+ const content = event.getContent();
+
+ if (!type || !type.startsWith(EVENT_PREFIX)) {
+ return false;
+ }
+
+ // from here on we're fairly sure that this is supposed to be
+ // part of a verification request, so be noisy when rejecting something
+ if (!content) {
+ logger.log("VerificationRequest: validateEvent: no content");
+ return false;
+ }
+
+ if (type === REQUEST_TYPE || type === READY_TYPE) {
+ if (!Array.isArray(content.methods)) {
+ logger.log("VerificationRequest: validateEvent: " + "fail because methods");
+ return false;
+ }
+ }
+
+ if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
+ if (typeof content.from_device !== "string" || content.from_device.length === 0) {
+ logger.log("VerificationRequest: validateEvent: " + "fail because from_device");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public get invalid(): boolean {
+ return this.phase === PHASE_UNSENT;
+ }
+
+ /** returns whether the phase is PHASE_REQUESTED */
+ public get requested(): boolean {
+ return this.phase === PHASE_REQUESTED;
+ }
+
+ /** returns whether the phase is PHASE_CANCELLED */
+ public get cancelled(): boolean {
+ return this.phase === PHASE_CANCELLED;
+ }
+
+ /** returns whether the phase is PHASE_READY */
+ public get ready(): boolean {
+ return this.phase === PHASE_READY;
+ }
+
+ /** returns whether the phase is PHASE_STARTED */
+ public get started(): boolean {
+ return this.phase === PHASE_STARTED;
+ }
+
+ /** returns whether the phase is PHASE_DONE */
+ public get done(): boolean {
+ return this.phase === PHASE_DONE;
+ }
+
+ /** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
+ public get methods(): VerificationMethod[] {
+ return this.commonMethods;
+ }
+
+ /** the method picked in the .start event */
+ public get chosenMethod(): VerificationMethod | null {
+ return this._chosenMethod;
+ }
+
+ public calculateEventTimeout(event: MatrixEvent): number {
+ let effectiveExpiresAt = this.channel.getTimestamp(event) + TIMEOUT_FROM_EVENT_TS;
+
+ if (this.requestReceivedAt && !this.initiatedByMe && this.phase <= PHASE_REQUESTED) {
+ const expiresAtByReceipt = this.requestReceivedAt + TIMEOUT_FROM_EVENT_RECEIPT;
+ effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
+ }
+
+ return Math.max(0, effectiveExpiresAt - Date.now());
+ }
+
+ /** The current remaining amount of ms before the request should be automatically cancelled */
+ public get timeout(): number {
+ const requestEvent = this.getEventByEither(REQUEST_TYPE);
+ if (requestEvent) {
+ return this.calculateEventTimeout(requestEvent);
+ }
+ return 0;
+ }
+
+ /**
+ * The key verification request event.
+ * @returns The request event, or falsey if not found.
+ */
+ public get requestEvent(): MatrixEvent | undefined {
+ return this.getEventByEither(REQUEST_TYPE);
+ }
+
+ /** current phase of the request. Some properties might only be defined in a current phase. */
+ public get phase(): Phase {
+ return this._phase;
+ }
+
+ /** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
+ public get verifier(): VerificationBase<any, any> | undefined {
+ return this._verifier;
+ }
+
+ public get canAccept(): boolean {
+ return this.phase < PHASE_READY && !this._accepting && !this._declining;
+ }
+
+ public get accepting(): boolean {
+ return this._accepting;
+ }
+
+ public get declining(): boolean {
+ return this._declining;
+ }
+
+ /** whether this request has sent it's initial event and needs more events to complete */
+ public get pending(): boolean {
+ return !this.observeOnly && this._phase !== PHASE_DONE && this._phase !== PHASE_CANCELLED;
+ }
+
+ /** Only set after a .ready if the other party can scan a QR code */
+ public get qrCodeData(): QRCodeData | null {
+ return this._qrCodeData;
+ }
+
+ /** 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
+ * @param force - to check even if the phase is not ready or started yet, internal usage
+ * @returns whether or not the other party said the supported the method */
+ public otherPartySupportsMethod(method: string, force = false): boolean {
+ if (!force && !this.ready && !this.started) {
+ return false;
+ }
+ const theirMethodEvent = this.eventsByThem.get(REQUEST_TYPE) || this.eventsByThem.get(READY_TYPE);
+ if (!theirMethodEvent) {
+ // if we started straight away with .start event,
+ // we are assuming that the other side will support the
+ // chosen method, so return true for that.
+ if (this.started && this.initiatedByMe) {
+ const myStartEvent = this.eventsByUs.get(START_TYPE);
+ const content = myStartEvent && myStartEvent.getContent();
+ const myStartMethod = content && content.method;
+ return method == myStartMethod;
+ }
+ return false;
+ }
+ const content = theirMethodEvent.getContent();
+ if (!content) {
+ return false;
+ }
+ const { methods } = content;
+ if (!Array.isArray(methods)) {
+ return false;
+ }
+
+ return methods.includes(method);
+ }
+
+ /** Whether this request was initiated by the syncing user.
+ * For InRoomChannel, this is who sent the .request event.
+ * For ToDeviceChannel, this is who sent the .start event
+ */
+ public get initiatedByMe(): boolean {
+ // event created by us but no remote echo has been received yet
+ const noEventsYet = this.eventsByUs.size + this.eventsByThem.size === 0;
+ if (this._phase === PHASE_UNSENT && noEventsYet) {
+ return true;
+ }
+ const hasMyRequest = this.eventsByUs.has(REQUEST_TYPE);
+ const hasTheirRequest = this.eventsByThem.has(REQUEST_TYPE);
+ if (hasMyRequest && !hasTheirRequest) {
+ return true;
+ }
+ if (!hasMyRequest && hasTheirRequest) {
+ return false;
+ }
+ const hasMyStart = this.eventsByUs.has(START_TYPE);
+ const hasTheirStart = this.eventsByThem.has(START_TYPE);
+ if (hasMyStart && !hasTheirStart) {
+ return true;
+ }
+ return false;
+ }
+
+ /** The id of the user that initiated the request */
+ public get requestingUserId(): string {
+ if (this.initiatedByMe) {
+ return this.client.getUserId()!;
+ } else {
+ return this.otherUserId;
+ }
+ }
+
+ /** The id of the user that (will) receive(d) the request */
+ public get receivingUserId(): string {
+ if (this.initiatedByMe) {
+ return this.otherUserId;
+ } else {
+ return this.client.getUserId()!;
+ }
+ }
+
+ /** The user id of the other party in this request */
+ public get otherUserId(): string {
+ return this.channel.userId!;
+ }
+
+ public get isSelfVerification(): boolean {
+ return this.client.getUserId() === this.otherUserId;
+ }
+
+ /**
+ * The id of the user that cancelled the request,
+ * only defined when phase is PHASE_CANCELLED
+ */
+ public get cancellingUserId(): string | undefined {
+ const myCancel = this.eventsByUs.get(CANCEL_TYPE);
+ const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
+
+ if (myCancel && (!theirCancel || myCancel.getId()! < theirCancel.getId()!)) {
+ return myCancel.getSender();
+ }
+ if (theirCancel) {
+ return theirCancel.getSender();
+ }
+ return undefined;
+ }
+
+ /**
+ * The cancellation code e.g m.user which is responsible for cancelling this verification
+ */
+ public get cancellationCode(): string {
+ const ev = this.getEventByEither(CANCEL_TYPE);
+ return ev ? ev.getContent().code : null;
+ }
+
+ public get observeOnly(): boolean {
+ return this._observeOnly;
+ }
+
+ /**
+ * Gets which device the verification should be started with
+ * given the events sent so far in the verification. This is the
+ * same algorithm used to determine which device to send the
+ * verification to when no specific device is specified.
+ * @returns The device information
+ */
+ public get targetDevice(): ITargetDevice {
+ const theirFirstEvent =
+ this.eventsByThem.get(REQUEST_TYPE) ||
+ this.eventsByThem.get(READY_TYPE) ||
+ this.eventsByThem.get(START_TYPE);
+ const theirFirstContent = theirFirstEvent?.getContent();
+ const fromDevice = theirFirstContent?.from_device;
+ return {
+ userId: this.otherUserId,
+ deviceId: fromDevice,
+ };
+ }
+
+ /* Start the key verification, creating a verifier and sending a .start event.
+ * 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.userId the id of the user to direct this request to
+ * @param targetDevice.deviceId the id of the device to direct this request to
+ * @returns the verifier of the given method
+ */
+ public beginKeyVerification(
+ method: VerificationMethod,
+ targetDevice: ITargetDevice | null = null,
+ ): VerificationBase<any, any> {
+ // need to allow also when unsent in case of to_device
+ if (!this.observeOnly && !this._verifier) {
+ const validStartPhase =
+ this.phase === PHASE_REQUESTED ||
+ this.phase === PHASE_READY ||
+ (this.phase === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE));
+ if (validStartPhase) {
+ // when called on a request that was initiated with .request event
+ // check the method is supported by both sides
+ if (this.commonMethods.length && !this.commonMethods.includes(method)) {
+ throw newUnknownMethodError();
+ }
+ this._verifier = this.createVerifier(method, null, targetDevice);
+ if (!this._verifier) {
+ throw newUnknownMethodError();
+ }
+ this._chosenMethod = method;
+ }
+ }
+ return this._verifier!;
+ }
+
+ /**
+ * sends the initial .request event.
+ * @returns resolves when the event has been sent.
+ */
+ public async sendRequest(): Promise<void> {
+ if (!this.observeOnly && this._phase === PHASE_UNSENT) {
+ const methods = [...this.verificationMethods.keys()];
+ await this.channel.send(REQUEST_TYPE, { methods });
+ }
+ }
+
+ /**
+ * Cancels the request, sending a cancellation to the other party
+ * @param reason - the error reason to send the cancellation with
+ * @param code - the error code to send the cancellation with
+ * @returns resolves when the event has been sent.
+ */
+ public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise<void> {
+ if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
+ this._declining = true;
+ this.emit(VerificationRequestEvent.Change);
+ if (this._verifier) {
+ return this._verifier.cancel(errorFactory(code, reason)());
+ } else {
+ this._cancellingUserId = this.client.getUserId()!;
+ await this.channel.send(CANCEL_TYPE, { code, reason });
+ }
+ }
+ }
+
+ /**
+ * Accepts the request, sending a .ready event to the other party
+ * @returns resolves when the event has been sent.
+ */
+ public async accept(): Promise<void> {
+ if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
+ const methods = [...this.verificationMethods.keys()];
+ this._accepting = true;
+ this.emit(VerificationRequestEvent.Change);
+ await this.channel.send(READY_TYPE, { methods });
+ }
+ }
+
+ /**
+ * Can be used to listen for state changes until the callback returns true.
+ * @param fn - callback to evaluate whether the request is in the desired state.
+ * Takes the request as an argument.
+ * @returns that resolves once the callback returns true
+ * @throws Error when the request is cancelled
+ */
+ public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
+ return new Promise((resolve, reject) => {
+ const check = (): boolean => {
+ let handled = false;
+ if (fn(this)) {
+ resolve(this);
+ handled = true;
+ } else if (this.cancelled) {
+ reject(new Error("cancelled"));
+ handled = true;
+ }
+ if (handled) {
+ this.off(VerificationRequestEvent.Change, check);
+ }
+ return handled;
+ };
+ if (!check()) {
+ this.on(VerificationRequestEvent.Change, check);
+ }
+ });
+ }
+
+ private setPhase(phase: Phase, notify = true): void {
+ this._phase = phase;
+ if (notify) {
+ this.emit(VerificationRequestEvent.Change);
+ }
+ }
+
+ private getEventByEither(type: string): MatrixEvent | undefined {
+ return this.eventsByThem.get(type) || this.eventsByUs.get(type);
+ }
+
+ private getEventBy(type: string, byThem = false): MatrixEvent | undefined {
+ if (byThem) {
+ return this.eventsByThem.get(type);
+ } else {
+ return this.eventsByUs.get(type);
+ }
+ }
+
+ private calculatePhaseTransitions(): ITransition[] {
+ const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
+ const phase = (): Phase => transitions[transitions.length - 1].phase;
+
+ // always pass by .request first to be sure channel.userId has been set
+ const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
+ const requestEvent = this.getEventBy(REQUEST_TYPE, hasRequestByThem);
+ if (requestEvent) {
+ transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
+ }
+
+ const readyEvent = requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
+ if (readyEvent && phase() === PHASE_REQUESTED) {
+ transitions.push({ phase: PHASE_READY, event: readyEvent });
+ }
+
+ let startEvent: MatrixEvent | undefined;
+ if (readyEvent || !requestEvent) {
+ const theirStartEvent = this.eventsByThem.get(START_TYPE);
+ const ourStartEvent = this.eventsByUs.get(START_TYPE);
+ // any party can send .start after a .ready or unsent
+ if (theirStartEvent && ourStartEvent) {
+ startEvent =
+ theirStartEvent.getSender()! < ourStartEvent.getSender()! ? theirStartEvent : ourStartEvent;
+ } else {
+ startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
+ }
+ } else {
+ startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
+ }
+ if (startEvent) {
+ const fromRequestPhase =
+ phase() === PHASE_REQUESTED && requestEvent?.getSender() !== startEvent.getSender();
+ const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
+ if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
+ transitions.push({ phase: PHASE_STARTED, event: startEvent });
+ }
+ }
+
+ const ourDoneEvent = this.eventsByUs.get(DONE_TYPE);
+ if (this.verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
+ transitions.push({ phase: PHASE_DONE });
+ }
+
+ const cancelEvent = this.getEventByEither(CANCEL_TYPE);
+ if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
+ transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent });
+ return transitions;
+ }
+
+ return transitions;
+ }
+
+ private transitionToPhase(transition: ITransition): void {
+ const { phase, event } = transition;
+ // get common methods
+ if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
+ if (!this.wasSentByOwnDevice(event)) {
+ const content = event!.getContent<{
+ methods: string[];
+ }>();
+ this.commonMethods = content.methods.filter((m) => this.verificationMethods.has(m));
+ }
+ }
+ // detect if we're not a party in the request, and we should just observe
+ if (!this.observeOnly) {
+ // if requested or accepted by one of my other devices
+ if (phase === PHASE_REQUESTED || phase === PHASE_STARTED || phase === PHASE_READY) {
+ if (
+ this.channel.receiveStartFromOtherDevices &&
+ this.wasSentByOwnUser(event) &&
+ !this.wasSentByOwnDevice(event)
+ ) {
+ this._observeOnly = true;
+ }
+ }
+ }
+ // create verifier
+ if (phase === PHASE_STARTED) {
+ const { method } = event!.getContent();
+ if (!this._verifier && !this.observeOnly) {
+ this._verifier = this.createVerifier(method, event);
+ if (!this._verifier) {
+ this.cancel({
+ code: "m.unknown_method",
+ reason: `Unknown method: ${method}`,
+ });
+ } else {
+ this._chosenMethod = method;
+ }
+ }
+ }
+ }
+
+ private applyPhaseTransitions(): ITransition[] {
+ const transitions = this.calculatePhaseTransitions();
+ const existingIdx = transitions.findIndex((t) => t.phase === this.phase);
+ // trim off phases we already went through, if any
+ const newTransitions = transitions.slice(existingIdx + 1);
+ // transition to all new phases
+ for (const transition of newTransitions) {
+ this.transitionToPhase(transition);
+ }
+ return newTransitions;
+ }
+
+ private isWinningStartRace(newEvent: MatrixEvent): boolean {
+ if (newEvent.getType() !== START_TYPE) {
+ return false;
+ }
+ const oldEvent = this._verifier!.startEvent;
+
+ let oldRaceIdentifier;
+ if (this.isSelfVerification) {
+ // if the verifier does not have a startEvent,
+ // it is because it's still sending and we are on the initator side
+ // we know we are sending a .start event because we already
+ // have a verifier (checked in calling method)
+ if (oldEvent) {
+ const oldContent = oldEvent.getContent();
+ oldRaceIdentifier = oldContent && oldContent.from_device;
+ } else {
+ oldRaceIdentifier = this.client.getDeviceId();
+ }
+ } else {
+ if (oldEvent) {
+ oldRaceIdentifier = oldEvent.getSender();
+ } else {
+ oldRaceIdentifier = this.client.getUserId();
+ }
+ }
+
+ let newRaceIdentifier;
+ if (this.isSelfVerification) {
+ const newContent = newEvent.getContent();
+ newRaceIdentifier = newContent && newContent.from_device;
+ } else {
+ newRaceIdentifier = newEvent.getSender();
+ }
+ return newRaceIdentifier < oldRaceIdentifier;
+ }
+
+ public hasEventId(eventId: string): boolean {
+ for (const event of this.eventsByUs.values()) {
+ if (event.getId() === eventId) {
+ return true;
+ }
+ }
+ for (const event of this.eventsByThem.values()) {
+ if (event.getId() === eventId) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Changes the state of the request and verifier in response to a key verification event.
+ * @param type - the "symbolic" event type, as returned by the `getEventType` function on the channel.
+ * @param event - the event to handle. Don't call getType() on it but use the `type` parameter instead.
+ * @param isLiveEvent - whether this is an even received through sync or not
+ * @param isRemoteEcho - whether this is the remote echo of an event sent by the same device
+ * @param isSentByUs - whether this event is sent by a party that can accept and/or observe the request like one of our peers.
+ * For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
+ * @returns a promise that resolves when any requests as an answer to the passed-in event are sent.
+ */
+ public async handleEvent(
+ type: string,
+ event: MatrixEvent,
+ isLiveEvent: boolean,
+ isRemoteEcho: boolean,
+ isSentByUs: boolean,
+ ): Promise<void> {
+ // if reached phase cancelled or done, ignore anything else that comes
+ if (this.done || this.cancelled) {
+ return;
+ }
+ const wasObserveOnly = this._observeOnly;
+
+ this.adjustObserveOnly(event, isLiveEvent);
+
+ if (!this.observeOnly && !isRemoteEcho) {
+ if (await this.cancelOnError(type, event)) {
+ return;
+ }
+ }
+
+ // This assumes verification won't need to send an event with
+ // the same type for the same party twice.
+ // This is true for QR and SAS verification, and was
+ // added here to prevent verification getting cancelled
+ // when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
+ const isDuplicateEvent = isSentByUs ? this.eventsByUs.has(type) : this.eventsByThem.has(type);
+ if (isDuplicateEvent) {
+ return;
+ }
+
+ const oldPhase = this.phase;
+ this.addEvent(type, event, isSentByUs);
+
+ // this will create if needed the verifier so needs to happen before calling it
+ const newTransitions = this.applyPhaseTransitions();
+ try {
+ // only pass events from the other side to the verifier,
+ // no remote echos of our own events
+ if (this._verifier && !this.observeOnly) {
+ const newEventWinsRace = this.isWinningStartRace(event);
+ if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
+ this._verifier.switchStartEvent(event);
+ } else if (!isRemoteEcho) {
+ if (type === CANCEL_TYPE || this._verifier.events?.includes(type)) {
+ this._verifier.handleEvent(event);
+ }
+ }
+ }
+
+ if (newTransitions.length) {
+ // create QRCodeData if the other side can scan
+ // important this happens before emitting a phase change,
+ // so listeners can rely on it being there already
+ // We only do this for live events because it is important that
+ // we sign the keys that were in the QR code, and not the keys
+ // we happen to have at some later point in time.
+ if (isLiveEvent && newTransitions.some((t) => t.phase === PHASE_READY)) {
+ const shouldGenerateQrCode = this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
+ if (shouldGenerateQrCode) {
+ this._qrCodeData = await QRCodeData.create(this, this.client);
+ }
+ }
+
+ const lastTransition = newTransitions[newTransitions.length - 1];
+ const { phase } = lastTransition;
+
+ this.setupTimeout(phase);
+ // set phase as last thing as this emits the "change" event
+ this.setPhase(phase);
+ } else if (this._observeOnly !== wasObserveOnly) {
+ this.emit(VerificationRequestEvent.Change);
+ }
+ } finally {
+ // log events we processed so we can see from rageshakes what events were added to a request
+ logger.log(
+ `Verification request ${this.channel.transactionId}: ` +
+ `${type} event with id:${event.getId()}, ` +
+ `content:${JSON.stringify(event.getContent())} ` +
+ `deviceId:${this.channel.deviceId}, ` +
+ `sender:${event.getSender()}, isSentByUs:${isSentByUs}, ` +
+ `isLiveEvent:${isLiveEvent}, isRemoteEcho:${isRemoteEcho}, ` +
+ `phase:${oldPhase}=>${this.phase}, ` +
+ `observeOnly:${wasObserveOnly}=>${this._observeOnly}`,
+ );
+ }
+ }
+
+ private setupTimeout(phase: Phase): void {
+ const shouldTimeout = !this.timeoutTimer && !this.observeOnly && phase === PHASE_REQUESTED;
+
+ if (shouldTimeout) {
+ this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout);
+ }
+ if (this.timeoutTimer) {
+ const shouldClear =
+ phase === PHASE_STARTED || phase === PHASE_READY || phase === PHASE_DONE || phase === PHASE_CANCELLED;
+ if (shouldClear) {
+ clearTimeout(this.timeoutTimer);
+ this.timeoutTimer = null;
+ }
+ }
+ }
+
+ private cancelOnTimeout = async (): Promise<void> => {
+ try {
+ if (this.initiatedByMe) {
+ await this.cancel({
+ reason: "Other party didn't accept in time",
+ code: "m.timeout",
+ });
+ } else {
+ await this.cancel({
+ reason: "User didn't accept in time",
+ code: "m.timeout",
+ });
+ }
+ } catch (err) {
+ logger.error("Error while cancelling verification request", err);
+ }
+ };
+
+ private async cancelOnError(type: string, event: MatrixEvent): Promise<boolean> {
+ if (type === START_TYPE) {
+ const method = event.getContent().method;
+ if (!this.verificationMethods.has(method)) {
+ await this.cancel(errorFromEvent(newUnknownMethodError()));
+ return true;
+ }
+ }
+
+ const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
+ const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED && this.phase !== PHASE_STARTED;
+ // only if phase has passed from PHASE_UNSENT should we cancel, because events
+ // are allowed to come in in any order (at least with InRoomChannel). So we only know
+ // we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED.
+ // Before that, we could be looking at somebody else's verification request and we just
+ // happen to be in the room
+ if (this.phase !== PHASE_UNSENT && (isUnexpectedRequest || isUnexpectedReady)) {
+ logger.warn(`Cancelling, unexpected ${type} verification ` + `event from ${event.getSender()}`);
+ const reason = `Unexpected ${type} event in phase ${this.phase}`;
+ await this.cancel(errorFromEvent(newUnexpectedMessageError({ reason })));
+ return true;
+ }
+ return false;
+ }
+
+ private adjustObserveOnly(event: MatrixEvent, isLiveEvent = false): void {
+ // don't send out events for historical requests
+ if (!isLiveEvent) {
+ this._observeOnly = true;
+ }
+ if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) {
+ this._observeOnly = true;
+ }
+ }
+
+ private addEvent(type: string, event: MatrixEvent, isSentByUs = false): void {
+ if (isSentByUs) {
+ this.eventsByUs.set(type, event);
+ } else {
+ this.eventsByThem.set(type, event);
+ }
+
+ // once we know the userId of the other party (from the .request event)
+ // see if any event by anyone else crept into this.eventsByThem
+ if (type === REQUEST_TYPE) {
+ for (const [type, event] of this.eventsByThem.entries()) {
+ if (event.getSender() !== this.otherUserId) {
+ this.eventsByThem.delete(type);
+ }
+ }
+ // also remember when we received the request event
+ this.requestReceivedAt = Date.now();
+ }
+ }
+
+ private createVerifier(
+ method: VerificationMethod,
+ startEvent: MatrixEvent | null = null,
+ targetDevice: ITargetDevice | null = null,
+ ): VerificationBase<any, any> | undefined {
+ if (!targetDevice) {
+ targetDevice = this.targetDevice;
+ }
+ const { userId, deviceId } = targetDevice;
+
+ const VerifierCtor = this.verificationMethods.get(method);
+ if (!VerifierCtor) {
+ logger.warn("could not find verifier constructor for method", method);
+ return;
+ }
+ return new VerifierCtor(this.channel, this.client, userId!, deviceId!, startEvent, this);
+ }
+
+ private wasSentByOwnUser(event?: MatrixEvent): boolean {
+ return event?.getSender() === this.client.getUserId();
+ }
+
+ // only for .request, .ready or .start
+ private wasSentByOwnDevice(event?: MatrixEvent): boolean {
+ if (!this.wasSentByOwnUser(event)) {
+ return false;
+ }
+ const content = event!.getContent();
+ if (!content || content.from_device !== this.client.getDeviceId()) {
+ return false;
+ }
+ return true;
+ }
+
+ public onVerifierCancelled(): void {
+ this._cancelled = true;
+ // move to cancelled phase
+ const newTransitions = this.applyPhaseTransitions();
+ if (newTransitions.length) {
+ this.setPhase(newTransitions[newTransitions.length - 1].phase);
+ }
+ }
+
+ public onVerifierFinished(): void {
+ this.channel.send(EventType.KeyVerificationDone, {});
+ this.verifierHasFinished = true;
+ // move to .done phase
+ const newTransitions = this.applyPhaseTransitions();
+ if (newTransitions.length) {
+ this.setPhase(newTransitions[newTransitions.length - 1].phase);
+ }
+ }
+
+ public getEventFromOtherParty(type: string): MatrixEvent | undefined {
+ return this.eventsByThem.get(type);
+ }
+}