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 { SyncState } from "./sync.api";
import { EventTimelineSet } from "./models/event-timeline-set"; import { EventTimelineSet } from "./models/event-timeline-set";
import { VerificationRequest } from "./crypto/verification/request/VerificationRequest"; 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 * as ContentHelpers from "./content-helpers";
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning"; import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
import { Room } from "./models/room"; 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. * Request cross-signing keys from another device during verification.
* *
@@ -746,15 +748,19 @@ export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: O
* @param {string} userId The user ID being verified * @param {string} userId The user ID being verified
* @param {string} deviceId The device 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 this is a self-verification, ask the other party for keys
if (baseApis.getUserId() !== userId) { if (baseApis.getUserId() !== userId) {
return; return;
} }
logger.log("Cross-signing: Self-verification done; requesting keys"); logger.log("Cross-signing: Self-verification done; requesting keys");
// This happens asynchronously, and we're not concerned about waiting for // This happens asynchronously, and we're not concerned about waiting for
// it. We return here in order to test. // it. We return here in order to test.
return new Promise((resolve, reject) => { return new Promise<KeysDuringVerification>((resolve, reject) => {
const client = baseApis; const client = baseApis;
const original = client.crypto.crossSigningInfo; 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 // https://github.com/vector-im/element-web/issues/12604
// then change here to reject on the timeout // then change here to reject on the timeout
// Requests can be ignored, so don't wait around forever // Requests can be ignored, so don't wait around forever
const timeout = new Promise((resolve, reject) => { const timeout = new Promise<void>((resolve) => {
setTimeout( setTimeout(
resolve, resolve,
KEY_REQUEST_TIMEOUT_MS, KEY_REQUEST_TIMEOUT_MS,
@@ -814,13 +820,13 @@ export async function requestKeysDuringVerification(baseApis: MatrixClient, user
})(); })();
// We call getCrossSigningKey() for its side-effects // We call getCrossSigningKey() for its side-effects
return Promise.race([ return Promise.race<KeysDuringVerification>([
Promise.all([ Promise.all([
crossSigning.getCrossSigningKey("master"), crossSigning.getCrossSigningKey("master"),
crossSigning.getCrossSigningKey("self_signing"), crossSigning.getCrossSigningKey("self_signing"),
crossSigning.getCrossSigningKey("user_signing"), crossSigning.getCrossSigningKey("user_signing"),
backupKeyPromise, backupKeyPromise,
]), ]) as Promise<[[string, PkSigning], [string, PkSigning], [string, PkSigning], void]>,
timeout, timeout,
]).then(resolve, reject); ]).then(resolve, reject);
}).catch((e) => { }).catch((e) => {

View File

@@ -68,7 +68,7 @@ export class DeviceInfo {
* *
* @return {module:crypto~DeviceInfo} new 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); const res = new DeviceInfo(deviceId);
for (const prop in obj) { for (const prop in obj) {
if (obj.hasOwnProperty(prop)) { if (obj.hasOwnProperty(prop)) {

View File

@@ -26,7 +26,7 @@ import { EventEmitter } from 'events';
import { ReEmitter } from '../ReEmitter'; import { ReEmitter } from '../ReEmitter';
import { logger } from '../logger'; import { logger } from '../logger';
import { OlmDevice } from "./OlmDevice"; import { IExportedDevice, OlmDevice } from "./OlmDevice";
import * as olmlib from "./olmlib"; import * as olmlib from "./olmlib";
import { DeviceInfoMap, DeviceList } from "./DeviceList"; import { DeviceInfoMap, DeviceList } from "./DeviceList";
import { DeviceInfo, IDevice } from "./deviceinfo"; import { DeviceInfo, IDevice } from "./deviceinfo";
@@ -66,6 +66,7 @@ import { IRecoveryKey, IEncryptedEventInfo } from "./api";
import { IKeyBackupInfo } from "./keybackup"; import { IKeyBackupInfo } from "./keybackup";
import { ISyncStateData } from "../sync"; import { ISyncStateData } from "../sync";
import { CryptoStore } from "./store/base"; import { CryptoStore } from "./store/base";
import { IVerificationChannel } from "./verification/request/Channel";
const DeviceVerification = DeviceInfo.DeviceVerification; const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -84,12 +85,12 @@ const defaultVerificationMethods = {
* verification method names * verification method names
*/ */
// legacy export identifier // legacy export identifier
export enum verificationMethods { export const verificationMethods = {
RECIPROCATE_QR_CODE = ReciprocateQRCode.NAME, RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
SAS = SASVerification.NAME, SAS: SASVerification.NAME,
} };
export type VerificationMethod = verificationMethods; export type VerificationMethod = keyof typeof verificationMethods | string;
export function isCryptoAvailable(): boolean { export function isCryptoAvailable(): boolean {
return Boolean(global.Olm); return Boolean(global.Olm);
@@ -98,7 +99,7 @@ export function isCryptoAvailable(): boolean {
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
interface IInitOpts { interface IInitOpts {
exportedOlmDevice?: any; // TODO types exportedOlmDevice?: IExportedDevice;
pickleKey?: string; pickleKey?: string;
} }
@@ -2177,11 +2178,11 @@ export class Crypto extends EventEmitter {
return this.inRoomVerificationRequests.findRequestInProgress(roomId); return this.inRoomVerificationRequests.findRequestInProgress(roomId);
} }
public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest { public getVerificationRequestsToDeviceInProgress(userId: string): VerificationRequest[] {
return this.toDeviceVerificationRequests.getRequestsInProgress(userId); 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); const existingRequest = this.inRoomVerificationRequests.findRequestInProgress(roomId);
if (existingRequest) { if (existingRequest) {
return Promise.resolve(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) { if (!devices) {
devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId)); devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId));
} }
@@ -2212,9 +2213,9 @@ export class Crypto extends EventEmitter {
private async requestVerificationWithChannel( private async requestVerificationWithChannel(
userId: string, userId: string,
channel: any, // TODO types channel: IVerificationChannel,
requestsMap: any, // TODO types requestsMap: any, // TODO types
): VerificationRequest { ): Promise<VerificationRequest> {
let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis); let request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
// if transaction id is already known, add request // if transaction id is already known, add request
if (channel.transactionId) { if (channel.transactionId) {
@@ -2260,14 +2261,11 @@ export class Crypto extends EventEmitter {
userId: string, userId: string,
deviceId: string, deviceId: string,
method: VerificationMethod, method: VerificationMethod,
): VerificationRequest { ): Promise<VerificationRequest> {
const transactionId = ToDeviceChannel.makeTransactionId(); const transactionId = ToDeviceChannel.makeTransactionId();
const channel = new ToDeviceChannel( const channel = new ToDeviceChannel(this.baseApis, userId, [deviceId], transactionId, deviceId);
this.baseApis, userId, [deviceId], transactionId, deviceId); const request = new VerificationRequest(channel, this.verificationMethods, this.baseApis);
const request = new VerificationRequest( this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(userId, transactionId, request);
channel, this.verificationMethods, this.baseApis);
this.toDeviceVerificationRequests.setRequestBySenderAndTxnId(
userId, transactionId, request);
const verifier = request.beginKeyVerification(method, { userId, deviceId }); const verifier = request.beginKeyVerification(method, { userId, deviceId });
// either reject by an error from verify() while sending .start // either reject by an error from verify() while sending .start
// or resolve when the request receives the // or resolve when the request receives the

View File

@@ -25,18 +25,33 @@ import { EventEmitter } from 'events';
import { logger } from '../../logger'; import { logger } from '../../logger';
import { DeviceInfo } from '../deviceinfo'; import { DeviceInfo } from '../deviceinfo';
import { newTimeoutError } from "./Error"; 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"); const timeoutException = new Error("Verification timed out");
export class SwitchStartEventError extends Error { export class SwitchStartEventError extends Error {
constructor(startEvent) { constructor(public readonly startEvent: MatrixEvent) {
super(); super();
this.startEvent = startEvent;
} }
} }
export type KeyVerifier = (keyId: string, device: DeviceInfo, keyInfo: string) => void;
export class VerificationBase extends EventEmitter { 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. * Base class for verification methods.
* *
@@ -64,22 +79,18 @@ export class VerificationBase extends EventEmitter {
* @param {object} [request] the key verification request object related to * @param {object} [request] the key verification request object related to
* this verification, if any * 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(); 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, // if there is no start event yet,
// we probably want to send it, // we probably want to send it,
// which happens if we initiate // which happens if we initiate
@@ -88,16 +99,16 @@ export class VerificationBase extends EventEmitter {
} }
const sender = this.startEvent.getSender(); const sender = this.startEvent.getSender();
const content = this.startEvent.getContent(); const content = this.startEvent.getContent();
return sender === this._baseApis.getUserId() && return sender === this.baseApis.getUserId() &&
content.from_device === this._baseApis.getDeviceId(); content.from_device === this.baseApis.getDeviceId();
} }
_resetTimer() { private resetTimer(): void {
logger.info("Refreshing/starting the verification transaction timeout timer"); logger.info("Refreshing/starting the verification transaction timeout timer");
if (this._transactionTimeoutTimer !== null) { if (this.transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer); clearTimeout(this.transactionTimeoutTimer);
} }
this._transactionTimeoutTimer = setTimeout(() => { this.transactionTimeoutTimer = setTimeout(() => {
if (!this._done && !this.cancelled) { if (!this._done && !this.cancelled) {
logger.info("Triggering verification timeout"); logger.info("Triggering verification timeout");
this.cancel(timeoutException); this.cancel(timeoutException);
@@ -105,18 +116,18 @@ export class VerificationBase extends EventEmitter {
}, 10 * 60 * 1000); // 10 minutes }, 10 * 60 * 1000); // 10 minutes
} }
_endTimer() { private endTimer(): void {
if (this._transactionTimeoutTimer !== null) { if (this.transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer); clearTimeout(this.transactionTimeoutTimer);
this._transactionTimeoutTimer = null; this.transactionTimeoutTimer = null;
} }
} }
_send(type, uncompletedContent) { protected send(type: string, uncompletedContent: Record<string, any>): Promise<void> {
return this._channel.send(type, uncompletedContent); return this.channel.send(type, uncompletedContent);
} }
_waitForEvent(type) { protected waitForEvent(type: string): Promise<MatrixEvent> {
if (this._done) { if (this._done) {
return Promise.reject(new Error("Verification is already done")); return Promise.reject(new Error("Verification is already done"));
} }
@@ -125,24 +136,24 @@ export class VerificationBase extends EventEmitter {
return Promise.resolve(existingEvent); return Promise.resolve(existingEvent);
} }
this._expectedEvent = type; this.expectedEvent = type;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._resolveEvent = resolve; this.resolveEvent = resolve;
this._rejectEvent = reject; this.rejectEvent = reject;
}); });
} }
canSwitchStartEvent() { public canSwitchStartEvent(event: MatrixEvent): boolean {
return false; return false;
} }
switchStartEvent(event) { public switchStartEvent(event: MatrixEvent): void {
if (this.canSwitchStartEvent(event)) { if (this.canSwitchStartEvent(event)) {
logger.log("Verification Base: switching verification start event", logger.log("Verification Base: switching verification start event",
{ restartingFlow: !!this._rejectEvent }); { restartingFlow: !!this.rejectEvent });
if (this._rejectEvent) { if (this.rejectEvent) {
const reject = this._rejectEvent; const reject = this.rejectEvent;
this._rejectEvent = undefined; this.rejectEvent = undefined;
reject(new SwitchStartEventError(event)); reject(new SwitchStartEventError(event));
} else { } else {
this.startEvent = event; this.startEvent = event;
@@ -150,21 +161,21 @@ export class VerificationBase extends EventEmitter {
} }
} }
handleEvent(e) { public handleEvent(e: MatrixEvent): void {
if (this._done) { if (this._done) {
return; return;
} else if (e.getType() === this._expectedEvent) { } else if (e.getType() === this.expectedEvent) {
// if we receive an expected m.key.verification.done, then just // if we receive an expected m.key.verification.done, then just
// ignore it, since we don't need to do anything about it // ignore it, since we don't need to do anything about it
if (this._expectedEvent !== "m.key.verification.done") { if (this.expectedEvent !== "m.key.verification.done") {
this._expectedEvent = undefined; this.expectedEvent = undefined;
this._rejectEvent = undefined; this.rejectEvent = undefined;
this._resetTimer(); this.resetTimer();
this._resolveEvent(e); this.resolveEvent(e);
} }
} else if (e.getType() === "m.key.verification.cancel") { } else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject; const reject = this.reject;
this._reject = undefined; this.reject = undefined;
// there is only promise to reject if verify has been called // there is only promise to reject if verify has been called
if (reject) { if (reject) {
const content = e.getContent(); const content = e.getContent();
@@ -172,36 +183,36 @@ export class VerificationBase extends EventEmitter {
reject(new Error(`Other side cancelled verification ` + reject(new Error(`Other side cancelled verification ` +
`because ${reason} (${code})`)); `because ${reason} (${code})`));
} }
} else if (this._expectedEvent) { } else if (this.expectedEvent) {
// only cancel if there is an event expected. // only cancel if there is an event expected.
// if there is no event expected, it means verify() wasn't called // if there is no event expected, it means verify() wasn't called
// and we're just replaying the timeline events when syncing // and we're just replaying the timeline events when syncing
// after a refresh when the events haven't been stored in the cache yet. // after a refresh when the events haven't been stored in the cache yet.
const exception = new Error( const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent "Unexpected message: expecting " + this.expectedEvent
+ " but got " + e.getType(), + " but got " + e.getType(),
); );
this._expectedEvent = undefined; this.expectedEvent = undefined;
if (this._rejectEvent) { if (this.rejectEvent) {
const reject = this._rejectEvent; const reject = this.rejectEvent;
this._rejectEvent = undefined; this.rejectEvent = undefined;
reject(exception); reject(exception);
} }
this.cancel(exception); this.cancel(exception);
} }
} }
done() { public done(): Promise<KeysDuringVerification> {
this._endTimer(); // always kill the activity timer this.endTimer(); // always kill the activity timer
if (!this._done) { if (!this._done) {
this.request.onVerifierFinished(); this.request.onVerifierFinished();
this._resolve(); this.resolve();
return requestKeysDuringVerification(this._baseApis, this.userId, this.deviceId); return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId);
} }
} }
cancel(e) { public cancel(e: Error | MatrixEvent): void {
this._endTimer(); // always kill the activity timer this.endTimer(); // always kill the activity timer
if (!this._done) { if (!this._done) {
this.cancelled = true; this.cancelled = true;
this.request.onVerifierCancelled(); this.request.onVerifierCancelled();
@@ -210,7 +221,7 @@ export class VerificationBase extends EventEmitter {
// cancelled by the other user) // cancelled by the other user)
if (e === timeoutException) { if (e === timeoutException) {
const timeoutEvent = newTimeoutError(); const timeoutEvent = newTimeoutError();
this._send(timeoutEvent.getType(), timeoutEvent.getContent()); this.send(timeoutEvent.getType(), timeoutEvent.getContent());
} else if (e instanceof MatrixEvent) { } else if (e instanceof MatrixEvent) {
const sender = e.getSender(); const sender = e.getSender();
if (sender !== this.userId) { if (sender !== this.userId) {
@@ -219,29 +230,29 @@ export class VerificationBase extends EventEmitter {
content.code = content.code || "m.unknown"; content.code = content.code || "m.unknown";
content.reason = content.reason || content.body content.reason = content.reason || content.body
|| "Unknown reason"; || "Unknown reason";
this._send("m.key.verification.cancel", content); this.send("m.key.verification.cancel", content);
} else { } else {
this._send("m.key.verification.cancel", { this.send("m.key.verification.cancel", {
code: "m.unknown", code: "m.unknown",
reason: content.body || "Unknown reason", reason: content.body || "Unknown reason",
}); });
} }
} }
} else { } else {
this._send("m.key.verification.cancel", { this.send("m.key.verification.cancel", {
code: "m.unknown", code: "m.unknown",
reason: e.toString(), reason: e.toString(),
}); });
} }
} }
if (this._promise !== null) { if (this.promise !== null) {
// when we cancel without a promise, we end up with a promise // when we cancel without a promise, we end up with a promise
// but no reject function. If cancel is called again, we'd error. // 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 { } else {
// FIXME: this causes an "Uncaught promise" console message // FIXME: this causes an "Uncaught promise" console message
// if nothing ends up chaining this promise. // 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 // Also emit a 'cancel' event that the app can listen for to detect cancellation
// before calling verify() // before calling verify()
@@ -255,31 +266,32 @@ export class VerificationBase extends EventEmitter {
* @returns {Promise} Promise which resolves when the verification has * @returns {Promise} Promise which resolves when the verification has
* completed. * completed.
*/ */
verify() { public verify(): Promise<void> {
if (this._promise) return this._promise; if (this.promise) return this.promise;
this._promise = new Promise((resolve, reject) => { this.promise = new Promise((resolve, reject) => {
this._resolve = (...args) => { this.resolve = (...args) => {
this._done = true; this._done = true;
this._endTimer(); this.endTimer();
resolve(...args); resolve(...args);
}; };
this._reject = (...args) => { this.reject = (e: Error) => {
this._done = true; this._done = true;
this._endTimer(); this.endTimer();
reject(...args); reject(e);
}; };
}); });
if (this._doVerification && !this._started) { if (this.doVerification && !this.started) {
this._started = true; this.started = true;
this._resetTimer(); // restart the timeout this.resetTimer(); // restart the timeout
Promise.resolve(this._doVerification()) Promise.resolve(this.doVerification()).then(this.done.bind(this), this.cancel.bind(this));
.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 // 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 // not know about all of them, so keep track of the keys that we know
// about, and ignore the rest // about, and ignore the rest
@@ -287,15 +299,14 @@ export class VerificationBase extends EventEmitter {
for (const [keyId, keyInfo] of Object.entries(keys)) { for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1]; const deviceId = keyId.split(':', 2)[1];
const device = this._baseApis.getStoredDevice(userId, deviceId); const device = this.baseApis.getStoredDevice(userId, deviceId);
if (device) { if (device) {
await verifier(keyId, device, keyInfo); verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId); verifiedDevices.push(deviceId);
} else { } else {
const crossSigningInfo = this._baseApis.crypto.deviceList const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId);
.getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
await verifier(keyId, DeviceInfo.fromStorage({ verifier(keyId, DeviceInfo.fromStorage({
keys: { keys: {
[keyId]: deviceId, [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 // to upload each signature in a separate API call which is silly because the
// API supports as many signatures as you like. // API supports as many signatures as you like.
for (const deviceId of verifiedDevices) { 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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"; import { MatrixEvent } from "../../models/event";
export function newVerificationError(code, reason, extradata) { export function newVerificationError(code: string, reason: string, extraData: Record<string, any>): MatrixEvent {
const content = Object.assign({}, { code, reason }, extradata); const content = Object.assign({}, { code, reason }, extraData);
return new MatrixEvent({ return new MatrixEvent({
type: "m.key.verification.cancel", type: "m.key.verification.cancel",
content, content,
}); });
} }
export function errorFactory(code, reason) { export function errorFactory(code: string, reason: string): (extraData?: Record<string, any>) => MatrixEvent {
return function(extradata) { return function(extraData?: Record<string, any>) {
return newVerificationError(code, reason, extradata); return newVerificationError(code, reason, extraData);
}; };
} }
@@ -84,7 +84,7 @@ export const newInvalidMessageError = errorFactory(
"m.invalid_message", "Invalid message", "m.invalid_message", "Invalid message",
); );
export function errorFromEvent(event) { export function errorFromEvent(event: MatrixEvent): { code: string, reason: string } {
const content = event.getContent(); const content = event.getContent();
if (content) { if (content) {
const { code, reason } = content; const { code, reason } = content;

View File

@@ -21,23 +21,35 @@ limitations under the License.
*/ */
import { VerificationBase as Base } from "./Base"; 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 * @class crypto/verification/IllegalMethod/IllegalMethod
* @extends {module:crypto/verification/Base} * @extends {module:crypto/verification/Base}
*/ */
export class IllegalMethod extends Base { export class IllegalMethod extends Base {
static factory(...args) { public static factory(
return new IllegalMethod(...args); 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 // Typically the name will be something else, but to complete
// the contract we offer a default one here. // the contract we offer a default one here.
return "org.matrix.illegal_method"; return "org.matrix.illegal_method";
} }
async _doVerification() { protected doVerification = async (): Promise<void> => {
throw new Error("Verification is not possible with this method"); throw new Error("Verification is not possible with this method");
} };
} }

View File

@@ -1,6 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 { VerificationBase as Base } from "./Base";
import { import { newKeyMismatchError, newUserCancelledError } from './Error';
newKeyMismatchError, import { decodeBase64, encodeUnpaddedBase64 } from "../olmlib";
newUserCancelledError,
} from './Error';
import { encodeUnpaddedBase64, decodeBase64 } from "../olmlib";
import { logger } from '../../logger'; 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 SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.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} * @extends {module:crypto/verification/Base}
*/ */
export class ReciprocateQRCode extends Base { export class ReciprocateQRCode extends Base {
static factory(...args) { public reciprocateQREvent: {
return new ReciprocateQRCode(...args); 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"; return "m.reciprocate.v1";
} }
async _doVerification() { protected doVerification = async (): Promise<void> => {
if (!this.startEvent) { if (!this.startEvent) {
// TODO: Support scanning QR codes // TODO: Support scanning QR codes
throw new Error("It is not currently possible to start verification" + 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 // 2. ask if other user shows shield as well
await new Promise((resolve, reject) => { await new Promise<void>((resolve, reject) => {
this.reciprocateQREvent = { this.reciprocateQREvent = {
confirm: resolve, confirm: resolve,
cancel: () => reject(newUserCancelledError()), cancel: () => reject(newUserCancelledError()),
@@ -67,21 +80,21 @@ export class ReciprocateQRCode extends Base {
}); });
// 3. determine key to sign / mark as trusted // 3. determine key to sign / mark as trusted
const keys = {}; const keys: Record<string, string> = {};
switch (qrCodeData.mode) { 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 // add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey; const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey; keys[`ed25519:${masterKey}`] = masterKey;
break; break;
} }
case MODE_VERIFY_SELF_TRUSTED: { case Mode.VerifySelfTrusted: {
const deviceId = this.request.targetDevice.deviceId; const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey; keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
break; break;
} }
case MODE_VERIFY_SELF_UNTRUSTED: { case Mode.VerifySelfUntrusted: {
const masterKey = qrCodeData.myMasterKey; const masterKey = qrCodeData.myMasterKey;
keys[`ed25519:${masterKey}`] = masterKey; keys[`ed25519:${masterKey}`] = masterKey;
break; 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) // 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 // make sure the device has the expected keys
const targetKey = keys[keyId]; const targetKey = keys[keyId];
if (!targetKey) throw newKeyMismatchError(); 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 CODE_VERSION = 0x02; // the version of binary QR codes we support
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format 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 enum Mode {
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key 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 { export class QRCodeData {
constructor( constructor(
mode, sharedSecret, otherUserMasterKey, public readonly mode: Mode,
otherDeviceKey, myMasterKey, buffer, private readonly sharedSecret: string,
) { // only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code
this._sharedSecret = sharedSecret; public readonly otherUserMasterKey: string | undefined,
this._mode = mode; // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code
this._otherUserMasterKey = otherUserMasterKey; public readonly otherDeviceKey: string | undefined,
this._otherDeviceKey = otherDeviceKey; // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code
this._myMasterKey = myMasterKey; public readonly myMasterKey: string | undefined,
this._buffer = buffer; private readonly buffer: Buffer,
} ) {}
static async create(request, client) { public static async create(request: VerificationRequest, client: MatrixClient): Promise<QRCodeData> {
const sharedSecret = QRCodeData._generateSharedSecret(); const sharedSecret = QRCodeData.generateSharedSecret();
const mode = QRCodeData._determineMode(request, client); const mode = QRCodeData.determineMode(request, client);
let otherUserMasterKey = null; let otherUserMasterKey = null;
let otherDeviceKey = null; let otherDeviceKey = null;
let myMasterKey = null; let myMasterKey = null;
if (mode === MODE_VERIFY_OTHER_USER) { if (mode === Mode.VerifyOtherUser) {
const otherUserCrossSigningInfo = const otherUserCrossSigningInfo =
client.getStoredCrossSigningForUser(request.otherUserId); client.getStoredCrossSigningForUser(request.otherUserId);
otherUserMasterKey = otherUserCrossSigningInfo.getId("master"); otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) { } else if (mode === Mode.VerifySelfTrusted) {
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client); otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client);
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) { } else if (mode === Mode.VerifySelfUntrusted) {
const myUserId = client.getUserId(); const myUserId = client.getUserId();
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId); const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
myMasterKey = myCrossSigningInfo.getId("master"); myMasterKey = myCrossSigningInfo.getId("master");
} }
const qrData = QRCodeData._generateQrData( const qrData = QRCodeData.generateQrData(
request, client, mode, request, client, mode,
sharedSecret, sharedSecret,
otherUserMasterKey, otherUserMasterKey,
otherDeviceKey, otherDeviceKey,
myMasterKey, myMasterKey,
); );
const buffer = QRCodeData._generateBuffer(qrData); const buffer = QRCodeData.generateBuffer(qrData);
return new QRCodeData(mode, sharedSecret, return new QRCodeData(mode, sharedSecret,
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer); 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. * The unpadded base64 encoded shared secret.
*/ */
get encodedSharedSecret() { public get encodedSharedSecret(): string {
return this._sharedSecret; return this.sharedSecret;
} }
static _generateSharedSecret() { private static generateSharedSecret(): string {
const secretBytes = new Uint8Array(11); const secretBytes = new Uint8Array(11);
global.crypto.getRandomValues(secretBytes); global.crypto.getRandomValues(secretBytes);
return encodeUnpaddedBase64(secretBytes); return encodeUnpaddedBase64(secretBytes);
} }
static async _getOtherDeviceKey(request, client) { private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise<string> {
const myUserId = client.getUserId(); const myUserId = client.getUserId();
const otherDevice = request.targetDevice; const otherDevice = request.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null; const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
@@ -212,31 +206,35 @@ export class QRCodeData {
if (!device) { if (!device) {
throw new Error("could not find device " + otherDeviceId); throw new Error("could not find device " + otherDeviceId);
} }
const key = device.getFingerprint(); return device.getFingerprint();
return key;
} }
static _determineMode(request, client) { private static determineMode(request: VerificationRequest, client: MatrixClient): Mode {
const myUserId = client.getUserId(); const myUserId = client.getUserId();
const otherUserId = request.otherUserId; const otherUserId = request.otherUserId;
let mode = MODE_VERIFY_OTHER_USER; let mode = Mode.VerifyOtherUser;
if (myUserId === otherUserId) { if (myUserId === otherUserId) {
// Mode changes depending on whether or not we trust the master cross signing key // Mode changes depending on whether or not we trust the master cross signing key
const myTrust = client.checkUserTrust(myUserId); const myTrust = client.checkUserTrust(myUserId);
if (myTrust.isCrossSigningVerified()) { if (myTrust.isCrossSigningVerified()) {
mode = MODE_VERIFY_SELF_TRUSTED; mode = Mode.VerifySelfTrusted;
} else { } else {
mode = MODE_VERIFY_SELF_UNTRUSTED; mode = Mode.VerifySelfUntrusted;
} }
} }
return mode; return mode;
} }
static _generateQrData(request, client, mode, private static generateQrData(
encodedSharedSecret, otherUserMasterKey, request: VerificationRequest,
otherDeviceKey, myMasterKey, client: MatrixClient,
) { mode: Mode,
encodedSharedSecret: string,
otherUserMasterKey: string,
otherDeviceKey: string,
myMasterKey: string,
): IQrData {
const myUserId = client.getUserId(); const myUserId = client.getUserId();
const transactionId = request.channel.transactionId; const transactionId = request.channel.transactionId;
const qrData = { const qrData = {
@@ -251,16 +249,16 @@ export class QRCodeData {
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId); const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
if (mode === MODE_VERIFY_OTHER_USER) { if (mode === Mode.VerifyOtherUser) {
// First key is our master cross signing key // First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other user's master cross signing key // Second key is the other user's master cross signing key
qrData.secondKeyB64 = otherUserMasterKey; qrData.secondKeyB64 = otherUserMasterKey;
} else if (mode === MODE_VERIFY_SELF_TRUSTED) { } else if (mode === Mode.VerifySelfTrusted) {
// First key is our master cross signing key // First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
qrData.secondKeyB64 = otherDeviceKey; qrData.secondKeyB64 = otherDeviceKey;
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) { } else if (mode === Mode.VerifySelfUntrusted) {
// First key is our device's key // First key is our device's key
qrData.firstKeyB64 = client.getDeviceEd25519Key(); qrData.firstKeyB64 = client.getDeviceEd25519Key();
// Second key is what we think our master cross signing key is // Second key is what we think our master cross signing key is
@@ -269,7 +267,7 @@ export class QRCodeData {
return qrData; return qrData;
} }
static _generateBuffer(qrData) { private static generateBuffer(qrData: IQrData): Buffer {
let buf = Buffer.alloc(0); // we'll concat our way through life let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b) => { 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -29,6 +29,8 @@ import {
newUserCancelledError, newUserCancelledError,
} from './Error'; } from './Error';
import { logger } from '../../logger'; 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"; const START_TYPE = "m.key.verification.start";
@@ -38,7 +40,7 @@ const EVENTS = [
"m.key.verification.mac", "m.key.verification.mac",
]; ];
let olmutil; let olmutil: Utility;
const newMismatchedSASError = errorFactory( const newMismatchedSASError = errorFactory(
"m.mismatched_sas", "Mismatched short authentication string", "m.mismatched_sas", "Mismatched short authentication string",
@@ -48,7 +50,7 @@ const newMismatchedCommitmentError = errorFactory(
"m.mismatched_commitment", "Mismatched commitment", "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 | * | 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 ["🐶", "dog"], // 0
["🐱", "cat"], // 1 ["🐱", "cat"], // 1
["🦁", "lion"], // 2 ["🦁", "lion"], // 2
@@ -131,7 +135,7 @@ const emojiMapping = [
["📌", "pin"], // 63 ["📌", "pin"], // 63
]; ];
function generateEmojiSas(sasBytes) { function generateEmojiSas(sasBytes: number[]): EmojiMapping[] {
const emojis = [ const emojis = [
// just like base64 encoding // just like base64 encoding
sasBytes[0] >> 2, sasBytes[0] >> 2,
@@ -151,8 +155,13 @@ const sasGenerators = {
emoji: generateEmojiSas, emoji: generateEmojiSas,
}; };
function generateSas(sasBytes, methods) { export interface IGeneratedSas {
const sas = {}; decimal?: [number, number, number];
emoji?: EmojiMapping[];
}
function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas {
const sas: IGeneratedSas = {};
for (const method of methods) { for (const method of methods) {
if (method in sasGenerators) { if (method in sasGenerators) {
sas[method] = sasGenerators[method](sasBytes); sas[method] = sasGenerators[method](sasBytes);
@@ -166,7 +175,7 @@ const macMethods = {
"hmac-sha256": "calculate_mac_long_kdf", "hmac-sha256": "calculate_mac_long_kdf",
}; };
function calculateMAC(olmSAS, method) { function calculateMAC(olmSAS: OlmSAS, method: string) {
return function(...args) { return function(...args) {
const macFunction = olmSAS[macMethods[method]]; const macFunction = olmSAS[macMethods[method]];
const mac = macFunction.apply(olmSAS, args); const mac = macFunction.apply(olmSAS, args);
@@ -176,23 +185,23 @@ function calculateMAC(olmSAS, method) {
} }
const calculateKeyAgreement = { const calculateKeyAgreement = {
"curve25519-hkdf-sha256": function(sas, olmSAS, bytes) { "curve25519-hkdf-sha256": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|` const ourInfo = `${sas.baseApis.getUserId()}|${sas.baseApis.deviceId}|`
+ `${sas.ourSASPubKey}|`; + `${sas.ourSASPubKey}|`;
const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`; const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
const sasInfo = const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS|" "MATRIX_KEY_VERIFICATION_SAS|"
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
+ sas._channel.transactionId; + sas.channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes); return olmSAS.generate_bytes(sasInfo, bytes);
}, },
"curve25519": function(sas, olmSAS, bytes) { "curve25519": function(sas: SAS, olmSAS: OlmSAS, bytes: number): Uint8Array {
const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`; const ourInfo = `${sas.baseApis.getUserId()}${sas.baseApis.deviceId}`;
const theirInfo = `${sas.userId}${sas.deviceId}`; const theirInfo = `${sas.userId}${sas.deviceId}`;
const sasInfo = const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS" "MATRIX_KEY_VERIFICATION_SAS"
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo) + (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
+ sas._channel.transactionId; + sas.channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes); 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 MAC_SET = new Set(MAC_LIST);
const SAS_SET = new Set(SAS_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)) : []; return anArray instanceof Array ? anArray.filter(x => aSet.has(x)) : [];
} }
@@ -220,28 +229,39 @@ function intersection(anArray, aSet) {
* @extends {module:crypto/verification/Base} * @extends {module:crypto/verification/Base}
*/ */
export class SAS extends 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"; return "m.sas.v1";
} }
get events() { public get events(): string[] {
return EVENTS; return EVENTS;
} }
async _doVerification() { protected doVerification = async (): Promise<void> => {
await global.Olm.init(); await global.Olm.init();
olmutil = olmutil || new global.Olm.Utility(); olmutil = olmutil || new global.Olm.Utility();
// make sure user's keys are downloaded // make sure user's keys are downloaded
await this._baseApis.downloadKeys([this.userId]); await this.baseApis.downloadKeys([this.userId]);
let retry = false; let retry = false;
do { do {
try { try {
if (this.initiatedByMe) { if (this.initiatedByMe) {
return await this._doSendVerification(); return await this.doSendVerification();
} else { } else {
return await this._doRespondVerification(); return await this.doRespondVerification();
} }
} catch (err) { } catch (err) {
if (err instanceof SwitchStartEventError) { if (err instanceof SwitchStartEventError) {
@@ -253,38 +273,37 @@ export class SAS extends Base {
} }
} }
} while (retry); } while (retry);
} };
canSwitchStartEvent(event) { public canSwitchStartEvent(event: MatrixEvent): boolean {
if (event.getType() !== START_TYPE) { if (event.getType() !== START_TYPE) {
return false; return false;
} }
const content = event.getContent(); const content = event.getContent();
return content && content.method === SAS.NAME && return content && content.method === SAS.NAME && this.waitingForAccept;
this._waitingForAccept;
} }
async _sendStart() { private async sendStart(): Promise<Record<string, any>> {
const startContent = this._channel.completeContent(START_TYPE, { const startContent = this.channel.completeContent(START_TYPE, {
method: SAS.NAME, method: SAS.NAME,
from_device: this._baseApis.deviceId, from_device: this.baseApis.deviceId,
key_agreement_protocols: KEY_AGREEMENT_LIST, key_agreement_protocols: KEY_AGREEMENT_LIST,
hashes: HASHES_LIST, hashes: HASHES_LIST,
message_authentication_codes: MAC_LIST, message_authentication_codes: MAC_LIST,
// FIXME: allow app to specify what SAS methods can be used // FIXME: allow app to specify what SAS methods can be used
short_authentication_string: SAS_LIST, short_authentication_string: SAS_LIST,
}); });
await this._channel.sendCompleted(START_TYPE, startContent); await this.channel.sendCompleted(START_TYPE, startContent);
return startContent; return startContent;
} }
async _doSendVerification() { private async doSendVerification(): Promise<void> {
this._waitingForAccept = true; this.waitingForAccept = true;
let startContent; let startContent;
if (this.startEvent) { if (this.startEvent) {
startContent = this._channel.completedContentFromEvent(this.startEvent); startContent = this.channel.completedContentFromEvent(this.startEvent);
} else { } else {
startContent = await this._sendStart(); startContent = await this.sendStart();
} }
// we might have switched to a different start event, // we might have switched to a different start event,
@@ -297,9 +316,9 @@ export class SAS extends Base {
let e; let e;
try { try {
e = await this._waitForEvent("m.key.verification.accept"); e = await this.waitForEvent("m.key.verification.accept");
} finally { } finally {
this._waitingForAccept = false; this.waitingForAccept = false;
} }
let content = e.getContent(); let content = e.getContent();
const sasMethods const sasMethods
@@ -319,11 +338,11 @@ export class SAS extends Base {
const olmSAS = new global.Olm.SAS(); const olmSAS = new global.Olm.SAS();
try { try {
this.ourSASPubKey = olmSAS.get_pubkey(); this.ourSASPubKey = olmSAS.get_pubkey();
await this._send("m.key.verification.key", { await this.send("m.key.verification.key", {
key: this.ourSASPubKey, 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 // FIXME: make sure event is properly formed
content = e.getContent(); content = e.getContent();
const commitmentStr = content.key + anotherjson.stringify(startContent); const commitmentStr = content.key + anotherjson.stringify(startContent);
@@ -335,12 +354,12 @@ export class SAS extends Base {
olmSAS.set_their_key(content.key); olmSAS.set_their_key(content.key);
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => { const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = { this.sasEvent = {
sas: generateSas(sasBytes, sasMethods), sas: generateSas(sasBytes, sasMethods),
confirm: async () => { confirm: async () => {
try { try {
await this._sendMAC(olmSAS, macMethod); await this.sendMAC(olmSAS, macMethod);
resolve(); resolve();
} catch (err) { } catch (err) {
reject(err); reject(err);
@@ -353,54 +372,45 @@ export class SAS extends Base {
}); });
[e] = await Promise.all([ [e] = await Promise.all([
this._waitForEvent("m.key.verification.mac") this.waitForEvent("m.key.verification.mac")
.then((e) => { .then((e) => {
// we don't expect any more messages from the other // we don't expect any more messages from the other
// party, and they may send a m.key.verification.done // party, and they may send a m.key.verification.done
// when they're done on their end // when they're done on their end
this._expectedEvent = "m.key.verification.done"; this.expectedEvent = "m.key.verification.done";
return e; return e;
}), }),
verifySAS, verifySAS,
]); ]);
content = e.getContent(); content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod); await this.checkMAC(olmSAS, content, macMethod);
} finally { } finally {
olmSAS.free(); olmSAS.free();
} }
} }
async _doRespondVerification() { private async doRespondVerification(): Promise<void> {
// as m.related_to is not included in the encrypted content in e2e rooms, // as m.related_to is not included in the encrypted content in e2e rooms,
// we need to make sure it is added // 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, // Note: we intersect using our pre-made lists, rather than the sets,
// so that the result will be in our order of preference. Then // so that the result will be in our order of preference. Then
// fetching the first element from the array will give our preferred // fetching the first element from the array will give our preferred
// method out of the ones offered by the other party. // method out of the ones offered by the other party.
const keyAgreement const keyAgreement = intersection(KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols))[0];
= intersection( const hashMethod = intersection(HASHES_LIST, new Set(content.hashes))[0];
KEY_AGREEMENT_LIST, new Set(content.key_agreement_protocols), const macMethod = intersection(MAC_LIST, new Set(content.message_authentication_codes))[0];
)[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 // FIXME: allow app to specify what SAS methods can be used
const sasMethods const sasMethods = intersection(content.short_authentication_string, SAS_SET);
= intersection(content.short_authentication_string, SAS_SET); if (!(keyAgreement !== undefined && hashMethod !== undefined && macMethod !== undefined && sasMethods.length)) {
if (!(keyAgreement !== undefined
&& hashMethod !== undefined
&& macMethod !== undefined
&& sasMethods.length)) {
throw newUnknownMethodError(); throw newUnknownMethodError();
} }
const olmSAS = new global.Olm.SAS(); const olmSAS = new global.Olm.SAS();
try { try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content); 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, key_agreement_protocol: keyAgreement,
hash: hashMethod, hash: hashMethod,
message_authentication_code: macMethod, message_authentication_code: macMethod,
@@ -409,23 +419,23 @@ export class SAS extends Base {
commitment: olmutil.sha256(commitmentStr), 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 // FIXME: make sure event is properly formed
content = e.getContent(); content = e.getContent();
this.theirSASPubKey = content.key; this.theirSASPubKey = content.key;
olmSAS.set_their_key(content.key); olmSAS.set_their_key(content.key);
this.ourSASPubKey = olmSAS.get_pubkey(); this.ourSASPubKey = olmSAS.get_pubkey();
await this._send("m.key.verification.key", { await this.send("m.key.verification.key", {
key: this.ourSASPubKey, key: this.ourSASPubKey,
}); });
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => { const verifySAS = new Promise<void>((resolve, reject) => {
this.sasEvent = { this.sasEvent = {
sas: generateSas(sasBytes, sasMethods), sas: generateSas(sasBytes, sasMethods),
confirm: async () => { confirm: async () => {
try { try {
await this._sendMAC(olmSAS, macMethod); await this.sendMAC(olmSAS, macMethod);
resolve(); resolve();
} catch (err) { } catch (err) {
reject(err); reject(err);
@@ -438,39 +448,39 @@ export class SAS extends Base {
}); });
[e] = await Promise.all([ [e] = await Promise.all([
this._waitForEvent("m.key.verification.mac") this.waitForEvent("m.key.verification.mac")
.then((e) => { .then((e) => {
// we don't expect any more messages from the other // we don't expect any more messages from the other
// party, and they may send a m.key.verification.done // party, and they may send a m.key.verification.done
// when they're done on their end // when they're done on their end
this._expectedEvent = "m.key.verification.done"; this.expectedEvent = "m.key.verification.done";
return e; return e;
}), }),
verifySAS, verifySAS,
]); ]);
content = e.getContent(); content = e.getContent();
await this._checkMAC(olmSAS, content, macMethod); await this.checkMAC(olmSAS, content, macMethod);
} finally { } finally {
olmSAS.free(); olmSAS.free();
} }
} }
_sendMAC(olmSAS, method) { private sendMAC(olmSAS: OlmSAS, method: string): Promise<void> {
const mac = {}; const mac = {};
const keyList = []; const keyList = [];
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this._baseApis.getUserId() + this._baseApis.deviceId + this.baseApis.getUserId() + this.baseApis.deviceId
+ this.userId + this.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)( mac[deviceKeyId] = calculateMAC(olmSAS, method)(
this._baseApis.getDeviceEd25519Key(), this.baseApis.getDeviceEd25519Key(),
baseInfo + deviceKeyId, baseInfo + deviceKeyId,
); );
keyList.push(deviceKeyId); keyList.push(deviceKeyId);
const crossSigningId = this._baseApis.getCrossSigningId(); const crossSigningId = this.baseApis.getCrossSigningId();
if (crossSigningId) { if (crossSigningId) {
const crossSigningKeyId = `ed25519:${crossSigningId}`; const crossSigningKeyId = `ed25519:${crossSigningId}`;
mac[crossSigningKeyId] = calculateMAC(olmSAS, method)( mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(
@@ -484,14 +494,14 @@ export class SAS extends Base {
keyList.sort().join(","), keyList.sort().join(","),
baseInfo + "KEY_IDS", 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" const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId + this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId + this.baseApis.getUserId() + this.baseApis.deviceId
+ this._channel.transactionId; + this.channel.transactionId;
if (content.keys !== calculateMAC(olmSAS, method)( if (content.keys !== calculateMAC(olmSAS, method)(
Object.keys(content.mac).sort().join(","), Object.keys(content.mac).sort().join(","),
@@ -500,7 +510,7 @@ export class SAS extends Base {
throw newKeyMismatchError(); 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)( if (keyInfo !== calculateMAC(olmSAS, method)(
device.keys[keyId], device.keys[keyId],
baseInfo + keyId, baseInfo + keyId,

View File

@@ -22,8 +22,12 @@ import {
START_TYPE, START_TYPE,
} from "./VerificationRequest"; } from "./VerificationRequest";
import { logger } from '../../../logger'; 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_REFERENCE = "m.reference";
const M_RELATES_TO = "m.relates_to"; const M_RELATES_TO = "m.relates_to";
@@ -31,36 +35,34 @@ const M_RELATES_TO = "m.relates_to";
* A key verification channel that sends verification events in the timeline of a room. * 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. * 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 {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} 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. * @param {string} userId id of user that the verification request is directed at, should be present in the room.
*/ */
constructor(client, roomId, userId = null) { constructor(
this._client = client; private readonly client: MatrixClient,
this._roomId = roomId; public readonly roomId: string,
this.userId = userId; public userId: string = null,
this._requestEventId = null; ) {
} }
get receiveStartFromOtherDevices() { public get receiveStartFromOtherDevices(): boolean {
return true; return true;
} }
get roomId() {
return this._roomId;
}
/** The transaction id generated/used by this verification channel */ /** The transaction id generated/used by this verification channel */
get transactionId() { public get transactionId(): string {
return this._requestEventId; return this.requestEventId;
} }
static getOtherPartyUserId(event, client) { public static getOtherPartyUserId(event: MatrixEvent, client: MatrixClient): string {
const type = InRoomChannel.getEventType(event); const type = InRoomChannel.getEventType(event);
if (type !== REQUEST_TYPE) { if (type !== REQUEST_TYPE) {
return; return;
} }
const ownUserId = client.getUserId(); const ownUserId = client.getUserId();
const sender = event.getSender(); const sender = event.getSender();
@@ -78,25 +80,29 @@ export class InRoomChannel {
* @param {MatrixEvent} event the event to get the timestamp of * @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent * @return {number} the timestamp when the event was sent
*/ */
getTimestamp(event) { public getTimestamp(event: MatrixEvent): number {
return event.getTs(); return event.getTs();
} }
/** /**
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel * 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 * @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; 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 * Extract the transaction id used by a given key verification event, if any
* @param {MatrixEvent} event the event * @param {MatrixEvent} event the event
* @returns {string} the transaction id * @returns {string} the transaction id
*/ */
static getTransactionId(event) { public static getTransactionId(event: MatrixEvent): string {
if (InRoomChannel.getEventType(event) === REQUEST_TYPE) { if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
return event.getId(); return event.getId();
} else { } else {
@@ -114,9 +120,9 @@ export class InRoomChannel {
* `handleEvent` can do more checks and choose to ignore invalid events. * `handleEvent` can do more checks and choose to ignore invalid events.
* @param {MatrixEvent} event the event to validate * @param {MatrixEvent} event the event to validate
* @param {MatrixClient} client the client to get the current user and device id from * @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); const txnId = InRoomChannel.getTransactionId(event);
if (typeof txnId !== "string" || txnId.length === 0) { if (typeof txnId !== "string" || txnId.length === 0) {
return false; return false;
@@ -152,7 +158,7 @@ export class InRoomChannel {
* @param {MatrixEvent} event the event to get the type of * @param {MatrixEvent} event the event to get the type of
* @returns {string} the "symbolic" event type * @returns {string} the "symbolic" event type
*/ */
static getEventType(event) { public static getEventType(event: MatrixEvent): string {
const type = event.getType(); const type = event.getType();
if (type === MESSAGE_TYPE) { if (type === MESSAGE_TYPE) {
const content = event.getContent(); 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. * Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle * @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to * @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not * @param {boolean} 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. * @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 // prevent processing the same event multiple times, as under
// some circumstances Room.timeline can get emitted twice for the same event // some circumstances Room.timeline can get emitted twice for the same event
if (request.hasEventId(event.getId())) { if (request.hasEventId(event.getId())) {
@@ -187,18 +193,18 @@ export class InRoomChannel {
// do validations that need state (roomId, userId), // do validations that need state (roomId, userId),
// ignore if invalid // ignore if invalid
if (event.getRoomId() !== this._roomId) { if (event.getRoomId() !== this.roomId) {
return; return;
} }
// set userId if not set already // set userId if not set already
if (this.userId === null) { if (this.userId === null) {
const userId = InRoomChannel.getOtherPartyUserId(event, this._client); const userId = InRoomChannel.getOtherPartyUserId(event, this.client);
if (userId) { if (userId) {
this.userId = userId; this.userId = userId;
} }
} }
// ignore events not sent by us or the other party // ignore events not sent by us or the other party
const ownUserId = this._client.getUserId(); const ownUserId = this.client.getUserId();
const sender = event.getSender(); const sender = event.getSender();
if (this.userId !== null) { if (this.userId !== null) {
if (sender !== ownUserId && sender !== this.userId) { if (sender !== ownUserId && sender !== this.userId) {
@@ -207,12 +213,12 @@ export class InRoomChannel {
return; return;
} }
} }
if (this._requestEventId === null) { if (this.requestEventId === null) {
this._requestEventId = InRoomChannel.getTransactionId(event); this.requestEventId = InRoomChannel.getTransactionId(event);
} }
const isRemoteEcho = !!event.getUnsigned().transaction_id; const isRemoteEcho = !!event.getUnsigned().transaction_id;
const isSentByUs = event.getSender() === this._client.getUserId(); const isSentByUs = event.getSender() === this.client.getUserId();
return await request.handleEvent( return await request.handleEvent(
type, event, isLiveEvent, isRemoteEcho, isSentByUs); type, event, isLiveEvent, isRemoteEcho, isSentByUs);
@@ -226,13 +232,14 @@ export class InRoomChannel {
* @param {MatrixEvent} event the received event * @param {MatrixEvent} event the received event
* @returns {Object} the content object with the relation added again * @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 // ensure m.related_to is included in e2ee rooms
// as the field is excluded from encryption // as the field is excluded from encryption
const content = Object.assign({}, event.getContent()); const content = Object.assign({}, event.getContent());
content[M_RELATES_TO] = event.getRelation(); content[M_RELATES_TO] = event.getRelation();
return content; return content;
} }
/** /**
* Add all the fields to content needed for sending it over this channel. * 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 * 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 * @param {object} content the (incomplete) content
* @returns {object} the complete content, as it will be sent. * @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); content = Object.assign({}, content);
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) { 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) { if (type === REQUEST_TYPE) {
// type is mapped to m.room.message in the send method // type is mapped to m.room.message in the send method
content = { 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 " + "your key, but your client does not support in-chat key " +
"verification. You will need to use legacy key " + "verification. You will need to use legacy key " +
"verification to verify keys.", "verification to verify keys.",
@@ -274,7 +281,7 @@ export class InRoomChannel {
* @param {object} uncompletedContent the (incomplete) content * @param {object} uncompletedContent the (incomplete) content
* @returns {Promise} the promise of the request * @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); const content = this.completeContent(type, uncompletedContent);
return this.sendCompleted(type, content); return this.sendCompleted(type, content);
} }
@@ -285,74 +292,69 @@ export class InRoomChannel {
* @param {object} content * @param {object} content
* @returns {Promise} the promise of the request * @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; let sendType = type;
if (type === REQUEST_TYPE) { if (type === REQUEST_TYPE) {
sendType = MESSAGE_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) { if (type === REQUEST_TYPE) {
this._requestEventId = response.event_id; this.requestEventId = response.event_id;
} }
} }
} }
export class InRoomRequests { export class InRoomRequests {
constructor() { private requestsByRoomId = new Map<string, Map<string, VerificationRequest>>();
this._requestsByRoomId = new Map();
}
getRequest(event) { public getRequest(event: MatrixEvent): VerificationRequest {
const roomId = event.getRoomId(); const roomId = event.getRoomId();
const txnId = InRoomChannel.getTransactionId(event); const txnId = InRoomChannel.getTransactionId(event);
return this._getRequestByTxnId(roomId, txnId); return this.getRequestByTxnId(roomId, txnId);
} }
getRequestByChannel(channel) { public getRequestByChannel(channel: InRoomChannel): VerificationRequest {
return this._getRequestByTxnId(channel.roomId, channel.transactionId); return this.getRequestByTxnId(channel.roomId, channel.transactionId);
} }
_getRequestByTxnId(roomId, txnId) { private getRequestByTxnId(roomId: string, txnId: string): VerificationRequest {
const requestsByTxnId = this._requestsByRoomId.get(roomId); const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) { if (requestsByTxnId) {
return requestsByTxnId.get(txnId); return requestsByTxnId.get(txnId);
} }
} }
setRequest(event, request) { public setRequest(event: MatrixEvent, request: VerificationRequest): void {
this._setRequest( this._setRequest(event.getRoomId(), InRoomChannel.getTransactionId(event), request);
event.getRoomId(),
InRoomChannel.getTransactionId(event),
request,
);
} }
setRequestByChannel(channel, request) { public setRequestByChannel(channel: InRoomChannel, request: VerificationRequest): void {
this._setRequest(channel.roomId, channel.transactionId, request); this._setRequest(channel.roomId, channel.transactionId, request);
} }
_setRequest(roomId, txnId, request) { // eslint-disable-next-line @typescript-eslint/naming-convention
let requestsByTxnId = this._requestsByRoomId.get(roomId); private _setRequest(roomId: string, txnId: string, request: VerificationRequest): void {
let requestsByTxnId = this.requestsByRoomId.get(roomId);
if (!requestsByTxnId) { if (!requestsByTxnId) {
requestsByTxnId = new Map(); requestsByTxnId = new Map();
this._requestsByRoomId.set(roomId, requestsByTxnId); this.requestsByRoomId.set(roomId, requestsByTxnId);
} }
requestsByTxnId.set(txnId, request); requestsByTxnId.set(txnId, request);
} }
removeRequest(event) { public removeRequest(event: MatrixEvent): void {
const roomId = event.getRoomId(); const roomId = event.getRoomId();
const requestsByTxnId = this._requestsByRoomId.get(roomId); const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) { if (requestsByTxnId) {
requestsByTxnId.delete(InRoomChannel.getTransactionId(event)); requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) { if (requestsByTxnId.size === 0) {
this._requestsByRoomId.delete(roomId); this.requestsByRoomId.delete(roomId);
} }
} }
} }
findRequestInProgress(roomId) { public findRequestInProgress(roomId: string): VerificationRequest {
const requestsByTxnId = this._requestsByRoomId.get(roomId); const requestsByTxnId = this.requestsByRoomId.get(roomId);
if (requestsByTxnId) { if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) { for (const request of requestsByTxnId.values()) {
if (request.pending) { if (request.pending) {

View File

@@ -28,25 +28,31 @@ import {
} from "./VerificationRequest"; } from "./VerificationRequest";
import { errorFromEvent, newUnexpectedMessageError } from "../Error"; import { errorFromEvent, newUnexpectedMessageError } from "../Error";
import { MatrixEvent } from "../../../models/event"; 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. * A key verification channel that sends verification events over to_device messages.
* Generates its own transaction ids. * Generates its own transaction ids.
*/ */
export class ToDeviceChannel { export class ToDeviceChannel implements IVerificationChannel {
// userId and devices of user we're about to verify public request?: VerificationRequest;
constructor(client, userId, devices, transactionId = null, deviceId = null) {
this._client = client;
this.userId = userId;
this._devices = devices;
this.transactionId = transactionId;
this._deviceId = deviceId;
}
isToDevices(devices) { // userId and devices of user we're about to verify
if (devices.length === this._devices.length) { 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) { 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) { if (!d) {
return false; return false;
} }
@@ -57,11 +63,7 @@ export class ToDeviceChannel {
} }
} }
get deviceId() { public static getEventType(event: MatrixEvent): string {
return this._deviceId;
}
static getEventType(event) {
return event.getType(); return event.getType();
} }
@@ -70,7 +72,7 @@ export class ToDeviceChannel {
* @param {MatrixEvent} event the event * @param {MatrixEvent} event the event
* @returns {string} the transaction id * @returns {string} the transaction id
*/ */
static getTransactionId(event) { public static getTransactionId(event: MatrixEvent): string {
const content = event.getContent(); const content = event.getContent();
return content && content.transaction_id; 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 * 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 * @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; 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. * 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 * 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. * `handleEvent` can do more checks and choose to ignore invalid events.
* @param {MatrixEvent} event the event to validate * @param {MatrixEvent} event the event to validate
* @param {MatrixClient} client the client to get the current user and device id from * @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()) { if (event.isCancelled()) {
logger.warn("Ignoring flagged verification request from " logger.warn("Ignoring flagged verification request from "
+ event.getSender()); + event.getSender());
@@ -134,7 +140,7 @@ export class ToDeviceChannel {
* @param {MatrixEvent} event the event to get the timestamp of * @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent * @return {number} the timestamp when the event was sent
*/ */
getTimestamp(event) { public getTimestamp(event: MatrixEvent): number {
const content = event.getContent(); const content = event.getContent();
return content && content.timestamp; 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. * Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle * @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to * @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not * @param {boolean} 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. * @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 type = event.getType();
const content = event.getContent(); const content = event.getContent();
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) { if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
@@ -155,17 +161,16 @@ export class ToDeviceChannel {
} }
const deviceId = content.from_device; const deviceId = content.from_device;
// adopt deviceId if not set before and valid // adopt deviceId if not set before and valid
if (!this._deviceId && this._devices.includes(deviceId)) { if (!this.deviceId && this.devices.includes(deviceId)) {
this._deviceId = deviceId; this.deviceId = deviceId;
} }
// if no device id or different from addopted one, cancel with sender // if no device id or different from adopted one, cancel with sender
if (!this._deviceId || this._deviceId !== deviceId) { if (!this.deviceId || this.deviceId !== deviceId) {
// also check that message came from the device we sent the request to earlier on // also check that message came from the device we sent the request to earlier on
// and do send a cancel message to that device // and do send a cancel message to that device
// (but don't cancel the request for the device we should be talking to) // (but don't cancel the request for the device we should be talking to)
const cancelContent = const cancelContent = this.completeContent(errorFromEvent(newUnexpectedMessageError()));
this.completeContent(errorFromEvent(newUnexpectedMessageError())); return this.sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
} }
} }
const wasStarted = request.phase === PHASE_STARTED || const wasStarted = request.phase === PHASE_STARTED ||
@@ -178,16 +183,16 @@ export class ToDeviceChannel {
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE; const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
// the request has picked a ready or start event, tell the other devices about it // the request has picked a ready or start event, tell the other devices about it
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) { if (isAcceptingEvent && !wasStarted && isStarted && this.deviceId) {
const nonChosenDevices = this._devices.filter( const nonChosenDevices = this.devices.filter(
d => d !== this._deviceId && d !== this._client.getDeviceId(), d => d !== this.deviceId && d !== this.client.getDeviceId(),
); );
if (nonChosenDevices.length) { if (nonChosenDevices.length) {
const message = this.completeContent({ const message = this.completeContent({
code: "m.accepted", code: "m.accepted",
reason: "Verification request accepted by another device", 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 * @param {MatrixEvent} event the received event
* @returns {Object} the content object * @returns {Object} the content object
*/ */
completedContentFromEvent(event) { public completedContentFromEvent(event: MatrixEvent): Record<string, any> {
return event.getContent(); return event.getContent();
} }
@@ -210,14 +215,14 @@ export class ToDeviceChannel {
* @param {object} content the (incomplete) content * @param {object} content the (incomplete) content
* @returns {object} the complete content, as it will be sent. * @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 // make a copy
content = Object.assign({}, content); content = Object.assign({}, content);
if (this.transactionId) { if (this.transactionId) {
content.transaction_id = this.transactionId; content.transaction_id = this.transactionId;
} }
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) { 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) { if (type === REQUEST_TYPE) {
content.timestamp = Date.now(); content.timestamp = Date.now();
@@ -231,7 +236,7 @@ export class ToDeviceChannel {
* @param {object} uncompletedContent the (incomplete) content * @param {object} uncompletedContent the (incomplete) content
* @returns {Promise} the promise of the request * @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 // create transaction id when sending request
if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) { if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) {
this.transactionId = ToDeviceChannel.makeTransactionId(); this.transactionId = ToDeviceChannel.makeTransactionId();
@@ -246,21 +251,21 @@ export class ToDeviceChannel {
* @param {object} content * @param {object} content
* @returns {Promise} the promise of the request * @returns {Promise} the promise of the request
*/ */
async sendCompleted(type, content) { public async sendCompleted(type: string, content: Record<string, any>): Promise<void> {
let result; let result;
if (type === REQUEST_TYPE || (type === CANCEL_TYPE && !this.__deviceId)) { 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 { } 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 VerificationRequest state machine requires remote echos of the event
// the client sends itself, so we fake this for to_device messages // the client sends itself, so we fake this for to_device messages
const remoteEchoEvent = new MatrixEvent({ const remoteEchoEvent = new MatrixEvent({
sender: this._client.getUserId(), sender: this.client.getUserId(),
content, content,
type, type,
}); });
await this._request.handleEvent( await this.request.handleEvent(
type, type,
remoteEchoEvent, remoteEchoEvent,
/*isLiveEvent=*/true, /*isLiveEvent=*/true,
@@ -270,16 +275,14 @@ export class ToDeviceChannel {
return result; return result;
} }
_sendToDevices(type, content, devices) { private async sendToDevices(type: string, content: Record<string, any>, devices: string[]): Promise<void> {
if (devices.length) { if (devices.length) {
const msgMap = {}; const msgMap = {};
for (const deviceId of devices) { for (const deviceId of devices) {
msgMap[deviceId] = content; msgMap[deviceId] = content;
} }
return this._client.sendToDevice(type, { [this.userId]: msgMap }); await this.client.sendToDevice(type, { [this.userId]: msgMap });
} else {
return Promise.resolve();
} }
} }
@@ -287,68 +290,62 @@ export class ToDeviceChannel {
* Allow Crypto module to create and know the transaction id before the .start event gets sent. * Allow Crypto module to create and know the transaction id before the .start event gets sent.
* @returns {string} the transaction id * @returns {string} the transaction id
*/ */
static makeTransactionId() { public static makeTransactionId(): string {
return randomString(32); return randomString(32);
} }
} }
export class ToDeviceRequests { export class ToDeviceRequests {
constructor() { private requestsByUserId = new Map<string, Map<string, Request>>();
this._requestsByUserId = new Map();
}
getRequest(event) { public getRequest(event: MatrixEvent): Request {
return this.getRequestBySenderAndTxnId( return this.getRequestBySenderAndTxnId(
event.getSender(), event.getSender(),
ToDeviceChannel.getTransactionId(event), ToDeviceChannel.getTransactionId(event),
); );
} }
getRequestByChannel(channel) { public getRequestByChannel(channel: ToDeviceChannel): Request {
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId); return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
} }
getRequestBySenderAndTxnId(sender, txnId) { public getRequestBySenderAndTxnId(sender: string, txnId: string): Request {
const requestsByTxnId = this._requestsByUserId.get(sender); const requestsByTxnId = this.requestsByUserId.get(sender);
if (requestsByTxnId) { if (requestsByTxnId) {
return requestsByTxnId.get(txnId); return requestsByTxnId.get(txnId);
} }
} }
setRequest(event, request) { public setRequest(event: MatrixEvent, request: Request): void {
this.setRequestBySenderAndTxnId( this.setRequestBySenderAndTxnId(event.getSender(), ToDeviceChannel.getTransactionId(event), request);
event.getSender(),
ToDeviceChannel.getTransactionId(event),
request,
);
} }
setRequestByChannel(channel, request) { public setRequestByChannel(channel: ToDeviceChannel, request: Request): void {
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request); this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
} }
setRequestBySenderAndTxnId(sender, txnId, request) { public setRequestBySenderAndTxnId(sender: string, txnId: string, request: Request): void {
let requestsByTxnId = this._requestsByUserId.get(sender); let requestsByTxnId = this.requestsByUserId.get(sender);
if (!requestsByTxnId) { if (!requestsByTxnId) {
requestsByTxnId = new Map(); requestsByTxnId = new Map();
this._requestsByUserId.set(sender, requestsByTxnId); this.requestsByUserId.set(sender, requestsByTxnId);
} }
requestsByTxnId.set(txnId, request); requestsByTxnId.set(txnId, request);
} }
removeRequest(event) { public removeRequest(event: MatrixEvent): void {
const userId = event.getSender(); const userId = event.getSender();
const requestsByTxnId = this._requestsByUserId.get(userId); const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) { if (requestsByTxnId) {
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event)); requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) { if (requestsByTxnId.size === 0) {
this._requestsByUserId.delete(userId); this.requestsByUserId.delete(userId);
} }
} }
} }
findRequestInProgress(userId, devices) { public findRequestInProgress(userId: string, devices: string[]): Request {
const requestsByTxnId = this._requestsByUserId.get(userId); const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) { if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) { for (const request of requestsByTxnId.values()) {
if (request.pending && request.channel.isToDevices(devices)) { if (request.pending && request.channel.isToDevices(devices)) {
@@ -358,8 +355,8 @@ export class ToDeviceRequests {
} }
} }
getRequestsInProgress(userId) { public getRequestsInProgress(userId: string): Request[] {
const requestsByTxnId = this._requestsByUserId.get(userId); const requestsByTxnId = this.requestsByUserId.get(userId);
if (requestsByTxnId) { if (requestsByTxnId) {
return Array.from(requestsByTxnId.values()).filter(r => r.pending); return Array.from(requestsByTxnId.values()).filter(r => r.pending);
} }

View File

@@ -1,6 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@@ -24,6 +23,11 @@ import {
newUnknownMethodError, newUnknownMethodError,
} from "../Error"; } from "../Error";
import { QRCodeData, SCAN_QR_CODE_METHOD } from "../QRCode"; 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 // How long after the event's timestamp that the request times out
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes 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 DONE_TYPE = EVENT_PREFIX + "done";
export const READY_TYPE = EVENT_PREFIX + "ready"; export const READY_TYPE = EVENT_PREFIX + "ready";
export const PHASE_UNSENT = 1; export enum Phase {
export const PHASE_REQUESTED = 2; Unsent = 1,
export const PHASE_READY = 3; Requested,
export const PHASE_STARTED = 4; Ready,
export const PHASE_CANCELLED = 5; Started,
export const PHASE_DONE = 6; 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. * 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`. * send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
* @event "change" whenever the state of the request object has changed. * @event "change" whenever the state of the request object has changed.
*/ */
export class VerificationRequest extends EventEmitter { export class VerificationRequest<C extends IVerificationChannel = IVerificationChannel> extends EventEmitter {
constructor(channel, verificationMethods, client) { private eventsByUs = new Map<string, MatrixEvent>();
super(); private eventsByThem = new Map<string, MatrixEvent>();
this.channel = channel; private _observeOnly = false;
this.channel._request = this; private timeoutTimer: number = null;
this._verificationMethods = verificationMethods; private _accepting = false;
this._client = client; private _declining = false;
this._commonMethods = []; private verifierHasFinished = false;
this._setPhase(PHASE_UNSENT, false); private _cancelled = false;
this._eventsByUs = new Map(); private _chosenMethod: VerificationMethod = null;
this._eventsByThem = new Map(); // we keep a copy of the QR Code data (including other user master key) around
this._observeOnly = false; // for QR reciprocate verification, to protect against
this._timeoutTimer = null; // cross-signing identity reset between the .ready and .start event
this._accepting = false; // and signing the wrong key after .start
this._declining = false; private _qrCodeData: QRCodeData = null;
this._verifierHasFinished = false;
this._cancelled = false;
this._chosenMethod = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
this._qrCodeData = null;
// The timestamp when we received the request event from the other side // 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 {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 {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 * @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(); const content = event.getContent();
if (!type || !type.startsWith(EVENT_PREFIX)) { if (!type || !type.startsWith(EVENT_PREFIX)) {
@@ -128,53 +158,53 @@ export class VerificationRequest extends EventEmitter {
return true; return true;
} }
get invalid() { public get invalid(): boolean {
return this.phase === PHASE_UNSENT; return this.phase === PHASE_UNSENT;
} }
/** returns whether the phase is PHASE_REQUESTED */ /** returns whether the phase is PHASE_REQUESTED */
get requested() { public get requested(): boolean {
return this.phase === PHASE_REQUESTED; return this.phase === PHASE_REQUESTED;
} }
/** returns whether the phase is PHASE_CANCELLED */ /** returns whether the phase is PHASE_CANCELLED */
get cancelled() { public get cancelled(): boolean {
return this.phase === PHASE_CANCELLED; return this.phase === PHASE_CANCELLED;
} }
/** returns whether the phase is PHASE_READY */ /** returns whether the phase is PHASE_READY */
get ready() { public get ready(): boolean {
return this.phase === PHASE_READY; return this.phase === PHASE_READY;
} }
/** returns whether the phase is PHASE_STARTED */ /** returns whether the phase is PHASE_STARTED */
get started() { public get started(): boolean {
return this.phase === PHASE_STARTED; return this.phase === PHASE_STARTED;
} }
/** returns whether the phase is PHASE_DONE */ /** returns whether the phase is PHASE_DONE */
get done() { public get done(): boolean {
return this.phase === PHASE_DONE; return this.phase === PHASE_DONE;
} }
/** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */ /** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
get methods() { public get methods(): VerificationMethod[] {
return this._commonMethods; return this.commonMethods;
} }
/** the method picked in the .start event */ /** the method picked in the .start event */
get chosenMethod() { public get chosenMethod(): VerificationMethod {
return this._chosenMethod; return this._chosenMethod;
} }
calculateEventTimeout(event) { public calculateEventTimeout(event: MatrixEvent): number {
let effectiveExpiresAt = this.channel.getTimestamp(event) let effectiveExpiresAt = this.channel.getTimestamp(event)
+ TIMEOUT_FROM_EVENT_TS; + TIMEOUT_FROM_EVENT_TS;
if (this._requestReceivedAt && !this.initiatedByMe && if (this.requestReceivedAt && !this.initiatedByMe &&
this.phase <= PHASE_REQUESTED this.phase <= PHASE_REQUESTED
) { ) {
const expiresAtByReceipt = this._requestReceivedAt const expiresAtByReceipt = this.requestReceivedAt
+ TIMEOUT_FROM_EVENT_RECEIPT; + TIMEOUT_FROM_EVENT_RECEIPT;
effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt); 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 */ /** The current remaining amount of ms before the request should be automatically cancelled */
get timeout() { public get timeout(): number {
const requestEvent = this._getEventByEither(REQUEST_TYPE); const requestEvent = this.getEventByEither(REQUEST_TYPE);
if (requestEvent) { if (requestEvent) {
return this.calculateEventTimeout(requestEvent); return this.calculateEventTimeout(requestEvent);
} }
@@ -195,41 +225,41 @@ export class VerificationRequest extends EventEmitter {
* The key verification request event. * The key verification request event.
* @returns {MatrixEvent} The request event, or falsey if not found. * @returns {MatrixEvent} The request event, or falsey if not found.
*/ */
get requestEvent() { public get requestEvent(): MatrixEvent {
return this._getEventByEither(REQUEST_TYPE); return this.getEventByEither(REQUEST_TYPE);
} }
/** current phase of the request. Some properties might only be defined in a current phase. */ /** current phase of the request. Some properties might only be defined in a current phase. */
get phase() { public get phase(): Phase {
return this._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. */ /** 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; return this._verifier;
} }
get canAccept() { public get canAccept(): boolean {
return this.phase < PHASE_READY && !this._accepting && !this._declining; return this.phase < PHASE_READY && !this._accepting && !this._declining;
} }
get accepting() { public get accepting(): boolean {
return this._accepting; return this._accepting;
} }
get declining() { public get declining(): boolean {
return this._declining; return this._declining;
} }
/** whether this request has sent it's initial event and needs more events to complete */ /** whether this request has sent it's initial event and needs more events to complete */
get pending() { public get pending(): boolean {
return !this.observeOnly && return !this.observeOnly &&
this._phase !== PHASE_DONE && this._phase !== PHASE_DONE &&
this._phase !== PHASE_CANCELLED; this._phase !== PHASE_CANCELLED;
} }
/** Only set after a .ready if the other party can scan a QR code */ /** Only set after a .ready if the other party can scan a QR code */
get qrCodeData() { public get qrCodeData(): QRCodeData {
return this._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. * For methods that need to be supported by both ends, use the `methods` property.
* @param {string} method the method to check * @param {string} method the method to check
* @param {boolean} force to check even if the phase is not ready or started yet, internal usage * @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 */ * @return {boolean} whether or not the other party said the supported the method */
otherPartySupportsMethod(method, force = false) { public otherPartySupportsMethod(method: string, force = false): boolean {
if (!force && !this.ready && !this.started) { if (!force && !this.ready && !this.started) {
return false; return false;
} }
const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) || const theirMethodEvent = this.eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE); this.eventsByThem.get(READY_TYPE);
if (!theirMethodEvent) { if (!theirMethodEvent) {
// if we started straight away with .start event, // if we started straight away with .start event,
// we are assuming that the other side will support the // we are assuming that the other side will support the
// chosen method, so return true for that. // chosen method, so return true for that.
if (this.started && this.initiatedByMe) { 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 content = myStartEvent && myStartEvent.getContent();
const myStartMethod = content && content.method; const myStartMethod = content && content.method;
return method == myStartMethod; return method == myStartMethod;
@@ -274,22 +304,22 @@ export class VerificationRequest extends EventEmitter {
* For InRoomChannel, this is who sent the .request event. * For InRoomChannel, this is who sent the .request event.
* For ToDeviceChannel, this is who sent the .start 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 // 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) { if (this._phase === PHASE_UNSENT && noEventsYet) {
return true; return true;
} }
const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE); const hasMyRequest = this.eventsByUs.has(REQUEST_TYPE);
const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE); const hasTheirRequest = this.eventsByThem.has(REQUEST_TYPE);
if (hasMyRequest && !hasTheirRequest) { if (hasMyRequest && !hasTheirRequest) {
return true; return true;
} }
if (!hasMyRequest && hasTheirRequest) { if (!hasMyRequest && hasTheirRequest) {
return false; return false;
} }
const hasMyStart = this._eventsByUs.has(START_TYPE); const hasMyStart = this.eventsByUs.has(START_TYPE);
const hasTheirStart = this._eventsByThem.has(START_TYPE); const hasTheirStart = this.eventsByThem.has(START_TYPE);
if (hasMyStart && !hasTheirStart) { if (hasMyStart && !hasTheirStart) {
return true; return true;
} }
@@ -297,39 +327,39 @@ export class VerificationRequest extends EventEmitter {
} }
/** The id of the user that initiated the request */ /** The id of the user that initiated the request */
get requestingUserId() { public get requestingUserId(): string {
if (this.initiatedByMe) { if (this.initiatedByMe) {
return this._client.getUserId(); return this.client.getUserId();
} else { } else {
return this.otherUserId; return this.otherUserId;
} }
} }
/** The id of the user that (will) receive(d) the request */ /** The id of the user that (will) receive(d) the request */
get receivingUserId() { public get receivingUserId(): string {
if (this.initiatedByMe) { if (this.initiatedByMe) {
return this.otherUserId; return this.otherUserId;
} else { } else {
return this._client.getUserId(); return this.client.getUserId();
} }
} }
/** The user id of the other party in this request */ /** The user id of the other party in this request */
get otherUserId() { public get otherUserId(): string {
return this.channel.userId; return this.channel.userId;
} }
get isSelfVerification() { public get isSelfVerification(): boolean {
return this._client.getUserId() === this.otherUserId; return this.client.getUserId() === this.otherUserId;
} }
/** /**
* The id of the user that cancelled the request, * The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED * only defined when phase is PHASE_CANCELLED
*/ */
get cancellingUserId() { public get cancellingUserId(): string {
const myCancel = this._eventsByUs.get(CANCEL_TYPE); const myCancel = this.eventsByUs.get(CANCEL_TYPE);
const theirCancel = this._eventsByThem.get(CANCEL_TYPE); const theirCancel = this.eventsByThem.get(CANCEL_TYPE);
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) { if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
return myCancel.getSender(); 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 * The cancellation code e.g m.user which is responsible for cancelling this verification
*/ */
get cancellationCode() { public get cancellationCode(): string {
const ev = this._getEventByEither(CANCEL_TYPE); const ev = this.getEventByEither(CANCEL_TYPE);
return ev ? ev.getContent().code : null; return ev ? ev.getContent().code : null;
} }
get observeOnly() { public get observeOnly(): boolean {
return this._observeOnly; return this._observeOnly;
} }
@@ -359,11 +389,11 @@ export class VerificationRequest extends EventEmitter {
* verification to when no specific device is specified. * verification to when no specific device is specified.
* @returns {{userId: *, deviceId: *}} The device information * @returns {{userId: *, deviceId: *}} The device information
*/ */
get targetDevice() { public get targetDevice(): ITargetDevice {
const theirFirstEvent = const theirFirstEvent =
this._eventsByThem.get(REQUEST_TYPE) || this.eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE) || this.eventsByThem.get(READY_TYPE) ||
this._eventsByThem.get(START_TYPE); this.eventsByThem.get(START_TYPE);
const theirFirstContent = theirFirstEvent.getContent(); const theirFirstContent = theirFirstEvent.getContent();
const fromDevice = theirFirstContent.from_device; const fromDevice = theirFirstContent.from_device;
return { return {
@@ -379,21 +409,20 @@ export class VerificationRequest extends EventEmitter {
* @param {string?} targetDevice.deviceId the id of the device to direct this request to * @param {string?} targetDevice.deviceId the id of the device to direct this request to
* @returns {VerifierBase} the verifier of the given method * @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 // need to allow also when unsent in case of to_device
if (!this.observeOnly && !this._verifier) { if (!this.observeOnly && !this._verifier) {
const validStartPhase = const validStartPhase =
this.phase === PHASE_REQUESTED || this.phase === PHASE_REQUESTED ||
this.phase === PHASE_READY || this.phase === PHASE_READY ||
(this.phase === PHASE_UNSENT && (this.phase === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE));
this.channel.constructor.canCreateRequest(START_TYPE));
if (validStartPhase) { if (validStartPhase) {
// when called on a request that was initiated with .request event // when called on a request that was initiated with .request event
// check the method is supported by both sides // 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(); throw newUnknownMethodError();
} }
this._verifier = this._createVerifier(method, null, targetDevice); this._verifier = this.createVerifier(method, null, targetDevice);
if (!this._verifier) { if (!this._verifier) {
throw newUnknownMethodError(); throw newUnknownMethodError();
} }
@@ -407,9 +436,9 @@ export class VerificationRequest extends EventEmitter {
* sends the initial .request event. * sends the initial .request event.
* @returns {Promise} resolves when the event has been sent. * @returns {Promise} resolves when the event has been sent.
*/ */
async sendRequest() { public async sendRequest(): Promise<void> {
if (!this.observeOnly && this._phase === PHASE_UNSENT) { if (!this.observeOnly && this._phase === PHASE_UNSENT) {
const methods = [...this._verificationMethods.keys()]; const methods = [...this.verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, { methods }); 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 * @param {string?} error.code the error code to send the cancellation with
* @returns {Promise} resolves when the event has been sent. * @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) { if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
this._declining = true; this._declining = true;
this.emit("change"); this.emit("change");
if (this._verifier) { if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason)()); return this._verifier.cancel(errorFactory(code, reason)());
} else { } else {
this._cancellingUserId = this._client.getUserId(); this._cancellingUserId = this.client.getUserId();
await this.channel.send(CANCEL_TYPE, { code, reason }); 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 * Accepts the request, sending a .ready event to the other party
* @returns {Promise} resolves when the event has been sent. * @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) { if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
const methods = [...this._verificationMethods.keys()]; const methods = [...this.verificationMethods.keys()];
this._accepting = true; this._accepting = true;
this.emit("change"); this.emit("change");
await this.channel.send(READY_TYPE, { methods }); 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 * @returns {Promise} that resolves once the callback returns true
* @throws {Error} when the request is cancelled * @throws {Error} when the request is cancelled
*/ */
waitFor(fn) { public waitFor(fn: (request: VerificationRequest) => boolean): Promise<VerificationRequest> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const check = () => { const check = () => {
let handled = false; 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; this._phase = phase;
if (notify) { if (notify) {
this.emit("change"); this.emit("change");
} }
} }
_getEventByEither(type) { private getEventByEither(type: string): MatrixEvent {
return this._eventsByThem.get(type) || this._eventsByUs.get(type); return this.eventsByThem.get(type) || this.eventsByUs.get(type);
} }
_getEventBy(type, byThem) { private getEventBy(type: string, byThem = false): MatrixEvent {
if (byThem) { if (byThem) {
return this._eventsByThem.get(type); return this.eventsByThem.get(type);
} else { } else {
return this._eventsByUs.get(type); return this.eventsByUs.get(type);
} }
} }
_calculatePhaseTransitions() { private calculatePhaseTransitions(): ITransition[] {
const transitions = [{ phase: PHASE_UNSENT }]; const transitions: ITransition[] = [{ phase: PHASE_UNSENT }];
const phase = () => transitions[transitions.length - 1].phase; const phase = () => transitions[transitions.length - 1].phase;
// always pass by .request first to be sure channel.userId has been set // always pass by .request first to be sure channel.userId has been set
const hasRequestByThem = this._eventsByThem.has(REQUEST_TYPE); const hasRequestByThem = this.eventsByThem.has(REQUEST_TYPE);
const requestEvent = this._getEventBy(REQUEST_TYPE, hasRequestByThem); const requestEvent = this.getEventBy(REQUEST_TYPE, hasRequestByThem);
if (requestEvent) { if (requestEvent) {
transitions.push({ phase: PHASE_REQUESTED, event: requestEvent }); transitions.push({ phase: PHASE_REQUESTED, event: requestEvent });
} }
const readyEvent = const readyEvent =
requestEvent && this._getEventBy(READY_TYPE, !hasRequestByThem); requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem);
if (readyEvent && phase() === PHASE_REQUESTED) { if (readyEvent && phase() === PHASE_REQUESTED) {
transitions.push({ phase: PHASE_READY, event: readyEvent }); transitions.push({ phase: PHASE_READY, event: readyEvent });
} }
let startEvent; let startEvent;
if (readyEvent || !requestEvent) { if (readyEvent || !requestEvent) {
const theirStartEvent = this._eventsByThem.get(START_TYPE); const theirStartEvent = this.eventsByThem.get(START_TYPE);
const ourStartEvent = this._eventsByUs.get(START_TYPE); const ourStartEvent = this.eventsByUs.get(START_TYPE);
// any party can send .start after a .ready or unsent // any party can send .start after a .ready or unsent
if (theirStartEvent && ourStartEvent) { if (theirStartEvent && ourStartEvent) {
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ? startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ?
@@ -523,24 +552,22 @@ export class VerificationRequest extends EventEmitter {
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent; startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
} }
} else { } else {
startEvent = this._getEventBy(START_TYPE, !hasRequestByThem); startEvent = this.getEventBy(START_TYPE, !hasRequestByThem);
} }
if (startEvent) { if (startEvent) {
const fromRequestPhase = phase() === PHASE_REQUESTED && const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender();
requestEvent.getSender() !== startEvent.getSender(); const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE);
const fromUnsentPhase = phase() === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE);
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) { if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
transitions.push({ phase: PHASE_STARTED, event: startEvent }); transitions.push({ phase: PHASE_STARTED, event: startEvent });
} }
} }
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE); const ourDoneEvent = this.eventsByUs.get(DONE_TYPE);
if (this._verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) { if (this.verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
transitions.push({ phase: PHASE_DONE }); transitions.push({ phase: PHASE_DONE });
} }
const cancelEvent = this._getEventByEither(CANCEL_TYPE); const cancelEvent = this.getEventByEither(CANCEL_TYPE);
if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) { if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent }); transitions.push({ phase: PHASE_CANCELLED, event: cancelEvent });
return transitions; return transitions;
@@ -549,14 +576,14 @@ export class VerificationRequest extends EventEmitter {
return transitions; return transitions;
} }
_transitionToPhase(transition) { private transitionToPhase(transition: ITransition): void {
const { phase, event } = transition; const { phase, event } = transition;
// get common methods // get common methods
if (phase === PHASE_REQUESTED || phase === PHASE_READY) { if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
if (!this._wasSentByOwnDevice(event)) { if (!this.wasSentByOwnDevice(event)) {
const content = event.getContent(); const content = event.getContent();
this._commonMethods = this.commonMethods =
content.methods.filter(m => this._verificationMethods.has(m)); content.methods.filter(m => this.verificationMethods.has(m));
} }
} }
// detect if we're not a party in the request, and we should just observe // 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 ( if (
this.channel.receiveStartFromOtherDevices && this.channel.receiveStartFromOtherDevices &&
this._wasSentByOwnUser(event) && this.wasSentByOwnUser(event) &&
!this._wasSentByOwnDevice(event) !this.wasSentByOwnDevice(event)
) { ) {
this._observeOnly = true; this._observeOnly = true;
} }
@@ -579,7 +606,7 @@ export class VerificationRequest extends EventEmitter {
if (phase === PHASE_STARTED) { if (phase === PHASE_STARTED) {
const { method } = event.getContent(); const { method } = event.getContent();
if (!this._verifier && !this.observeOnly) { if (!this._verifier && !this.observeOnly) {
this._verifier = this._createVerifier(method, event); this._verifier = this.createVerifier(method, event);
if (!this._verifier) { if (!this._verifier) {
this.cancel({ this.cancel({
code: "m.unknown_method", code: "m.unknown_method",
@@ -592,19 +619,19 @@ export class VerificationRequest extends EventEmitter {
} }
} }
_applyPhaseTransitions() { private applyPhaseTransitions(): ITransition[] {
const transitions = this._calculatePhaseTransitions(); const transitions = this.calculatePhaseTransitions();
const existingIdx = transitions.findIndex(t => t.phase === this.phase); const existingIdx = transitions.findIndex(t => t.phase === this.phase);
// trim off phases we already went through, if any // trim off phases we already went through, if any
const newTransitions = transitions.slice(existingIdx + 1); const newTransitions = transitions.slice(existingIdx + 1);
// transition to all new phases // transition to all new phases
for (const transition of newTransitions) { for (const transition of newTransitions) {
this._transitionToPhase(transition); this.transitionToPhase(transition);
} }
return newTransitions; return newTransitions;
} }
_isWinningStartRace(newEvent) { private isWinningStartRace(newEvent: MatrixEvent): boolean {
if (newEvent.getType() !== START_TYPE) { if (newEvent.getType() !== START_TYPE) {
return false; return false;
} }
@@ -620,13 +647,13 @@ export class VerificationRequest extends EventEmitter {
const oldContent = oldEvent.getContent(); const oldContent = oldEvent.getContent();
oldRaceIdentifier = oldContent && oldContent.from_device; oldRaceIdentifier = oldContent && oldContent.from_device;
} else { } else {
oldRaceIdentifier = this._client.getDeviceId(); oldRaceIdentifier = this.client.getDeviceId();
} }
} else { } else {
if (oldEvent) { if (oldEvent) {
oldRaceIdentifier = oldEvent.getSender(); oldRaceIdentifier = oldEvent.getSender();
} else { } else {
oldRaceIdentifier = this._client.getUserId(); oldRaceIdentifier = this.client.getUserId();
} }
} }
@@ -640,13 +667,13 @@ export class VerificationRequest extends EventEmitter {
return newRaceIdentifier < oldRaceIdentifier; return newRaceIdentifier < oldRaceIdentifier;
} }
hasEventId(eventId) { public hasEventId(eventId: string): boolean {
for (const event of this._eventsByUs.values()) { for (const event of this.eventsByUs.values()) {
if (event.getId() === eventId) { if (event.getId() === eventId) {
return true; return true;
} }
} }
for (const event of this._eventsByThem.values()) { for (const event of this.eventsByThem.values()) {
if (event.getId() === eventId) { if (event.getId() === eventId) {
return true; 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. * 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 {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 {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 {boolean} 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 {boolean} 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} 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. * 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 reached phase cancelled or done, ignore anything else that comes
if (this.done || this.cancelled) { if (this.done || this.cancelled) {
return; return;
} }
const wasObserveOnly = this._observeOnly; const wasObserveOnly = this._observeOnly;
this._adjustObserveOnly(event, isLiveEvent); this.adjustObserveOnly(event, isLiveEvent);
if (!this.observeOnly && !isRemoteEcho) { if (!this.observeOnly && !isRemoteEcho) {
if (await this._cancelOnError(type, event)) { if (await this.cancelOnError(type, event)) {
return; return;
} }
} }
@@ -685,27 +718,26 @@ export class VerificationRequest extends EventEmitter {
// added here to prevent verification getting cancelled // added here to prevent verification getting cancelled
// when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365) // when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
const isDuplicateEvent = isSentByUs ? const isDuplicateEvent = isSentByUs ?
this._eventsByUs.has(type) : this.eventsByUs.has(type) :
this._eventsByThem.has(type); this.eventsByThem.has(type);
if (isDuplicateEvent) { if (isDuplicateEvent) {
return; return;
} }
const oldPhase = this.phase; 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 // this will create if needed the verifier so needs to happen before calling it
const newTransitions = this._applyPhaseTransitions(); const newTransitions = this.applyPhaseTransitions();
try { try {
// only pass events from the other side to the verifier, // only pass events from the other side to the verifier,
// no remote echos of our own events // no remote echos of our own events
if (this._verifier && !this.observeOnly) { if (this._verifier && !this.observeOnly) {
const newEventWinsRace = this._isWinningStartRace(event); const newEventWinsRace = this.isWinningStartRace(event);
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) { if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
this._verifier.switchStartEvent(event); this._verifier.switchStartEvent(event);
} else if (!isRemoteEcho) { } else if (!isRemoteEcho) {
if (type === CANCEL_TYPE || (this._verifier.events if (type === CANCEL_TYPE || this._verifier.events?.includes(type)) {
&& this._verifier.events.includes(type))) {
this._verifier.handleEvent(event); this._verifier.handleEvent(event);
} }
} }
@@ -722,16 +754,16 @@ export class VerificationRequest extends EventEmitter {
const shouldGenerateQrCode = const shouldGenerateQrCode =
this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true); this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
if (shouldGenerateQrCode) { 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 lastTransition = newTransitions[newTransitions.length - 1];
const { phase } = lastTransition; const { phase } = lastTransition;
this._setupTimeout(phase); this.setupTimeout(phase);
// set phase as last thing as this emits the "change" event // set phase as last thing as this emits the "change" event
this._setPhase(phase); this.setPhase(phase);
} else if (this._observeOnly !== wasObserveOnly) { } else if (this._observeOnly !== wasObserveOnly) {
this.emit("change"); this.emit("change");
} }
@@ -748,26 +780,26 @@ export class VerificationRequest extends EventEmitter {
} }
} }
_setupTimeout(phase) { private setupTimeout(phase: Phase): void {
const shouldTimeout = !this._timeoutTimer && !this.observeOnly && const shouldTimeout = !this.timeoutTimer && !this.observeOnly &&
phase === PHASE_REQUESTED; phase === PHASE_REQUESTED;
if (shouldTimeout) { 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 || const shouldClear = phase === PHASE_STARTED ||
phase === PHASE_READY || phase === PHASE_READY ||
phase === PHASE_DONE || phase === PHASE_DONE ||
phase === PHASE_CANCELLED; phase === PHASE_CANCELLED;
if (shouldClear) { if (shouldClear) {
clearTimeout(this._timeoutTimer); clearTimeout(this.timeoutTimer);
this._timeoutTimer = null; this.timeoutTimer = null;
} }
} }
} }
_cancelOnTimeout = () => { private cancelOnTimeout = () => {
try { try {
if (this.initiatedByMe) { if (this.initiatedByMe) {
this.cancel({ 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) { if (type === START_TYPE) {
const method = event.getContent().method; const method = event.getContent().method;
if (!this._verificationMethods.has(method)) { if (!this.verificationMethods.has(method)) {
await this.cancel(errorFromEvent(newUnknownMethodError())); await this.cancel(errorFromEvent(newUnknownMethodError()));
return true; return true;
} }
@@ -811,7 +843,7 @@ export class VerificationRequest extends EventEmitter {
return false; return false;
} }
_adjustObserveOnly(event, isLiveEvent) { private adjustObserveOnly(event: MatrixEvent, isLiveEvent = false): void {
// don't send out events for historical requests // don't send out events for historical requests
if (!isLiveEvent) { if (!isLiveEvent) {
this._observeOnly = true; 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) { if (isSentByUs) {
this._eventsByUs.set(type, event); this.eventsByUs.set(type, event);
} else { } else {
this._eventsByThem.set(type, event); this.eventsByThem.set(type, event);
} }
// once we know the userId of the other party (from the .request 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) { 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) { if (event.getSender() !== this.otherUserId) {
this._eventsByThem.delete(type); this.eventsByThem.delete(type);
} }
} }
// also remember when we received the request event // 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) { if (!targetDevice) {
targetDevice = this.targetDevice; targetDevice = this.targetDevice;
} }
const { userId, deviceId } = targetDevice; const { userId, deviceId } = targetDevice;
const VerifierCtor = this._verificationMethods.get(method); const VerifierCtor = this.verificationMethods.get(method);
if (!VerifierCtor) { if (!VerifierCtor) {
logger.warn("could not find verifier constructor for method", method); logger.warn("could not find verifier constructor for method", method);
return; return;
} }
return new VerifierCtor( return new VerifierCtor(this.channel, this.client, userId, deviceId, startEvent, this);
this.channel,
this._client,
userId,
deviceId,
startEvent,
this,
);
} }
_wasSentByOwnUser(event) { private wasSentByOwnUser(event: MatrixEvent): boolean {
return event.getSender() === this._client.getUserId(); return event.getSender() === this.client.getUserId();
} }
// only for .request, .ready or .start // only for .request, .ready or .start
_wasSentByOwnDevice(event) { private wasSentByOwnDevice(event: MatrixEvent): boolean {
if (!this._wasSentByOwnUser(event)) { if (!this.wasSentByOwnUser(event)) {
return false; return false;
} }
const content = event.getContent(); const content = event.getContent();
if (!content || content.from_device !== this._client.getDeviceId()) { if (!content || content.from_device !== this.client.getDeviceId()) {
return false; return false;
} }
return true; return true;
} }
onVerifierCancelled() { public onVerifierCancelled(): void {
this._cancelled = true; this._cancelled = true;
// move to cancelled phase // move to cancelled phase
const newTransitions = this._applyPhaseTransitions(); const newTransitions = this.applyPhaseTransitions();
if (newTransitions.length) { 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.channel.send("m.key.verification.done", {});
this._verifierHasFinished = true; this.verifierHasFinished = true;
// move to .done phase // move to .done phase
const newTransitions = this._applyPhaseTransitions(); const newTransitions = this.applyPhaseTransitions();
if (newTransitions.length) { if (newTransitions.length) {
this._setPhase(newTransitions[newTransitions.length - 1].phase); this.setPhase(newTransitions[newTransitions.length - 1].phase);
} }
} }
getEventFromOtherParty(type) { public getEventFromOtherParty(type: string): MatrixEvent {
return this._eventsByThem.get(type); return this.eventsByThem.get(type);
} }
} }