Compare commits
54 Commits
v0.6.3
...
v0.6.4-rc.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f4eba16d6 | |||
| 9da913f5a6 | |||
| a15aa0f7a4 | |||
| efa1eee6e2 | |||
| 55179f0a1a | |||
| 01593d1a69 | |||
| 40e22cfa86 | |||
| 97aeaec8d2 | |||
| 0b9f85d97b | |||
| d266486581 | |||
| c47d2fc750 | |||
| 66c4c8882f | |||
| 72d7dd7690 | |||
| fff354669c | |||
| 07ae4b0be6 | |||
| cc51805c39 | |||
| c61ac2a845 | |||
| 6d67de06a2 | |||
| ec1273893f | |||
| 1e26077d58 | |||
| ad67f002e3 | |||
| 572df32dca | |||
| 6b8181c06f | |||
| 5900542cfb | |||
| a28b825c4d | |||
| d105854619 | |||
| a4f192bc88 | |||
| db925d7fde | |||
| 16b4865035 | |||
| 20b310484b | |||
| 611a191b0e | |||
| 8b856b9d15 | |||
| 3f7df0d15c | |||
| e0917d3c47 | |||
| 19c257703c | |||
| 62b6262534 | |||
| 55bd3ac302 | |||
| 7a7f345f28 | |||
| ff2282a41a | |||
| b5c7c700d5 | |||
| de6330fb80 | |||
| aafb1ffdef | |||
| c5d738d25c | |||
| 15d8252909 | |||
| 8189c58fc3 | |||
| b3e7f4ea21 | |||
| 9c9ae562ec | |||
| f93eea095e | |||
| 09255a52f7 | |||
| 6f9c8c3007 | |||
| d98867b810 | |||
| de7061184b | |||
| 892ca56808 | |||
| 828c7ba451 |
+1
-3
@@ -1,5 +1,3 @@
|
||||
# Keep this file in sync with .npmignore.
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
.lock-wscript
|
||||
@@ -8,7 +6,7 @@ coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
/dist
|
||||
|
||||
# version file and tarball created by 'npm pack'
|
||||
/git-revision.txt
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
# Keep this file in sync with .gitignore.
|
||||
|
||||
.jsdoc
|
||||
node_modules
|
||||
.lock-wscript
|
||||
build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
dist/browser-matrix-dev.js
|
||||
|
||||
# tarball created by 'npm pack'.
|
||||
/matrix-js-sdk-*.tgz
|
||||
|
||||
@@ -1,3 +1,48 @@
|
||||
Changes in [0.6.4-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.2) (2016-11-02)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.1...v0.6.4-rc.2)
|
||||
|
||||
* Add getRoomTags method to client
|
||||
[\#236](https://github.com/matrix-org/matrix-js-sdk/pull/236)
|
||||
|
||||
Changes in [0.6.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.1) (2016-11-02)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.3...v0.6.4-rc.1)
|
||||
|
||||
Breaking Changes
|
||||
----------------
|
||||
* Bundled version of the JS SDK are no longer versioned along with
|
||||
source files in the dist/ directory. As of this release, they
|
||||
will be included in the release tarball, but not the source
|
||||
repository.
|
||||
|
||||
Other Changes
|
||||
-------------
|
||||
* More fixes to the release script
|
||||
[\#272](https://github.com/matrix-org/matrix-js-sdk/pull/272)
|
||||
* Update the release process to use github releases
|
||||
[\#271](https://github.com/matrix-org/matrix-js-sdk/pull/271)
|
||||
* Don't package the world when we release
|
||||
[\#270](https://github.com/matrix-org/matrix-js-sdk/pull/270)
|
||||
* Add ability to set a filter prior to the first /sync
|
||||
[\#269](https://github.com/matrix-org/matrix-js-sdk/pull/269)
|
||||
* Sign one-time keys, and verify their signatures
|
||||
[\#243](https://github.com/matrix-org/matrix-js-sdk/pull/243)
|
||||
* Check for duplicate message indexes for group messages
|
||||
[\#241](https://github.com/matrix-org/matrix-js-sdk/pull/241)
|
||||
* Rotate megolm sessions
|
||||
[\#240](https://github.com/matrix-org/matrix-js-sdk/pull/240)
|
||||
* Check recipient and sender in Olm messages
|
||||
[\#239](https://github.com/matrix-org/matrix-js-sdk/pull/239)
|
||||
* Consistency checks for E2E device downloads
|
||||
[\#237](https://github.com/matrix-org/matrix-js-sdk/pull/237)
|
||||
* Support User-Interactive auth for delete device
|
||||
[\#235](https://github.com/matrix-org/matrix-js-sdk/pull/235)
|
||||
* Utility to help with interactive auth
|
||||
[\#234](https://github.com/matrix-org/matrix-js-sdk/pull/234)
|
||||
* Fix sync breaking when an invalid filterId is in localStorage
|
||||
[\#228](https://github.com/matrix-org/matrix-js-sdk/pull/228)
|
||||
|
||||
Changes in [0.6.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.3) (2016-10-12)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.2...v0.6.3)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
Contributing code to matrix-js-sdk
|
||||
==================================
|
||||
|
||||
matrix-js-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
|
||||
@@ -10,9 +10,10 @@ Quickstart
|
||||
|
||||
In a browser
|
||||
------------
|
||||
Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>`` to
|
||||
your page. There will be a global variable ``matrixcs`` attached to
|
||||
``window`` through which you can access the SDK.
|
||||
Download either the full or minified version from
|
||||
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
|
||||
``<script>`` to your page. There will be a global variable ``matrixcs``
|
||||
attached to ``window`` through which you can access the SDK.
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
|
||||
|
||||
Vendored
-5826
File diff suppressed because it is too large
Load Diff
-3
File diff suppressed because one or more lines are too long
Vendored
-6490
File diff suppressed because it is too large
Load Diff
-2
File diff suppressed because one or more lines are too long
Vendored
-9900
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-10023
File diff suppressed because it is too large
Load Diff
-3
File diff suppressed because one or more lines are too long
Vendored
-10864
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-15420
File diff suppressed because it is too large
Load Diff
-5
File diff suppressed because one or more lines are too long
Vendored
-15577
File diff suppressed because it is too large
Load Diff
-7
File diff suppressed because one or more lines are too long
Vendored
-15785
File diff suppressed because it is too large
Load Diff
-6
File diff suppressed because one or more lines are too long
Vendored
-15832
File diff suppressed because it is too large
Load Diff
-7
File diff suppressed because one or more lines are too long
Vendored
-16305
File diff suppressed because it is too large
Load Diff
-8
File diff suppressed because one or more lines are too long
Vendored
-16327
File diff suppressed because it is too large
Load Diff
-8
File diff suppressed because one or more lines are too long
Vendored
-37345
File diff suppressed because one or more lines are too long
-38
File diff suppressed because one or more lines are too long
Vendored
-41222
File diff suppressed because one or more lines are too long
-38
File diff suppressed because one or more lines are too long
Vendored
-40341
File diff suppressed because one or more lines are too long
-36
File diff suppressed because one or more lines are too long
Vendored
-40409
File diff suppressed because one or more lines are too long
-32
File diff suppressed because one or more lines are too long
Vendored
-40513
File diff suppressed because one or more lines are too long
-35
File diff suppressed because one or more lines are too long
Vendored
-40708
File diff suppressed because one or more lines are too long
-34
File diff suppressed because one or more lines are too long
Vendored
-1
@@ -1 +0,0 @@
|
||||
Release builds and development builds will reside here.
|
||||
@@ -28,4 +28,6 @@ rm -f matrix-js-sdk-*.tgz
|
||||
npm pack ||
|
||||
fail "npm pack finished with return code $?"
|
||||
|
||||
npm run gendoc || fail "JSDoc failed with code $?"
|
||||
|
||||
exit $RC
|
||||
|
||||
+17
-6
@@ -749,16 +749,23 @@ MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
|
||||
* Delete the given device
|
||||
*
|
||||
* @param {string} device_id device to delete
|
||||
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deleteDevice = function(device_id) {
|
||||
MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
|
||||
var path = utils.encodeUri("/devices/$device_id", {
|
||||
$device_id: device_id,
|
||||
});
|
||||
|
||||
var body = {};
|
||||
|
||||
if (auth) {
|
||||
body.auth = auth;
|
||||
}
|
||||
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "DELETE", path, undefined, undefined,
|
||||
undefined, "DELETE", path, undefined, body,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
@@ -969,24 +976,28 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
|
||||
*
|
||||
* @param {string[][]} devices a list of [userId, deviceId] pairs
|
||||
*
|
||||
* @param {module:client.callback=} callback
|
||||
* @param {string} [key_algorithm = signed_curve25519] desired key type
|
||||
*
|
||||
* @return {module:client.Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, callback) {
|
||||
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
|
||||
var queries = {};
|
||||
|
||||
if (key_algorithm === undefined) {
|
||||
key_algorithm = "signed_curve25519";
|
||||
}
|
||||
|
||||
for (var i = 0; i < devices.length; ++i) {
|
||||
var userId = devices[i][0];
|
||||
var deviceId = devices[i][1];
|
||||
var query = queries[userId] || {};
|
||||
queries[userId] = query;
|
||||
query[deviceId] = "curve25519";
|
||||
query[deviceId] = key_algorithm;
|
||||
}
|
||||
var content = {one_time_keys: queries};
|
||||
return this._http.authedRequestWithPrefix(
|
||||
callback, "POST", "/keys/claim", undefined, content,
|
||||
undefined, "POST", "/keys/claim", undefined, content,
|
||||
httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
+39
-1
@@ -696,6 +696,22 @@ MatrixClient.prototype.setRoomTopic = function(roomId, topic, callback) {
|
||||
undefined, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getRoomTags = function(roomId, callback) {
|
||||
var path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/", {
|
||||
$userId: this.credentials.userId,
|
||||
$roomId: roomId,
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "GET", path, undefined
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} roomId
|
||||
* @param {string} tagName name of room tag to be set
|
||||
@@ -2395,7 +2411,26 @@ MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
|
||||
}
|
||||
// debuglog("Existing filter ID %s: %s; new filter: %s",
|
||||
// filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
|
||||
return;
|
||||
self.store.setFilterIdByName(filterName, undefined);
|
||||
return undefined;
|
||||
}, function(error) {
|
||||
// Synapse currently returns the following when the filter cannot be found:
|
||||
// {
|
||||
// errcode: "M_UNKNOWN",
|
||||
// name: "M_UNKNOWN",
|
||||
// message: "No row found",
|
||||
// data: Object, httpStatus: 404
|
||||
// }
|
||||
if (error.httpStatus === 404 &&
|
||||
(error.errcode === "M_UNKNOWN" || error.errcode === "M_NOT_FOUND")) {
|
||||
// Clear existing filterId from localStorage
|
||||
// if it no longer exists on the server
|
||||
self.store.setFilterIdByName(filterName, undefined);
|
||||
// Return a undefined value for existingId further down the promise chain
|
||||
return undefined;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2492,6 +2527,9 @@ MatrixClient.prototype.getTurnServers = function() {
|
||||
*
|
||||
* @param {Number=} opts.pollTimeout The number of milliseconds to wait on /events.
|
||||
* Default: 30000 (30 seconds).
|
||||
*
|
||||
* @param {Filter=} opts.filter The filter to apply to /sync calls. This will override
|
||||
* the opts.initialSyncLimit, which would normally result in a timeline limit filter.
|
||||
*/
|
||||
MatrixClient.prototype.startClient = function(opts) {
|
||||
if (this.clientRunning) {
|
||||
|
||||
+33
-1
@@ -58,6 +58,18 @@ function OlmDevice(sessionStore) {
|
||||
// we don't bother stashing outboundgroupsessions in the sessionstore -
|
||||
// instead we keep them here.
|
||||
this._outboundGroupSessionStore = {};
|
||||
|
||||
// Store a set of decrypted message indexes for each group session.
|
||||
// This partially mitigates a replay attack where a MITM resends a group
|
||||
// message into the room.
|
||||
//
|
||||
// TODO: If we ever remove an event from memory we will also need to remove
|
||||
// it from this map. Otherwise if we download the event from the server we
|
||||
// will think that it is a duplicate.
|
||||
//
|
||||
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
|
||||
// Values are true.
|
||||
this._inboundGroupSessionMessageIndexes = {};
|
||||
}
|
||||
|
||||
function _initialise_account(sessionStore, pickleKey, account) {
|
||||
@@ -374,6 +386,10 @@ OlmDevice.prototype.encryptMessage = function(
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
if (payloadString === undefined) {
|
||||
throw new Error("payloadString undefined");
|
||||
}
|
||||
|
||||
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
|
||||
var res = session.encrypt(payloadString);
|
||||
self._saveSession(theirDeviceIdentityKey, session);
|
||||
@@ -648,6 +664,22 @@ OlmDevice.prototype.decryptGroupMessage = function(
|
||||
function decrypt(session, keysClaimed) {
|
||||
var res = session.decrypt(body);
|
||||
|
||||
var plaintext = res.plaintext;
|
||||
if (plaintext === undefined) {
|
||||
// Compatibility for older olm versions.
|
||||
plaintext = res;
|
||||
} else {
|
||||
// Check if we have seen this message index before to detect replay attacks.
|
||||
var messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
|
||||
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) {
|
||||
throw new Error(
|
||||
"Duplicate message index, possible replay attack: " +
|
||||
messageIndexKey
|
||||
);
|
||||
}
|
||||
self._inboundGroupSessionMessageIndexes[messageIndexKey] = true;
|
||||
}
|
||||
|
||||
// the sender must have had the senderKey to persuade us to save the
|
||||
// session.
|
||||
var keysProved = {curve25519: senderKey};
|
||||
@@ -656,7 +688,7 @@ OlmDevice.prototype.decryptGroupMessage = function(
|
||||
roomId, senderKey, sessionId, session, keysClaimed
|
||||
);
|
||||
return {
|
||||
result: res,
|
||||
result: plaintext,
|
||||
keysClaimed: keysClaimed,
|
||||
keysProved: keysProved,
|
||||
};
|
||||
|
||||
@@ -45,13 +45,16 @@ module.exports.DECRYPTION_CLASSES = {};
|
||||
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
|
||||
*
|
||||
* @param {object} params parameters
|
||||
* @param {string} params.userId The UserID for the local user
|
||||
* @param {string} params.deviceId The identifier for this device.
|
||||
* @param {module:crypto} params.crypto crypto core
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
* @param {string} params.roomId The ID of the room we will be sending to
|
||||
* @param {object} params.config The body of the m.room.encryption event
|
||||
*/
|
||||
var EncryptionAlgorithm = function(params) {
|
||||
this._userId = params.userId;
|
||||
this._deviceId = params.deviceId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
@@ -101,9 +104,11 @@ EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
|
||||
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
|
||||
*
|
||||
* @param {object} params parameters
|
||||
* @param {string} params.userId The UserID for the local user
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
*/
|
||||
var DecryptionAlgorithm = function(params) {
|
||||
this._userId = params.userId;
|
||||
this._olmDevice = params.olmDevice;
|
||||
};
|
||||
/** */
|
||||
|
||||
+140
-71
@@ -27,6 +27,52 @@ var utils = require("../../utils");
|
||||
var olmlib = require("../olmlib");
|
||||
var base = require("./base");
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} sessionId
|
||||
*
|
||||
* @property {string} sessionId
|
||||
* @property {Number} useCount number of times this session has been used
|
||||
* @property {Number} creationTime when the session was created (ms since the epoch)
|
||||
* @property {module:client.Promise?} sharePromise If a share operation is in progress,
|
||||
* a promise which resolves when it is complete.
|
||||
*/
|
||||
function OutboundSessionInfo(sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
this.useCount = 0;
|
||||
this.creationTime = new Date().getTime();
|
||||
this.sharePromise = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if it's time to rotate the session
|
||||
*
|
||||
* @param {Number} rotationPeriodMsgs
|
||||
* @param {Number} rotationPeriodMs
|
||||
* @return {Boolean}
|
||||
*/
|
||||
OutboundSessionInfo.prototype.needsRotation = function(
|
||||
rotationPeriodMsgs, rotationPeriodMs
|
||||
) {
|
||||
var sessionLifetime = new Date().getTime() - this.creationTime;
|
||||
|
||||
if (this.useCount >= rotationPeriodMsgs ||
|
||||
sessionLifetime >= rotationPeriodMs
|
||||
) {
|
||||
console.log(
|
||||
"Rotating megolm session after " + this.useCount +
|
||||
" messages, " + sessionLifetime + "ms"
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Megolm encryption implementation
|
||||
*
|
||||
@@ -38,15 +84,28 @@ var base = require("./base");
|
||||
*/
|
||||
function MegolmEncryption(params) {
|
||||
base.EncryptionAlgorithm.call(this, params);
|
||||
this._prepPromise = null;
|
||||
this._outboundSessionId = null;
|
||||
this._discardNewSession = false;
|
||||
|
||||
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
|
||||
// that even if this is non-null, it may not be ready for use (in which
|
||||
// case _outboundSession.sharePromise will be non-null.)
|
||||
this._outboundSession = null;
|
||||
|
||||
// devices which have joined since we last sent a message.
|
||||
// userId -> {deviceId -> true}, or
|
||||
// userId -> true
|
||||
this._devicesPendingKeyShare = {};
|
||||
this._sharePromise = null;
|
||||
|
||||
// default rotation periods
|
||||
this._sessionRotationPeriodMsgs = 100;
|
||||
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
|
||||
|
||||
if (params.config.rotation_period_ms !== undefined) {
|
||||
this._sessionRotationPeriodMs = params.config.rotation_period_ms;
|
||||
}
|
||||
|
||||
if (params.config.rotation_period_msgs !== undefined) {
|
||||
this._sessionRotationPeriodMsgs = params.config.rotation_period_msgs;
|
||||
}
|
||||
}
|
||||
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
|
||||
|
||||
@@ -55,34 +114,27 @@ utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the megolm
|
||||
* sessionId when setup is complete.
|
||||
* @return {module:client.Promise} Promise which resolves to the
|
||||
* OutboundSessionInfo when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
||||
var self = this;
|
||||
|
||||
if (this._prepPromise) {
|
||||
// prep already in progress
|
||||
return this._prepPromise;
|
||||
}
|
||||
|
||||
var sessionId = this._outboundSessionId;
|
||||
var session = this._outboundSession;
|
||||
|
||||
// need to make a brand new session?
|
||||
if (!sessionId) {
|
||||
this._prepPromise = this._prepareNewSession(room).
|
||||
finally(function() {
|
||||
self._prepPromise = null;
|
||||
});
|
||||
return this._prepPromise;
|
||||
if (!session || session.needsRotation(self._sessionRotationPeriodMsgs,
|
||||
self._sessionRotationPeriodMs)
|
||||
) {
|
||||
this._outboundSession = session = this._prepareNewSession(room);
|
||||
}
|
||||
|
||||
if (this._sharePromise) {
|
||||
if (session.sharePromise) {
|
||||
// key share already in progress
|
||||
return this._sharePromise;
|
||||
return session.sharePromise;
|
||||
}
|
||||
|
||||
// prep already done, but check for new devices
|
||||
// no share in progress: check for new devices
|
||||
var shareMap = this._devicesPendingKeyShare;
|
||||
this._devicesPendingKeyShare = {};
|
||||
|
||||
@@ -99,15 +151,15 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
||||
}
|
||||
}
|
||||
|
||||
this._sharePromise = this._shareKeyWithDevices(
|
||||
sessionId, shareMap
|
||||
session.sharePromise = this._shareKeyWithDevices(
|
||||
session.sessionId, shareMap
|
||||
).finally(function() {
|
||||
self._sharePromise = null;
|
||||
session.sharePromise = null;
|
||||
}).then(function() {
|
||||
return sessionId;
|
||||
return session;
|
||||
});
|
||||
|
||||
return this._sharePromise;
|
||||
return session.sharePromise;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -115,8 +167,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the megolm
|
||||
* sessionId when setup is complete.
|
||||
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*/
|
||||
MegolmEncryption.prototype._prepareNewSession = function(room) {
|
||||
var session_id = this._olmDevice.createOutboundGroupSession();
|
||||
@@ -131,6 +182,8 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
|
||||
// so we can reset this.
|
||||
this._devicesPendingKeyShare = {};
|
||||
|
||||
var session = new OutboundSessionInfo(session_id);
|
||||
|
||||
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
@@ -145,24 +198,17 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
|
||||
|
||||
// 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.
|
||||
return this._crypto.downloadKeys(
|
||||
session.sharePromise = this._crypto.downloadKeys(
|
||||
roomMembers, false
|
||||
).then(function(res) {
|
||||
return self._shareKeyWithDevices(session_id, shareMap);
|
||||
}).then(function() {
|
||||
if (self._discardNewSession) {
|
||||
// we've had cause to reset the session_id since starting this process.
|
||||
// we'll use the current session for any currently pending events, but
|
||||
// don't save it as the current _outboundSessionId, so that new events
|
||||
// will use a new session.
|
||||
console.log("Session generation complete, but discarding");
|
||||
} else {
|
||||
self._outboundSessionId = session_id;
|
||||
}
|
||||
return session_id;
|
||||
return session;
|
||||
}).finally(function() {
|
||||
self._discardNewSession = false;
|
||||
session.sharePromise = null;
|
||||
});
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -244,17 +290,27 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
|
||||
|
||||
var deviceInfo = sessionResult.device;
|
||||
|
||||
var encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
|
||||
olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
self._userId,
|
||||
self._deviceId,
|
||||
self._olmDevice,
|
||||
userId,
|
||||
deviceInfo,
|
||||
payload
|
||||
);
|
||||
|
||||
if (!contentMap[userId]) {
|
||||
contentMap[userId] = {};
|
||||
}
|
||||
|
||||
contentMap[userId][deviceId] =
|
||||
olmlib.encryptMessageForDevices(
|
||||
self._deviceId,
|
||||
self._olmDevice,
|
||||
[deviceInfo.getIdentityKey()],
|
||||
payload
|
||||
);
|
||||
contentMap[userId][deviceId] = encryptedContent;
|
||||
haveTargets = true;
|
||||
}
|
||||
}
|
||||
@@ -279,7 +335,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
|
||||
*/
|
||||
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
var self = this;
|
||||
return this._ensureOutboundSession(room).then(function(session_id) {
|
||||
return this._ensureOutboundSession(room).then(function(session) {
|
||||
var payloadJson = {
|
||||
room_id: self._roomId,
|
||||
type: eventType,
|
||||
@@ -287,19 +343,20 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
};
|
||||
|
||||
var ciphertext = self._olmDevice.encryptGroupMessage(
|
||||
session_id, JSON.stringify(payloadJson)
|
||||
session.sessionId, JSON.stringify(payloadJson)
|
||||
);
|
||||
|
||||
var encryptedContent = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: ciphertext,
|
||||
session_id: session_id,
|
||||
session_id: session.sessionId,
|
||||
// Include our device ID so that recipients can send us a
|
||||
// m.new_device message if they don't have our session key.
|
||||
device_id: self._deviceId,
|
||||
};
|
||||
|
||||
session.useCount++;
|
||||
return encryptedContent;
|
||||
});
|
||||
};
|
||||
@@ -312,6 +369,11 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
* @param {string=} oldMembership previous membership
|
||||
*/
|
||||
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
|
||||
// if we haven't yet made a session, there's nothing to do here.
|
||||
if (!this._outboundSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newMembership = member.membership;
|
||||
|
||||
if (newMembership === 'join') {
|
||||
@@ -325,19 +387,12 @@ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembers
|
||||
}
|
||||
|
||||
// otherwise we assume the user is leaving, and start a new outbound session.
|
||||
if (this._outboundSessionId) {
|
||||
console.log("Discarding outbound megolm session due to change in " +
|
||||
"membership of " + member.userId + " (" + oldMembership +
|
||||
"->" + newMembership + ")");
|
||||
this._outboundSessionId = null;
|
||||
}
|
||||
console.log("Discarding outbound megolm session due to change in " +
|
||||
"membership of " + member.userId + " (" + oldMembership +
|
||||
"->" + newMembership + ")");
|
||||
|
||||
if (this._prepPromise) {
|
||||
console.log("Discarding as-yet-incomplete megolm session due to " +
|
||||
"change in membership of " + member.userId + " (" +
|
||||
oldMembership + "->" + newMembership + ")");
|
||||
this._discardNewSession = true;
|
||||
}
|
||||
// this ensures that we will start a new session on the next message.
|
||||
this._outboundSession = null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -413,21 +468,35 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
|
||||
throw new base.DecryptionError("Missing fields in input");
|
||||
}
|
||||
|
||||
var res;
|
||||
try {
|
||||
var res = this._olmDevice.decryptGroupMessage(
|
||||
res = this._olmDevice.decryptGroupMessage(
|
||||
event.room_id, content.sender_key, content.session_id, content.ciphertext
|
||||
);
|
||||
if (res === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
payload: JSON.parse(res.result),
|
||||
keysClaimed: res.keysClaimed,
|
||||
keysProved: res.keysProved,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new base.DecryptionError(e);
|
||||
}
|
||||
|
||||
if (res === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var payload = JSON.parse(res.result);
|
||||
|
||||
// belt-and-braces check that the room id matches that indicated by the HS
|
||||
// (this is somewhat redundant, since the megolm session is scoped to the
|
||||
// room, so neither the sender nor a MITM can lie about the room_id).
|
||||
if (payload.room_id !== event.room_id) {
|
||||
throw new base.DecryptionError(
|
||||
"Message intended for room " + payload.room_id
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
payload: payload,
|
||||
keysClaimed: res.keysClaimed,
|
||||
keysProved: res.keysProved,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -95,32 +95,43 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
|
||||
var self = this;
|
||||
return this._ensureSession(users).then(function() {
|
||||
var participantKeys = [];
|
||||
var payloadFields = {
|
||||
room_id: room.roomId,
|
||||
type: eventType,
|
||||
content: content,
|
||||
};
|
||||
|
||||
var encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
|
||||
for (var i = 0; i < users.length; ++i) {
|
||||
var userId = users[i];
|
||||
var devices = self._crypto.getStoredDevicesForUser(userId);
|
||||
|
||||
for (var j = 0; j < devices.length; ++j) {
|
||||
var deviceInfo = devices[j];
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
if (key == self._olmDevice.deviceCurve25519Key) {
|
||||
// don't bother setting up session to ourself
|
||||
// don't bother sending to ourself
|
||||
continue;
|
||||
}
|
||||
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
|
||||
// don't bother setting up sessions with blocked users
|
||||
continue;
|
||||
}
|
||||
participantKeys.push(key);
|
||||
|
||||
olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
self._userId, self._deviceId, self._olmDevice,
|
||||
userId, deviceInfo, payloadFields
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return olmlib.encryptMessageForDevices(
|
||||
self._deviceId, self._olmDevice, participantKeys, {
|
||||
room_id: room.roomId,
|
||||
type: eventType,
|
||||
content: content,
|
||||
}
|
||||
);
|
||||
return encryptedContent;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -173,10 +184,70 @@ OlmDecryption.prototype.decryptEvent = function(event) {
|
||||
throw new base.DecryptionError("Bad Encrypted Message");
|
||||
}
|
||||
|
||||
|
||||
// TODO: Check the sender user id matches the sender key.
|
||||
// TODO: check the room_id and fingerprint
|
||||
var payload = JSON.parse(payloadString);
|
||||
|
||||
// check that we were the intended recipient, to avoid unknown-key attack
|
||||
// https://github.com/vector-im/vector-web/issues/2483
|
||||
if (payload.recipient === undefined) {
|
||||
// older versions of riot did not set this field, so we cannot make
|
||||
// this check. TODO: kill this off once our users have updated
|
||||
console.warn(
|
||||
"Olm event (id=" + event.event_id + ") contains no 'recipient' " +
|
||||
"property; cannot prevent unknown-key attack");
|
||||
} else if (payload.recipient != this._userId) {
|
||||
console.warn(
|
||||
"Event " + event.event_id + ": Intended recipient " +
|
||||
payload.recipient + " does not match our id " + this._userId
|
||||
);
|
||||
throw new base.DecryptionError(
|
||||
"Message was intented for " + payload.recipient
|
||||
);
|
||||
}
|
||||
|
||||
if (payload.recipient_keys === undefined) {
|
||||
// ditto
|
||||
console.warn(
|
||||
"Olm event (id=" + event.event_id + ") contains no " +
|
||||
"'recipient_keys' property; cannot prevent unknown-key attack");
|
||||
} else if (payload.recipient_keys.ed25519 !=
|
||||
this._olmDevice.deviceEd25519Key) {
|
||||
console.warn(
|
||||
"Event " + event.event_id + ": Intended recipient ed25519 key " +
|
||||
payload.recipient_keys.ed25519 + " did not match ours"
|
||||
);
|
||||
throw new base.DecryptionError("Message not intended for this device");
|
||||
}
|
||||
|
||||
// check that the original sender matches what the homeserver told us, to
|
||||
// avoid people masquerading as others.
|
||||
// (this check is also provided via the sender's embedded ed25519 key,
|
||||
// which is checked elsewhere).
|
||||
if (payload.sender === undefined) {
|
||||
// ditto
|
||||
console.warn(
|
||||
"Olm event (id=" + event.event_id + ") contains no " +
|
||||
"'sender' property; cannot prevent unknown-key attack");
|
||||
} else if (payload.sender != event.sender) {
|
||||
console.warn(
|
||||
"Event " + event.event_id + ": original sender " + payload.sender +
|
||||
" does not match reported sender " + event.sender
|
||||
);
|
||||
throw new base.DecryptionError(
|
||||
"Message forwarded from " + payload.sender
|
||||
);
|
||||
}
|
||||
|
||||
// Olm events intended for a room have a room_id.
|
||||
if (payload.room_id !== event.room_id) {
|
||||
console.warn(
|
||||
"Event " + event.event_id + ": original room " + payload.room_id +
|
||||
" does not match reported room " + event.room_id
|
||||
);
|
||||
throw new base.DecryptionError(
|
||||
"Message intended for room " + payload.room_id
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
payload: payload,
|
||||
sessionExists: true,
|
||||
|
||||
+120
-38
@@ -174,7 +174,7 @@ Crypto.prototype.uploadKeys = function(maxKeys) {
|
||||
// these factors.
|
||||
|
||||
// We first find how many keys the server has for us.
|
||||
var keyCount = res.one_time_key_counts.curve25519 || 0;
|
||||
var keyCount = res.one_time_key_counts.signed_curve25519 || 0;
|
||||
// We then check how many keys we can store in the Account object.
|
||||
var maxOneTimeKeys = self._olmDevice.maxNumberOfOneTimeKeys();
|
||||
// Try to keep at most half that number on the server. This leaves the
|
||||
@@ -217,11 +217,7 @@ function _uploadDeviceKeys(crypto) {
|
||||
keys: crypto._deviceKeys,
|
||||
user_id: userId,
|
||||
};
|
||||
|
||||
var sig = crypto._olmDevice.sign(anotherjson.stringify(deviceKeys));
|
||||
deviceKeys.signatures = {};
|
||||
deviceKeys.signatures[userId] = {};
|
||||
deviceKeys.signatures[userId]["ed25519:" + deviceId] = sig;
|
||||
crypto._signObject(deviceKeys);
|
||||
|
||||
return crypto._baseApis.uploadKeysRequest({
|
||||
device_keys: deviceKeys,
|
||||
@@ -239,9 +235,14 @@ function _uploadOneTimeKeys(crypto) {
|
||||
|
||||
for (var keyId in oneTimeKeys.curve25519) {
|
||||
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
|
||||
oneTimeJson["curve25519:" + keyId] = oneTimeKeys.curve25519[keyId];
|
||||
var k = {
|
||||
key: oneTimeKeys.curve25519[keyId],
|
||||
};
|
||||
crypto._signObject(k);
|
||||
oneTimeJson["signed_curve25519:" + keyId] = k;
|
||||
}
|
||||
}
|
||||
|
||||
return crypto._baseApis.uploadKeysRequest({
|
||||
one_time_keys: oneTimeJson
|
||||
}, {
|
||||
@@ -350,9 +351,22 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_storeDeviceKeys(
|
||||
_olmDevice, userId, deviceId, userStore, userResult[deviceId]
|
||||
)) {
|
||||
var deviceResult = userResult[deviceId];
|
||||
|
||||
// check that the user_id and device_id in the response object are
|
||||
// correct
|
||||
if (deviceResult.user_id !== userId) {
|
||||
console.warn("Mismatched user_id " + deviceResult.user_id +
|
||||
" in keys from " + userId + ":" + deviceId);
|
||||
continue;
|
||||
}
|
||||
if (deviceResult.device_id !== deviceId) {
|
||||
console.warn("Mismatched device_id " + deviceResult.device_id +
|
||||
" in keys from " + userId + ":" + deviceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_storeDeviceKeys(_olmDevice, userStore, deviceResult)) {
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
@@ -365,12 +379,15 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
*
|
||||
* returns true if a change was made, else false
|
||||
*/
|
||||
function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult) {
|
||||
function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
|
||||
if (!deviceResult.keys) {
|
||||
// no keys?
|
||||
return false;
|
||||
}
|
||||
|
||||
var deviceId = deviceResult.device_id;
|
||||
var userId = deviceResult.user_id;
|
||||
|
||||
var signKeyId = "ed25519:" + deviceId;
|
||||
var signKey = deviceResult.keys[signKeyId];
|
||||
if (!signKey) {
|
||||
@@ -380,23 +397,9 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
|
||||
}
|
||||
|
||||
var unsigned = deviceResult.unsigned || {};
|
||||
var signatures = deviceResult.signatures || {};
|
||||
var userSigs = signatures[userId] || {};
|
||||
var signature = userSigs[signKeyId];
|
||||
if (!signature) {
|
||||
console.log("Device " + userId + ":" + deviceId +
|
||||
" is not signed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// prepare the canonical json: remove 'unsigned' and signatures, and
|
||||
// stringify with anotherjson
|
||||
delete deviceResult.unsigned;
|
||||
delete deviceResult.signatures;
|
||||
var json = anotherjson.stringify(deviceResult);
|
||||
|
||||
try {
|
||||
_olmDevice.verifySignature(signKey, json, signature);
|
||||
_verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
||||
} catch (e) {
|
||||
console.log("Unable to verify signature on device " +
|
||||
userId + ":" + deviceId + ":", e);
|
||||
@@ -694,11 +697,13 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
||||
this._sessionStore.storeEndToEndRoom(roomId, config);
|
||||
|
||||
var alg = new AlgClass({
|
||||
userId: this._userId,
|
||||
deviceId: this._deviceId,
|
||||
crypto: this,
|
||||
olmDevice: this._olmDevice,
|
||||
baseApis: this._baseApis,
|
||||
roomId: roomId,
|
||||
config: config,
|
||||
});
|
||||
this._roomAlgorithms[roomId] = alg;
|
||||
};
|
||||
@@ -767,8 +772,9 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
||||
// That should eventually resolve itself, but it's poor form.
|
||||
|
||||
var self = this;
|
||||
var oneTimeKeyAlgorithm = "signed_curve25519";
|
||||
return this._baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm
|
||||
).then(function(res) {
|
||||
for (var i = 0; i < devicesWithoutSession.length; ++i) {
|
||||
var device = devicesWithoutSession[i];
|
||||
@@ -780,22 +786,48 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
||||
var deviceRes = userRes[deviceId];
|
||||
var oneTimeKey = null;
|
||||
for (var keyId in deviceRes) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
|
||||
oneTimeKey = deviceRes[keyId];
|
||||
}
|
||||
}
|
||||
if (oneTimeKey) {
|
||||
var sid = self._olmDevice.createOutboundSession(
|
||||
deviceInfo.getIdentityKey(), oneTimeKey
|
||||
);
|
||||
console.log("Started new sessionid " + sid +
|
||||
" for device " + userId + ":" + deviceId);
|
||||
|
||||
result[userId][deviceId].sessionId = sid;
|
||||
} else {
|
||||
console.warn("No one-time keys for device " +
|
||||
userId + ":" + deviceId);
|
||||
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;
|
||||
});
|
||||
@@ -901,6 +933,7 @@ Crypto.prototype.decryptEvent = function(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);
|
||||
@@ -1084,6 +1117,7 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
|
||||
);
|
||||
}
|
||||
var alg = new AlgClass({
|
||||
userId: this._userId,
|
||||
olmDevice: this._olmDevice,
|
||||
});
|
||||
alg.onRoomKeyEvent(event);
|
||||
@@ -1161,6 +1195,54 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
|
||||
}).done();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* sign the given object with our ed25519 key
|
||||
*
|
||||
* @param {Object} obj Object to which we will add a 'signatures' property
|
||||
*/
|
||||
Crypto.prototype._signObject = function(obj) {
|
||||
var sigs = {};
|
||||
sigs[this._userId] = {};
|
||||
sigs[this._userId]["ed25519:" + this._deviceId] =
|
||||
this._olmDevice.sign(anotherjson.stringify(obj));
|
||||
obj.signatures = sigs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify the signature on an object
|
||||
*
|
||||
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
|
||||
*
|
||||
* @param {Object} obj object to check signature on. Note that this will be
|
||||
* stripped of its 'signatures' and 'unsigned' properties.
|
||||
*
|
||||
* @param {string} signingUserId ID of the user whose signature should be checked
|
||||
*
|
||||
* @param {string} signingDeviceId ID of the device whose signature should be checked
|
||||
*
|
||||
* @param {string} signingKey base64-ed ed25519 public key
|
||||
*/
|
||||
function _verifySignature(olmDevice, obj, signingUserId, signingDeviceId, signingKey) {
|
||||
var signKeyId = "ed25519:" + signingDeviceId;
|
||||
var signatures = obj.signatures || {};
|
||||
var userSigs = signatures[signingUserId] || {};
|
||||
var signature = userSigs[signKeyId];
|
||||
if (!signature) {
|
||||
throw Error("No signature");
|
||||
}
|
||||
|
||||
// prepare the canonical json: remove unsigned and signatures, and stringify with
|
||||
// anotherjson
|
||||
delete obj.unsigned;
|
||||
delete obj.signatures;
|
||||
var json = anotherjson.stringify(obj);
|
||||
|
||||
olmDevice.verifySignature(
|
||||
signingKey, json, signature
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see module:crypto/algorithms/base.DecryptionError
|
||||
*/
|
||||
|
||||
+44
-32
@@ -34,22 +34,38 @@ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an event payload for a list of devices
|
||||
* Encrypt an event payload for an Olm device
|
||||
*
|
||||
* @param {Object<string, string>} resultsObject The `ciphertext` property
|
||||
* of the m.room.encrypted event to which to add our result
|
||||
*
|
||||
* @param {string} ourUserId
|
||||
* @param {string} ourDeviceId
|
||||
* @param {module:crypto/OlmDevice} olmDevice olm.js wrapper
|
||||
* @param {string[]} participantKeys list of curve25519 keys to encrypt for
|
||||
* @param {string} recipientUserId
|
||||
* @param {module:crypto/deviceinfo} recipientDevice
|
||||
* @param {object} payloadFields fields to include in the encrypted payload
|
||||
*
|
||||
* @return {object} content for an m.room.encrypted event
|
||||
*/
|
||||
module.exports.encryptMessageForDevices = function(
|
||||
ourDeviceId, olmDevice, participantKeys, payloadFields
|
||||
module.exports.encryptMessageForDevice = function(
|
||||
resultsObject,
|
||||
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
|
||||
payloadFields
|
||||
) {
|
||||
participantKeys.sort();
|
||||
var participantHash = ""; // Olm.sha256(participantKeys.join());
|
||||
var payloadJson = {
|
||||
fingerprint: participantHash,
|
||||
var deviceKey = recipientDevice.getIdentityKey();
|
||||
var sessionId = olmDevice.getSessionIdForDevice(deviceKey);
|
||||
if (sessionId === null) {
|
||||
// If we don't have a session for a device then
|
||||
// we can't encrypt a message for it.
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Using sessionid " + sessionId + " for device " +
|
||||
recipientUserId + ":" + recipientDevice.deviceId
|
||||
);
|
||||
|
||||
var payload = {
|
||||
sender: ourUserId,
|
||||
sender_device: ourDeviceId,
|
||||
|
||||
// Include the Ed25519 key so that the recipient knows what
|
||||
@@ -63,28 +79,24 @@ module.exports.encryptMessageForDevices = function(
|
||||
keys: {
|
||||
"ed25519": olmDevice.deviceEd25519Key,
|
||||
},
|
||||
};
|
||||
utils.extend(payloadJson, payloadFields);
|
||||
|
||||
var ciphertext = {};
|
||||
var payloadString = JSON.stringify(payloadJson);
|
||||
for (var i = 0; i < participantKeys.length; ++i) {
|
||||
var deviceKey = participantKeys[i];
|
||||
var sessionId = olmDevice.getSessionIdForDevice(deviceKey);
|
||||
if (sessionId === null) {
|
||||
// If we don't have a session for a device then
|
||||
// we can't encrypt a message for it.
|
||||
continue;
|
||||
}
|
||||
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
|
||||
ciphertext[deviceKey] = olmDevice.encryptMessage(
|
||||
deviceKey, sessionId, payloadString
|
||||
);
|
||||
}
|
||||
var encryptedContent = {
|
||||
algorithm: module.exports.OLM_ALGORITHM,
|
||||
sender_key: olmDevice.deviceCurve25519Key,
|
||||
ciphertext: ciphertext
|
||||
// include the recipient device details in the payload,
|
||||
// to avoid unknown key attacks, per
|
||||
// https://github.com/vector-im/vector-web/issues/2483
|
||||
recipient: recipientUserId,
|
||||
recipient_keys: {
|
||||
"ed25519": recipientDevice.getFingerprint(),
|
||||
},
|
||||
};
|
||||
return encryptedContent;
|
||||
|
||||
// TODO: technically, a bunch of that stuff only needs to be included for
|
||||
// pre-key messages: after that, both sides know exactly which devices are
|
||||
// involved in the session. If we're looking to reduce data transfer in the
|
||||
// future, we could elide them for subsequent messages.
|
||||
|
||||
utils.extend(payload, payloadFields);
|
||||
|
||||
resultsObject[deviceKey] = olmDevice.encryptMessage(
|
||||
deviceKey, sessionId, JSON.stringify(payload)
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -587,7 +587,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
* @param {object|string} data
|
||||
* @param {object=} opts
|
||||
*
|
||||
* @param {boolean=true} opts.json Json-encode data before sending, and
|
||||
* @param {boolean} [opts.json =true] Json-encode data before sending, and
|
||||
* decode response on receipt. (We will still json-decode error
|
||||
* responses, even if this is false.)
|
||||
*
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/** @module interactive-auth */
|
||||
var q = require("q");
|
||||
|
||||
var utils = require("./utils");
|
||||
|
||||
/**
|
||||
* Abstracts the logic used to drive the interactive auth process.
|
||||
*
|
||||
* <p>Components implementing an interactive auth flow should instantiate one of
|
||||
* these, passing in the necessary callbacks to the constructor. They should
|
||||
* then call attemptAuth, which will return a promise which will resolve or
|
||||
* reject when the interactive-auth process completes.
|
||||
*
|
||||
* <p>Meanwhile, calls will be made to the startAuthStage and doRequest
|
||||
* callbacks, and information gathered from the user can be submitted with
|
||||
* submitAuthDict.
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:interactive-auth
|
||||
*
|
||||
* @param {object} opts options object
|
||||
*
|
||||
* @param {object?} opts.authData error response from the last request. If
|
||||
* null, a request will be made with no auth before starting.
|
||||
*
|
||||
* @param {function(object?): module:client.Promise} opts.doRequest
|
||||
* called with the new auth dict to submit the request. Should return a
|
||||
* promise which resolves to the successful response or rejects with a
|
||||
* MatrixError.
|
||||
*
|
||||
* @param {function(string, object?)} opts.startAuthStage
|
||||
* called to ask the UI to start a particular auth stage. The arguments
|
||||
* are: the login type (eg m.login.password); and (if the last request
|
||||
* returned an error), an error object, with fields 'errcode' and 'error'.
|
||||
*
|
||||
*/
|
||||
function InteractiveAuth(opts) {
|
||||
this._data = opts.authData;
|
||||
this._requestCallback = opts.doRequest;
|
||||
this._startAuthStageCallback = opts.startAuthStage;
|
||||
this._completionDeferred = null;
|
||||
}
|
||||
|
||||
InteractiveAuth.prototype = {
|
||||
/**
|
||||
* begin the authentication process.
|
||||
*
|
||||
* @return {module:client.Promise} which resolves to the response on success,
|
||||
* or rejects with the error on failure.
|
||||
*/
|
||||
attemptAuth: function() {
|
||||
this._completionDeferred = q.defer();
|
||||
|
||||
if (!this._data) {
|
||||
this._doRequest(null);
|
||||
} else {
|
||||
this._startNextAuthStage();
|
||||
}
|
||||
|
||||
return this._completionDeferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* get the auth session ID
|
||||
*
|
||||
* @return {string} session id
|
||||
*/
|
||||
getSessionId: function() {
|
||||
return this._data ? this._data.session : undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* get the server params for a given stage
|
||||
*
|
||||
* @param {string} login type for the stage
|
||||
* @return {object?} any parameters from the server for this stage
|
||||
*/
|
||||
getStageParams: function(loginType) {
|
||||
var params = {};
|
||||
if (this._data && this._data.params) {
|
||||
params = this._data.params;
|
||||
}
|
||||
return params[loginType];
|
||||
},
|
||||
|
||||
/**
|
||||
* submit a new auth dict and fire off the request. This will either
|
||||
* make attemptAuth resolve/reject, or cause the startAuthStage callback
|
||||
* to be called for a new stage.
|
||||
*
|
||||
* @param {object} authData new auth dict to send to the server. Should
|
||||
* include a `type` propterty denoting the login type, as well as any
|
||||
* other params for that stage.
|
||||
*/
|
||||
submitAuthDict: function(authData) {
|
||||
if (!this._completionDeferred) {
|
||||
throw new Error("submitAuthDict() called before attemptAuth()");
|
||||
}
|
||||
|
||||
// use the sessionid from the last request.
|
||||
var auth = {
|
||||
session: this._data.session,
|
||||
};
|
||||
utils.extend(auth, authData);
|
||||
|
||||
this._doRequest(auth);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fire off a request, and either resolve the promise, or call
|
||||
* startAuthStage.
|
||||
*
|
||||
* @private
|
||||
* @param {object?} auth new auth dict, including session id
|
||||
*/
|
||||
_doRequest: function(auth) {
|
||||
var self = this;
|
||||
|
||||
// hackery to make sure that synchronous exceptions end up in the catch
|
||||
// handler (without the additional event loop entailed by q.fcall or an
|
||||
// extra q().then)
|
||||
var prom;
|
||||
try {
|
||||
prom = this._requestCallback(auth);
|
||||
} catch (e) {
|
||||
prom = q.reject(e);
|
||||
}
|
||||
|
||||
prom.then(
|
||||
function(result) {
|
||||
console.log("result from request: ", result);
|
||||
self._completionDeferred.resolve(result);
|
||||
}, function(error) {
|
||||
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
|
||||
// doesn't look like an interactive-auth failure. fail the whole lot.
|
||||
throw error;
|
||||
}
|
||||
self._data = error.data;
|
||||
self._startNextAuthStage();
|
||||
}
|
||||
).catch(this._completionDeferred.reject).done();
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick the next stage and call the callback
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_startNextAuthStage: function() {
|
||||
var nextStage = this._chooseStage();
|
||||
if (!nextStage) {
|
||||
throw new Error("No incomplete flows from the server");
|
||||
}
|
||||
|
||||
var stageError = null;
|
||||
if (this._data.errcode || this._data.error) {
|
||||
stageError = {
|
||||
errcode: this._data.errcode || "",
|
||||
error: this._data.error || "",
|
||||
};
|
||||
}
|
||||
this._startAuthStageCallback(nextStage, stageError);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick the next auth stage
|
||||
*
|
||||
* @private
|
||||
* @return {string?} login type
|
||||
*/
|
||||
_chooseStage: function() {
|
||||
var flow = this._chooseFlow();
|
||||
console.log("Active flow => %s", JSON.stringify(flow));
|
||||
var nextStage = this._firstUncompletedStage(flow);
|
||||
console.log("Next stage: %s", nextStage);
|
||||
return nextStage;
|
||||
},
|
||||
|
||||
/**
|
||||
* Pick one of the flows from the returned list
|
||||
*
|
||||
* @private
|
||||
* @return {object} flow
|
||||
*/
|
||||
_chooseFlow: function() {
|
||||
var flows = this._data.flows || [];
|
||||
// always use the first flow for now
|
||||
return flows[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the first uncompleted stage in the given flow
|
||||
*
|
||||
* @private
|
||||
* @param {object} flow
|
||||
* @return {string} login type
|
||||
*/
|
||||
_firstUncompletedStage: function(flow) {
|
||||
var completed = (this._data || {}).completed || [];
|
||||
for (var i = 0; i < flow.stages.length; ++i) {
|
||||
var stageType = flow.stages[i];
|
||||
if (completed.indexOf(stageType) === -1) {
|
||||
return stageType;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/** */
|
||||
module.exports = InteractiveAuth;
|
||||
@@ -55,6 +55,9 @@ module.exports.ContentRepo = require("./content-repo");
|
||||
module.exports.Filter = require("./filter");
|
||||
/** The {@link module:timeline-window~TimelineWindow} class. */
|
||||
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
|
||||
/** The {@link module:interactive-auth} class. */
|
||||
module.exports.InteractiveAuth = require("./interactive-auth");
|
||||
|
||||
|
||||
/**
|
||||
* Create a new Matrix Call.
|
||||
|
||||
+7
-2
@@ -391,8 +391,13 @@ SyncApi.prototype.sync = function() {
|
||||
}
|
||||
|
||||
function getFilter() {
|
||||
var filter = new Filter(client.credentials.userId);
|
||||
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
||||
var filter;
|
||||
if (self.opts.filter) {
|
||||
filter = self.opts.filter;
|
||||
} else {
|
||||
filter = new Filter(client.credentials.userId);
|
||||
filter.setTimelineLimit(self.opts.initialSyncLimit);
|
||||
}
|
||||
|
||||
client.getOrCreateFilter(
|
||||
getFilterName(client.credentials.userId), filter
|
||||
|
||||
@@ -337,6 +337,9 @@ var deepCompare = module.exports.deepCompare = function(x, y) {
|
||||
*
|
||||
* All enumerable properties, included inherited ones, are copied.
|
||||
*
|
||||
* This is approximately equivalent to ES6's Object.assign, except
|
||||
* that the latter doesn't copy inherited properties.
|
||||
*
|
||||
* @param {Object} target The object that will receive new properties
|
||||
* @param {...Object} source Objects from which to copy properties
|
||||
*
|
||||
|
||||
+26
-9
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.6.3",
|
||||
"version": "0.6.4-rc.2",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
|
||||
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
|
||||
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
|
||||
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
|
||||
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
|
||||
"build": "jshint -c .jshint lib/ && rimraf dist && mkdir dist && browserify --exclude olm browser-index.js -o dist/browser-matrix.js --ignore-missing && uglifyjs -c -m -o dist/browser-matrix.min.js dist/browser-matrix.js",
|
||||
"dist": "npm run build",
|
||||
"watch": "watchify --exclude olm browser-index.js -o dist/browser-matrix-dev.js -v",
|
||||
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
|
||||
"release": "npm run build && mkdir -p dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js",
|
||||
"prepublish": "git rev-parse HEAD > git-revision.txt",
|
||||
"version": "npm run release && git add dist/$npm_package_version"
|
||||
"prepublish": "git rev-parse HEAD > git-revision.txt"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/matrix-org/matrix-js-sdk"
|
||||
@@ -23,6 +22,23 @@
|
||||
"browser": "browser-index.js",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"RELEASING.md",
|
||||
"examples",
|
||||
"git-hooks",
|
||||
"git-revision.txt",
|
||||
"index.js",
|
||||
"browser-index.js",
|
||||
"jenkins.sh",
|
||||
"lib",
|
||||
"package.json",
|
||||
"release.sh",
|
||||
"spec"
|
||||
],
|
||||
"dependencies": {
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
@@ -31,12 +47,13 @@
|
||||
"request": "^2.53.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"watchify": "^3.2.1",
|
||||
"istanbul": "^0.3.13",
|
||||
"jasmine-node": "^1.14.5",
|
||||
"jshint": "^2.8.0",
|
||||
"jsdoc": "^3.4.0",
|
||||
"uglifyjs": "^2.4.10"
|
||||
"jshint": "^2.8.0",
|
||||
"rimraf": "^2.5.4",
|
||||
"uglifyjs": "^2.4.10",
|
||||
"watchify": "^3.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-1.3.0.tgz"
|
||||
|
||||
+76
-5
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a release of matrix-js-sdk. Performs the steps documented
|
||||
# in RELEASING.md
|
||||
@@ -10,6 +10,9 @@
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; exit)
|
||||
hub --version > /dev/null || (echo "hub is required: please install it"; exit)
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
@@ -56,11 +59,27 @@ if [ $# -ne 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
fi
|
||||
|
||||
# ignore leading v on release
|
||||
release="${1#v}"
|
||||
tag="v${release}"
|
||||
rel_branch="release-$tag"
|
||||
|
||||
prerelease=0
|
||||
# We check if this build is a prerelease by looking to
|
||||
# see if the version has a hyphen in it. Crude,
|
||||
# but semver doesn't support postreleases so anything
|
||||
# with a hyphen is a prerelease.
|
||||
echo $release | grep -q '-' && prerelease=1
|
||||
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
echo Making a PRE-RELEASE
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
if ! command -v update_changelog >/dev/null 2>&1; then
|
||||
echo "release.sh requires github-changelog-generator. Try:" >&2
|
||||
@@ -70,9 +89,16 @@ if [ -z "$skip_changelog" ]; then
|
||||
fi
|
||||
|
||||
# we might already be on the release branch, in which case, yay
|
||||
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
|
||||
# If we're on any branch starting with 'release', we don't create
|
||||
# a separate release branch (this allows us to use the same
|
||||
# release branch for releases and release candidates).
|
||||
curbranch=$(git symbolic-ref --short HEAD)
|
||||
if [[ "$curbranch" != release* ]]; then
|
||||
echo "Creating release branch"
|
||||
git checkout -b "$rel_branch"
|
||||
else
|
||||
echo "Using current branch ($curbranch) for release"
|
||||
rel_branch=$curbranch
|
||||
fi
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
@@ -90,7 +116,52 @@ set -x
|
||||
|
||||
# Bump package.json, build the dist, and tag
|
||||
echo "npm version"
|
||||
npm version "$release"
|
||||
# npm version will automatically commit its modification
|
||||
# and make a release tag. We don't want it to create the tag
|
||||
# because github will do that, but we can only turn off both
|
||||
# of these behaviours, so we have to manually commit the
|
||||
# result.
|
||||
npm version --no-git-tag-version "$release"
|
||||
git commit package.json -m "$tag"
|
||||
|
||||
# If there is a 'dist' script in the package.json,
|
||||
# run it in a separate checkout of the project, then
|
||||
# upload any files in the 'dist' directory as release
|
||||
# assets.
|
||||
# We make a completely separate checkout to be sure
|
||||
# we're using released versions of the dependencies
|
||||
# (rather than whatever we're pulling in from npm link)
|
||||
assets=''
|
||||
dodist=0
|
||||
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
|
||||
if [ $dodist -eq 0 ]; then
|
||||
projdir=`pwd`
|
||||
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
|
||||
echo "Building distribution copy in $builddir"
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git co "$rel_branch"
|
||||
npm install
|
||||
npm run dist
|
||||
popd
|
||||
for i in "$builddir"/dist/*; do
|
||||
assets="$assets -a $i"
|
||||
done
|
||||
fi
|
||||
|
||||
# push the release branch (github can't release from
|
||||
# a branch it doesn't have)
|
||||
git push origin "$rel_branch"
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
hub release create $hubflags $assets -m "$tag" "$tag"
|
||||
|
||||
if [ $dodist -eq 0 ]; then
|
||||
rm -rf "$builddir"
|
||||
fi
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
@@ -113,8 +184,8 @@ git checkout master
|
||||
git pull
|
||||
git merge --ff-only "$rel_branch"
|
||||
|
||||
# push everything to github
|
||||
git push origin master "$rel_branch" "$tag"
|
||||
# push master and docs (if generated) to github
|
||||
git push origin master
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
@@ -61,7 +61,7 @@ function expectKeyUpload(deviceId, httpBackend) {
|
||||
expect(content.one_time_keys).not.toBeDefined();
|
||||
expect(content.device_keys).toBeDefined();
|
||||
keys.device_keys = content.device_keys;
|
||||
return {one_time_key_counts: {curve25519: 0}};
|
||||
return {one_time_key_counts: {signed_curve25519: 0}};
|
||||
});
|
||||
|
||||
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
|
||||
@@ -76,7 +76,7 @@ function expectKeyUpload(deviceId, httpBackend) {
|
||||
}
|
||||
expect(count).toEqual(5);
|
||||
keys.one_time_keys = content.one_time_keys;
|
||||
return {one_time_key_counts: {curve25519: count}};
|
||||
return {one_time_key_counts: {signed_curve25519: count}};
|
||||
});
|
||||
|
||||
return httpBackend.flush(uploadPath, 2).then(function() {
|
||||
@@ -176,10 +176,11 @@ function expectAliClaimKeys() {
|
||||
expect(bobOneTimeKeys).toBeDefined();
|
||||
|
||||
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
|
||||
var claimType = content.one_time_keys[bobUserId][bobDeviceId];
|
||||
expect(claimType).toEqual("signed_curve25519");
|
||||
for (var keyId in bobOneTimeKeys) {
|
||||
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
|
||||
if (keyId.indexOf("curve25519:") === 0) {
|
||||
if (keyId.indexOf(claimType + ":") === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -338,15 +339,15 @@ function expectSendMessageRequest(httpBackend) {
|
||||
|
||||
function aliRecvMessage() {
|
||||
var message = bobMessages.shift();
|
||||
return recvMessage(aliHttpBackend, aliClient, message);
|
||||
return recvMessage(aliHttpBackend, aliClient, bobUserId, message);
|
||||
}
|
||||
|
||||
function bobRecvMessage() {
|
||||
var message = aliMessages.shift();
|
||||
return recvMessage(bobHttpBackend, bobClient, message);
|
||||
return recvMessage(bobHttpBackend, bobClient, aliUserId, message);
|
||||
}
|
||||
|
||||
function recvMessage(httpBackend, client, message) {
|
||||
function recvMessage(httpBackend, client, sender, message) {
|
||||
var syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
@@ -361,7 +362,8 @@ function recvMessage(httpBackend, client, message) {
|
||||
test_utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message
|
||||
content: message,
|
||||
sender: sender,
|
||||
})
|
||||
]
|
||||
}
|
||||
@@ -529,6 +531,77 @@ describe("MatrixClient crypto", function() {
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect userId", function(done) {
|
||||
var eveUserId = "@eve:localhost";
|
||||
|
||||
var bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bvcxz',
|
||||
keys: {
|
||||
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
|
||||
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
|
||||
},
|
||||
user_id: '@eve:localhost',
|
||||
signatures: {
|
||||
'@eve:localhost': {
|
||||
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
|
||||
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
var result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
q.all(
|
||||
aliClient.downloadKeys([bobUserId, eveUserId]),
|
||||
aliHttpBackend.flush("/keys/query", 1)
|
||||
).then(function() {
|
||||
// should get an empty list
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
|
||||
expect(aliClient.listDeviceKeys(eveUserId)).toEqual([]);
|
||||
}).catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect deviceId", function(done) {
|
||||
var bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bad_device',
|
||||
keys: {
|
||||
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
|
||||
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
|
||||
},
|
||||
user_id: '@bob:localhost',
|
||||
signatures: {
|
||||
'@bob:localhost': {
|
||||
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
|
||||
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var bobKeys = {};
|
||||
bobKeys[bobDeviceId] = bobDeviceKeys;
|
||||
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
var result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
q.all(
|
||||
aliClient.downloadKeys([bobUserId]),
|
||||
aliHttpBackend.flush("/keys/query", 1)
|
||||
).then(function() {
|
||||
// should get an empty list
|
||||
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
|
||||
}).catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali enables encryption", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
@@ -557,6 +630,63 @@ describe("MatrixClient crypto", function() {
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobStartClient)
|
||||
.then(function() {
|
||||
var message = aliMessages.shift();
|
||||
var syncData = {
|
||||
next_batch: "x",
|
||||
rooms: {
|
||||
join: {
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
syncData.rooms.join[roomId] = {
|
||||
timeline: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message,
|
||||
sender: "@bogus:sender",
|
||||
})
|
||||
]
|
||||
}
|
||||
};
|
||||
bobHttpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
var deferred = q.defer();
|
||||
var onEvent = function(event) {
|
||||
console.log(bobClient.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
bobClient.removeListener("event", onEvent);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
bobClient.on("event", onEvent);
|
||||
|
||||
bobHttpBackend.flush();
|
||||
return deferred.promise;
|
||||
})
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
|
||||
@@ -295,58 +295,65 @@ describe("MatrixClient", function() {
|
||||
|
||||
describe("downloadKeys", function() {
|
||||
it("should do an HTTP request and then store the keys", function(done) {
|
||||
var ed25519key = "wV5E3EUSHpHuoZLljNzojlabjGdXT3Mz7rugG9zgbkI";
|
||||
var ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||
// ed25519key = client.getDeviceEd25519Key();
|
||||
var borisKeys = {
|
||||
dev1: {
|
||||
algorithms: ["1"], keys: { "ed25519:dev1": ed25519key },
|
||||
algorithms: ["1"],
|
||||
device_id: "dev1",
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
signatures: {
|
||||
boris: {
|
||||
"ed25519:dev1":
|
||||
"u99n8WZ61G//K6eVgYc+RDLVapmjttxqhjNucIFGEIJ" +
|
||||
"oA4TUY8FmiGv3zl0EA71zrvPDfnFL5XLNsdc55NGbDg"
|
||||
"ed25519:dev1":
|
||||
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
|
||||
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw"
|
||||
}
|
||||
},
|
||||
unsigned: { "abc": "def" },
|
||||
user_id: "boris",
|
||||
}
|
||||
};
|
||||
var chazKeys = {
|
||||
dev2: {
|
||||
algorithms: ["2"], keys: { "ed25519:dev2": ed25519key },
|
||||
algorithms: ["2"],
|
||||
device_id: "dev2",
|
||||
keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
chaz: {
|
||||
"ed25519:dev2":
|
||||
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
|
||||
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
|
||||
}
|
||||
},
|
||||
unsigned: { "ghi": "def" },
|
||||
}
|
||||
};
|
||||
var daveKeys = {
|
||||
dev3: {
|
||||
algorithms: ["3"], keys: { "ed25519:dev2": ed25519key },
|
||||
signatures: {
|
||||
dave: {
|
||||
"ed25519:dev2":
|
||||
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
|
||||
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
|
||||
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
|
||||
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ"
|
||||
}
|
||||
},
|
||||
unsigned: { "ghi": "def" },
|
||||
user_id: "chaz",
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
function sign(o) {
|
||||
var anotherjson = require('another-json');
|
||||
var b = JSON.parse(JSON.stringify(o));
|
||||
delete(b.signatures);
|
||||
delete(b.unsigned);
|
||||
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
|
||||
};
|
||||
|
||||
console.log("Ed25519: " + ed25519key);
|
||||
console.log("boris:", sign(borisKeys.dev1));
|
||||
console.log("chaz:", sign(chazKeys.dev2));
|
||||
*/
|
||||
|
||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}, dave: {}}});
|
||||
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}}});
|
||||
}).respond(200, {
|
||||
device_keys: {
|
||||
boris: borisKeys,
|
||||
chaz: chazKeys,
|
||||
dave: daveKeys,
|
||||
},
|
||||
});
|
||||
|
||||
client.downloadKeys(["boris", "chaz", "dave"]).then(function(res) {
|
||||
client.downloadKeys(["boris", "chaz"]).then(function(res) {
|
||||
assertObjectContains(res.boris.dev1, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
@@ -360,9 +367,6 @@ describe("MatrixClient", function() {
|
||||
algorithms: ["2"],
|
||||
unsigned: { "ghi": "def" },
|
||||
});
|
||||
|
||||
// dave's key fails validation.
|
||||
expect(res.dave).toEqual({});
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
@@ -383,6 +387,23 @@ describe("MatrixClient", function() {
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteDevice", function() {
|
||||
var auth = {a: 1};
|
||||
it("should pass through an auth dict", function(done) {
|
||||
httpBackend.when(
|
||||
"DELETE", "/_matrix/client/unstable/devices/my_device"
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual({auth: auth});
|
||||
}).respond(200);
|
||||
|
||||
client.deleteDevice(
|
||||
"my_device", auth
|
||||
).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function assertObjectContains(obj, expected) {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
var q = require("q");
|
||||
var sdk = require("../..");
|
||||
var utils = require("../test-utils");
|
||||
|
||||
var InteractiveAuth = sdk.InteractiveAuth;
|
||||
var MatrixError = sdk.MatrixError;
|
||||
|
||||
describe("InteractiveAuth", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this);
|
||||
});
|
||||
|
||||
it("should start an auth stage and complete it", function(done) {
|
||||
var doRequest = jasmine.createSpy('doRequest');
|
||||
var startAuthStage = jasmine.createSpy('startAuthStage');
|
||||
|
||||
var ia = new InteractiveAuth({
|
||||
doRequest: doRequest,
|
||||
startAuthStage: startAuthStage,
|
||||
authData: {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: ["logintype"] },
|
||||
],
|
||||
params: {
|
||||
"logintype": { param: "aa" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams("logintype")).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// first we expect a call here
|
||||
startAuthStage.andCallFake(function(stage) {
|
||||
expect(stage).toEqual("logintype");
|
||||
ia.submitAuthDict({
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
var requestRes = {"a": "b"};
|
||||
doRequest.andCallFake(function(authData) {
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
return q(requestRes);
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(1);
|
||||
expect(startAuthStage.calls.length).toEqual(1);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
|
||||
it("should make a request if no authdata is provided", function(done) {
|
||||
var doRequest = jasmine.createSpy('doRequest');
|
||||
var startAuthStage = jasmine.createSpy('startAuthStage');
|
||||
|
||||
var ia = new InteractiveAuth({
|
||||
doRequest: doRequest,
|
||||
startAuthStage: startAuthStage,
|
||||
});
|
||||
|
||||
expect(ia.getSessionId()).toBe(undefined);
|
||||
expect(ia.getStageParams("logintype")).toBe(undefined);
|
||||
|
||||
// first we expect a call to doRequest
|
||||
doRequest.andCallFake(function(authData) {
|
||||
console.log("request1", authData);
|
||||
expect(authData).toBe(null);
|
||||
var err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{ stages: ["logintype"] },
|
||||
],
|
||||
params: {
|
||||
"logintype": { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
// .. which should be followed by a call to startAuthStage
|
||||
var requestRes = {"a": "b"};
|
||||
startAuthStage.andCallFake(function(stage) {
|
||||
expect(stage).toEqual("logintype");
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams("logintype")).toEqual({
|
||||
param: "aa",
|
||||
});
|
||||
|
||||
// submitAuthDict should trigger another call to doRequest
|
||||
doRequest.andCallFake(function(authData) {
|
||||
console.log("request2", authData);
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
return q(requestRes);
|
||||
});
|
||||
|
||||
ia.submitAuthDict({
|
||||
type: "logintype",
|
||||
foo: "bar",
|
||||
});
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(2);
|
||||
expect(startAuthStage.calls.length).toEqual(1);
|
||||
}).catch(utils.failTest).done(done);
|
||||
});
|
||||
});
|
||||
@@ -104,6 +104,7 @@ describe("MatrixClient", function() {
|
||||
if (next.error) {
|
||||
return q.reject({
|
||||
errcode: next.error.errcode,
|
||||
httpStatus: next.error.httpStatus,
|
||||
name: next.error.errcode,
|
||||
message: "Expected testing error",
|
||||
data: next.error
|
||||
@@ -207,6 +208,44 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOrCreateFilter", function() {
|
||||
it("should POST createFilter if no id is present in localStorage", function() {
|
||||
});
|
||||
it("should use an existing filter if id is present in localStorage", function() {
|
||||
});
|
||||
it("should handle localStorage filterId missing from the server", function(done) {
|
||||
function getFilterName(userId, suffix) {
|
||||
// scope this on the user ID because people may login on many accounts
|
||||
// and they all need to be stored!
|
||||
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
|
||||
}
|
||||
var invalidFilterId = 'invalidF1lt3r';
|
||||
httpLookups = [];
|
||||
httpLookups.push({
|
||||
method: "GET",
|
||||
path: FILTER_PATH + '/' + invalidFilterId,
|
||||
error: {
|
||||
errcode: "M_UNKNOWN",
|
||||
name: "M_UNKNOWN",
|
||||
message: "No row found",
|
||||
data: { errcode: "M_UNKNOWN", error: "No row found" },
|
||||
httpStatus: 404
|
||||
}
|
||||
});
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
store.getFilterIdByName.andReturn(invalidFilterId);
|
||||
|
||||
var filterName = getFilterName(client.credentials.userId);
|
||||
client.store.setFilterIdByName(filterName, invalidFilterId);
|
||||
var filter = new sdk.Filter(client.credentials.userId);
|
||||
|
||||
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryImmediately", function() {
|
||||
it("should return false if there is no request waiting", function() {
|
||||
client.startClient();
|
||||
|
||||
Reference in New Issue
Block a user