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 branch 'develop' into matthew/blacklist-unverified
This commit is contained in:
18
package.json
18
package.json
@@ -9,9 +9,9 @@
|
||||
"check": "npm run buildtest && jasmine-node specbuild --verbose --junitreport --captureExceptions",
|
||||
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
|
||||
"start": "babel -s -w -d lib src",
|
||||
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify --exclude olm browser-index.js -o dist/browser-matrix.js --ignore-missing && uglifyjs -c -m -o dist/browser-matrix.min.js dist/browser-matrix.js",
|
||||
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
|
||||
"dist": "npm run build",
|
||||
"watch": "watchify --exclude olm browser-index.js -o dist/browser-matrix-dev.js -v",
|
||||
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
|
||||
"lint": "eslint --max-warnings 121 src spec",
|
||||
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
@@ -47,7 +47,6 @@
|
||||
"dependencies": {
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"browserify": "^10.2.3",
|
||||
"q": "^1.4.1",
|
||||
"request": "^2.53.0"
|
||||
},
|
||||
@@ -55,16 +54,29 @@
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-preset-es2015": "^6.18.0",
|
||||
"browserify": "^14.0.0",
|
||||
"browserify-shim": "^3.8.13",
|
||||
"eslint": "^3.13.1",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"exorcist": "^0.4.0",
|
||||
"istanbul": "^0.3.13",
|
||||
"jasmine-node": "^1.14.5",
|
||||
"jsdoc": "^3.4.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"sourceify": "^0.1.0",
|
||||
"uglifyjs": "^2.4.10",
|
||||
"watchify": "^3.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-2.2.1.tgz"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"sourceify",
|
||||
"browserify-shim"
|
||||
]
|
||||
},
|
||||
"browserify-shim": {
|
||||
"olm": "global:Olm"
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -21,6 +21,11 @@ limitations under the License.
|
||||
* @module crypto/OlmDevice
|
||||
*/
|
||||
const Olm = require("olm");
|
||||
if (!Olm) {
|
||||
// this happens if we were loaded via browserify and the Olm module was not
|
||||
// loaded.
|
||||
throw new Error("Olm is not defined");
|
||||
}
|
||||
const utils = require("../utils");
|
||||
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ const olmlib = require("./olmlib");
|
||||
const algorithms = require("./algorithms");
|
||||
const DeviceInfo = require("./deviceinfo");
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
const DeviceList = require('./DeviceList').default;
|
||||
|
||||
/**
|
||||
* Cryptography bits
|
||||
@@ -55,12 +56,9 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
||||
this._deviceId = deviceId;
|
||||
|
||||
this._initialSyncCompleted = false;
|
||||
// userId -> true
|
||||
this._pendingUsersWithNewDevices = {};
|
||||
// userId -> [promise, ...]
|
||||
this._keyDownloadsInProgressByUser = {};
|
||||
|
||||
this._olmDevice = new OlmDevice(sessionStore);
|
||||
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
|
||||
|
||||
// EncryptionAlgorithm instance for each room
|
||||
this._roomEncryptors = {};
|
||||
@@ -88,7 +86,7 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
||||
if (!myDevices) {
|
||||
// we don't yet have a list of our own devices; make sure we
|
||||
// get one when we flush the pendingUsersWithNewDevices.
|
||||
this._pendingUsersWithNewDevices[this._userId] = true;
|
||||
this._deviceList.invalidateUserDeviceList(this._userId);
|
||||
myDevices = {};
|
||||
}
|
||||
|
||||
@@ -304,267 +302,9 @@ function _uploadOneTimeKeys(crypto) {
|
||||
* module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
Crypto.prototype.downloadKeys = function(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);
|
||||
});
|
||||
return this._deviceList.downloadKeys(userIds, forceDownload);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
@@ -574,17 +314,7 @@ function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||
* managed to get a list of devices for this user yet.
|
||||
*/
|
||||
Crypto.prototype.getStoredDevicesForUser = function(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;
|
||||
return this._deviceList.getStoredDevicesForUser(userId);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -593,15 +323,11 @@ Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
||||
* @param {string} userId
|
||||
* @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
|
||||
*/
|
||||
Crypto.prototype.getStoredDevice = function(userId, deviceId) {
|
||||
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (!devs || !devs[deviceId]) {
|
||||
return undefined;
|
||||
}
|
||||
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||
return this._deviceList.getStoredDevice(userId, deviceId);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -647,54 +373,6 @@ Crypto.prototype.listDeviceKeys = function(userId) {
|
||||
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
|
||||
*
|
||||
@@ -796,7 +474,7 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
// was sent from. In the case of Megolm, it's actually the Curve25519
|
||||
// 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,
|
||||
);
|
||||
|
||||
@@ -854,10 +532,6 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
||||
throw new Error("Unable to encrypt with " + config.algorithm);
|
||||
}
|
||||
|
||||
// remove spurious keys
|
||||
config = {
|
||||
algorithm: config.algorithm,
|
||||
};
|
||||
this._sessionStore.storeEndToEndRoom(roomId, config);
|
||||
|
||||
const alg = new AlgClass({
|
||||
@@ -1066,7 +740,7 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
|
||||
this._initialSyncCompleted = true;
|
||||
|
||||
// catch up on any m.new_device events which arrived during the initial sync.
|
||||
this._flushNewDeviceRequests();
|
||||
this._deviceList.flushNewDeviceRequests();
|
||||
|
||||
if (this._sessionStore.getDeviceAnnounced()) {
|
||||
return;
|
||||
@@ -1198,49 +872,15 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendingUsersWithNewDevices[userId] = true;
|
||||
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._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.
|
||||
|
||||
Reference in New Issue
Block a user