Compare commits

...

54 Commits

Author SHA1 Message Date
David Baker 4f4eba16d6 v0.6.4-rc.2 2016-11-02 11:01:09 +00:00
David Baker 9da913f5a6 Prepare changelog for v0.6.4-rc.2 2016-11-02 11:01:09 +00:00
David Baker a15aa0f7a4 Merge branch 'release-v0.6.4' 2016-11-02 10:30:05 +00:00
David Baker efa1eee6e2 v0.6.4-rc.1 2016-11-02 10:26:53 +00:00
David Baker 55179f0a1a Prepare changelog for v0.6.4-rc.1 2016-11-02 10:26:53 +00:00
David Baker 01593d1a69 Set the release branch variable
when using the current branch, otherwise we'll try to check out
the wrong thing
2016-11-02 10:25:47 +00:00
Richard van der Hoff 40e22cfa86 Merge pull request #272 from matrix-org/dbkr/release_script_fixes
More fixes to the release script
2016-11-02 10:05:41 +00:00
David Baker 97aeaec8d2 More fixes to the release script
* Don't create a new release branch if the current branch starts
   with 'release'
 * This is definitely a bash script at this point
 * Fix update_changelog test
 * Tabs
2016-11-02 09:56:00 +00:00
David Baker 0b9f85d97b Merge pull request #271 from matrix-org/dbkr/build_process
Update the release process to use github releases
2016-11-01 17:43:06 +00:00
David Baker d266486581 Check we have the various scripts
Rather than trying to use them and failing at annoying points
2016-11-01 16:48:16 +00:00
David Baker c47d2fc750 too many ses(es) 2016-11-01 15:38:20 +00:00
David Baker 66c4c8882f Right repo 2016-11-01 15:37:39 +00:00
David Baker 72d7dd7690 /dist not dist 2016-11-01 15:36:37 +00:00
David Baker fff354669c Update README to point to where releases now live 2016-11-01 14:53:26 +00:00
David Baker 07ae4b0be6 Add 'dist' script to js-sdk
(which just runs npm build, but the presence of the dist script
will inform the release process to run it and upload release
assets)
2016-11-01 14:36:56 +00:00
David Baker cc51805c39 Use github release in release.sh
Adds the 'dist' target for building assets for distribution,
which the release script will run, uploading resulting files
as release assets.
2016-11-01 14:34:48 +00:00
David Baker c61ac2a845 Delete aaaaaaall of dist/
and gitignore dist since we now just use it for build output.
2016-10-31 18:31:24 +00:00
David Baker 6d67de06a2 Build bundled files straight to dist
Rather than to version specific directories beneath dist, since
we're now not keeping every built version in the source tree.

Also:

 * build the minified version at the same time,
 * Include rimraf so we can rm -r dist (npm has re-ordered the
   deps)
 * Exclude olm from the bundled file
2016-10-31 18:25:34 +00:00
Richard van der Hoff ec1273893f Merge pull request #270 from matrix-org/dbkr/no_pack_world
Don't package the world when we release
2016-10-31 18:09:29 +00:00
David Baker 1e26077d58 Include browser-index.js 2016-10-31 18:06:37 +00:00
David Baker ad67f002e3 Remove comment about syncing with .npmignore
Given there is no longer a .npmignore
2016-10-31 18:04:26 +00:00
David Baker 572df32dca Don't package the world when we release
Include files explicitly rather than excluding them with .npmignore

As https://github.com/vector-im/vector-web/pull/2516
2016-10-31 17:29:34 +00:00
David Baker 6b8181c06f Merge pull request #269 from matrix-org/luke/feature-initial-sync-filter
Add ability to set a filter prior to the first /sync
2016-10-26 11:37:55 +01:00
lukebarnard 5900542cfb Add ability to set a filter prior to initial sync.
Useful for only syncing with a subset of joined rooms or only retrieving certain relevant types of events.
2016-10-25 20:05:25 +01:00
Richard van der Hoff a28b825c4d Merge pull request #236 from pik/get-room-tags
Add getRoomTags method to client
2016-10-21 14:26:52 +01:00
Richard van der Hoff d105854619 Merge pull request #243 from matrix-org/rav/sign_one_time_keys
Sign one-time keys, and verify their signatures
2016-10-21 14:24:07 +01:00
Richard van der Hoff a4f192bc88 Sign one-time keys, and verify their signatures
We have decided that signing one-time keys is the lesser of two evils;
accordingly, use a new key algorithm type (`signed_curve25519`), sign the
one-time keys that we upload to the server, and verify the signatures on those
we download.

