1
0
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:
Richard van der Hoff
2016-08-04 09:03:39 +01:00
parent d9867ba458
commit ad6eec329d
4 changed files with 914 additions and 684 deletions

View File

@@ -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
// ========================== // ==========================

View File

@@ -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 &lt;key type&gt;:&lt;id&gt; -> 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
View 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 &lt;key type&gt;:&lt;id&gt; -> 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;

View File

@@ -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,