1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-25 05:23:13 +03:00

Convert crypto/verification/* to Typescript

This commit is contained in:
Michael Telatynski
2021-09-08 12:34:44 +01:00
parent 0991626e85
commit b6c857e2a2
12 changed files with 692 additions and 625 deletions

View File

@@ -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";

View File

@@ -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,7 +748,11 @@ 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;
@@ -754,7 +760,7 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
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) => {
return new Promise<KeysDuringVerification>((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<void>((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<KeysDuringVerification>([
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) => {

View File

@@ -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<IDevice>, deviceId: string): DeviceInfo {
const res = new DeviceInfo(deviceId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {

View File

@@ -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<VerificationRequest> {
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<VerificationRequest> {
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<VerificationRequest> {
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<VerificationRequest> {
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

View File

@@ -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<void> = 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<string, any>): Promise<void> {
return this.channel.send(type, uncompletedContent);
}
_waitForEvent(type) {
protected waitForEvent(type: string): Promise<MatrixEvent> {
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<KeysDuringVerification> {
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<void> {
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<void>;
protected async verifyKeys(userId: string, keys: Record<string, string>, verifier: KeyVerifier): Promise<void> {
// we try to verify all the keys that we're told about, but we might
// not know about all of them, so keep track of the keys that we know
// about, and ignore the rest
@@ -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;
}
}

View File

@@ -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<string, any>): 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<string, any>) => MatrixEvent {
return function(extraData?: Record<string, any>) {
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;

View File

@@ -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<void> => {
throw new Error("Verification is not possible with this method");
}
};
}

View File

@@ -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<void> => {
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<void>((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<string, string> = {};
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<QRCodeData> {
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<string> {
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) => {

View File

@@ -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<T>(anArray: T[], aSet: Set<T>): 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<void>;
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<void> => {
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<Record<string, any>> {
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<void> {
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<void>((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<void> {
// as m.related_to is not included in the encrypted content in e2e rooms,
// we need to make sure it is added
let content = this._channel.completedContentFromEvent(this.startEvent);
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<void>((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<void> {
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<void> {
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,

View File

@@ -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,33 +35,31 @@ 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;
@@ -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<void> {
// prevent processing the same event multiple times, as under
// some circumstances Room.timeline can get emitted twice for the same event
if (request.hasEventId(event.getId())) {
@@ -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<string, any> {
// ensure m.related_to is included in e2ee rooms
// as the field is excluded from encryption
const content = Object.assign({}, event.getContent());
content[M_RELATES_TO] = event.getRelation();
return content;
}
/**
* Add all the fields to content needed for sending it over this channel.
* This is public so verification methods (SAS uses this) can get the exact
@@ -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<string, any>): Record<string, any> {
content = Object.assign({}, content);
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
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<string, any>): Promise<void> {
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<string, any>): Promise<void> {
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<string, Map<string, VerificationRequest>>();
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) {

View File

@@ -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<ToDeviceChannel>;
/**
* 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<void> {
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<string, any> {
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<string, any>): Record<string, any> {
// make a copy
content = Object.assign({}, content);
if (this.transactionId) {
content.transaction_id = this.transactionId;
}
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
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<string, any> = {}): Promise<void> {
// 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<string, any>): Promise<void> {
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<string, any>, devices: string[]): Promise<void> {
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<string, Map<string, Request>>();
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);
}

View File

@@ -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;
export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends EventEmitter {
private eventsByUs = new Map<string, MatrixEvent>();
private eventsByThem = new Map<string, MatrixEvent>();
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
this._qrCodeData = null;
private _qrCodeData: QRCodeData = null;
// The timestamp when we received the request event from the other side
this._requestReceivedAt = null;
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<VerificationMethod, typeof VerificationBase>,
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<void> {
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<void> {
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<void> {
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<VerificationRequest> {
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<void> {
// if reached phase cancelled or done, ignore anything else that comes
if (this.done || this.cancelled) {
return;
}
const wasObserveOnly = this._observeOnly;
this._adjustObserveOnly(event, isLiveEvent);
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<boolean> {
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);
}
}