diff --git a/lib/base-apis.js b/lib/base-apis.js
index 325fdf4ee..40102c191 100644
--- a/lib/base-apis.js
+++ b/lib/base-apis.js
@@ -705,7 +705,7 @@ MatrixBaseApis.prototype.search = function(opts, callback) {
* @param {Object} content body of upload request
*
* @param {Object=} opts
-
+ *
* @param {string=} opts.device_id explicit device_id to use for upload
* (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
// ==========================
diff --git a/lib/client.js b/lib/client.js
index 2e1f5e4c5..a150fb79a 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -24,7 +24,6 @@ var PushProcessor = require('./pushprocessor');
var EventEmitter = require("events").EventEmitter;
var q = require("q");
var url = require('url');
-var anotherjson = require('another-json');
var httpApi = require("./http-api");
var MatrixEvent = require("./models/event").MatrixEvent;
@@ -44,20 +43,12 @@ var SCROLLBACK_DELAY_MS = 3000;
var CRYPTO_ENABLED = false;
try {
- var OlmDevice = require("./OlmDevice");
+ var Crypto = require("./crypto");
CRYPTO_ENABLED = true;
} catch (e) {
// 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
* custom modules. Normally, {@link createClient} should be used
@@ -116,7 +107,6 @@ function MatrixClient(opts) {
MatrixBaseApis.call(this, opts);
this.store = opts.store || new StubStore();
- this.sessionStore = opts.sessionStore || null;
this.deviceId = opts.deviceId || null;
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName || "js-sdk device";
@@ -126,38 +116,6 @@ function MatrixClient(opts) {
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;
if (this.scheduler) {
var self = this;
@@ -192,6 +150,18 @@ function MatrixClient(opts) {
this._txnCtr = 0;
this.timelineSupport = Boolean(opts.timelineSupport);
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.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
@@ -291,7 +261,7 @@ MatrixClient.prototype.retryImmediately = function() {
* @return {boolean} True if end-to-end is enabled.
*/
MatrixClient.prototype.isCryptoEnabled = function() {
- return CRYPTO_ENABLED && this.sessionStore !== null;
+ return this._crypto !== null;
};
@@ -302,105 +272,26 @@ MatrixClient.prototype.isCryptoEnabled = function() {
* disabled.
*/
MatrixClient.prototype.getDeviceEd25519Key = function() {
- if (!this._olmDevice) {
+ if (!this._crypto) {
return null;
}
- return this._olmDevice.deviceEd25519Key;
+ return this._crypto.getDeviceEd25519Key();
};
/**
* 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
- * @param {object} deferred A deferred to resolve when the keys are uploaded.
* @return {object} A promise that will resolve when the keys are uploaded.
*/
-MatrixClient.prototype.uploadKeys = function(maxKeys, deferred) {
- 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);
- }
+MatrixClient.prototype.uploadKeys = function(maxKeys) {
+ if (this._crypto === null) {
+ throw new Error("End-to-end encryption disabled");
+ }
- if (numberToGenerate <= 0) {
- return;
- }
-
- self._olmDevice.generateOneTimeKeys(numberToGenerate);
- return _uploadOneTimeKeys(self);
- });
+ return this._crypto.uploadKeys(maxKeys);
};
-// 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
* store.
@@ -408,126 +299,15 @@ function _uploadOneTimeKeys(client) {
* @param {bool} forceDownload Always download the keys even if cached.
*
* @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) {
- if (this.sessionStore === null) {
+ if (this._crypto === null) {
return q.reject(new Error("End-to-end encryption disabled"));
}
- var stored = {};
- 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;
- });
+ return this._crypto.downloadKeys(userIds, forceDownload);
};
-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
*
@@ -537,37 +317,10 @@ function _updateStoredDeviceKeysForUser(userId, userStore, userResult) {
* "key", and "display_name" parameters.
*/
MatrixClient.prototype.listDeviceKeys = function(userId) {
- if (!this.sessionStore) {
- return [];
+ if (this._crypto === null) {
+ throw new Error("End-to-end encryption disabled");
}
- 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;
+ return this._crypto.listDeviceKeys(userId);
};
/**
@@ -608,40 +361,13 @@ MatrixClient.prototype.setDeviceBlocked = function(userId, deviceId, blocked) {
};
function _setDeviceVerification(client, userId, deviceId, verified, blocked) {
- if (!client.sessionStore) {
+ if (!client._crypto) {
throw new Error("End-to-End encryption disabled");
}
-
- var devices = client.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;
- client.sessionStore.storeEndToEndDevicesForUser(userId, devices);
-
- client.emit("deviceVerificationChanged", userId, deviceId, dev);
+ client._crypto.setDeviceVerification(userId, deviceId, verified, blocked);
+ client.emit("deviceVerificationChanged", userId, deviceId);
}
-
/**
* 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}.
*/
MatrixClient.prototype.isEventSenderVerified = function(event) {
- if (!this.sessionStore) {
+ if (!this._crypto) {
return false;
}
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;
+
if (!sender_key) {
return false;
}
- for (var deviceId in devices) {
- if (!devices.hasOwnProperty(deviceId)) {
- continue;
- }
+ var algorithm = cryptoContent.algorithm;
- 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.verified == DeviceVerification.VERIFIED;
- }
- }
- }
-
- // doesn't match a known device
- return false;
+ return this._crypto.isSenderKeyVerified(
+ event.getSender(), algorithm, sender_key
+ );
};
/**
@@ -714,24 +412,13 @@ function setupCryptoEventHandler(client) {
function onCryptoEvent(client, event) {
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 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 {
client.setRoomEncryption(roomId, content).done();
} catch (e) {
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.
*/
MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
- if (!this._olmDevice) {
+ if (!this._crypto) {
throw new Error("End-to-End encryption disabled");
}
- var self = this;
-
- if (config.algorithm === OLM_ALGORITHM) {
- // remove spurious keys
- 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);
- });
+ var roomMembers = [];
+ var room = this.getRoom(roomId);
+ if (!room) {
+ console.warn("Enabling encryption in unknown room " + roomId);
} else {
- throw new Error("Unknown algorithm: " + config.algorithm);
- }
-};
-
-/**
- * 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 missingUsers (a list of users with no known
- * olm devices), and missingDevices 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]);
- }
- }
- }
- }
+ roomMembers = utils.map(room.getJoinedMembers(), function(u) {
+ return u.userId;
+ });
}
- if (devicesWithoutSession.length === 0) {
- 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);
- }
+ return this._crypto.setRoomEncryption(roomId, config, roomMembers);
};
/**
@@ -877,13 +452,49 @@ MatrixClient.prototype.disableRoomEncryption = function(roomId) {
* @return {bool} whether encryption is enabled.
*/
MatrixClient.prototype.isRoomEncrypted = function(roomId) {
- if (CRYPTO_ENABLED && this.sessionStore !== null) {
- return (this.sessionStore.getEndToEndRoom(roomId) && true) || false;
- } else {
+ if (!this._crypto) {
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.
@@ -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
// adds the event to the queue, or sends it
// 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
// same code path.
return q().then(function() {
- _encryptEventIfNeeded(client, event);
+ if (client._crypto) {
+ client._crypto.encryptEventIfNeeded(event, room);
+ }
var promise;
// this event may be queued
@@ -3062,8 +2470,8 @@ MatrixClient.prototype.startClient = function(opts) {
this._clientOpts = opts;
- if (this._olmDevice) {
- this.uploadKeys(5).done();
+ if (this._crypto) {
+ this._crypto.uploadKeys(5).done();
}
// periodically poll for turn servers if we support voip
@@ -3514,7 +2922,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
*
* @event module:client~MatrixClient#"deviceVerificationChanged"
* @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
*/
/**
diff --git a/lib/crypto.js b/lib/crypto.js
new file mode 100644
index 000000000..cbf43a299
--- /dev/null
+++ b/lib/crypto.js
@@ -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 missingUsers (a list of users with no known
+ * olm devices), and missingDevices 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;
diff --git a/spec/integ/matrix-client-methods.spec.js b/spec/integ/matrix-client-methods.spec.js
index 5f3584460..3b0dfb8d5 100644
--- a/spec/integ/matrix-client-methods.spec.js
+++ b/spec/integ/matrix-client-methods.spec.js
@@ -26,6 +26,7 @@ describe("MatrixClient", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
+ deviceId: "aliceDevice",
accessToken: accessToken,
store: store,
sessionStore: sessionStore,