Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7357952ec | |||
| b796246d9d | |||
| 2e3c349c5e | |||
| 7f4ff352e8 | |||
| b11bff5a5b | |||
| 223bd459f6 | |||
| ed1673c66c | |||
| 0fda43b603 | |||
| d3b63c592e | |||
| a32af7d77c | |||
| 742d942baa | |||
| 73e86bfc5d | |||
| 0a4c41c958 | |||
| 79a699f0be | |||
| 4a2e6a826b | |||
| 95f56f95ec | |||
| 67e75fb7af | |||
| ebda89d1ae | |||
| 5ce5299651 | |||
| 41bd518182 | |||
| 739e94302d | |||
| e54541aecf | |||
| 338c707579 | |||
| 301ab01911 | |||
| 99089c0f5f | |||
| ec124847d7 | |||
| 89ced19874 |
@@ -1,3 +1,31 @@
|
||||
Changes in [0.7.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.2) (2016-12-15)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1...v0.7.2)
|
||||
|
||||
* Bump to Olm 2.0
|
||||
[\#309](https://github.com/matrix-org/matrix-js-sdk/pull/309)
|
||||
* Sanity check payload length before encrypting
|
||||
[\#307](https://github.com/matrix-org/matrix-js-sdk/pull/307)
|
||||
* Remove dead _sendPingToDevice function
|
||||
[\#308](https://github.com/matrix-org/matrix-js-sdk/pull/308)
|
||||
* Add setRoomDirectoryVisibilityAppService
|
||||
[\#306](https://github.com/matrix-org/matrix-js-sdk/pull/306)
|
||||
* Update release script to do signed releases
|
||||
[\#305](https://github.com/matrix-org/matrix-js-sdk/pull/305)
|
||||
* e2e: Wait for pending device lists
|
||||
[\#304](https://github.com/matrix-org/matrix-js-sdk/pull/304)
|
||||
* Start a new megolm session when devices are blacklisted
|
||||
[\#303](https://github.com/matrix-org/matrix-js-sdk/pull/303)
|
||||
* E2E: Download our own devicelist on startup
|
||||
[\#302](https://github.com/matrix-org/matrix-js-sdk/pull/302)
|
||||
|
||||
Changes in [0.7.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1) (2016-12-09)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1-rc.1...v0.7.1)
|
||||
|
||||
No changes
|
||||
|
||||
|
||||
Changes in [0.7.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1-rc.1) (2016-12-05)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.0...v0.7.1-rc.1)
|
||||
|
||||
@@ -570,6 +570,30 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibility =
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the visbility of a room bridged to a 3rd party network in
|
||||
* the current HS's room directory.
|
||||
* @param {string} networkId the network ID of the 3rd party
|
||||
* instance under which this room is published under.
|
||||
* @param {string} roomId
|
||||
* @param {string} visibility "public" to make the room visible
|
||||
* in the public directory, or "private" to make
|
||||
* it invisible.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
|
||||
function(networkId, roomId, visibility, callback) {
|
||||
var path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", {
|
||||
$networkId: networkId,
|
||||
$roomId: roomId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, { "visibility": visibility }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Media operations
|
||||
// ================
|
||||
|
||||
+27
-4
@@ -20,10 +20,33 @@ limitations under the License.
|
||||
*
|
||||
* @module crypto/OlmDevice
|
||||
*/
|
||||
|
||||
var Olm = require("olm");
|
||||
var utils = require("../utils");
|
||||
|
||||
|
||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||
var MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
|
||||
|
||||
function checkPayloadLength(payloadString) {
|
||||
if (payloadString === undefined) {
|
||||
throw new Error("payloadString undefined");
|
||||
}
|
||||
|
||||
if (payloadString.length > MAX_PLAINTEXT_LENGTH) {
|
||||
// might as well fail early here rather than letting the olm library throw
|
||||
// a cryptic memory allocation error.
|
||||
//
|
||||
// Note that even if we manage to do the encryption, the message send may fail,
|
||||
// because by the time we've wrapped the ciphertext in the event object, it may
|
||||
// exceed 65K. But at least we won't just fail with "abort()" in that case.
|
||||
throw new Error("Message too long (" + payloadString.length + " bytes). " +
|
||||
"The maximum for an encrypted message is " +
|
||||
MAX_PLAINTEXT_LENGTH + " bytes.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Manages the olm cryptography functions. Each OlmDevice has a single
|
||||
* OlmAccount and a number of OlmSessions.
|
||||
@@ -386,9 +409,7 @@ OlmDevice.prototype.encryptMessage = function(
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
if (payloadString === undefined) {
|
||||
throw new Error("payloadString undefined");
|
||||
}
|
||||
checkPayloadLength(payloadString);
|
||||
|
||||
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
|
||||
var res = session.encrypt(payloadString);
|
||||
@@ -515,6 +536,8 @@ OlmDevice.prototype.createOutboundGroupSession = function() {
|
||||
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
|
||||
var self = this;
|
||||
|
||||
checkPayloadLength(payloadString);
|
||||
|
||||
return this._getOutboundGroupSession(sessionId, function(session) {
|
||||
var res = session.encrypt(payloadString);
|
||||
self._saveOutboundGroupSession(session);
|
||||
|
||||
+108
-61
@@ -36,8 +36,6 @@ var base = require("./base");
|
||||
* @property {string} sessionId
|
||||
* @property {Number} useCount number of times this session has been used
|
||||
* @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
|
||||
@@ -47,7 +45,6 @@ function OutboundSessionInfo(sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
this.useCount = 0;
|
||||
this.creationTime = new Date().getTime();
|
||||
this.sharePromise = null;
|
||||
this.sharedWithDevices = {};
|
||||
}
|
||||
|
||||
@@ -78,6 +75,45 @@ OutboundSessionInfo.prototype.needsRotation = function(
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determine if this session has been shared with devices which it shouldn't
|
||||
* have been.
|
||||
*
|
||||
* @param {Object} devicesInRoom userId -> {deviceId -> object}
|
||||
* devices we should shared the session with.
|
||||
*
|
||||
* @return {Boolean} true if we have shared the session with devices which aren't
|
||||
* in devicesInRoom.
|
||||
*/
|
||||
OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
|
||||
devicesInRoom
|
||||
) {
|
||||
|
||||
for (var userId in this.sharedWithDevices) {
|
||||
if (!this.sharedWithDevices.hasOwnProperty(userId)) { continue; }
|
||||
|
||||
if (!devicesInRoom.hasOwnProperty(userId)) {
|
||||
console.log("Starting new session because we shared with " + userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var deviceId in this.sharedWithDevices[userId]) {
|
||||
if (!this.sharedWithDevices[userId].hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
|
||||
console.log(
|
||||
"Starting new session because we shared with " +
|
||||
userId + ":" + deviceId
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Megolm encryption implementation
|
||||
*
|
||||
@@ -90,10 +126,12 @@ OutboundSessionInfo.prototype.needsRotation = function(
|
||||
function MegolmEncryption(params) {
|
||||
base.EncryptionAlgorithm.call(this, params);
|
||||
|
||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
||||
// that even if this is non-null, it may not be ready for use (in which
|
||||
// case _outboundSession.sharePromise will be non-null.)
|
||||
this._outboundSession = null;
|
||||
// the most recent attempt to set up a session. This is used to serialise
|
||||
// the session setups, so that we have a race-free view of which session we
|
||||
// are using, and which devices we have shared the keys with. It resolves
|
||||
// with an OutboundSessionInfo (or undefined, for the first message in the
|
||||
// room).
|
||||
this._setupPromise = q();
|
||||
|
||||
// default rotation periods
|
||||
this._sessionRotationPeriodMsgs = 100;
|
||||
@@ -117,25 +155,37 @@ utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
|
||||
* @return {module:client.Promise} Promise which resolves to the
|
||||
* OutboundSessionInfo when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
||||
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
|
||||
var self = this;
|
||||
|
||||
var session = this._outboundSession;
|
||||
var session;
|
||||
|
||||
// need to make a brand new session?
|
||||
if (!session || session.needsRotation(self._sessionRotationPeriodMsgs,
|
||||
self._sessionRotationPeriodMs)
|
||||
) {
|
||||
this._outboundSession = session = this._prepareNewSession(room);
|
||||
}
|
||||
// takes the previous OutboundSessionInfo, and considers whether to create
|
||||
// a new one. Also shares the key with any (new) devices in the room.
|
||||
// Updates `session` to hold the final OutboundSessionInfo.
|
||||
//
|
||||
// returns a promise which resolves once the keyshare is successful.
|
||||
function prepareSession(oldSession) {
|
||||
session = oldSession;
|
||||
|
||||
if (session.sharePromise) {
|
||||
// key share already in progress
|
||||
return session.sharePromise;
|
||||
}
|
||||
// need to make a brand new session?
|
||||
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
|
||||
self._sessionRotationPeriodMs)
|
||||
) {
|
||||
console.log("Starting new megolm session because we need to rotate.");
|
||||
session = null;
|
||||
}
|
||||
|
||||
// no share in progress: check if we need to share with any devices
|
||||
var prom = this._getDevicesInRoom(room).then(function(devicesInRoom) {
|
||||
// determine if we have shared with anyone we shouldn't have
|
||||
if (session && session.sharedWithTooManyDevices(devicesInRoom)) {
|
||||
session = null;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
session = self._prepareNewSession();
|
||||
}
|
||||
|
||||
// now check if we need to share with any devices
|
||||
var shareMap = {};
|
||||
|
||||
for (var userId in devicesInRoom) {
|
||||
@@ -152,10 +202,6 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
||||
|
||||
var deviceInfo = userDevices[deviceId];
|
||||
|
||||
if (deviceInfo.isBlocked()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
if (key == self._olmDevice.deviceCurve25519Key) {
|
||||
// don't bother sending to ourself
|
||||
@@ -175,24 +221,27 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
||||
return self._shareKeyWithDevices(
|
||||
session, shareMap
|
||||
);
|
||||
}).finally(function() {
|
||||
session.sharePromise = null;
|
||||
}).then(function() {
|
||||
return session;
|
||||
});
|
||||
}
|
||||
|
||||
session.sharePromise = prom;
|
||||
return prom;
|
||||
// helper which returns the session prepared by prepareSession
|
||||
function returnSession() { return session; }
|
||||
|
||||
// first wait for the previous share to complete
|
||||
var prom = this._setupPromise.then(prepareSession);
|
||||
|
||||
// _setupPromise resolves to `session` whether or not the share succeeds
|
||||
this._setupPromise = prom.then(returnSession, returnSession);
|
||||
|
||||
// but we return a promise which only resolves if the share was successful.
|
||||
return prom.then(returnSession);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*/
|
||||
MegolmEncryption.prototype._prepareNewSession = function(room) {
|
||||
MegolmEncryption.prototype._prepareNewSession = function() {
|
||||
var session_id = this._olmDevice.createOutboundGroupSession();
|
||||
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
|
||||
|
||||
@@ -335,7 +384,9 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
|
||||
*/
|
||||
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
var self = this;
|
||||
return this._ensureOutboundSession(room).then(function(session) {
|
||||
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
|
||||
return self._ensureOutboundSession(devicesInRoom);
|
||||
}).then(function(session) {
|
||||
var payloadJson = {
|
||||
room_id: self._roomId,
|
||||
type: eventType,
|
||||
@@ -362,30 +413,7 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event causing the change
|
||||
* @param {module:models/room-member} member user whose membership changed
|
||||
* @param {string=} oldMembership previous membership
|
||||
*/
|
||||
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
|
||||
var newMembership = member.membership;
|
||||
|
||||
if (newMembership === 'join' || newMembership === 'invite') {
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise we assume the user is leaving, and start a new outbound session.
|
||||
console.log("Discarding outbound megolm session due to change in " +
|
||||
"membership of " + member.userId + " (" + oldMembership +
|
||||
"->" + newMembership + ")");
|
||||
|
||||
// this ensures that we will start a new session on the next message.
|
||||
this._outboundSession = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the list of devices for all users in the room
|
||||
* Get the list of unblocked devices for all users in the room
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
@@ -402,7 +430,26 @@ MegolmEncryption.prototype._getDevicesInRoom = function(room) {
|
||||
// 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);
|
||||
return this._crypto.downloadKeys(roomMembers, false).then(function(devices) {
|
||||
// remove any blocked devices
|
||||
for (var userId in devices) {
|
||||
if (!devices.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var userDevices = devices[userId];
|
||||
for (var deviceId in userDevices) {
|
||||
if (!userDevices.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
if (userDevices[deviceId].isBlocked()) {
|
||||
delete userDevices[deviceId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+88
-105
@@ -55,8 +55,10 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
||||
this._deviceId = deviceId;
|
||||
|
||||
this._initialSyncCompleted = false;
|
||||
// userId -> deviceId -> true
|
||||
this._pendingNewDevices = {};
|
||||
// userId -> true
|
||||
this._pendingUsersWithNewDevices = {};
|
||||
// userId -> [promise, ...]
|
||||
this._keyDownloadsInProgressByUser = {};
|
||||
|
||||
this._olmDevice = new OlmDevice(sessionStore);
|
||||
|
||||
@@ -77,24 +79,32 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
||||
this._deviceKeys["curve25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceCurve25519Key;
|
||||
|
||||
// add our own deviceinfo to the sessionstore
|
||||
var deviceInfo = {
|
||||
keys: this._deviceKeys,
|
||||
algorithms: this._supportedAlgorithms,
|
||||
verified: DeviceVerification.VERIFIED,
|
||||
};
|
||||
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
||||
this._userId
|
||||
) || {};
|
||||
myDevices[this._deviceId] = deviceInfo;
|
||||
this._sessionStore.storeEndToEndDevicesForUser(
|
||||
this._userId, myDevices
|
||||
);
|
||||
|
||||
_registerEventHandlers(this, eventEmitter);
|
||||
if (!myDevices) {
|
||||
// we don't yet have a list of our own devices; make sure we
|
||||
// get one when we flush the pendingUsersWithNewDevices.
|
||||
this._pendingUsersWithNewDevices[this._userId] = true;
|
||||
myDevices = {};
|
||||
}
|
||||
|
||||
// map from userId -> deviceId -> roomId -> timestamp
|
||||
this._lastNewDeviceMessageTsByUserDeviceRoom = {};
|
||||
if (!myDevices[this._deviceId]) {
|
||||
// add our own deviceinfo to the sessionstore
|
||||
var deviceInfo = {
|
||||
keys: this._deviceKeys,
|
||||
algorithms: this._supportedAlgorithms,
|
||||
verified: DeviceVerification.VERIFIED,
|
||||
};
|
||||
|
||||
myDevices[this._deviceId] = deviceInfo;
|
||||
this._sessionStore.storeEndToEndDevicesForUser(
|
||||
this._userId, myDevices
|
||||
);
|
||||
}
|
||||
|
||||
_registerEventHandlers(this, eventEmitter);
|
||||
}
|
||||
|
||||
function _registerEventHandlers(crypto, eventEmitter) {
|
||||
@@ -274,52 +284,67 @@ function _uploadOneTimeKeys(crypto) {
|
||||
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||
var self = this;
|
||||
|
||||
// map from userid -> deviceid -> DeviceInfo
|
||||
var stored = {};
|
||||
function storeDev(userId, dev) {
|
||||
stored[userId][dev.deviceId] = dev;
|
||||
}
|
||||
// promises we need to wait for while the download happens
|
||||
var promises = [];
|
||||
|
||||
// list of userids we need to download keys for
|
||||
var downloadUsers = [];
|
||||
|
||||
function perUserCatch(u) {
|
||||
return function(e) {
|
||||
console.warn('Error downloading keys for user ' + u + ':', e);
|
||||
};
|
||||
}
|
||||
|
||||
if (forceDownload) {
|
||||
downloadUsers = userIds;
|
||||
} else {
|
||||
for (var i = 0; i < userIds.length; ++i) {
|
||||
var userId = userIds[i];
|
||||
var devices = this.getStoredDevicesForUser(userId);
|
||||
var u = userIds[i];
|
||||
|
||||
if (!devices) {
|
||||
downloadUsers.push(userId);
|
||||
} else {
|
||||
stored[userId] = {};
|
||||
devices.map(storeDev.bind(null, userId));
|
||||
var inprogress = this._keyDownloadsInProgressByUser[u];
|
||||
if (inprogress) {
|
||||
// wait for the download to complete
|
||||
promises.push(q.any(inprogress).catch(perUserCatch(u)));
|
||||
} else if (!this.getStoredDevicesForUser(u)) {
|
||||
downloadUsers.push(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadUsers.length === 0) {
|
||||
return q(stored);
|
||||
if (downloadUsers.length > 0) {
|
||||
var r = this._doKeyDownloadForUsers(downloadUsers);
|
||||
downloadUsers.map(function(u) {
|
||||
promises.push(r[u].catch(perUserCatch(u)));
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
return self._getDevicesFromStore(userIds);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the stored device keys for a list of user ids
|
||||
*
|
||||
* @param {string[]} userIds the list of users to list keys for.
|
||||
*
|
||||
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
Crypto.prototype._getDevicesFromStore = function(userIds) {
|
||||
var stored = {};
|
||||
var self = this;
|
||||
userIds.map(function(u) {
|
||||
stored[u] = {};
|
||||
var devices = self.getStoredDevicesForUser(u) || [];
|
||||
devices.map(function(dev) {
|
||||
stored[u][dev.deviceId] = dev;
|
||||
});
|
||||
});
|
||||
return stored;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string[]} downloadUsers list of userIds
|
||||
*
|
||||
@@ -334,8 +359,23 @@ Crypto.prototype._doKeyDownloadForUsers = function(downloadUsers) {
|
||||
var promiseMap = {};
|
||||
|
||||
downloadUsers.map(function(u) {
|
||||
deferMap[u] = q.defer();
|
||||
promiseMap[u] = deferMap[u].promise;
|
||||
var deferred = q.defer();
|
||||
var promise = deferred.promise.finally(function() {
|
||||
var inProgress = self._keyDownloadsInProgressByUser[u];
|
||||
utils.removeElement(inProgress, function(e) { return e === promise; });
|
||||
if (inProgress.length === 0) {
|
||||
// no more downloads for this user; remove the element
|
||||
delete self._keyDownloadsInProgressByUser[u];
|
||||
}
|
||||
});
|
||||
|
||||
if (!self._keyDownloadsInProgressByUser[u]) {
|
||||
self._keyDownloadsInProgressByUser[u] = [];
|
||||
}
|
||||
self._keyDownloadsInProgressByUser[u].push(promise);
|
||||
|
||||
deferMap[u] = deferred;
|
||||
promiseMap[u] = promise;
|
||||
});
|
||||
|
||||
this._baseApis.downloadKeysForUsers(
|
||||
@@ -919,58 +959,6 @@ Crypto.prototype.decryptEvent = function(event) {
|
||||
alg.decryptEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a "m.new_device" message to remind it that we exist and are a member
|
||||
* of a room.
|
||||
*
|
||||
* This is rate limited to send a message at most once an hour per desination.
|
||||
*
|
||||
* @param {string} userId The ID of the user to ping.
|
||||
* @param {string?} deviceId The ID of the device to ping. If null, all
|
||||
* devices.
|
||||
* @param {string} roomId The ID of the room we want to remind them about.
|
||||
*/
|
||||
Crypto.prototype._sendPingToDevice = function(userId, deviceId, roomId) {
|
||||
if (deviceId === null) {
|
||||
deviceId = "*";
|
||||
}
|
||||
|
||||
var lastMessageTsMap = this._lastNewDeviceMessageTsByUserDeviceRoom;
|
||||
|
||||
var lastTsByDevice = lastMessageTsMap[userId];
|
||||
if (!lastTsByDevice) {
|
||||
lastTsByDevice = lastMessageTsMap[userId] = {};
|
||||
}
|
||||
|
||||
var lastTsByRoom = lastTsByDevice[deviceId];
|
||||
if (!lastTsByRoom) {
|
||||
lastTsByRoom = lastTsByDevice[deviceId] = {};
|
||||
}
|
||||
|
||||
var lastTs = lastTsByRoom[roomId];
|
||||
var timeNowMs = Date.now();
|
||||
var oneHourMs = 1000 * 60 * 60;
|
||||
|
||||
if (lastTs !== undefined && lastTs + oneHourMs > timeNowMs) {
|
||||
// rate-limiting
|
||||
return;
|
||||
}
|
||||
|
||||
var content = {};
|
||||
content[userId] = {};
|
||||
content[userId][deviceId] = {
|
||||
device_id: this._deviceId,
|
||||
rooms: [roomId],
|
||||
};
|
||||
|
||||
this._baseApis.sendToDevice(
|
||||
"m.new_device", // OH HAI!
|
||||
content
|
||||
).done();
|
||||
|
||||
lastTsByRoom[roomId] = timeNowMs;
|
||||
};
|
||||
|
||||
/**
|
||||
* handle an m.room.encryption event
|
||||
*
|
||||
@@ -1134,8 +1122,7 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendingNewDevices[userId] = this._pendingNewDevices[userId] || {};
|
||||
this._pendingNewDevices[userId][deviceId] = true;
|
||||
this._pendingUsersWithNewDevices[userId] = true;
|
||||
|
||||
// we delay handling these until the intialsync has completed, so that we
|
||||
// can do all of them together.
|
||||
@@ -1150,10 +1137,7 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
|
||||
Crypto.prototype._flushNewDeviceRequests = function() {
|
||||
var self = this;
|
||||
|
||||
var pending = this._pendingNewDevices;
|
||||
var users = utils.keys(pending).filter(function(u) {
|
||||
return utils.keys(pending[u]).length > 0;
|
||||
});
|
||||
var users = utils.keys(this._pendingUsersWithNewDevices);
|
||||
|
||||
if (users.length === 0) {
|
||||
return;
|
||||
@@ -1163,7 +1147,7 @@ Crypto.prototype._flushNewDeviceRequests = function() {
|
||||
|
||||
// we've kicked off requests to these users: remove their
|
||||
// pending flag for now.
|
||||
this._pendingNewDevices = {};
|
||||
this._pendingUsersWithNewDevices = {};
|
||||
|
||||
users.map(function(u) {
|
||||
r[u] = r[u].catch(function(e) {
|
||||
@@ -1175,8 +1159,7 @@ Crypto.prototype._flushNewDeviceRequests = function() {
|
||||
// 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]);
|
||||
self._pendingUsersWithNewDevices[u] = true;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -159,9 +159,10 @@ module.exports.ensureOlmSessionsForDevices = function(
|
||||
return baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm
|
||||
).then(function(res) {
|
||||
var otk_res = res.one_time_keys || {};
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
|
||||
var userRes = res.one_time_keys[userId] || {};
|
||||
var userRes = otk_res[userId] || {};
|
||||
var devices = devicesByUser[userId];
|
||||
for (var j = 0; j < devices.length; j++) {
|
||||
var deviceInfo = devices[j];
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.7.1-rc.1",
|
||||
"version": "0.7.2",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -56,6 +56,6 @@
|
||||
"watchify": "^3.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-1.3.0.tgz"
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz"
|
||||
}
|
||||
}
|
||||
|
||||
+40
-5
@@ -63,6 +63,8 @@ if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
fi
|
||||
latest_changes=`mktemp`
|
||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
@@ -114,16 +116,24 @@ fi
|
||||
|
||||
set -x
|
||||
|
||||
# Bump package.json, build the dist, and tag
|
||||
# Bump package.json and build the dist
|
||||
echo "npm version"
|
||||
# npm version will automatically commit its modification
|
||||
# and make a release tag. We don't want it to create the tag
|
||||
# because github will do that, but we can only turn off both
|
||||
# of these behaviours, so we have to manually commit the
|
||||
# result.
|
||||
# because it can only sign with the default key, but we can
|
||||
# only turn off both of these behaviours, so we have to
|
||||
# manually commit the result.
|
||||
npm version --no-git-tag-version "$release"
|
||||
git commit package.json -m "$tag"
|
||||
|
||||
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
|
||||
fi
|
||||
|
||||
|
||||
# If there is a 'dist' script in the package.json,
|
||||
# run it in a separate checkout of the project, then
|
||||
# upload any files in the 'dist' directory as release
|
||||
@@ -145,9 +155,16 @@ if [ $dodist -eq 0 ]; then
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" npm run dist
|
||||
|
||||
popd
|
||||
|
||||
for i in "$builddir"/dist/*; do
|
||||
assets="$assets -a $i"
|
||||
if [ -n "$signing_id" ]
|
||||
then
|
||||
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
|
||||
assets="$assets -a $i.asc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
@@ -155,15 +172,33 @@ fi
|
||||
# a branch it doesn't have)
|
||||
git push origin "$rel_branch"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
|
||||
# push the tag
|
||||
git push origin "$tag"
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
hub release create $hubflags $assets -m "$tag" "$tag"
|
||||
|
||||
release_text=`mktemp`
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${release_text}"
|
||||
cat "${latest_changes}" >> "${release_text}"
|
||||
hub release create $hubflags $assets -f "${release_text}" "$tag"
|
||||
|
||||
if [ $dodist -eq 0 ]; then
|
||||
rm -rf "$builddir"
|
||||
fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Outputs the body of the first entry of changelog file on stdin
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
found_first_header = False
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if re.match(r"^Changes in \[.*\]", line):
|
||||
if found_first_header:
|
||||
break
|
||||
found_first_header = True
|
||||
elif not re.match(r"^=+$", line) and len(line) > 0:
|
||||
print line
|
||||
@@ -383,6 +383,15 @@ function recvMessage(httpBackend, client, sender, message) {
|
||||
|
||||
function aliStartClient() {
|
||||
expectAliKeyUpload().catch(test_utils.failTest);
|
||||
|
||||
// ali will try to query her own keys on start
|
||||
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[aliUserId] = {};
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
startClient(aliHttpBackend, aliClient);
|
||||
return aliHttpBackend.flush().then(function() {
|
||||
console.log("Ali client started");
|
||||
@@ -391,6 +400,15 @@ function aliStartClient() {
|
||||
|
||||
function bobStartClient() {
|
||||
expectBobKeyUpload().catch(test_utils.failTest);
|
||||
|
||||
// bob will try to query his own keys on start
|
||||
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
startClient(bobHttpBackend, bobClient);
|
||||
return bobHttpBackend.flush().then(function() {
|
||||
console.log("Bob client started");
|
||||
|
||||
+498
-222
@@ -29,6 +29,8 @@ var utils = require('../../lib/utils');
|
||||
var test_utils = require('../test-utils');
|
||||
var MockHttpBackend = require('../mock-request');
|
||||
|
||||
var ROOM_ID = "!room:id";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
@@ -59,12 +61,25 @@ function TestClient(userId, deviceId, accessToken) {
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*
|
||||
* @param {object?} deviceQueryResponse the list of our existing devices to return from
|
||||
* the /query request. Defaults to empty device list
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function() {
|
||||
TestClient.prototype.start = function(existingDevices) {
|
||||
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/query').respond(200, function(path, content) {
|
||||
expect(content.device_keys[self.userId]).toEqual({});
|
||||
var res = existingDevices;
|
||||
if (!res) {
|
||||
res = { device_keys: {} };
|
||||
res.device_keys[self.userId] = {};
|
||||
}
|
||||
return res;
|
||||
});
|
||||
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).not.toBeDefined();
|
||||
expect(content.device_keys).toBeDefined();
|
||||
@@ -243,19 +258,122 @@ function encryptGroupSessionKey(opts) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* get a /sync response which contains a single room (ROOM_ID),
|
||||
* with the members given
|
||||
*
|
||||
* @param {string[]} roomMembers
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function getSyncResponse(roomMembers) {
|
||||
var roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
test_utils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: roomMembers[i],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
var syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = roomResponse;
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("megolm", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ROOM_ID = "!room:id";
|
||||
|
||||
var testOlmAccount;
|
||||
var testSenderKey;
|
||||
var aliceTestClient;
|
||||
var testDeviceKeys;
|
||||
|
||||
beforeEach(function(done) {
|
||||
/**
|
||||
* Get the device keys for testOlmAccount in a format suitable for a
|
||||
* response to /keys/query
|
||||
*/
|
||||
function getTestKeysQueryResponse(userId) {
|
||||
var testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
var 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: userId,
|
||||
};
|
||||
var j = anotherjson.stringify(testDeviceKeys);
|
||||
var sig = testOlmAccount.sign(j);
|
||||
testDeviceKeys.signatures = {};
|
||||
testDeviceKeys.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
var queryResponse = {
|
||||
device_keys: {},
|
||||
};
|
||||
|
||||
queryResponse.device_keys[userId] = {
|
||||
'DEVICE_ID': testDeviceKeys,
|
||||
};
|
||||
|
||||
return queryResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a one-time key for testOlmAccount in a format suitable for a
|
||||
* response to /keys/claim
|
||||
*/
|
||||
function getTestKeysClaimResponse(userId) {
|
||||
testOlmAccount.generate_one_time_keys(1);
|
||||
var testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
var keyId = utils.keys(testOneTimeKeys.curve25519)[0];
|
||||
var oneTimeKey = testOneTimeKeys.curve25519[keyId];
|
||||
var keyResult = {
|
||||
'key': oneTimeKey,
|
||||
};
|
||||
var j = anotherjson.stringify(keyResult);
|
||||
var sig = testOlmAccount.sign(j);
|
||||
keyResult.signatures = {};
|
||||
keyResult.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
var claimResponse = {one_time_keys: {}};
|
||||
claimResponse.one_time_keys[userId] = {
|
||||
'DEVICE_ID': {},
|
||||
};
|
||||
claimResponse.one_time_keys[userId].DEVICE_ID['signed_curve25519:' + keyId] =
|
||||
keyResult;
|
||||
return claimResponse;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
test_utils.beforeEach(this);
|
||||
|
||||
aliceTestClient = new TestClient(
|
||||
@@ -266,25 +384,6 @@ describe("megolm", function() {
|
||||
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() {
|
||||
@@ -292,44 +391,47 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", function(done) {
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
return aliceTestClient.start().then(function() {
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
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,
|
||||
});
|
||||
// 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,
|
||||
});
|
||||
// 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() {
|
||||
// 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');
|
||||
@@ -337,65 +439,67 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
it("Alice gets a second room_key message", function(done) {
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
return aliceTestClient.start().then(function() {
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
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,
|
||||
});
|
||||
// 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,
|
||||
});
|
||||
// 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,
|
||||
});
|
||||
// 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 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);
|
||||
// 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() {
|
||||
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');
|
||||
@@ -403,54 +507,30 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
it('Alice sends a megolm message', function(done) {
|
||||
// establish an olm session with alice
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
var p2pSession;
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
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);
|
||||
// establish an olm session with alice
|
||||
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
var inboundGroupSession;
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
@@ -468,9 +548,11 @@ describe("megolm", function() {
|
||||
'PUT', '/send/'
|
||||
).respond(200, function(path, content) {
|
||||
var ct = content.ciphertext;
|
||||
var decrypted = JSON.parse(inboundGroupSession.decrypt(ct));
|
||||
var r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
|
||||
console.log('Decrypted received megolm message', decrypted);
|
||||
expect(r.message_index).toEqual(0);
|
||||
var decrypted = JSON.parse(r.plaintext);
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
|
||||
@@ -487,32 +569,12 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
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.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
|
||||
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, {
|
||||
@@ -543,54 +605,28 @@ describe("megolm", function() {
|
||||
|
||||
|
||||
it("We shouldn't attempt to send to blocked devices", function(done) {
|
||||
// establish an olm session with alice
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
// establish an olm session with alice
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [olmEvent],
|
||||
},
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
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);
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
|
||||
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,
|
||||
},
|
||||
}
|
||||
});
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
@@ -614,4 +650,244 @@ describe("megolm", function() {
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("We should start a new megolm session when a device is blocked", function(done) {
|
||||
var p2pSession;
|
||||
var megolmSessionId;
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to send a megolm message');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log('sendToDevice: ', content);
|
||||
var m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(1); // normal message
|
||||
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
console.log('decrypted sendToDevice:', decrypted);
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
megolmSessionId = decrypted.content.session_id;
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log('/send:', content);
|
||||
expect(content.session_id).toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
console.log('Telling alice to send another megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log('/send:', content);
|
||||
expect(content.session_id).not.toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/2676
|
||||
it("Alice should send to her other devices", function(done) {
|
||||
// for this test, we make the testOlmAccount be another of Alice's devices.
|
||||
// it ought to get include in messages Alice sends.
|
||||
|
||||
var p2pSession;
|
||||
var inboundGroupSession;
|
||||
var decrypted;
|
||||
|
||||
return aliceTestClient.start(
|
||||
getTestKeysQueryResponse(aliceTestClient.userId)
|
||||
).then(function() {
|
||||
// an encrypted room with just alice
|
||||
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: aliceTestClient.userId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush();
|
||||
}).then(function() {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
|
||||
200, function(path, content)
|
||||
{
|
||||
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
|
||||
.toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId);
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log("sendToDevice: ", content);
|
||||
var m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
|
||||
p2pSession = new Olm.Session();
|
||||
p2pSession.create_inbound(testOlmAccount, ct.body);
|
||||
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 r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
decrypted = JSON.parse(r.plaintext);
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
|
||||
it('Alice should wait for device list to complete when sending a megolm message',
|
||||
function(done) {
|
||||
var p2pSession;
|
||||
var inboundGroupSession;
|
||||
|
||||
var downloadPromise;
|
||||
var sendPromise;
|
||||
|
||||
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 r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
|
||||
expect(r.message_index).toEqual(0);
|
||||
var decrypted = JSON.parse(r.plaintext);
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
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');
|
||||
|
||||
// this will block
|
||||
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(function() {
|
||||
|
||||
// so will this.
|
||||
sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
|
||||
}).then(function() {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
return aliceTestClient.httpBackend.flush();
|
||||
}).then(function() {
|
||||
return q.all([downloadPromise, sendPromise]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,6 @@ describe("Crypto", function() {
|
||||
}
|
||||
|
||||
it("Crypto exposes the correct olm library version", function() {
|
||||
expect(Crypto.getOlmVersion()).toEqual([1, 3, 0]);
|
||||
expect(Crypto.getOlmVersion()).toEqual([2, 0, 0]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user