This will mean that develop won't be able to talk to master, but hey, we're in
beta.
2016-10-21 12:24:19 +01:00
Richard van der Hoff db925d7fde Merge branch 'master' into develop 2016-10-21 12:07:16 +01:00
Mark Haines 16b4865035 Merge pull request #241 from matrix-org/markjh/check_for_duplicate_message_ids
Check for duplicate message indexes for group messages
2016-10-21 09:55:38 +01:00
Mark Haines 20b310484b Document the format of the keys 2016-10-21 09:54:57 +01:00
Richard van der Hoff 611a191b0e Merge pull request #240 from matrix-org/rav/rotate_megolm_sessions
Rotate megolm sessions
2016-10-20 21:06:28 +01:00
Mark Haines 8b856b9d15 Wrap the longer lines 2016-10-20 18:02:48 +01:00
Mark Haines 3f7df0d15c Fiddle linebreaks 2016-10-20 17:59:15 +01:00
Mark Haines e0917d3c47 Check for duplicate message indexes for group messages 2016-10-20 17:49:37 +01:00
Richard van der Hoff 19c257703c Rotate megolm sessions
In order to mitigate backward-secrecy concerns, make sure that we rotate the
outbound megolm session at regular intervals (every week/100 msgs by default).
2016-10-20 15:42:06 +01:00
pik 62b6262534 Add getRoomTags method
Signed-off-by: pik <alexander.maznev@gmail.com>
2016-10-19 10:10:33 -05:00
Richard van der Hoff 55bd3ac302 Add CONTRIBUTING.rst 2016-10-19 11:55:09 +01:00
Richard van der Hoff 7a7f345f28 Merge pull request #239 from matrix-org/rav/fix_unknown_key
Check recipient and sender in Olm messages
2016-10-19 11:44:48 +01:00
Richard van der Hoff ff2282a41a Merge pull request #237 from matrix-org/rav/check_userid_in_device_list
Consistency checks for E2E device downloads
2016-10-19 11:30:07 +01:00
Richard van der Hoff b5c7c700d5 Check recipient and sender in Olm messages
Embed the sender, recipient, and recipient keys in the plaintext of Olm
messages, and check those fields on receipt.

Fixes https://github.com/vector-im/vector-web/issues/2483
2016-10-19 11:24:59 +01:00
Richard van der Hoff de6330fb80 Fix up failing test
Update a failing test to include user_id and device_id in the right place.

Remove one of the cases since it's somewhat redundant to
matrix-client-crypto-spec anyway.
2016-10-18 21:09:10 +01:00
Richard van der Hoff aafb1ffdef Consistency checks for E2E device downloads
Check that the user_id and device_id in device query responses match those that
we expect.

This resolves an unknown-key attack whereby Eve can re-sign Bob's keys with her
own key, thus getting Alice to send her messages which she can then forward to
Bob, making Bob think that Alice sent the messages to him.
2016-10-18 13:40:13 +01:00
David Baker c5d738d25c Merge pull request #235 from matrix-org/rav/delete_device_ui_auth
Support User-Interactive auth for delete device
2016-10-12 18:13:49 +01:00
David Baker 15d8252909 Merge pull request #234 from matrix-org/rav/interactive_auth
Utility to help with interactive auth
2016-10-12 18:13:36 +01:00
Richard van der Hoff 8189c58fc3 Use utils.extend instead of Object.assign
... because javascript is awful
2016-10-12 15:21:47 +01:00
David Baker b3e7f4ea21 gjslint wants a space before the '='... 2016-10-12 11:42:10 +01:00
David Baker 9c9ae562ec Merge branch 'master' into develop 2016-10-12 11:38:08 +01:00
David Baker f93eea095e Fail the build if the docs don't generate 2016-10-12 11:32:42 +01:00
David Baker 09255a52f7 Merge branch 'release-v0.6.3' 2016-10-12 11:27:05 +01:00
David Baker 6f9c8c3007 Apparently that jsdoc syntax is not valid 2016-10-12 11:24:14 +01:00
Richard van der Hoff d98867b810 User-Interactive auth for delete device
Allow app to pass in an auth dict on delete device
2016-10-12 08:37:16 +01:00
Richard van der Hoff de7061184b Utility to help with interactive auth 2016-10-12 08:27:53 +01:00
Richard van der Hoff 892ca56808 Merge pull request #228 from pik/bug-invalid-filter
Fix sync breaking when an invalid filterId is in localStorage
2016-10-09 20:35:09 +01:00
pik 828c7ba451 Fix sync breaking when an invalid filterId is in localStorage
* if getFilter fails for a filterId, null out the localStorage id and
   redirect to the createFilter path
 * add spec
 * fix unit/matrix-client.spec.js http response not matching synapse
2016-10-09 14:17:18 -05:00
60 changed files with 1248 additions and 379395 deletions
+1 -3
View File
@@ -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
View File
@@ -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
+45
View File
@@ -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)
+4
View File
@@ -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
+4 -3
View File
@@ -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.
-5826
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-6490
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-9900
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10023
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10864
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15420
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15577
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15785
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15832
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-16305
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-16327
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-37345
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-41222
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-40341
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-40409
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-40513
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-40708
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
-1
View File
@@ -1 +0,0 @@
Release builds and development builds will reside here.
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
};
+5
View File
@@ -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
View File
@@ -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,
};
};
/**
+84 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.)
*
+228
View File
@@ -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;
+3
View File
@@ -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
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+138 -8
View File
@@ -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)
+48 -27
View File
@@ -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) {
+141
View File
@@ -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);
});
});
+39
View File
@@ -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();