You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-29 16:43:09 +03:00
Factor crypto stuff out of MatrixClient
Introduce a new Crypto class which encapsulates all of the the crypto-related gubbins, replacing it with thin wrappers in MatrixClient.
This commit is contained in:
@@ -705,7 +705,7 @@ MatrixBaseApis.prototype.search = function(opts, callback) {
|
|||||||
* @param {Object} content body of upload request
|
* @param {Object} content body of upload request
|
||||||
*
|
*
|
||||||
* @param {Object=} opts
|
* @param {Object=} opts
|
||||||
|
*
|
||||||
* @param {string=} opts.device_id explicit device_id to use for upload
|
* @param {string=} opts.device_id explicit device_id to use for upload
|
||||||
* (default is to use the same as that used during auth).
|
* (default is to use the same as that used during auth).
|
||||||
*
|
*
|
||||||
@@ -730,6 +730,56 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download device keys
|
||||||
|
*
|
||||||
|
* @param {string[]} userIds list of users to get keys for
|
||||||
|
*
|
||||||
|
* @param {module:client.callback=} callback
|
||||||
|
*
|
||||||
|
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||||
|
* an error response ({@link module:http-api.MatrixError}).
|
||||||
|
*/
|
||||||
|
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
|
||||||
|
var downloadQuery = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < userIds.length; ++i) {
|
||||||
|
downloadQuery[userIds[i]] = {};
|
||||||
|
}
|
||||||
|
var content = {device_keys: downloadQuery};
|
||||||
|
return this._http.authedRequestWithPrefix(
|
||||||
|
callback, "POST", "/keys/query", undefined, content,
|
||||||
|
httpApi.PREFIX_UNSTABLE
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim one-time keys
|
||||||
|
*
|
||||||
|
* @param {string[][]} devices a list of [userId, deviceId] pairs
|
||||||
|
*
|
||||||
|
* @param {module:client.callback=} callback
|
||||||
|
*
|
||||||
|
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||||
|
* an error response ({@link module:http-api.MatrixError}).
|
||||||
|
*/
|
||||||
|
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, callback) {
|
||||||
|
var queries = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < devices.length; ++i) {
|
||||||
|
var userId = devices[i][0];
|
||||||
|
var deviceId = devices[i][1];
|
||||||
|
var query = queries[userId] || {};
|
||||||
|
queries[userId] = query;
|
||||||
|
query[deviceId] = "curve25519";
|
||||||
|
}
|
||||||
|
var content = {one_time_keys: queries};
|
||||||
|
return this._http.authedRequestWithPrefix(
|
||||||
|
callback, "POST", "/keys/claim", undefined, content,
|
||||||
|
httpApi.PREFIX_UNSTABLE
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// Identity Server Operations
|
// Identity Server Operations
|
||||||
// ==========================
|
// ==========================
|
||||||
|
|||||||
774
lib/client.js
774
lib/client.js
@@ -24,7 +24,6 @@ var PushProcessor = require('./pushprocessor');
|
|||||||
var EventEmitter = require("events").EventEmitter;
|
var EventEmitter = require("events").EventEmitter;
|
||||||
var q = require("q");
|
var q = require("q");
|
||||||
var url = require('url');
|
var url = require('url');
|
||||||
var anotherjson = require('another-json');
|
|
||||||
|
|
||||||
var httpApi = require("./http-api");
|
var httpApi = require("./http-api");
|
||||||
var MatrixEvent = require("./models/event").MatrixEvent;
|
var MatrixEvent = require("./models/event").MatrixEvent;
|
||||||
@@ -44,20 +43,12 @@ var SCROLLBACK_DELAY_MS = 3000;
|
|||||||
var CRYPTO_ENABLED = false;
|
var CRYPTO_ENABLED = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var OlmDevice = require("./OlmDevice");
|
var Crypto = require("./crypto");
|
||||||
CRYPTO_ENABLED = true;
|
CRYPTO_ENABLED = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Olm not installed.
|
// Olm not installed.
|
||||||
}
|
}
|
||||||
|
|
||||||
var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
|
||||||
|
|
||||||
var DeviceVerification = {
|
|
||||||
VERIFIED: 1,
|
|
||||||
UNVERIFIED: 0,
|
|
||||||
BLOCKED: -1,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a Matrix Client. Only directly construct this if you want to use
|
* Construct a Matrix Client. Only directly construct this if you want to use
|
||||||
* custom modules. Normally, {@link createClient} should be used
|
* custom modules. Normally, {@link createClient} should be used
|
||||||
@@ -116,7 +107,6 @@ function MatrixClient(opts) {
|
|||||||
MatrixBaseApis.call(this, opts);
|
MatrixBaseApis.call(this, opts);
|
||||||
|
|
||||||
this.store = opts.store || new StubStore();
|
this.store = opts.store || new StubStore();
|
||||||
this.sessionStore = opts.sessionStore || null;
|
|
||||||
|
|
||||||
this.deviceId = opts.deviceId || null;
|
this.deviceId = opts.deviceId || null;
|
||||||
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName || "js-sdk device";
|
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName || "js-sdk device";
|
||||||
@@ -126,38 +116,6 @@ function MatrixClient(opts) {
|
|||||||
userId: userId,
|
userId: userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._olmDevice = null;
|
|
||||||
this._cryptoAlgorithms = [];
|
|
||||||
|
|
||||||
if (CRYPTO_ENABLED && this.sessionStore !== null && userId !== null &&
|
|
||||||
this.deviceId !== null) {
|
|
||||||
this._olmDevice = new OlmDevice(opts.sessionStore);
|
|
||||||
this._cryptoAlgorithms.push(OLM_ALGORITHM);
|
|
||||||
|
|
||||||
// build our device keys: these will later be uploaded
|
|
||||||
this._deviceKeys = {};
|
|
||||||
this._deviceKeys["ed25519:" + this.deviceId] =
|
|
||||||
this._olmDevice.deviceEd25519Key;
|
|
||||||
this._deviceKeys["curve25519:" + this.deviceId] =
|
|
||||||
this._olmDevice.deviceCurve25519Key;
|
|
||||||
|
|
||||||
// add our own deviceinfo to the sessionstore
|
|
||||||
var deviceInfo = {
|
|
||||||
keys: this._deviceKeys,
|
|
||||||
algorithms: this._cryptoAlgorithms,
|
|
||||||
verified: DeviceVerification.VERIFIED,
|
|
||||||
};
|
|
||||||
var myDevices = this.sessionStore.getEndToEndDevicesForUser(
|
|
||||||
userId
|
|
||||||
) || {};
|
|
||||||
myDevices[opts.deviceId] = deviceInfo;
|
|
||||||
this.sessionStore.storeEndToEndDevicesForUser(
|
|
||||||
userId, myDevices
|
|
||||||
);
|
|
||||||
|
|
||||||
setupCryptoEventHandler(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scheduler = opts.scheduler;
|
this.scheduler = opts.scheduler;
|
||||||
if (this.scheduler) {
|
if (this.scheduler) {
|
||||||
var self = this;
|
var self = this;
|
||||||
@@ -192,6 +150,18 @@ function MatrixClient(opts) {
|
|||||||
this._txnCtr = 0;
|
this._txnCtr = 0;
|
||||||
this.timelineSupport = Boolean(opts.timelineSupport);
|
this.timelineSupport = Boolean(opts.timelineSupport);
|
||||||
this.urlPreviewCache = {};
|
this.urlPreviewCache = {};
|
||||||
|
|
||||||
|
this._crypto = null;
|
||||||
|
if (CRYPTO_ENABLED && opts.sessionStore !== null &&
|
||||||
|
userId !== null && this.deviceId !== null) {
|
||||||
|
this._crypto = new Crypto(
|
||||||
|
this,
|
||||||
|
opts.sessionStore,
|
||||||
|
userId, this.deviceId
|
||||||
|
);
|
||||||
|
|
||||||
|
setupCryptoEventHandler(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
utils.inherits(MatrixClient, EventEmitter);
|
utils.inherits(MatrixClient, EventEmitter);
|
||||||
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
|
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
|
||||||
@@ -291,7 +261,7 @@ MatrixClient.prototype.retryImmediately = function() {
|
|||||||
* @return {boolean} True if end-to-end is enabled.
|
* @return {boolean} True if end-to-end is enabled.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.isCryptoEnabled = function() {
|
MatrixClient.prototype.isCryptoEnabled = function() {
|
||||||
return CRYPTO_ENABLED && this.sessionStore !== null;
|
return this._crypto !== null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -302,105 +272,26 @@ MatrixClient.prototype.isCryptoEnabled = function() {
|
|||||||
* disabled.
|
* disabled.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.getDeviceEd25519Key = function() {
|
MatrixClient.prototype.getDeviceEd25519Key = function() {
|
||||||
if (!this._olmDevice) {
|
if (!this._crypto) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return this._olmDevice.deviceEd25519Key;
|
return this._crypto.getDeviceEd25519Key();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload the device keys to the homeserver and ensure that the
|
* Upload the device keys to the homeserver and ensure that the
|
||||||
* homeserver has enough one-time keys.
|
* homeserver has enough one-time keys.
|
||||||
* @param {number} maxKeys The maximum number of keys to generate
|
* @param {number} maxKeys The maximum number of keys to generate
|
||||||
* @param {object} deferred A deferred to resolve when the keys are uploaded.
|
|
||||||
* @return {object} A promise that will resolve when the keys are uploaded.
|
* @return {object} A promise that will resolve when the keys are uploaded.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.uploadKeys = function(maxKeys, deferred) {
|
MatrixClient.prototype.uploadKeys = function(maxKeys) {
|
||||||
var self = this;
|
if (this._crypto === null) {
|
||||||
return _uploadDeviceKeys(this).then(function(res) {
|
throw new Error("End-to-end encryption disabled");
|
||||||
var keyCount = res.one_time_key_counts.curve25519 || 0;
|
}
|
||||||
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
|
|
||||||
var keyLimit = Math.floor(maxOneTimeKeys / 2);
|
|
||||||
var numberToGenerate = Math.max(keyLimit - keyCount, 0);
|
|
||||||
if (maxKeys !== undefined) {
|
|
||||||
numberToGenerate = Math.min(numberToGenerate, maxKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numberToGenerate <= 0) {
|
return this._crypto.uploadKeys(maxKeys);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self._olmDevice.generateOneTimeKeys(numberToGenerate);
|
|
||||||
return _uploadOneTimeKeys(self);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// returns a promise which resolves to the response
|
|
||||||
function _uploadDeviceKeys(client) {
|
|
||||||
if (!client._olmDevice) {
|
|
||||||
return q.reject(new Error("End-to-end encryption disabled"));
|
|
||||||
}
|
|
||||||
|
|
||||||
var userId = client.credentials.userId;
|
|
||||||
var deviceId = client.deviceId;
|
|
||||||
|
|
||||||
var deviceKeys = {
|
|
||||||
algorithms: client._cryptoAlgorithms,
|
|
||||||
device_id: deviceId,
|
|
||||||
keys: client._deviceKeys,
|
|
||||||
user_id: userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
var sig = client._olmDevice.sign(anotherjson.stringify(deviceKeys));
|
|
||||||
deviceKeys.signatures = {};
|
|
||||||
deviceKeys.signatures[userId] = {};
|
|
||||||
deviceKeys.signatures[userId]["ed25519:" + deviceId] = sig;
|
|
||||||
|
|
||||||
return client.uploadKeysRequest({
|
|
||||||
device_keys: deviceKeys,
|
|
||||||
}, {
|
|
||||||
// for now, we set the device id explicitly, as we may not be using the
|
|
||||||
// same one as used in login.
|
|
||||||
device_id: deviceId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns a promise which resolves to the response
|
|
||||||
function _uploadOneTimeKeys(client) {
|
|
||||||
if (!client._olmDevice) {
|
|
||||||
return q.reject(new Error("End-to-end encryption disabled"));
|
|
||||||
}
|
|
||||||
|
|
||||||
var oneTimeKeys = client._olmDevice.getOneTimeKeys();
|
|
||||||
var oneTimeJson = {};
|
|
||||||
|
|
||||||
for (var keyId in oneTimeKeys.curve25519) {
|
|
||||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
|
||||||
oneTimeJson["curve25519:" + keyId] = oneTimeKeys.curve25519[keyId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return client.uploadKeysRequest({
|
|
||||||
one_time_keys: oneTimeJson
|
|
||||||
}, {
|
|
||||||
// for now, we set the device id explicitly, as we may not be using the
|
|
||||||
// same one as used in login.
|
|
||||||
device_id: client.deviceId,
|
|
||||||
}).then(function(res) {
|
|
||||||
client._olmDevice.markKeysAsPublished();
|
|
||||||
return res;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stored information about a user's device
|
|
||||||
* @typedef {Object} DeviceInfo
|
|
||||||
* @property {string[]} list of algorithms supported by this device
|
|
||||||
* @property {Object} keys a map from <key type>:<id> -> key
|
|
||||||
* @property {DeviceVerification} whether the device has been verified by the user
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the keys for a list of users and stores the keys in the session
|
* Download the keys for a list of users and stores the keys in the session
|
||||||
* store.
|
* store.
|
||||||
@@ -408,126 +299,15 @@ function _uploadOneTimeKeys(client) {
|
|||||||
* @param {bool} forceDownload Always download the keys even if cached.
|
* @param {bool} forceDownload Always download the keys even if cached.
|
||||||
*
|
*
|
||||||
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
|
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
|
||||||
* module:client~DeviceInfo|DeviceInfo}.
|
* module:crypto~DeviceInfo|DeviceInfo}.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
|
MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||||
if (this.sessionStore === null) {
|
if (this._crypto === null) {
|
||||||
return q.reject(new Error("End-to-end encryption disabled"));
|
return q.reject(new Error("End-to-end encryption disabled"));
|
||||||
}
|
}
|
||||||
var stored = {};
|
return this._crypto.downloadKeys(userIds, forceDownload);
|
||||||
var downloadQuery = {};
|
|
||||||
var downloadKeys = false;
|
|
||||||
for (var i = 0; i < userIds.length; ++i) {
|
|
||||||
var userId = userIds[i];
|
|
||||||
var devices = this.sessionStore.getEndToEndDevicesForUser(userId);
|
|
||||||
|
|
||||||
stored[userId] = devices || {};
|
|
||||||
if (devices && !forceDownload) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
downloadKeys = true;
|
|
||||||
downloadQuery[userId] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!downloadKeys) {
|
|
||||||
return q(stored);
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = "/keys/query";
|
|
||||||
var content = {device_keys: downloadQuery};
|
|
||||||
var self = this;
|
|
||||||
return this._http.authedRequestWithPrefix(
|
|
||||||
undefined, "POST", path, undefined, content,
|
|
||||||
httpApi.PREFIX_UNSTABLE
|
|
||||||
).then(function(res) {
|
|
||||||
for (var userId in res.device_keys) {
|
|
||||||
if (!downloadQuery.hasOwnProperty(userId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var userStore = stored[userId];
|
|
||||||
var updated = _updateStoredDeviceKeysForUser(
|
|
||||||
userId, userStore, res.device_keys[userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
self.sessionStore.storeEndToEndDevicesForUser(
|
|
||||||
userId, userStore
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return stored;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function _updateStoredDeviceKeysForUser(userId, userStore, userResult) {
|
|
||||||
var 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
var deviceRes = userResult[deviceId];
|
|
||||||
var deviceStore;
|
|
||||||
|
|
||||||
if (!deviceRes.keys) {
|
|
||||||
// no keys?
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var signKey = deviceRes.keys["ed25519:" + deviceId];
|
|
||||||
if (!signKey) {
|
|
||||||
console.log("Device " + userId + ": " +
|
|
||||||
deviceId + " has no ed25519 key");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deviceId in userStore) {
|
|
||||||
// already have this device.
|
|
||||||
deviceStore = userStore[deviceId];
|
|
||||||
|
|
||||||
if (deviceStore.keys["ed25519:" + deviceId] != 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");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
userStore[deviceId] = deviceStore = {
|
|
||||||
verified: DeviceVerification.UNVERIFIED
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: check signature. Remember that we need to check for
|
|
||||||
// _olmDevice.
|
|
||||||
|
|
||||||
deviceStore.keys = deviceRes.keys;
|
|
||||||
deviceStore.algorithms = deviceRes.algorithms;
|
|
||||||
deviceStore.unsigned = deviceRes.unsigned;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List the stored device keys for a user id
|
* List the stored device keys for a user id
|
||||||
*
|
*
|
||||||
@@ -537,37 +317,10 @@ function _updateStoredDeviceKeysForUser(userId, userStore, userResult) {
|
|||||||
* "key", and "display_name" parameters.
|
* "key", and "display_name" parameters.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.listDeviceKeys = function(userId) {
|
MatrixClient.prototype.listDeviceKeys = function(userId) {
|
||||||
if (!this.sessionStore) {
|
if (this._crypto === null) {
|
||||||
return [];
|
throw new Error("End-to-end encryption disabled");
|
||||||
}
|
}
|
||||||
var devices = this.sessionStore.getEndToEndDevicesForUser(userId);
|
return this._crypto.listDeviceKeys(userId);
|
||||||
var result = [];
|
|
||||||
if (devices) {
|
|
||||||
var deviceId;
|
|
||||||
var deviceIds = [];
|
|
||||||
for (deviceId in devices) {
|
|
||||||
if (devices.hasOwnProperty(deviceId)) {
|
|
||||||
deviceIds.push(deviceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deviceIds.sort();
|
|
||||||
for (var i = 0; i < deviceIds.length; ++i) {
|
|
||||||
deviceId = deviceIds[i];
|
|
||||||
var device = devices[deviceId];
|
|
||||||
var ed25519Key = device.keys["ed25519:" + deviceId];
|
|
||||||
var unsigned = device.unsigned || {};
|
|
||||||
if (ed25519Key) {
|
|
||||||
result.push({
|
|
||||||
id: deviceId,
|
|
||||||
key: ed25519Key,
|
|
||||||
verified: Boolean(device.verified == DeviceVerification.VERIFIED),
|
|
||||||
blocked: Boolean(device.verified == DeviceVerification.BLOCKED),
|
|
||||||
display_name: unsigned.device_display_name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -608,40 +361,13 @@ MatrixClient.prototype.setDeviceBlocked = function(userId, deviceId, blocked) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function _setDeviceVerification(client, userId, deviceId, verified, blocked) {
|
function _setDeviceVerification(client, userId, deviceId, verified, blocked) {
|
||||||
if (!client.sessionStore) {
|
if (!client._crypto) {
|
||||||
throw new Error("End-to-End encryption disabled");
|
throw new Error("End-to-End encryption disabled");
|
||||||
}
|
}
|
||||||
|
client._crypto.setDeviceVerification(userId, deviceId, verified, blocked);
|
||||||
var devices = client.sessionStore.getEndToEndDevicesForUser(userId);
|
client.emit("deviceVerificationChanged", userId, deviceId);
|
||||||
if (!devices || !devices[deviceId]) {
|
|
||||||
throw new Error("Unknown device " + userId + ":" + deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var dev = devices[deviceId];
|
|
||||||
var verificationStatus = dev.verified;
|
|
||||||
|
|
||||||
if (verified) {
|
|
||||||
verificationStatus = DeviceVerification.VERIFIED;
|
|
||||||
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
|
||||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blocked) {
|
|
||||||
verificationStatus = DeviceVerification.BLOCKED;
|
|
||||||
} else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) {
|
|
||||||
verificationStatus = DeviceVerification.UNVERIFIED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dev.verified === verificationStatus) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dev.verified = verificationStatus;
|
|
||||||
client.sessionStore.storeEndToEndDevicesForUser(userId, devices);
|
|
||||||
|
|
||||||
client.emit("deviceVerificationChanged", userId, deviceId, dev);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the sender of an event is verified
|
* Check if the sender of an event is verified
|
||||||
*
|
*
|
||||||
@@ -651,50 +377,22 @@ function _setDeviceVerification(client, userId, deviceId, verified, blocked) {
|
|||||||
* {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}.
|
* {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.isEventSenderVerified = function(event) {
|
MatrixClient.prototype.isEventSenderVerified = function(event) {
|
||||||
if (!this.sessionStore) {
|
if (!this._crypto) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cryptoContent = event.getWireContent();
|
var cryptoContent = event.getWireContent();
|
||||||
var algorithm = cryptoContent.algorithm;
|
|
||||||
|
|
||||||
if (algorithm !== OLM_ALGORITHM) {
|
|
||||||
console.warn("unable to verify event with algorithm " + algorithm);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var devices = this.sessionStore.getEndToEndDevicesForUser(event.getSender());
|
|
||||||
if (!devices) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sender_key = cryptoContent.sender_key;
|
var sender_key = cryptoContent.sender_key;
|
||||||
|
|
||||||
if (!sender_key) {
|
if (!sender_key) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var deviceId in devices) {
|
var algorithm = cryptoContent.algorithm;
|
||||||
if (!devices.hasOwnProperty(deviceId)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var device = devices[deviceId];
|
return this._crypto.isSenderKeyVerified(
|
||||||
for (var keyId in device.keys) {
|
event.getSender(), algorithm, sender_key
|
||||||
if (!device.keys.hasOwnProperty(keyId)) {
|
);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (keyId.indexOf("curve25519:") !== 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var deviceKey = device.keys[keyId];
|
|
||||||
if (deviceKey == sender_key) {
|
|
||||||
return device.verified == DeviceVerification.VERIFIED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// doesn't match a known device
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -714,24 +412,13 @@ function setupCryptoEventHandler(client) {
|
|||||||
|
|
||||||
function onCryptoEvent(client, event) {
|
function onCryptoEvent(client, event) {
|
||||||
var roomId = event.getRoomId();
|
var roomId = event.getRoomId();
|
||||||
|
|
||||||
// if we already have encryption in this room, we should ignore this event
|
|
||||||
// (for now at least. maybe we should alert the user somehow?)
|
|
||||||
var content = event.getContent();
|
var content = event.getContent();
|
||||||
var existingConfig = client.sessionStore.getEndToEndRoom(roomId);
|
|
||||||
if (existingConfig) {
|
|
||||||
if (JSON.stringify(existingConfig) != JSON.stringify(content)) {
|
|
||||||
console.error("Ignoring m.room.encryption event which requests " +
|
|
||||||
"a change of config in " + roomId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
client.setRoomEncryption(roomId, content).done();
|
client.setRoomEncryption(roomId, content).done();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error configuring encryption in room " + roomId +
|
console.error("Error configuring encryption in room " + roomId +
|
||||||
": " + e);
|
":", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,133 +429,21 @@ function onCryptoEvent(client, event) {
|
|||||||
* @return {Object} A promise that will resolve when encryption is setup.
|
* @return {Object} A promise that will resolve when encryption is setup.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
|
MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
|
||||||
if (!this._olmDevice) {
|
if (!this._crypto) {
|
||||||
throw new Error("End-to-End encryption disabled");
|
throw new Error("End-to-End encryption disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
var self = this;
|
var roomMembers = [];
|
||||||
|
var room = this.getRoom(roomId);
|
||||||
if (config.algorithm === OLM_ALGORITHM) {
|
if (!room) {
|
||||||
// remove spurious keys
|
console.warn("Enabling encryption in unknown room " + roomId);
|
||||||
config = {
|
|
||||||
algorithm: OLM_ALGORITHM,
|
|
||||||
};
|
|
||||||
this.sessionStore.storeEndToEndRoom(roomId, config);
|
|
||||||
|
|
||||||
var room = this.getRoom(roomId);
|
|
||||||
|
|
||||||
if (!room) {
|
|
||||||
console.warn("Enabling encryption in unknown room " + roomId);
|
|
||||||
return q({});
|
|
||||||
}
|
|
||||||
|
|
||||||
var users = utils.map(room.getJoinedMembers(), function(u) {
|
|
||||||
return u.userId;
|
|
||||||
});
|
|
||||||
|
|
||||||
return self.downloadKeys(users, true).then(function(res) {
|
|
||||||
return self._ensureOlmSessionsForUsers(users);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown algorithm: " + config.algorithm);
|
roomMembers = utils.map(room.getJoinedMembers(), function(u) {
|
||||||
}
|
return u.userId;
|
||||||
};
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to make sure we have established olm sessions for the given users.
|
|
||||||
*
|
|
||||||
* @param {string[]} users list of user ids
|
|
||||||
*
|
|
||||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
|
||||||
* an object with keys <tt>missingUsers</tt> (a list of users with no known
|
|
||||||
* olm devices), and <tt>missingDevices</tt> a list of olm devices with no
|
|
||||||
* known one-time keys.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
MatrixClient.prototype._ensureOlmSessionsForUsers = function(users) {
|
|
||||||
var devicesWithoutSession = [];
|
|
||||||
var userWithoutDevices = [];
|
|
||||||
for (var i = 0; i < users.length; ++i) {
|
|
||||||
var userId = users[i];
|
|
||||||
var devices = this.sessionStore.getEndToEndDevicesForUser(userId);
|
|
||||||
if (!devices) {
|
|
||||||
userWithoutDevices.push(userId);
|
|
||||||
} else {
|
|
||||||
for (var deviceId in devices) {
|
|
||||||
if (devices.hasOwnProperty(deviceId)) {
|
|
||||||
var keys = devices[deviceId];
|
|
||||||
var key = keys.keys["curve25519:" + deviceId];
|
|
||||||
if (key == this._olmDevice.deviceCurve25519Key) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!this.sessionStore.getEndToEndSessions(key)) {
|
|
||||||
devicesWithoutSession.push([userId, deviceId, key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (devicesWithoutSession.length === 0) {
|
return this._crypto.setRoomEncryption(roomId, config, roomMembers);
|
||||||
return q({
|
|
||||||
missingUsers: userWithoutDevices,
|
|
||||||
missingDevices: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var queries = {};
|
|
||||||
for (i = 0; i < devicesWithoutSession.length; ++i) {
|
|
||||||
var device = devicesWithoutSession[i];
|
|
||||||
var query = queries[device[0]] || {};
|
|
||||||
queries[device[0]] = query;
|
|
||||||
query[device[1]] = "curve25519";
|
|
||||||
}
|
|
||||||
var path = "/keys/claim";
|
|
||||||
var content = {one_time_keys: queries};
|
|
||||||
var self = this;
|
|
||||||
return this._http.authedRequestWithPrefix(
|
|
||||||
undefined, "POST", path, undefined, content,
|
|
||||||
httpApi.PREFIX_UNSTABLE
|
|
||||||
).then(function(res) {
|
|
||||||
var missing = {};
|
|
||||||
for (i = 0; i < devicesWithoutSession.length; ++i) {
|
|
||||||
var device = devicesWithoutSession[i];
|
|
||||||
var userRes = res.one_time_keys[device[0]] || {};
|
|
||||||
var deviceRes = userRes[device[1]];
|
|
||||||
var oneTimeKey;
|
|
||||||
for (var keyId in deviceRes) {
|
|
||||||
if (keyId.indexOf("curve25519:") === 0) {
|
|
||||||
oneTimeKey = deviceRes[keyId];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oneTimeKey) {
|
|
||||||
var sid = self._olmDevice.createOutboundSession(
|
|
||||||
device[2], oneTimeKey
|
|
||||||
);
|
|
||||||
console.log("Started new sessionid " + sid +
|
|
||||||
" for device " + device[2]);
|
|
||||||
} else {
|
|
||||||
missing[device[0]] = missing[device[0]] || [];
|
|
||||||
missing[device[0]].push([device[1]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
missingUsers: userWithoutDevices,
|
|
||||||
missingDevices: missing
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable encryption for a room.
|
|
||||||
* @param {string} roomId the room to disable encryption for.
|
|
||||||
*/
|
|
||||||
MatrixClient.prototype.disableRoomEncryption = function(roomId) {
|
|
||||||
if (this.sessionStore !== null) {
|
|
||||||
this.sessionStore.storeEndToEndRoom(roomId, null);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -877,13 +452,49 @@ MatrixClient.prototype.disableRoomEncryption = function(roomId) {
|
|||||||
* @return {bool} whether encryption is enabled.
|
* @return {bool} whether encryption is enabled.
|
||||||
*/
|
*/
|
||||||
MatrixClient.prototype.isRoomEncrypted = function(roomId) {
|
MatrixClient.prototype.isRoomEncrypted = function(roomId) {
|
||||||
if (CRYPTO_ENABLED && this.sessionStore !== null) {
|
if (!this._crypto) {
|
||||||
return (this.sessionStore.getEndToEndRoom(roomId) && true) || false;
|
|
||||||
} else {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this._crypto.isRoomEncrypted(roomId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt a received event according to the algorithm specified in the event.
|
||||||
|
*
|
||||||
|
* @param {MatrixClient} client
|
||||||
|
* @param {object} raw event
|
||||||
|
*
|
||||||
|
* @return {object} decrypted payload (with properties 'type', 'content')
|
||||||
|
*/
|
||||||
|
function _decryptMessage(client, event) {
|
||||||
|
if (!client._crypto) {
|
||||||
|
return _badEncryptedMessage(event, "**Encryption not enabled**");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return client._crypto.decryptEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
if (!(e instanceof Crypto.DecryptionError)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return _badEncryptedMessage(event, "**" + e.message + "**");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _badEncryptedMessage(event, reason) {
|
||||||
|
return {
|
||||||
|
type: "m.room.message",
|
||||||
|
content: {
|
||||||
|
msgtype: "m.bad.encrypted",
|
||||||
|
body: reason,
|
||||||
|
content: event.content,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room ops
|
||||||
|
// ========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the room for the given room ID.
|
* Get the room for the given room ID.
|
||||||
@@ -1206,211 +817,6 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypt an event according to the configuration of the room, if necessary.
|
|
||||||
*
|
|
||||||
* @param {MatrixClient} client
|
|
||||||
* @param {module:models/event.MatrixEvent} event event to be sent
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function _encryptEventIfNeeded(client, event) {
|
|
||||||
if (event.isEncrypted()) {
|
|
||||||
// this event has already been encrypted; this happens if the
|
|
||||||
// encryption step succeeded, but the send step failed on the first
|
|
||||||
// attempt.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.getType() !== "m.room.message") {
|
|
||||||
// we only encrypt m.room.message
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!client.sessionStore) {
|
|
||||||
// End to end encryption isn't enabled if we don't have a session
|
|
||||||
// store.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var roomId = event.getRoomId();
|
|
||||||
|
|
||||||
var e2eRoomInfo = client.sessionStore.getEndToEndRoom(roomId);
|
|
||||||
if (!e2eRoomInfo || !e2eRoomInfo.algorithm) {
|
|
||||||
// not encrypting messages in this room
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var encryptedContent = _encryptMessage(
|
|
||||||
client, roomId, e2eRoomInfo, event.getType(), event.getContent()
|
|
||||||
);
|
|
||||||
event.makeEncrypted("m.room.encrypted", encryptedContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _encryptMessage(client, roomId, e2eRoomInfo, eventType, content) {
|
|
||||||
if (!client.sessionStore) {
|
|
||||||
throw new Error(
|
|
||||||
"Client must have an end-to-end session store to encrypt messages"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e2eRoomInfo.algorithm === OLM_ALGORITHM) {
|
|
||||||
var room = client.getRoom(roomId);
|
|
||||||
if (!room) {
|
|
||||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
|
||||||
}
|
|
||||||
|
|
||||||
// pick the list of recipients based on the membership list.
|
|
||||||
//
|
|
||||||
// TODO: there is a race condition here! What if a new user turns up
|
|
||||||
// just as you are sending a secret message?
|
|
||||||
|
|
||||||
var users = utils.map(room.getJoinedMembers(), function(u) {
|
|
||||||
return u.userId;
|
|
||||||
});
|
|
||||||
|
|
||||||
var participantKeys = [];
|
|
||||||
for (var i = 0; i < users.length; ++i) {
|
|
||||||
var userId = users[i];
|
|
||||||
var devices = client.sessionStore.getEndToEndDevicesForUser(userId);
|
|
||||||
for (var deviceId in devices) {
|
|
||||||
if (devices.hasOwnProperty(deviceId)) {
|
|
||||||
var dev = devices[deviceId];
|
|
||||||
if (dev.verified === DeviceVerification.BLOCKED) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var keyId in dev.keys) {
|
|
||||||
if (keyId.indexOf("curve25519:") === 0) {
|
|
||||||
participantKeys.push(dev.keys[keyId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
participantKeys.sort();
|
|
||||||
var participantHash = ""; // Olm.sha256(participantKeys.join());
|
|
||||||
var payloadJson = {
|
|
||||||
room_id: roomId,
|
|
||||||
type: eventType,
|
|
||||||
fingerprint: participantHash,
|
|
||||||
sender_device: client.deviceId,
|
|
||||||
content: content
|
|
||||||
};
|
|
||||||
var ciphertext = {};
|
|
||||||
var payloadString = JSON.stringify(payloadJson);
|
|
||||||
for (i = 0; i < participantKeys.length; ++i) {
|
|
||||||
var deviceKey = participantKeys[i];
|
|
||||||
if (deviceKey == client._olmDevice.deviceCurve25519Key) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var sessionIds = client._olmDevice.getSessionIdsForDevice(deviceKey);
|
|
||||||
// Use the session with the lowest ID.
|
|
||||||
sessionIds.sort();
|
|
||||||
if (sessionIds.length === 0) {
|
|
||||||
// If we don't have a session for a device then
|
|
||||||
// we can't encrypt a message for it.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
var sessionId = sessionIds[0];
|
|
||||||
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
|
|
||||||
ciphertext[deviceKey] = client._olmDevice.encryptMessage(
|
|
||||||
deviceKey, sessionId, payloadString
|
|
||||||
);
|
|
||||||
}
|
|
||||||
var encryptedContent = {
|
|
||||||
algorithm: e2eRoomInfo.algorithm,
|
|
||||||
sender_key: client._olmDevice.deviceCurve25519Key,
|
|
||||||
ciphertext: ciphertext
|
|
||||||
};
|
|
||||||
return encryptedContent;
|
|
||||||
} else {
|
|
||||||
throw new Error("Unknown end-to-end algorithm: " + e2eRoomInfo.algorithm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decrypt a received event according to the algorithm specified in the event.
|
|
||||||
*
|
|
||||||
* @param {MatrixClient} client
|
|
||||||
* @param {object} raw event
|
|
||||||
*
|
|
||||||
* @return {object} decrypted payload (with properties 'type', 'content')
|
|
||||||
*/
|
|
||||||
function _decryptMessage(client, event) {
|
|
||||||
if (client.sessionStore === null || !CRYPTO_ENABLED) {
|
|
||||||
// End to end encryption isn't enabled if we don't have a session
|
|
||||||
// store.
|
|
||||||
return _badEncryptedMessage(event, "**Encryption not enabled**");
|
|
||||||
}
|
|
||||||
|
|
||||||
var content = event.content;
|
|
||||||
if (content.algorithm === OLM_ALGORITHM) {
|
|
||||||
var deviceKey = content.sender_key;
|
|
||||||
var ciphertext = content.ciphertext;
|
|
||||||
|
|
||||||
if (!ciphertext) {
|
|
||||||
return _badEncryptedMessage(event, "**Missing ciphertext**");
|
|
||||||
}
|
|
||||||
if (!(client._olmDevice.deviceCurve25519Key in content.ciphertext)) {
|
|
||||||
return _badEncryptedMessage(event, "**Not included in recipients**");
|
|
||||||
}
|
|
||||||
var message = content.ciphertext[client._olmDevice.deviceCurve25519Key];
|
|
||||||
var sessionIds = client._olmDevice.getSessionIdsForDevice(deviceKey);
|
|
||||||
var payloadString = null;
|
|
||||||
var foundSession = false;
|
|
||||||
for (var i = 0; i < sessionIds.length; i++) {
|
|
||||||
var sessionId = sessionIds[i];
|
|
||||||
var res = client._olmDevice.decryptMessage(
|
|
||||||
deviceKey, sessionId, message.type, message.body
|
|
||||||
);
|
|
||||||
payloadString = res.payload;
|
|
||||||
if (payloadString) {
|
|
||||||
console.log("decrypted with sessionId " + sessionId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.matchesInbound) {
|
|
||||||
// this was a prekey message which matched this session; don't
|
|
||||||
// create a new session.
|
|
||||||
foundSession = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.type === 0 && !foundSession && payloadString === null) {
|
|
||||||
try {
|
|
||||||
payloadString = client._olmDevice.createInboundSession(
|
|
||||||
deviceKey, message.type, message.body
|
|
||||||
);
|
|
||||||
console.log("created new inbound sesion");
|
|
||||||
} catch (e) {
|
|
||||||
// Failed to decrypt with a new session.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Check the sender user id matches the sender key.
|
|
||||||
|
|
||||||
if (payloadString !== null) {
|
|
||||||
return JSON.parse(payloadString);
|
|
||||||
} else {
|
|
||||||
return _badEncryptedMessage(event, "**Bad Encrypted Message**");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _badEncryptedMessage(event, "**Unknown algorithm**");
|
|
||||||
}
|
|
||||||
|
|
||||||
function _badEncryptedMessage(event, reason) {
|
|
||||||
return {
|
|
||||||
type: "m.room.message",
|
|
||||||
content: {
|
|
||||||
msgtype: "m.bad.encrypted",
|
|
||||||
body: reason,
|
|
||||||
content: event.content,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// encrypts the event if necessary
|
// encrypts the event if necessary
|
||||||
// adds the event to the queue, or sends it
|
// adds the event to the queue, or sends it
|
||||||
// marks the event as sent/unsent
|
// marks the event as sent/unsent
|
||||||
@@ -1420,7 +826,9 @@ function _sendEvent(client, room, event, callback) {
|
|||||||
// so that we can handle synchronous and asynchronous exceptions with the
|
// so that we can handle synchronous and asynchronous exceptions with the
|
||||||
// same code path.
|
// same code path.
|
||||||
return q().then(function() {
|
return q().then(function() {
|
||||||
_encryptEventIfNeeded(client, event);
|
if (client._crypto) {
|
||||||
|
client._crypto.encryptEventIfNeeded(event, room);
|
||||||
|
}
|
||||||
|
|
||||||
var promise;
|
var promise;
|
||||||
// this event may be queued
|
// this event may be queued
|
||||||
@@ -3062,8 +2470,8 @@ MatrixClient.prototype.startClient = function(opts) {
|
|||||||
|
|
||||||
this._clientOpts = opts;
|
this._clientOpts = opts;
|
||||||
|
|
||||||
if (this._olmDevice) {
|
if (this._crypto) {
|
||||||
this.uploadKeys(5).done();
|
this._crypto.uploadKeys(5).done();
|
||||||
}
|
}
|
||||||
|
|
||||||
// periodically poll for turn servers if we support voip
|
// periodically poll for turn servers if we support voip
|
||||||
@@ -3514,7 +2922,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
|||||||
*
|
*
|
||||||
* @event module:client~MatrixClient#"deviceVerificationChanged"
|
* @event module:client~MatrixClient#"deviceVerificationChanged"
|
||||||
* @param {string} userId the owner of the verified device
|
* @param {string} userId the owner of the verified device
|
||||||
* @param {module:client~DeviceInfo} device information about the verified device
|
* @param {string} deviceId the id of the verified device
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
771
lib/crypto.js
Normal file
771
lib/crypto.js
Normal file
@@ -0,0 +1,771 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket 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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal module
|
||||||
|
*
|
||||||
|
* @module crypto
|
||||||
|
*/
|
||||||
|
|
||||||
|
var anotherjson = require('another-json');
|
||||||
|
var q = require("q");
|
||||||
|
|
||||||
|
var utils = require("./utils");
|
||||||
|
var OlmDevice = require("./OlmDevice");
|
||||||
|
|
||||||
|
var OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||||
|
|
||||||
|
var DeviceVerification = {
|
||||||
|
VERIFIED: 1,
|
||||||
|
UNVERIFIED: 0,
|
||||||
|
BLOCKED: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stored information about a user's device
|
||||||
|
*
|
||||||
|
* @typedef {Object} DeviceInfo
|
||||||
|
*
|
||||||
|
* @property {string[]} altorithms list of algorithms supported by this device
|
||||||
|
*
|
||||||
|
* @property {Object} keys a map from <key type>:<id> -> key
|
||||||
|
*
|
||||||
|
* @property {DeviceVerification} verified whether the device has been
|
||||||
|
* verified by the user
|
||||||
|
*
|
||||||
|
* @property {Object} unsigned additional data from the homeserver
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cryptography bits
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*
|
||||||
|
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||||
|
*
|
||||||
|
* @param {module:store/session/webstorage~WebStorageSessionStore} sessionStore
|
||||||
|
* Store to be used for end-to-end crypto session data
|
||||||
|
*
|
||||||
|
* @param {string} userId The user ID for the local user
|
||||||
|
*
|
||||||
|
* @param {string} deviceId The identifier for this device.
|
||||||
|
*/
|
||||||
|
function Crypto(baseApis, sessionStore, userId, deviceId) {
|
||||||
|
this._baseApis = baseApis;
|
||||||
|
this._sessionStore = sessionStore;
|
||||||
|
this._userId = userId;
|
||||||
|
this._deviceId = deviceId;
|
||||||
|
|
||||||
|
this._cryptoAlgorithms = [];
|
||||||
|
|
||||||
|
this._olmDevice = new OlmDevice(sessionStore);
|
||||||
|
this._cryptoAlgorithms = [OLM_ALGORITHM];
|
||||||
|
|
||||||
|
// build our device keys: these will later be uploaded
|
||||||
|
this._deviceKeys = {};
|
||||||
|
this._deviceKeys["ed25519:" + this._deviceId] =
|
||||||
|
this._olmDevice.deviceEd25519Key;
|
||||||
|
this._deviceKeys["curve25519:" + this._deviceId] =
|
||||||
|
this._olmDevice.deviceCurve25519Key;
|
||||||
|
|
||||||
|
// add our own deviceinfo to the sessionstore
|
||||||
|
var deviceInfo = {
|
||||||
|
keys: this._deviceKeys,
|
||||||
|
algorithms: this._cryptoAlgorithms,
|
||||||
|
verified: DeviceVerification.VERIFIED,
|
||||||
|
};
|
||||||
|
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
||||||
|
this._userId
|
||||||
|
) || {};
|
||||||
|
myDevices[this._deviceId] = deviceInfo;
|
||||||
|
this._sessionStore.storeEndToEndDevicesForUser(
|
||||||
|
this._userId, myDevices
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Ed25519 key for this device
|
||||||
|
*
|
||||||
|
* @return {string} base64-encoded ed25519 key.
|
||||||
|
*/
|
||||||
|
Crypto.prototype.getDeviceEd25519Key = function() {
|
||||||
|
return this._olmDevice.deviceEd25519Key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload the device keys to the homeserver and ensure that the
|
||||||
|
* homeserver has enough one-time keys.
|
||||||
|
* @param {number} maxKeys The maximum number of keys to generate
|
||||||
|
* @return {object} A promise that will resolve when the keys are uploaded.
|
||||||
|
*/
|
||||||
|
Crypto.prototype.uploadKeys = function(maxKeys) {
|
||||||
|
var self = this;
|
||||||
|
return _uploadDeviceKeys(this).then(function(res) {
|
||||||
|
var keyCount = res.one_time_key_counts.curve25519 || 0;
|
||||||
|
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
|
||||||
|
var keyLimit = Math.floor(maxOneTimeKeys / 2);
|
||||||
|
var numberToGenerate = Math.max(keyLimit - keyCount, 0);
|
||||||
|
if (maxKeys !== undefined) {
|
||||||
|
numberToGenerate = Math.min(numberToGenerate, maxKeys);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberToGenerate <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self._olmDevice.generateOneTimeKeys(numberToGenerate);
|
||||||
|
return _uploadOneTimeKeys(self);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// returns a promise which resolves to the response
|
||||||
|
function _uploadDeviceKeys(crypto) {
|
||||||
|
var userId = crypto._userId;
|
||||||
|
var deviceId = crypto._deviceId;
|
||||||
|
|
||||||
|
var deviceKeys = {
|
||||||
|
algorithms: crypto._cryptoAlgorithms,
|
||||||
|
device_id: deviceId,
|
||||||
|
keys: crypto._deviceKeys,
|
||||||
|
user_id: userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
var sig = crypto._olmDevice.sign(anotherjson.stringify(deviceKeys));
|
||||||
|
deviceKeys.signatures = {};
|
||||||
|
deviceKeys.signatures[userId] = {};
|
||||||
|
deviceKeys.signatures[userId]["ed25519:" + deviceId] = sig;
|
||||||
|
|
||||||
|
return crypto._baseApis.uploadKeysRequest({
|
||||||
|
device_keys: deviceKeys,
|
||||||
|
}, {
|
||||||
|
// for now, we set the device id explicitly, as we may not be using the
|
||||||
|
// same one as used in login.
|
||||||
|
device_id: deviceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a promise which resolves to the response
|
||||||
|
function _uploadOneTimeKeys(crypto) {
|
||||||
|
var oneTimeKeys = crypto._olmDevice.getOneTimeKeys();
|
||||||
|
var oneTimeJson = {};
|
||||||
|
|
||||||
|
for (var keyId in oneTimeKeys.curve25519) {
|
||||||
|
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||||
|
oneTimeJson["curve25519:" + keyId] = oneTimeKeys.curve25519[keyId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crypto._baseApis.uploadKeysRequest({
|
||||||
|
one_time_keys: oneTimeJson
|
||||||
|
}, {
|
||||||
|
// for now, we set the device id explicitly, as we may not be using the
|
||||||
|
// same one as used in login.
|
||||||
|
device_id: crypto._deviceId,
|
||||||
|
}).then(function(res) {
|
||||||
|
crypto._olmDevice.markKeysAsPublished();
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}.
|
||||||
|
*/
|
||||||
|
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||||
|
var self = this;
|
||||||
|
var stored = {};
|
||||||
|
var downloadUsers = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < userIds.length; ++i) {
|
||||||
|
var userId = userIds[i];
|
||||||
|
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
|
|
||||||
|
stored[userId] = devices || {};
|
||||||
|
if (devices && !forceDownload) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
downloadUsers.push(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (downloadUsers.length === 0) {
|
||||||
|
return q(stored);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._baseApis.downloadKeysForUsers(
|
||||||
|
downloadUsers
|
||||||
|
).then(function(res) {
|
||||||
|
for (var userId in res.device_keys) {
|
||||||
|
if (!stored.hasOwnProperty(userId)) {
|
||||||
|
// spurious result
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userStore = stored[userId];
|
||||||
|
var updated = _updateStoredDeviceKeysForUser(
|
||||||
|
userId, userStore, res.device_keys[userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
self._sessionStore.storeEndToEndDevicesForUser(
|
||||||
|
userId, userStore
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stored;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function _updateStoredDeviceKeysForUser(userId, userStore, userResult) {
|
||||||
|
var 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviceRes = userResult[deviceId];
|
||||||
|
var deviceStore;
|
||||||
|
|
||||||
|
if (!deviceRes.keys) {
|
||||||
|
// no keys?
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var signKey = deviceRes.keys["ed25519:" + deviceId];
|
||||||
|
if (!signKey) {
|
||||||
|
console.log("Device " + userId + ": " +
|
||||||
|
deviceId + " has no ed25519 key");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceId in userStore) {
|
||||||
|
// already have this device.
|
||||||
|
deviceStore = userStore[deviceId];
|
||||||
|
|
||||||
|
if (deviceStore.keys["ed25519:" + deviceId] != 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");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userStore[deviceId] = deviceStore = {
|
||||||
|
verified: DeviceVerification.UNVERIFIED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: check signature. Remember that we need to check for
|
||||||
|
// _olmDevice.
|
||||||
|
|
||||||
|
deviceStore.keys = deviceRes.keys;
|
||||||
|
deviceStore.algorithms = deviceRes.algorithms;
|
||||||
|
deviceStore.unsigned = deviceRes.unsigned;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the stored device keys for a user id
|
||||||
|
*
|
||||||
|
* @param {string} userId the user to list keys for.
|
||||||
|
*
|
||||||
|
* @return {object[]} list of devices with "id", "verified", "blocked",
|
||||||
|
* "key", and "display_name" parameters.
|
||||||
|
*/
|
||||||
|
Crypto.prototype.listDeviceKeys = function(userId) {
|
||||||
|
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
|
var result = [];
|
||||||
|
if (devices) {
|
||||||
|
var deviceId;
|
||||||
|
var deviceIds = [];
|
||||||
|
for (deviceId in devices) {
|
||||||
|
if (devices.hasOwnProperty(deviceId)) {
|
||||||
|
deviceIds.push(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deviceIds.sort();
|
||||||
|
for (var i = 0; i < deviceIds.length; ++i) {
|
||||||
|
deviceId = deviceIds[i];
|
||||||
|
var device = devices[deviceId];
|
||||||
|
var ed25519Key = device.keys["ed25519:" + deviceId];
|
||||||
|
var unsigned = device.unsigned || {};
|
||||||
|
if (ed25519Key) {
|
||||||
|
result.push({
|
||||||
|
id: deviceId,
|
||||||
|
key: ed25519Key,
|
||||||
|
verified: Boolean(device.verified == DeviceVerification.VERIFIED),
|
||||||
|
blocked: Boolean(device.verified == DeviceVerification.BLOCKED),
|
||||||
|
display_name: unsigned.device_display_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 !== OLM_ALGORITHM) {
|
||||||
|
// we only deal in olm keys
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
|
if (!devices) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var deviceId in devices) {
|
||||||
|
if (!devices.hasOwnProperty(deviceId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var device = devices[deviceId];
|
||||||
|
for (var keyId in device.keys) {
|
||||||
|
if (!device.keys.hasOwnProperty(keyId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (keyId.indexOf("curve25519:") !== 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var deviceKey = device.keys[keyId];
|
||||||
|
if (deviceKey == sender_key) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't match a known device
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the blocked/verified state of the given device
|
||||||
|
*
|
||||||
|
* @param {string} userId owner of the device
|
||||||
|
* @param {string} deviceId unique identifier for the device
|
||||||
|
*
|
||||||
|
* @param {?boolean} verified whether to mark the device as verified. Null to
|
||||||
|
* leave unchanged.
|
||||||
|
*
|
||||||
|
* @param {?boolean} blocked whether to mark the device as blocked. Null to
|
||||||
|
* leave unchanged.
|
||||||
|
*/
|
||||||
|
Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified, blocked) {
|
||||||
|
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
|
if (!devices || !devices[deviceId]) {
|
||||||
|
throw new Error("Unknown device " + userId + ":" + deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dev = devices[deviceId];
|
||||||
|
var verificationStatus = dev.verified;
|
||||||
|
|
||||||
|
if (verified) {
|
||||||
|
verificationStatus = DeviceVerification.VERIFIED;
|
||||||
|
} else if (verified !== null && verificationStatus == DeviceVerification.VERIFIED) {
|
||||||
|
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blocked) {
|
||||||
|
verificationStatus = DeviceVerification.BLOCKED;
|
||||||
|
} else if (blocked !== null && verificationStatus == DeviceVerification.BLOCKED) {
|
||||||
|
verificationStatus = DeviceVerification.UNVERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dev.verified === verificationStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dev.verified = verificationStatus;
|
||||||
|
this._sessionStore.storeEndToEndDevicesForUser(userId, devices);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identify a device by curve25519 identity key and determine its verification state
|
||||||
|
*
|
||||||
|
* @param {string} userId owner of the device
|
||||||
|
* @param {string} algorithm encryption algorithm
|
||||||
|
* @param {string} sender_key curve25519 key to match
|
||||||
|
*
|
||||||
|
* @return {boolean} true if the device is verified
|
||||||
|
*/
|
||||||
|
Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
|
||||||
|
var device = this.getDeviceByIdentityKey(userId, algorithm, sender_key);
|
||||||
|
if (!device) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return device.verified == DeviceVerification.VERIFIED;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure a room to use encryption (ie, save a flag in the sessionstore).
|
||||||
|
*
|
||||||
|
* @param {string} roomId The room ID to enable encryption in.
|
||||||
|
* @param {object} config The encryption config for the room.
|
||||||
|
* @param {string[]} roomMembers userIds of room members to start sessions with
|
||||||
|
*
|
||||||
|
* @return {Object} A promise that will resolve when encryption is setup.
|
||||||
|
*/
|
||||||
|
Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// if we already have encryption in this room, we should ignore this event
|
||||||
|
// (for now at least. maybe we should alert the user somehow?)
|
||||||
|
var existingConfig = this._sessionStore.getEndToEndRoom(roomId);
|
||||||
|
if (existingConfig) {
|
||||||
|
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
|
||||||
|
console.error("Ignoring m.room.encryption event which requests " +
|
||||||
|
"a change of config in " + roomId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.algorithm !== OLM_ALGORITHM) {
|
||||||
|
throw new Error("Unknown algorithm: " + config.algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove spurious keys
|
||||||
|
config = {
|
||||||
|
algorithm: OLM_ALGORITHM,
|
||||||
|
};
|
||||||
|
this._sessionStore.storeEndToEndRoom(roomId, config);
|
||||||
|
|
||||||
|
return self.downloadKeys(roomMembers, true).then(function(res) {
|
||||||
|
return self._ensureOlmSessionsForUsers(roomMembers);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to make sure we have established olm sessions for the given users.
|
||||||
|
*
|
||||||
|
* @param {string[]} users list of user ids
|
||||||
|
*
|
||||||
|
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||||
|
* an object with keys <tt>missingUsers</tt> (a list of users with no known
|
||||||
|
* olm devices), and <tt>missingDevices</tt> a list of olm devices with no
|
||||||
|
* known one-time keys.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
Crypto.prototype._ensureOlmSessionsForUsers = function(users) {
|
||||||
|
var devicesWithoutSession = [];
|
||||||
|
var userWithoutDevices = [];
|
||||||
|
for (var i = 0; i < users.length; ++i) {
|
||||||
|
var userId = users[i];
|
||||||
|
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
|
if (!devices) {
|
||||||
|
userWithoutDevices.push(userId);
|
||||||
|
} else {
|
||||||
|
for (var deviceId in devices) {
|
||||||
|
if (devices.hasOwnProperty(deviceId)) {
|
||||||
|
var keys = devices[deviceId];
|
||||||
|
var key = keys.keys["curve25519:" + deviceId];
|
||||||
|
if (key == this._olmDevice.deviceCurve25519Key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!this._sessionStore.getEndToEndSessions(key)) {
|
||||||
|
devicesWithoutSession.push([userId, deviceId, key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devicesWithoutSession.length === 0) {
|
||||||
|
return q({
|
||||||
|
missingUsers: userWithoutDevices,
|
||||||
|
missingDevices: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
return this._baseApis.claimOneTimeKeys(
|
||||||
|
devicesWithoutSession
|
||||||
|
).then(function(res) {
|
||||||
|
var missing = {};
|
||||||
|
for (i = 0; i < devicesWithoutSession.length; ++i) {
|
||||||
|
var device = devicesWithoutSession[i];
|
||||||
|
var userRes = res.one_time_keys[device[0]] || {};
|
||||||
|
var deviceRes = userRes[device[1]];
|
||||||
|
var oneTimeKey;
|
||||||
|
for (var keyId in deviceRes) {
|
||||||
|
if (keyId.indexOf("curve25519:") === 0) {
|
||||||
|
oneTimeKey = deviceRes[keyId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oneTimeKey) {
|
||||||
|
var sid = self._olmDevice.createOutboundSession(
|
||||||
|
device[2], oneTimeKey
|
||||||
|
);
|
||||||
|
console.log("Started new sessionid " + sid +
|
||||||
|
" for device " + device[2]);
|
||||||
|
} else {
|
||||||
|
missing[device[0]] = missing[device[0]] || [];
|
||||||
|
missing[device[0]].push([device[1]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
missingUsers: userWithoutDevices,
|
||||||
|
missingDevices: missing
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether encryption is enabled for a room.
|
||||||
|
* @param {string} roomId the room id to query.
|
||||||
|
* @return {bool} whether encryption is enabled.
|
||||||
|
*/
|
||||||
|
Crypto.prototype.isRoomEncrypted = function(roomId) {
|
||||||
|
return (this._sessionStore.getEndToEndRoom(roomId) && true) || false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt an event according to the configuration of the room, if necessary.
|
||||||
|
*
|
||||||
|
* @param {module:models/event.MatrixEvent} event event to be sent
|
||||||
|
* @param {module:models/room.Room} room destination room
|
||||||
|
*/
|
||||||
|
Crypto.prototype.encryptEventIfNeeded = function(event, room) {
|
||||||
|
if (event.isEncrypted()) {
|
||||||
|
// this event has already been encrypted; this happens if the
|
||||||
|
// encryption step succeeded, but the send step failed on the first
|
||||||
|
// attempt.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.getType() !== "m.room.message") {
|
||||||
|
// we only encrypt m.room.message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomId = event.getRoomId();
|
||||||
|
|
||||||
|
var e2eRoomInfo = this._sessionStore.getEndToEndRoom(roomId);
|
||||||
|
if (!e2eRoomInfo || !e2eRoomInfo.algorithm) {
|
||||||
|
// not encrypting messages in this room
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encryptedContent = this._encryptMessage(
|
||||||
|
room, e2eRoomInfo, event.getType(), event.getContent()
|
||||||
|
);
|
||||||
|
event.makeEncrypted("m.room.encrypted", encryptedContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {module:models/room.Room} room
|
||||||
|
* @param {object} e2eRoomInfo
|
||||||
|
* @param {string} eventType
|
||||||
|
* @param {object} content
|
||||||
|
*
|
||||||
|
* @return {object} new event body
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
Crypto.prototype._encryptMessage = function(room, e2eRoomInfo, eventType, content) {
|
||||||
|
if (e2eRoomInfo.algorithm !== OLM_ALGORITHM) {
|
||||||
|
throw new Error("Unknown end-to-end algorithm: " + e2eRoomInfo.algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!room) {
|
||||||
|
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick the list of recipients based on the membership list.
|
||||||
|
//
|
||||||
|
// TODO: there is a race condition here! What if a new user turns up
|
||||||
|
// just as you are sending a secret message?
|
||||||
|
|
||||||
|
var users = utils.map(room.getJoinedMembers(), function(u) {
|
||||||
|
return u.userId;
|
||||||
|
});
|
||||||
|
|
||||||
|
var participantKeys = [];
|
||||||
|
for (var i = 0; i < users.length; ++i) {
|
||||||
|
var userId = users[i];
|
||||||
|
var devices = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
|
for (var deviceId in devices) {
|
||||||
|
if (devices.hasOwnProperty(deviceId)) {
|
||||||
|
var dev = devices[deviceId];
|
||||||
|
if (dev.verified === DeviceVerification.BLOCKED) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var keyId in dev.keys) {
|
||||||
|
if (keyId.indexOf("curve25519:") === 0) {
|
||||||
|
participantKeys.push(dev.keys[keyId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
participantKeys.sort();
|
||||||
|
var participantHash = ""; // Olm.sha256(participantKeys.join());
|
||||||
|
var payloadJson = {
|
||||||
|
room_id: room.roomId,
|
||||||
|
type: eventType,
|
||||||
|
fingerprint: participantHash,
|
||||||
|
sender_device: this._deviceId,
|
||||||
|
content: content
|
||||||
|
};
|
||||||
|
var ciphertext = {};
|
||||||
|
var payloadString = JSON.stringify(payloadJson);
|
||||||
|
for (i = 0; i < participantKeys.length; ++i) {
|
||||||
|
var deviceKey = participantKeys[i];
|
||||||
|
if (deviceKey == this._olmDevice.deviceCurve25519Key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
|
||||||
|
// Use the session with the lowest ID.
|
||||||
|
sessionIds.sort();
|
||||||
|
if (sessionIds.length === 0) {
|
||||||
|
// If we don't have a session for a device then
|
||||||
|
// we can't encrypt a message for it.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var sessionId = sessionIds[0];
|
||||||
|
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
|
||||||
|
ciphertext[deviceKey] = this._olmDevice.encryptMessage(
|
||||||
|
deviceKey, sessionId, payloadString
|
||||||
|
);
|
||||||
|
}
|
||||||
|
var encryptedContent = {
|
||||||
|
algorithm: e2eRoomInfo.algorithm,
|
||||||
|
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||||
|
ciphertext: ciphertext
|
||||||
|
};
|
||||||
|
return encryptedContent;
|
||||||
|
};
|
||||||
|
|
||||||
|
function DecryptionError(msg) {
|
||||||
|
this.message = msg;
|
||||||
|
}
|
||||||
|
utils.inherits(DecryptionError, Error);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when decryption fails
|
||||||
|
*/
|
||||||
|
Crypto.DecryptionError = DecryptionError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt a received event
|
||||||
|
*
|
||||||
|
* @param {object} event raw event
|
||||||
|
*
|
||||||
|
* @return {object} decrypted payload (with properties 'type', 'content')
|
||||||
|
*
|
||||||
|
* @raises {DecryptionError} if there is a problem decrypting the event
|
||||||
|
*/
|
||||||
|
Crypto.prototype.decryptEvent = function(event) {
|
||||||
|
var content = event.content;
|
||||||
|
if (content.algorithm !== OLM_ALGORITHM) {
|
||||||
|
throw new DecryptionError("Unknown algorithm");
|
||||||
|
}
|
||||||
|
|
||||||
|
var deviceKey = content.sender_key;
|
||||||
|
var ciphertext = content.ciphertext;
|
||||||
|
|
||||||
|
if (!ciphertext) {
|
||||||
|
throw new DecryptionError("Missing ciphertext");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(this._olmDevice.deviceCurve25519Key in content.ciphertext)) {
|
||||||
|
throw new DecryptionError("Not included in recipients");
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = content.ciphertext[this._olmDevice.deviceCurve25519Key];
|
||||||
|
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
|
||||||
|
var payloadString = null;
|
||||||
|
var foundSession = false;
|
||||||
|
for (var i = 0; i < sessionIds.length; i++) {
|
||||||
|
var sessionId = sessionIds[i];
|
||||||
|
var res = this._olmDevice.decryptMessage(
|
||||||
|
deviceKey, sessionId, message.type, message.body
|
||||||
|
);
|
||||||
|
payloadString = res.payload;
|
||||||
|
if (payloadString) {
|
||||||
|
console.log("decrypted with sessionId " + sessionId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.matchesInbound) {
|
||||||
|
// this was a prekey message which matched this session; don't
|
||||||
|
// create a new session.
|
||||||
|
foundSession = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === 0 && !foundSession && payloadString === null) {
|
||||||
|
try {
|
||||||
|
payloadString = this._olmDevice.createInboundSession(
|
||||||
|
deviceKey, message.type, message.body
|
||||||
|
);
|
||||||
|
console.log("created new inbound sesion");
|
||||||
|
} catch (e) {
|
||||||
|
// Failed to decrypt with a new session.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Check the sender user id matches the sender key.
|
||||||
|
if (payloadString !== null) {
|
||||||
|
return JSON.parse(payloadString);
|
||||||
|
} else {
|
||||||
|
throw new DecryptionError("Bad Encrypted Message");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** */
|
||||||
|
module.exports = Crypto;
|
||||||
@@ -26,6 +26,7 @@ describe("MatrixClient", function() {
|
|||||||
client = sdk.createClient({
|
client = sdk.createClient({
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
deviceId: "aliceDevice",
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
store: store,
|
store: store,
|
||||||
sessionStore: sessionStore,
|
sessionStore: sessionStore,
|
||||||
|
|||||||
Reference in New Issue
Block a user