You've already forked matrix-js-sdk
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:
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- node # Latest stable version of nodejs.
|
||||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -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)
|
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)
|
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.2...v0.6.4)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
../../../dist/browser-matrix-dev.js
|
../../../dist/browser-matrix.js
|
||||||
@@ -135,11 +135,15 @@ rl.on('line', function(line) {
|
|||||||
// ==== END User input
|
// ==== END User input
|
||||||
|
|
||||||
// show the room list after syncing.
|
// show the room list after syncing.
|
||||||
matrixClient.on("syncComplete", function() {
|
matrixClient.on("sync", function(state, prevState, data) {
|
||||||
|
switch (state) {
|
||||||
|
case "PREPARED":
|
||||||
setRoomList();
|
setRoomList();
|
||||||
printRoomList();
|
printRoomList();
|
||||||
printHelp();
|
printHelp();
|
||||||
rl.prompt();
|
rl.prompt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
matrixClient.on("Room", function() {
|
matrixClient.on("Room", function() {
|
||||||
|
|||||||
@@ -44,7 +44,15 @@ window.onload = function() {
|
|||||||
disableButtons(true, true, true);
|
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>";
|
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
|
||||||
disableButtons(false, true, true);
|
disableButtons(false, true, true);
|
||||||
|
|
||||||
@@ -85,5 +93,5 @@ client.on("syncComplete", function () {
|
|||||||
call = c;
|
call = c;
|
||||||
addListeners(call);
|
addListeners(call);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
client.startClient();
|
client.startClient();
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
../../../dist/browser-matrix-dev.js
|
../../../dist/browser-matrix.js
|
||||||
@@ -351,7 +351,7 @@ MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
|
|||||||
if (this._crypto === null) {
|
if (this._crypto === null) {
|
||||||
throw new Error("End-to-end encryption disabled");
|
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.
|
* Decrypt a received event according to the algorithm specified in the event.
|
||||||
*
|
*
|
||||||
* @param {MatrixClient} client
|
* @param {MatrixClient} client
|
||||||
* @param {object} raw event
|
* @param {MatrixEvent} event
|
||||||
*
|
|
||||||
* @return {MatrixEvent}
|
|
||||||
*/
|
*/
|
||||||
function _decryptEvent(client, event) {
|
function _decryptEvent(client, event) {
|
||||||
if (!client._crypto) {
|
if (!client._crypto) {
|
||||||
return _badEncryptedMessage(event, "**Encryption not enabled**");
|
_badEncryptedMessage(event, "**Encryption not enabled**");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var decryptionResult;
|
|
||||||
try {
|
try {
|
||||||
decryptionResult = client._crypto.decryptEvent(event);
|
client._crypto.decryptEvent(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!(e instanceof Crypto.DecryptionError)) {
|
if (!(e instanceof Crypto.DecryptionError)) {
|
||||||
throw e;
|
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) {
|
function _badEncryptedMessage(event, reason) {
|
||||||
return new MatrixEvent(event, {
|
event.setClearData({
|
||||||
type: "m.room.message",
|
type: "m.room.message",
|
||||||
content: {
|
content: {
|
||||||
msgtype: "m.bad.encrypted",
|
msgtype: "m.bad.encrypted",
|
||||||
body: reason,
|
body: reason,
|
||||||
content: event.content,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2829,10 +2822,11 @@ function _resolve(callback, defer, res) {
|
|||||||
|
|
||||||
function _PojoToMatrixEventMapper(client) {
|
function _PojoToMatrixEventMapper(client) {
|
||||||
function mapper(plainOldJsObject) {
|
function mapper(plainOldJsObject) {
|
||||||
if (plainOldJsObject.type === "m.room.encrypted") {
|
var event = new MatrixEvent(plainOldJsObject);
|
||||||
return _decryptEvent(client, plainOldJsObject);
|
if (event.isEncrypted()) {
|
||||||
|
_decryptEvent(client, event);
|
||||||
}
|
}
|
||||||
return new MatrixEvent(plainOldJsObject);
|
return event;
|
||||||
}
|
}
|
||||||
return mapper;
|
return mapper;
|
||||||
}
|
}
|
||||||
@@ -2872,6 +2866,10 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires whenever the SDK receives a new event.
|
* 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"
|
* @event module:client~MatrixClient#"event"
|
||||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||||
* @example
|
* @example
|
||||||
|
|||||||
@@ -627,6 +627,24 @@ OlmDevice.prototype.addInboundGroupSession = function(
|
|||||||
roomId, senderKey, sessionId, sessionKey, keysClaimed
|
roomId, senderKey, sessionId, sessionKey, keysClaimed
|
||||||
) {
|
) {
|
||||||
var self = this;
|
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();
|
var session = new Olm.InboundGroupSession();
|
||||||
try {
|
try {
|
||||||
session.create(sessionKey);
|
session.create(sessionKey);
|
||||||
|
|||||||
@@ -88,15 +88,6 @@ EncryptionAlgorithm.prototype.onRoomMembership = function(
|
|||||||
event, member, oldMembership
|
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
|
* base type for decryption implementations
|
||||||
*
|
*
|
||||||
@@ -105,11 +96,16 @@ EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
|
|||||||
*
|
*
|
||||||
* @param {object} params parameters
|
* @param {object} params parameters
|
||||||
* @param {string} params.userId The UserID for the local user
|
* @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 {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) {
|
var DecryptionAlgorithm = function(params) {
|
||||||
this._userId = params.userId;
|
this._userId = params.userId;
|
||||||
|
this._crypto = params.crypto;
|
||||||
this._olmDevice = params.olmDevice;
|
this._olmDevice = params.olmDevice;
|
||||||
|
this._roomId = params.roomId;
|
||||||
};
|
};
|
||||||
/** */
|
/** */
|
||||||
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
|
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
|
||||||
|
|||||||
@@ -38,12 +38,17 @@ var base = require("./base");
|
|||||||
* @property {Number} creationTime when the session was created (ms since the epoch)
|
* @property {Number} creationTime when the session was created (ms since the epoch)
|
||||||
* @property {module:client.Promise?} sharePromise If a share operation is in progress,
|
* @property {module:client.Promise?} sharePromise If a share operation is in progress,
|
||||||
* a promise which resolves when it is complete.
|
* 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) {
|
function OutboundSessionInfo(sessionId) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.useCount = 0;
|
this.useCount = 0;
|
||||||
this.creationTime = new Date().getTime();
|
this.creationTime = new Date().getTime();
|
||||||
this.sharePromise = null;
|
this.sharePromise = null;
|
||||||
|
this.sharedWithDevices = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -90,11 +95,6 @@ function MegolmEncryption(params) {
|
|||||||
// case _outboundSession.sharePromise will be non-null.)
|
// case _outboundSession.sharePromise will be non-null.)
|
||||||
this._outboundSession = 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
|
// default rotation periods
|
||||||
this._sessionRotationPeriodMsgs = 100;
|
this._sessionRotationPeriodMsgs = 100;
|
||||||
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
||||||
@@ -134,32 +134,55 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
|||||||
return session.sharePromise;
|
return session.sharePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no share in progress: check for new devices
|
// no share in progress: check if we need to share with any devices
|
||||||
var shareMap = this._devicesPendingKeyShare;
|
var prom = this._getDevicesInRoom(room).then(function(devicesInRoom) {
|
||||||
this._devicesPendingKeyShare = {};
|
var shareMap = {};
|
||||||
|
|
||||||
// check each user is (still) a member of the room
|
for (var userId in devicesInRoom) {
|
||||||
for (var userId in shareMap) {
|
if (!devicesInRoom.hasOwnProperty(userId)) {
|
||||||
if (!shareMap.hasOwnProperty(userId)) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX what about rooms where invitees can see the content?
|
var userDevices = devicesInRoom[userId];
|
||||||
var member = room.getMember(userId);
|
|
||||||
if (member.membership !== "join") {
|
for (var deviceId in userDevices) {
|
||||||
delete shareMap[userId];
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
session.sharePromise = this._shareKeyWithDevices(
|
return self._shareKeyWithDevices(
|
||||||
session.sessionId, shareMap
|
session, shareMap
|
||||||
).finally(function() {
|
);
|
||||||
|
}).finally(function() {
|
||||||
session.sharePromise = null;
|
session.sharePromise = null;
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
return session;
|
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}
|
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
|
||||||
);
|
);
|
||||||
|
|
||||||
// we're going to share the key with all current members of the room,
|
return new OutboundSessionInfo(session_id);
|
||||||
// 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
*
|
*
|
||||||
* @param {string} session_id
|
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||||
*
|
*
|
||||||
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
|
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||||
* Map from userid to either: true (meaning this is a new user in the room,
|
* map from userid to list of devices
|
||||||
* 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).
|
|
||||||
*
|
*
|
||||||
* @return {module:client.Promise} Promise which resolves once the key sharing
|
* @return {module:client.Promise} Promise which resolves once the key sharing
|
||||||
* message has been sent.
|
* message has been sent.
|
||||||
*/
|
*/
|
||||||
MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) {
|
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
|
var key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
|
||||||
var payload = {
|
var payload = {
|
||||||
type: "m.room_key",
|
type: "m.room_key",
|
||||||
content: {
|
content: {
|
||||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||||
room_id: this._roomId,
|
room_id: this._roomId,
|
||||||
session_id: session_id,
|
session_id: session.sessionId,
|
||||||
session_key: key.key,
|
session_key: key.key,
|
||||||
chain_index: key.chain_index,
|
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.
|
|
||||||
|
|
||||||
return self._crypto.ensureOlmSessionsForUsers(
|
|
||||||
utils.keys(shareMap)
|
|
||||||
).then(function(devicemap) {
|
|
||||||
var contentMap = {};
|
var contentMap = {};
|
||||||
|
|
||||||
|
return olmlib.ensureOlmSessionsForDevices(
|
||||||
|
this._olmDevice, this._baseApis, devicesByUser
|
||||||
|
).then(function(devicemap) {
|
||||||
var haveTargets = false;
|
var haveTargets = false;
|
||||||
|
|
||||||
for (var userId in devicemap) {
|
for (var userId in devicesByUser) {
|
||||||
if (!devicemap.hasOwnProperty(userId)) {
|
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var devicesToShareWith = shareMap[userId];
|
var devicesToShareWith = devicesByUser[userId];
|
||||||
var sessionResults = devicemap[userId];
|
var sessionResults = devicemap[userId];
|
||||||
|
|
||||||
for (var deviceId in sessionResults) {
|
for (var i = 0; i < devicesToShareWith.length; i++) {
|
||||||
if (!sessionResults.hasOwnProperty(deviceId)) {
|
var deviceInfo = devicesToShareWith[i];
|
||||||
continue;
|
var deviceId = deviceInfo.deviceId;
|
||||||
}
|
|
||||||
|
|
||||||
if (devicesToShareWith === true) {
|
|
||||||
// all devices
|
|
||||||
} else if (!devicesToShareWith[deviceId]) {
|
|
||||||
// not a new device
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sessionResult = sessionResults[deviceId];
|
var sessionResult = sessionResults[deviceId];
|
||||||
if (!sessionResult.sessionId) {
|
if (!sessionResult.sessionId) {
|
||||||
@@ -288,8 +269,6 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
|
|||||||
"sharing keys with device " + userId + ":" + deviceId
|
"sharing keys with device " + userId + ":" + deviceId
|
||||||
);
|
);
|
||||||
|
|
||||||
var deviceInfo = sessionResult.device;
|
|
||||||
|
|
||||||
var encryptedContent = {
|
var encryptedContent = {
|
||||||
algorithm: olmlib.OLM_ALGORITHM,
|
algorithm: olmlib.OLM_ALGORITHM,
|
||||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||||
@@ -321,6 +300,27 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
|
|||||||
|
|
||||||
// TODO: retries
|
// TODO: retries
|
||||||
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
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
|
* @param {string=} oldMembership previous membership
|
||||||
*/
|
*/
|
||||||
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
|
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;
|
var newMembership = member.membership;
|
||||||
|
|
||||||
if (newMembership === 'join') {
|
if (newMembership === 'join' || newMembership === 'invite') {
|
||||||
this._onNewRoomMember(member.userId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newMembership === 'invite' && oldMembership !== 'join') {
|
|
||||||
// we don't (yet) share keys with invited members, so nothing to do yet
|
|
||||||
return;
|
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
|
* @param {module:models/room} room
|
||||||
*/
|
|
||||||
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 {string} userId owner of the device
|
* @return {module:client.Promise} Promise which resolves to a map
|
||||||
* @param {string} deviceId deviceId of the device
|
* from userId to deviceId to deviceInfo
|
||||||
*/
|
*/
|
||||||
MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
|
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
|
||||||
var d = this._devicesPendingKeyShare[userId];
|
// 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 are happy to use a cached version here: we assume that if we already
|
||||||
// we already want to share keys with all devices for this user
|
// have a list of the user's devices, then we already share an e2e room
|
||||||
return;
|
// with them, which means that they will have announced any new devices via
|
||||||
}
|
// an m.new_device.
|
||||||
|
return this._crypto.downloadKeys(roomMembers, false);
|
||||||
if (!d) {
|
|
||||||
this._devicesPendingKeyShare[userId] = d = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
d[deviceId] = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Megolm decryption implementation
|
* Megolm decryption implementation
|
||||||
*
|
*
|
||||||
@@ -445,13 +416,17 @@ MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
|
|||||||
*/
|
*/
|
||||||
function MegolmDecryption(params) {
|
function MegolmDecryption(params) {
|
||||||
base.DecryptionAlgorithm.call(this, 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);
|
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*
|
*
|
||||||
* @param {object} event raw event
|
* @param {MatrixEvent} event
|
||||||
*
|
*
|
||||||
* @return {null} The event referred to an unknown megolm session
|
* @return {null} The event referred to an unknown megolm session
|
||||||
* @return {module:crypto.DecryptionResult} decryption result
|
* @return {module:crypto.DecryptionResult} decryption result
|
||||||
@@ -460,7 +435,7 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
|||||||
* problem decrypting the event
|
* problem decrypting the event
|
||||||
*/
|
*/
|
||||||
MegolmDecryption.prototype.decryptEvent = function(event) {
|
MegolmDecryption.prototype.decryptEvent = function(event) {
|
||||||
var content = event.content;
|
var content = event.getWireContent();
|
||||||
|
|
||||||
if (!content.sender_key || !content.session_id ||
|
if (!content.sender_key || !content.session_id ||
|
||||||
!content.ciphertext
|
!content.ciphertext
|
||||||
@@ -471,14 +446,19 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
var res;
|
var res;
|
||||||
try {
|
try {
|
||||||
res = this._olmDevice.decryptGroupMessage(
|
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) {
|
} catch (e) {
|
||||||
|
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||||
|
this._addEventToPendingList(event);
|
||||||
|
}
|
||||||
throw new base.DecryptionError(e);
|
throw new base.DecryptionError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res === null) {
|
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);
|
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
|
// 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
|
// (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).
|
// 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(
|
throw new base.DecryptionError(
|
||||||
"Message intended for room " + payload.room_id
|
"Message intended for room " + payload.room_id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
event.setClearData(payload, res.keysProved, res.keysClaimed);
|
||||||
payload: payload,
|
|
||||||
keysClaimed: res.keysClaimed,
|
|
||||||
keysProved: res.keysProved,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.room_id, event.getSenderKey(), content.session_id,
|
||||||
content.session_key, event.getKeysClaimed()
|
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(
|
base.registerAlgorithm(
|
||||||
|
|||||||
@@ -151,15 +151,13 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
|
|||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*
|
*
|
||||||
* @param {object} event raw event
|
* @param {MatrixEvent} event
|
||||||
*
|
|
||||||
* @return {module:crypto.DecryptionResult} decryption result
|
|
||||||
*
|
*
|
||||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||||
* problem decrypting the event
|
* problem decrypting the event
|
||||||
*/
|
*/
|
||||||
OlmDecryption.prototype.decryptEvent = function(event) {
|
OlmDecryption.prototype.decryptEvent = function(event) {
|
||||||
var content = event.content;
|
var content = event.getWireContent();
|
||||||
var deviceKey = content.sender_key;
|
var deviceKey = content.sender_key;
|
||||||
var ciphertext = content.ciphertext;
|
var ciphertext = content.ciphertext;
|
||||||
|
|
||||||
@@ -178,7 +176,7 @@ OlmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Failed to decrypt Olm event (id=" +
|
"Failed to decrypt Olm event (id=" +
|
||||||
event.event_id + ") from " + deviceKey +
|
event.getId() + ") from " + deviceKey +
|
||||||
": " + e.message
|
": " + e.message
|
||||||
);
|
);
|
||||||
throw new base.DecryptionError("Bad Encrypted 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
|
// check that we were the intended recipient, to avoid unknown-key attack
|
||||||
// https://github.com/vector-im/vector-web/issues/2483
|
// https://github.com/vector-im/vector-web/issues/2483
|
||||||
if (payload.recipient === undefined) {
|
if (payload.recipient != this._userId) {
|
||||||
// older versions of riot did not set this field, so we cannot make
|
|
||||||
// this check. TODO: kill this off once our users have updated
|
|
||||||
console.warn(
|
console.warn(
|
||||||
"Olm event (id=" + event.event_id + ") contains no 'recipient' " +
|
"Event " + event.getId() + ": Intended recipient " +
|
||||||
"property; cannot prevent unknown-key attack");
|
|
||||||
} else if (payload.recipient != this._userId) {
|
|
||||||
console.warn(
|
|
||||||
"Event " + event.event_id + ": Intended recipient " +
|
|
||||||
payload.recipient + " does not match our id " + this._userId
|
payload.recipient + " does not match our id " + this._userId
|
||||||
);
|
);
|
||||||
throw new base.DecryptionError(
|
throw new base.DecryptionError(
|
||||||
@@ -204,15 +196,10 @@ OlmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.recipient_keys === undefined) {
|
if (payload.recipient_keys.ed25519 !=
|
||||||
// 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 !=
|
|
||||||
this._olmDevice.deviceEd25519Key) {
|
this._olmDevice.deviceEd25519Key) {
|
||||||
console.warn(
|
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"
|
payload.recipient_keys.ed25519 + " did not match ours"
|
||||||
);
|
);
|
||||||
throw new base.DecryptionError("Message not intended for this device");
|
throw new base.DecryptionError("Message not intended for this device");
|
||||||
@@ -222,15 +209,10 @@ OlmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
// avoid people masquerading as others.
|
// avoid people masquerading as others.
|
||||||
// (this check is also provided via the sender's embedded ed25519 key,
|
// (this check is also provided via the sender's embedded ed25519 key,
|
||||||
// which is checked elsewhere).
|
// which is checked elsewhere).
|
||||||
if (payload.sender === undefined) {
|
if (payload.sender != event.getSender()) {
|
||||||
// ditto
|
|
||||||
console.warn(
|
console.warn(
|
||||||
"Olm event (id=" + event.event_id + ") contains no " +
|
"Event " + event.getId() + ": original sender " + payload.sender +
|
||||||
"'sender' property; cannot prevent unknown-key attack");
|
" does not match reported sender " + event.getSender()
|
||||||
} else if (payload.sender != event.sender) {
|
|
||||||
console.warn(
|
|
||||||
"Event " + event.event_id + ": original sender " + payload.sender +
|
|
||||||
" does not match reported sender " + event.sender
|
|
||||||
);
|
);
|
||||||
throw new base.DecryptionError(
|
throw new base.DecryptionError(
|
||||||
"Message forwarded from " + payload.sender
|
"Message forwarded from " + payload.sender
|
||||||
@@ -238,9 +220,9 @@ OlmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Olm events intended for a room have a room_id.
|
// 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(
|
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
|
" does not match reported room " + event.room_id
|
||||||
);
|
);
|
||||||
throw new base.DecryptionError(
|
throw new base.DecryptionError(
|
||||||
@@ -248,12 +230,7 @@ OlmDecryption.prototype.decryptEvent = function(event) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
event.setClearData(payload, {curve25519: deviceKey}, payload.keys || {});
|
||||||
payload: payload,
|
|
||||||
sessionExists: true,
|
|
||||||
keysProved: {curve25519: deviceKey},
|
|
||||||
keysClaimed: payload.keys || {}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,10 +54,17 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
|||||||
this._userId = userId;
|
this._userId = userId;
|
||||||
this._deviceId = deviceId;
|
this._deviceId = deviceId;
|
||||||
|
|
||||||
|
this._initialSyncCompleted = false;
|
||||||
|
// userId -> deviceId -> true
|
||||||
|
this._pendingNewDevices = {};
|
||||||
|
|
||||||
this._olmDevice = new OlmDevice(sessionStore);
|
this._olmDevice = new OlmDevice(sessionStore);
|
||||||
|
|
||||||
// EncryptionAlgorithm instance for each room
|
// EncryptionAlgorithm instance for each room
|
||||||
this._roomAlgorithms = {};
|
this._roomEncryptors = {};
|
||||||
|
|
||||||
|
// map from algorithm to DecryptionAlgorithm instance, for each room
|
||||||
|
this._roomDecryptors = {};
|
||||||
|
|
||||||
this._supportedAlgorithms = utils.keys(
|
this._supportedAlgorithms = utils.keys(
|
||||||
algorithms.DECRYPTION_CLASSES
|
algorithms.DECRYPTION_CLASSES
|
||||||
@@ -269,22 +276,26 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
|||||||
|
|
||||||
// map from userid -> deviceid -> DeviceInfo
|
// map from userid -> deviceid -> DeviceInfo
|
||||||
var stored = {};
|
var stored = {};
|
||||||
|
function storeDev(userId, dev) {
|
||||||
|
stored[userId][dev.deviceId] = dev;
|
||||||
|
}
|
||||||
|
|
||||||
// list of userids we need to download keys for
|
// list of userids we need to download keys for
|
||||||
var downloadUsers = [];
|
var downloadUsers = [];
|
||||||
|
|
||||||
|
if (forceDownload) {
|
||||||
|
downloadUsers = userIds;
|
||||||
|
} else {
|
||||||
for (var i = 0; i < userIds.length; ++i) {
|
for (var i = 0; i < userIds.length; ++i) {
|
||||||
var userId = userIds[i];
|
var userId = userIds[i];
|
||||||
stored[userId] = {};
|
|
||||||
|
|
||||||
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) {
|
if (!devices) {
|
||||||
downloadUsers.push(userId);
|
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 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
|
downloadUsers
|
||||||
).then(function(res) {
|
).done(function(res) {
|
||||||
for (var userId in res.device_keys) {
|
var dk = res.device_keys || {};
|
||||||
if (!stored.hasOwnProperty(userId)) {
|
|
||||||
// spurious result
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// map from deviceid -> deviceinfo for this user
|
// map from deviceid -> deviceinfo for this user
|
||||||
var userStore = stored[userId];
|
var userStore = {};
|
||||||
var updated = _updateStoredDeviceKeysForUser(
|
var devs = self._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
self._olmDevice, userId, userStore, res.device_keys[userId]
|
if (devs) {
|
||||||
);
|
for (deviceId in devs) {
|
||||||
|
if (devs.hasOwnProperty(deviceId)) {
|
||||||
if (!updated) {
|
var d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||||
continue;
|
userStore[deviceId] = d;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateStoredDeviceKeysForUser(
|
||||||
|
self._olmDevice, userId, userStore, dk[userId]
|
||||||
|
);
|
||||||
|
|
||||||
// update the session store
|
// update the session store
|
||||||
var storage = {};
|
var storage = {};
|
||||||
for (var deviceId in userStore) {
|
for (deviceId in userStore) {
|
||||||
if (!userStore.hasOwnProperty(deviceId)) {
|
if (!userStore.hasOwnProperty(deviceId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -323,9 +385,16 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
|||||||
self._sessionStore.storeEndToEndDevicesForUser(
|
self._sessionStore.storeEndToEndDevicesForUser(
|
||||||
userId, storage
|
userId, storage
|
||||||
);
|
);
|
||||||
|
|
||||||
|
deferMap[userId].resolve();
|
||||||
}
|
}
|
||||||
return stored;
|
}, function(err) {
|
||||||
|
downloadUsers.map(function(u) {
|
||||||
|
deferMap[u].reject(err);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promiseMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||||
@@ -399,7 +468,7 @@ function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
|||||||
var unsigned = deviceResult.unsigned || {};
|
var unsigned = deviceResult.unsigned || {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Unable to verify signature on device " +
|
console.log("Unable to verify signature on device " +
|
||||||
userId + ":" + deviceId + ":", e);
|
userId + ":" + deviceId + ":", e);
|
||||||
@@ -437,12 +506,13 @@ function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
|||||||
*
|
*
|
||||||
* @param {string} userId the user to list keys for.
|
* @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) {
|
Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
||||||
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||||
if (!devs) {
|
if (!devs) {
|
||||||
return [];
|
return null;
|
||||||
}
|
}
|
||||||
var res = [];
|
var res = [];
|
||||||
for (var deviceId in devs) {
|
for (var deviceId in devs) {
|
||||||
@@ -453,6 +523,22 @@ Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
|||||||
return res;
|
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
|
* List the stored device keys for a user id
|
||||||
@@ -465,7 +551,7 @@ Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
|||||||
* "key", and "display_name" parameters.
|
* "key", and "display_name" parameters.
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.listDeviceKeys = function(userId) {
|
Crypto.prototype.listDeviceKeys = function(userId) {
|
||||||
var devices = this.getStoredDevicesForUser(userId);
|
var devices = this.getStoredDevicesForUser(userId) || [];
|
||||||
|
|
||||||
var result = [];
|
var result = [];
|
||||||
|
|
||||||
@@ -597,7 +683,7 @@ Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified, bl
|
|||||||
* @return {Object.<string, {deviceIdKey: string, sessions: object[]}>}
|
* @return {Object.<string, {deviceIdKey: string, sessions: object[]}>}
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.getOlmSessionsForUser = function(userId) {
|
Crypto.prototype.getOlmSessionsForUser = function(userId) {
|
||||||
var devices = this.getStoredDevicesForUser(userId);
|
var devices = this.getStoredDevicesForUser(userId) || [];
|
||||||
var result = {};
|
var result = {};
|
||||||
for (var j = 0; j < devices.length; ++j) {
|
for (var j = 0; j < devices.length; ++j) {
|
||||||
var device = devices[j];
|
var device = devices[j];
|
||||||
@@ -705,7 +791,7 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
|||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
config: config,
|
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
|
* @param {string[]} users list of user ids
|
||||||
*
|
*
|
||||||
@@ -726,19 +813,15 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
|||||||
* {@link module:crypto~OlmSessionResult}
|
* {@link module:crypto~OlmSessionResult}
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
||||||
var devicesWithoutSession = [
|
var devicesByUser = {};
|
||||||
// [userId, deviceId, deviceInfo], ...
|
|
||||||
];
|
|
||||||
var result = {};
|
|
||||||
|
|
||||||
for (var i = 0; i < users.length; ++i) {
|
for (var i = 0; i < users.length; ++i) {
|
||||||
var userId = users[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) {
|
for (var j = 0; j < devices.length; ++j) {
|
||||||
var deviceInfo = devices[j];
|
var deviceInfo = devices[j];
|
||||||
var deviceId = deviceInfo.deviceId;
|
|
||||||
|
|
||||||
var key = deviceInfo.getIdentityKey();
|
var key = deviceInfo.getIdentityKey();
|
||||||
if (key == this._olmDevice.deviceCurve25519Key) {
|
if (key == this._olmDevice.deviceCurve25519Key) {
|
||||||
@@ -750,87 +833,13 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sessionId = this._olmDevice.getSessionIdForDevice(key);
|
devicesByUser[userId].push(deviceInfo);
|
||||||
if (sessionId === null) {
|
|
||||||
devicesWithoutSession.push([userId, deviceId, deviceInfo]);
|
|
||||||
}
|
|
||||||
result[userId][deviceId] = {
|
|
||||||
device: deviceInfo,
|
|
||||||
sessionId: sessionId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (devicesWithoutSession.length === 0) {
|
return olmlib.ensureOlmSessionsForDevices(
|
||||||
return q(result);
|
this._olmDevice, this._baseApis, devicesByUser
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -839,10 +848,9 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
|||||||
* @return {bool} whether encryption is enabled.
|
* @return {bool} whether encryption is enabled.
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.isRoomEncrypted = function(roomId) {
|
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.
|
* Encrypt an event according to the configuration of the room, if necessary.
|
||||||
*
|
*
|
||||||
@@ -862,18 +870,13 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getType() !== "m.room.message") {
|
|
||||||
// we only encrypt m.room.message
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||||
}
|
}
|
||||||
|
|
||||||
var roomId = event.getRoomId();
|
var roomId = event.getRoomId();
|
||||||
|
|
||||||
var alg = this._roomAlgorithms[roomId];
|
var alg = this._roomEncryptors[roomId];
|
||||||
if (!alg) {
|
if (!alg) {
|
||||||
// not encrypting messages in this room
|
// 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
|
* Decrypt a received event
|
||||||
*
|
*
|
||||||
* @param {object} event raw event
|
* @param {MatrixEvent} event
|
||||||
*
|
|
||||||
* @return {module:crypto.DecryptionResult} decryption result
|
|
||||||
*
|
*
|
||||||
* @raises {algorithms.DecryptionError} if there is a problem decrypting the event
|
* @raises {algorithms.DecryptionError} if there is a problem decrypting the event
|
||||||
*/
|
*/
|
||||||
Crypto.prototype.decryptEvent = function(event) {
|
Crypto.prototype.decryptEvent = function(event) {
|
||||||
var content = event.content;
|
var content = event.getWireContent();
|
||||||
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
|
var alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm);
|
||||||
if (!AlgClass) {
|
alg.decryptEvent(event);
|
||||||
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");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1044,6 +998,11 @@ Crypto.prototype._onCryptoEvent = function(event) {
|
|||||||
* @param {module:models/room[]} rooms list of rooms the client knows about
|
* @param {module:models/room[]} rooms list of rooms the client knows about
|
||||||
*/
|
*/
|
||||||
Crypto.prototype._onInitialSyncCompleted = function(rooms) {
|
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()) {
|
if (this._sessionStore.getDeviceAnnounced()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1056,7 +1015,7 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
|
|||||||
var room = rooms[i];
|
var room = rooms[i];
|
||||||
|
|
||||||
// check for rooms with encryption enabled
|
// check for rooms with encryption enabled
|
||||||
var alg = this._roomAlgorithms[room.roomId];
|
var alg = this._roomEncryptors[room.roomId];
|
||||||
if (!alg) {
|
if (!alg) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1110,16 +1069,13 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
|
|||||||
*/
|
*/
|
||||||
Crypto.prototype._onRoomKeyEvent = function(event) {
|
Crypto.prototype._onRoomKeyEvent = function(event) {
|
||||||
var content = event.getContent();
|
var content = event.getContent();
|
||||||
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
|
|
||||||
if (!AlgClass) {
|
if (!content.room_id || !content.algorithm) {
|
||||||
throw new algorithms.DecryptionError(
|
console.error("key event is missing fields");
|
||||||
"Unable to handle keys for " + content.algorithm
|
return;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
var alg = new AlgClass({
|
|
||||||
userId: this._userId,
|
var alg = this._getRoomDecryptor(content.room_id, content.algorithm);
|
||||||
olmDevice: this._olmDevice,
|
|
||||||
});
|
|
||||||
alg.onRoomKeyEvent(event);
|
alg.onRoomKeyEvent(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1143,7 +1099,7 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
|
|||||||
|
|
||||||
var roomId = member.roomId;
|
var roomId = member.roomId;
|
||||||
|
|
||||||
var alg = this._roomAlgorithms[roomId];
|
var alg = this._roomEncryptors[roomId];
|
||||||
if (!alg) {
|
if (!alg) {
|
||||||
// not encrypting in this room
|
// not encrypting in this room
|
||||||
return;
|
return;
|
||||||
@@ -1173,26 +1129,110 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
|
|||||||
console.log("m.new_device event from " + userId + ":" + deviceId +
|
console.log("m.new_device event from " + userId + ":" + deviceId +
|
||||||
" for rooms " + rooms);
|
" 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;
|
var self = this;
|
||||||
this.downloadKeys(
|
|
||||||
[userId], true
|
var pending = this._pendingNewDevices;
|
||||||
).then(function() {
|
var users = utils.keys(pending).filter(function(u) {
|
||||||
for (var i = 0; i < rooms.length; i++) {
|
return utils.keys(pending[u]).length > 0;
|
||||||
var roomId = rooms[i];
|
});
|
||||||
var alg = self._roomAlgorithms[roomId];
|
|
||||||
if (!alg) {
|
if (users.length === 0) {
|
||||||
// not encrypting in this room
|
return;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
alg.onNewDevice(userId, deviceId);
|
|
||||||
}
|
var r = this._doKeyDownloadForUsers(users);
|
||||||
}).catch(function(e) {
|
|
||||||
|
// 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(
|
console.error(
|
||||||
"Error updating device keys for new device " + userId + ":" +
|
'Error updating device keys for user ' + u + ':', e
|
||||||
deviceId,
|
|
||||||
e
|
|
||||||
);
|
);
|
||||||
}).done();
|
|
||||||
|
// 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 = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
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
|
* @see module:crypto/algorithms/base.DecryptionError
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ limitations under the License.
|
|||||||
* Utilities common to olm encryption algorithms
|
* Utilities common to olm encryption algorithms
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var q = require('q');
|
||||||
|
var anotherjson = require('another-json');
|
||||||
|
|
||||||
var utils = require("../utils");
|
var utils = require("../utils");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,3 +103,166 @@ module.exports.encryptMessageForDevice = function(
|
|||||||
deviceKey, sessionId, JSON.stringify(payload)
|
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
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ module.exports.MatrixHttpApi.prototype = {
|
|||||||
*
|
*
|
||||||
* @param {object} file The object to upload. On a browser, something that
|
* @param {object} file The object to upload. On a browser, something that
|
||||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
* 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
|
* @param {object} opts options object
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -82,6 +82,27 @@ module.exports.request = function(r) {
|
|||||||
request = 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}
|
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
|
||||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
* 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.
|
* @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
|
* The request callback interface for performing HTTP requests. This matches the
|
||||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ limitations under the License.
|
|||||||
* @module models/event
|
* @module models/event
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var EventEmitter = require("events").EventEmitter;
|
||||||
|
|
||||||
|
var utils = require('../utils.js');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum for event statuses.
|
* Enum for event statuses.
|
||||||
* @readonly
|
* @readonly
|
||||||
@@ -51,15 +55,6 @@ module.exports.EventStatus = {
|
|||||||
*
|
*
|
||||||
* @param {Object} event The raw event to be wrapped in this DAO
|
* @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
|
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
|
||||||
* this property</b> directly unless you absolutely have to. Prefer the getter
|
* this property</b> directly unless you absolutely have to. Prefer the getter
|
||||||
* methods defined on this class. Using the getter methods shields your app
|
* 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>
|
* Default: true. <strong>This property is experimental and may change.</strong>
|
||||||
*/
|
*/
|
||||||
module.exports.MatrixEvent = function MatrixEvent(
|
module.exports.MatrixEvent = function MatrixEvent(
|
||||||
event, clearEvent, keysProved, keysClaimed
|
event
|
||||||
) {
|
) {
|
||||||
this.event = event || {};
|
this.event = event || {};
|
||||||
this.sender = null;
|
this.sender = null;
|
||||||
this.target = null;
|
this.target = null;
|
||||||
this.status = null;
|
this.status = null;
|
||||||
this.forwardLooking = true;
|
this.forwardLooking = true;
|
||||||
|
|
||||||
this._clearEvent = clearEvent || {};
|
|
||||||
this._pushActions = null;
|
this._pushActions = null;
|
||||||
|
|
||||||
this._keysProved = keysProved || {};
|
this._clearEvent = {};
|
||||||
this._keysClaimed = keysClaimed || {};
|
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.
|
* Get the event_id for this event.
|
||||||
@@ -239,12 +235,37 @@ module.exports.MatrixEvent.prototype = {
|
|||||||
this._keysClaimed = keys;
|
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.
|
* Check if the event is encrypted.
|
||||||
* @return {boolean} True if this event is encrypted.
|
* @return {boolean} True if this event is encrypted.
|
||||||
*/
|
*/
|
||||||
isEncrypted: function() {
|
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) {
|
setPushActions: function(pushActions) {
|
||||||
this._pushActions = pushActions;
|
this._pushActions = pushActions;
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
|
|
||||||
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
|
/* 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},
|
'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
|
||||||
|
*/
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ function debuglog() {
|
|||||||
function SyncApi(client, opts) {
|
function SyncApi(client, opts) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
opts.initialSyncLimit = opts.initialSyncLimit || 8;
|
opts.initialSyncLimit = (
|
||||||
|
opts.initialSyncLimit === undefined ? 8 : opts.initialSyncLimit
|
||||||
|
);
|
||||||
opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
|
opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
|
||||||
opts.pollTimeout = opts.pollTimeout || (30 * 1000);
|
opts.pollTimeout = opts.pollTimeout || (30 * 1000);
|
||||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||||
@@ -542,6 +544,7 @@ SyncApi.prototype._sync = function(syncOptions) {
|
|||||||
self._connectionReturnedDefer.reject();
|
self._connectionReturnedDefer.reject();
|
||||||
self._connectionReturnedDefer = null;
|
self._connectionReturnedDefer = null;
|
||||||
}
|
}
|
||||||
|
self._updateSyncState("STOPPED");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.error("/sync error %s", err);
|
console.error("/sync error %s", err);
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
|||||||
// remove some events from the other end, if necessary
|
// remove some events from the other end, if necessary
|
||||||
var excess = this._eventCount - this._windowLimit;
|
var excess = this._eventCount - this._windowLimit;
|
||||||
if (excess > 0) {
|
if (excess > 0) {
|
||||||
this._unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||||
}
|
}
|
||||||
return q(true);
|
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 {number} delta number of events to remove from the timeline
|
||||||
* @param {boolean} startOfTimeline if events should be removed from the start
|
* @param {boolean} startOfTimeline if events should be removed from the start
|
||||||
* of the timeline.
|
* of the timeline.
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
TimelineWindow.prototype._unpaginate = function(delta, startOfTimeline) {
|
TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
|
||||||
var tl = startOfTimeline ? this._start : this._end;
|
var tl = startOfTimeline ? this._start : this._end;
|
||||||
|
|
||||||
// sanity-check the delta
|
// sanity-check the delta
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ module.exports.isFunction = function(value) {
|
|||||||
* @return {boolean} True if it is an array.
|
* @return {boolean} True if it is an array.
|
||||||
*/
|
*/
|
||||||
module.exports.isArray = function(value) {
|
module.exports.isArray = function(value) {
|
||||||
return Boolean(value && value.constructor === Array);
|
return Array.isArray ? Array.isArray(value) :
|
||||||
|
Boolean(value && value.constructor === Array);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-js-sdk",
|
"name": "matrix-js-sdk",
|
||||||
"version": "0.6.4",
|
"version": "0.7.0",
|
||||||
"description": "Matrix Client-Server SDK for Javascript",
|
"description": "Matrix Client-Server SDK for Javascript",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
jq --version > /dev/null || (echo "jq 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"; exit)
|
hub --version > /dev/null || (echo "hub is required: please install it"; kill $$)
|
||||||
|
|
||||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ if [ $dodist -eq 0 ]; then
|
|||||||
echo "Building distribution copy in $builddir"
|
echo "Building distribution copy in $builddir"
|
||||||
pushd "$builddir"
|
pushd "$builddir"
|
||||||
git clone "$projdir" .
|
git clone "$projdir" .
|
||||||
git co "$rel_branch"
|
git checkout "$rel_branch"
|
||||||
npm install
|
npm install
|
||||||
# We haven't tagged yet, so tell the dist script what version
|
# We haven't tagged yet, so tell the dist script what version
|
||||||
# it's building
|
# it's building
|
||||||
|
|||||||
@@ -5,21 +5,6 @@ var HttpBackend = require("../mock-request");
|
|||||||
var utils = require("../../lib/utils");
|
var utils = require("../../lib/utils");
|
||||||
var test_utils = require("../test-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 aliHttpBackend;
|
||||||
var bobHttpBackend;
|
var bobHttpBackend;
|
||||||
var aliClient;
|
var aliClient;
|
||||||
@@ -36,7 +21,6 @@ var aliDeviceKeys;
|
|||||||
var bobDeviceKeys;
|
var bobDeviceKeys;
|
||||||
var bobDeviceCurve25519Key;
|
var bobDeviceCurve25519Key;
|
||||||
var bobDeviceEd25519Key;
|
var bobDeviceEd25519Key;
|
||||||
var aliLocalStore;
|
|
||||||
var aliStorage;
|
var aliStorage;
|
||||||
var bobStorage;
|
var bobStorage;
|
||||||
var aliMessages;
|
var aliMessages;
|
||||||
@@ -461,11 +445,9 @@ describe("MatrixClient crypto", function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
aliLocalStore = new MockStorageApi();
|
|
||||||
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
|
|
||||||
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
|
|
||||||
test_utils.beforeEach(this);
|
test_utils.beforeEach(this);
|
||||||
|
|
||||||
|
aliStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
|
||||||
aliHttpBackend = new HttpBackend();
|
aliHttpBackend = new HttpBackend();
|
||||||
aliClient = sdk.createClient({
|
aliClient = sdk.createClient({
|
||||||
baseUrl: "http://alis.server",
|
baseUrl: "http://alis.server",
|
||||||
@@ -476,6 +458,7 @@ describe("MatrixClient crypto", function() {
|
|||||||
request: aliHttpBackend.requestFn,
|
request: aliHttpBackend.requestFn,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bobStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
|
||||||
bobHttpBackend = new HttpBackend();
|
bobHttpBackend = new HttpBackend();
|
||||||
bobClient = sdk.createClient({
|
bobClient = sdk.createClient({
|
||||||
baseUrl: "http://bobs.server",
|
baseUrl: "http://bobs.server",
|
||||||
@@ -500,6 +483,27 @@ describe("MatrixClient crypto", function() {
|
|||||||
bobClient.stopClient();
|
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) {
|
it("Bob uploads without one-time keys and with one-time keys", function(done) {
|
||||||
q()
|
q()
|
||||||
.then(bobUploadsKeys)
|
.then(bobUploadsKeys)
|
||||||
@@ -735,4 +739,31 @@ describe("MatrixClient crypto", function() {
|
|||||||
}).then(aliRecvMessage)
|
}).then(aliRecvMessage)
|
||||||
.catch(test_utils.failTest).done(done);
|
.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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -371,21 +371,6 @@ describe("MatrixClient", function() {
|
|||||||
|
|
||||||
httpBackend.flush();
|
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() {
|
describe("deleteDevice", function() {
|
||||||
|
|||||||
617
spec/integ/megolm.spec.js
Normal file
617
spec/integ/megolm.spec.js
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -65,7 +65,7 @@ module.exports.mkEvent = function(opts) {
|
|||||||
content: opts.content,
|
content: opts.content,
|
||||||
event_id: "$" + Math.random() + "-" + Math.random()
|
event_id: "$" + Math.random() + "-" + Math.random()
|
||||||
};
|
};
|
||||||
if (opts.skey) {
|
if (opts.skey !== undefined) {
|
||||||
event.state_key = opts.skey;
|
event.state_key = opts.skey;
|
||||||
}
|
}
|
||||||
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
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
|
* <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.
|
* 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
|
* @example
|
||||||
* it("should not throw", function(done) {
|
* it("should not throw", function(done) {
|
||||||
@@ -168,6 +177,27 @@ module.exports.mkMessage = function(opts) {
|
|||||||
* }).catch(utils.failTest).done(done);
|
* }).catch(utils.failTest).done(done);
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
module.exports.failTest = function(error) {
|
module.exports.failTest = function(err) {
|
||||||
expect(error.stack).toBe(null);
|
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];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user