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; const Olm = global.Olm;
jest.useFakeTimers();
describe("verification request", function() { describe("verification request", function() {
if (!global.Olm) { if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present'); 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) // XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer(); 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); expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay) // 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], bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
); );
await aliceRequest.waitFor(r => r.started);
aliceVerifier = aliceRequest.verifier;
aliceVerifier.on("show_sas", (e) => { aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) { if (!e.sas.emoji || !e.sas.decimal) {
e.cancel(); e.cancel();

View File

@@ -33,38 +33,48 @@ export async function makeTestClients(userInfos, options) {
content: msg, content: msg,
}); });
const client = clientMap[userId][deviceId]; const client = clientMap[userId][deviceId];
if (event.isEncrypted()) { const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client._crypto) event.attemptDecryption(client._crypto) :
.then(() => client.emit("toDeviceEvent", event)); Promise.resolve();
} else {
setTimeout( decryptionPromise.then(
() => client.emit("toDeviceEvent", event), () => client.emit("toDeviceEvent", event),
0,
); );
} }
} }
} }
} }
}
}; };
const sendEvent = function(room, type, content) { const sendEvent = function(room, type, content) {
// make up a unique ID as the event ID // make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this 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 sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type, type: type,
content: content, content: content,
room_id: room, room_id: room,
event_id: eventId, event_id: eventId,
}); origin_server_ts: Date.now(),
for (const tc of clients) { };
setTimeout( const event = new MatrixEvent(rawEvent);
() => tc.client.emit("Room.timeline", event), const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
0, 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) { 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 * @param {Array} methods array of verification methods to use. Defaults to
* all known methods * all known methods
* *
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier * @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
* when the request is accepted by the other user * when the request has been sent to the other party.
*/ */
MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) { MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) {
if (this._crypto === null) { if (this._crypto === null) {
@@ -875,22 +875,6 @@ MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods)
return this._crypto.requestVerificationDM(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. * 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 * @param {Array} devices array of device IDs to send requests to. Defaults to
* all devices owned by the user * all devices owned by the user
* *
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier * @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
* when the request is accepted by the other user * when the request has been sent to the other party.
*/ */
MatrixClient.prototype.requestVerification = function(userId, methods, devices) { MatrixClient.prototype.requestVerification = function(userId, methods, devices) {
if (this._crypto === null) { if (this._crypto === null) {

View File

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

View File

@@ -67,9 +67,6 @@ export class VerificationBase extends EventEmitter {
this._done = false; this._done = false;
this._promise = null; this._promise = null;
this._transactionTimeoutTimer = null; this._transactionTimeoutTimer = null;
// At this point, the verification request was received so start the timeout timer.
this._resetTimer();
} }
_resetTimer() { _resetTimer() {
@@ -122,8 +119,15 @@ export class VerificationBase extends EventEmitter {
} 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;
reject(new Error("Other side cancelled verification")); const content = e.getContent();
} else { 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( const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent "Unexpected message: expecting " + this._expectedEvent
+ " but got " + e.getType(), + " but got " + e.getType(),

View File

@@ -23,12 +23,10 @@ limitations under the License.
import {MatrixEvent} from "../../models/event"; import {MatrixEvent} from "../../models/event";
export function newVerificationError(code, reason, extradata) { export function newVerificationError(code, reason, extradata) {
extradata = extradata || {}; const content = Object.assign({}, {code, reason}, extradata);
extradata.code = code;
extradata.reason = reason;
return new MatrixEvent({ return new MatrixEvent({
type: "m.key.verification.cancel", 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. 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 MESSAGE_TYPE = "m.room.message";
const M_REFERENCE = "m.reference"; 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} 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) { constructor(client, roomId, userId = null) {
this._client = client; this._client = client;
this._roomId = roomId; this._roomId = roomId;
this._userId = userId; this.userId = userId;
this._requestEventId = null; this._requestEventId = null;
} }
@@ -43,16 +49,45 @@ export class InRoomChannel {
return true; return true;
} }
get receiveStartFromOtherDevices() {
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() { get transactionId() {
return this._requestEventId; 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 * @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
*/ */
static getTimestamp(event) { getTimestamp(event) {
return event.getTs(); return event.getTs();
} }
@@ -93,23 +128,28 @@ export class InRoomChannel {
static validateEvent(event, client) { static validateEvent(event, client) {
const txnId = InRoomChannel.getTransactionId(event); const txnId = InRoomChannel.getTransactionId(event);
if (typeof txnId !== "string" || txnId.length === 0) { if (typeof txnId !== "string" || txnId.length === 0) {
logger.log("InRoomChannel: validateEvent: no valid txnId " + txnId);
return false; return false;
} }
const type = InRoomChannel.getEventType(event); const type = InRoomChannel.getEventType(event);
const content = event.getContent(); const content = event.getContent();
if (type === REQUEST_TYPE) { 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; return false;
} }
const ownUserId = client.getUserId();
// ignore requests that are not direct to or sent by the syncing user // 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 false;
} }
} }
return VerificationRequest.validateEvent( return VerificationRequest.validateEvent(type, event, client);
type, event, InRoomChannel.getTimestamp(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. * 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
* @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 anwser to the passed-in event are sent.
*/ */
async handleEvent(event, request) { async handleEvent(event, request, isLiveEvent) {
const type = InRoomChannel.getEventType(event); const type = InRoomChannel.getEventType(event);
// 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 || event.getSender() !== this._userId) {
if (event.getRoomId() !== this._roomId) {
return; return;
} }
// set transactionId when receiving a .request // set userId if not set already
if (!this._requestEventId && type === REQUEST_TYPE) { if (this.userId === null) {
this._requestEventId = event.getId(); 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) { completeContent(type, content) {
content = Object.assign({}, 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(); content.from_device = this._client.getDeviceId();
} }
if (type === REQUEST_TYPE) { if (type === REQUEST_TYPE) {
@@ -191,7 +251,7 @@ export class InRoomChannel {
"verification. You will need to use legacy key " + "verification. You will need to use legacy key " +
"verification to verify keys.", "verification to verify keys.",
msgtype: REQUEST_TYPE, msgtype: REQUEST_TYPE,
to: this._userId, to: this.userId,
from_device: content.from_device, from_device: content.from_device,
methods: content.methods, 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 { import {
CANCEL_TYPE, CANCEL_TYPE,
PHASE_STARTED, PHASE_STARTED,
PHASE_READY,
REQUEST_TYPE, REQUEST_TYPE,
READY_TYPE,
START_TYPE, START_TYPE,
VerificationRequest, VerificationRequest,
} from "./VerificationRequest"; } from "./VerificationRequest";
import {errorFromEvent, newUnexpectedMessageError} from "../Error"; import {errorFromEvent, newUnexpectedMessageError} from "../Error";
import {MatrixEvent} from "../../../models/event";
/** /**
* A key verification channel that sends verification events over to_device messages. * 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 // userId and devices of user we're about to verify
constructor(client, userId, devices, transactionId = null, deviceId = null) { constructor(client, userId, devices, transactionId = null, deviceId = null) {
this._client = client; this._client = client;
this._userId = userId; this.userId = userId;
this._devices = devices; this._devices = devices;
this.transactionId = transactionId; this.transactionId = transactionId;
this._deviceId = deviceId; this._deviceId = deviceId;
@@ -80,10 +83,12 @@ export class ToDeviceChannel {
} }
const content = event.getContent(); const content = event.getContent();
if (!content) { if (!content) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
return false; return false;
} }
if (!content.transaction_id) { if (!content.transaction_id) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
return false; return false;
} }
@@ -91,6 +96,7 @@ export class ToDeviceChannel {
if (type === REQUEST_TYPE) { if (type === REQUEST_TYPE) {
if (!Number.isFinite(content.timestamp)) { if (!Number.isFinite(content.timestamp)) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
return false; return false;
} }
if (event.getSender() === client.getUserId() && if (event.getSender() === client.getUserId() &&
@@ -98,19 +104,19 @@ export class ToDeviceChannel {
) { ) {
// ignore requests from ourselves, because it doesn't make sense for a // ignore requests from ourselves, because it doesn't make sense for a
// device to verify itself // device to verify itself
logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
return false; return false;
} }
} }
return VerificationRequest.validateEvent( return VerificationRequest.validateEvent(type, event, client);
type, event, ToDeviceChannel.getTimestamp(event), client);
} }
/** /**
* @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
*/ */
static getTimestamp(event) { getTimestamp(event) {
const content = event.getContent(); const content = event.getContent();
return content && content.timestamp; 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. * 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
* @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 anwser to the passed-in event are sent.
*/ */
async handleEvent(event, request) { async handleEvent(event, request, isLiveEvent) {
const type = event.getType(); const type = event.getType();
const content = event.getContent(); const content = event.getContent();
if (type === REQUEST_TYPE || type === START_TYPE) { if (type === REQUEST_TYPE || type === START_TYPE) {
@@ -143,14 +150,17 @@ export class ToDeviceChannel {
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]); 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, isLiveEvent, false);
await request.handleEvent(
event.getType(), event, ToDeviceChannel.getTimestamp(event));
const isStarted = request.phase === PHASE_STARTED;
// the request has picked a start event, tell the other devices about it const isStarted = request.phase === PHASE_STARTED ||
if (type === START_TYPE && !wasStarted && isStarted && this._deviceId) { 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); const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
if (nonChosenDevices.length) { if (nonChosenDevices.length) {
const message = this.completeContent({ const message = this.completeContent({
@@ -186,7 +196,7 @@ export class ToDeviceChannel {
if (this.transactionId) { if (this.transactionId) {
content.transaction_id = 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(); content.from_device = this._client.getDeviceId();
} }
if (type === REQUEST_TYPE) { if (type === REQUEST_TYPE) {
@@ -216,12 +226,27 @@ export class ToDeviceChannel {
* @param {object} content * @param {object} content
* @returns {Promise} the promise of the request * @returns {Promise} the promise of the request
*/ */
sendCompleted(type, content) { async sendCompleted(type, content) {
let result;
if (type === REQUEST_TYPE) { if (type === REQUEST_TYPE) {
return this._sendToDevices(type, content, this._devices); result = await this._sendToDevices(type, content, this._devices);
} else { } 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) { _sendToDevices(type, content, devices) {
@@ -231,7 +256,7 @@ export class ToDeviceChannel {
msgMap[deviceId] = content; msgMap[deviceId] = content;
} }
return this._client.sendToDevice(type, {[this._userId]: msgMap}); return this._client.sendToDevice(type, {[this.userId]: msgMap});
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
@@ -245,3 +270,60 @@ export class ToDeviceChannel {
return randomString(32); 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 {logger} from '../../../logger';
import {RequestCallbackChannel} from "./RequestCallbackChannel";
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import { import {
errorFactory, errorFactory,
@@ -41,11 +40,11 @@ export const REQUEST_TYPE = EVENT_PREFIX + "request";
export const START_TYPE = EVENT_PREFIX + "start"; export const START_TYPE = EVENT_PREFIX + "start";
export const CANCEL_TYPE = EVENT_PREFIX + "cancel"; 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 const PHASE_UNSENT = 1;
export const PHASE_REQUESTED = 2; export const PHASE_REQUESTED = 2;
// const PHASE_READY = 3; export const PHASE_READY = 3;
export const PHASE_STARTED = 4; export const PHASE_STARTED = 4;
export const PHASE_CANCELLED = 5; export const PHASE_CANCELLED = 5;
export const PHASE_DONE = 6; 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. * @event "change" whenever the state of the request object has changed.
*/ */
export class VerificationRequest extends EventEmitter { export class VerificationRequest extends EventEmitter {
constructor(channel, verificationMethods, userId, client) { constructor(channel, verificationMethods, client) {
super(); super();
this.channel = channel; this.channel = channel;
this.channel._request = this;
this._verificationMethods = verificationMethods; this._verificationMethods = verificationMethods;
this._client = client; this._client = client;
this._commonMethods = []; this._commonMethods = [];
this._setPhase(PHASE_UNSENT, false); this._setPhase(PHASE_UNSENT, false);
this._requestEvent = null; this._eventsByUs = new Map();
this._otherUserId = userId; this._eventsByThem = new Map();
this._initiatedByMe = null; this._observeOnly = false;
this._startTimestamp = null; this._timeoutTimer = null;
} }
/** /**
@@ -76,37 +76,36 @@ export class VerificationRequest extends EventEmitter {
* Invoked by the same static method in either channel. * 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 {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 {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 * @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 {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(); const content = event.getContent();
if (!content) {
logger.log("VerificationRequest: validateEvent: no content");
}
if (!type.startsWith(EVENT_PREFIX)) { if (!type.startsWith(EVENT_PREFIX)) {
logger.log("VerificationRequest: validateEvent: " +
"fail because type doesnt start with " + EVENT_PREFIX);
return false; return false;
} }
if (type === REQUEST_TYPE) { if (type === REQUEST_TYPE || type === READY_TYPE) {
if (!Array.isArray(content.methods)) { if (!Array.isArray(content.methods)) {
logger.log("VerificationRequest: validateEvent: " +
"fail because methods");
return false; 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" || if (typeof content.from_device !== "string" ||
content.from_device.length === 0 content.from_device.length === 0
) { ) {
return false; logger.log("VerificationRequest: validateEvent: "+
} "fail because from_device");
}
// 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");
return false; return false;
} }
} }
@@ -114,20 +113,53 @@ export class VerificationRequest extends EventEmitter {
return true; 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() { get methods() {
return this._commonMethods; 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() { 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 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 */ /** the m.key.verification.request event that started this request, provided for compatibility with previous verification code */
get event() { 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. */ /** 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 */ /** whether this request has sent it's initial event and needs more events to complete */
get pending() { get pending() {
return this._phase !== PHASE_UNSENT return this._phase !== PHASE_DONE
&& this._phase !== PHASE_DONE
&& this._phase !== PHASE_CANCELLED; && this._phase !== PHASE_CANCELLED;
} }
@@ -152,7 +183,25 @@ export class VerificationRequest extends EventEmitter {
* For ToDeviceChannel, this is who sent the .start event * For ToDeviceChannel, this is who sent the .start event
*/ */
get initiatedByMe() { 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 */ /** the id of the user that initiated the request */
@@ -160,19 +209,45 @@ export class VerificationRequest extends EventEmitter {
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() { get receivingUserId() {
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 */
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. /* 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. * 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. * @param {string} method the name of the verification method to use.
@@ -182,8 +257,13 @@ export class VerificationRequest extends EventEmitter {
*/ */
beginKeyVerification(method, targetDevice = null) { beginKeyVerification(method, targetDevice = null) {
// need to allow also when unsent in case of to_device // need to allow also when unsent in case of to_device
if (!this._verifier) { if (!this.observeOnly && !this._verifier) {
if (this._hasValidPreStartPhase()) { 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 // 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)) {
@@ -203,12 +283,9 @@ export class VerificationRequest extends EventEmitter {
* @returns {Promise} resolves when the event has been sent. * @returns {Promise} resolves when the event has been sent.
*/ */
async sendRequest() { async sendRequest() {
if (this._phase === PHASE_UNSENT) { if (!this.observeOnly && this._phase === PHASE_UNSENT) {
this._initiatedByMe = true;
this._setPhase(PHASE_REQUESTED, false);
const methods = [...this._verificationMethods.keys()]; const methods = [...this._verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, {methods}); 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. * @returns {Promise} resolves when the event has been sent.
*/ */
async cancel({reason = "User declined", code = "m.user"} = {}) { async cancel({reason = "User declined", code = "m.user"} = {}) {
if (this._phase !== PHASE_CANCELLED) { if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
if (this._verifier) { if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason)); return this._verifier.cancel(errorFactory(code, reason));
} else { } else {
this._setPhase(PHASE_CANCELLED, false); this._cancellingUserId = this._client.getUserId();
await this.channel.send(CANCEL_TYPE, {code, reason}); 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() { * Accepts the request, sending a .ready event to the other party
if (this.verifier) { * @returns {Promise} resolves when the event has been sent.
return Promise.resolve(this.verifier); */
} else { async accept() {
return new Promise(resolve => { if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
const checkVerifier = () => { const methods = [...this._verificationMethods.keys()];
if (this.verifier) { await this.channel.send(READY_TYPE, {methods});
this.off("change", checkVerifier);
resolve(this.verifier);
} }
}
/**
* 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) { _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. * 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 {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. * @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/ */
async handleEvent(type, event, timestamp) { async handleEvent(type, event, isLiveEvent, isRemoteEcho) {
const content = event.getContent(); // if reached phase cancelled or done, ignore anything else that comes
if (type === REQUEST_TYPE || type === START_TYPE) { if (!this.pending) {
if (this._startTimestamp === null) { return;
this._startTimestamp = timestamp;
}
}
if (type === REQUEST_TYPE) {
await this._handleRequest(content, event);
} else if (type === START_TYPE) {
await this._handleStart(content, event);
} }
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 if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) { && this._verifier.events.includes(type))) {
this._verifier.handleEvent(event); this._verifier.handleEvent(event);
} }
} }
if (type === CANCEL_TYPE) { if (newTransitions.length) {
this._handleCancel(); const lastTransition = newTransitions[newTransitions.length - 1];
} else if (type === DONE_TYPE) { const {phase} = lastTransition;
this._handleDone();
this._setupTimeout(phase);
// set phase as last thing as this emits the "change" event
this._setPhase(phase);
} }
} }
async _handleRequest(content, event) { _setupTimeout(phase) {
if (this._phase === PHASE_UNSENT) { const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
const otherMethods = content.methods; phase === PHASE_REQUESTED && this.initiatedByMe;
this._commonMethods = otherMethods.
filter(m => this._verificationMethods.has(m)); if (shouldTimeout) {
this._requestEvent = event; this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
this._initiatedByMe = this._wasSentByMe(event); }
this._setPhase(PHASE_REQUESTED); if (this._timeoutTimer) {
} else if (this._phase !== PHASE_REQUESTED) { const shouldClear = phase === PHASE_STARTED ||
logger.warn("Ignoring flagged verification request from " + phase === PHASE_READY ||
event.getSender()); phase === PHASE_DONE ||
await this.cancel(errorFromEvent(newUnexpectedMessageError())); phase === PHASE_CANCELLED;
if (shouldClear) {
clearTimeout(this._timeoutTimer);
this._timeoutTimer = null;
}
} }
} }
_hasValidPreStartPhase() { _cancelOnTimeout = () => {
return this._phase === PHASE_REQUESTED || try {
( this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
this.channel.constructor.canCreateRequest(START_TYPE) && } catch (err) {
this._phase === PHASE_UNSENT console.error("Error while cancelling verification request", err);
);
} }
};
async _handleStart(content, event) { async _cancelOnError(type, event) {
if (this._hasValidPreStartPhase()) { if (type === START_TYPE) {
const {method} = content; 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;
}
}
/* 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 { } else {
// if not in requested phase this._eventsByThem.set(type, event);
if (this.phase === PHASE_UNSENT) {
this._initiatedByMe = this._wasSentByMe(event);
}
this._verifier = this._createVerifier(method, event);
this._setPhase(PHASE_STARTED);
}
}
} }
/** // once we know the userId of the other party (from the .request event)
* Called by RequestCallbackChannel when the verifier sends an event // see if any event by anyone else crept into this._eventsByThem
* @param {string} type the "symbolic" event type if (type === REQUEST_TYPE) {
* @param {object} content the completed or uncompleted content for the event to be sent for (const [type, event] of this._eventsByThem.entries()) {
*/ if (event.getSender() !== this.otherUserId) {
handleVerifierSend(type, content) { this._eventsByThem.delete(type);
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);
} }
} }
} }
_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) { _createVerifier(method, startEvent = null, targetDevice = null) {
const startSentByMe = startEvent && this._wasSentByMe(startEvent); const startedByMe = !startEvent || this._wasSentByOwnDevice(startEvent);
const {userId, deviceId} = this._getVerifierTarget(startEvent, targetDevice); 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); const VerifierCtor = this._verificationMethods.get(method);
if (!VerifierCtor) { if (!VerifierCtor) {
console.warn("could not find verifier constructor for method", method); console.warn("could not find verifier constructor for method", method);
return; return;
} }
// invokes handleVerifierSend when verifier sends something
const callbackMedium = new RequestCallbackChannel(this, this.channel);
return new VerifierCtor( return new VerifierCtor(
callbackMedium, this.channel,
this._client, this._client,
userId, userId,
deviceId, deviceId,
startSentByMe ? null : startEvent, startedByMe ? null : startEvent,
); );
} }
_getVerifierTarget(startEvent, targetDevice) { _wasSentByOwnUser(event) {
// targetDevice should be set when creating a verifier for to_device before the .start event has been sent, return event.getSender() === this._client.getUserId();
// 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};
}
} }
// only for .request and .start // only for .request, .ready or .start
_wasSentByMe(event) { _wasSentByOwnDevice(event) {
if (event.getSender() !== this._client.getUserId()) { if (!this._wasSentByOwnUser(event)) {
return false; return false;
} }
const content = event.getContent(); const content = event.getContent();

View File

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

View File

@@ -154,6 +154,12 @@ export const MatrixEvent = function(
* attempt may succeed) * attempt may succeed)
*/ */
this._retryDecryption = false; 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); utils.inherits(MatrixEvent, EventEmitter);
@@ -1054,6 +1060,10 @@ utils.extend(MatrixEvent.prototype, {
encrypted: this.event, 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 {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace' * @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline" * @fires module:client~MatrixClient#event:"Room.timeline"
* @private * @private
*/ */
Room.prototype._addLiveEvent = function(event, duplicateStrategy) { Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) {
if (event.isRedaction()) { if (event.isRedaction()) {
const redactId = event.event.redacts; const redactId = event.event.redacts;
@@ -1117,7 +1118,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
// add to our timeline sets // add to our timeline sets
for (let i = 0; i < this._timelineSets.length; i++) { 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 // 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 * this function will be ignored entirely, preserving the existing event in the
* timeline. Events are identical based on their event ID <b>only</b>. * 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'. * @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; let i;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) { if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'"); 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++) { for (i = 0; i < events.length; i++) {
// TODO: We should have a filter to say "only add state event // TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline". // 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, oldSyncToken: null,
nextSyncToken, nextSyncToken,
catchingUp: false, catchingUp: false,
fromCache: true,
}; };
const data = { 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, // set summary after processing events,
// because it will trigger a name calculation // 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 * @param {MatrixEvent[]} stateEventList A list of state events. This is the state
* at the *START* of the timeline list if it is supplied. * at the *START* of the timeline list if it is supplied.
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index * @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. * is earlier in time. Higher index is later.
*/ */
SyncApi.prototype._processRoomEvents = function(room, stateEventList, SyncApi.prototype._processRoomEvents = function(room, stateEventList,
timelineEventList) { timelineEventList, fromCache) {
// If there are no events in the timeline yet, initialise it with // If there are no events in the timeline yet, initialise it with
// the given state events // the given state events
const liveTimeline = room.getLiveTimeline(); const liveTimeline = room.getLiveTimeline();
@@ -1621,7 +1624,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// if the timeline has any state events in it. // 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 // This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc. // to be decorated with sender etc.
room.addLiveEvents(timelineEventList || []); room.addLiveEvents(timelineEventList || [], null, fromCache);
}; };
/** /**