You've already forked matrix-js-sdk
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:
@@ -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";
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -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");
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
@@ -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) => {
|
||||||
@@ -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,
|
||||||
@@ -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) {
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user