You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-12-01 04:43:29 +03:00
Merge pull request #346 from matrix-org/rav/factor_out_devicelist
Factor out device list management
This commit is contained in:
446
src/crypto/DeviceList.js
Normal file
446
src/crypto/DeviceList.js
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @module crypto/DeviceList
|
||||||
|
*
|
||||||
|
* Manages the list of other users' devices
|
||||||
|
*/
|
||||||
|
|
||||||
|
import q from 'q';
|
||||||
|
|
||||||
|
import DeviceInfo from './deviceinfo';
|
||||||
|
import olmlib from './olmlib';
|
||||||
|
import utils from '../utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alias module:crypto/DeviceList
|
||||||
|
*/
|
||||||
|
export default class DeviceList {
|
||||||
|
constructor(baseApis, sessionStore, olmDevice) {
|
||||||
|
this._baseApis = baseApis;
|
||||||
|
this._sessionStore = sessionStore;
|
||||||
|
this._olmDevice = olmDevice;
|
||||||
|
|
||||||
|
// userId -> true
|
||||||
|
this._pendingUsersWithNewDevices = {};
|
||||||
|
|
||||||
|
// userId -> [promise, ...]
|
||||||
|
this._keyDownloadsInProgressByUser = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the keys for a list of users and stores the keys in the session
|
||||||
|
* store.
|
||||||
|
* @param {Array} userIds The users to fetch.
|
||||||
|
* @param {bool} forceDownload Always download the keys even if cached.
|
||||||
|
*
|
||||||
|
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stored device keys for a list of user ids
|
||||||
|
*
|
||||||
|
* @param {string[]} userIds the list of users to list keys for.
|
||||||
|
*
|
||||||
|
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
|
||||||
|
*/
|
||||||
|
_getDevicesFromStore(userIds) {
|
||||||
|
const stored = {};
|
||||||
|
const self = this;
|
||||||
|
userIds.map(function(u) {
|
||||||
|
stored[u] = {};
|
||||||
|
const devices = self.getStoredDevicesForUser(u) || [];
|
||||||
|
devices.map(function(dev) {
|
||||||
|
stored[u][dev.deviceId] = dev;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stored device keys for a user id
|
||||||
|
*
|
||||||
|
* @param {string} userId the user to list keys for.
|
||||||
|
*
|
||||||
|
* @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't
|
||||||
|
* managed to get a list of devices for this user yet.
|
||||||
|
*/
|
||||||
|
getStoredDevicesForUser(userId) {
|
||||||
|
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
|
if (!devs) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const res = [];
|
||||||
|
for (const deviceId in devs) {
|
||||||
|
if (devs.hasOwnProperty(deviceId)) {
|
||||||
|
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stored keys for a single device
|
||||||
|
*
|
||||||
|
* @param {string} userId
|
||||||
|
* @param {string} deviceId
|
||||||
|
*
|
||||||
|
* @return {module:crypto/deviceinfo?} device, or undefined
|
||||||
|
* if we don't know about this device
|
||||||
|
*/
|
||||||
|
getStoredDevice(userId, deviceId) {
|
||||||
|
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
|
if (!devs || !devs[deviceId]) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a device by curve25519 identity key
|
||||||
|
*
|
||||||
|
* @param {string} userId owner of the device
|
||||||
|
* @param {string} algorithm encryption algorithm
|
||||||
|
* @param {string} sender_key curve25519 key to match
|
||||||
|
*
|
||||||
|
* @return {module:crypto/deviceinfo?}
|
||||||
|
*/
|
||||||
|
getDeviceByIdentityKey(userId, algorithm, sender_key) {
|
||||||
|
if (
|
||||||
|
algorithm !== olmlib.OLM_ALGORITHM &&
|
||||||
|
algorithm !== olmlib.MEGOLM_ALGORITHM
|
||||||
|
) {
|
||||||
|
// we only deal in olm keys
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
|
if (!devices) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const deviceId in devices) {
|
||||||
|
if (!devices.hasOwnProperty(deviceId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = devices[deviceId];
|
||||||
|
for (const keyId in device.keys) {
|
||||||
|
if (!device.keys.hasOwnProperty(keyId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (keyId.indexOf("curve25519:") !== 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const deviceKey = device.keys[keyId];
|
||||||
|
if (deviceKey == sender_key) {
|
||||||
|
return DeviceInfo.fromStorage(device, deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't match a known device
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param {String} userId
|
||||||
|
*/
|
||||||
|
invalidateUserDeviceList(userId) {
|
||||||
|
this._pendingUsersWithNewDevices[userId] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start device queries for any users who sent us an m.new_device recently
|
||||||
|
*/
|
||||||
|
flushNewDeviceRequests() {
|
||||||
|
const users = Object.keys(this._pendingUsersWithNewDevices);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const r = this._doKeyDownloadForUsers(users);
|
||||||
|
|
||||||
|
// we've kicked off requests to these users: remove their
|
||||||
|
// pending flag for now.
|
||||||
|
this._pendingUsersWithNewDevices = {};
|
||||||
|
|
||||||
|
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.
|
||||||
|
//
|
||||||
|
this._pendingUsersWithNewDevices[u] = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
q.all(Object.values(r)).done();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} downloadUsers list of userIds
|
||||||
|
*
|
||||||
|
* @return {Object} a map from userId to a promise for a result for that user
|
||||||
|
*/
|
||||||
|
_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 dk = res.device_keys || {};
|
||||||
|
|
||||||
|
for (let i = 0; i < downloadUsers.length; ++i) {
|
||||||
|
const userId = downloadUsers[i];
|
||||||
|
var deviceId;
|
||||||
|
|
||||||
|
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);
|
||||||
|
if (devs) {
|
||||||
|
for (deviceId in devs) {
|
||||||
|
if (devs.hasOwnProperty(deviceId)) {
|
||||||
|
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||||
|
userStore[deviceId] = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateStoredDeviceKeysForUser(
|
||||||
|
self._olmDevice, userId, userStore, dk[userId],
|
||||||
|
);
|
||||||
|
|
||||||
|
// update the session store
|
||||||
|
const storage = {};
|
||||||
|
for (deviceId in userStore) {
|
||||||
|
if (!userStore.hasOwnProperty(deviceId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage[deviceId] = userStore[deviceId].toStorage();
|
||||||
|
}
|
||||||
|
self._sessionStore.storeEndToEndDevicesForUser(
|
||||||
|
userId, storage,
|
||||||
|
);
|
||||||
|
|
||||||
|
deferMap[userId].resolve();
|
||||||
|
}
|
||||||
|
}, function(err) {
|
||||||
|
downloadUsers.map(function(u) {
|
||||||
|
deferMap[u].reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promiseMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||||
|
userResult) {
|
||||||
|
let updated = false;
|
||||||
|
|
||||||
|
// remove any devices in the store which aren't in the response
|
||||||
|
for (var deviceId in userStore) {
|
||||||
|
if (!userStore.hasOwnProperty(deviceId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(deviceId in userResult)) {
|
||||||
|
console.log("Device " + userId + ":" + deviceId +
|
||||||
|
" has been removed");
|
||||||
|
delete userStore[deviceId];
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (deviceId in userResult) {
|
||||||
|
if (!userResult.hasOwnProperty(deviceId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceResult = userResult[deviceId];
|
||||||
|
|
||||||
|
// check that the user_id and device_id in the response object are
|
||||||
|
// correct
|
||||||
|
if (deviceResult.user_id !== userId) {
|
||||||
|
console.warn("Mismatched user_id " + deviceResult.user_id +
|
||||||
|
" in keys from " + userId + ":" + deviceId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (deviceResult.device_id !== deviceId) {
|
||||||
|
console.warn("Mismatched device_id " + deviceResult.device_id +
|
||||||
|
" in keys from " + userId + ":" + deviceId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_storeDeviceKeys(_olmDevice, userStore, deviceResult)) {
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Process a device in a /query response, and add it to the userStore
|
||||||
|
*
|
||||||
|
* returns true if a change was made, else false
|
||||||
|
*/
|
||||||
|
function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||||
|
if (!deviceResult.keys) {
|
||||||
|
// no keys?
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviceId = deviceResult.device_id;
|
||||||
|
const userId = deviceResult.user_id;
|
||||||
|
|
||||||
|
const signKeyId = "ed25519:" + deviceId;
|
||||||
|
const signKey = deviceResult.keys[signKeyId];
|
||||||
|
if (!signKey) {
|
||||||
|
console.log("Device " + userId + ":" + deviceId +
|
||||||
|
" has no ed25519 key");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsigned = deviceResult.unsigned || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Unable to verify signature on device " +
|
||||||
|
userId + ":" + deviceId + ":", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceInfo
|
||||||
|
let deviceStore;
|
||||||
|
|
||||||
|
if (deviceId in userStore) {
|
||||||
|
// already have this device.
|
||||||
|
deviceStore = userStore[deviceId];
|
||||||
|
|
||||||
|
if (deviceStore.getFingerprint() != signKey) {
|
||||||
|
// this should only happen if the list has been MITMed; we are
|
||||||
|
// best off sticking with the original keys.
|
||||||
|
//
|
||||||
|
// Should we warn the user about it somehow?
|
||||||
|
console.warn("Ed25519 key for device" + userId + ": " +
|
||||||
|
deviceId + " has changed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceStore.keys = deviceResult.keys || {};
|
||||||
|
deviceStore.algorithms = deviceResult.algorithms || [];
|
||||||
|
deviceStore.unsigned = unsigned;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ const olmlib = require("./olmlib");
|
|||||||
const algorithms = require("./algorithms");
|
const algorithms = require("./algorithms");
|
||||||
const DeviceInfo = require("./deviceinfo");
|
const DeviceInfo = require("./deviceinfo");
|
||||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||||
|
const DeviceList = require('./DeviceList').default;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cryptography bits
|
* Cryptography bits
|
||||||
@@ -55,12 +56,9 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
|||||||
this._deviceId = deviceId;
|
this._deviceId = deviceId;
|
||||||
|
|
||||||
this._initialSyncCompleted = false;
|
this._initialSyncCompleted = false;
|
||||||
// userId -> true
|
|
||||||
this._pendingUsersWithNewDevices = {};
|
|
||||||
// userId -> [promise, ...]
|
|
||||||
this._keyDownloadsInProgressByUser = {};
|
|
||||||
|
|
||||||
this._olmDevice = new OlmDevice(sessionStore);
|
this._olmDevice = new OlmDevice(sessionStore);
|
||||||
|
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
|
||||||
|
|
||||||
// EncryptionAlgorithm instance for each room
|
// EncryptionAlgorithm instance for each room
|
||||||
this._roomEncryptors = {};
|
this._roomEncryptors = {};
|
||||||
@@ -86,7 +84,7 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
|||||||
if (!myDevices) {
|
if (!myDevices) {
|
||||||
// we don't yet have a list of our own devices; make sure we
|
// we don't yet have a list of our own devices; make sure we
|
||||||
// get one when we flush the pendingUsersWithNewDevices.
|
// get one when we flush the pendingUsersWithNewDevices.
|
||||||
this._pendingUsersWithNewDevices[this._userId] = true;
|
this._deviceList.invalidateUserDeviceList(this._userId);
|
||||||
myDevices = {};
|
myDevices = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,267 +281,9 @@ function _uploadOneTimeKeys(crypto) {
|
|||||||
* module:crypto/deviceinfo|DeviceInfo}.
|
* module:crypto/deviceinfo|DeviceInfo}.
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||||
const self = this;
|
return this._deviceList.downloadKeys(userIds, forceDownload);
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the stored device keys for a list of user ids
|
|
||||||
*
|
|
||||||
* @param {string[]} userIds the list of users to list keys for.
|
|
||||||
*
|
|
||||||
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
|
|
||||||
*/
|
|
||||||
Crypto.prototype._getDevicesFromStore = function(userIds) {
|
|
||||||
const stored = {};
|
|
||||||
const self = this;
|
|
||||||
userIds.map(function(u) {
|
|
||||||
stored[u] = {};
|
|
||||||
const devices = self.getStoredDevicesForUser(u) || [];
|
|
||||||
devices.map(function(dev) {
|
|
||||||
stored[u][dev.deviceId] = dev;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return stored;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string[]} downloadUsers list of userIds
|
|
||||||
*
|
|
||||||
* @return {Object} a map from userId to a promise for a result for that user
|
|
||||||
*/
|
|
||||||
Crypto.prototype._doKeyDownloadForUsers = function(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 dk = res.device_keys || {};
|
|
||||||
|
|
||||||
for (let i = 0; i < downloadUsers.length; ++i) {
|
|
||||||
const userId = downloadUsers[i];
|
|
||||||
var deviceId;
|
|
||||||
|
|
||||||
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);
|
|
||||||
if (devs) {
|
|
||||||
for (deviceId in devs) {
|
|
||||||
if (devs.hasOwnProperty(deviceId)) {
|
|
||||||
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
|
||||||
userStore[deviceId] = d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateStoredDeviceKeysForUser(
|
|
||||||
self._olmDevice, userId, userStore, dk[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
// update the session store
|
|
||||||
const storage = {};
|
|
||||||
for (deviceId in userStore) {
|
|
||||||
if (!userStore.hasOwnProperty(deviceId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
storage[deviceId] = userStore[deviceId].toStorage();
|
|
||||||
}
|
|
||||||
self._sessionStore.storeEndToEndDevicesForUser(
|
|
||||||
userId, storage,
|
|
||||||
);
|
|
||||||
|
|
||||||
deferMap[userId].resolve();
|
|
||||||
}
|
|
||||||
}, function(err) {
|
|
||||||
downloadUsers.map(function(u) {
|
|
||||||
deferMap[u].reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return promiseMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
|
||||||
userResult) {
|
|
||||||
let updated = false;
|
|
||||||
|
|
||||||
// remove any devices in the store which aren't in the response
|
|
||||||
for (var deviceId in userStore) {
|
|
||||||
if (!userStore.hasOwnProperty(deviceId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(deviceId in userResult)) {
|
|
||||||
console.log("Device " + userId + ":" + deviceId +
|
|
||||||
" has been removed");
|
|
||||||
delete userStore[deviceId];
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (deviceId in userResult) {
|
|
||||||
if (!userResult.hasOwnProperty(deviceId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deviceResult = userResult[deviceId];
|
|
||||||
|
|
||||||
// check that the user_id and device_id in the response object are
|
|
||||||
// correct
|
|
||||||
if (deviceResult.user_id !== userId) {
|
|
||||||
console.warn("Mismatched user_id " + deviceResult.user_id +
|
|
||||||
" in keys from " + userId + ":" + deviceId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (deviceResult.device_id !== deviceId) {
|
|
||||||
console.warn("Mismatched device_id " + deviceResult.device_id +
|
|
||||||
" in keys from " + userId + ":" + deviceId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_storeDeviceKeys(_olmDevice, userStore, deviceResult)) {
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Process a device in a /query response, and add it to the userStore
|
|
||||||
*
|
|
||||||
* returns true if a change was made, else false
|
|
||||||
*/
|
|
||||||
function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
|
||||||
if (!deviceResult.keys) {
|
|
||||||
// no keys?
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deviceId = deviceResult.device_id;
|
|
||||||
const userId = deviceResult.user_id;
|
|
||||||
|
|
||||||
const signKeyId = "ed25519:" + deviceId;
|
|
||||||
const signKey = deviceResult.keys[signKeyId];
|
|
||||||
if (!signKey) {
|
|
||||||
console.log("Device " + userId + ":" + deviceId +
|
|
||||||
" has no ed25519 key");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unsigned = deviceResult.unsigned || {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Unable to verify signature on device " +
|
|
||||||
userId + ":" + deviceId + ":", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeviceInfo
|
|
||||||
let deviceStore;
|
|
||||||
|
|
||||||
if (deviceId in userStore) {
|
|
||||||
// already have this device.
|
|
||||||
deviceStore = userStore[deviceId];
|
|
||||||
|
|
||||||
if (deviceStore.getFingerprint() != signKey) {
|
|
||||||
// this should only happen if the list has been MITMed; we are
|
|
||||||
// best off sticking with the original keys.
|
|
||||||
//
|
|
||||||
// Should we warn the user about it somehow?
|
|
||||||
console.warn("Ed25519 key for device" + userId + ": " +
|
|
||||||
deviceId + " has changed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceStore.keys = deviceResult.keys || {};
|
|
||||||
deviceStore.algorithms = deviceResult.algorithms || [];
|
|
||||||
deviceStore.unsigned = unsigned;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the stored device keys for a user id
|
* Get the stored device keys for a user id
|
||||||
*
|
*
|
||||||
@@ -553,17 +293,7 @@ function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
|||||||
* managed to get a list of devices for this user yet.
|
* managed to get a list of devices for this user yet.
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
||||||
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
return this._deviceList.getStoredDevicesForUser(userId);
|
||||||
if (!devs) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const res = [];
|
|
||||||
for (const deviceId in devs) {
|
|
||||||
if (devs.hasOwnProperty(deviceId)) {
|
|
||||||
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -572,15 +302,11 @@ Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
|||||||
* @param {string} userId
|
* @param {string} userId
|
||||||
* @param {string} deviceId
|
* @param {string} deviceId
|
||||||
*
|
*
|
||||||
* @return {module:crypto/deviceinfo?} list of devices, or undefined
|
* @return {module:crypto/deviceinfo?} device, or undefined
|
||||||
* if we don't know about this device
|
* if we don't know about this device
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.getStoredDevice = function(userId, deviceId) {
|
Crypto.prototype.getStoredDevice = function(userId, deviceId) {
|
||||||
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
return this._deviceList.getStoredDevice(userId, deviceId);
|
||||||
if (!devs || !devs[deviceId]) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -626,54 +352,6 @@ Crypto.prototype.listDeviceKeys = function(userId) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a device by curve25519 identity key
|
|
||||||
*
|
|
||||||
* @param {string} userId owner of the device
|
|
||||||
* @param {string} algorithm encryption algorithm
|
|
||||||
* @param {string} sender_key curve25519 key to match
|
|
||||||
*
|
|
||||||
* @return {module:crypto/deviceinfo?}
|
|
||||||
*/
|
|
||||||
Crypto.prototype.getDeviceByIdentityKey = function(userId, algorithm, sender_key) {
|
|
||||||
if (
|
|
||||||
algorithm !== olmlib.OLM_ALGORITHM &&
|
|
||||||
algorithm !== olmlib.MEGOLM_ALGORITHM
|
|
||||||
) {
|
|
||||||
// we only deal in olm keys
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
|
||||||
if (!devices) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const deviceId in devices) {
|
|
||||||
if (!devices.hasOwnProperty(deviceId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const device = devices[deviceId];
|
|
||||||
for (const keyId in device.keys) {
|
|
||||||
if (!device.keys.hasOwnProperty(keyId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (keyId.indexOf("curve25519:") !== 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const deviceKey = device.keys[keyId];
|
|
||||||
if (deviceKey == sender_key) {
|
|
||||||
return DeviceInfo.fromStorage(device, deviceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// doesn't match a known device
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the blocked/verified state of the given device
|
* Update the blocked/verified state of the given device
|
||||||
*
|
*
|
||||||
@@ -775,7 +453,7 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
|||||||
// was sent from. In the case of Megolm, it's actually the Curve25519
|
// was sent from. In the case of Megolm, it's actually the Curve25519
|
||||||
// identity key of the device which set up the Megolm session.
|
// identity key of the device which set up the Megolm session.
|
||||||
|
|
||||||
const device = this.getDeviceByIdentityKey(
|
const device = this._deviceList.getDeviceByIdentityKey(
|
||||||
event.getSender(), algorithm, sender_key,
|
event.getSender(), algorithm, sender_key,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1041,7 +719,7 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
|
|||||||
this._initialSyncCompleted = true;
|
this._initialSyncCompleted = true;
|
||||||
|
|
||||||
// catch up on any m.new_device events which arrived during the initial sync.
|
// catch up on any m.new_device events which arrived during the initial sync.
|
||||||
this._flushNewDeviceRequests();
|
this._deviceList.flushNewDeviceRequests();
|
||||||
|
|
||||||
if (this._sessionStore.getDeviceAnnounced()) {
|
if (this._sessionStore.getDeviceAnnounced()) {
|
||||||
return;
|
return;
|
||||||
@@ -1173,49 +851,15 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._pendingUsersWithNewDevices[userId] = true;
|
this._deviceList.invalidateUserDeviceList(userId);
|
||||||
|
|
||||||
// we delay handling these until the intialsync has completed, so that we
|
// we delay handling these until the intialsync has completed, so that we
|
||||||
// can do all of them together.
|
// can do all of them together.
|
||||||
if (this._initialSyncCompleted) {
|
if (this._initialSyncCompleted) {
|
||||||
this._flushNewDeviceRequests();
|
this._deviceList.flushNewDeviceRequests();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Start device queries for any users who sent us an m.new_device recently
|
|
||||||
*/
|
|
||||||
Crypto.prototype._flushNewDeviceRequests = function() {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const users = utils.keys(this._pendingUsersWithNewDevices);
|
|
||||||
|
|
||||||
if (users.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const r = this._doKeyDownloadForUsers(users);
|
|
||||||
|
|
||||||
// we've kicked off requests to these users: remove their
|
|
||||||
// pending flag for now.
|
|
||||||
this._pendingUsersWithNewDevices = {};
|
|
||||||
|
|
||||||
users.map(function(u) {
|
|
||||||
r[u] = r[u].catch(function(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.
|
|
||||||
//
|
|
||||||
self._pendingUsersWithNewDevices[u] = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
q.all(utils.values(r)).done();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a decryptor for a given room and algorithm.
|
* Get a decryptor for a given room and algorithm.
|
||||||
|
|||||||
Reference in New Issue
Block a user