1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-12-19 10:22:30 +03:00

Merge branch 'release-v0.7.0'

This commit is contained in:
Matthew Hodgson
2016-11-19 01:56:11 +02:00
25 changed files with 1468 additions and 518 deletions

3
.travis.yml Normal file
View File

@@ -0,0 +1,3 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.

View File

@@ -1,3 +1,38 @@
Changes in [0.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.0) (2016-11-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4...v0.7.0)
* Avoid a packetstorm of device queries on startup
[\#297](https://github.com/matrix-org/matrix-js-sdk/pull/297)
* E2E: Check devices to share keys with on each send
[\#295](https://github.com/matrix-org/matrix-js-sdk/pull/295)
* Apply unknown-keyshare mitigations
[\#296](https://github.com/matrix-org/matrix-js-sdk/pull/296)
* distinguish unknown users from deviceless users
[\#294](https://github.com/matrix-org/matrix-js-sdk/pull/294)
* Allow starting client with initialSyncLimit = 0
[\#293](https://github.com/matrix-org/matrix-js-sdk/pull/293)
* Make timeline-window _unpaginate public and rename to unpaginate
[\#289](https://github.com/matrix-org/matrix-js-sdk/pull/289)
* Send a STOPPED sync updated after call to stopClient
[\#286](https://github.com/matrix-org/matrix-js-sdk/pull/286)
* Fix bug in verifying megolm event senders
[\#292](https://github.com/matrix-org/matrix-js-sdk/pull/292)
* Handle decryption of events after they arrive
[\#288](https://github.com/matrix-org/matrix-js-sdk/pull/288)
* Fix examples.
[\#287](https://github.com/matrix-org/matrix-js-sdk/pull/287)
* Add a travis.yml
[\#278](https://github.com/matrix-org/matrix-js-sdk/pull/278)
* Encrypt all events, including 'm.call.*'
[\#277](https://github.com/matrix-org/matrix-js-sdk/pull/277)
* Ignore reshares of known megolm sessions
[\#276](https://github.com/matrix-org/matrix-js-sdk/pull/276)
* Log to the console on unknown session
[\#274](https://github.com/matrix-org/matrix-js-sdk/pull/274)
* Make it easier for SDK users to wrap prevailing the 'request' function
[\#273](https://github.com/matrix-org/matrix-js-sdk/pull/273)
Changes in [0.6.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4) (2016-11-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.2...v0.6.4)

View File

@@ -1 +1 @@
../../../dist/browser-matrix-dev.js
../../../dist/browser-matrix.js

View File

@@ -135,11 +135,15 @@ rl.on('line', function(line) {
// ==== END User input
// show the room list after syncing.
matrixClient.on("syncComplete", function() {
setRoomList();
printRoomList();
printHelp();
rl.prompt();
matrixClient.on("sync", function(state, prevState, data) {
switch (state) {
case "PREPARED":
setRoomList();
printRoomList();
printHelp();
rl.prompt();
break;
}
});
matrixClient.on("Room", function() {

View File

@@ -44,7 +44,15 @@ window.onload = function() {
disableButtons(true, true, true);
};
client.on("syncComplete", function () {
matrixClient.on("sync", function(state, prevState, data) {
switch (state) {
case "PREPARED":
syncComplete();
break;
}
});
function syncComplete() {
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
disableButtons(false, true, true);
@@ -85,5 +93,5 @@ client.on("syncComplete", function () {
call = c;
addListeners(call);
});
});
}
client.startClient();

View File

@@ -1 +1 @@
../../../dist/browser-matrix-dev.js
../../../dist/browser-matrix.js

View File

@@ -351,7 +351,7 @@ MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getStoredDevicesForUser(userId);
return this._crypto.getStoredDevicesForUser(userId) || [];
};
@@ -463,38 +463,31 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) {
* Decrypt a received event according to the algorithm specified in the event.
*
* @param {MatrixClient} client
* @param {object} raw event
*
* @return {MatrixEvent}
* @param {MatrixEvent} event
*/
function _decryptEvent(client, event) {
if (!client._crypto) {
return _badEncryptedMessage(event, "**Encryption not enabled**");
_badEncryptedMessage(event, "**Encryption not enabled**");
return;
}
var decryptionResult;
try {
decryptionResult = client._crypto.decryptEvent(event);
client._crypto.decryptEvent(event);
} catch (e) {
if (!(e instanceof Crypto.DecryptionError)) {
throw e;
}
return _badEncryptedMessage(event, "**" + e.message + "**");
_badEncryptedMessage(event, "**" + e.message + "**");
return;
}
return new MatrixEvent(
event, decryptionResult.payload,
decryptionResult.keysProved,
decryptionResult.keysClaimed
);
}
function _badEncryptedMessage(event, reason) {
return new MatrixEvent(event, {
event.setClearData({
type: "m.room.message",
content: {
msgtype: "m.bad.encrypted",
body: reason,
content: event.content,
},
});
}
@@ -2829,10 +2822,11 @@ function _resolve(callback, defer, res) {
function _PojoToMatrixEventMapper(client) {
function mapper(plainOldJsObject) {
if (plainOldJsObject.type === "m.room.encrypted") {
return _decryptEvent(client, plainOldJsObject);
var event = new MatrixEvent(plainOldJsObject);
if (event.isEncrypted()) {
_decryptEvent(client, event);
}
return new MatrixEvent(plainOldJsObject);
return event;
}
return mapper;
}
@@ -2872,6 +2866,10 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
/**
* Fires whenever the SDK receives a new event.
* <p>
* This is only fired for live events received via /sync - it is not fired for
* events received over context, search, or pagination APIs.
*
* @event module:client~MatrixClient#"event"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @example

View File

@@ -627,6 +627,24 @@ OlmDevice.prototype.addInboundGroupSession = function(
roomId, senderKey, sessionId, sessionKey, keysClaimed
) {
var self = this;
/* if we already have this session, consider updating it */
function updateSession(session) {
console.log("Update for megolm session " + senderKey + "/" + sessionId);
// for now we just ignore updates. TODO: implement something here
return true;
}
var r = this._getInboundGroupSession(
roomId, senderKey, sessionId, updateSession
);
if (r !== null) {
return;
}
// new session.
var session = new Olm.InboundGroupSession();
try {
session.create(sessionKey);

View File

@@ -88,15 +88,6 @@ EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership
) {};
/**
* Called when a new device announces itself in the room
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
*/
EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
/**
* base type for decryption implementations
*
@@ -105,11 +96,16 @@ EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
*
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
var DecryptionAlgorithm = function(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._roomId = params.roomId;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;

View File

@@ -38,12 +38,17 @@ var base = require("./base");
* @property {Number} creationTime when the session was created (ms since the epoch)
* @property {module:client.Promise?} sharePromise If a share operation is in progress,
* a promise which resolves when it is complete.
*
* @property {object} sharedWithDevices
* devices with which we have shared the session key
* userId -> {deviceId -> msgindex}
*/
function OutboundSessionInfo(sessionId) {
this.sessionId = sessionId;
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharePromise = null;
this.sharedWithDevices = {};
}
@@ -90,11 +95,6 @@ function MegolmEncryption(params) {
// case _outboundSession.sharePromise will be non-null.)
this._outboundSession = null;
// devices which have joined since we last sent a message.
// userId -> {deviceId -> true}, or
// userId -> true
this._devicesPendingKeyShare = {};
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
@@ -134,32 +134,55 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
return session.sharePromise;
}
// no share in progress: check for new devices
var shareMap = this._devicesPendingKeyShare;
this._devicesPendingKeyShare = {};
// no share in progress: check if we need to share with any devices
var prom = this._getDevicesInRoom(room).then(function(devicesInRoom) {
var shareMap = {};
// check each user is (still) a member of the room
for (var userId in shareMap) {
if (!shareMap.hasOwnProperty(userId)) {
continue;
for (var userId in devicesInRoom) {
if (!devicesInRoom.hasOwnProperty(userId)) {
continue;
}
var userDevices = devicesInRoom[userId];
for (var deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
var deviceInfo = userDevices[deviceId];
if (deviceInfo.isBlocked()) {
continue;
}
var key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (
!session.sharedWithDevices[userId] ||
session.sharedWithDevices[userId][deviceId] === undefined
) {
shareMap[userId] = shareMap[userId] || [];
shareMap[userId].push(deviceInfo);
}
}
}
// XXX what about rooms where invitees can see the content?
var member = room.getMember(userId);
if (member.membership !== "join") {
delete shareMap[userId];
}
}
session.sharePromise = this._shareKeyWithDevices(
session.sessionId, shareMap
).finally(function() {
return self._shareKeyWithDevices(
session, shareMap
);
}).finally(function() {
session.sharePromise = null;
}).then(function() {
return session;
});
return session.sharePromise;
session.sharePromise = prom;
return prom;
};
/**
@@ -178,95 +201,53 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
);
// we're going to share the key with all current members of the room,
// so we can reset this.
this._devicesPendingKeyShare = {};
var session = new OutboundSessionInfo(session_id);
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var shareMap = {};
for (var i = 0; i < roomMembers.length; i++) {
var userId = roomMembers[i];
shareMap[userId] = true;
}
var self = this;
// TODO: we need to give the user a chance to block any devices or users
// before we send them the keys; it's too late to download them here.
session.sharePromise = this._crypto.downloadKeys(
roomMembers, false
).then(function(res) {
return self._shareKeyWithDevices(session_id, shareMap);
}).then(function() {
return session;
}).finally(function() {
session.sharePromise = null;
});
return session;
return new OutboundSessionInfo(session_id);
};
/**
* @private
*
* @param {string} session_id
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
* Map from userid to either: true (meaning this is a new user in the room,
* so all of his devices need the keys); or a map from deviceid to true
* (meaning this user has one or more new devices, which need the keys).
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) {
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
var self = this;
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
var key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
var payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session_id,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
}
};
// we downloaded the user's device list when they joined the room, or when
// the new device announced itself, so there is no need to do so now.
var contentMap = {};
return self._crypto.ensureOlmSessionsForUsers(
utils.keys(shareMap)
return olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser
).then(function(devicemap) {
var contentMap = {};
var haveTargets = false;
for (var userId in devicemap) {
if (!devicemap.hasOwnProperty(userId)) {
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
var devicesToShareWith = shareMap[userId];
var devicesToShareWith = devicesByUser[userId];
var sessionResults = devicemap[userId];
for (var deviceId in sessionResults) {
if (!sessionResults.hasOwnProperty(deviceId)) {
continue;
}
if (devicesToShareWith === true) {
// all devices
} else if (!devicesToShareWith[deviceId]) {
// not a new device
continue;
}
for (var i = 0; i < devicesToShareWith.length; i++) {
var deviceInfo = devicesToShareWith[i];
var deviceId = deviceInfo.deviceId;
var sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
@@ -288,8 +269,6 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
"sharing keys with device " + userId + ":" + deviceId
);
var deviceInfo = sessionResult.device;
var encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
@@ -321,6 +300,27 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
// TODO: retries
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
}).then(function() {
// Add the devices we have shared with to session.sharedWithDevices.
//
// we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key
// for dead devices on every message.
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
if (!session.sharedWithDevices[userId]) {
session.sharedWithDevices[userId] = {};
}
var devicesToShareWith = devicesByUser[userId];
for (var i = 0; i < devicesToShareWith.length; i++) {
var deviceInfo = devicesToShareWith[i];
session.sharedWithDevices[userId][deviceInfo.deviceId] =
key.chain_index;
}
}
});
};
@@ -369,20 +369,9 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
* @param {string=} oldMembership previous membership
*/
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
// if we haven't yet made a session, there's nothing to do here.
if (!this._outboundSession) {
return;
}
var newMembership = member.membership;
if (newMembership === 'join') {
this._onNewRoomMember(member.userId);
return;
}
if (newMembership === 'invite' && oldMembership !== 'join') {
// we don't (yet) share keys with invited members, so nothing to do yet
if (newMembership === 'join' || newMembership === 'invite') {
return;
}
@@ -396,44 +385,26 @@ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembers
};
/**
* handle a new user joining a room
* Get the list of devices for all users in the room
*
* @param {string} userId new member
*/
MegolmEncryption.prototype._onNewRoomMember = function(userId) {
// make sure we have a list of this user's devices. We are happy to use a
// cached version here: we assume that if we already have a list of the
// user's devices, then we already share an e2e room with them, which means
// that they will have announced any new devices via an m.new_device.
this._crypto.downloadKeys([userId], false).done();
// also flag this user up for needing a keyshare.
this._devicesPendingKeyShare[userId] = true;
};
/**
* @inheritdoc
* @param {module:models/room} room
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
* @return {module:client.Promise} Promise which resolves to a map
* from userId to deviceId to deviceInfo
*/
MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
var d = this._devicesPendingKeyShare[userId];
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// XXX what about rooms where invitees can see the content?
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
if (d === true) {
// we already want to share keys with all devices for this user
return;
}
if (!d) {
this._devicesPendingKeyShare[userId] = d = {};
}
d[deviceId] = true;
// We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via
// an m.new_device.
return this._crypto.downloadKeys(roomMembers, false);
};
/**
* Megolm decryption implementation
*
@@ -445,13 +416,17 @@ MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
*/
function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
// events which we couldn't decrypt due to unknown sessions / indexes: map from
// senderKey|sessionId to list of MatrixEvents
this._pendingEvents = {};
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {object} event raw event
* @param {MatrixEvent} event
*
* @return {null} The event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
@@ -460,7 +435,7 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
* problem decrypting the event
*/
MegolmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
var content = event.getWireContent();
if (!content.sender_key || !content.session_id ||
!content.ciphertext
@@ -471,14 +446,19 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
var res;
try {
res = this._olmDevice.decryptGroupMessage(
event.room_id, content.sender_key, content.session_id, content.ciphertext
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext
);
} catch (e) {
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this._addEventToPendingList(event);
}
throw new base.DecryptionError(e);
}
if (res === null) {
return null;
// We've got a message for a session we don't have.
this._addEventToPendingList(event);
throw new base.DecryptionError("Unknown inbound session id");
}
var payload = JSON.parse(res.result);
@@ -486,17 +466,31 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
// belt-and-braces check that the room id matches that indicated by the HS
// (this is somewhat redundant, since the megolm session is scoped to the
// room, so neither the sender nor a MITM can lie about the room_id).
if (payload.room_id !== event.room_id) {
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
"Message intended for room " + payload.room_id
);
}
return {
payload: payload,
keysClaimed: res.keysClaimed,
keysProved: res.keysProved,
};
event.setClearData(payload, res.keysProved, res.keysClaimed);
};
/**
* Add an event to the list of those we couldn't decrypt the first time we
* saw them.
*
* @private
*
* @param {module:models/event.MatrixEvent} event
*/
MegolmDecryption.prototype._addEventToPendingList = function(event) {
var content = event.getWireContent();
var k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
this._pendingEvents[k] = [];
}
this._pendingEvents[k].push(event);
};
/**
@@ -520,6 +514,22 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, event.getKeysClaimed()
);
var k = event.getSenderKey() + "|" + content.session_id;
var pending = this._pendingEvents[k];
if (pending) {
// have another go at decrypting events sent with this session.
delete this._pendingEvents[k];
for (var i = 0; i < pending.length; i++) {
try {
this.decryptEvent(pending[i]);
console.log("successful re-decryption of", pending[i]);
} catch (e) {
console.log("Still can't decrypt", pending[i], e.stack || e);
}
}
}
};
base.registerAlgorithm(

View File

@@ -151,15 +151,13 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {object} event raw event
*
* @return {module:crypto.DecryptionResult} decryption result
* @param {MatrixEvent} event
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
OlmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
var content = event.getWireContent();
var deviceKey = content.sender_key;
var ciphertext = content.ciphertext;
@@ -178,7 +176,7 @@ OlmDecryption.prototype.decryptEvent = function(event) {
} catch (e) {
console.warn(
"Failed to decrypt Olm event (id=" +
event.event_id + ") from " + deviceKey +
event.getId() + ") from " + deviceKey +
": " + e.message
);
throw new base.DecryptionError("Bad Encrypted Message");
@@ -188,15 +186,9 @@ OlmDecryption.prototype.decryptEvent = function(event) {
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient === undefined) {
// older versions of riot did not set this field, so we cannot make
// this check. TODO: kill this off once our users have updated
if (payload.recipient != this._userId) {
console.warn(
"Olm event (id=" + event.event_id + ") contains no 'recipient' " +
"property; cannot prevent unknown-key attack");
} else if (payload.recipient != this._userId) {
console.warn(
"Event " + event.event_id + ": Intended recipient " +
"Event " + event.getId() + ": Intended recipient " +
payload.recipient + " does not match our id " + this._userId
);
throw new base.DecryptionError(
@@ -204,15 +196,10 @@ OlmDecryption.prototype.decryptEvent = function(event) {
);
}
if (payload.recipient_keys === undefined) {
// ditto
console.warn(
"Olm event (id=" + event.event_id + ") contains no " +
"'recipient_keys' property; cannot prevent unknown-key attack");
} else if (payload.recipient_keys.ed25519 !=
if (payload.recipient_keys.ed25519 !=
this._olmDevice.deviceEd25519Key) {
console.warn(
"Event " + event.event_id + ": Intended recipient ed25519 key " +
"Event " + event.getId() + ": Intended recipient ed25519 key " +
payload.recipient_keys.ed25519 + " did not match ours"
);
throw new base.DecryptionError("Message not intended for this device");
@@ -222,15 +209,10 @@ OlmDecryption.prototype.decryptEvent = function(event) {
// avoid people masquerading as others.
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender === undefined) {
// ditto
if (payload.sender != event.getSender()) {
console.warn(
"Olm event (id=" + event.event_id + ") contains no " +
"'sender' property; cannot prevent unknown-key attack");
} else if (payload.sender != event.sender) {
console.warn(
"Event " + event.event_id + ": original sender " + payload.sender +
" does not match reported sender " + event.sender
"Event " + event.getId() + ": original sender " + payload.sender +
" does not match reported sender " + event.getSender()
);
throw new base.DecryptionError(
"Message forwarded from " + payload.sender
@@ -238,9 +220,9 @@ OlmDecryption.prototype.decryptEvent = function(event) {
}
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.room_id) {
if (payload.room_id !== event.getRoomId()) {
console.warn(
"Event " + event.event_id + ": original room " + payload.room_id +
"Event " + event.getId() + ": original room " + payload.room_id +
" does not match reported room " + event.room_id
);
throw new base.DecryptionError(
@@ -248,12 +230,7 @@ OlmDecryption.prototype.decryptEvent = function(event) {
);
}
return {
payload: payload,
sessionExists: true,
keysProved: {curve25519: deviceKey},
keysClaimed: payload.keys || {}
};
event.setClearData(payload, {curve25519: deviceKey}, payload.keys || {});
};

View File

@@ -54,10 +54,17 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
this._userId = userId;
this._deviceId = deviceId;
this._initialSyncCompleted = false;
// userId -> deviceId -> true
this._pendingNewDevices = {};
this._olmDevice = new OlmDevice(sessionStore);
// EncryptionAlgorithm instance for each room
this._roomAlgorithms = {};
this._roomEncryptors = {};
// map from algorithm to DecryptionAlgorithm instance, for each room
this._roomDecryptors = {};
this._supportedAlgorithms = utils.keys(
algorithms.DECRYPTION_CLASSES
@@ -269,22 +276,26 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
// map from userid -> deviceid -> DeviceInfo
var stored = {};
function storeDev(userId, dev) {
stored[userId][dev.deviceId] = dev;
}
// list of userids we need to download keys for
var downloadUsers = [];
for (var i = 0; i < userIds.length; ++i) {
var userId = userIds[i];
stored[userId] = {};
if (forceDownload) {
downloadUsers = userIds;
} else {
for (var i = 0; i < userIds.length; ++i) {
var userId = userIds[i];
var devices = this.getStoredDevicesForUser(userId);
var devices = this.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var dev = devices[j];
stored[userId][dev.deviceId] = dev;
}
if (devices.length === 0 || forceDownload) {
downloadUsers.push(userId);
if (!devices) {
downloadUsers.push(userId);
} else {
stored[userId] = {};
devices.map(storeDev.bind(null, userId));
}
}
}
@@ -292,28 +303,79 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
return q(stored);
}
return this._baseApis.downloadKeysForUsers(
var r = this._doKeyDownloadForUsers(downloadUsers);
var promises = [];
downloadUsers.map(function(u) {
promises.push(r[u].catch(function(e) {
console.warn('Error downloading keys for user ' + u + ':', e);
}).then(function() {
stored[u] = {};
var devices = self.getStoredDevicesForUser(u) || [];
devices.map(storeDev.bind(null, u));
}));
});
return q.all(promises).then(function() {
return stored;
});
};
/**
* @param {string[]} downloadUsers list of userIds
*
* @return {Object a map from userId to a promise for a result for that user
*/
Crypto.prototype._doKeyDownloadForUsers = function(downloadUsers) {
var self = this;
console.log('Starting key download for ' + downloadUsers);
var deferMap = {};
var promiseMap = {};
downloadUsers.map(function(u) {
deferMap[u] = q.defer();
promiseMap[u] = deferMap[u].promise;
});
this._baseApis.downloadKeysForUsers(
downloadUsers
).then(function(res) {
for (var userId in res.device_keys) {
if (!stored.hasOwnProperty(userId)) {
// spurious result
).done(function(res) {
var dk = res.device_keys || {};
for (var i = 0; i < downloadUsers.length; ++i) {
var userId = downloadUsers[i];
var deviceId;
console.log('got keys for ' + userId + ':', dk[userId]);
if (!dk[userId]) {
// no result for this user
var err = 'Unknown';
// TODO: do something with res.failures
deferMap[userId].reject(err);
continue;
}
// map from deviceid -> deviceinfo for this user
var userStore = stored[userId];
var updated = _updateStoredDeviceKeysForUser(
self._olmDevice, userId, userStore, res.device_keys[userId]
);
if (!updated) {
continue;
var userStore = {};
var devs = self._sessionStore.getEndToEndDevicesForUser(userId);
if (devs) {
for (deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
var d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
}
}
}
_updateStoredDeviceKeysForUser(
self._olmDevice, userId, userStore, dk[userId]
);
// update the session store
var storage = {};
for (var deviceId in userStore) {
for (deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}
@@ -323,9 +385,16 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
self._sessionStore.storeEndToEndDevicesForUser(
userId, storage
);
deferMap[userId].resolve();
}
return stored;
}, function(err) {
downloadUsers.map(function(u) {
deferMap[u].reject(err);
});
});
return promiseMap;
};
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
@@ -399,7 +468,7 @@ function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
var unsigned = deviceResult.unsigned || {};
try {
_verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) {
console.log("Unable to verify signature on device " +
userId + ":" + deviceId + ":", e);
@@ -437,12 +506,13 @@ function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
*
* @param {string} userId the user to list keys for.
*
* @return {module:crypto/deviceinfo[]} list of devices
* @return {module:crypto/deviceinfo[]?} list of devices, or null if we haven't
* managed to get a list of devices for this user yet.
*/
Crypto.prototype.getStoredDevicesForUser = function(userId) {
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs) {
return [];
return null;
}
var res = [];
for (var deviceId in devs) {
@@ -453,6 +523,22 @@ Crypto.prototype.getStoredDevicesForUser = function(userId) {
return res;
};
/**
* Get the stored keys for a single device
*
* @param {string} userId
* @param {string} deviceId
*
* @return {module:crypto/deviceinfo?} list of devices, or undefined
* if we don't know about this device
*/
Crypto.prototype.getStoredDevice = function(userId, deviceId) {
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs || !devs[deviceId]) {
return undefined;
}
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
};
/**
* List the stored device keys for a user id
@@ -465,7 +551,7 @@ Crypto.prototype.getStoredDevicesForUser = function(userId) {
* "key", and "display_name" parameters.
*/
Crypto.prototype.listDeviceKeys = function(userId) {
var devices = this.getStoredDevicesForUser(userId);
var devices = this.getStoredDevicesForUser(userId) || [];
var result = [];
@@ -597,7 +683,7 @@ Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified, bl
* @return {Object.<string, {deviceIdKey: string, sessions: object[]}>}
*/
Crypto.prototype.getOlmSessionsForUser = function(userId) {
var devices = this.getStoredDevicesForUser(userId);
var devices = this.getStoredDevicesForUser(userId) || [];
var result = {};
for (var j = 0; j < devices.length; ++j) {
var device = devices[j];
@@ -705,7 +791,7 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
roomId: roomId,
config: config,
});
this._roomAlgorithms[roomId] = alg;
this._roomEncryptors[roomId] = alg;
};
@@ -717,7 +803,8 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
*/
/**
* Try to make sure we have established olm sessions for the given users.
* Try to make sure we have established olm sessions for all known devices for
* the given users.
*
* @param {string[]} users list of user ids
*
@@ -726,19 +813,15 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
* {@link module:crypto~OlmSessionResult}
*/
Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
var devicesWithoutSession = [
// [userId, deviceId, deviceInfo], ...
];
var result = {};
var devicesByUser = {};
for (var i = 0; i < users.length; ++i) {
var userId = users[i];
result[userId] = {};
devicesByUser[userId] = [];
var devices = this.getStoredDevicesForUser(userId);
var devices = this.getStoredDevicesForUser(userId) || [];
for (var j = 0; j < devices.length; ++j) {
var deviceInfo = devices[j];
var deviceId = deviceInfo.deviceId;
var key = deviceInfo.getIdentityKey();
if (key == this._olmDevice.deviceCurve25519Key) {
@@ -750,87 +833,13 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
continue;
}
var sessionId = this._olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
devicesWithoutSession.push([userId, deviceId, deviceInfo]);
}
result[userId][deviceId] = {
device: deviceInfo,
sessionId: sessionId,
};
devicesByUser[userId].push(deviceInfo);
}
}
if (devicesWithoutSession.length === 0) {
return q(result);
}
// TODO: this has a race condition - if we try to send another message
// while we are claiming a key, we will end up claiming two and setting up
// two sessions.
//
// That should eventually resolve itself, but it's poor form.
var self = this;
var oneTimeKeyAlgorithm = "signed_curve25519";
return this._baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm
).then(function(res) {
for (var i = 0; i < devicesWithoutSession.length; ++i) {
var device = devicesWithoutSession[i];
var userId = device[0];
var deviceId = device[1];
var deviceInfo = device[2];
var userRes = res.one_time_keys[userId] || {};
var deviceRes = userRes[deviceId];
var oneTimeKey = null;
for (var keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
}
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId
);
continue;
}
try {
_verifySignature(
self._olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint()
);
} catch (e) {
console.log(
"Unable to verify signature on one-time key for device " +
userId + ":" + deviceId + ":", e
);
continue;
}
var sid;
try {
sid = self._olmDevice.createOutboundSession(
deviceInfo.getIdentityKey(), oneTimeKey.key
);
} catch (e) {
// possibly a bad key
console.error("Error starting session with device " +
userId + ":" + deviceId + ": " + e);
continue;
}
console.log("Started new sessionid " + sid +
" for device " + userId + ":" + deviceId);
result[userId][deviceId].sessionId = sid;
}
return result;
});
return olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser
);
};
/**
@@ -839,10 +848,9 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
* @return {bool} whether encryption is enabled.
*/
Crypto.prototype.isRoomEncrypted = function(roomId) {
return Boolean(this._roomAlgorithms[roomId]);
return Boolean(this._roomEncryptors[roomId]);
};
/**
* Encrypt an event according to the configuration of the room, if necessary.
*
@@ -862,18 +870,13 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
return null;
}
if (event.getType() !== "m.room.message") {
// we only encrypt m.room.message
return null;
}
if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms");
}
var roomId = event.getRoomId();
var alg = this._roomAlgorithms[roomId];
var alg = this._roomEncryptors[roomId];
if (!alg) {
// not encrypting messages in this room
@@ -903,66 +906,17 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
});
};
/**
* @typedef {Object} module:crypto.DecryptionResult
*
* @property {Object} payload decrypted payload (with properties 'type',
* 'content').
*
* @property {Object<string, string>} keysClaimed keys that the sender of the
* event claims ownership of: map from key type to base64-encoded key
*
* @property {Object<string, string>} keysProved keys that the sender of the
* event is known to have ownership of: map from key type to base64-encoded
* key
*/
/**
* Decrypt a received event
*
* @param {object} event raw event
*
* @return {module:crypto.DecryptionResult} decryption result
* @param {MatrixEvent} event
*
* @raises {algorithms.DecryptionError} if there is a problem decrypting the event
*/
Crypto.prototype.decryptEvent = function(event) {
var content = event.content;
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
if (!AlgClass) {
throw new algorithms.DecryptionError("Unable to decrypt " + content.algorithm);
}
var alg = new AlgClass({
userId: this._userId,
olmDevice: this._olmDevice,
});
var r = alg.decryptEvent(event);
if (r !== null) {
return r;
} else {
// We've got a message for a session we don't have. Maybe the sender
// forgot to tell us about the session. Remind the sender that we
// exist so that they might tell us about the session on their next
// send.
//
// (Alternatively, it might be that we are just looking at
// scrollback... at least we rate-limit the m.new_device events :/)
//
// XXX: this is a band-aid which masks symptoms of other bugs. It would
// be nice to get rid of it.
if (event.room_id !== undefined && event.sender !== undefined) {
var device_id = event.content.device_id;
if (device_id === undefined) {
// if the sending device didn't tell us its device_id, fall
// back to all devices.
device_id = null;
}
this._sendPingToDevice(event.sender, device_id, event.room_id);
}
throw new algorithms.DecryptionError("Unknown inbound session id");
}
var content = event.getWireContent();
var alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm);
alg.decryptEvent(event);
};
/**
@@ -1044,6 +998,11 @@ Crypto.prototype._onCryptoEvent = function(event) {
* @param {module:models/room[]} rooms list of rooms the client knows about
*/
Crypto.prototype._onInitialSyncCompleted = function(rooms) {
this._initialSyncCompleted = true;
// catch up on any m.new_device events which arrived during the initial sync.
this._flushNewDeviceRequests();
if (this._sessionStore.getDeviceAnnounced()) {
return;
}
@@ -1056,7 +1015,7 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
var room = rooms[i];
// check for rooms with encryption enabled
var alg = this._roomAlgorithms[room.roomId];
var alg = this._roomEncryptors[room.roomId];
if (!alg) {
continue;
}
@@ -1110,16 +1069,13 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
*/
Crypto.prototype._onRoomKeyEvent = function(event) {
var content = event.getContent();
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
if (!AlgClass) {
throw new algorithms.DecryptionError(
"Unable to handle keys for " + content.algorithm
);
if (!content.room_id || !content.algorithm) {
console.error("key event is missing fields");
return;
}
var alg = new AlgClass({
userId: this._userId,
olmDevice: this._olmDevice,
});
var alg = this._getRoomDecryptor(content.room_id, content.algorithm);
alg.onRoomKeyEvent(event);
};
@@ -1143,7 +1099,7 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
var roomId = member.roomId;
var alg = this._roomAlgorithms[roomId];
var alg = this._roomEncryptors[roomId];
if (!alg) {
// not encrypting in this room
return;
@@ -1173,26 +1129,110 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
console.log("m.new_device event from " + userId + ":" + deviceId +
" for rooms " + rooms);
if (this.getStoredDevice(userId, deviceId)) {
console.log("Known device; ignoring");
return;
}
this._pendingNewDevices[userId] = this._pendingNewDevices[userId] || {};
this._pendingNewDevices[userId][deviceId] = true;
// we delay handling these until the intialsync has completed, so that we
// can do all of them together.
if (this._initialSyncCompleted) {
this._flushNewDeviceRequests();
}
};
/**
* Start device queries for any users who sent us an m.new_device recently
*/
Crypto.prototype._flushNewDeviceRequests = function() {
var self = this;
this.downloadKeys(
[userId], true
).then(function() {
for (var i = 0; i < rooms.length; i++) {
var roomId = rooms[i];
var alg = self._roomAlgorithms[roomId];
if (!alg) {
// not encrypting in this room
continue;
}
alg.onNewDevice(userId, deviceId);
var pending = this._pendingNewDevices;
var users = utils.keys(pending).filter(function(u) {
return utils.keys(pending[u]).length > 0;
});
if (users.length === 0) {
return;
}
var r = this._doKeyDownloadForUsers(users);
// we've kicked off requests to these users: remove their
// pending flag for now.
this._pendingNewDevices = {};
users.map(function(u) {
r[u] = r[u].catch(function(e) {
console.error(
'Error updating device keys for user ' + u + ':', e
);
// reinstate the pending flags on any users which failed; this will
// mean that we will do another download in the future, but won't
// tight-loop.
//
self._pendingNewDevices[u] = self._pendingNewDevices[u] || {};
utils.update(self._pendingNewDevices[u], pending[u]);
});
});
q.all(utils.values(r)).done();
};
/**
* Get a decryptor for a given room and algorithm.
*
* If we already have a decryptor for the given room and algorithm, return
* it. Otherwise try to instantiate it.
*
* @private
*
* @param {string?} roomId room id for decryptor. If undefined, a temporary
* decryptor is instantiated.
*
* @param {string} algorithm crypto algorithm
*
* @return {module:crypto.algorithms.base.DecryptionAlgorithm}
*
* @raises {module:crypto.algorithms.DecryptionError} if the algorithm is
* unknown
*/
Crypto.prototype._getRoomDecryptor = function(roomId, algorithm) {
var decryptors;
var alg;
roomId = roomId || null;
if (roomId) {
decryptors = this._roomDecryptors[roomId];
if (!decryptors) {
this._roomDecryptors[roomId] = decryptors = {};
}
}).catch(function(e) {
console.error(
"Error updating device keys for new device " + userId + ":" +
deviceId,
e
);
}).done();
alg = decryptors[algorithm];
if (alg) {
return alg;
}
}
var AlgClass = algorithms.DECRYPTION_CLASSES[algorithm];
if (!AlgClass) {
throw new algorithms.DecryptionError("Unable to decrypt " + algorithm);
}
alg = new AlgClass({
userId: this._userId,
crypto: this,
olmDevice: this._olmDevice,
roomId: roomId,
});
if (decryptors) {
decryptors[algorithm] = alg;
}
return alg;
};
@@ -1209,40 +1249,6 @@ Crypto.prototype._signObject = function(obj) {
obj.signatures = sigs;
};
/**
* Verify the signature on an object
*
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
*
* @param {Object} obj object to check signature on. Note that this will be
* stripped of its 'signatures' and 'unsigned' properties.
*
* @param {string} signingUserId ID of the user whose signature should be checked
*
* @param {string} signingDeviceId ID of the device whose signature should be checked
*
* @param {string} signingKey base64-ed ed25519 public key
*/
function _verifySignature(olmDevice, obj, signingUserId, signingDeviceId, signingKey) {
var signKeyId = "ed25519:" + signingDeviceId;
var signatures = obj.signatures || {};
var userSigs = signatures[signingUserId] || {};
var signature = userSigs[signKeyId];
if (!signature) {
throw Error("No signature");
}
// prepare the canonical json: remove unsigned and signatures, and stringify with
// anotherjson
delete obj.unsigned;
delete obj.signatures;
var json = anotherjson.stringify(obj);
olmDevice.verifySignature(
signingKey, json, signature
);
}
/**
* @see module:crypto/algorithms/base.DecryptionError
*/

View File

@@ -20,6 +20,9 @@ limitations under the License.
* Utilities common to olm encryption algorithms
*/
var q = require('q');
var anotherjson = require('another-json');
var utils = require("../utils");
/**
@@ -100,3 +103,166 @@ module.exports.encryptMessageForDevice = function(
deviceKey, sessionId, JSON.stringify(payload)
);
};
/**
* Try to make sure we have established olm sessions for the given devices.
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
module.exports.ensureOlmSessionsForDevices = function(
olmDevice, baseApis, devicesByUser
) {
var devicesWithoutSession = [
// [userId, deviceId], ...
];
var result = {};
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
result[userId] = {};
var devices = devicesByUser[userId];
for (var j = 0; j < devices.length; j++) {
var deviceInfo = devices[j];
var deviceId = deviceInfo.deviceId;
var key = deviceInfo.getIdentityKey();
var sessionId = olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
devicesWithoutSession.push([userId, deviceId]);
}
result[userId][deviceId] = {
device: deviceInfo,
sessionId: sessionId,
};
}
}
if (devicesWithoutSession.length === 0) {
return q(result);
}
// TODO: this has a race condition - if we try to send another message
// while we are claiming a key, we will end up claiming two and setting up
// two sessions.
//
// That should eventually resolve itself, but it's poor form.
var oneTimeKeyAlgorithm = "signed_curve25519";
return baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm
).then(function(res) {
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
var userRes = res.one_time_keys[userId] || {};
var devices = devicesByUser[userId];
for (var j = 0; j < devices.length; j++) {
var deviceInfo = devices[j];
var deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
// we already have a result for this device
continue;
}
var deviceRes = userRes[deviceId] || {};
var oneTimeKey = null;
for (var keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
}
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId
);
continue;
}
var sid = _verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo
);
result[userId][deviceId].sessionId = sid;
}
}
return result;
});
};
function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
var deviceId = deviceInfo.deviceId;
try {
_verifySignature(
olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint()
);
} catch (e) {
console.error(
"Unable to verify signature on one-time key for device " +
userId + ":" + deviceId + ":", e
);
return null;
}
var sid;
try {
sid = olmDevice.createOutboundSession(
deviceInfo.getIdentityKey(), oneTimeKey.key
);
} catch (e) {
// possibly a bad key
console.error("Error starting session with device " +
userId + ":" + deviceId + ": " + e);
return null;
}
console.log("Started new sessionid " + sid +
" for device " + userId + ":" + deviceId);
return sid;
}
/**
* Verify the signature on an object
*
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
*
* @param {Object} obj object to check signature on. Note that this will be
* stripped of its 'signatures' and 'unsigned' properties.
*
* @param {string} signingUserId ID of the user whose signature should be checked
*
* @param {string} signingDeviceId ID of the device whose signature should be checked
*
* @param {string} signingKey base64-ed ed25519 public key
*/
var _verifySignature = module.exports.verifySignature = function(
olmDevice, obj, signingUserId, signingDeviceId, signingKey
) {
var signKeyId = "ed25519:" + signingDeviceId;
var signatures = obj.signatures || {};
var userSigs = signatures[signingUserId] || {};
var signature = userSigs[signKeyId];
if (!signature) {
throw Error("No signature");
}
// prepare the canonical json: remove unsigned and signatures, and stringify with
// anotherjson
delete obj.unsigned;
delete obj.signatures;
var json = anotherjson.stringify(obj);
olmDevice.verifySignature(
signingKey, json, signature
);
};

View File

@@ -104,7 +104,7 @@ module.exports.MatrixHttpApi.prototype = {
*
* @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a a Buffer, String or ReadStream.
* a Buffer, String or ReadStream.
*
* @param {object} opts options object
*

View File

@@ -82,6 +82,27 @@ module.exports.request = function(r) {
request = r;
};
/**
* Return the currently-set request function.
* @return {requestFunction} The current request function.
*/
module.exports.getRequest = function() {
return request;
};
/**
* Apply wrapping code around the request function. The wrapper function is
* installed as the new request handler, and when invoked it is passed the
* previous value, along with the options and callback arguments.
* @param {requestWrapperFunction} wrapper The wrapping function.
*/
module.exports.wrapRequest = function(wrapper) {
var origRequest = request;
request = function(options, callback) {
return wrapper(origRequest, options, callback);
};
};
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
@@ -129,6 +150,16 @@ module.exports.createClient = function(opts) {
* @param {requestCallback} callback The request callback.
*/
/**
* A wrapper for the request function interface.
* @callback requestWrapperFunction
* @param {requestFunction} origRequest The underlying request function being
* wrapped
* @param {Object} opts The options for this HTTP request, given in the same
* form as {@link requestFunction}.
* @param {requestCallback} callback The request callback.
*/
/**
* The request callback interface for performing HTTP requests. This matches the
* API for the {@link https://github.com/request/request#requestoptions-callback|

View File

@@ -21,6 +21,10 @@ limitations under the License.
* @module models/event
*/
var EventEmitter = require("events").EventEmitter;
var utils = require('../utils.js');
/**
* Enum for event statuses.
* @readonly
@@ -51,15 +55,6 @@ module.exports.EventStatus = {
*
* @param {Object} event The raw event to be wrapped in this DAO
*
* @param {Object=} clearEvent For encrypted events, the plaintext payload for
* the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @param {Object=} keysProved Keys owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getKeysProved}.
*
* @param {Object=} keysClaimed Keys the sender of this event claims.
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
*
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
* this property</b> directly unless you absolutely have to. Prefer the getter
* methods defined on this class. Using the getter methods shields your app
@@ -75,22 +70,23 @@ module.exports.EventStatus = {
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(
event, clearEvent, keysProved, keysClaimed
event
) {
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.forwardLooking = true;
this._clearEvent = clearEvent || {};
this._pushActions = null;
this._keysProved = keysProved || {};
this._keysClaimed = keysClaimed || {};
this._clearEvent = {};
this._keysProved = {};
this._keysClaimed = {};
};
utils.inherits(module.exports.MatrixEvent, EventEmitter);
module.exports.MatrixEvent.prototype = {
utils.extend(module.exports.MatrixEvent.prototype, {
/**
* Get the event_id for this event.
@@ -239,12 +235,37 @@ module.exports.MatrixEvent.prototype = {
this._keysClaimed = keys;
},
/**
* Update the cleartext data on this event.
*
* (This is used after decrypting an event; it should not be used by applications).
*
* @internal
*
* @fires module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {Object} clearEvent The plaintext payload for the event
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @param {Object=} keysProved Keys owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getKeysProved}.
*
* @param {Object=} keysClaimed Keys the sender of this event claims.
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
*/
setClearData: function(clearEvent, keysProved, keysClaimed) {
this._clearEvent = clearEvent;
this._keysProved = keysProved || {};
this._keysClaimed = keysClaimed || {};
this.emit("Event.decrypted", this);
},
/**
* Check if the event is encrypted.
* @return {boolean} True if this event is encrypted.
*/
isEncrypted: function() {
return Boolean(this._clearEvent.type);
return this.event.type === "m.room.encrypted";
},
/**
@@ -353,7 +374,7 @@ module.exports.MatrixEvent.prototype = {
setPushActions: function(pushActions) {
this._pushActions = pushActions;
},
};
});
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
@@ -394,3 +415,15 @@ var _REDACT_KEEP_CONTENT_MAP = {
},
'm.room.aliases': {'aliases': 1},
};
/**
* Fires when an event is decrypted
*
* @event module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {module:models/event.MatrixEvent} event
* The matrix event which has been decrypted
*/

View File

@@ -60,7 +60,9 @@ function debuglog() {
function SyncApi(client, opts) {
this.client = client;
opts = opts || {};
opts.initialSyncLimit = opts.initialSyncLimit || 8;
opts.initialSyncLimit = (
opts.initialSyncLimit === undefined ? 8 : opts.initialSyncLimit
);
opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
opts.pollTimeout = opts.pollTimeout || (30 * 1000);
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
@@ -542,6 +544,7 @@ SyncApi.prototype._sync = function(syncOptions) {
self._connectionReturnedDefer.reject();
self._connectionReturnedDefer = null;
}
self._updateSyncState("STOPPED");
return;
}
console.error("/sync error %s", err);

View File

@@ -234,7 +234,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
// remove some events from the other end, if necessary
var excess = this._eventCount - this._windowLimit;
if (excess > 0) {
this._unpaginate(excess, direction != EventTimeline.BACKWARDS);
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
return q(true);
}
@@ -287,15 +287,13 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
/**
* Trim the window to the windowlimit
* Remove `delta` events from the start or end of the timeline.
*
* @param {number} delta number of events to remove from the timeline
* @param {boolean} startOfTimeline if events should be removed from the start
* of the timeline.
*
* @private
*/
TimelineWindow.prototype._unpaginate = function(delta, startOfTimeline) {
TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
var tl = startOfTimeline ? this._start : this._end;
// sanity-check the delta

View File

@@ -204,7 +204,8 @@ module.exports.isFunction = function(value) {
* @return {boolean} True if it is an array.
*/
module.exports.isArray = function(value) {
return Boolean(value && value.constructor === Array);
return Array.isArray ? Array.isArray(value) :
Boolean(value && value.constructor === Array);
};
/**

View File

@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "0.6.4",
"version": "0.7.0",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {

View File

@@ -10,8 +10,8 @@
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; exit)
hub --version > /dev/null || (echo "hub is required: please install it"; exit)
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
hub --version > /dev/null || (echo "hub is required: please install it"; kill $$)
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
@@ -140,7 +140,7 @@ if [ $dodist -eq 0 ]; then
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
git co "$rel_branch"
git checkout "$rel_branch"
npm install
# We haven't tagged yet, so tell the dist script what version
# it's building

View File

@@ -5,21 +5,6 @@ var HttpBackend = require("../mock-request");
var utils = require("../../lib/utils");
var test_utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
}
};
var aliHttpBackend;
var bobHttpBackend;
var aliClient;
@@ -36,7 +21,6 @@ var aliDeviceKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliLocalStore;
var aliStorage;
var bobStorage;
var aliMessages;
@@ -461,11 +445,9 @@ describe("MatrixClient crypto", function() {
}
beforeEach(function() {
aliLocalStore = new MockStorageApi();
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
test_utils.beforeEach(this);
aliStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
aliHttpBackend = new HttpBackend();
aliClient = sdk.createClient({
baseUrl: "http://alis.server",
@@ -476,6 +458,7 @@ describe("MatrixClient crypto", function() {
request: aliHttpBackend.requestFn,
});
bobStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
bobHttpBackend = new HttpBackend();
bobClient = sdk.createClient({
baseUrl: "http://bobs.server",
@@ -500,6 +483,27 @@ describe("MatrixClient crypto", function() {
bobClient.stopClient();
});
it('Ali knows the difference between a new user and one with no devices',
function(done) {
aliHttpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:id': {},
}
});
var p1 = aliClient.downloadKeys(['@bob:id']);
var p2 = aliHttpBackend.flush('/keys/query', 1);
q.all([p1, p2]).then(function() {
var devices = aliStorage.getEndToEndDevicesForUser('@bob:id');
expect(utils.keys(devices).length).toEqual(0);
// request again: should be no more requests
return aliClient.downloadKeys(['@bob:id']);
}).nodeify(done);
}
);
it("Bob uploads without one-time keys and with one-time keys", function(done) {
q()
.then(bobUploadsKeys)
@@ -735,4 +739,31 @@ describe("MatrixClient crypto", function() {
}).then(aliRecvMessage)
.catch(test_utils.failTest).done(done);
});
it("Ali does a key query when she gets a new_device event", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(function() {
var syncData = {
next_batch: '2',
to_device: {
events: [
test_utils.mkEvent({
content: {
device_id: 'TEST_DEVICE',
rooms: [],
},
sender: bobUserId,
type: 'm.new_device',
}),
],
},
};
aliHttpBackend.when('GET', '/sync').respond(200, syncData);
return aliHttpBackend.flush('/sync', 1);
}).then(expectAliQueryKeys)
.nodeify(done);
});
});

View File

@@ -371,21 +371,6 @@ describe("MatrixClient", function() {
httpBackend.flush();
});
it("should return a rejected promise if the request fails", function(done) {
httpBackend.when("POST", "/keys/query").respond(400);
var exceptionThrown;
client.downloadKeys(["bottom"]).then(function() {
fail("download didn't fail");
}, function(err) {
exceptionThrown = err;
}).then(function() {
expect(exceptionThrown).toBeTruthy();
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
});
describe("deleteDevice", function() {

617
spec/integ/megolm.spec.js Normal file
View File

@@ -0,0 +1,617 @@
/*
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";
try {
var Olm = require('olm');
} catch (e) {}
var anotherjson = require('another-json');
var q = require('q');
var sdk = require('../..');
var utils = require('../../lib/utils');
var test_utils = require('../test-utils');
var MockHttpBackend = require('../mock-request');
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @constructor
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*/
function TestClient(userId, deviceId, accessToken) {
this.userId = userId;
this.deviceId = deviceId;
this.storage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: this.storage,
request: this.httpBackend.requestFn,
});
this.deviceKeys = null;
this.oneTimeKeys = [];
}
/**
* start the client, and wait for it to initialise.
*
* @return {Promise}
*/
TestClient.prototype.start = function() {
var self = this;
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.one_time_keys).not.toBeDefined();
expect(content.device_keys).toBeDefined();
self.deviceKeys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}};
});
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.device_keys).not.toBeDefined();
expect(content.one_time_keys).toBeDefined();
expect(content.one_time_keys).not.toEqual({});
self.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
signed_curve25519: utils.keys(self.oneTimeKeys).length
}};
});
this.client.startClient();
return this.httpBackend.flush();
};
/**
* stop the client
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
};
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getDeviceKey = function() {
var key_id = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[key_id];
};
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getSigningKey = function() {
var key_id = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[key_id];
};
/**
* start an Olm session with a given recipient
*
* @param {Olm.Account} olmAccount
* @param {TestClient} recipientTestClient
* @return {Olm.Session}
*/
function createOlmSession(olmAccount, recipientTestClient) {
var otk_id = utils.keys(recipientTestClient.oneTimeKeys)[0];
var otk = recipientTestClient.oneTimeKeys[otk_id];
var session = new Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key
);
return session;
}
/**
* encrypt an event with olm
*
* @param {object} opts
* @param {string=} opts.sender
* @param {string} opts.senderKey
* @param {Olm.Session} opts.p2pSession
* @param {TestClient} opts.recipient
* @param {object=} opts.plaincontent
* @param {string=} opts.plaintype
*
* @return {object} event
*/
function encryptOlmEvent(opts) {
expect(opts.senderKey).toBeDefined();
expect(opts.p2pSession).toBeDefined();
expect(opts.recipient).toBeDefined();
var plaintext = {
content: opts.plaincontent || {},
recipient: opts.recipient.userId,
recipient_keys: {
ed25519: opts.recipient.getSigningKey(),
},
sender: opts.sender || '@bob:xyz',
type: opts.plaintype || 'm.test',
};
var event = {
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
ciphertext: {},
sender_key: opts.senderKey,
},
sender: opts.sender || '@bob:xyz',
type: 'm.room.encrypted',
};
event.content.ciphertext[opts.recipient.getDeviceKey()] =
opts.p2pSession.encrypt(JSON.stringify(plaintext));
return event;
}
/**
* encrypt an event with megolm
*
* @param {object} opts
* @param {string} opts.senderKey
* @param {Olm.OutboundGroupSession} opts.groupSession
* @param {object=} opts.plaintext
* @param {string=} opts.room_id
*
* @return {object} event
*/
function encryptMegolmEvent(opts) {
expect(opts.senderKey).toBeDefined();
expect(opts.groupSession).toBeDefined();
var plaintext = opts.plaintext || {};
if (!plaintext.content) {
plaintext.content = {
body: '42',
msgtype: "m.text",
};
}
if (!plaintext.type) {
plaintext.type = "m.room.message";
}
if (!plaintext.room_id) {
expect(opts.room_id).toBeDefined();
plaintext.room_id = opts.room_id;
}
return {
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
device_id: "testDevice",
sender_key: opts.senderKey,
session_id: opts.groupSession.session_id(),
},
type: "m.room.encrypted",
};
}
/**
* build an encrypted room_key event to share a group session
*
* @param {object} opts
* @param {string} opts.senderKey
* @param {TestClient} opts.recipient
* @param {Olm.Session} opts.p2pSession
* @param {Olm.OutboundGroupSession} opts.groupSession
* @param {string=} opts.room_id
*
* @return {object} event
*/
function encryptGroupSessionKey(opts) {
return encryptOlmEvent({
senderKey: opts.senderKey,
recipient: opts.recipient,
p2pSession: opts.p2pSession,
plaincontent: {
algorithm: 'm.megolm.v1.aes-sha2',
room_id: opts.room_id,
session_id: opts.groupSession.session_id(),
session_key: opts.groupSession.session_key(),
},
plaintype: 'm.room_key',
});
}
describe("megolm", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
var ROOM_ID = "!room:id";
var testOlmAccount;
var testSenderKey;
var aliceTestClient;
var testDeviceKeys;
beforeEach(function(done) {
test_utils.beforeEach(this);
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs"
);
testOlmAccount = new Olm.Account();
testOlmAccount.create();
var testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
testDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'DEVICE_ID',
keys: {
'curve25519:DEVICE_ID': testE2eKeys.curve25519,
'ed25519:DEVICE_ID': testE2eKeys.ed25519,
},
user_id: '@bob:xyz',
};
var j = anotherjson.stringify(testDeviceKeys);
var sig = testOlmAccount.sign(j);
testDeviceKeys.signatures = {
'@bob:xyz': {
'ed25519:DEVICE_ID': sig,
},
};
return aliceTestClient.start().nodeify(done);
});
afterEach(function() {
aliceTestClient.stop();
});
it("Alice receives a megolm message", function(done) {
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
var roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
var messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
var syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1).then(function() {
var room = aliceTestClient.client.getRoom(ROOM_ID);
var event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
it("Alice gets a second room_key message", function(done) {
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
var roomKeyEncrypted1 = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
var messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// make a second room_key event now that we have advanced the group
// session.
var roomKeyEncrypted2 = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// on the first sync, send the best room key
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted1],
},
});
// on the second sync, send the advanced room key, along with the
// message. This simulates the situation where Alice has been sent a
// later copy of the room key and is reloading the client.
var syncResponse2 = {
next_batch: 2,
to_device: {
events: [roomKeyEncrypted2],
},
rooms: {
join: {},
},
};
syncResponse2.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
return aliceTestClient.httpBackend.flush("/sync", 2).then(function() {
var room = aliceTestClient.client.getRoom(ROOM_ID);
var event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
it('Alice sends a megolm message', function(done) {
// establish an olm session with alice
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
var syncResponse = {
next_batch: 1,
to_device: {
events: [olmEvent],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
state: {
events: [
test_utils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
test_utils.mkMembership({
mship: 'join',
sender: '@bob:xyz',
}),
],
},
};
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
var inboundGroupSession;
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {
'DEVICE_ID': testDeviceKeys,
},
}
});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/'
).respond(200, function(path, content) {
var m = content.messages['@bob:xyz'].DEVICE_ID;
var ct = m.ciphertext[testSenderKey];
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual('m.room_key');
inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, function(path, content) {
var ct = content.ciphertext;
var decrypted = JSON.parse(inboundGroupSession.decrypt(ct));
console.log('Decrypted received megolm message', decrypted);
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
return {
event_id: '$event_id',
};
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function(done) {
var syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
state: {
events: [
test_utils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
test_utils.mkMembership({
mship: 'join',
sender: '@bob:xyz',
}),
],
},
};
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
}
});
return q.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, {
event_id: '$event_id',
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
it("We shouldn't attempt to send to blocked devices", function(done) {
// establish an olm session with alice
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
var syncResponse = {
next_batch: 1,
to_device: {
events: [olmEvent],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
state: {
events: [
test_utils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
test_utils.mkMembership({
mship: 'join',
sender: '@bob:xyz',
}),
],
},
};
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
console.log('Forcing alice to download our device keys');
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {
'DEVICE_ID': testDeviceKeys,
},
}
});
return q.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log('Telling alice to block our device');
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
console.log('Telling alice to send a megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, {
event_id: '$event_id',
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
});

View File

@@ -65,7 +65,7 @@ module.exports.mkEvent = function(opts) {
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random()
};
if (opts.skey) {
if (opts.skey !== undefined) {
event.state_key = opts.skey;
}
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
@@ -159,7 +159,16 @@ module.exports.mkMessage = function(opts) {
* <p>This is useful for use with integration tests which use asyncronous
* methods: it can be added as a 'catch' handler in a promise chain.
*
* @param {Error} error exception to be reported
* @param {Error} err exception to be reported
*
* @deprecated
* It turns out there are easier ways of doing this. Just use nodeify():
*
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).nodeify(done);
* });
*
* @example
* it("should not throw", function(done) {
@@ -168,6 +177,27 @@ module.exports.mkMessage = function(opts) {
* }).catch(utils.failTest).done(done);
* });
*/
module.exports.failTest = function(error) {
expect(error.stack).toBe(null);
module.exports.failTest = function(err) {
expect(true).toBe(false, "Testfunc threw: " + err.stack);
};
/**
* A mock implementation of webstorage
*
* @constructor
*/
module.exports.MockStorageApi = function() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
}
};