1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-05 17:02:07 +03:00

Merge pull request #1140 from matrix-org/bwindels/verification-right-panel

Support for .ready verification event (MSC2366) & other things
This commit is contained in:
Bruno Windels
2020-01-20 17:17:51 +00:00
committed by GitHub
15 changed files with 772 additions and 413 deletions

View File

@@ -22,6 +22,8 @@ import {makeTestClients} from './util';
const Olm = global.Olm;
jest.useFakeTimers();
describe("verification request", function() {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
@@ -64,7 +66,9 @@ describe("verification request", function() {
// XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer();
});
const aliceVerifier = await alice.client.requestVerification("@bob:example.com");
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
await aliceRequest.waitFor(r => r.started);
const aliceVerifier = aliceRequest.verifier;
expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay)

View File

@@ -442,9 +442,11 @@ describe("SAS verification", function() {
});
});
aliceVerifier = await alice.client.requestVerificationDM(
const aliceRequest = await alice.client.requestVerificationDM(
bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
);
await aliceRequest.waitFor(r => r.started);
aliceVerifier = aliceRequest.verifier;
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();

View File

@@ -33,38 +33,48 @@ export async function makeTestClients(userInfos, options) {
content: msg,
});
const client = clientMap[userId][deviceId];
if (event.isEncrypted()) {
event.attemptDecryption(client._crypto)
.then(() => client.emit("toDeviceEvent", event));
} else {
setTimeout(
const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client._crypto) :
Promise.resolve();
decryptionPromise.then(
() => client.emit("toDeviceEvent", event),
0,
);
}
}
}
}
}
};
const sendEvent = function(room, type, content) {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
const event = new MatrixEvent({
const rawEvent = {
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: content,
room_id: room,
event_id: eventId,
});
for (const tc of clients) {
setTimeout(
() => tc.client.emit("Room.timeline", event),
0,
);
}
origin_server_ts: Date.now(),
};
const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
unsigned: {
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
},
}));
return {event_id: eventId};
setImmediate(() => {
for (const tc of clients) {
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
console.log("sending remote echo!!");
tc.client.emit("Room.timeline", remoteEcho);
} else {
tc.client.emit("Room.timeline", event);
}
}
});
return Promise.resolve({event_id: eventId});
};
for (const userInfo of userInfos) {

View File

@@ -865,8 +865,8 @@ async function _setDeviceVerification(
* @param {Array} methods array of verification methods to use. Defaults to
* all known methods
*
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
* when the request is accepted by the other user
* @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
* when the request has been sent to the other party.
*/
MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) {
if (this._crypto === null) {
@@ -875,22 +875,6 @@ MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods)
return this._crypto.requestVerificationDM(userId, roomId, methods);
};
/**
* Accept a key verification request from a DM.
*
* @param {module:models/event~MatrixEvent} event the verification request
* that is accepted
* @param {string} method the verification mmethod to use
*
* @returns {module:crypto/verification/Base} a verifier
*/
MatrixClient.prototype.acceptVerificationDM = function(event, method) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.acceptVerificationDM(event, method);
};
/**
* Request a key verification from another user.
*
@@ -900,8 +884,8 @@ MatrixClient.prototype.acceptVerificationDM = function(event, method) {
* @param {Array} devices array of device IDs to send requests to. Defaults to
* all devices owned by the user
*
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
* when the request is accepted by the other user
* @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
* when the request has been sent to the other party.
*/
MatrixClient.prototype.requestVerification = function(userId, methods, devices) {
if (this._crypto === null) {

View File

@@ -46,8 +46,8 @@ import {SAS} from './verification/SAS';
import {keyFromPassphrase} from './key_passphrase';
import {encodeRecoveryKey} from './recoverykey';
import {VerificationRequest} from "./verification/request/VerificationRequest";
import {InRoomChannel} from "./verification/request/InRoomChannel";
import {ToDeviceChannel} from "./verification/request/ToDeviceChannel";
import {InRoomChannel, InRoomRequests} from "./verification/request/InRoomChannel";
import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDeviceChannel";
import * as httpApi from "../http-api";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -204,8 +204,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
// }
this._lastNewSessionForced = {};
this._toDeviceVerificationRequests = new Map();
this._inRoomVerificationRequests = new Map();
this._toDeviceVerificationRequests = new ToDeviceRequests();
this._inRoomVerificationRequests = new InRoomRequests();
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
@@ -1147,17 +1147,13 @@ Crypto.prototype.registerEventHandlers = function(eventEmitter) {
}
});
eventEmitter.on("toDeviceEvent", function(event) {
crypto._onToDeviceEvent(event);
});
eventEmitter.on("toDeviceEvent", crypto._onToDeviceEvent.bind(crypto));
eventEmitter.on("Room.timeline", function(event) {
crypto._onTimelineEvent(event);
});
const timelineHandler = crypto._onTimelineEvent.bind(crypto);
eventEmitter.on("Event.decrypted", function(event) {
crypto._onTimelineEvent(event);
});
eventEmitter.on("Room.timeline", timelineHandler);
eventEmitter.on("Event.decrypted", timelineHandler);
};
@@ -1557,98 +1553,79 @@ Crypto.prototype.setDeviceVerification = async function(
return deviceObj;
};
Crypto.prototype.requestVerificationDM = async function(userId, roomId, methods) {
Crypto.prototype.requestVerificationDM = function(userId, roomId, methods) {
const channel = new InRoomChannel(this._baseApis, roomId, userId);
const request = await this._requestVerificationWithChannel(
return this._requestVerificationWithChannel(
userId,
methods,
channel,
this._inRoomVerificationRequests,
);
return await request.waitForVerifier();
};
Crypto.prototype.acceptVerificationDM = function(event, method) {
if(!InRoomChannel.validateEvent(event, this._baseApis)) {
return;
}
const sender = event.getSender();
const requestsByTxnId = this._inRoomVerificationRequests.get(sender);
if (!requestsByTxnId) {
return;
}
const transactionId = InRoomChannel.getTransactionId(event);
const request = requestsByTxnId.get(transactionId);
if (!request) {
return;
}
return request.beginKeyVerification(method);
};
Crypto.prototype.requestVerification = async function(userId, methods, devices) {
Crypto.prototype.requestVerification = function(userId, methods, devices) {
if (!devices) {
devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId));
}
const channel = new ToDeviceChannel(this._baseApis, userId, devices);
const request = await this._requestVerificationWithChannel(
return this._requestVerificationWithChannel(
userId,
methods,
channel,
this._toDeviceVerificationRequests,
);
return await request.waitForVerifier();
};
Crypto.prototype._requestVerificationWithChannel = async function(
userId, methods, channel, requestsMap,
) {
if (!methods) {
// .keys() returns an iterator, so we need to explicitly turn it into an array
methods = [...this._verificationMethods.keys()];
let verificationMethods = this._verificationMethods;
if (methods) {
verificationMethods = methods.reduce((map, name) => {
const method = this._verificationMethods.get(name);
if (!method) {
throw new Error(`Verification method ${name} is not supported.`);
} else {
map.set(name, method);
}
// TODO: filter by given methods
const request = new VerificationRequest(
channel, this._verificationMethods, userId, this._baseApis);
return map;
}, new Map());
}
let request = new VerificationRequest(
channel, verificationMethods, this._baseApis);
await request.sendRequest();
let requestsByTxnId = requestsMap.get(userId);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
requestsMap.set(userId, requestsByTxnId);
// don't replace the request created by a racing remote echo
const racingRequest = requestsMap.getRequestByChannel(channel);
if (racingRequest) {
request = racingRequest;
} else {
logger.log(`Crypto: adding new request to ` +
`requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`);
requestsMap.setRequestByChannel(channel, request);
}
// TODO: we're only adding the request to the map once it has been sent
// but if the other party is really fast they could potentially respond to the
// request before the server tells us the event got sent, and we would probably
// create a new request object
requestsByTxnId.set(channel.transactionId, request);
return request;
};
Crypto.prototype.beginKeyVerification = function(
method, userId, deviceId, transactionId = null,
) {
let requestsByTxnId = this._toDeviceVerificationRequests.get(userId);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._toDeviceVerificationRequests.set(userId, requestsByTxnId);
}
let request;
if (transactionId) {
request = requestsByTxnId.get(transactionId);
request = this._toDeviceVerificationRequests.getRequestBySenderAndTxnId(
userId, transactionId);
if (!request) {
throw new Error(
`No request found for user ${userId} with ` +
`transactionId ${transactionId}`);
}
} else {
transactionId = ToDeviceChannel.makeTransactionId();
const channel = new ToDeviceChannel(
this._baseApis, userId, [deviceId], transactionId, deviceId);
request = new VerificationRequest(
channel, this._verificationMethods, userId, this._baseApis);
requestsByTxnId.set(transactionId, request);
}
if (!request) {
throw new Error(
`No request found for user ${userId} with transactionId ${transactionId}`);
channel, this._verificationMethods, this._baseApis);
this._toDeviceVerificationRequests.setRequestBySenderAndTxnId(
userId, transactionId, request);
}
return request.beginKeyVerification(method, {userId, deviceId});
};
@@ -2534,7 +2511,6 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
if (!ToDeviceChannel.validateEvent(event, this._baseApis)) {
return;
}
const transactionId = ToDeviceChannel.getTransactionId(event);
const createRequest = event => {
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
return;
@@ -2551,10 +2527,13 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
[deviceId],
);
return new VerificationRequest(
channel, this._verificationMethods, userId, this._baseApis);
channel, this._verificationMethods, this._baseApis);
};
this._handleVerificationEvent(event, transactionId,
this._toDeviceVerificationRequests, createRequest);
this._handleVerificationEvent(
event,
this._toDeviceVerificationRequests,
createRequest,
);
};
/**
@@ -2562,65 +2541,65 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
*
* @private
* @param {module:models/event.MatrixEvent} event the timeline event
* @param {module:models/Room} room not used
* @param {bool} atStart not used
* @param {bool} removed not used
* @param {bool} data.liveEvent whether this is a live event
*/
Crypto.prototype._onTimelineEvent = function(event) {
Crypto.prototype._onTimelineEvent = function(
event, room, atStart, removed, {liveEvent} = {},
) {
if (!InRoomChannel.validateEvent(event, this._baseApis)) {
return;
}
const transactionId = InRoomChannel.getTransactionId(event);
const createRequest = event => {
if (!InRoomChannel.canCreateRequest(InRoomChannel.getEventType(event))) {
return;
}
const userId = event.getSender();
const channel = new InRoomChannel(
this._baseApis,
event.getRoomId(),
userId,
);
return new VerificationRequest(
channel, this._verificationMethods, userId, this._baseApis);
channel, this._verificationMethods, this._baseApis);
};
this._handleVerificationEvent(event, transactionId,
this._inRoomVerificationRequests, createRequest);
this._handleVerificationEvent(
event,
this._inRoomVerificationRequests,
createRequest,
liveEvent,
);
};
Crypto.prototype._handleVerificationEvent = async function(
event, transactionId, requestsMap, createRequest,
event, requestsMap, createRequest, isLiveEvent = true,
) {
const sender = event.getSender();
let requestsByTxnId = requestsMap.get(sender);
let request = requestsMap.getRequest(event);
let isNewRequest = false;
let request = requestsByTxnId && requestsByTxnId.get(transactionId);
if (!request) {
request = createRequest(event);
// a request could not be made from this event, so ignore event
if (!request) {
logger.log(`Crypto: could not find VerificationRequest for ` +
`${event.getType()}, and could not create one, so ignoring.`);
return;
}
isNewRequest = true;
if (!requestsByTxnId) {
requestsByTxnId = new Map();
requestsMap.set(sender, requestsByTxnId);
}
requestsByTxnId.set(transactionId, request);
requestsMap.setRequest(event, request);
}
event.setVerificationRequest(request);
try {
const hadVerifier = !!request.verifier;
await request.channel.handleEvent(event, request);
await request.channel.handleEvent(event, request, isLiveEvent);
// emit start event when verifier got set
if (!hadVerifier && request.verifier) {
this._baseApis.emit("crypto.verification.start", request.verifier);
}
} catch (err) {
console.error("error while handling verification event", event, err);
logger.error("error while handling verification event: " + err.message);
}
if (!request.pending) {
requestsByTxnId.delete(transactionId);
if (requestsByTxnId.size === 0) {
requestsMap.delete(sender);
}
} else if (isNewRequest && !request.initiatedByMe) {
const shouldEmit = isNewRequest &&
!request.initiatedByMe &&
!request.invalid && // check it has enough events to pass the UNSENT stage
!request.observeOnly;
if (shouldEmit) {
this._baseApis.emit("crypto.verification.request", request);
}
};

View File

@@ -67,9 +67,6 @@ export class VerificationBase extends EventEmitter {
this._done = false;
this._promise = null;
this._transactionTimeoutTimer = null;
// At this point, the verification request was received so start the timeout timer.
this._resetTimer();
}
_resetTimer() {
@@ -122,8 +119,15 @@ export class VerificationBase extends EventEmitter {
} else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject;
this._reject = undefined;
reject(new Error("Other side cancelled verification"));
} else {
const content = e.getContent();
const {reason, code} = content;
reject(new Error(`Other side cancelled verification ` +
`because ${reason} (${code})`));
} else if (this._expectedEvent) {
// only cancel if there is an event expected.
// if there is no event expected, it means verify() wasn't called
// and we're just replaying the timeline events when syncing
// after a refresh when the events haven't been stored in the cache yet.
const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent
+ " but got " + e.getType(),

View File

@@ -23,12 +23,10 @@ limitations under the License.
import {MatrixEvent} from "../../models/event";
export function newVerificationError(code, reason, extradata) {
extradata = extradata || {};
extradata.code = code;
extradata.reason = reason;
const content = Object.assign({}, {code, reason}, extradata);
return new MatrixEvent({
type: "m.key.verification.cancel",
content: extradata,
content,
});
}

View File

@@ -15,7 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {REQUEST_TYPE, START_TYPE, VerificationRequest} from "./VerificationRequest";
import {
VerificationRequest,
REQUEST_TYPE,
READY_TYPE,
START_TYPE,
} from "./VerificationRequest";
import {logger} from '../../../logger';
const MESSAGE_TYPE = "m.room.message";
const M_REFERENCE = "m.reference";
@@ -31,10 +37,10 @@ export class InRoomChannel {
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
*/
constructor(client, roomId, userId) {
constructor(client, roomId, userId = null) {
this._client = client;
this._roomId = roomId;
this._userId = userId;
this.userId = userId;
this._requestEventId = null;
}
@@ -43,16 +49,45 @@ export class InRoomChannel {
return true;
}
get receiveStartFromOtherDevices() {
return true;
}
get roomId() {
return this._roomId;
}
/** The transaction id generated/used by this verification channel */
get transactionId() {
return this._requestEventId;
}
static getOtherPartyUserId(event, client) {
const type = InRoomChannel.getEventType(event);
if (type !== REQUEST_TYPE) {
return;
}
const ownUserId = client.getUserId();
const sender = event.getSender();
const content = event.getContent();
const receiver = content.to;
// request is not sent by or directed at us
if (sender !== ownUserId && receiver !== ownUserId) {
return sender;
}
if (sender === ownUserId) {
return receiver;
} else {
return sender;
}
}
/**
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
static getTimestamp(event) {
getTimestamp(event) {
return event.getTs();
}
@@ -93,23 +128,28 @@ export class InRoomChannel {
static validateEvent(event, client) {
const txnId = InRoomChannel.getTransactionId(event);
if (typeof txnId !== "string" || txnId.length === 0) {
logger.log("InRoomChannel: validateEvent: no valid txnId " + txnId);
return false;
}
const type = InRoomChannel.getEventType(event);
const content = event.getContent();
if (type === REQUEST_TYPE) {
if (typeof content.to !== "string" || !content.to.length) {
if (!content || typeof content.to !== "string" || !content.to.length) {
logger.log("InRoomChannel: validateEvent: " +
"no valid to " + (content && content.to));
return false;
}
const ownUserId = client.getUserId();
// ignore requests that are not direct to or sent by the syncing user
if (event.getSender() !== ownUserId && content.to !== ownUserId) {
if (!InRoomChannel.getOtherPartyUserId(event, client)) {
logger.log("InRoomChannel: validateEvent: " +
`not directed to or sent by me: ${event.getSender()}` +
`, ${content && content.to}`);
return false;
}
}
return VerificationRequest.validateEvent(
type, event, InRoomChannel.getTimestamp(event), client);
return VerificationRequest.validateEvent(type, event, client);
}
/**
@@ -137,21 +177,41 @@ export class InRoomChannel {
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request) {
async handleEvent(event, request, isLiveEvent) {
const type = InRoomChannel.getEventType(event);
// do validations that need state (roomId, userId),
// ignore if invalid
if (event.getRoomId() !== this._roomId || event.getSender() !== this._userId) {
if (event.getRoomId() !== this._roomId) {
return;
}
// set transactionId when receiving a .request
if (!this._requestEventId && type === REQUEST_TYPE) {
this._requestEventId = event.getId();
// set userId if not set already
if (this.userId === null) {
const userId = InRoomChannel.getOtherPartyUserId(event, this._client);
if (userId) {
this.userId = userId;
}
}
// ignore events not sent by us or the other party
const ownUserId = this._client.getUserId();
const sender = event.getSender();
if (this.userId !== null) {
if (sender !== ownUserId && sender !== this.userId) {
logger.log(`InRoomChannel: ignoring verification event from ` +
`non-participating sender ${sender}`);
return;
}
}
if (this._requestEventId === null) {
this._requestEventId = InRoomChannel.getTransactionId(event);
}
return await request.handleEvent(type, event, InRoomChannel.getTimestamp(event));
const isRemoteEcho = !!event.getUnsigned().transaction_id;
return await request.handleEvent(type, event, isLiveEvent, isRemoteEcho);
}
/**
@@ -180,7 +240,7 @@ export class InRoomChannel {
*/
completeContent(type, content) {
content = Object.assign({}, content);
if (type === REQUEST_TYPE || type === START_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
}
if (type === REQUEST_TYPE) {
@@ -191,7 +251,7 @@ export class InRoomChannel {
"verification. You will need to use legacy key " +
"verification to verify keys.",
msgtype: REQUEST_TYPE,
to: this._userId,
to: this.userId,
from_device: content.from_device,
methods: content.methods,
};
@@ -232,3 +292,59 @@ export class InRoomChannel {
}
}
}
export class InRoomRequests {
constructor() {
this._requestsByRoomId = new Map();
}
getRequest(event) {
const roomId = event.getRoomId();
const txnId = InRoomChannel.getTransactionId(event);
// console.log(`looking for request in room ${roomId} with txnId ${txnId} for an ${event.getType()} from ${event.getSender()}...`);
return this._getRequestByTxnId(roomId, txnId);
}
getRequestByChannel(channel) {
return this._getRequestByTxnId(channel.roomId, channel.transactionId);
}
_getRequestByTxnId(roomId, txnId) {
const requestsByTxnId = this._requestsByRoomId.get(roomId);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this._setRequest(
event.getRoomId(),
InRoomChannel.getTransactionId(event),
request,
);
}
setRequestByChannel(channel, request) {
this._setRequest(channel.roomId, channel.transactionId, request);
}
_setRequest(roomId, txnId, request) {
let requestsByTxnId = this._requestsByRoomId.get(roomId);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByRoomId.set(roomId, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
const roomId = event.getRoomId();
const requestsByTxnId = this._requestsByRoomId.get(roomId);
if (requestsByTxnId) {
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByRoomId.delete(roomId);
}
}
}
}

View File

@@ -1,59 +0,0 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/** a key verification channel that wraps over an actual channel to pass it to a verifier,
* to notify the VerificationRequest when the verifier tries to send anything over the channel.
* This way, the VerificationRequest can update its state based on events sent by the verifier.
* Anything that is not sending is just routing through to the wrapped channel.
*/
export class RequestCallbackChannel {
constructor(request, channel) {
this._request = request;
this._channel = channel;
}
get transactionId() {
return this._channel.transactionId;
}
get needsDoneMessage() {
return this._channel.needsDoneMessage;
}
handleEvent(event, request) {
return this._channel.handleEvent(event, request);
}
completedContentFromEvent(event) {
return this._channel.completedContentFromEvent(event);
}
completeContent(type, content) {
return this._channel.completeContent(type, content);
}
async send(type, uncompletedContent) {
this._request.handleVerifierSend(type, uncompletedContent);
const result = await this._channel.send(type, uncompletedContent);
return result;
}
async sendCompleted(type, content) {
this._request.handleVerifierSend(type, content);
const result = await this._channel.sendCompleted(type, content);
return result;
}
}

View File

@@ -20,11 +20,14 @@ import {logger} from '../../../logger';
import {
CANCEL_TYPE,
PHASE_STARTED,
PHASE_READY,
REQUEST_TYPE,
READY_TYPE,
START_TYPE,
VerificationRequest,
} from "./VerificationRequest";
import {errorFromEvent, newUnexpectedMessageError} from "../Error";
import {MatrixEvent} from "../../../models/event";
/**
* A key verification channel that sends verification events over to_device messages.
@@ -34,7 +37,7 @@ export class ToDeviceChannel {
// userId and devices of user we're about to verify
constructor(client, userId, devices, transactionId = null, deviceId = null) {
this._client = client;
this._userId = userId;
this.userId = userId;
this._devices = devices;
this.transactionId = transactionId;
this._deviceId = deviceId;
@@ -80,10 +83,12 @@ export class ToDeviceChannel {
}
const content = event.getContent();
if (!content) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
return false;
}
if (!content.transaction_id) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
return false;
}
@@ -91,6 +96,7 @@ export class ToDeviceChannel {
if (type === REQUEST_TYPE) {
if (!Number.isFinite(content.timestamp)) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
return false;
}
if (event.getSender() === client.getUserId() &&
@@ -98,19 +104,19 @@ export class ToDeviceChannel {
) {
// ignore requests from ourselves, because it doesn't make sense for a
// device to verify itself
logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
return false;
}
}
return VerificationRequest.validateEvent(
type, event, ToDeviceChannel.getTimestamp(event), client);
return VerificationRequest.validateEvent(type, event, client);
}
/**
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
static getTimestamp(event) {
getTimestamp(event) {
const content = event.getContent();
return content && content.timestamp;
}
@@ -119,9 +125,10 @@ export class ToDeviceChannel {
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request) {
async handleEvent(event, request, isLiveEvent) {
const type = event.getType();
const content = event.getContent();
if (type === REQUEST_TYPE || type === START_TYPE) {
@@ -143,14 +150,17 @@ export class ToDeviceChannel {
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
}
}
const wasStarted = request.phase === PHASE_STARTED ||
request.phase === PHASE_READY;
const wasStarted = request.phase === PHASE_STARTED;
await request.handleEvent(
event.getType(), event, ToDeviceChannel.getTimestamp(event));
const isStarted = request.phase === PHASE_STARTED;
await request.handleEvent(event.getType(), event, isLiveEvent, false);
// the request has picked a start event, tell the other devices about it
if (type === START_TYPE && !wasStarted && isStarted && this._deviceId) {
const isStarted = request.phase === PHASE_STARTED ||
request.phase === PHASE_READY;
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
// the request has picked a ready or start event, tell the other devices about it
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
if (nonChosenDevices.length) {
const message = this.completeContent({
@@ -186,7 +196,7 @@ export class ToDeviceChannel {
if (this.transactionId) {
content.transaction_id = this.transactionId;
}
if (type === REQUEST_TYPE || type === START_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
}
if (type === REQUEST_TYPE) {
@@ -216,12 +226,27 @@ export class ToDeviceChannel {
* @param {object} content
* @returns {Promise} the promise of the request
*/
sendCompleted(type, content) {
async sendCompleted(type, content) {
let result;
if (type === REQUEST_TYPE) {
return this._sendToDevices(type, content, this._devices);
result = await this._sendToDevices(type, content, this._devices);
} else {
return this._sendToDevices(type, content, [this._deviceId]);
result = await this._sendToDevices(type, content, [this._deviceId]);
}
// the VerificationRequest state machine requires remote echos of the event
// the client sends itself, so we fake this for to_device messages
const remoteEchoEvent = new MatrixEvent({
sender: this._client.getUserId(),
content,
type,
});
await this._request.handleEvent(
type,
remoteEchoEvent,
/*isLiveEvent=*/true,
/*isRemoteEcho=*/true,
);
return result;
}
_sendToDevices(type, content, devices) {
@@ -231,7 +256,7 @@ export class ToDeviceChannel {
msgMap[deviceId] = content;
}
return this._client.sendToDevice(type, {[this._userId]: msgMap});
return this._client.sendToDevice(type, {[this.userId]: msgMap});
} else {
return Promise.resolve();
}
@@ -245,3 +270,60 @@ export class ToDeviceChannel {
return randomString(32);
}
}
export class ToDeviceRequests {
constructor() {
this._requestsByUserId = new Map();
}
getRequest(event) {
return this.getRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
);
}
getRequestByChannel(channel) {
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
}
getRequestBySenderAndTxnId(sender, txnId) {
const requestsByTxnId = this._requestsByUserId.get(sender);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this.setRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
request,
);
}
setRequestByChannel(channel, request) {
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
}
setRequestBySenderAndTxnId(sender, txnId, request) {
let requestsByTxnId = this._requestsByUserId.get(sender);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByUserId.set(sender, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
const userId = event.getSender();
const requestsByTxnId = this._requestsByUserId.get(userId);
if (requestsByTxnId) {
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByUserId.delete(userId);
}
}
}
}

View File

@@ -16,7 +16,6 @@ limitations under the License.
*/
import {logger} from '../../../logger';
import {RequestCallbackChannel} from "./RequestCallbackChannel";
import {EventEmitter} from 'events';
import {
errorFactory,
@@ -41,11 +40,11 @@ export const REQUEST_TYPE = EVENT_PREFIX + "request";
export const START_TYPE = EVENT_PREFIX + "start";
export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
export const DONE_TYPE = EVENT_PREFIX + "done";
// export const READY_TYPE = EVENT_PREFIX + "ready";
export const READY_TYPE = EVENT_PREFIX + "ready";
export const PHASE_UNSENT = 1;
export const PHASE_REQUESTED = 2;
// const PHASE_READY = 3;
export const PHASE_READY = 3;
export const PHASE_STARTED = 4;
export const PHASE_CANCELLED = 5;
export const PHASE_DONE = 6;
@@ -58,17 +57,18 @@ export const PHASE_DONE = 6;
* @event "change" whenever the state of the request object has changed.
*/
export class VerificationRequest extends EventEmitter {
constructor(channel, verificationMethods, userId, client) {
constructor(channel, verificationMethods, client) {
super();
this.channel = channel;
this.channel._request = this;
this._verificationMethods = verificationMethods;
this._client = client;
this._commonMethods = [];
this._setPhase(PHASE_UNSENT, false);
this._requestEvent = null;
this._otherUserId = userId;
this._initiatedByMe = null;
this._startTimestamp = null;
this._eventsByUs = new Map();
this._eventsByThem = new Map();
this._observeOnly = false;
this._timeoutTimer = null;
}
/**
@@ -76,37 +76,36 @@ export class VerificationRequest extends EventEmitter {
* Invoked by the same static method in either 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 {number} timestamp the timestamp in milliseconds when this event was sent.
* @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
*/
static validateEvent(type, event, timestamp, client) {
static validateEvent(type, event, client) {
const content = event.getContent();
if (!content) {
logger.log("VerificationRequest: validateEvent: no content");
}
if (!type.startsWith(EVENT_PREFIX)) {
logger.log("VerificationRequest: validateEvent: " +
"fail because type doesnt start with " + EVENT_PREFIX);
return false;
}
if (type === REQUEST_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE) {
if (!Array.isArray(content.methods)) {
logger.log("VerificationRequest: validateEvent: " +
"fail because methods");
return false;
}
}
if (type === REQUEST_TYPE || type === START_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
if (typeof content.from_device !== "string" ||
content.from_device.length === 0
) {
return false;
}
}
// a timestamp is not provided on all to_device events
if (Number.isFinite(timestamp)) {
const elapsed = Date.now() - timestamp;
// ignore if event is too far in the past or too far in the future
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)) {
logger.log("received verification that is too old or from the future");
logger.log("VerificationRequest: validateEvent: "+
"fail because from_device");
return false;
}
}
@@ -114,20 +113,53 @@ export class VerificationRequest extends EventEmitter {
return true;
}
/** once the phase is PHASE_STARTED, common methods supported by both sides */
get invalid() {
return this.phase === PHASE_UNSENT;
}
/** returns whether the phase is PHASE_REQUESTED */
get requested() {
return this.phase === PHASE_REQUESTED;
}
/** returns whether the phase is PHASE_CANCELLED */
get cancelled() {
return this.phase === PHASE_CANCELLED;
}
/** returns whether the phase is PHASE_READY */
get ready() {
return this.phase === PHASE_READY;
}
/** returns whether the phase is PHASE_STARTED */
get started() {
return this.phase === PHASE_STARTED;
}
/** returns whether the phase is PHASE_DONE */
get done() {
return this.phase === PHASE_DONE;
}
/** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
get methods() {
return this._commonMethods;
}
/** the timeout of the request, provided for compatibility with previous verification code */
/** the current remaining amount of ms before the request should be automatically cancelled */
get timeout() {
const elapsed = Date.now() - this._startTimestamp;
const requestEvent = this._getEventByEither(REQUEST_TYPE);
if (requestEvent) {
const elapsed = Date.now() - this.channel.getTimestamp(requestEvent);
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
}
return 0;
}
/** the m.key.verification.request event that started this request, provided for compatibility with previous verification code */
get event() {
return this._requestEvent;
return this._getEventByEither(REQUEST_TYPE) || this._getEventByEither(START_TYPE);
}
/** current phase of the request. Some properties might only be defined in a current phase. */
@@ -142,8 +174,7 @@ export class VerificationRequest extends EventEmitter {
/** whether this request has sent it's initial event and needs more events to complete */
get pending() {
return this._phase !== PHASE_UNSENT
&& this._phase !== PHASE_DONE
return this._phase !== PHASE_DONE
&& this._phase !== PHASE_CANCELLED;
}
@@ -152,7 +183,25 @@ export class VerificationRequest extends EventEmitter {
* For ToDeviceChannel, this is who sent the .start event
*/
get initiatedByMe() {
return this._initiatedByMe;
// event created by us but no remote echo has been received yet
const noEventsYet = (this._eventsByUs.size + this._eventsByThem.size) === 0;
if (this._phase === PHASE_UNSENT && noEventsYet) {
return true;
}
const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE);
const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE);
if (hasMyRequest && !hasTheirRequest) {
return true;
}
if (!hasMyRequest && hasTheirRequest) {
return false;
}
const hasMyStart = this._eventsByUs.has(START_TYPE);
const hasTheirStart = this._eventsByThem.has(START_TYPE);
if (hasMyStart && !hasTheirStart) {
return true;
}
return false;
}
/** the id of the user that initiated the request */
@@ -160,19 +209,45 @@ export class VerificationRequest extends EventEmitter {
if (this.initiatedByMe) {
return this._client.getUserId();
} else {
return this._otherUserId;
return this.otherUserId;
}
}
/** the id of the user that (will) receive(d) the request */
get receivingUserId() {
if (this.initiatedByMe) {
return this._otherUserId;
return this.otherUserId;
} else {
return this._client.getUserId();
}
}
/** the user id of the other party in this request */
get otherUserId() {
return this.channel.userId;
}
/**
* the id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED
*/
get cancellingUserId() {
const myCancel = this._eventsByUs.get(CANCEL_TYPE);
const theirCancel = this._eventsByThem.get(CANCEL_TYPE);
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
return myCancel.getSender();
}
if (theirCancel) {
return theirCancel.getSender();
}
return undefined;
}
get observeOnly() {
return this._observeOnly;
}
/* Start the key verification, creating a verifier and sending a .start event.
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
* @param {string} method the name of the verification method to use.
@@ -182,8 +257,13 @@ export class VerificationRequest extends EventEmitter {
*/
beginKeyVerification(method, targetDevice = null) {
// need to allow also when unsent in case of to_device
if (!this._verifier) {
if (this._hasValidPreStartPhase()) {
if (!this.observeOnly && !this._verifier) {
const validStartPhase =
this.phase === PHASE_REQUESTED ||
this.phase === PHASE_READY ||
(this.phase === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE));
if (validStartPhase) {
// when called on a request that was initiated with .request event
// check the method is supported by both sides
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
@@ -203,12 +283,9 @@ export class VerificationRequest extends EventEmitter {
* @returns {Promise} resolves when the event has been sent.
*/
async sendRequest() {
if (this._phase === PHASE_UNSENT) {
this._initiatedByMe = true;
this._setPhase(PHASE_REQUESTED, false);
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
const methods = [...this._verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, {methods});
this.emit("change");
}
}
@@ -219,32 +296,54 @@ export class VerificationRequest extends EventEmitter {
* @returns {Promise} resolves when the event has been sent.
*/
async cancel({reason = "User declined", code = "m.user"} = {}) {
if (this._phase !== PHASE_CANCELLED) {
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason));
} else {
this._setPhase(PHASE_CANCELLED, false);
this._cancellingUserId = this._client.getUserId();
await this.channel.send(CANCEL_TYPE, {code, reason});
}
this.emit("change");
}
}
/** @returns {Promise} with the verifier once it becomes available. Can be used after calling `sendRequest`. */
waitForVerifier() {
if (this.verifier) {
return Promise.resolve(this.verifier);
} else {
return new Promise(resolve => {
const checkVerifier = () => {
if (this.verifier) {
this.off("change", checkVerifier);
resolve(this.verifier);
/**
* Accepts the request, sending a .ready event to the other party
* @returns {Promise} resolves when the event has been sent.
*/
async accept() {
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
const methods = [...this._verificationMethods.keys()];
await this.channel.send(READY_TYPE, {methods});
}
}
/**
* Can be used to listen for state changes until the callback returns true.
* @param {Function} fn callback to evaluate whether the request is in the desired state.
* Takes the request as an argument.
* @returns {Promise} that resolves once the callback returns true
* @throws {Error} when the request is cancelled
*/
waitFor(fn) {
return new Promise((resolve, reject) => {
const check = () => {
let handled = false;
if (fn(this)) {
resolve(this);
handled = true;
} else if (this.cancelled) {
reject(new Error("cancelled"));
handled = true;
}
if (handled) {
this.off("change", check);
}
return handled;
};
this.on("change", checkVerifier);
});
if (!check()) {
this.on("change", check);
}
});
}
_setPhase(phase, notify = true) {
@@ -254,155 +353,278 @@ export class VerificationRequest extends EventEmitter {
}
}
_getEventByEither(type) {
return this._eventsByThem.get(type) || this._eventsByUs.get(type);
}
_getEventByOther(type, notSender) {
if (notSender === this._client.getUserId()) {
return this._eventsByThem.get(type);
} else {
return this._eventsByUs.get(type);
}
}
_getEventBy(type, sender) {
if (sender === this._client.getUserId()) {
return this._eventsByUs.get(type);
} else {
return this._eventsByThem.get(type);
}
}
_calculatePhaseTransitions() {
const transitions = [{phase: PHASE_UNSENT}];
const phase = () => transitions[transitions.length - 1].phase;
// always pass by .request first to be sure channel.userId has been set
const requestEvent = this._getEventByEither(REQUEST_TYPE);
if (requestEvent) {
transitions.push({phase: PHASE_REQUESTED, event: requestEvent});
}
const readyEvent =
requestEvent && this._getEventByOther(READY_TYPE, requestEvent.getSender());
if (readyEvent && phase() === PHASE_REQUESTED) {
transitions.push({phase: PHASE_READY, event: readyEvent});
}
const startEvent = readyEvent || !requestEvent ?
this._getEventByEither(START_TYPE) : // any party can send .start after a .ready or unsent
this._getEventByOther(START_TYPE, requestEvent.getSender());
if (startEvent) {
const fromRequestPhase = phase() === PHASE_REQUESTED &&
requestEvent.getSender() !== startEvent.getSender();
const fromUnsentPhase = phase() === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE);
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
transitions.push({phase: PHASE_STARTED, event: startEvent});
}
}
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
const theirDoneEvent = this._eventsByThem.get(DONE_TYPE);
if (ourDoneEvent && theirDoneEvent && phase() === PHASE_STARTED) {
transitions.push({phase: PHASE_DONE});
}
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
if (cancelEvent && phase() !== PHASE_DONE) {
transitions.push({phase: PHASE_CANCELLED, event: cancelEvent});
return transitions;
}
return transitions;
}
_transitionToPhase(transition) {
const {phase, event} = transition;
// get common methods
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
if (!this._wasSentByOwnDevice(event)) {
const content = event.getContent();
this._commonMethods =
content.methods.filter(m => this._verificationMethods.has(m));
}
}
// detect if we're not a party in the request, and we should just observe
if (!this.observeOnly) {
// if requested or accepted by one of my other devices
if (phase === PHASE_REQUESTED ||
phase === PHASE_STARTED ||
phase === PHASE_READY
) {
if (
this.channel.receiveStartFromOtherDevices &&
this._wasSentByOwnUser(event) &&
!this._wasSentByOwnDevice(event)
) {
this._observeOnly = true;
}
}
}
// create verifier
if (phase === PHASE_STARTED) {
const {method} = event.getContent();
if (!this._verifier && !this.observeOnly) {
this._verifier = this._createVerifier(method, event);
}
}
}
/**
* Changes the state of the request and verifier in response to a key verification event.
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
* @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead.
* @param {number} timestamp the timestamp in milliseconds when this event was sent.
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(type, event, timestamp) {
const content = event.getContent();
if (type === REQUEST_TYPE || type === START_TYPE) {
if (this._startTimestamp === null) {
this._startTimestamp = timestamp;
}
}
if (type === REQUEST_TYPE) {
await this._handleRequest(content, event);
} else if (type === START_TYPE) {
await this._handleStart(content, event);
async handleEvent(type, event, isLiveEvent, isRemoteEcho) {
// if reached phase cancelled or done, ignore anything else that comes
if (!this.pending) {
return;
}
if (this._verifier) {
this._adjustObserveOnly(event, isLiveEvent);
if (!this.observeOnly && !isRemoteEcho) {
if (await this._cancelOnError(type, event)) {
return;
}
}
this._addEvent(type, event, isRemoteEcho);
const transitions = this._calculatePhaseTransitions();
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
// trim off phases we already went through, if any
const newTransitions = transitions.slice(existingIdx + 1);
// transition to all new phases
for (const transition of newTransitions) {
this._transitionToPhase(transition);
}
// only pass events from the other side to the verifier,
// no remote echos of our own events
if (this._verifier && !isRemoteEcho) {
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
this._verifier.handleEvent(event);
}
}
if (type === CANCEL_TYPE) {
this._handleCancel();
} else if (type === DONE_TYPE) {
this._handleDone();
if (newTransitions.length) {
const lastTransition = newTransitions[newTransitions.length - 1];
const {phase} = lastTransition;
this._setupTimeout(phase);
// set phase as last thing as this emits the "change" event
this._setPhase(phase);
}
}
async _handleRequest(content, event) {
if (this._phase === PHASE_UNSENT) {
const otherMethods = content.methods;
this._commonMethods = otherMethods.
filter(m => this._verificationMethods.has(m));
this._requestEvent = event;
this._initiatedByMe = this._wasSentByMe(event);
this._setPhase(PHASE_REQUESTED);
} else if (this._phase !== PHASE_REQUESTED) {
logger.warn("Ignoring flagged verification request from " +
event.getSender());
await this.cancel(errorFromEvent(newUnexpectedMessageError()));
_setupTimeout(phase) {
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
phase === PHASE_REQUESTED && this.initiatedByMe;
if (shouldTimeout) {
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
}
if (this._timeoutTimer) {
const shouldClear = phase === PHASE_STARTED ||
phase === PHASE_READY ||
phase === PHASE_DONE ||
phase === PHASE_CANCELLED;
if (shouldClear) {
clearTimeout(this._timeoutTimer);
this._timeoutTimer = null;
}
}
}
_hasValidPreStartPhase() {
return this._phase === PHASE_REQUESTED ||
(
this.channel.constructor.canCreateRequest(START_TYPE) &&
this._phase === PHASE_UNSENT
);
_cancelOnTimeout = () => {
try {
this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
} catch (err) {
console.error("Error while cancelling verification request", err);
}
};
async _handleStart(content, event) {
if (this._hasValidPreStartPhase()) {
const {method} = content;
async _cancelOnError(type, event) {
if (type === START_TYPE) {
const method = event.getContent().method;
if (!this._verificationMethods.has(method)) {
await this.cancel(errorFromEvent(newUnknownMethodError()));
return true;
}
}
/* FIXME: https://github.com/vector-im/riot-web/issues/11765 */
const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED;
if (isUnexpectedRequest || isUnexpectedReady) {
logger.warn(`Cancelling, unexpected ${type} verification ` +
`event from ${event.getSender()}`);
const reason = `Unexpected ${type} event in phase ${this.phase}`;
await this.cancel(errorFromEvent(newUnexpectedMessageError({reason})));
return true;
}
return false;
}
_adjustObserveOnly(event, isLiveEvent) {
// don't send out events for historical requests
if (!isLiveEvent) {
this._observeOnly = true;
}
// a timestamp is not provided on all to_device events
const timestamp = this.channel.getTimestamp(event);
if (Number.isFinite(timestamp)) {
const elapsed = Date.now() - timestamp;
// don't allow interaction on old requests
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)
) {
this._observeOnly = true;
}
}
}
_addEvent(type, event, isRemoteEcho) {
if (isRemoteEcho || this._wasSentByOwnDevice(event)) {
this._eventsByUs.set(type, event);
} else {
// if not in requested phase
if (this.phase === PHASE_UNSENT) {
this._initiatedByMe = this._wasSentByMe(event);
}
this._verifier = this._createVerifier(method, event);
this._setPhase(PHASE_STARTED);
}
}
this._eventsByThem.set(type, event);
}
/**
* Called by RequestCallbackChannel when the verifier sends an event
* @param {string} type the "symbolic" event type
* @param {object} content the completed or uncompleted content for the event to be sent
*/
handleVerifierSend(type, content) {
if (type === CANCEL_TYPE) {
this._handleCancel();
} else if (type === START_TYPE) {
if (this._phase === PHASE_UNSENT || this._phase === PHASE_REQUESTED) {
// if unsent, we're sending a (first) .start event and hence requesting the verification.
// in any other situation, the request was initiated by the other party.
this._initiatedByMe = this.phase === PHASE_UNSENT;
this._setPhase(PHASE_STARTED);
// once we know the userId of the other party (from the .request event)
// see if any event by anyone else crept into this._eventsByThem
if (type === REQUEST_TYPE) {
for (const [type, event] of this._eventsByThem.entries()) {
if (event.getSender() !== this.otherUserId) {
this._eventsByThem.delete(type);
}
}
}
_handleCancel() {
if (this._phase !== PHASE_CANCELLED) {
this._setPhase(PHASE_CANCELLED);
}
}
_handleDone() {
if (this._phase === PHASE_STARTED) {
this._setPhase(PHASE_DONE);
}
}
_createVerifier(method, startEvent = null, targetDevice = null) {
const startSentByMe = startEvent && this._wasSentByMe(startEvent);
const {userId, deviceId} = this._getVerifierTarget(startEvent, targetDevice);
const startedByMe = !startEvent || this._wasSentByOwnDevice(startEvent);
if (!targetDevice) {
const theirFirstEvent =
this._eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE) ||
this._eventsByThem.get(START_TYPE);
const theirFirstContent = theirFirstEvent.getContent();
const fromDevice = theirFirstContent.from_device;
targetDevice = {
userId: this.otherUserId,
deviceId: fromDevice,
};
}
const {userId, deviceId} = targetDevice;
const VerifierCtor = this._verificationMethods.get(method);
if (!VerifierCtor) {
console.warn("could not find verifier constructor for method", method);
return;
}
// invokes handleVerifierSend when verifier sends something
const callbackMedium = new RequestCallbackChannel(this, this.channel);
return new VerifierCtor(
callbackMedium,
this.channel,
this._client,
userId,
deviceId,
startSentByMe ? null : startEvent,
startedByMe ? null : startEvent,
);
}
_getVerifierTarget(startEvent, targetDevice) {
// targetDevice should be set when creating a verifier for to_device before the .start event has been sent,
// so the userId and deviceId are provided
if (targetDevice) {
return targetDevice;
} else {
let targetEvent;
if (startEvent && !this._wasSentByMe(startEvent)) {
targetEvent = startEvent;
} else if (this._requestEvent && !this._wasSentByMe(this._requestEvent)) {
targetEvent = this._requestEvent;
} else {
throw new Error(
"can't determine who the verifier should be targeted at. " +
"No .request or .start event and no targetDevice");
}
const userId = targetEvent.getSender();
const content = targetEvent.getContent();
const deviceId = content && content.from_device;
return {userId, deviceId};
}
_wasSentByOwnUser(event) {
return event.getSender() === this._client.getUserId();
}
// only for .request and .start
_wasSentByMe(event) {
if (event.getSender() !== this._client.getUserId()) {
// only for .request, .ready or .start
_wasSentByOwnDevice(event) {
if (!this._wasSentByOwnUser(event)) {
return false;
}
const content = event.getContent();

View File

@@ -490,8 +490,9 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
*/
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy, fromCache) {
if (this._filter) {
const events = this._filter.filterRoomTimeline([event]);
if (!events.length) {
@@ -529,7 +530,7 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
return;
}
this.addEventToTimeline(event, this._liveTimeline, false);
this.addEventToTimeline(event, this._liveTimeline, false, fromCache);
};
/**
@@ -541,11 +542,12 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
* @param {MatrixEvent} event
* @param {EventTimeline} timeline
* @param {boolean} toStartOfTimeline
* @param {boolean} fromCache whether the sync response came from cache
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
toStartOfTimeline) {
toStartOfTimeline, fromCache) {
const eventId = event.getId();
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
@@ -555,7 +557,7 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
const data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline && !fromCache,
};
this.emit("Room.timeline", event, this.room,
Boolean(toStartOfTimeline), false, data);

View File

@@ -154,6 +154,12 @@ export const MatrixEvent = function(
* attempt may succeed)
*/
this._retryDecryption = false;
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
* `Crypto` will set this the `VerificationRequest` for the event
* so it can be easily accessed from the timeline.
*/
this.verificationRequest = null;
};
utils.inherits(MatrixEvent, EventEmitter);
@@ -1054,6 +1060,10 @@ utils.extend(MatrixEvent.prototype, {
encrypted: this.event,
};
},
setVerificationRequest: function(request) {
this.verificationRequest = request;
},
});

View File

@@ -1067,10 +1067,11 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) {
if (event.isRedaction()) {
const redactId = event.event.redacts;
@@ -1117,7 +1118,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
// add to our timeline sets
for (let i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
this._timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
}
// synthesize and inject implicit read receipts
@@ -1427,9 +1428,10 @@ Room.prototype._revertRedactionLocalEcho = function(redactionEvent) {
* this function will be ignored entirely, preserving the existing event in the
* timeline. Events are identical based on their event ID <b>only</b>.
*
* @param {boolean} fromCache whether the sync response came from cache
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
Room.prototype.addLiveEvents = function(events, duplicateStrategy, fromCache) {
let i;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
@@ -1455,7 +1457,7 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
for (i = 0; i < events.length; i++) {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this._addLiveEvent(events[i], duplicateStrategy);
this._addLiveEvent(events[i], duplicateStrategy, fromCache);
}
};

View File

@@ -688,6 +688,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
oldSyncToken: null,
nextSyncToken,
catchingUp: false,
fromCache: true,
};
const data = {
@@ -1237,7 +1238,8 @@ SyncApi.prototype._processSyncResponse = async function(
}
}
self._processRoomEvents(room, stateEvents, timelineEvents);
self._processRoomEvents(room, stateEvents,
timelineEvents, syncEventData.fromCache);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1564,10 +1566,11 @@ SyncApi.prototype._resolveInvites = function(room) {
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
* at the *START* of the timeline list if it is supplied.
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
* @param {boolean} fromCache whether the sync response came from cache
* is earlier in time. Higher index is later.
*/
SyncApi.prototype._processRoomEvents = function(room, stateEventList,
timelineEventList) {
timelineEventList, fromCache) {
// If there are no events in the timeline yet, initialise it with
// the given state events
const liveTimeline = room.getLiveTimeline();
@@ -1621,7 +1624,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// if the timeline has any state events in it.
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
room.addLiveEvents(timelineEventList || []);
room.addLiveEvents(timelineEventList || [], null, fromCache);
};
/**