Compare commits

..

21 Commits

Author SHA1 Message Date
Richard van der Hoff e8c6002d08 v0.7.5 2017-02-04 10:15:09 +00:00
Richard van der Hoff d9033812a2 Prepare changelog for v0.7.5 2017-02-04 10:15:02 +00:00
Richard van der Hoff 2e6b93f886 v0.7.5-rc.3 2017-02-03 15:24:28 +00:00
Richard van der Hoff afc4e145b6 Prepare changelog for v0.7.5-rc.3 2017-02-03 15:24:21 +00:00
Richard van der Hoff cee243a2a2 prep changelog 2017-02-03 15:21:15 +00:00
Richard van der Hoff 5fd74109ff Fix device list update
s/flushNewDeviceRequests/refreshOutdatedDeviceLists/ - this got fixed on one PR
and apparenlty I failed to merge the changes correctly
2017-02-03 14:29:50 +00:00
Richard van der Hoff a3cc8eb1f6 Include DeviceInfo in deviceVerificationChanged events
... to help the UI update itself
2017-02-03 14:27:08 +00:00
David Baker bd4de4832c v0.7.5-rc.2 2017-02-03 13:01:15 +00:00
David Baker 9e74c934a1 Prepare changelog for v0.7.5-rc.2 2017-02-03 13:01:14 +00:00
David Baker a056d4916a Prepare changelog for v0.7.5-rc.2 2017-02-03 13:00:26 +00:00
David Baker 31630859a2 Prepare changelog for v0.7.5-rc.2 2017-02-03 12:57:45 +00:00
David Baker 8cb41f6797 Merge remote-tracking branch 'origin/develop' into release-v0.7.5 2017-02-03 12:54:32 +00:00
Richard van der Hoff c3a8aeca42 Merge pull request #348 from matrix-org/rav/device_list_stream
Use the device change notifications interface
2017-02-03 12:49:33 +00:00
Richard van der Hoff eaa95fb1e5 Merge pull request #347 from matrix-org/rav/rewrite_device_query_logic
Rewrite the device key query logic
2017-02-03 12:49:11 +00:00
Richard van der Hoff 8d502743a5 Refresh device list on startup
On initialsync, call the /keys/changes api to see which users have updated
their devices. (On failure, invalidate all of them).
2017-02-03 00:33:56 +00:00
Richard van der Hoff 732a764ec6 Refactor crypto initialsync handling
Pass a store into the Crypto object so that it doesn't need to make assumptions
about the EventEmitter, and use the new metadata on sync events to distinguish
between initialsyncs and normal syncs
2017-02-03 00:33:54 +00:00
Richard van der Hoff 9975786bac Store the token corresponding to the last device update in localstorage
... so that we can, in future, use it when restarting the client.
2017-02-03 00:32:24 +00:00
Richard van der Hoff 89ef4aa6e7 Handle device change notifications from /sync
When we get a notification from /sync that a user has updated their device
list, mark the list outdated, and then fire off a device query.
2017-02-03 00:32:16 +00:00
Richard van der Hoff 7e82ac3620 Merge branch 'develop' into rav/rewrite_device_query_logic 2017-02-03 00:12:46 +00:00
Richard van der Hoff c3440c506c Address review comments
Update some comments, and s/flushNewDeviceRequests/refreshOutdatedDeviceLists/.
2017-02-03 00:10:13 +00:00
Richard van der Hoff 94addb6315 Rewrite the device key query logic
Only permit one query per user at a time.
2017-02-02 13:49:43 +00:00
9 changed files with 407 additions and 182 deletions
+25
View File
@@ -1,3 +1,28 @@
Changes in [0.7.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5) (2017-02-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.3...v0.7.5)
No changes from 0.7.5-rc.3
Changes in [0.7.5-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.3) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.2...v0.7.5-rc.3)
* Include DeviceInfo in deviceVerificationChanged events
[a3cc8eb](https://github.com/matrix-org/matrix-js-sdk/commit/a3cc8eb1f6d165576a342596f638316721cb26b6)
* Fix device list update
[5fd7410](https://github.com/matrix-org/matrix-js-sdk/commit/5fd74109ffc56b73deb40c2604d84c38b8032c40)
Changes in [0.7.5-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.2) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.1...v0.7.5-rc.2)
* Use the device change notifications interface
[\#348](https://github.com/matrix-org/matrix-js-sdk/pull/348)
* Rewrite the device key query logic
[\#347](https://github.com/matrix-org/matrix-js-sdk/pull/347)
Changes in [0.7.5-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.1) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.4...v0.7.5-rc.1)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "0.7.5-rc.1",
"version": "0.7.5",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
+5 -1
View File
@@ -347,7 +347,11 @@ describe("MatrixClient", function() {
*/
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}}});
expect(req.data).toEqual({device_keys: {
'@alice:localhost': {},
'boris': {},
'chaz': {},
}});
}).respond(200, {
device_keys: {
boris: borisKeys,
+45 -8
View File
@@ -1018,20 +1018,35 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
*
* @param {string[]} userIds list of users to get keys for
*
* @param {module:client.callback=} callback
* @param {Object=} opts
*
* @param {string=} opts.token sync token to pass in the query request, to help
* the HS give the most recent results
*
* @return {module:client.Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
const downloadQuery = {};
for (let i = 0; i < userIds.length; ++i) {
downloadQuery[userIds[i]] = {};
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
if (utils.isFunction(opts)) {
// opts used to be 'callback'.
throw new Error(
'downloadKeysForUsers no longer accepts a callback parameter',
);
}
const content = {device_keys: downloadQuery};
opts = opts || {};
const content = {
device_keys: {},
};
if ('token' in opts) {
content.token = opts.token;
}
userIds.forEach((u) => {
content.device_keys[u] = {};
});
return this._http.authedRequestWithPrefix(
callback, "POST", "/keys/query", undefined, content,
undefined, "POST", "/keys/query", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
};
@@ -1067,6 +1082,28 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
);
};
/**
* Ask the server for a list of users who have changed their device lists
* between a pair of sync tokens
*
* @param {string} oldToken
* @param {string} newToken
*
* @return {module:client.Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
const qps = {
from: oldToken,
to: newToken,
};
return this._http.authedRequestWithPrefix(
undefined, "GET", "/keys/changes", qps, undefined,
httpApi.PREFIX_UNSTABLE,
);
};
// Identity Server Operations
// ==========================
+27 -4
View File
@@ -158,6 +158,7 @@ function MatrixClient(opts) {
this, this,
opts.sessionStore,
userId, this.deviceId,
this.store,
);
this.olmVersion = Crypto.getOlmVersion();
@@ -417,8 +418,10 @@ function _setDeviceVerification(client, userId, deviceId, verified, blocked, kno
if (!client._crypto) {
throw new Error("End-to-End encryption disabled");
}
client._crypto.setDeviceVerification(userId, deviceId, verified, blocked, known);
client.emit("deviceVerificationChanged", userId, deviceId);
const dev = client._crypto.setDeviceVerification(
userId, deviceId, verified, blocked, known,
);
client.emit("deviceVerificationChanged", userId, deviceId, dev);
}
/**
@@ -2665,8 +2668,6 @@ MatrixClient.prototype.startClient = function(opts) {
};
}
this._clientOpts = opts;
if (this._crypto) {
this._crypto.uploadKeys(5).done();
const tenMinutes = 1000 * 60 * 10;
@@ -2684,6 +2685,13 @@ MatrixClient.prototype.startClient = function(opts) {
console.error("Still have sync object whilst not running: stopping old one");
this._syncApi.stop();
}
// shallow-copy the opts dict before modifying and storing it
opts = Object.assign({}, opts);
opts.crypto = this._crypto;
this._clientOpts = opts;
this._syncApi = new SyncApi(this, opts);
this._syncApi.sync();
};
@@ -3067,12 +3075,26 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* </ul>
*
* @event module:client~MatrixClient#"sync"
*
* @param {string} state An enum representing the syncing state. One of "PREPARED",
* "SYNCING", "ERROR", "STOPPED".
*
* @param {?string} prevState An enum representing the previous syncing state.
* One of "PREPARED", "SYNCING", "ERROR", "STOPPED" <b>or null</b>.
*
* @param {?Object} data Data about this transition.
*
* @param {MatrixError} data.err The matrix error if <code>state=ERROR</code>.
*
* @param {String} data.oldSyncToken The 'since' token passed to /sync.
* <code>null</code> for the first successful sync since this client was
* started. Only present if <code>state=PREPARED</code> or
* <code>state=SYNCING</code>.
*
* @param {String} data.nextSyncToken The 'next_batch' result from /sync, which
* will become the 'since' token for the next call to /sync. Only present if
* <code>state=PREPARED</code> or <code>state=SYNCING</code>.
*
* @example
* matrixClient.on("sync", function(state, prevState, data) {
* switch (state) {
@@ -3143,6 +3165,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* @event module:client~MatrixClient#"deviceVerificationChanged"
* @param {string} userId the owner of the verified device
* @param {string} deviceId the id of the verified device
* @param {module:crypto/deviceinfo} deviceInfo updated device information
*/
/**
+115 -121
View File
@@ -25,7 +25,6 @@ import q from 'q';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
import utils from '../utils';
/**
* @alias module:crypto/DeviceList
@@ -36,11 +35,14 @@ export default class DeviceList {
this._sessionStore = sessionStore;
this._olmDevice = olmDevice;
// users with outdated device lists
// userId -> true
this._pendingUsersWithNewDevices = {};
// userId -> [promise, ...]
// userId -> promise
this._keyDownloadsInProgressByUser = {};
this.lastKnownSyncToken = null;
}
/**
@@ -53,45 +55,30 @@ export default class DeviceList {
* module:crypto/deviceinfo|DeviceInfo}.
*/
downloadKeys(userIds, forceDownload) {
const self = this;
// promises we need to wait for while the download happens
const promises = [];
// list of userids we need to download keys for
let downloadUsers = [];
function perUserCatch(u) {
return function(e) {
console.warn('Error downloading keys for user ' + u + ':', e);
};
}
if (forceDownload) {
downloadUsers = userIds;
} else {
for (let i = 0; i < userIds.length; ++i) {
const u = userIds[i];
const inprogress = this._keyDownloadsInProgressByUser[u];
if (inprogress) {
// wait for the download to complete
promises.push(q.any(inprogress).catch(perUserCatch(u)));
} else if (!this.getStoredDevicesForUser(u)) {
downloadUsers.push(u);
let needsRefresh = false;
userIds.forEach((u) => {
if (this._keyDownloadsInProgressByUser[u]) {
// just wait for the existing download to complete
promises.push(this._keyDownloadsInProgressByUser[u]);
} else {
if (forceDownload || !this.getStoredDevicesForUser(u)) {
this.invalidateUserDeviceList(u);
}
if (this._pendingUsersWithNewDevices[u]) {
needsRefresh = true;
}
}
});
if (needsRefresh) {
promises.push(this.refreshOutdatedDeviceLists(true));
}
if (downloadUsers.length > 0) {
const r = this._doKeyDownloadForUsers(downloadUsers);
downloadUsers.map(function(u) {
promises.push(r[u].catch(perUserCatch(u)));
});
}
return q.all(promises).then(function() {
return self._getDevicesFromStore(userIds);
return q.all(promises).then(() => {
return this._getDevicesFromStore(userIds);
});
}
@@ -205,139 +192,146 @@ export default class DeviceList {
* Mark the cached device list for the given user outdated.
*
* This doesn't set off an update, so that several users can be batched
* together. Call flushDeviceListRequests() for that.
* together. Call refreshOutdatedDeviceLists() for that.
*
* @param {String} userId
*/
invalidateUserDeviceList(userId) {
// sanity-check the userId. This is mostly paranoia, but if synapse
// can't parse the userId we give it as an mxid, it 500s the whole
// request and we can never update the device lists again (because
// the broken userId is always 'invalid' and always included in any
// refresh request).
// By checking it is at least a string, we can eliminate a class of
// silly errors.
if (typeof userId !== 'string') {
throw new Error('userId must be a string; was '+userId);
}
this._pendingUsersWithNewDevices[userId] = true;
}
/**
* Start device queries for any users who sent us an m.new_device recently
* Start device queries for any users with outdated device lists
*
* We tolerate multiple concurrent device queries, but only one query per
* user.
*
* If any users already have downloads in progress, they are ignored - they
* will be refreshed when the current download completes anyway, so
* each user with outdated device lists will be updated eventually.
*
* The returned promise resolves immediately if there are no users with
* outdated device lists, or if all users with outdated device lists already
* have a query in progress.
*
* Otherwise, a new query request is made, and the promise resolves
* once that query completes. If the query fails, the promise will reject
* if rejectOnFailure was truthy, otherwise it will still resolve.
*
* @param {Boolean?} rejectOnFailure true to make the returned promise
* reject if the device list query fails.
*
* @return {Promise}
*/
flushNewDeviceRequests() {
const users = Object.keys(this._pendingUsersWithNewDevices);
refreshOutdatedDeviceLists(rejectOnFailure) {
const users = Object.keys(this._pendingUsersWithNewDevices).filter(
(u) => !this._keyDownloadsInProgressByUser[u],
);
if (users.length === 0) {
return;
return q();
}
const r = this._doKeyDownloadForUsers(users);
let prom = this._doKeyDownloadForUsers(users).then(() => {
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
});
// we've kicked off requests to these users: remove their
// pending flag for now.
this._pendingUsersWithNewDevices = {};
// flush out any more requests that were blocked up while that
// was going on, but let the initial promise complete now.
//
this.refreshOutdatedDeviceLists().done();
}, (e) => {
console.error(
'Error updating device key cache for ' + users + ":", e,
);
users.map((u) => {
r[u] = r[u].catch((e) => {
console.error(
'Error updating device keys for user ' + u + ':', e,
);
// reinstate the pending flags on any users which failed; this will
// mean that we will do another download in the future, but won't
// tight-loop.
//
// reinstate the pending flags on any users which failed; this will
// mean that we will do another download in the future, but won't
// tight-loop.
//
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
this._pendingUsersWithNewDevices[u] = true;
});
// TODO: schedule a retry.
throw e;
});
q.all(Object.values(r)).done();
users.forEach((u) => {
delete this._pendingUsersWithNewDevices[u];
this._keyDownloadsInProgressByUser[u] = prom;
});
if (!rejectOnFailure) {
// normally we just want to swallow the exception - we've already
// logged it futher up.
prom = prom.catch((e) => {});
}
return prom;
}
/**
* @param {string[]} downloadUsers list of userIds
*
* @return {Object} a map from userId to a promise for a result for that user
* @return {Promise}
*/
_doKeyDownloadForUsers(downloadUsers) {
const self = this;
console.log('Starting key download for ' + downloadUsers);
const deferMap = {};
const promiseMap = {};
downloadUsers.map(function(u) {
const deferred = q.defer();
const promise = deferred.promise.finally(function() {
const inProgress = self._keyDownloadsInProgressByUser[u];
utils.removeElement(inProgress, function(e) {
return e === promise;
});
if (inProgress.length === 0) {
// no more downloads for this user; remove the element
delete self._keyDownloadsInProgressByUser[u];
}
});
if (!self._keyDownloadsInProgressByUser[u]) {
self._keyDownloadsInProgressByUser[u] = [];
}
self._keyDownloadsInProgressByUser[u].push(promise);
deferMap[u] = deferred;
promiseMap[u] = promise;
});
this._baseApis.downloadKeysForUsers(
downloadUsers,
).done(function(res) {
const token = this.lastKnownSyncToken;
const opts = {};
if (token) {
opts.token = token;
}
return this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then((res) => {
const dk = res.device_keys || {};
for (let i = 0; i < downloadUsers.length; ++i) {
const userId = downloadUsers[i];
var deviceId;
for (const userId of downloadUsers) {
console.log('got keys for ' + userId + ':', dk[userId]);
if (!dk[userId]) {
// no result for this user
const err = 'Unknown';
// TODO: do something with res.failures
deferMap[userId].reject(err);
continue;
}
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = self._sessionStore.getEndToEndDevicesForUser(userId);
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (devs) {
for (deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
}
}
Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
});
}
_updateStoredDeviceKeysForUser(
self._olmDevice, userId, userStore, dk[userId],
);
this._olmDevice, userId, userStore, dk[userId] || {},
);
// update the session store
const storage = {};
for (deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}
Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage();
}
self._sessionStore.storeEndToEndDevicesForUser(
});
this._sessionStore.storeEndToEndDevicesForUser(
userId, storage,
);
);
deferMap[userId].resolve();
if (token) {
this._sessionStore.storeEndToEndDeviceSyncToken(token);
}
}
}, function(err) {
downloadUsers.map(function(u) {
deferMap[u].reject(err);
});
});
return promiseMap;
}
}
+149 -45
View File
@@ -48,14 +48,16 @@ const DeviceList = require('./DeviceList').default;
* @param {string} userId The user ID for the local user
*
* @param {string} deviceId The identifier for this device.
*
* @param {Object} clientStore the MatrixClient data store.
*/
function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId,
clientStore) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._userId = userId;
this._deviceId = deviceId;
this._initialSyncCompleted = false;
this._clientStore = clientStore;
this._olmDevice = new OlmDevice(sessionStore);
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
@@ -111,11 +113,8 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
function _registerEventHandlers(crypto, eventEmitter) {
eventEmitter.on("sync", function(syncState, oldState, data) {
try {
if (syncState == "PREPARED") {
// XXX ugh. we're assuming the eventEmitter is a MatrixClient.
// how can we avoid doing so?
const rooms = eventEmitter.getRooms();
crypto._onInitialSyncCompleted(rooms);
if (syncState === "SYNCING") {
crypto._onSyncCompleted(data);
}
} catch (e) {
console.error("Error handling sync", e);
@@ -387,6 +386,8 @@ Crypto.prototype.listDeviceKeys = function(userId) {
*
* @param {?boolean} known whether to mark that the user has been made aware of
* the existence of this device. Null to leave unchanged
*
* @return {module:crypto/deviceinfo} updated DeviceInfo
*/
Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified,
blocked, known) {
@@ -415,12 +416,12 @@ Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified,
knownStatus = known;
}
if (dev.verified === verificationStatus && dev.known === knownStatus) {
return;
if (dev.verified !== verificationStatus || dev.known !== knownStatus) {
dev.verified = verificationStatus;
dev.known = knownStatus;
this._sessionStore.storeEndToEndDevicesForUser(userId, devices);
}
dev.verified = verificationStatus;
dev.known = knownStatus;
this._sessionStore.storeEndToEndDevicesForUser(userId, devices);
return DeviceInfo.fromStorage(dev, deviceId);
};
@@ -710,6 +711,18 @@ Crypto.prototype.decryptEvent = function(event) {
alg.decryptEvent(event);
};
/**
* Handle the notification from /sync that a user has updated their device list.
*
* @param {String} userId
*/
Crypto.prototype.userDeviceListChanged = function(userId) {
this._deviceList.invalidateUserDeviceList(userId);
// don't flush the outdated device list yet - we do it once we finish
// processing the sync.
};
/**
* handle an m.room.encryption event
*
@@ -729,19 +742,50 @@ Crypto.prototype._onCryptoEvent = function(event) {
};
/**
* handle the completion of the initial sync.
* handle the completion of a /sync
*
* Announces the new device.
* This is called after the processing of each successful /sync response.
* It is an opportunity to do a batch process on the information received.
*
* @param {Object} syncData the data from the 'MatrixClient.sync' event
*/
Crypto.prototype._onSyncCompleted = function(syncData) {
this._deviceList.lastKnownSyncToken = syncData.nextSyncToken;
if (!syncData.oldSyncToken) {
// an initialsync.
this._sendNewDeviceEvents();
// if we have a deviceSyncToken, we can tell the deviceList to
// invalidate devices which have changed since then.
const oldSyncToken = this._sessionStore.getEndToEndDeviceSyncToken();
if (oldSyncToken) {
this._invalidateDeviceListsSince(oldSyncToken).catch((e) => {
// if that failed, we fall back to invalidating everyone.
console.warn("Error fetching changed device list", e);
this._invalidateDeviceListForAllActiveUsers();
return this._deviceList.refreshOutdatedDeviceLists();
}).done();
} else {
// otherwise, we have to invalidate all devices for all users we
// share a room with.
this._invalidateDeviceListForAllActiveUsers();
}
}
// catch up on any new devices we got told about during the sync.
this._deviceList.refreshOutdatedDeviceLists().done();
};
/**
* Send m.new_device messages to any devices we share a room with.
*
* (TODO: we can get rid of this once a suitable number of homeservers and
* clients support the more reliable device list update stream mechanism)
*
* @private
* @param {module:models/room[]} rooms list of rooms the client knows about
*/
Crypto.prototype._onInitialSyncCompleted = function(rooms) {
this._initialSyncCompleted = true;
// catch up on any m.new_device events which arrived during the initial sync.
this._deviceList.flushNewDeviceRequests();
Crypto.prototype._sendNewDeviceEvents = function() {
if (this._sessionStore.getDeviceAnnounced()) {
return;
}
@@ -750,23 +794,7 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
// we have arrived.
// build a list of rooms for each user.
const roomsByUser = {};
for (let i = 0; i < rooms.length; i++) {
const room = rooms[i];
// check for rooms with encryption enabled
const alg = this._roomEncryptors[room.roomId];
if (!alg) {
continue;
}
// ignore any rooms which we have left
const me = room.getMember(this._userId);
if (!me || (
me.membership !== "join" && me.membership !== "invite"
)) {
continue;
}
for (const room of this._getE2eRooms()) {
const members = room.getJoinedMembers();
for (let j = 0; j < members.length; j++) {
const m = members[j];
@@ -800,6 +828,88 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
});
};
/**
* Ask the server which users have new devices since a given token,
* invalidate them, and start an update query.
*
* @param {String} oldSyncToken
*
* @returns {Promise} resolves once the query is complete. Rejects if the
* keyChange query fails.
*/
Crypto.prototype._invalidateDeviceListsSince = function(oldSyncToken) {
return this._baseApis.getKeyChanges(
oldSyncToken, this.lastKnownSyncToken,
).then((r) => {
if (!r.changed || !Array.isArray(r.changed)) {
return;
}
// only invalidate users we share an e2e room with - we don't
// care about users in non-e2e rooms.
const filteredUserIds = this._getE2eRoomMembers();
r.changed.forEach((u) => {
if (u in filteredUserIds) {
this._deviceList.invalidateUserDeviceList(u);
}
});
return this._deviceList.refreshOutdatedDeviceLists();
});
};
/**
* Invalidate any stored device list for any users we share an e2e room with
*
* @private
*/
Crypto.prototype._invalidateDeviceListForAllActiveUsers = function() {
Object.keys(this._getE2eRoomMembers()).forEach((m) => {
this._deviceList.invalidateUserDeviceList(m);
});
};
/**
* get the users we share an e2e-enabled room with
*
* @returns {Object<string>} userid->userid map (should be a Set but argh ES6)
*/
Crypto.prototype._getE2eRoomMembers = function() {
const userIds = Object.create(null);
const rooms = this._getE2eRooms();
for (const r of rooms) {
const members = r.getJoinedMembers();
members.forEach((m) => { userIds[m.userId] = m.userId; });
}
return userIds;
};
/**
* Get a list of the e2e-enabled rooms we are members of
*
* @returns {module:models.Room[]}
*/
Crypto.prototype._getE2eRooms = function() {
return this._clientStore.getRooms().filter((room) => {
// check for rooms with encryption enabled
const alg = this._roomEncryptors[room.roomId];
if (!alg) {
return false;
}
// ignore any rooms which we have left
const me = room.getMember(this._userId);
if (!me || (
me.membership !== "join" && me.membership !== "invite"
)) {
return false;
}
return true;
});
};
/**
* Handle a key event
*
@@ -873,12 +983,6 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
}
this._deviceList.invalidateUserDeviceList(userId);
// we delay handling these until the intialsync has completed, so that we
// can do all of them together.
if (this._initialSyncCompleted) {
this._deviceList.flushNewDeviceRequests();
}
};
+24
View File
@@ -99,6 +99,27 @@ WebStorageSessionStore.prototype = {
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
},
/**
* Store the sync token corresponding to the device list.
*
* This is used when starting the client, to get a list of the users who
* have changed their device list since the list time we were running.
*
* @param {String?} token
*/
storeEndToEndDeviceSyncToken: function(token) {
setJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN, token);
},
/**
* Get the sync token corresponding to the device list.
*
* @return {String?} token
*/
getEndToEndDeviceSyncToken: function() {
return getJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN);
},
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
@@ -180,6 +201,7 @@ WebStorageSessionStore.prototype = {
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
@@ -199,6 +221,8 @@ function keyEndToEndRoom(roomId) {
function getJsonItem(store, key) {
try {
// if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key));
} catch (e) {
debuglog("Failed to get key %s: %s", key, e);
+16 -2
View File
@@ -58,6 +58,7 @@ function debuglog() {
* @constructor
* @param {MatrixClient} client The matrix client instance to use.
* @param {Object} opts Config options
* @param {module:crypto=} opts.crypto Crypto manager
*/
function SyncApi(client, opts) {
this.client = client;
@@ -529,13 +530,18 @@ SyncApi.prototype._sync = function(syncOptions) {
}
// emit synced events
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
};
if (!syncOptions.hasSyncedBefore) {
self._updateSyncState("PREPARED");
self._updateSyncState("PREPARED", syncEventData);
syncOptions.hasSyncedBefore = true;
}
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
self._updateSyncState("SYNCING");
self._updateSyncState("SYNCING", syncEventData);
self._sync(syncOptions);
}, function(err) {
@@ -584,6 +590,7 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// next_batch: $token,
// presence: { events: [] },
// account_data: { events: [] },
// device_lists: { changed: ["@user:server", ... ]},
// to_device: { events: [] },
// rooms: {
// invite: {
@@ -859,6 +866,13 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
client.getNotifTimelineSet().addLiveEvent(event);
});
}
// Handle device list updates
if (this.opts.crypto && data.device_lists && data.device_lists.changed) {
data.device_lists.changed.forEach((u) => {
this.opts.crypto.userDeviceListChanged(u);
});
}
};
/**