diff --git a/src/client.ts b/src/client.ts index fd21ca022..d9c63ea39 100644 --- a/src/client.ts +++ b/src/client.ts @@ -92,7 +92,7 @@ import { import { SyncState } from "./sync.api"; import { EventTimelineSet } from "./models/event-timeline-set"; import { VerificationRequest } from "./crypto/verification/request/VerificationRequest"; -import { Base as Verification } from "./crypto/verification/Base"; +import { VerificationBase as Verification } from "./crypto/verification/Base"; import * as ContentHelpers from "./content-helpers"; import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning"; import { Room } from "./models/room"; diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index 364c447d2..515280e3a 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -739,6 +739,8 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O }; } +export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], [string, PkSigning], void] | void; + /** * Request cross-signing keys from another device during verification. * @@ -746,15 +748,19 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O * @param {string} userId The user ID being verified * @param {string} deviceId The device ID being verified */ -export async function requestKeysDuringVerification(baseApis: MatrixClient, userId: string, deviceId: string) { +export function requestKeysDuringVerification( + baseApis: MatrixClient, + userId: string, + deviceId: string, +): Promise<[[string, PkSigning], [string, PkSigning], [string, PkSigning], void] | void> { // If this is a self-verification, ask the other party for keys if (baseApis.getUserId() !== userId) { return; } logger.log("Cross-signing: Self-verification done; requesting keys"); // This happens asynchronously, and we're not concerned about waiting for - // it. We return here in order to test. - return new Promise((resolve, reject) => { + // it. We return here in order to test. + return new Promise((resolve, reject) => { const client = baseApis; const original = client.crypto.crossSigningInfo; @@ -781,7 +787,7 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user // https://github.com/vector-im/element-web/issues/12604 // then change here to reject on the timeout // Requests can be ignored, so don't wait around forever - const timeout = new Promise((resolve, reject) => { + const timeout = new Promise((resolve) => { setTimeout( resolve, KEY_REQUEST_TIMEOUT_MS, @@ -814,13 +820,13 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user })(); // We call getCrossSigningKey() for its side-effects - return Promise.race([ + return Promise.race([ Promise.all([ crossSigning.getCrossSigningKey("master"), crossSigning.getCrossSigningKey("self_signing"), crossSigning.getCrossSigningKey("user_signing"), backupKeyPromise, - ]), + ]) as Promise<[[string, PkSigning], [string, PkSigning], [string, PkSigning], void]>, timeout, ]).then(resolve, reject); }).catch((e) => { diff --git a/src/crypto/deviceinfo.ts b/src/crypto/deviceinfo.ts index 870899349..1c241a2b8 100644 --- a/src/crypto/deviceinfo.ts +++ b/src/crypto/deviceinfo.ts @@ -68,7 +68,7 @@ export class DeviceInfo { * * @return {module:crypto~DeviceInfo} new DeviceInfo */ - public static fromStorage(obj: IDevice, deviceId: string): DeviceInfo { + public static fromStorage(obj: Partial, deviceId: string): DeviceInfo { const res = new DeviceInfo(deviceId); for (const prop in obj) { if (obj.hasOwnProperty(prop)) { diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 1862c6a2c..eae1c8dea 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -26,7 +26,7 @@ import { EventEmitter } from 'events'; import { ReEmitter } from '../ReEmitter'; import { logger } from '../logger'; -import { OlmDevice } from "./OlmDevice"; +import { IExportedDevice, OlmDevice } from "./OlmDevice"; import * as olmlib from "./olmlib"; import { DeviceInfoMap, DeviceList } from "./DeviceList"; import { DeviceInfo, IDevice } from "./deviceinfo"; @@ -66,6 +66,7 @@ import { IRecoveryKey, IEncryptedEventInfo } from "./api"; import { IKeyBackupInfo } from "./keybackup"; import { ISyncStateData } from "../sync"; import { CryptoStore } from "./store/base"; +import { IVerificationChannel } from "./verification/request/Channel"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -84,12 +85,12 @@ const defaultVerificationMethods = { * verification method names */ // legacy export identifier -export enum verificationMethods { - RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME, - SAS = SASVerification.NAME, -} +export const verificationMethods = { + RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME, + SAS: SASVerification.NAME, +}; -export type VerificationMethod = verificationMethods; +export type VerificationMethod = keyof typeof verificationMethods | string; export function isCryptoAvailable(): boolean { return Boolean(global.Olm); @@ -98,7 +99,7 @@ export function isCryptoAvailable(): boolean { const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; interface IInitOpts { - exportedOlmDevice?: any; // TODO types + exportedOlmDevice?: IExportedDevice; pickleKey?: string; } @@ -2177,11 +2178,11 @@ export class Crypto extends EventEmitter { return this.inRoomVerificationRequests.findRequestInProgress(roomId); } - public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest { + public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] { return this.toDeviceVerificationRequests.getRequestsInProgress(userId); } - public requestVerificationDM(userId: string, roomId: string): VerificationRequest { + public requestVerificationDM(userId: string, roomId: string): Promise { const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId); if (existingRequest) { return Promise.resolve(existingRequest); @@ -2194,7 +2195,7 @@ export class Crypto extends EventEmitter { ); } - public requestVerification(userId: string, devices: string[]): VerificationRequest { + public requestVerification(userId: string, devices: string[]): Promise { if (!devices) { devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId)); } @@ -2212,9 +2213,9 @@ export class Crypto extends EventEmitter { private async requestVerificationWithChannel( userId: string, - channel: any, // TODO types + channel: IVerificationChannel, requestsMap: any, // TODO types - ): VerificationRequest { + ): Promise { let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis); // if transaction id is already known, add request if (channel.transactionId) { @@ -2260,14 +2261,11 @@ export class Crypto extends EventEmitter { userId: string, deviceId: string, method: VerificationMethod, - ): VerificationRequest { + ): Promise { const transactionId = ToDeviceChannel.makeTransactionId(); - const channel = new ToDeviceChannel( - this.baseApis, userId, [deviceId], transactionId, deviceId); - const request = new VerificationRequest( - channel, this.verificationMethods, this.baseApis); - this.toDeviceVerificationRequests.setRequestBySenderAndTxnId( - userId, transactionId, request); + const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId); + const request = new VerificationRequest(channel, this.verificationMethods, this.baseApis); + this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request); const verifier = request.beginKeyVerification(method, { userId, deviceId }); // either reject by an error from verify() while sending .start // or resolve when the request receives the diff --git a/src/crypto/verification/Base.js b/src/crypto/verification/Base.ts similarity index 62% rename from src/crypto/verification/Base.js rename to src/crypto/verification/Base.ts index 4ca01bd11..f373bbc1b 100644 --- a/src/crypto/verification/Base.js +++ b/src/crypto/verification/Base.ts @@ -25,18 +25,33 @@ import { EventEmitter } from 'events'; import { logger } from '../../logger'; import { DeviceInfo } from '../deviceinfo'; import { newTimeoutError } from "./Error"; -import { requestKeysDuringVerification } from "../CrossSigning"; +import { KeysDuringVerification, requestKeysDuringVerification } from "../CrossSigning"; +import { IVerificationChannel } from "./request/Channel"; +import { MatrixClient } from "../../client"; +import { VerificationRequest } from "./request/VerificationRequest"; const timeoutException = new Error("Verification timed out"); export class SwitchStartEventError extends Error { - constructor(startEvent) { + constructor(public readonly startEvent: MatrixEvent) { super(); - this.startEvent = startEvent; } } +export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void; + export class VerificationBase extends EventEmitter { + private cancelled = false; + private _done = false; + private promise: Promise = null; + private transactionTimeoutTimer: number = 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. * @@ -64,22 +79,18 @@ export class VerificationBase extends EventEmitter { * @param {object} [request] the key verification request object related to * this verification, if any */ - constructor(channel, baseApis, userId, deviceId, startEvent, request) { + constructor( + public readonly channel: IVerificationChannel, + public readonly baseApis: MatrixClient, + public readonly userId: string, + public readonly deviceId: string, + public startEvent: MatrixEvent, + public readonly request: VerificationRequest, + ) { super(); - this._channel = channel; - this._baseApis = baseApis; - this.userId = userId; - this.deviceId = deviceId; - this.startEvent = startEvent; - this.request = request; - - this.cancelled = false; - this._done = false; - this._promise = null; - this._transactionTimeoutTimer = null; } - get initiatedByMe() { + public get initiatedByMe(): boolean { // if there is no start event yet, // we probably want to send it, // which happens if we initiate @@ -88,16 +99,16 @@ export class VerificationBase extends EventEmitter { } const sender = this.startEvent.getSender(); const content = this.startEvent.getContent(); - return sender === this._baseApis.getUserId() && - content.from_device === this._baseApis.getDeviceId(); + return sender === this.baseApis.getUserId() && + content.from_device === this.baseApis.getDeviceId(); } - _resetTimer() { + private resetTimer(): void { logger.info("Refreshing/starting the verification transaction timeout timer"); - if (this._transactionTimeoutTimer !== null) { - clearTimeout(this._transactionTimeoutTimer); + if (this.transactionTimeoutTimer !== null) { + clearTimeout(this.transactionTimeoutTimer); } - this._transactionTimeoutTimer = setTimeout(() => { + this.transactionTimeoutTimer = setTimeout(() => { if (!this._done && !this.cancelled) { logger.info("Triggering verification timeout"); this.cancel(timeoutException); @@ -105,18 +116,18 @@ export class VerificationBase extends EventEmitter { }, 10 * 60 * 1000); // 10 minutes } - _endTimer() { - if (this._transactionTimeoutTimer !== null) { - clearTimeout(this._transactionTimeoutTimer); - this._transactionTimeoutTimer = null; + private endTimer(): void { + if (this.transactionTimeoutTimer !== null) { + clearTimeout(this.transactionTimeoutTimer); + this.transactionTimeoutTimer = null; } } - _send(type, uncompletedContent) { - return this._channel.send(type, uncompletedContent); + protected send(type: string, uncompletedContent: Record): Promise { + return this.channel.send(type, uncompletedContent); } - _waitForEvent(type) { + protected waitForEvent(type: string): Promise { if (this._done) { return Promise.reject(new Error("Verification is already done")); } @@ -125,24 +136,24 @@ export class VerificationBase extends EventEmitter { return Promise.resolve(existingEvent); } - this._expectedEvent = type; + this.expectedEvent = type; return new Promise((resolve, reject) => { - this._resolveEvent = resolve; - this._rejectEvent = reject; + this.resolveEvent = resolve; + this.rejectEvent = reject; }); } - canSwitchStartEvent() { + public canSwitchStartEvent(event: MatrixEvent): boolean { return false; } - switchStartEvent(event) { + 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; + { restartingFlow: !!this.rejectEvent }); + if (this.rejectEvent) { + const reject = this.rejectEvent; + this.rejectEvent = undefined; reject(new SwitchStartEventError(event)); } else { this.startEvent = event; @@ -150,21 +161,21 @@ export class VerificationBase extends EventEmitter { } } - handleEvent(e) { + public handleEvent(e: MatrixEvent): void { if (this._done) { return; - } else if (e.getType() === this._expectedEvent) { + } 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 !== "m.key.verification.done") { - this._expectedEvent = undefined; - this._rejectEvent = undefined; - this._resetTimer(); - this._resolveEvent(e); + if (this.expectedEvent !== "m.key.verification.done") { + this.expectedEvent = undefined; + this.rejectEvent = undefined; + this.resetTimer(); + this.resolveEvent(e); } } else if (e.getType() === "m.key.verification.cancel") { - const reject = this._reject; - this._reject = undefined; + const reject = this.reject; + this.reject = undefined; // there is only promise to reject if verify has been called if (reject) { const content = e.getContent(); @@ -172,36 +183,36 @@ export class VerificationBase extends EventEmitter { reject(new Error(`Other side cancelled verification ` + `because ${reason} (${code})`)); } - } else if (this._expectedEvent) { + } 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 + "Unexpected message: expecting " + this.expectedEvent + " but got " + e.getType(), ); - this._expectedEvent = undefined; - if (this._rejectEvent) { - const reject = this._rejectEvent; - this._rejectEvent = undefined; + this.expectedEvent = undefined; + if (this.rejectEvent) { + const reject = this.rejectEvent; + this.rejectEvent = undefined; reject(exception); } this.cancel(exception); } } - done() { - this._endTimer(); // always kill the activity timer + public done(): Promise { + this.endTimer(); // always kill the activity timer if (!this._done) { this.request.onVerifierFinished(); - this._resolve(); - return requestKeysDuringVerification(this._baseApis, this.userId, this.deviceId); + this.resolve(); + return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId); } } - cancel(e) { - this._endTimer(); // always kill the activity timer + public cancel(e: Error | MatrixEvent): void { + this.endTimer(); // always kill the activity timer if (!this._done) { this.cancelled = true; this.request.onVerifierCancelled(); @@ -210,7 +221,7 @@ export class VerificationBase extends EventEmitter { // cancelled by the other user) if (e === timeoutException) { const timeoutEvent = newTimeoutError(); - this._send(timeoutEvent.getType(), timeoutEvent.getContent()); + this.send(timeoutEvent.getType(), timeoutEvent.getContent()); } else if (e instanceof MatrixEvent) { const sender = e.getSender(); if (sender !== this.userId) { @@ -219,29 +230,29 @@ export class VerificationBase extends EventEmitter { content.code = content.code || "m.unknown"; content.reason = content.reason || content.body || "Unknown reason"; - this._send("m.key.verification.cancel", content); + this.send("m.key.verification.cancel", content); } else { - this._send("m.key.verification.cancel", { + this.send("m.key.verification.cancel", { code: "m.unknown", reason: content.body || "Unknown reason", }); } } } else { - this._send("m.key.verification.cancel", { + this.send("m.key.verification.cancel", { code: "m.unknown", reason: e.toString(), }); } } - if (this._promise !== null) { + 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); + 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); + this.promise = Promise.reject(e); } // Also emit a 'cancel' event that the app can listen for to detect cancellation // before calling verify() @@ -255,31 +266,32 @@ export class VerificationBase extends EventEmitter { * @returns {Promise} Promise which resolves when the verification has * completed. */ - verify() { - if (this._promise) return this._promise; + public verify(): Promise { + if (this.promise) return this.promise; - this._promise = new Promise((resolve, reject) => { - this._resolve = (...args) => { + this.promise = new Promise((resolve, reject) => { + this.resolve = (...args) => { this._done = true; - this._endTimer(); + this.endTimer(); resolve(...args); }; - this._reject = (...args) => { + this.reject = (e: Error) => { this._done = true; - this._endTimer(); - reject(...args); + this.endTimer(); + reject(e); }; }); - if (this._doVerification && !this._started) { - this._started = true; - this._resetTimer(); // restart the timeout - Promise.resolve(this._doVerification()) - .then(this.done.bind(this), this.cancel.bind(this)); + if (this.doVerification && !this.started) { + this.started = true; + this.resetTimer(); // restart the timeout + Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this)); } - return this._promise; + return this.promise; } - async _verifyKeys(userId, keys, verifier) { + protected doVerification?: () => Promise; + + protected async verifyKeys(userId: string, keys: Record, verifier: KeyVerifier): Promise { // 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 @@ -287,15 +299,14 @@ export class VerificationBase extends EventEmitter { for (const [keyId, keyInfo] of Object.entries(keys)) { const deviceId = keyId.split(':', 2)[1]; - const device = this._baseApis.getStoredDevice(userId, deviceId); + const device = this.baseApis.getStoredDevice(userId, deviceId); if (device) { - await verifier(keyId, device, keyInfo); + verifier(keyId, device, keyInfo); verifiedDevices.push(deviceId); } else { - const crossSigningInfo = this._baseApis.crypto.deviceList - .getStoredCrossSigningForUser(userId); + const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId); if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { - await verifier(keyId, DeviceInfo.fromStorage({ + verifier(keyId, DeviceInfo.fromStorage({ keys: { [keyId]: deviceId, }, @@ -323,7 +334,11 @@ export class VerificationBase extends EventEmitter { // 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 of verifiedDevices) { - await this._baseApis.setDeviceVerified(userId, deviceId); + await this.baseApis.setDeviceVerified(userId, deviceId); } } + + public get events(): string[] | undefined { + return undefined; + } } diff --git a/src/crypto/verification/Error.js b/src/crypto/verification/Error.ts similarity index 78% rename from src/crypto/verification/Error.js rename to src/crypto/verification/Error.ts index 794976f56..60faacf33 100644 --- a/src/crypto/verification/Error.js +++ b/src/crypto/verification/Error.ts @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +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. @@ -22,17 +22,17 @@ limitations under the License. import { MatrixEvent } from "../../models/event"; -export function newVerificationError(code, reason, extradata) { - const content = Object.assign({}, { code, reason }, extradata); +export function newVerificationError(code: string, reason: string, extraData: Record): MatrixEvent { + const content = Object.assign({}, { code, reason }, extraData); return new MatrixEvent({ type: "m.key.verification.cancel", content, }); } -export function errorFactory(code, reason) { - return function(extradata) { - return newVerificationError(code, reason, extradata); +export function errorFactory(code: string, reason: string): (extraData?: Record) => MatrixEvent { + return function(extraData?: Record) { + return newVerificationError(code, reason, extraData); }; } @@ -84,7 +84,7 @@ export const newInvalidMessageError = errorFactory( "m.invalid_message", "Invalid message", ); -export function errorFromEvent(event) { +export function errorFromEvent(event: MatrixEvent): { code: string, reason: string } { const content = event.getContent(); if (content) { const { code, reason } = content; diff --git a/src/crypto/verification/IllegalMethod.js b/src/crypto/verification/IllegalMethod.ts similarity index 62% rename from src/crypto/verification/IllegalMethod.js rename to src/crypto/verification/IllegalMethod.ts index 3eb8d79dd..b752d7404 100644 --- a/src/crypto/verification/IllegalMethod.js +++ b/src/crypto/verification/IllegalMethod.ts @@ -21,23 +21,35 @@ limitations under the License. */ import { VerificationBase as Base } from "./Base"; +import { IVerificationChannel } from "./request/Channel"; +import { MatrixClient } from "../../client"; +import { MatrixEvent } from "../../models/event"; +import { VerificationRequest } from "./request/VerificationRequest"; /** * @class crypto/verification/IllegalMethod/IllegalMethod * @extends {module:crypto/verification/Base} */ export class IllegalMethod extends Base { - static factory(...args) { - return new IllegalMethod(...args); + 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); } - static get NAME() { + // 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"; } - async _doVerification() { + protected doVerification = async (): Promise => { throw new Error("Verification is not possible with this method"); - } + }; } diff --git a/src/crypto/verification/QRCode.js b/src/crypto/verification/QRCode.ts similarity index 66% rename from src/crypto/verification/QRCode.js rename to src/crypto/verification/QRCode.ts index 1a271c0a6..cafb5a315 100644 --- a/src/crypto/verification/QRCode.js +++ b/src/crypto/verification/QRCode.ts @@ -1,6 +1,5 @@ /* -Copyright 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +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. @@ -21,12 +20,13 @@ limitations under the License. */ import { VerificationBase as Base } from "./Base"; -import { - newKeyMismatchError, - newUserCancelledError, -} from './Error'; -import { encodeUnpaddedBase64, decodeBase64 } from "../olmlib"; +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"; @@ -36,15 +36,28 @@ export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1"; * @extends {module:crypto/verification/Base} */ export class ReciprocateQRCode extends Base { - static factory(...args) { - return new ReciprocateQRCode(...args); + public reciprocateQREvent: { + confirm(): void; + cancel(): void; + }; + + 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); } - static get NAME() { + // eslint-disable-next-line @typescript-eslint/naming-convention + public static get NAME(): string { return "m.reciprocate.v1"; } - async _doVerification() { + protected doVerification = async (): Promise => { if (!this.startEvent) { // TODO: Support scanning QR codes throw new Error("It is not currently possible to start verification" + @@ -58,7 +71,7 @@ export class ReciprocateQRCode extends Base { } // 2. ask if other user shows shield as well - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { this.reciprocateQREvent = { confirm: resolve, cancel: () => reject(newUserCancelledError()), @@ -67,21 +80,21 @@ export class ReciprocateQRCode extends Base { }); // 3. determine key to sign / mark as trusted - const keys = {}; + const keys: Record = {}; switch (qrCodeData.mode) { - case MODE_VERIFY_OTHER_USER: { + 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_VERIFY_SELF_TRUSTED: { + case Mode.VerifySelfTrusted: { const deviceId = this.request.targetDevice.deviceId; keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey; break; } - case MODE_VERIFY_SELF_UNTRUSTED: { + case Mode.VerifySelfUntrusted: { const masterKey = qrCodeData.myMasterKey; keys[`ed25519:${masterKey}`] = masterKey; break; @@ -89,7 +102,7 @@ export class ReciprocateQRCode extends Base { } // 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) => { + 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(); @@ -108,103 +121,84 @@ export class ReciprocateQRCode extends Base { } } }); - } + }; } const CODE_VERSION = 0x02; // the version of binary QR codes we support const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format -const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us -const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key -const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key + +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 { constructor( - mode, sharedSecret, otherUserMasterKey, - otherDeviceKey, myMasterKey, buffer, - ) { - this._sharedSecret = sharedSecret; - this._mode = mode; - this._otherUserMasterKey = otherUserMasterKey; - this._otherDeviceKey = otherDeviceKey; - this._myMasterKey = myMasterKey; - this._buffer = buffer; - } + 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 | undefined, + // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code + public readonly otherDeviceKey: string | undefined, + // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code + public readonly myMasterKey: string | undefined, + private readonly buffer: Buffer, + ) {} - static async create(request, client) { - const sharedSecret = QRCodeData._generateSharedSecret(); - const mode = QRCodeData._determineMode(request, client); + public static async create(request: VerificationRequest, client: MatrixClient): Promise { + const sharedSecret = QRCodeData.generateSharedSecret(); + const mode = QRCodeData.determineMode(request, client); let otherUserMasterKey = null; let otherDeviceKey = null; let myMasterKey = null; - if (mode === MODE_VERIFY_OTHER_USER) { + if (mode === Mode.VerifyOtherUser) { const otherUserCrossSigningInfo = client.getStoredCrossSigningForUser(request.otherUserId); otherUserMasterKey = otherUserCrossSigningInfo.getId("master"); - } else if (mode === MODE_VERIFY_SELF_TRUSTED) { - otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client); - } else if (mode === MODE_VERIFY_SELF_UNTRUSTED) { + } 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( + const qrData = QRCodeData.generateQrData( request, client, mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, ); - const buffer = QRCodeData._generateBuffer(qrData); + const buffer = QRCodeData.generateBuffer(qrData); return new QRCodeData(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer); } - get buffer() { - return this._buffer; - } - - get mode() { - return this._mode; - } - - /** - * only set when mode is MODE_VERIFY_SELF_TRUSTED - * @return {string} device key of other party at time of generating QR code - */ - get otherDeviceKey() { - return this._otherDeviceKey; - } - - /** - * only set when mode is MODE_VERIFY_OTHER_USER - * @return {string} master key of other party at time of generating QR code - */ - get otherUserMasterKey() { - return this._otherUserMasterKey; - } - - /** - * only set when mode is MODE_VERIFY_SELF_UNTRUSTED - * @return {string} own master key at time of generating QR code - */ - get myMasterKey() { - return this._myMasterKey; - } - /** * The unpadded base64 encoded shared secret. */ - get encodedSharedSecret() { - return this._sharedSecret; + public get encodedSharedSecret(): string { + return this.sharedSecret; } - static _generateSharedSecret() { + private static generateSharedSecret(): string { const secretBytes = new Uint8Array(11); global.crypto.getRandomValues(secretBytes); return encodeUnpaddedBase64(secretBytes); } - static async _getOtherDeviceKey(request, client) { + private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise { const myUserId = client.getUserId(); const otherDevice = request.targetDevice; const otherDeviceId = otherDevice ? otherDevice.deviceId : null; @@ -212,31 +206,35 @@ export class QRCodeData { if (!device) { throw new Error("could not find device " + otherDeviceId); } - const key = device.getFingerprint(); - return key; + return device.getFingerprint(); } - static _determineMode(request, client) { + private static determineMode(request: VerificationRequest, client: MatrixClient): Mode { const myUserId = client.getUserId(); const otherUserId = request.otherUserId; - let mode = MODE_VERIFY_OTHER_USER; + 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_VERIFY_SELF_TRUSTED; + mode = Mode.VerifySelfTrusted; } else { - mode = MODE_VERIFY_SELF_UNTRUSTED; + mode = Mode.VerifySelfUntrusted; } } return mode; } - static _generateQrData(request, client, mode, - encodedSharedSecret, otherUserMasterKey, - otherDeviceKey, myMasterKey, - ) { + 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 = { @@ -251,16 +249,16 @@ export class QRCodeData { const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId); - if (mode === MODE_VERIFY_OTHER_USER) { + 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_VERIFY_SELF_TRUSTED) { + } 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_VERIFY_SELF_UNTRUSTED) { + } 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 @@ -269,7 +267,7 @@ export class QRCodeData { return qrData; } - static _generateBuffer(qrData) { + private static generateBuffer(qrData: IQrData): Buffer { let buf = Buffer.alloc(0); // we'll concat our way through life const appendByte = (b) => { diff --git a/src/crypto/verification/SAS.js b/src/crypto/verification/SAS.ts similarity index 74% rename from src/crypto/verification/SAS.js rename to src/crypto/verification/SAS.ts index 8c3de1768..a7d679787 100644 --- a/src/crypto/verification/SAS.js +++ b/src/crypto/verification/SAS.ts @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +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. @@ -29,6 +29,8 @@ import { newUserCancelledError, } from './Error'; import { logger } from '../../logger'; +import { Utility, SAS as OlmSAS } from "@matrix-org/olm"; +import { IContent, MatrixEvent } from "../../models/event"; const START_TYPE = "m.key.verification.start"; @@ -38,7 +40,7 @@ const EVENTS = [ "m.key.verification.mac", ]; -let olmutil; +let olmutil: Utility; const newMismatchedSASError = errorFactory( "m.mismatched_sas", "Mismatched short authentication string", @@ -48,7 +50,7 @@ const newMismatchedCommitmentError = errorFactory( "m.mismatched_commitment", "Mismatched commitment", ); -function generateDecimalSas(sasBytes) { +function generateDecimalSas(sasBytes: number[]): [number, number, number] { /** * +--------+--------+--------+--------+--------+ * | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | @@ -64,7 +66,9 @@ function generateDecimalSas(sasBytes) { ]; } -const emojiMapping = [ +type EmojiMapping = [emoji: string, name: string]; + +const emojiMapping: EmojiMapping[] = [ ["🐶", "dog"], // 0 ["🐱", "cat"], // 1 ["🦁", "lion"], // 2 @@ -131,7 +135,7 @@ const emojiMapping = [ ["📌", "pin"], // 63 ]; -function generateEmojiSas(sasBytes) { +function generateEmojiSas(sasBytes: number[]): EmojiMapping[] { const emojis = [ // just like base64 encoding sasBytes[0] >> 2, @@ -151,8 +155,13 @@ const sasGenerators = { emoji: generateEmojiSas, }; -function generateSas(sasBytes, methods) { - const sas = {}; +export interface IGeneratedSas { + decimal?: [number, number, number]; + emoji?: EmojiMapping[]; +} + +function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas { + const sas: IGeneratedSas = {}; for (const method of methods) { if (method in sasGenerators) { sas[method] = sasGenerators[method](sasBytes); @@ -166,7 +175,7 @@ const macMethods = { "hmac-sha256": "calculate_mac_long_kdf", }; -function calculateMAC(olmSAS, method) { +function calculateMAC(olmSAS: OlmSAS, method: string) { return function(...args) { const macFunction = olmSAS[macMethods[method]]; const mac = macFunction.apply(olmSAS, args); @@ -176,23 +185,23 @@ function calculateMAC(olmSAS, method) { } const calculateKeyAgreement = { - "curve25519-hkdf-sha256": function(sas, olmSAS, bytes) { - const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|` + "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; + + sas.channel.transactionId; return olmSAS.generate_bytes(sasInfo, bytes); }, - "curve25519": function(sas, olmSAS, bytes) { - const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`; + "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; + + sas.channel.transactionId; return olmSAS.generate_bytes(sasInfo, bytes); }, }; @@ -211,7 +220,7 @@ const HASHES_SET = new Set(HASHES_LIST); const MAC_SET = new Set(MAC_LIST); const SAS_SET = new Set(SAS_LIST); -function intersection(anArray, aSet) { +function intersection(anArray: T[], aSet: Set): T[] { return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : []; } @@ -220,28 +229,39 @@ function intersection(anArray, aSet) { * @extends {module:crypto/verification/Base} */ export class SAS extends Base { - static get NAME() { + private waitingForAccept: boolean; + public ourSASPubKey: string; + public theirSASPubKey: string; + public sasEvent: { + sas: IGeneratedSas; + confirm(): Promise; + cancel(): void; + mismatch(): void; + }; + + // eslint-disable-next-line @typescript-eslint/naming-convention + public static get NAME(): string { return "m.sas.v1"; } - get events() { + public get events(): string[] { return EVENTS; } - async _doVerification() { + protected doVerification = async (): Promise => { await global.Olm.init(); olmutil = olmutil || new global.Olm.Utility(); // make sure user's keys are downloaded - await this._baseApis.downloadKeys([this.userId]); + await this.baseApis.downloadKeys([this.userId]); let retry = false; do { try { if (this.initiatedByMe) { - return await this._doSendVerification(); + return await this.doSendVerification(); } else { - return await this._doRespondVerification(); + return await this.doRespondVerification(); } } catch (err) { if (err instanceof SwitchStartEventError) { @@ -253,38 +273,37 @@ export class SAS extends Base { } } } while (retry); - } + }; - canSwitchStartEvent(event) { + public canSwitchStartEvent(event: MatrixEvent): boolean { if (event.getType() !== START_TYPE) { return false; } const content = event.getContent(); - return content && content.method === SAS.NAME && - this._waitingForAccept; + return content && content.method === SAS.NAME && this.waitingForAccept; } - async _sendStart() { - const startContent = this._channel.completeContent(START_TYPE, { + private async sendStart(): Promise> { + const startContent = this.channel.completeContent(START_TYPE, { method: SAS.NAME, - from_device: this._baseApis.deviceId, + 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); + await this.channel.sendCompleted(START_TYPE, startContent); return startContent; } - async _doSendVerification() { - this._waitingForAccept = true; + private async doSendVerification(): Promise { + this.waitingForAccept = true; let startContent; if (this.startEvent) { - startContent = this._channel.completedContentFromEvent(this.startEvent); + startContent = this.channel.completedContentFromEvent(this.startEvent); } else { - startContent = await this._sendStart(); + startContent = await this.sendStart(); } // we might have switched to a different start event, @@ -297,9 +316,9 @@ export class SAS extends Base { let e; try { - e = await this._waitForEvent("m.key.verification.accept"); + e = await this.waitForEvent("m.key.verification.accept"); } finally { - this._waitingForAccept = false; + this.waitingForAccept = false; } let content = e.getContent(); const sasMethods @@ -319,11 +338,11 @@ export class SAS extends Base { const olmSAS = new global.Olm.SAS(); try { this.ourSASPubKey = olmSAS.get_pubkey(); - await this._send("m.key.verification.key", { + await this.send("m.key.verification.key", { key: this.ourSASPubKey, }); - e = await this._waitForEvent("m.key.verification.key"); + e = await this.waitForEvent("m.key.verification.key"); // FIXME: make sure event is properly formed content = e.getContent(); const commitmentStr = content.key + anotherjson.stringify(startContent); @@ -335,12 +354,12 @@ export class SAS extends Base { olmSAS.set_their_key(content.key); const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); - const verifySAS = new Promise((resolve, reject) => { + const verifySAS = new Promise((resolve, reject) => { this.sasEvent = { sas: generateSas(sasBytes, sasMethods), confirm: async () => { try { - await this._sendMAC(olmSAS, macMethod); + await this.sendMAC(olmSAS, macMethod); resolve(); } catch (err) { reject(err); @@ -353,54 +372,45 @@ export class SAS extends Base { }); [e] = await Promise.all([ - this._waitForEvent("m.key.verification.mac") + this.waitForEvent("m.key.verification.mac") .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 = "m.key.verification.done"; + this.expectedEvent = "m.key.verification.done"; return e; }), verifySAS, ]); content = e.getContent(); - await this._checkMAC(olmSAS, content, macMethod); + await this.checkMAC(olmSAS, content, macMethod); } finally { olmSAS.free(); } } - async _doRespondVerification() { + private async doRespondVerification(): Promise { // 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); + 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]; + 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)) { + 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("m.key.verification.accept", { + await this.send("m.key.verification.accept", { key_agreement_protocol: keyAgreement, hash: hashMethod, message_authentication_code: macMethod, @@ -409,23 +419,23 @@ export class SAS extends Base { commitment: olmutil.sha256(commitmentStr), }); - let e = await this._waitForEvent("m.key.verification.key"); + let e = await this.waitForEvent("m.key.verification.key"); // 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("m.key.verification.key", { + await this.send("m.key.verification.key", { key: this.ourSASPubKey, }); const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); - const verifySAS = new Promise((resolve, reject) => { + const verifySAS = new Promise((resolve, reject) => { this.sasEvent = { sas: generateSas(sasBytes, sasMethods), confirm: async () => { try { - await this._sendMAC(olmSAS, macMethod); + await this.sendMAC(olmSAS, macMethod); resolve(); } catch (err) { reject(err); @@ -438,39 +448,39 @@ export class SAS extends Base { }); [e] = await Promise.all([ - this._waitForEvent("m.key.verification.mac") + this.waitForEvent("m.key.verification.mac") .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 = "m.key.verification.done"; + this.expectedEvent = "m.key.verification.done"; return e; }), verifySAS, ]); content = e.getContent(); - await this._checkMAC(olmSAS, content, macMethod); + await this.checkMAC(olmSAS, content, macMethod); } finally { olmSAS.free(); } } - _sendMAC(olmSAS, method) { + private sendMAC(olmSAS: OlmSAS, method: string): Promise { const mac = {}; const keyList = []; const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" - + this._baseApis.getUserId() + this._baseApis.deviceId + + this.baseApis.getUserId() + this.baseApis.deviceId + this.userId + this.deviceId - + this._channel.transactionId; + + this.channel.transactionId; - const deviceKeyId = `ed25519:${this._baseApis.deviceId}`; + const deviceKeyId = `ed25519:${this.baseApis.deviceId}`; mac[deviceKeyId] = calculateMAC(olmSAS, method)( - this._baseApis.getDeviceEd25519Key(), + this.baseApis.getDeviceEd25519Key(), baseInfo + deviceKeyId, ); keyList.push(deviceKeyId); - const crossSigningId = this._baseApis.getCrossSigningId(); + const crossSigningId = this.baseApis.getCrossSigningId(); if (crossSigningId) { const crossSigningKeyId = `ed25519:${crossSigningId}`; mac[crossSigningKeyId] = calculateMAC(olmSAS, method)( @@ -484,14 +494,14 @@ export class SAS extends Base { keyList.sort().join(","), baseInfo + "KEY_IDS", ); - return this._send("m.key.verification.mac", { mac, keys }); + return this.send("m.key.verification.mac", { mac, keys }); } - async _checkMAC(olmSAS, content, method) { + private async checkMAC(olmSAS: OlmSAS, content: IContent, method: string): Promise { const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.userId + this.deviceId - + this._baseApis.getUserId() + this._baseApis.deviceId - + this._channel.transactionId; + + this.baseApis.getUserId() + this.baseApis.deviceId + + this.channel.transactionId; if (content.keys !== calculateMAC(olmSAS, method)( Object.keys(content.mac).sort().join(","), @@ -500,7 +510,7 @@ export class SAS extends Base { throw newKeyMismatchError(); } - await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => { + await this.verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => { if (keyInfo !== calculateMAC(olmSAS, method)( device.keys[keyId], baseInfo + keyId, diff --git a/src/crypto/verification/request/InRoomChannel.js b/src/crypto/verification/request/InRoomChannel.ts similarity index 73% rename from src/crypto/verification/request/InRoomChannel.js rename to src/crypto/verification/request/InRoomChannel.ts index 72b6af4b2..97dddcd38 100644 --- a/src/crypto/verification/request/InRoomChannel.js +++ b/src/crypto/verification/request/InRoomChannel.ts @@ -22,8 +22,12 @@ import { 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"; -const MESSAGE_TYPE = "m.room.message"; +const MESSAGE_TYPE = EventType.RoomMessage; const M_REFERENCE = "m.reference"; const M_RELATES_TO = "m.relates_to"; @@ -31,36 +35,34 @@ 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 { +export class InRoomChannel implements IVerificationChannel { + private requestEventId = null; + /** * @param {MatrixClient} client the matrix client, to send messages with and get current user & device from. * @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user. * @param {string} userId id of user that the verification request is directed at, should be present in the room. */ - constructor(client, roomId, userId = null) { - this._client = client; - this._roomId = roomId; - this.userId = userId; - this._requestEventId = null; + constructor( + private readonly client: MatrixClient, + public readonly roomId: string, + public userId: string = null, + ) { } - get receiveStartFromOtherDevices() { + public get receiveStartFromOtherDevices(): boolean { return true; } - get roomId() { - return this._roomId; - } - /** The transaction id generated/used by this verification channel */ - get transactionId() { - return this._requestEventId; + public get transactionId(): string { + return this.requestEventId; } - static getOtherPartyUserId(event, client) { + public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string { const type = InRoomChannel.getEventType(event); if (type !== REQUEST_TYPE) { - return; + return; } const ownUserId = client.getUserId(); const sender = event.getSender(); @@ -78,25 +80,29 @@ export class InRoomChannel { * @param {MatrixEvent} event the event to get the timestamp of * @return {number} the timestamp when the event was sent */ - getTimestamp(event) { + 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 {string} type the event type to check - * @returns {bool} boolean flag + * @returns {boolean} boolean flag */ - static canCreateRequest(type) { + 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 {MatrixEvent} event the event * @returns {string} the transaction id */ - static getTransactionId(event) { + public static getTransactionId(event: MatrixEvent): string { if (InRoomChannel.getEventType(event) === REQUEST_TYPE) { return event.getId(); } else { @@ -114,9 +120,9 @@ export class InRoomChannel { * `handleEvent` can do more checks and choose to ignore invalid events. * @param {MatrixEvent} event the event to validate * @param {MatrixClient} client the client to get the current user and device id from - * @returns {bool} whether the event is valid and should be passed to handleEvent + * @returns {boolean} whether the event is valid and should be passed to handleEvent */ - static validateEvent(event, client) { + public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean { const txnId = InRoomChannel.getTransactionId(event); if (typeof txnId !== "string" || txnId.length === 0) { return false; @@ -152,7 +158,7 @@ export class InRoomChannel { * @param {MatrixEvent} event the event to get the type of * @returns {string} the "symbolic" event type */ - static getEventType(event) { + public static getEventType(event: MatrixEvent): string { const type = event.getType(); if (type === MESSAGE_TYPE) { const content = event.getContent(); @@ -174,10 +180,10 @@ export class InRoomChannel { * Changes the state of the channel, request, and verifier in response to a key verification event. * @param {MatrixEvent} event to handle * @param {VerificationRequest} request the request to forward handling to - * @param {bool} isLiveEvent whether this is an even received through sync or not - * @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent. + * @param {boolean} isLiveEvent whether this is an even received through sync or not + * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. */ - async handleEvent(event, request, isLiveEvent) { + public async handleEvent(event: MatrixEvent, request: VerificationRequest, isLiveEvent = false): Promise { // 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())) { @@ -187,18 +193,18 @@ export class InRoomChannel { // do validations that need state (roomId, userId), // ignore if invalid - if (event.getRoomId() !== this._roomId) { + if (event.getRoomId() !== this.roomId) { return; } // set userId if not set already if (this.userId === null) { - const userId = InRoomChannel.getOtherPartyUserId(event, this._client); + 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 ownUserId = this.client.getUserId(); const sender = event.getSender(); if (this.userId !== null) { if (sender !== ownUserId && sender !== this.userId) { @@ -207,12 +213,12 @@ export class InRoomChannel { return; } } - if (this._requestEventId === null) { - this._requestEventId = InRoomChannel.getTransactionId(event); + if (this.requestEventId === null) { + this.requestEventId = InRoomChannel.getTransactionId(event); } const isRemoteEcho = !!event.getUnsigned().transaction_id; - const isSentByUs = event.getSender() === this._client.getUserId(); + const isSentByUs = event.getSender() === this.client.getUserId(); return await request.handleEvent( type, event, isLiveEvent, isRemoteEcho, isSentByUs); @@ -226,13 +232,14 @@ export class InRoomChannel { * @param {MatrixEvent} event the received event * @returns {Object} the content object with the relation added again */ - completedContentFromEvent(event) { + public completedContentFromEvent(event: MatrixEvent): Record { // 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 @@ -242,15 +249,15 @@ export class InRoomChannel { * @param {object} content the (incomplete) content * @returns {object} the complete content, as it will be sent. */ - completeContent(type, content) { + public completeContent(type: string, content: Record): Record { content = Object.assign({}, content); if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) { - content.from_device = this._client.getDeviceId(); + 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 " + + 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.", @@ -274,7 +281,7 @@ export class InRoomChannel { * @param {object} uncompletedContent the (incomplete) content * @returns {Promise} the promise of the request */ - send(type, uncompletedContent) { + public send(type: string, uncompletedContent: Record): Promise { const content = this.completeContent(type, uncompletedContent); return this.sendCompleted(type, content); } @@ -285,74 +292,69 @@ export class InRoomChannel { * @param {object} content * @returns {Promise} the promise of the request */ - async sendCompleted(type, content) { + public async sendCompleted(type: string, content: Record): Promise { let sendType = type; if (type === REQUEST_TYPE) { sendType = MESSAGE_TYPE; } - const response = await this._client.sendEvent(this._roomId, sendType, content); + const response = await this.client.sendEvent(this.roomId, sendType, content); if (type === REQUEST_TYPE) { - this._requestEventId = response.event_id; + this.requestEventId = response.event_id; } } } export class InRoomRequests { - constructor() { - this._requestsByRoomId = new Map(); - } + private requestsByRoomId = new Map>(); - getRequest(event) { + public getRequest(event: MatrixEvent): VerificationRequest { const roomId = event.getRoomId(); const txnId = InRoomChannel.getTransactionId(event); - return this._getRequestByTxnId(roomId, txnId); + return this.getRequestByTxnId(roomId, txnId); } - getRequestByChannel(channel) { - return this._getRequestByTxnId(channel.roomId, channel.transactionId); + public getRequestByChannel(channel: InRoomChannel): VerificationRequest { + return this.getRequestByTxnId(channel.roomId, channel.transactionId); } - _getRequestByTxnId(roomId, txnId) { - const requestsByTxnId = this._requestsByRoomId.get(roomId); + private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest { + const requestsByTxnId = this.requestsByRoomId.get(roomId); if (requestsByTxnId) { return requestsByTxnId.get(txnId); } } - setRequest(event, request) { - this._setRequest( - event.getRoomId(), - InRoomChannel.getTransactionId(event), - request, - ); + public setRequest(event: MatrixEvent, request: VerificationRequest): void { + this._setRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request); } - setRequestByChannel(channel, request) { + public setRequestByChannel(channel: InRoomChannel, request: VerificationRequest): void { this._setRequest(channel.roomId, channel.transactionId, request); } - _setRequest(roomId, txnId, request) { - let requestsByTxnId = this._requestsByRoomId.get(roomId); + // eslint-disable-next-line @typescript-eslint/naming-convention + private _setRequest(roomId: string, txnId: string, request: VerificationRequest): void { + let requestsByTxnId = this.requestsByRoomId.get(roomId); if (!requestsByTxnId) { requestsByTxnId = new Map(); - this._requestsByRoomId.set(roomId, requestsByTxnId); + this.requestsByRoomId.set(roomId, requestsByTxnId); } requestsByTxnId.set(txnId, request); } - removeRequest(event) { + public removeRequest(event: MatrixEvent): void { const roomId = event.getRoomId(); - const requestsByTxnId = this._requestsByRoomId.get(roomId); + const requestsByTxnId = this.requestsByRoomId.get(roomId); if (requestsByTxnId) { requestsByTxnId.delete(InRoomChannel.getTransactionId(event)); if (requestsByTxnId.size === 0) { - this._requestsByRoomId.delete(roomId); + this.requestsByRoomId.delete(roomId); } } } - findRequestInProgress(roomId) { - const requestsByTxnId = this._requestsByRoomId.get(roomId); + public findRequestInProgress(roomId: string): VerificationRequest { + const requestsByTxnId = this.requestsByRoomId.get(roomId); if (requestsByTxnId) { for (const request of requestsByTxnId.values()) { if (request.pending) { diff --git a/src/crypto/verification/request/ToDeviceChannel.js b/src/crypto/verification/request/ToDeviceChannel.ts similarity index 69% rename from src/crypto/verification/request/ToDeviceChannel.js rename to src/crypto/verification/request/ToDeviceChannel.ts index f80c0e523..d04f9b236 100644 --- a/src/crypto/verification/request/ToDeviceChannel.js +++ b/src/crypto/verification/request/ToDeviceChannel.ts @@ -28,25 +28,31 @@ import { } from "./VerificationRequest"; import { errorFromEvent, newUnexpectedMessageError } from "../Error"; import { MatrixEvent } from "../../../models/event"; +import { IVerificationChannel } from "./Channel"; +import { MatrixClient } from "../../../client"; + +type Request = VerificationRequest; /** * A key verification channel that sends verification events over to_device messages. * Generates its own transaction ids. */ -export class ToDeviceChannel { - // userId and devices of user we're about to verify - constructor(client, userId, devices, transactionId = null, deviceId = null) { - this._client = client; - this.userId = userId; - this._devices = devices; - this.transactionId = transactionId; - this._deviceId = deviceId; - } +export class ToDeviceChannel implements IVerificationChannel { + public request?: VerificationRequest; - isToDevices(devices) { - if (devices.length === this._devices.length) { + // userId and devices of user we're about to verify + constructor( + private readonly client: MatrixClient, + public readonly userId: string, + private readonly devices: string[], + public transactionId: string = null, + public deviceId: string = null, + ) {} + + public isToDevices(devices: string[]): boolean { + if (devices.length === this.devices.length) { for (const device of devices) { - const d = this._devices.find(d => d.deviceId === device.deviceId); + const d = this.devices.find(d => d.deviceId === device.deviceId); if (!d) { return false; } @@ -57,11 +63,7 @@ export class ToDeviceChannel { } } - get deviceId() { - return this._deviceId; - } - - static getEventType(event) { + public static getEventType(event: MatrixEvent): string { return event.getType(); } @@ -70,7 +72,7 @@ export class ToDeviceChannel { * @param {MatrixEvent} event the event * @returns {string} the transaction id */ - static getTransactionId(event) { + public static getTransactionId(event: MatrixEvent): string { const content = event.getContent(); return content && content.transaction_id; } @@ -78,12 +80,16 @@ export class ToDeviceChannel { /** * Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel * @param {string} type the event type to check - * @returns {bool} boolean flag + * @returns {boolean} boolean flag */ - static canCreateRequest(type) { + 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 @@ -91,9 +97,9 @@ export class ToDeviceChannel { * `handleEvent` can do more checks and choose to ignore invalid events. * @param {MatrixEvent} event the event to validate * @param {MatrixClient} client the client to get the current user and device id from - * @returns {bool} whether the event is valid and should be passed to handleEvent + * @returns {boolean} whether the event is valid and should be passed to handleEvent */ - static validateEvent(event, client) { + public static validateEvent(event: MatrixEvent, client: MatrixClient): boolean { if (event.isCancelled()) { logger.warn("Ignoring flagged verification request from " + event.getSender()); @@ -134,7 +140,7 @@ export class ToDeviceChannel { * @param {MatrixEvent} event the event to get the timestamp of * @return {number} the timestamp when the event was sent */ - getTimestamp(event) { + public getTimestamp(event: MatrixEvent): number { const content = event.getContent(); return content && content.timestamp; } @@ -143,10 +149,10 @@ export class ToDeviceChannel { * Changes the state of the channel, request, and verifier in response to a key verification event. * @param {MatrixEvent} event to handle * @param {VerificationRequest} request the request to forward handling to - * @param {bool} isLiveEvent whether this is an even received through sync or not - * @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent. + * @param {boolean} isLiveEvent whether this is an even received through sync or not + * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. */ - async handleEvent(event, request, isLiveEvent) { + public async handleEvent(event: MatrixEvent, request: Request, isLiveEvent = false): Promise { const type = event.getType(); const content = event.getContent(); if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) { @@ -155,17 +161,16 @@ export class ToDeviceChannel { } const deviceId = content.from_device; // adopt deviceId if not set before and valid - if (!this._deviceId && this._devices.includes(deviceId)) { - this._deviceId = deviceId; + if (!this.deviceId && this.devices.includes(deviceId)) { + this.deviceId = deviceId; } - // if no device id or different from addopted one, cancel with sender - if (!this._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(errorFromEvent(newUnexpectedMessageError())); - return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]); + const cancelContent = this.completeContent(errorFromEvent(newUnexpectedMessageError())); + return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]); } } const wasStarted = request.phase === PHASE_STARTED || @@ -178,16 +183,16 @@ export class ToDeviceChannel { 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 (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({ code: "m.accepted", reason: "Verification request accepted by another device", }); - await this._sendToDevices(CANCEL_TYPE, message, nonChosenDevices); + await this.sendToDevices(CANCEL_TYPE, message, nonChosenDevices); } } } @@ -197,7 +202,7 @@ export class ToDeviceChannel { * @param {MatrixEvent} event the received event * @returns {Object} the content object */ - completedContentFromEvent(event) { + public completedContentFromEvent(event: MatrixEvent): Record { return event.getContent(); } @@ -210,14 +215,14 @@ export class ToDeviceChannel { * @param {object} content the (incomplete) content * @returns {object} the complete content, as it will be sent. */ - completeContent(type, content) { + public completeContent(type: string, content: Record): Record { // 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(); + content.from_device = this.client.getDeviceId(); } if (type === REQUEST_TYPE) { content.timestamp = Date.now(); @@ -231,7 +236,7 @@ export class ToDeviceChannel { * @param {object} uncompletedContent the (incomplete) content * @returns {Promise} the promise of the request */ - send(type, uncompletedContent = {}) { + public send(type: string, uncompletedContent: Record = {}): Promise { // create transaction id when sending request if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) { this.transactionId = ToDeviceChannel.makeTransactionId(); @@ -246,21 +251,21 @@ export class ToDeviceChannel { * @param {object} content * @returns {Promise} the promise of the request */ - async sendCompleted(type, content) { + public async sendCompleted(type: string, content: Record): Promise { let result; if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.__deviceId)) { - result = await this._sendToDevices(type, content, this._devices); + result = await this.sendToDevices(type, content, this.devices); } else { - result = await this._sendToDevices(type, content, [this._deviceId]); + 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(), + sender: this.client.getUserId(), content, type, }); - await this._request.handleEvent( + await this.request.handleEvent( type, remoteEchoEvent, /*isLiveEvent=*/true, @@ -270,16 +275,14 @@ export class ToDeviceChannel { return result; } - _sendToDevices(type, content, devices) { + private async sendToDevices(type: string, content: Record, devices: string[]): Promise { if (devices.length) { const msgMap = {}; for (const deviceId of devices) { msgMap[deviceId] = content; } - return this._client.sendToDevice(type, { [this.userId]: msgMap }); - } else { - return Promise.resolve(); + await this.client.sendToDevice(type, { [this.userId]: msgMap }); } } @@ -287,68 +290,62 @@ export class ToDeviceChannel { * Allow Crypto module to create and know the transaction id before the .start event gets sent. * @returns {string} the transaction id */ - static makeTransactionId() { + public static makeTransactionId(): string { return randomString(32); } } export class ToDeviceRequests { - constructor() { - this._requestsByUserId = new Map(); - } + private requestsByUserId = new Map>(); - getRequest(event) { + public getRequest(event: MatrixEvent): Request { return this.getRequestBySenderAndTxnId( event.getSender(), ToDeviceChannel.getTransactionId(event), ); } - getRequestByChannel(channel) { + public getRequestByChannel(channel: ToDeviceChannel): Request { return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId); } - getRequestBySenderAndTxnId(sender, txnId) { - const requestsByTxnId = this._requestsByUserId.get(sender); + public getRequestBySenderAndTxnId(sender: string, txnId: string): Request { + const requestsByTxnId = this.requestsByUserId.get(sender); if (requestsByTxnId) { return requestsByTxnId.get(txnId); } } - setRequest(event, request) { - this.setRequestBySenderAndTxnId( - event.getSender(), - ToDeviceChannel.getTransactionId(event), - request, - ); + public setRequest(event: MatrixEvent, request: Request): void { + this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request); } - setRequestByChannel(channel, request) { + public setRequestByChannel(channel: ToDeviceChannel, request: Request): void { this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request); } - setRequestBySenderAndTxnId(sender, txnId, request) { - let requestsByTxnId = this._requestsByUserId.get(sender); + 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); + this.requestsByUserId.set(sender, requestsByTxnId); } requestsByTxnId.set(txnId, request); } - removeRequest(event) { + public removeRequest(event: MatrixEvent): void { const userId = event.getSender(); - const requestsByTxnId = this._requestsByUserId.get(userId); + const requestsByTxnId = this.requestsByUserId.get(userId); if (requestsByTxnId) { requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event)); if (requestsByTxnId.size === 0) { - this._requestsByUserId.delete(userId); + this.requestsByUserId.delete(userId); } } } - findRequestInProgress(userId, devices) { - const requestsByTxnId = this._requestsByUserId.get(userId); + public findRequestInProgress(userId: string, devices: string[]): Request { + const requestsByTxnId = this.requestsByUserId.get(userId); if (requestsByTxnId) { for (const request of requestsByTxnId.values()) { if (request.pending && request.channel.isToDevices(devices)) { @@ -358,8 +355,8 @@ export class ToDeviceRequests { } } - getRequestsInProgress(userId) { - const requestsByTxnId = this._requestsByUserId.get(userId); + public getRequestsInProgress(userId: string): Request[] { + const requestsByTxnId = this.requestsByUserId.get(userId); if (requestsByTxnId) { return Array.from(requestsByTxnId.values()).filter(r => r.pending); } diff --git a/src/crypto/verification/request/VerificationRequest.js b/src/crypto/verification/request/VerificationRequest.ts similarity index 68% rename from src/crypto/verification/request/VerificationRequest.js rename to src/crypto/verification/request/VerificationRequest.ts index 6a4f290b4..67af57930 100644 --- a/src/crypto/verification/request/VerificationRequest.js +++ b/src/crypto/verification/request/VerificationRequest.ts @@ -1,6 +1,5 @@ /* -Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +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. @@ -24,6 +23,11 @@ import { 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 { VerificationBase } from "../Base"; +import { VerificationMethod } from "../../index"; // How long after the event's timestamp that the request times out const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes @@ -44,12 +48,32 @@ export const CANCEL_TYPE = EVENT_PREFIX + "cancel"; export const DONE_TYPE = EVENT_PREFIX + "done"; export const READY_TYPE = EVENT_PREFIX + "ready"; -export const PHASE_UNSENT = 1; -export const PHASE_REQUESTED = 2; -export const PHASE_READY = 3; -export const PHASE_STARTED = 4; -export const PHASE_CANCELLED = 5; -export const PHASE_DONE = 6; +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; +} /** * State machine for verification requests. @@ -57,32 +81,38 @@ export const PHASE_DONE = 6; * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`. * @event "change" whenever the state of the request object has changed. */ -export class VerificationRequest extends EventEmitter { - constructor(channel, verificationMethods, client) { - super(); - this.channel = channel; - this.channel._request = this; - this._verificationMethods = verificationMethods; - this._client = client; - this._commonMethods = []; - this._setPhase(PHASE_UNSENT, false); - this._eventsByUs = new Map(); - this._eventsByThem = new Map(); - this._observeOnly = false; - this._timeoutTimer = null; - this._accepting = false; - this._declining = false; - this._verifierHasFinished = false; - this._cancelled = false; - this._chosenMethod = 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 - this._qrCodeData = null; +export class VerificationRequest extends EventEmitter { + private eventsByUs = new Map(); + private eventsByThem = new Map(); + private _observeOnly = false; + private timeoutTimer: number = null; + private _accepting = false; + private _declining = false; + private verifierHasFinished = false; + private _cancelled = false; + private _chosenMethod: VerificationMethod = 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; - // The timestamp when we received the request event from the other side - this._requestReceivedAt = null; + // The timestamp when we received the request event from the other side + private requestReceivedAt: number = null; + + private commonMethods: VerificationMethod[] = []; + private _phase: Phase; + private _cancellingUserId: string; + private _verifier: VerificationBase; + + constructor( + public readonly channel: C, + private readonly verificationMethods: Map, + private readonly client: MatrixClient, + ) { + super(); + this.channel.request = this; + this.setPhase(PHASE_UNSENT, false); } /** @@ -91,9 +121,9 @@ export class VerificationRequest extends EventEmitter { * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. * @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead. * @param {MatrixClient} client the client to get the current user and device id from - * @returns {bool} whether the event is valid and should be passed to handleEvent + * @returns {boolean} whether the event is valid and should be passed to handleEvent */ - static validateEvent(type, event, client) { + public static validateEvent(type: string, event: MatrixEvent, client: MatrixClient): boolean { const content = event.getContent(); if (!type || !type.startsWith(EVENT_PREFIX)) { @@ -128,53 +158,53 @@ export class VerificationRequest extends EventEmitter { return true; } - get invalid() { + public get invalid(): boolean { return this.phase === PHASE_UNSENT; } /** returns whether the phase is PHASE_REQUESTED */ - get requested() { + public get requested(): boolean { return this.phase === PHASE_REQUESTED; } /** returns whether the phase is PHASE_CANCELLED */ - get cancelled() { + public get cancelled(): boolean { return this.phase === PHASE_CANCELLED; } /** returns whether the phase is PHASE_READY */ - get ready() { + public get ready(): boolean { return this.phase === PHASE_READY; } /** returns whether the phase is PHASE_STARTED */ - get started() { + public get started(): boolean { return this.phase === PHASE_STARTED; } /** returns whether the phase is PHASE_DONE */ - get 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 */ - get methods() { - return this._commonMethods; + public get methods(): VerificationMethod[] { + return this.commonMethods; } /** the method picked in the .start event */ - get chosenMethod() { + public get chosenMethod(): VerificationMethod { return this._chosenMethod; } - calculateEventTimeout(event) { + public calculateEventTimeout(event: MatrixEvent): number { let effectiveExpiresAt = this.channel.getTimestamp(event) + TIMEOUT_FROM_EVENT_TS; - if (this._requestReceivedAt && !this.initiatedByMe && + if (this.requestReceivedAt && !this.initiatedByMe && this.phase <= PHASE_REQUESTED ) { - const expiresAtByReceipt = this._requestReceivedAt + const expiresAtByReceipt = this.requestReceivedAt + TIMEOUT_FROM_EVENT_RECEIPT; effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt); } @@ -183,8 +213,8 @@ export class VerificationRequest extends EventEmitter { } /** The current remaining amount of ms before the request should be automatically cancelled */ - get timeout() { - const requestEvent = this._getEventByEither(REQUEST_TYPE); + public get timeout(): number { + const requestEvent = this.getEventByEither(REQUEST_TYPE); if (requestEvent) { return this.calculateEventTimeout(requestEvent); } @@ -195,41 +225,41 @@ export class VerificationRequest extends EventEmitter { * The key verification request event. * @returns {MatrixEvent} The request event, or falsey if not found. */ - get requestEvent() { - return this._getEventByEither(REQUEST_TYPE); + public get requestEvent(): MatrixEvent { + return this.getEventByEither(REQUEST_TYPE); } /** current phase of the request. Some properties might only be defined in a current phase. */ - get 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. */ - get verifier() { + public get verifier(): VerificationBase { return this._verifier; } - get canAccept() { + public get canAccept(): boolean { return this.phase < PHASE_READY && !this._accepting && !this._declining; } - get accepting() { + public get accepting(): boolean { return this._accepting; } - get declining() { + public get declining(): boolean { return this._declining; } /** whether this request has sent it's initial event and needs more events to complete */ - get pending() { + 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 */ - get qrCodeData() { + public get qrCodeData(): QRCodeData { return this._qrCodeData; } @@ -239,19 +269,19 @@ export class VerificationRequest extends EventEmitter { * For methods that need to be supported by both ends, use the `methods` property. * @param {string} method the method to check * @param {boolean} force to check even if the phase is not ready or started yet, internal usage - * @return {bool} whether or not the other party said the supported the method */ - otherPartySupportsMethod(method, force = false) { + * @return {boolean} 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); + 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 myStartEvent = this.eventsByUs.get(START_TYPE); const content = myStartEvent && myStartEvent.getContent(); const myStartMethod = content && content.method; return method == myStartMethod; @@ -274,22 +304,22 @@ export class VerificationRequest extends EventEmitter { * For InRoomChannel, this is who sent the .request event. * For ToDeviceChannel, this is who sent the .start event */ - get initiatedByMe() { + 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; + 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); + 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); + const hasMyStart = this.eventsByUs.has(START_TYPE); + const hasTheirStart = this.eventsByThem.has(START_TYPE); if (hasMyStart && !hasTheirStart) { return true; } @@ -297,39 +327,39 @@ export class VerificationRequest extends EventEmitter { } /** The id of the user that initiated the request */ - get requestingUserId() { + public get requestingUserId(): string { if (this.initiatedByMe) { - return this._client.getUserId(); + return this.client.getUserId(); } else { return this.otherUserId; } } /** The id of the user that (will) receive(d) the request */ - get receivingUserId() { + public get receivingUserId(): string { if (this.initiatedByMe) { return this.otherUserId; } else { - return this._client.getUserId(); + return this.client.getUserId(); } } /** The user id of the other party in this request */ - get otherUserId() { + public get otherUserId(): string { return this.channel.userId; } - get isSelfVerification() { - return this._client.getUserId() === this.otherUserId; + 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 */ - get cancellingUserId() { - const myCancel = this._eventsByUs.get(CANCEL_TYPE); - const theirCancel = this._eventsByThem.get(CANCEL_TYPE); + public get cancellingUserId(): string { + const myCancel = this.eventsByUs.get(CANCEL_TYPE); + const theirCancel = this.eventsByThem.get(CANCEL_TYPE); if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) { return myCancel.getSender(); @@ -343,12 +373,12 @@ export class VerificationRequest extends EventEmitter { /** * The cancellation code e.g m.user which is responsible for cancelling this verification */ - get cancellationCode() { - const ev = this._getEventByEither(CANCEL_TYPE); + public get cancellationCode(): string { + const ev = this.getEventByEither(CANCEL_TYPE); return ev ? ev.getContent().code : null; } - get observeOnly() { + public get observeOnly(): boolean { return this._observeOnly; } @@ -359,11 +389,11 @@ export class VerificationRequest extends EventEmitter { * verification to when no specific device is specified. * @returns {{userId: *, deviceId: *}} The device information */ - get targetDevice() { + public get targetDevice(): ITargetDevice { const theirFirstEvent = - this._eventsByThem.get(REQUEST_TYPE) || - this._eventsByThem.get(READY_TYPE) || - this._eventsByThem.get(START_TYPE); + 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 { @@ -379,21 +409,20 @@ export class VerificationRequest extends EventEmitter { * @param {string?} targetDevice.deviceId the id of the device to direct this request to * @returns {VerifierBase} the verifier of the given method */ - beginKeyVerification(method, targetDevice = null) { + public beginKeyVerification(method: VerificationMethod, targetDevice: ITargetDevice = null): VerificationBase { // 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.constructor.canCreateRequest(START_TYPE)); + (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)) { + if (this.commonMethods.length && !this.commonMethods.includes(method)) { throw newUnknownMethodError(); } - this._verifier = this._createVerifier(method, null, targetDevice); + this._verifier = this.createVerifier(method, null, targetDevice); if (!this._verifier) { throw newUnknownMethodError(); } @@ -407,9 +436,9 @@ export class VerificationRequest extends EventEmitter { * sends the initial .request event. * @returns {Promise} resolves when the event has been sent. */ - async sendRequest() { + public async sendRequest(): Promise { if (!this.observeOnly && this._phase === PHASE_UNSENT) { - const methods = [...this._verificationMethods.keys()]; + const methods = [...this.verificationMethods.keys()]; await this.channel.send(REQUEST_TYPE, { methods }); } } @@ -420,14 +449,14 @@ export class VerificationRequest extends EventEmitter { * @param {string?} error.code the error code to send the cancellation with * @returns {Promise} resolves when the event has been sent. */ - async cancel({ reason = "User declined", code = "m.user" } = {}) { + public async cancel({ reason = "User declined", code = "m.user" } = {}): Promise { if (!this.observeOnly && this._phase !== PHASE_CANCELLED) { this._declining = true; this.emit("change"); if (this._verifier) { return this._verifier.cancel(errorFactory(code, reason)()); } else { - this._cancellingUserId = this._client.getUserId(); + this._cancellingUserId = this.client.getUserId(); await this.channel.send(CANCEL_TYPE, { code, reason }); } } @@ -437,9 +466,9 @@ export class VerificationRequest extends EventEmitter { * Accepts the request, sending a .ready event to the other party * @returns {Promise} resolves when the event has been sent. */ - async accept() { + public async accept(): Promise { if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) { - const methods = [...this._verificationMethods.keys()]; + const methods = [...this.verificationMethods.keys()]; this._accepting = true; this.emit("change"); await this.channel.send(READY_TYPE, { methods }); @@ -453,7 +482,7 @@ export class VerificationRequest extends EventEmitter { * @returns {Promise} that resolves once the callback returns true * @throws {Error} when the request is cancelled */ - waitFor(fn) { + public waitFor(fn: (request: VerificationRequest) => boolean): Promise { return new Promise((resolve, reject) => { const check = () => { let handled = false; @@ -475,46 +504,46 @@ export class VerificationRequest extends EventEmitter { }); } - _setPhase(phase, notify = true) { + private setPhase(phase: Phase, notify = true): void { this._phase = phase; if (notify) { this.emit("change"); } } - _getEventByEither(type) { - return this._eventsByThem.get(type) || this._eventsByUs.get(type); + private getEventByEither(type: string): MatrixEvent { + return this.eventsByThem.get(type) || this.eventsByUs.get(type); } - _getEventBy(type, byThem) { + private getEventBy(type: string, byThem = false): MatrixEvent { if (byThem) { - return this._eventsByThem.get(type); + return this.eventsByThem.get(type); } else { - return this._eventsByUs.get(type); + return this.eventsByUs.get(type); } } - _calculatePhaseTransitions() { - const transitions = [{ phase: PHASE_UNSENT }]; + private calculatePhaseTransitions(): ITransition[] { + const transitions: ITransition[] = [{ phase: PHASE_UNSENT }]; const 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); + 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); + requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem); if (readyEvent && phase() === PHASE_REQUESTED) { transitions.push({ phase: PHASE_READY, event: readyEvent }); } let startEvent; if (readyEvent || !requestEvent) { - const theirStartEvent = this._eventsByThem.get(START_TYPE); - const ourStartEvent = this._eventsByUs.get(START_TYPE); + 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() ? @@ -523,24 +552,22 @@ export class VerificationRequest extends EventEmitter { startEvent = theirStartEvent ? theirStartEvent : ourStartEvent; } } else { - startEvent = this._getEventBy(START_TYPE, !hasRequestByThem); + startEvent = this.getEventBy(START_TYPE, !hasRequestByThem); } if (startEvent) { - const fromRequestPhase = phase() === PHASE_REQUESTED && - requestEvent.getSender() !== startEvent.getSender(); - const fromUnsentPhase = phase() === PHASE_UNSENT && - this.channel.constructor.canCreateRequest(START_TYPE); + 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)) { + 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); + const cancelEvent = this.getEventByEither(CANCEL_TYPE); if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) { transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent }); return transitions; @@ -549,14 +576,14 @@ export class VerificationRequest extends EventEmitter { return transitions; } - _transitionToPhase(transition) { + private transitionToPhase(transition: ITransition): void { const { phase, event } = transition; // get common methods if (phase === PHASE_REQUESTED || phase === PHASE_READY) { - if (!this._wasSentByOwnDevice(event)) { + if (!this.wasSentByOwnDevice(event)) { const content = event.getContent(); - this._commonMethods = - content.methods.filter(m => this._verificationMethods.has(m)); + 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 @@ -568,8 +595,8 @@ export class VerificationRequest extends EventEmitter { ) { if ( this.channel.receiveStartFromOtherDevices && - this._wasSentByOwnUser(event) && - !this._wasSentByOwnDevice(event) + this.wasSentByOwnUser(event) && + !this.wasSentByOwnDevice(event) ) { this._observeOnly = true; } @@ -579,7 +606,7 @@ export class VerificationRequest extends EventEmitter { if (phase === PHASE_STARTED) { const { method } = event.getContent(); if (!this._verifier && !this.observeOnly) { - this._verifier = this._createVerifier(method, event); + this._verifier = this.createVerifier(method, event); if (!this._verifier) { this.cancel({ code: "m.unknown_method", @@ -592,19 +619,19 @@ export class VerificationRequest extends EventEmitter { } } - _applyPhaseTransitions() { - const transitions = this._calculatePhaseTransitions(); + 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); + this.transitionToPhase(transition); } return newTransitions; } - _isWinningStartRace(newEvent) { + private isWinningStartRace(newEvent: MatrixEvent): boolean { if (newEvent.getType() !== START_TYPE) { return false; } @@ -620,13 +647,13 @@ export class VerificationRequest extends EventEmitter { const oldContent = oldEvent.getContent(); oldRaceIdentifier = oldContent && oldContent.from_device; } else { - oldRaceIdentifier = this._client.getDeviceId(); + oldRaceIdentifier = this.client.getDeviceId(); } } else { if (oldEvent) { oldRaceIdentifier = oldEvent.getSender(); } else { - oldRaceIdentifier = this._client.getUserId(); + oldRaceIdentifier = this.client.getUserId(); } } @@ -640,13 +667,13 @@ export class VerificationRequest extends EventEmitter { return newRaceIdentifier < oldRaceIdentifier; } - hasEventId(eventId) { - for (const event of this._eventsByUs.values()) { + 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()) { + for (const event of this.eventsByThem.values()) { if (event.getId() === eventId) { return true; } @@ -658,23 +685,29 @@ export class VerificationRequest extends EventEmitter { * Changes the state of the request and verifier in response to a key verification event. * @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel. * @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead. - * @param {bool} isLiveEvent whether this is an even received through sync or not - * @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device - * @param {bool} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers. + * @param {boolean} isLiveEvent whether this is an even received through sync or not + * @param {boolean} isRemoteEcho whether this is the remote echo of an event sent by the same device + * @param {boolean} 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 {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent. + * @returns {Promise} a promise that resolves when any requests as an answer to the passed-in event are sent. */ - async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) { + public async handleEvent( + type: string, + event: MatrixEvent, + isLiveEvent: boolean, + isRemoteEcho: boolean, + isSentByUs: boolean, + ): Promise { // 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); + this.adjustObserveOnly(event, isLiveEvent); if (!this.observeOnly && !isRemoteEcho) { - if (await this._cancelOnError(type, event)) { + if (await this.cancelOnError(type, event)) { return; } } @@ -685,27 +718,26 @@ export class VerificationRequest extends EventEmitter { // 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); + this.eventsByUs.has(type) : + this.eventsByThem.has(type); if (isDuplicateEvent) { return; } const oldPhase = this.phase; - this._addEvent(type, event, isSentByUs); + this.addEvent(type, event, isSentByUs); // this will create if needed the verifier so needs to happen before calling it - const newTransitions = this._applyPhaseTransitions(); + 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); + 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 - && this._verifier.events.includes(type))) { + if (type === CANCEL_TYPE || this._verifier.events?.includes(type)) { this._verifier.handleEvent(event); } } @@ -722,16 +754,16 @@ export class VerificationRequest extends EventEmitter { const shouldGenerateQrCode = this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true); if (shouldGenerateQrCode) { - this._qrCodeData = await QRCodeData.create(this, this._client); + this._qrCodeData = await QRCodeData.create(this, this.client); } } const lastTransition = newTransitions[newTransitions.length - 1]; const { phase } = lastTransition; - this._setupTimeout(phase); + this.setupTimeout(phase); // set phase as last thing as this emits the "change" event - this._setPhase(phase); + this.setPhase(phase); } else if (this._observeOnly !== wasObserveOnly) { this.emit("change"); } @@ -748,26 +780,26 @@ export class VerificationRequest extends EventEmitter { } } - _setupTimeout(phase) { - const shouldTimeout = !this._timeoutTimer && !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); + this.timeoutTimer = setTimeout(this.cancelOnTimeout, this.timeout); } - if (this._timeoutTimer) { + 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; + clearTimeout(this.timeoutTimer); + this.timeoutTimer = null; } } } - _cancelOnTimeout = () => { + private cancelOnTimeout = () => { try { if (this.initiatedByMe) { this.cancel({ @@ -785,10 +817,10 @@ export class VerificationRequest extends EventEmitter { } }; - async _cancelOnError(type, event) { + private async cancelOnError(type: string, event: MatrixEvent): Promise { if (type === START_TYPE) { const method = event.getContent().method; - if (!this._verificationMethods.has(method)) { + if (!this.verificationMethods.has(method)) { await this.cancel(errorFromEvent(newUnknownMethodError())); return true; } @@ -811,7 +843,7 @@ export class VerificationRequest extends EventEmitter { return false; } - _adjustObserveOnly(event, isLiveEvent) { + private adjustObserveOnly(event: MatrixEvent, isLiveEvent = false): void { // don't send out events for historical requests if (!isLiveEvent) { this._observeOnly = true; @@ -821,83 +853,80 @@ export class VerificationRequest extends EventEmitter { } } - _addEvent(type, event, isSentByUs) { + private addEvent(type: string, event: MatrixEvent, isSentByUs = false): void { if (isSentByUs) { - this._eventsByUs.set(type, event); + this.eventsByUs.set(type, event); } else { - this._eventsByThem.set(type, event); + 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 + // see if any event by anyone else crept into this.eventsByThem if (type === REQUEST_TYPE) { - for (const [type, event] of this._eventsByThem.entries()) { + for (const [type, event] of this.eventsByThem.entries()) { if (event.getSender() !== this.otherUserId) { - this._eventsByThem.delete(type); + this.eventsByThem.delete(type); } } // also remember when we received the request event - this._requestReceivedAt = Date.now(); + this.requestReceivedAt = Date.now(); } } - _createVerifier(method, startEvent = null, targetDevice = null) { + private createVerifier( + method: VerificationMethod, + startEvent: MatrixEvent = null, + targetDevice: ITargetDevice = null, + ): VerificationBase { if (!targetDevice) { targetDevice = this.targetDevice; } const { userId, deviceId } = targetDevice; - const VerifierCtor = this._verificationMethods.get(method); + 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, - ); + return new VerifierCtor(this.channel, this.client, userId, deviceId, startEvent, this); } - _wasSentByOwnUser(event) { - return event.getSender() === this._client.getUserId(); + private wasSentByOwnUser(event: MatrixEvent): boolean { + return event.getSender() === this.client.getUserId(); } // only for .request, .ready or .start - _wasSentByOwnDevice(event) { - if (!this._wasSentByOwnUser(event)) { + private wasSentByOwnDevice(event: MatrixEvent): boolean { + if (!this.wasSentByOwnUser(event)) { return false; } const content = event.getContent(); - if (!content || content.from_device !== this._client.getDeviceId()) { + if (!content || content.from_device !== this.client.getDeviceId()) { return false; } return true; } - onVerifierCancelled() { + public onVerifierCancelled(): void { this._cancelled = true; // move to cancelled phase - const newTransitions = this._applyPhaseTransitions(); + const newTransitions = this.applyPhaseTransitions(); if (newTransitions.length) { - this._setPhase(newTransitions[newTransitions.length - 1].phase); + this.setPhase(newTransitions[newTransitions.length - 1].phase); } } - onVerifierFinished() { + public onVerifierFinished(): void { this.channel.send("m.key.verification.done", {}); - this._verifierHasFinished = true; + this.verifierHasFinished = true; // move to .done phase - const newTransitions = this._applyPhaseTransitions(); + const newTransitions = this.applyPhaseTransitions(); if (newTransitions.length) { - this._setPhase(newTransitions[newTransitions.length - 1].phase); + this.setPhase(newTransitions[newTransitions.length - 1].phase); } } - getEventFromOtherParty(type) { - return this._eventsByThem.get(type); + public getEventFromOtherParty(type: string): MatrixEvent { + return this.eventsByThem.get(type); } }