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

Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup

This commit is contained in:
J. Ryan Stinnett
2019-11-29 11:23:48 +00:00
67 changed files with 2496 additions and 1247 deletions

View File

@@ -54,6 +54,7 @@ import {
newUnexpectedMessageError,
newUnknownMethodError,
} from './verification/Error';
import {sleep} from '../utils';
const defaultVerificationMethods = {
[ScanQRCode.NAME]: ScanQRCode,
@@ -70,10 +71,44 @@ export const verificationMethods = {
SAS: SAS.NAME,
};
// the recommended amount of time before a verification request
// should be (automatically) cancelled without user interaction
// and ignored.
const VERIFICATION_REQUEST_TIMEOUT = 5 * 60 * 1000; //5m
// to avoid almost expired verification notifications
// from showing a notification and almost immediately
// disappearing, also ignore verification requests that
// are this amount of time away from expiring.
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; //3s
export function isCryptoAvailable() {
return Boolean(global.Olm);
}
/* subscribes to timeline events / to_device events for SAS verification */
function listenForEvents(client, roomId, listener) {
let isEncrypted = false;
if (roomId) {
isEncrypted = client.isRoomEncrypted(roomId);
}
if (isEncrypted) {
client.on("Event.decrypted", listener);
}
client.on("event", listener);
let subscribed = true;
return function() {
if (subscribed) {
if (isEncrypted) {
client.off("Event.decrypted", listener);
}
client.off("event", listener);
subscribed = false;
}
return null;
};
}
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
@@ -979,6 +1014,14 @@ Crypto.prototype.registerEventHandlers = function(eventEmitter) {
eventEmitter.on("toDeviceEvent", function(event) {
crypto._onToDeviceEvent(event);
});
eventEmitter.on("Room.timeline", function(event) {
crypto._onTimelineEvent(event);
});
eventEmitter.on("Event.decrypted", function(event) {
crypto._onTimelineEvent(event);
});
};
@@ -1362,13 +1405,15 @@ function verificationEventHandler(target, userId, roomId, eventId) {
|| event.getSender() !== userId) {
return;
}
const content = event.getContent();
if (!content["m.relates_to"]) {
// ignore events that haven't been decrypted yet.
// we also listen for undecrypted events, just in case
// the other side would be sending unencrypted events in an e2ee room
if (event.getType() === "m.room.encrypted") {
return;
}
const relatesTo
= content["m.relationship"] || content["m.relates_to"];
if (!relatesTo.rel_type
const relatesTo = event.getRelation();
if (!relatesTo
|| !relatesTo.rel_type
|| relatesTo.rel_type !== "m.reference"
|| !relatesTo.event_id
|| relatesTo.event_id !== eventId) {
@@ -1425,7 +1470,9 @@ Crypto.prototype.requestVerificationDM = async function(userId, roomId, methods)
);
// this handler gets removed when the verification finishes
// (see the verify method of crypto/verification/Base.js)
this._baseApis.on("event", verifier.handler);
const subscription =
listenForEvents(this._baseApis, roomId, verifier.handler);
verifier.setEventsSubscription(subscription);
resolve(verifier);
break;
}
@@ -1435,14 +1482,19 @@ Crypto.prototype.requestVerificationDM = async function(userId, roomId, methods)
}
}
};
this._baseApis.on("event", listener);
let initialResponseSubscription =
listenForEvents(this._baseApis, roomId, listener);
const resolve = (...args) => {
this._baseApis.off("event", listener);
if (initialResponseSubscription) {
initialResponseSubscription = initialResponseSubscription();
}
_resolve(...args);
};
const reject = (...args) => {
this._baseApis.off("event", listener);
if (initialResponseSubscription) {
initialResponseSubscription = initialResponseSubscription();
}
_reject(...args);
};
});
@@ -1477,7 +1529,9 @@ Crypto.prototype.acceptVerificationDM = function(event, Method) {
verifier.handler = verificationEventHandler(
verifier, event.getSender(), event.getRoomId(), event.getId(),
);
this._baseApis.on("event", verifier.handler);
const subscription = listenForEvents(
this._baseApis, event.getRoomId(), verifier.handler);
verifier.setEventsSubscription(subscription);
return verifier;
};
@@ -1668,7 +1722,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
// It would otherwise just throw later as an unknown algorithm would, but we may
// as well catch this here
if (!config.algorithm) {
console.log("Ignoring setRoomEncryption with no algorithm");
logger.log("Ignoring setRoomEncryption with no algorithm");
return;
}
@@ -1854,17 +1908,15 @@ Crypto.prototype.exportRoomKeys = async function() {
* @return {module:client.Promise} a promise which resolves once the keys have been imported
*/
Crypto.prototype.importRoomKeys = function(keys) {
return Promise.map(
keys, (key) => {
if (!key.room_id || !key.algorithm) {
logger.warn("ignoring room key entry with missing fields", key);
return null;
}
return Promise.all(keys.map((key) => {
if (!key.room_id || !key.algorithm) {
logger.warn("ignoring room key entry with missing fields", key);
return null;
}
const alg = this._getRoomDecryptor(key.room_id, key.algorithm);
return alg.importRoomKey(key);
},
);
const alg = this._getRoomDecryptor(key.room_id, key.algorithm);
return alg.importRoomKey(key);
}));
};
/**
@@ -1883,7 +1935,7 @@ Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
// requests from different clients hitting the server all at
// the same time when a new key is sent
const delay = Math.random() * maxDelay;
await Promise.delay(delay);
await sleep(delay);
let numFailures = 0; // number of consecutive failures
while (1) {
if (!this.backupKey) {
@@ -1917,7 +1969,7 @@ Crypto.prototype.scheduleKeyBackupSend = async function(maxDelay = 10000) {
}
if (numFailures) {
// exponential backoff if we have failures
await Promise.delay(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
}
}
} finally {
@@ -2326,6 +2378,9 @@ Crypto.prototype._getTrackedE2eRooms = function() {
Crypto.prototype._onToDeviceEvent = function(event) {
try {
logger.log(`received to_device ${event.getType()} from: ` +
`${event.getSender()} id: ${event.getId()}`);
if (event.getType() == "m.room_key"
|| event.getType() == "m.forwarded_room_key") {
this._onRoomKeyEvent(event);
@@ -2392,8 +2447,8 @@ Crypto.prototype._onKeyVerificationRequest = function(event) {
const content = event.getContent();
if (!("from_device" in content) || typeof content.from_device !== "string"
|| !("transaction_id" in content) || typeof content.from_device !== "string"
|| !("methods" in content) || !(content.methods instanceof Array)
|| !("transaction_id" in content)
|| !("methods" in content) || !Array.isArray(content.methods)
|| !("timestamp" in content) || typeof content.timestamp !== "number") {
logger.warn("received invalid verification request from " + event.getSender());
// ignore event if malformed
@@ -2469,6 +2524,7 @@ Crypto.prototype._onKeyVerificationRequest = function(event) {
// notify the application of the verification request, so it can
// decide what to do with it
const request = {
timeout: VERIFICATION_REQUEST_TIMEOUT,
event: event,
methods: methods,
beginKeyVerification: (method) => {
@@ -2601,6 +2657,31 @@ Crypto.prototype._onKeyVerificationStart = function(event) {
}
}
this._baseApis.emit("crypto.verification.start", verifier);
// Riot does not implement `m.key.verification.request` while
// verifying over to_device messages, but the protocol is made to
// work when starting with a `m.key.verification.start` event straight
// away as well. Verification over DM *does* start with a request event first,
// and to expose a uniform api to monitor verification requests, we mock
// the request api here for to_device messages.
// "crypto.verification.start" is kept for backwards compatibility.
// ahh, this will create 2 request notifications for clients that do support the request event
// so maybe we should remove emitting the request when actually receiving it *sigh*
const requestAdapter = {
event,
timeout: VERIFICATION_REQUEST_TIMEOUT,
methods: [content.method],
beginKeyVerification: (method) => {
if (content.method === method) {
return verifier;
}
},
cancel: () => {
return verifier.cancel("User cancelled");
},
};
this._baseApis.emit("crypto.verification.request", requestAdapter);
}
};
@@ -2633,6 +2714,53 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
}
};
/**
* Handle key verification requests sent as timeline events
*
* @private
* @param {module:models/event.MatrixEvent} event the timeline event
*/
Crypto.prototype._onTimelineEvent = function(event) {
if (event.getType() !== "m.room.message") {
return;
}
const content = event.getContent();
if (content.msgtype !== "m.key.verification.request") {
return;
}
// ignore event if malformed
if (!("from_device" in content) || typeof content.from_device !== "string"
|| !("methods" in content) || !Array.isArray(content.methods)
|| !("to" in content) || typeof content.to !== "string") {
logger.warn("received invalid verification request over DM from "
+ event.getSender());
return;
}
// check the request was directed to the syncing user
if (content.to !== this._baseApis.getUserId()) {
return;
}
const timeout = VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN;
if (event.getLocalAge() >= timeout) {
return;
}
const request = {
event,
timeout: VERIFICATION_REQUEST_TIMEOUT,
methods: content.methods,
beginKeyVerification: (method) => {
const verifier = this.acceptVerificationDM(event, method);
return verifier;
},
cancel: () => {
const verifier = this.acceptVerificationDM(event, content.methods[0]);
verifier.cancel("User declined");
},
};
this._baseApis.emit("crypto.verification.request", request);
};
/**
* Handle a toDevice event that couldn't be decrypted
*
@@ -2815,14 +2943,10 @@ Crypto.prototype._processReceivedRoomKeyRequests = async function() {
// cancellation (and end up with a cancelled request), rather than the
// cancellation before the request (and end up with an outstanding
// request which should have been cancelled.)
await Promise.map(
requests, (req) =>
this._processReceivedRoomKeyRequest(req),
);
await Promise.map(
cancellations, (cancellation) =>
this._processReceivedRoomKeyRequestCancellation(cancellation),
);
await Promise.all(requests.map((req) =>
this._processReceivedRoomKeyRequest(req)));
await Promise.all(cancellations.map((cancellation) =>
this._processReceivedRoomKeyRequestCancellation(cancellation)));
} catch (e) {
logger.error(`Error processing room key requsts: ${e}`);
} finally {