} keys a map from
* <key type>:<id> -> <base64-encoded key>>
*
- * @property {DeviceVerification} verified whether the device has been
+ * @property {module:crypto~DeviceVerification} verified whether the device has been
* verified by the user
*
* @property {Object} unsigned additional data from the homeserver
@@ -119,6 +123,7 @@ DeviceInfo.prototype.getDisplayname = function() {
/**
* Cryptography bits
*
+ * @alias module:crypto.Crypto
* @constructor
* @alias module:crypto
*
@@ -137,10 +142,10 @@ function Crypto(baseApis, sessionStore, userId, deviceId) {
this._userId = userId;
this._deviceId = deviceId;
- this._cryptoAlgorithms = [];
-
this._olmDevice = new OlmDevice(sessionStore);
- this._cryptoAlgorithms = [OLM_ALGORITHM];
+
+ // EncryptionAlgorithm instance for each room
+ this._roomAlgorithms = {};
// build our device keys: these will later be uploaded
this._deviceKeys = {};
@@ -152,7 +157,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId) {
// add our own deviceinfo to the sessionstore
var deviceInfo = {
keys: this._deviceKeys,
- algorithms: this._cryptoAlgorithms,
+ algorithms: [OLM_ALGORITHM],
verified: DeviceVerification.VERIFIED,
};
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
@@ -205,7 +210,7 @@ function _uploadDeviceKeys(crypto) {
var deviceId = crypto._deviceId;
var deviceKeys = {
- algorithms: crypto._cryptoAlgorithms,
+ algorithms: [OLM_ALGORITHM],
device_id: deviceId,
keys: crypto._deviceKeys,
user_id: userId,
@@ -388,7 +393,6 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
delete deviceResult.signatures;
var json = anotherjson.stringify(deviceResult);
- console.log("msg:", json);
try {
_olmDevice.verifySignature(signKey, json, signature);
} catch (e) {
@@ -599,8 +603,6 @@ Crypto.prototype.isSenderKeyVerified = function(userId, algorithm, sender_key) {
* @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);
@@ -608,23 +610,24 @@ Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
console.error("Ignoring m.room.encryption event which requests " +
"a change of config in " + roomId);
- return;
+ return q();
}
}
- if (config.algorithm !== OLM_ALGORITHM) {
- throw new Error("Unknown algorithm: " + config.algorithm);
+ var AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
+ if (!AlgClass) {
+ throw new Error("Unable to encrypt with " + config.algorithm);
}
// remove spurious keys
config = {
- algorithm: OLM_ALGORITHM,
+ algorithm: config.algorithm,
};
this._sessionStore.storeEndToEndRoom(roomId, config);
- return self.downloadKeys(roomMembers, true).then(function(res) {
- return self._ensureOlmSessionsForUsers(roomMembers);
- });
+ var alg = new AlgClass(this._deviceId, this, this._olmDevice);
+ this._roomAlgorithms[roomId] = alg;
+ return alg.initRoomEncryption(roomMembers);
};
/**
@@ -636,10 +639,8 @@ Crypto.prototype.setRoomEncryption = function(roomId, config, roomMembers) {
* 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) {
+Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
var devicesWithoutSession = [];
var userWithoutDevices = [];
for (var i = 0; i < users.length; ++i) {
@@ -710,7 +711,7 @@ Crypto.prototype._ensureOlmSessionsForUsers = function(users) {
* @return {bool} whether encryption is enabled.
*/
Crypto.prototype.isRoomEncrypted = function(roomId) {
- return (this._sessionStore.getEndToEndRoom(roomId) && true) || false;
+ return Boolean(this._roomAlgorithms[roomId]);
};
@@ -718,7 +719,7 @@ Crypto.prototype.isRoomEncrypted = function(roomId) {
* 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
+ * @param {module:models/room} room destination room
*/
Crypto.prototype.encryptEventIfNeeded = function(event, room) {
if (event.isEncrypted()) {
@@ -735,112 +736,27 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
var roomId = event.getRoomId();
- var e2eRoomInfo = this._sessionStore.getEndToEndRoom(roomId);
- if (!e2eRoomInfo || !e2eRoomInfo.algorithm) {
+ var alg = this._roomAlgorithms[roomId];
+ if (!alg) {
// not encrypting messages in this room
+
+ // check that the HS hasn't hidden the crypto event
+ if (this._sessionStore.getEndToEndRoom(roomId)) {
+ throw new Error(
+ "Room was previously configured to use encryption, but is " +
+ "no longer. Perhaps the homeserver is hiding the " +
+ "configuration event."
+ );
+ }
return;
}
- var encryptedContent = this._encryptMessage(
- room, e2eRoomInfo, event.getType(), event.getContent()
+ var encryptedContent = alg.encryptMessage(
+ room, 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.getStoredDevicesForUser(userId);
- for (var j = 0; j < devices.length; ++j) {
- var dev = devices[j];
- if (dev.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
*
@@ -848,66 +764,23 @@ Crypto.DecryptionError = DecryptionError;
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
- * @raises {DecryptionError} if there is a problem decrypting the event
+ * @raises {algorithms.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");
+ var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
+ if (!AlgClass) {
+ throw new algorithms.DecryptionError("Unable to decrypt " + content.algorithm);
}
+ var alg = new AlgClass(this._deviceId, this, this._olmDevice);
+ return alg.decryptEvent(event);
};
+/**
+ * @see module:crypto-algorithms/base.DecryptionError
+ */
+Crypto.DecryptionError = algorithms.DecryptionError;
+
+
/** */
module.exports = Crypto;
diff --git a/lib/matrix.js b/lib/matrix.js
index 8612527e9..4a8528cd9 100644
--- a/lib/matrix.js
+++ b/lib/matrix.js
@@ -30,7 +30,7 @@ module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
-/** The {@link module:models/room~Room|Room} class. */
+/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
diff --git a/lib/models/room.js b/lib/models/room.js
index cc8e955a5..7bf9b127f 100644
--- a/lib/models/room.js
+++ b/lib/models/room.js
@@ -75,6 +75,7 @@ function synthesizeReceipt(userId, event, receiptType) {
* map from event_id to timeline and index.
*
* @constructor
+ * @alias module:models/room
* @param {string} roomId Required. The ID of this room.
* @param {Object=} opts Configuration options
* @param {*} opts.storageToken Optional. The token which a data store can use
@@ -85,7 +86,7 @@ function synthesizeReceipt(userId, event, receiptType) {
* appear in a room's timeline. If "chronological", messages will appear
* in the timeline when the call to sendEvent was made. If
* "detached", pending messages will appear in a separate list,
- * accessbile via {@link module:models/room~Room#getPendingEvents}. Default:
+ * accessbile via {@link module:models/room#getPendingEvents}. Default:
* "chronological".
*
* @param {boolean} [opts.timelineSupport = false] Set to true to enable improved
diff --git a/lib/timeline-window.js b/lib/timeline-window.js
index 539429982..89acec9dc 100644
--- a/lib/timeline-window.js
+++ b/lib/timeline-window.js
@@ -41,7 +41,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
* Construct a TimelineWindow.
*
* This abstracts the separate timelines in a Matrix {@link
- * module:models/room~Room|Room} into a single iterable thing. It keeps track of
+ * module:models/room|Room} into a single iterable thing. It keeps track of
* the start and endpoints of the window, which can be advanced with the help
* of pagination requests.
*