Compare commits
184 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54eec40d20 | |||
| 3ab34f911b | |||
| d6e4d0a417 | |||
| fac40f5183 | |||
| ce684a6628 | |||
| 14fac241f7 | |||
| 335579e250 | |||
| c8565be3a5 | |||
| 76e76269cf | |||
| 3c43e2718d | |||
| f2676772c8 | |||
| c9bf4270fc | |||
| 41ddb7660b | |||
| b3e93ffadf | |||
| 582576b1c3 | |||
| 456135a6ec | |||
| c7357952ec | |||
| b796246d9d | |||
| 598e48e39b | |||
| 2e3c349c5e | |||
| 7f4ff352e8 | |||
| b11bff5a5b | |||
| 223bd459f6 | |||
| ed1673c66c | |||
| 0fda43b603 | |||
| d3b63c592e | |||
| a32af7d77c | |||
| 742d942baa | |||
| 73e86bfc5d | |||
| 0a4c41c958 | |||
| 79a699f0be | |||
| 4a2e6a826b | |||
| 95f56f95ec | |||
| 67e75fb7af | |||
| ebda89d1ae | |||
| 5ce5299651 | |||
| 41bd518182 | |||
| 739e94302d | |||
| e54541aecf | |||
| 338c707579 | |||
| 301ab01911 | |||
| 99089c0f5f | |||
| ec124847d7 | |||
| 89ced19874 | |||
| f997b4a1d5 | |||
| def99728e7 | |||
| fab234dccb | |||
| 0c0572948e | |||
| a4e281265f | |||
| ce71de0d78 | |||
| c5afcaeaf7 | |||
| 889bfce65d | |||
| 1f33d76e87 | |||
| d8de23228f | |||
| 579218aafc | |||
| aefdacc566 | |||
| d619495136 | |||
| 036d1da013 | |||
| 4ba8e7e072 | |||
| f6830992ea | |||
| 769a0cb76f | |||
| 766e837775 | |||
| 749f53a22b | |||
| 851b33aac2 | |||
| fc958a3922 | |||
| 2c31b72c52 | |||
| 8decb02027 | |||
| a0fd87c032 | |||
| c0d862c9f0 | |||
| 8143abc9e7 | |||
| af95dcaef6 | |||
| 5b4aedd4be | |||
| d8c0b16d7e | |||
| 909b56d48e | |||
| a5d857945a | |||
| 1a03e534bd | |||
| e623b539c4 | |||
| 2ff6f5f958 | |||
| 1532188d95 | |||
| 04093692c9 | |||
| a96389a3e4 | |||
| 00e7c84a93 | |||
| e0c924870d | |||
| 1a27ad22a7 | |||
| 7029083266 | |||
| a5f0ec7c7d | |||
| e7dcc06855 | |||
| 867ac49b50 | |||
| bfffbea4a0 | |||
| f8b1c124df | |||
| bc9e290c11 | |||
| 777ef83378 | |||
| 24283dcbd5 | |||
| 2113c83679 | |||
| 77508f38bb | |||
| 6c3eb19b74 | |||
| e173d822e8 | |||
| e2d3ace476 | |||
| 6f79a3107b | |||
| e6a3b2aa28 | |||
| e5dcfdf115 | |||
| 47e12fcc3e | |||
| d51b2884da | |||
| 28e9c10ded | |||
| e27bf04ced | |||
| 65f1b3c976 | |||
| 4529578cd6 | |||
| 6769c96942 | |||
| dcb987732c | |||
| 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 | |||
| 2e99d5da64 | |||
| 72c8586fad | |||
| d98867b810 | |||
| de7061184b | |||
| 4e2483b41a | |||
| d3db4ee63d | |||
| 5d049cc5e8 | |||
| 6218bad00f | |||
| 2968e9c0c7 | |||
| e73051b230 | |||
| acad3e69dd | |||
| 4794dfc17b | |||
| d505ab9eeb | |||
| 631eeb9bc0 | |||
| 892ca56808 | |||
| 828c7ba451 | |||
| a3d86c03b1 | |||
| 74d6cb802f | |||
| 1b83f66536 | |||
| e5d5cd901a | |||
| 92ae4dda72 | |||
| cd5a88c718 | |||
| 1c744a66e6 | |||
| 57cf7e1f7d | |||
| 86ea00cfee |
+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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- node # Latest stable version of nodejs.
|
||||
+167
@@ -1,3 +1,170 @@
|
||||
Changes in [0.7.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.3) (2017-01-04)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.2...v0.7.3)
|
||||
|
||||
* User presence list feature
|
||||
[\#310](https://github.com/matrix-org/matrix-js-sdk/pull/310)
|
||||
* Allow clients the ability to set a default local timeout
|
||||
[\#313](https://github.com/matrix-org/matrix-js-sdk/pull/313)
|
||||
* Add API to delete threepid
|
||||
[\#312](https://github.com/matrix-org/matrix-js-sdk/pull/312)
|
||||
|
||||
Changes in [0.7.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.2) (2016-12-15)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1...v0.7.2)
|
||||
|
||||
* Bump to Olm 2.0
|
||||
[\#309](https://github.com/matrix-org/matrix-js-sdk/pull/309)
|
||||
* Sanity check payload length before encrypting
|
||||
[\#307](https://github.com/matrix-org/matrix-js-sdk/pull/307)
|
||||
* Remove dead _sendPingToDevice function
|
||||
[\#308](https://github.com/matrix-org/matrix-js-sdk/pull/308)
|
||||
* Add setRoomDirectoryVisibilityAppService
|
||||
[\#306](https://github.com/matrix-org/matrix-js-sdk/pull/306)
|
||||
* Update release script to do signed releases
|
||||
[\#305](https://github.com/matrix-org/matrix-js-sdk/pull/305)
|
||||
* e2e: Wait for pending device lists
|
||||
[\#304](https://github.com/matrix-org/matrix-js-sdk/pull/304)
|
||||
* Start a new megolm session when devices are blacklisted
|
||||
[\#303](https://github.com/matrix-org/matrix-js-sdk/pull/303)
|
||||
* E2E: Download our own devicelist on startup
|
||||
[\#302](https://github.com/matrix-org/matrix-js-sdk/pull/302)
|
||||
|
||||
Changes in [0.7.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1) (2016-12-09)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1-rc.1...v0.7.1)
|
||||
|
||||
No changes
|
||||
|
||||
|
||||
Changes in [0.7.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1-rc.1) (2016-12-05)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.0...v0.7.1-rc.1)
|
||||
|
||||
* Avoid NPE when no sessionStore is given
|
||||
[\#300](https://github.com/matrix-org/matrix-js-sdk/pull/300)
|
||||
* Improve decryption error messages
|
||||
[\#299](https://github.com/matrix-org/matrix-js-sdk/pull/299)
|
||||
* Revert "Use native Array.isArray when available."
|
||||
[\#283](https://github.com/matrix-org/matrix-js-sdk/pull/283)
|
||||
* Use native Array.isArray when available.
|
||||
[\#282](https://github.com/matrix-org/matrix-js-sdk/pull/282)
|
||||
|
||||
Changes in [0.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.0) (2016-11-18)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4...v0.7.0)
|
||||
|
||||
* Avoid a packetstorm of device queries on startup
|
||||
[\#297](https://github.com/matrix-org/matrix-js-sdk/pull/297)
|
||||
* E2E: Check devices to share keys with on each send
|
||||
[\#295](https://github.com/matrix-org/matrix-js-sdk/pull/295)
|
||||
* Apply unknown-keyshare mitigations
|
||||
[\#296](https://github.com/matrix-org/matrix-js-sdk/pull/296)
|
||||
* distinguish unknown users from deviceless users
|
||||
[\#294](https://github.com/matrix-org/matrix-js-sdk/pull/294)
|
||||
* Allow starting client with initialSyncLimit = 0
|
||||
[\#293](https://github.com/matrix-org/matrix-js-sdk/pull/293)
|
||||
* Make timeline-window _unpaginate public and rename to unpaginate
|
||||
[\#289](https://github.com/matrix-org/matrix-js-sdk/pull/289)
|
||||
* Send a STOPPED sync updated after call to stopClient
|
||||
[\#286](https://github.com/matrix-org/matrix-js-sdk/pull/286)
|
||||
* Fix bug in verifying megolm event senders
|
||||
[\#292](https://github.com/matrix-org/matrix-js-sdk/pull/292)
|
||||
* Handle decryption of events after they arrive
|
||||
[\#288](https://github.com/matrix-org/matrix-js-sdk/pull/288)
|
||||
* Fix examples.
|
||||
[\#287](https://github.com/matrix-org/matrix-js-sdk/pull/287)
|
||||
* Add a travis.yml
|
||||
[\#278](https://github.com/matrix-org/matrix-js-sdk/pull/278)
|
||||
* Encrypt all events, including 'm.call.*'
|
||||
[\#277](https://github.com/matrix-org/matrix-js-sdk/pull/277)
|
||||
* Ignore reshares of known megolm sessions
|
||||
[\#276](https://github.com/matrix-org/matrix-js-sdk/pull/276)
|
||||
* Log to the console on unknown session
|
||||
[\#274](https://github.com/matrix-org/matrix-js-sdk/pull/274)
|
||||
* Make it easier for SDK users to wrap prevailing the 'request' function
|
||||
[\#273](https://github.com/matrix-org/matrix-js-sdk/pull/273)
|
||||
|
||||
Changes in [0.6.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4) (2016-11-04)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.2...v0.6.4)
|
||||
|
||||
* Change release script to pass version by environment variable
|
||||
|
||||
|
||||
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)
|
||||
|
||||
Breaking Changes
|
||||
----------------
|
||||
* Add a 'RECONNECTING' state to the sync states. This is an additional state
|
||||
between 'SYNCING' and 'ERROR', so most clients should not notice.
|
||||
|
||||
Other Changes
|
||||
----------------
|
||||
* Fix params getting replaced on register calls
|
||||
[\#233](https://github.com/matrix-org/matrix-js-sdk/pull/233)
|
||||
* Fix potential 30s delay on reconnect
|
||||
[\#232](https://github.com/matrix-org/matrix-js-sdk/pull/232)
|
||||
* uploadContent: Attempt some consistency between browser and node
|
||||
[\#230](https://github.com/matrix-org/matrix-js-sdk/pull/230)
|
||||
* Fix error handling on uploadContent
|
||||
[\#229](https://github.com/matrix-org/matrix-js-sdk/pull/229)
|
||||
* Fix uploadContent for node.js
|
||||
[\#226](https://github.com/matrix-org/matrix-js-sdk/pull/226)
|
||||
* Don't emit ERROR until a keepalive poke fails
|
||||
[\#223](https://github.com/matrix-org/matrix-js-sdk/pull/223)
|
||||
* Function to get the fallback url for interactive auth
|
||||
[\#224](https://github.com/matrix-org/matrix-js-sdk/pull/224)
|
||||
* Revert "Handle the first /sync failure differently."
|
||||
[\#222](https://github.com/matrix-org/matrix-js-sdk/pull/222)
|
||||
|
||||
Changes in [0.6.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.2) (2016-10-05)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.1...v0.6.2)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
There is a script `release.sh` which does the following, but if you need to do
|
||||
a release manually, here are the steps:
|
||||
|
||||
- `git checkout -b release-v0.x.x`
|
||||
- Update `CHANGELOG.md`
|
||||
- `npm version 0.x.x`
|
||||
- Merge `release-v0.x.x` onto `master`.
|
||||
- Push `master`.
|
||||
- Push the tag: `git push --tags`
|
||||
- `npm publish`
|
||||
- Generate documentation: `npm run gendoc` (this outputs HTML to `.jsdoc`)
|
||||
- Copy the documentation from `.jsdoc` to the `gh-pages` branch and update `index.html`
|
||||
- Merge `master` onto `develop`.
|
||||
- Push `develop`.
|
||||
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
-1
@@ -1 +0,0 @@
|
||||
Release builds and development builds will reside here.
|
||||
@@ -1 +1 @@
|
||||
../../../dist/browser-matrix-dev.js
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -135,11 +135,15 @@ rl.on('line', function(line) {
|
||||
// ==== END User input
|
||||
|
||||
// show the room list after syncing.
|
||||
matrixClient.on("syncComplete", function() {
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
matrixClient.on("sync", function(state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
setRoomList();
|
||||
printRoomList();
|
||||
printHelp();
|
||||
rl.prompt();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.on("Room", function() {
|
||||
|
||||
@@ -44,7 +44,15 @@ window.onload = function() {
|
||||
disableButtons(true, true, true);
|
||||
};
|
||||
|
||||
client.on("syncComplete", function () {
|
||||
matrixClient.on("sync", function(state, prevState, data) {
|
||||
switch (state) {
|
||||
case "PREPARED":
|
||||
syncComplete();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function syncComplete() {
|
||||
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
|
||||
disableButtons(false, true, true);
|
||||
|
||||
@@ -85,5 +93,5 @@ client.on("syncComplete", function () {
|
||||
call = c;
|
||||
addListeners(call);
|
||||
});
|
||||
});
|
||||
}
|
||||
client.startClient();
|
||||
|
||||
@@ -1 +1 @@
|
||||
../../../dist/browser-matrix-dev.js
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -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
|
||||
|
||||
+120
-17
@@ -45,6 +45,10 @@ var utils = require("./utils");
|
||||
*
|
||||
* @param {string} opts.accessToken The access_token for this user.
|
||||
*
|
||||
* @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
|
||||
* time to wait before timing out HTTP requests. If not specified, there is no
|
||||
* timeout.
|
||||
*
|
||||
* @param {Object} opts.queryParams Optional. Extra query parameters to append
|
||||
* to all requests with this client. Useful for application services which require
|
||||
* <code>?user_id=</code>.
|
||||
@@ -63,7 +67,8 @@ function MatrixBaseApis(opts) {
|
||||
request: opts.request,
|
||||
prefix: httpApi.PREFIX_R0,
|
||||
onlyData: true,
|
||||
extraParams: opts.queryParams
|
||||
extraParams: opts.queryParams,
|
||||
localTimeoutMs: opts.localTimeoutMs
|
||||
};
|
||||
this._http = new httpApi.MatrixHttpApi(this, httpOpts);
|
||||
|
||||
@@ -136,10 +141,12 @@ MatrixBaseApis.prototype.register = function(
|
||||
var params = {
|
||||
auth: auth
|
||||
};
|
||||
if (username !== undefined) { params.username = username; }
|
||||
if (password !== undefined) { params.password = password; }
|
||||
if (bindEmail !== undefined) { params.bind_email = bindEmail; }
|
||||
if (guestAccessToken !== undefined) { params.guest_access_token = guestAccessToken; }
|
||||
if (username !== undefined && username !== null) { params.username = username; }
|
||||
if (password !== undefined && password !== null) { params.password = password; }
|
||||
if (bindEmail !== undefined && bindEmail !== null) { params.bind_email = bindEmail; }
|
||||
if (guestAccessToken !== undefined && guestAccessToken !== null) {
|
||||
params.guest_access_token = guestAccessToken;
|
||||
}
|
||||
|
||||
return this.registerRequest(params, undefined, callback);
|
||||
};
|
||||
@@ -290,6 +297,23 @@ MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the fallback URL to use for unknown interactive-auth stages.
|
||||
*
|
||||
* @param {string} loginType the type of stage being attempted
|
||||
* @param {string} authSessionId the auth session ID provided by the homeserver
|
||||
*
|
||||
* @return {string} HS URL to hit to for the fallback interface
|
||||
*/
|
||||
MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId) {
|
||||
var path = utils.encodeUri("/auth/$loginType/fallback/web", {
|
||||
$loginType: loginType,
|
||||
});
|
||||
|
||||
return this._http.getUrl(path, {
|
||||
session: authSessionId,
|
||||
}, httpApi.PREFIX_R0);
|
||||
};
|
||||
|
||||
// Room operations
|
||||
// ===============
|
||||
@@ -551,19 +575,68 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibility =
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the visbility of a room bridged to a 3rd party network in
|
||||
* the current HS's room directory.
|
||||
* @param {string} networkId the network ID of the 3rd party
|
||||
* instance under which this room is published under.
|
||||
* @param {string} roomId
|
||||
* @param {string} visibility "public" to make the room visible
|
||||
* in the public directory, or "private" to make
|
||||
* it invisible.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
|
||||
function(networkId, roomId, visibility, callback) {
|
||||
var path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", {
|
||||
$networkId: networkId,
|
||||
$roomId: roomId
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
callback, "PUT", path, undefined, { "visibility": visibility }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Media operations
|
||||
// ================
|
||||
|
||||
/**
|
||||
* Upload a file to the media repository on the home server.
|
||||
* @param {File} file object
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*
|
||||
* @param {object} file The object to upload. On a browser, something that
|
||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||
* a a Buffer, String or ReadStream.
|
||||
*
|
||||
* @param {object} opts options object
|
||||
*
|
||||
* @param {string=} opts.name Name to give the file on the server. Defaults
|
||||
* to <tt>file.name</tt>.
|
||||
*
|
||||
* @param {string=} opts.type Content-type for the upload. Defaults to
|
||||
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
|
||||
*
|
||||
* @param {boolean=} opts.rawResponse Return the raw body, rather than
|
||||
* parsing the JSON. Defaults to false (except on node.js, where it
|
||||
* defaults to true for backwards compatibility).
|
||||
*
|
||||
* @param {boolean=} opts.onlyContentUri Just return the content URI,
|
||||
* rather than the whole body. Defaults to false (except on browsers,
|
||||
* where it defaults to true for backwards compatibility). Ignored if
|
||||
* opts.rawResponse is true.
|
||||
*
|
||||
* @param {Function=} opts.callback Deprecated. Optional. The callback to
|
||||
* invoke on success/failure. See the promise return values for more
|
||||
* information.
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to response object, as
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
MatrixBaseApis.prototype.uploadContent = function(file, callback) {
|
||||
return this._http.uploadContent(file, callback);
|
||||
MatrixBaseApis.prototype.uploadContent = function(file, opts) {
|
||||
return this._http.uploadContent(file, opts);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -644,6 +717,25 @@ MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} medium The threepid medium (eg. 'email')
|
||||
* @param {string} address The threepid address (eg. 'bob@example.com')
|
||||
* this must be as returned by getThreePids.
|
||||
* @return {module:client.Promise} Resolves: The server response on success
|
||||
* (generally the empty JSON object)
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.deleteThreePid = function(medium, address) {
|
||||
var path = "/account/3pid/delete";
|
||||
var data = {
|
||||
'medium': medium,
|
||||
'address': address
|
||||
};
|
||||
return this._http.authedRequestWithPrefix(
|
||||
undefined, "POST", path, null, data, httpApi.PREFIX_UNSTABLE
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make a request to change your password.
|
||||
* @param {Object} authDict
|
||||
@@ -705,16 +797,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
|
||||
);
|
||||
};
|
||||
@@ -925,24 +1024,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
|
||||
);
|
||||
};
|
||||
|
||||
+108
-23
@@ -94,6 +94,9 @@ try {
|
||||
* to all requests with this client. Useful for application services which require
|
||||
* <code>?user_id=</code>.
|
||||
*
|
||||
* @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
|
||||
* time to wait before timing out HTTP requests. If not specified, there is no timeout.
|
||||
*
|
||||
* @param {boolean} [opts.timelineSupport = false] Set to true to enable
|
||||
* improved timeline support ({@link
|
||||
* module:client~MatrixClient#getEventTimeline getEventTimeline}). It is
|
||||
@@ -149,7 +152,7 @@ function MatrixClient(opts) {
|
||||
this._notifTimelineSet = null;
|
||||
|
||||
this._crypto = null;
|
||||
if (CRYPTO_ENABLED && opts.sessionStore !== null &&
|
||||
if (CRYPTO_ENABLED && Boolean(opts.sessionStore) &&
|
||||
userId !== null && this.deviceId !== null) {
|
||||
this._crypto = new Crypto(
|
||||
this, this,
|
||||
@@ -351,7 +354,7 @@ MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
|
||||
if (this._crypto === null) {
|
||||
throw new Error("End-to-end encryption disabled");
|
||||
}
|
||||
return this._crypto.getStoredDevicesForUser(userId);
|
||||
return this._crypto.getStoredDevicesForUser(userId) || [];
|
||||
};
|
||||
|
||||
|
||||
@@ -463,38 +466,31 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) {
|
||||
* Decrypt a received event according to the algorithm specified in the event.
|
||||
*
|
||||
* @param {MatrixClient} client
|
||||
* @param {object} raw event
|
||||
*
|
||||
* @return {MatrixEvent}
|
||||
* @param {MatrixEvent} event
|
||||
*/
|
||||
function _decryptEvent(client, event) {
|
||||
if (!client._crypto) {
|
||||
return _badEncryptedMessage(event, "**Encryption not enabled**");
|
||||
_badEncryptedMessage(event, "Encryption not enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
var decryptionResult;
|
||||
try {
|
||||
decryptionResult = client._crypto.decryptEvent(event);
|
||||
client._crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
if (!(e instanceof Crypto.DecryptionError)) {
|
||||
throw e;
|
||||
}
|
||||
return _badEncryptedMessage(event, "**" + e.message + "**");
|
||||
_badEncryptedMessage(event, e.message);
|
||||
return;
|
||||
}
|
||||
return new MatrixEvent(
|
||||
event, decryptionResult.payload,
|
||||
decryptionResult.keysProved,
|
||||
decryptionResult.keysClaimed
|
||||
);
|
||||
}
|
||||
|
||||
function _badEncryptedMessage(event, reason) {
|
||||
return new MatrixEvent(event, {
|
||||
event.setClearData({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: reason,
|
||||
content: event.content,
|
||||
body: "** Unable to decrypt: " + reason + " **",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -696,6 +692,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
|
||||
@@ -1463,6 +1475,47 @@ MatrixClient.prototype.setPresence = function(opts, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
function _presenceList(callback, client, opts, method) {
|
||||
var path = utils.encodeUri("/presence/list/$userId", {
|
||||
$userId: client.credentials.userId
|
||||
});
|
||||
return client._http.authedRequest(callback, method, path, undefined, opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.getPresenceList = function(callback) {
|
||||
return _presenceList(callback, this, undefined, "GET");
|
||||
};
|
||||
|
||||
/**
|
||||
* Add users to the current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {string[]} userIds
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixClient.prototype.inviteToPresenceList = function(callback, userIds) {
|
||||
var opts = {"invite" : userIds};
|
||||
return _presenceList(callback, this, opts, "POST");
|
||||
};
|
||||
|
||||
/**
|
||||
* Drop users from the current user presence list.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @param {string[]} userIds
|
||||
* @return {module:client.Promise} Resolves: TODO
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
**/
|
||||
MatrixClient.prototype.dropFromPresenceList = function(callback, userIds) {
|
||||
var opts = {"drop" : userIds};
|
||||
return _presenceList(callback, this, opts, "POST");
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve older messages from the given room and put them in the timeline.
|
||||
*
|
||||
@@ -2395,7 +2448,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 +2564,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) {
|
||||
@@ -2791,10 +2866,11 @@ function _resolve(callback, defer, res) {
|
||||
|
||||
function _PojoToMatrixEventMapper(client) {
|
||||
function mapper(plainOldJsObject) {
|
||||
if (plainOldJsObject.type === "m.room.encrypted") {
|
||||
return _decryptEvent(client, plainOldJsObject);
|
||||
var event = new MatrixEvent(plainOldJsObject);
|
||||
if (event.isEncrypted()) {
|
||||
_decryptEvent(client, event);
|
||||
}
|
||||
return new MatrixEvent(plainOldJsObject);
|
||||
return event;
|
||||
}
|
||||
return mapper;
|
||||
}
|
||||
@@ -2834,6 +2910,10 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
|
||||
/**
|
||||
* Fires whenever the SDK receives a new event.
|
||||
* <p>
|
||||
* This is only fired for live events received via /sync - it is not fired for
|
||||
* events received over context, search, or pagination APIs.
|
||||
*
|
||||
* @event module:client~MatrixClient#"event"
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @example
|
||||
@@ -2867,6 +2947,9 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* the server for updates. This may be called multiple times even if the state is
|
||||
* already ERROR. <i>This is the equivalent of "syncError" in the previous
|
||||
* API.</i></li>
|
||||
* <li>RECONNECTING: The sync connedtion has dropped, but not in a way that should
|
||||
* be considered erroneous.
|
||||
* </li>
|
||||
* <li>STOPPED: The client has stopped syncing with server due to stopClient
|
||||
* being called.
|
||||
* </li>
|
||||
@@ -2876,8 +2959,10 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* +---->STOPPED
|
||||
* |
|
||||
* +----->PREPARED -------> SYNCING <--+
|
||||
* | ^ | |
|
||||
* null ------+ | +---------------+ |
|
||||
* | ^ ^ |
|
||||
* | | | |
|
||||
* | | V |
|
||||
* null ------+ | +-RECONNECTING<-+ |
|
||||
* | | V |
|
||||
* +------->ERROR ---------------------+
|
||||
*
|
||||
|
||||
+75
-2
@@ -20,10 +20,33 @@ limitations under the License.
|
||||
*
|
||||
* @module crypto/OlmDevice
|
||||
*/
|
||||
|
||||
var Olm = require("olm");
|
||||
var utils = require("../utils");
|
||||
|
||||
|
||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||
var MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
|
||||
|
||||
function checkPayloadLength(payloadString) {
|
||||
if (payloadString === undefined) {
|
||||
throw new Error("payloadString undefined");
|
||||
}
|
||||
|
||||
if (payloadString.length > MAX_PLAINTEXT_LENGTH) {
|
||||
// might as well fail early here rather than letting the olm library throw
|
||||
// a cryptic memory allocation error.
|
||||
//
|
||||
// Note that even if we manage to do the encryption, the message send may fail,
|
||||
// because by the time we've wrapped the ciphertext in the event object, it may
|
||||
// exceed 65K. But at least we won't just fail with "abort()" in that case.
|
||||
throw new Error("Message too long (" + payloadString.length + " bytes). " +
|
||||
"The maximum for an encrypted message is " +
|
||||
MAX_PLAINTEXT_LENGTH + " bytes.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Manages the olm cryptography functions. Each OlmDevice has a single
|
||||
* OlmAccount and a number of OlmSessions.
|
||||
@@ -58,6 +81,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 +409,8 @@ OlmDevice.prototype.encryptMessage = function(
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
checkPayloadLength(payloadString);
|
||||
|
||||
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
|
||||
var res = session.encrypt(payloadString);
|
||||
self._saveSession(theirDeviceIdentityKey, session);
|
||||
@@ -499,6 +536,8 @@ OlmDevice.prototype.createOutboundGroupSession = function() {
|
||||
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
|
||||
var self = this;
|
||||
|
||||
checkPayloadLength(payloadString);
|
||||
|
||||
return this._getOutboundGroupSession(sessionId, function(session) {
|
||||
var res = session.encrypt(payloadString);
|
||||
self._saveOutboundGroupSession(session);
|
||||
@@ -611,6 +650,24 @@ OlmDevice.prototype.addInboundGroupSession = function(
|
||||
roomId, senderKey, sessionId, sessionKey, keysClaimed
|
||||
) {
|
||||
var self = this;
|
||||
|
||||
/* if we already have this session, consider updating it */
|
||||
function updateSession(session) {
|
||||
console.log("Update for megolm session " + senderKey + "/" + sessionId);
|
||||
// for now we just ignore updates. TODO: implement something here
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var r = this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, updateSession
|
||||
);
|
||||
|
||||
if (r !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// new session.
|
||||
var session = new Olm.InboundGroupSession();
|
||||
try {
|
||||
session.create(sessionKey);
|
||||
@@ -648,6 +705,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 +729,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;
|
||||
@@ -85,15 +88,6 @@ EncryptionAlgorithm.prototype.onRoomMembership = function(
|
||||
event, member, oldMembership
|
||||
) {};
|
||||
|
||||
/**
|
||||
* Called when a new device announces itself in the room
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} deviceId deviceId of the device
|
||||
*/
|
||||
EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
|
||||
|
||||
|
||||
/**
|
||||
* base type for decryption implementations
|
||||
*
|
||||
@@ -101,10 +95,17 @@ 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} params.crypto crypto core
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {string=} params.roomId The ID of the room we will be receiving
|
||||
* from. Null for to-device events.
|
||||
*/
|
||||
var DecryptionAlgorithm = function(params) {
|
||||
this._userId = params.userId;
|
||||
this._crypto = params.crypto;
|
||||
this._olmDevice = params.olmDevice;
|
||||
this._roomId = params.roomId;
|
||||
};
|
||||
/** */
|
||||
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
|
||||
|
||||
+337
-209
@@ -27,6 +27,93 @@ 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 {object} sharedWithDevices
|
||||
* devices with which we have shared the session key
|
||||
* userId -> {deviceId -> msgindex}
|
||||
*/
|
||||
function OutboundSessionInfo(sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
this.useCount = 0;
|
||||
this.creationTime = new Date().getTime();
|
||||
this.sharedWithDevices = {};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determine if this session has been shared with devices which it shouldn't
|
||||
* have been.
|
||||
*
|
||||
* @param {Object} devicesInRoom userId -> {deviceId -> object}
|
||||
* devices we should shared the session with.
|
||||
*
|
||||
* @return {Boolean} true if we have shared the session with devices which aren't
|
||||
* in devicesInRoom.
|
||||
*/
|
||||
OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
|
||||
devicesInRoom
|
||||
) {
|
||||
|
||||
for (var userId in this.sharedWithDevices) {
|
||||
if (!this.sharedWithDevices.hasOwnProperty(userId)) { continue; }
|
||||
|
||||
if (!devicesInRoom.hasOwnProperty(userId)) {
|
||||
console.log("Starting new session because we shared with " + userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (var deviceId in this.sharedWithDevices[userId]) {
|
||||
if (!this.sharedWithDevices[userId].hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
|
||||
console.log(
|
||||
"Starting new session because we shared with " +
|
||||
userId + ":" + deviceId
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Megolm encryption implementation
|
||||
*
|
||||
@@ -38,15 +125,25 @@ var base = require("./base");
|
||||
*/
|
||||
function MegolmEncryption(params) {
|
||||
base.EncryptionAlgorithm.call(this, params);
|
||||
this._prepPromise = null;
|
||||
this._outboundSessionId = null;
|
||||
this._discardNewSession = false;
|
||||
|
||||
// devices which have joined since we last sent a message.
|
||||
// userId -> {deviceId -> true}, or
|
||||
// userId -> true
|
||||
this._devicesPendingKeyShare = {};
|
||||
this._sharePromise = null;
|
||||
// the most recent attempt to set up a session. This is used to serialise
|
||||
// the session setups, so that we have a race-free view of which session we
|
||||
// are using, and which devices we have shared the keys with. It resolves
|
||||
// with an OutboundSessionInfo (or undefined, for the first message in the
|
||||
// room).
|
||||
this._setupPromise = q();
|
||||
|
||||
// default rotation periods
|
||||
this._sessionRotationPeriodMsgs = 100;
|
||||
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,70 +152,96 @@ 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) {
|
||||
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
|
||||
var self = this;
|
||||
|
||||
if (this._prepPromise) {
|
||||
// prep already in progress
|
||||
return this._prepPromise;
|
||||
}
|
||||
var session;
|
||||
|
||||
var sessionId = this._outboundSessionId;
|
||||
// takes the previous OutboundSessionInfo, and considers whether to create
|
||||
// a new one. Also shares the key with any (new) devices in the room.
|
||||
// Updates `session` to hold the final OutboundSessionInfo.
|
||||
//
|
||||
// returns a promise which resolves once the keyshare is successful.
|
||||
function prepareSession(oldSession) {
|
||||
session = oldSession;
|
||||
|
||||
// need to make a brand new session?
|
||||
if (!sessionId) {
|
||||
this._prepPromise = this._prepareNewSession(room).
|
||||
finally(function() {
|
||||
self._prepPromise = null;
|
||||
});
|
||||
return this._prepPromise;
|
||||
}
|
||||
|
||||
if (this._sharePromise) {
|
||||
// key share already in progress
|
||||
return this._sharePromise;
|
||||
}
|
||||
|
||||
// prep already done, but check for new devices
|
||||
var shareMap = this._devicesPendingKeyShare;
|
||||
this._devicesPendingKeyShare = {};
|
||||
|
||||
// check each user is (still) a member of the room
|
||||
for (var userId in shareMap) {
|
||||
if (!shareMap.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
// need to make a brand new session?
|
||||
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
|
||||
self._sessionRotationPeriodMs)
|
||||
) {
|
||||
console.log("Starting new megolm session because we need to rotate.");
|
||||
session = null;
|
||||
}
|
||||
|
||||
// XXX what about rooms where invitees can see the content?
|
||||
var member = room.getMember(userId);
|
||||
if (member.membership !== "join") {
|
||||
delete shareMap[userId];
|
||||
// determine if we have shared with anyone we shouldn't have
|
||||
if (session && session.sharedWithTooManyDevices(devicesInRoom)) {
|
||||
session = null;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
session = self._prepareNewSession();
|
||||
}
|
||||
|
||||
// now check if we need to share with any devices
|
||||
var shareMap = {};
|
||||
|
||||
for (var userId in devicesInRoom) {
|
||||
if (!devicesInRoom.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var userDevices = devicesInRoom[userId];
|
||||
|
||||
for (var deviceId in userDevices) {
|
||||
if (!userDevices.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var deviceInfo = userDevices[deviceId];
|
||||
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
if (key == self._olmDevice.deviceCurve25519Key) {
|
||||
// don't bother sending to ourself
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!session.sharedWithDevices[userId] ||
|
||||
session.sharedWithDevices[userId][deviceId] === undefined
|
||||
) {
|
||||
shareMap[userId] = shareMap[userId] || [];
|
||||
shareMap[userId].push(deviceInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self._shareKeyWithDevices(
|
||||
session, shareMap
|
||||
);
|
||||
}
|
||||
|
||||
this._sharePromise = this._shareKeyWithDevices(
|
||||
sessionId, shareMap
|
||||
).finally(function() {
|
||||
self._sharePromise = null;
|
||||
}).then(function() {
|
||||
return sessionId;
|
||||
});
|
||||
// helper which returns the session prepared by prepareSession
|
||||
function returnSession() { return session; }
|
||||
|
||||
return this._sharePromise;
|
||||
// first wait for the previous share to complete
|
||||
var prom = this._setupPromise.then(prepareSession);
|
||||
|
||||
// _setupPromise resolves to `session` whether or not the share succeeds
|
||||
this._setupPromise = prom.then(returnSession, returnSession);
|
||||
|
||||
// but we return a promise which only resolves if the share was successful.
|
||||
return prom.then(returnSession);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @return {module: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) {
|
||||
MegolmEncryption.prototype._prepareNewSession = function() {
|
||||
var session_id = this._olmDevice.createOutboundGroupSession();
|
||||
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
|
||||
|
||||
@@ -127,100 +250,53 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
|
||||
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
|
||||
);
|
||||
|
||||
// we're going to share the key with all current members of the room,
|
||||
// so we can reset this.
|
||||
this._devicesPendingKeyShare = {};
|
||||
|
||||
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
var shareMap = {};
|
||||
for (var i = 0; i < roomMembers.length; i++) {
|
||||
var userId = roomMembers[i];
|
||||
shareMap[userId] = true;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
// TODO: we need to give the user a chance to block any devices or users
|
||||
// before we send them the keys; it's too late to download them here.
|
||||
return 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;
|
||||
}).finally(function() {
|
||||
self._discardNewSession = false;
|
||||
});
|
||||
return new OutboundSessionInfo(session_id);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {string} session_id
|
||||
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*
|
||||
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
|
||||
* Map from userid to either: true (meaning this is a new user in the room,
|
||||
* so all of his devices need the keys); or a map from deviceid to true
|
||||
* (meaning this user has one or more new devices, which need the keys).
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves once the key sharing
|
||||
* message has been sent.
|
||||
*/
|
||||
MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) {
|
||||
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
|
||||
var self = this;
|
||||
|
||||
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
|
||||
var key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
|
||||
var payload = {
|
||||
type: "m.room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: session_id,
|
||||
session_id: session.sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
}
|
||||
};
|
||||
|
||||
// we downloaded the user's device list when they joined the room, or when
|
||||
// the new device announced itself, so there is no need to do so now.
|
||||
var contentMap = {};
|
||||
|
||||
return self._crypto.ensureOlmSessionsForUsers(
|
||||
utils.keys(shareMap)
|
||||
return olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser
|
||||
).then(function(devicemap) {
|
||||
var contentMap = {};
|
||||
var haveTargets = false;
|
||||
|
||||
for (var userId in devicemap) {
|
||||
if (!devicemap.hasOwnProperty(userId)) {
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var devicesToShareWith = shareMap[userId];
|
||||
var devicesToShareWith = devicesByUser[userId];
|
||||
var sessionResults = devicemap[userId];
|
||||
|
||||
for (var deviceId in sessionResults) {
|
||||
if (!sessionResults.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (devicesToShareWith === true) {
|
||||
// all devices
|
||||
} else if (!devicesToShareWith[deviceId]) {
|
||||
// not a new device
|
||||
continue;
|
||||
}
|
||||
for (var i = 0; i < devicesToShareWith.length; i++) {
|
||||
var deviceInfo = devicesToShareWith[i];
|
||||
var deviceId = deviceInfo.deviceId;
|
||||
|
||||
var sessionResult = sessionResults[deviceId];
|
||||
if (!sessionResult.sessionId) {
|
||||
@@ -242,19 +318,27 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
|
||||
"sharing keys with device " + userId + ":" + deviceId
|
||||
);
|
||||
|
||||
var deviceInfo = sessionResult.device;
|
||||
var encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: self._olmDevice.deviceCurve25519Key,
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -265,6 +349,27 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap)
|
||||
|
||||
// TODO: retries
|
||||
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
}).then(function() {
|
||||
// Add the devices we have shared with to session.sharedWithDevices.
|
||||
//
|
||||
// we deliberately iterate over devicesByUser (ie, the devices we
|
||||
// attempted to share with) rather than the contentMap (those we did
|
||||
// share with), because we don't want to try to claim a one-time-key
|
||||
// for dead devices on every message.
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
if (!session.sharedWithDevices[userId]) {
|
||||
session.sharedWithDevices[userId] = {};
|
||||
}
|
||||
var devicesToShareWith = devicesByUser[userId];
|
||||
for (var i = 0; i < devicesToShareWith.length; i++) {
|
||||
var deviceInfo = devicesToShareWith[i];
|
||||
session.sharedWithDevices[userId][deviceInfo.deviceId] =
|
||||
key.chain_index;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -279,7 +384,9 @@ 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._getDevicesInRoom(room).then(function(devicesInRoom) {
|
||||
return self._ensureOutboundSession(devicesInRoom);
|
||||
}).then(function(session) {
|
||||
var payloadJson = {
|
||||
room_id: self._roomId,
|
||||
type: eventType,
|
||||
@@ -287,98 +394,64 @@ 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;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* Get the list of unblocked devices for all users in the room
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event event causing the change
|
||||
* @param {module:models/room-member} member user whose membership changed
|
||||
* @param {string=} oldMembership previous membership
|
||||
*/
|
||||
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
|
||||
var newMembership = member.membership;
|
||||
|
||||
if (newMembership === 'join') {
|
||||
this._onNewRoomMember(member.userId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newMembership === 'invite' && oldMembership !== 'join') {
|
||||
// we don't (yet) share keys with invited members, so nothing to do yet
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if (this._prepPromise) {
|
||||
console.log("Discarding as-yet-incomplete megolm session due to " +
|
||||
"change in membership of " + member.userId + " (" +
|
||||
oldMembership + "->" + newMembership + ")");
|
||||
this._discardNewSession = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* handle a new user joining a room
|
||||
* @param {module:models/room} room
|
||||
*
|
||||
* @param {string} userId new member
|
||||
* @return {module:client.Promise} Promise which resolves to a map
|
||||
* from userId to deviceId to deviceInfo
|
||||
*/
|
||||
MegolmEncryption.prototype._onNewRoomMember = function(userId) {
|
||||
// make sure we have a list of this user's devices. We are happy to use a
|
||||
// cached version here: we assume that if we already have a list of the
|
||||
// user's devices, then we already share an e2e room with them, which means
|
||||
// that they will have announced any new devices via an m.new_device.
|
||||
this._crypto.downloadKeys([userId], false).done();
|
||||
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
|
||||
// XXX what about rooms where invitees can see the content?
|
||||
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
// also flag this user up for needing a keyshare.
|
||||
this._devicesPendingKeyShare[userId] = true;
|
||||
// We are happy to use a cached version here: we assume that if we already
|
||||
// have a list of the user's devices, then we already share an e2e room
|
||||
// with them, which means that they will have announced any new devices via
|
||||
// an m.new_device.
|
||||
return this._crypto.downloadKeys(roomMembers, false).then(function(devices) {
|
||||
// remove any blocked devices
|
||||
for (var userId in devices) {
|
||||
if (!devices.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var userDevices = devices[userId];
|
||||
for (var deviceId in userDevices) {
|
||||
if (!userDevices.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
if (userDevices[deviceId].isBlocked()) {
|
||||
delete userDevices[deviceId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {string} userId owner of the device
|
||||
* @param {string} deviceId deviceId of the device
|
||||
*/
|
||||
MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
|
||||
var d = this._devicesPendingKeyShare[userId];
|
||||
|
||||
if (d === true) {
|
||||
// we already want to share keys with all devices for this user
|
||||
return;
|
||||
}
|
||||
|
||||
if (!d) {
|
||||
this._devicesPendingKeyShare[userId] = d = {};
|
||||
}
|
||||
|
||||
d[deviceId] = true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Megolm decryption implementation
|
||||
*
|
||||
@@ -390,13 +463,17 @@ MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
|
||||
*/
|
||||
function MegolmDecryption(params) {
|
||||
base.DecryptionAlgorithm.call(this, params);
|
||||
|
||||
// events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||
// senderKey|sessionId to list of MatrixEvents
|
||||
this._pendingEvents = {};
|
||||
}
|
||||
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} event raw event
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* @return {null} The event referred to an unknown megolm session
|
||||
* @return {module:crypto.DecryptionResult} decryption result
|
||||
@@ -405,7 +482,7 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
||||
* problem decrypting the event
|
||||
*/
|
||||
MegolmDecryption.prototype.decryptEvent = function(event) {
|
||||
var content = event.content;
|
||||
var content = event.getWireContent();
|
||||
|
||||
if (!content.sender_key || !content.session_id ||
|
||||
!content.ciphertext
|
||||
@@ -413,21 +490,56 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
|
||||
throw new base.DecryptionError("Missing fields in input");
|
||||
}
|
||||
|
||||
var res;
|
||||
try {
|
||||
var res = this._olmDevice.decryptGroupMessage(
|
||||
event.room_id, content.sender_key, content.session_id, content.ciphertext
|
||||
res = this._olmDevice.decryptGroupMessage(
|
||||
event.getRoomId(), 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) {
|
||||
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
this._addEventToPendingList(event);
|
||||
}
|
||||
throw new base.DecryptionError(e);
|
||||
}
|
||||
|
||||
if (res === null) {
|
||||
// We've got a message for a session we don't have.
|
||||
this._addEventToPendingList(event);
|
||||
throw new base.DecryptionError(
|
||||
"The sender's device has not sent us the keys for this message."
|
||||
);
|
||||
}
|
||||
|
||||
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.getRoomId()) {
|
||||
throw new base.DecryptionError(
|
||||
"Message intended for room " + payload.room_id
|
||||
);
|
||||
}
|
||||
|
||||
event.setClearData(payload, res.keysProved, res.keysClaimed);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Add an event to the list of those we couldn't decrypt the first time we
|
||||
* saw them.
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event
|
||||
*/
|
||||
MegolmDecryption.prototype._addEventToPendingList = function(event) {
|
||||
var content = event.getWireContent();
|
||||
var k = content.sender_key + "|" + content.session_id;
|
||||
if (!this._pendingEvents[k]) {
|
||||
this._pendingEvents[k] = [];
|
||||
}
|
||||
this._pendingEvents[k].push(event);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -451,6 +563,22 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
content.room_id, event.getSenderKey(), content.session_id,
|
||||
content.session_key, event.getKeysClaimed()
|
||||
);
|
||||
|
||||
var k = event.getSenderKey() + "|" + content.session_id;
|
||||
var pending = this._pendingEvents[k];
|
||||
if (pending) {
|
||||
// have another go at decrypting events sent with this session.
|
||||
delete this._pendingEvents[k];
|
||||
|
||||
for (var i = 0; i < pending.length; i++) {
|
||||
try {
|
||||
this.decryptEvent(pending[i]);
|
||||
console.log("successful re-decryption of", pending[i]);
|
||||
} catch (e) {
|
||||
console.log("Still can't decrypt", pending[i], e.stack || e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
base.registerAlgorithm(
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -140,15 +151,13 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @return {module:crypto.DecryptionResult} decryption result
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
|
||||
* problem decrypting the event
|
||||
*/
|
||||
OlmDecryption.prototype.decryptEvent = function(event) {
|
||||
var content = event.content;
|
||||
var content = event.getWireContent();
|
||||
var deviceKey = content.sender_key;
|
||||
var ciphertext = content.ciphertext;
|
||||
|
||||
@@ -167,22 +176,61 @@ OlmDecryption.prototype.decryptEvent = function(event) {
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
"Failed to decrypt Olm event (id=" +
|
||||
event.event_id + ") from " + deviceKey +
|
||||
event.getId() + ") from " + deviceKey +
|
||||
": " + e.message
|
||||
);
|
||||
throw new base.DecryptionError("Bad Encrypted Message");
|
||||
}
|
||||
|
||||
|
||||
// TODO: Check the sender user id matches the sender key.
|
||||
// TODO: check the room_id and fingerprint
|
||||
var payload = JSON.parse(payloadString);
|
||||
return {
|
||||
payload: payload,
|
||||
sessionExists: true,
|
||||
keysProved: {curve25519: deviceKey},
|
||||
keysClaimed: payload.keys || {}
|
||||
};
|
||||
|
||||
// check that we were the intended recipient, to avoid unknown-key attack
|
||||
// https://github.com/vector-im/vector-web/issues/2483
|
||||
if (payload.recipient != this._userId) {
|
||||
console.warn(
|
||||
"Event " + event.getId() + ": 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.ed25519 !=
|
||||
this._olmDevice.deviceEd25519Key) {
|
||||
console.warn(
|
||||
"Event " + event.getId() + ": Intended recipient ed25519 key " +
|
||||
payload.recipient_keys.ed25519 + " did not match ours"
|
||||
);
|
||||
throw new base.DecryptionError("Message not intended for this device");
|
||||
}
|
||||
|
||||
// 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 != event.getSender()) {
|
||||
console.warn(
|
||||
"Event " + event.getId() + ": original sender " + payload.sender +
|
||||
" does not match reported sender " + event.getSender()
|
||||
);
|
||||
throw new base.DecryptionError(
|
||||
"Message forwarded from " + payload.sender
|
||||
);
|
||||
}
|
||||
|
||||
// Olm events intended for a room have a room_id.
|
||||
if (payload.room_id !== event.getRoomId()) {
|
||||
console.warn(
|
||||
"Event " + event.getId() + ": original room " + payload.room_id +
|
||||
" does not match reported room " + event.room_id
|
||||
);
|
||||
throw new base.DecryptionError(
|
||||
"Message intended for room " + payload.room_id
|
||||
);
|
||||
}
|
||||
|
||||
event.setClearData(payload, {curve25519: deviceKey}, payload.keys || {});
|
||||
};
|
||||
|
||||
|
||||
|
||||
+344
-271
@@ -54,10 +54,19 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
||||
this._userId = userId;
|
||||
this._deviceId = deviceId;
|
||||
|
||||
this._initialSyncCompleted = false;
|
||||
// userId -> true
|
||||
this._pendingUsersWithNewDevices = {};
|
||||
// userId -> [promise, ...]
|
||||
this._keyDownloadsInProgressByUser = {};
|
||||
|
||||
this._olmDevice = new OlmDevice(sessionStore);
|
||||
|
||||
// EncryptionAlgorithm instance for each room
|
||||
this._roomAlgorithms = {};
|
||||
this._roomEncryptors = {};
|
||||
|
||||
// map from algorithm to DecryptionAlgorithm instance, for each room
|
||||
this._roomDecryptors = {};
|
||||
|
||||
this._supportedAlgorithms = utils.keys(
|
||||
algorithms.DECRYPTION_CLASSES
|
||||
@@ -70,24 +79,32 @@ function Crypto(baseApis, eventEmitter, sessionStore, userId, deviceId) {
|
||||
this._deviceKeys["curve25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceCurve25519Key;
|
||||
|
||||
// add our own deviceinfo to the sessionstore
|
||||
var deviceInfo = {
|
||||
keys: this._deviceKeys,
|
||||
algorithms: this._supportedAlgorithms,
|
||||
verified: DeviceVerification.VERIFIED,
|
||||
};
|
||||
var myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
||||
this._userId
|
||||
) || {};
|
||||
myDevices[this._deviceId] = deviceInfo;
|
||||
this._sessionStore.storeEndToEndDevicesForUser(
|
||||
this._userId, myDevices
|
||||
);
|
||||
|
||||
_registerEventHandlers(this, eventEmitter);
|
||||
if (!myDevices) {
|
||||
// we don't yet have a list of our own devices; make sure we
|
||||
// get one when we flush the pendingUsersWithNewDevices.
|
||||
this._pendingUsersWithNewDevices[this._userId] = true;
|
||||
myDevices = {};
|
||||
}
|
||||
|
||||
// map from userId -> deviceId -> roomId -> timestamp
|
||||
this._lastNewDeviceMessageTsByUserDeviceRoom = {};
|
||||
if (!myDevices[this._deviceId]) {
|
||||
// add our own deviceinfo to the sessionstore
|
||||
var deviceInfo = {
|
||||
keys: this._deviceKeys,
|
||||
algorithms: this._supportedAlgorithms,
|
||||
verified: DeviceVerification.VERIFIED,
|
||||
};
|
||||
|
||||
myDevices[this._deviceId] = deviceInfo;
|
||||
this._sessionStore.storeEndToEndDevicesForUser(
|
||||
this._userId, myDevices
|
||||
);
|
||||
}
|
||||
|
||||
_registerEventHandlers(this, eventEmitter);
|
||||
}
|
||||
|
||||
function _registerEventHandlers(crypto, eventEmitter) {
|
||||
@@ -174,7 +191,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 +234,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 +252,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
|
||||
}, {
|
||||
@@ -266,53 +284,138 @@ function _uploadOneTimeKeys(crypto) {
|
||||
Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||
var self = this;
|
||||
|
||||
// map from userid -> deviceid -> DeviceInfo
|
||||
var stored = {};
|
||||
// promises we need to wait for while the download happens
|
||||
var promises = [];
|
||||
|
||||
// list of userids we need to download keys for
|
||||
var downloadUsers = [];
|
||||
|
||||
for (var i = 0; i < userIds.length; ++i) {
|
||||
var userId = userIds[i];
|
||||
stored[userId] = {};
|
||||
function perUserCatch(u) {
|
||||
return function(e) {
|
||||
console.warn('Error downloading keys for user ' + u + ':', e);
|
||||
};
|
||||
}
|
||||
|
||||
var devices = this.getStoredDevicesForUser(userId);
|
||||
for (var j = 0; j < devices.length; ++j) {
|
||||
var dev = devices[j];
|
||||
stored[userId][dev.deviceId] = dev;
|
||||
}
|
||||
if (forceDownload) {
|
||||
downloadUsers = userIds;
|
||||
} else {
|
||||
for (var i = 0; i < userIds.length; ++i) {
|
||||
var u = userIds[i];
|
||||
|
||||
if (devices.length === 0 || forceDownload) {
|
||||
downloadUsers.push(userId);
|
||||
var inprogress = this._keyDownloadsInProgressByUser[u];
|
||||
if (inprogress) {
|
||||
// wait for the download to complete
|
||||
promises.push(q.any(inprogress).catch(perUserCatch(u)));
|
||||
} else if (!this.getStoredDevicesForUser(u)) {
|
||||
downloadUsers.push(u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadUsers.length === 0) {
|
||||
return q(stored);
|
||||
if (downloadUsers.length > 0) {
|
||||
var r = this._doKeyDownloadForUsers(downloadUsers);
|
||||
downloadUsers.map(function(u) {
|
||||
promises.push(r[u].catch(perUserCatch(u)));
|
||||
});
|
||||
}
|
||||
|
||||
return this._baseApis.downloadKeysForUsers(
|
||||
return q.all(promises).then(function() {
|
||||
return self._getDevicesFromStore(userIds);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the stored device keys for a list of user ids
|
||||
*
|
||||
* @param {string[]} userIds the list of users to list keys for.
|
||||
*
|
||||
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
|
||||
*/
|
||||
Crypto.prototype._getDevicesFromStore = function(userIds) {
|
||||
var stored = {};
|
||||
var self = this;
|
||||
userIds.map(function(u) {
|
||||
stored[u] = {};
|
||||
var devices = self.getStoredDevicesForUser(u) || [];
|
||||
devices.map(function(dev) {
|
||||
stored[u][dev.deviceId] = dev;
|
||||
});
|
||||
});
|
||||
return stored;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string[]} downloadUsers list of userIds
|
||||
*
|
||||
* @return {Object a map from userId to a promise for a result for that user
|
||||
*/
|
||||
Crypto.prototype._doKeyDownloadForUsers = function(downloadUsers) {
|
||||
var self = this;
|
||||
|
||||
console.log('Starting key download for ' + downloadUsers);
|
||||
|
||||
var deferMap = {};
|
||||
var promiseMap = {};
|
||||
|
||||
downloadUsers.map(function(u) {
|
||||
var deferred = q.defer();
|
||||
var promise = deferred.promise.finally(function() {
|
||||
var inProgress = self._keyDownloadsInProgressByUser[u];
|
||||
utils.removeElement(inProgress, function(e) { return e === promise; });
|
||||
if (inProgress.length === 0) {
|
||||
// no more downloads for this user; remove the element
|
||||
delete self._keyDownloadsInProgressByUser[u];
|
||||
}
|
||||
});
|
||||
|
||||
if (!self._keyDownloadsInProgressByUser[u]) {
|
||||
self._keyDownloadsInProgressByUser[u] = [];
|
||||
}
|
||||
self._keyDownloadsInProgressByUser[u].push(promise);
|
||||
|
||||
deferMap[u] = deferred;
|
||||
promiseMap[u] = promise;
|
||||
});
|
||||
|
||||
this._baseApis.downloadKeysForUsers(
|
||||
downloadUsers
|
||||
).then(function(res) {
|
||||
for (var userId in res.device_keys) {
|
||||
if (!stored.hasOwnProperty(userId)) {
|
||||
// spurious result
|
||||
).done(function(res) {
|
||||
var dk = res.device_keys || {};
|
||||
|
||||
for (var i = 0; i < downloadUsers.length; ++i) {
|
||||
var userId = downloadUsers[i];
|
||||
var deviceId;
|
||||
|
||||
console.log('got keys for ' + userId + ':', dk[userId]);
|
||||
|
||||
if (!dk[userId]) {
|
||||
// no result for this user
|
||||
var err = 'Unknown';
|
||||
// TODO: do something with res.failures
|
||||
deferMap[userId].reject(err);
|
||||
continue;
|
||||
}
|
||||
|
||||
// map from deviceid -> deviceinfo for this user
|
||||
var userStore = stored[userId];
|
||||
var updated = _updateStoredDeviceKeysForUser(
|
||||
self._olmDevice, userId, userStore, res.device_keys[userId]
|
||||
);
|
||||
|
||||
if (!updated) {
|
||||
continue;
|
||||
var userStore = {};
|
||||
var devs = self._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (devs) {
|
||||
for (deviceId in devs) {
|
||||
if (devs.hasOwnProperty(deviceId)) {
|
||||
var d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||
userStore[deviceId] = d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_updateStoredDeviceKeysForUser(
|
||||
self._olmDevice, userId, userStore, dk[userId]
|
||||
);
|
||||
|
||||
// update the session store
|
||||
var storage = {};
|
||||
for (var deviceId in userStore) {
|
||||
for (deviceId in userStore) {
|
||||
if (!userStore.hasOwnProperty(deviceId)) {
|
||||
continue;
|
||||
}
|
||||
@@ -322,9 +425,16 @@ Crypto.prototype.downloadKeys = function(userIds, forceDownload) {
|
||||
self._sessionStore.storeEndToEndDevicesForUser(
|
||||
userId, storage
|
||||
);
|
||||
|
||||
deferMap[userId].resolve();
|
||||
}
|
||||
return stored;
|
||||
}, function(err) {
|
||||
downloadUsers.map(function(u) {
|
||||
deferMap[u].reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
return promiseMap;
|
||||
};
|
||||
|
||||
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
@@ -350,9 +460,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 +488,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 +506,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);
|
||||
olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
|
||||
} catch (e) {
|
||||
console.log("Unable to verify signature on device " +
|
||||
userId + ":" + deviceId + ":", e);
|
||||
@@ -434,12 +546,13 @@ function _storeDeviceKeys(_olmDevice, userId, deviceId, userStore, deviceResult)
|
||||
*
|
||||
* @param {string} userId the user to list keys for.
|
||||
*
|
||||
* @return {module:crypto/deviceinfo[]} list of devices
|
||||
* @return {module:crypto/deviceinfo[]?} list of devices, or null if we haven't
|
||||
* managed to get a list of devices for this user yet.
|
||||
*/
|
||||
Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
||||
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (!devs) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
var res = [];
|
||||
for (var deviceId in devs) {
|
||||
@@ -450,6 +563,22 @@ Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the stored keys for a single device
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {string} deviceId
|
||||
*
|
||||
* @return {module:crypto/deviceinfo?} list of devices, or undefined
|
||||
* if we don't know about this device
|
||||
*/
|
||||
Crypto.prototype.getStoredDevice = function(userId, deviceId) {
|
||||
var devs = this._sessionStore.getEndToEndDevicesForUser(userId);
|
||||
if (!devs || !devs[deviceId]) {
|
||||
return undefined;
|
||||
}
|
||||
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||
};
|
||||
|
||||
/**
|
||||
* List the stored device keys for a user id
|
||||
@@ -462,7 +591,7 @@ Crypto.prototype.getStoredDevicesForUser = function(userId) {
|
||||
* "key", and "display_name" parameters.
|
||||
*/
|
||||
Crypto.prototype.listDeviceKeys = function(userId) {
|
||||
var devices = this.getStoredDevicesForUser(userId);
|
||||
var devices = this.getStoredDevicesForUser(userId) || [];
|
||||
|
||||
var result = [];
|
||||
|
||||
@@ -594,7 +723,7 @@ Crypto.prototype.setDeviceVerification = function(userId, deviceId, verified, bl
|
||||
* @return {Object.<string, {deviceIdKey: string, sessions: object[]}>}
|
||||
*/
|
||||
Crypto.prototype.getOlmSessionsForUser = function(userId) {
|
||||
var devices = this.getStoredDevicesForUser(userId);
|
||||
var devices = this.getStoredDevicesForUser(userId) || [];
|
||||
var result = {};
|
||||
for (var j = 0; j < devices.length; ++j) {
|
||||
var device = devices[j];
|
||||
@@ -694,13 +823,15 @@ 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;
|
||||
this._roomEncryptors[roomId] = alg;
|
||||
};
|
||||
|
||||
|
||||
@@ -712,7 +843,8 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Try to make sure we have established olm sessions for the given users.
|
||||
* Try to make sure we have established olm sessions for all known devices for
|
||||
* the given users.
|
||||
*
|
||||
* @param {string[]} users list of user ids
|
||||
*
|
||||
@@ -721,19 +853,15 @@ Crypto.prototype.setRoomEncryption = function(roomId, config) {
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
||||
var devicesWithoutSession = [
|
||||
// [userId, deviceId, deviceInfo], ...
|
||||
];
|
||||
var result = {};
|
||||
var devicesByUser = {};
|
||||
|
||||
for (var i = 0; i < users.length; ++i) {
|
||||
var userId = users[i];
|
||||
result[userId] = {};
|
||||
devicesByUser[userId] = [];
|
||||
|
||||
var devices = this.getStoredDevicesForUser(userId);
|
||||
var devices = this.getStoredDevicesForUser(userId) || [];
|
||||
for (var j = 0; j < devices.length; ++j) {
|
||||
var deviceInfo = devices[j];
|
||||
var deviceId = deviceInfo.deviceId;
|
||||
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
if (key == this._olmDevice.deviceCurve25519Key) {
|
||||
@@ -745,60 +873,13 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var sessionId = this._olmDevice.getSessionIdForDevice(key);
|
||||
if (sessionId === null) {
|
||||
devicesWithoutSession.push([userId, deviceId, deviceInfo]);
|
||||
}
|
||||
result[userId][deviceId] = {
|
||||
device: deviceInfo,
|
||||
sessionId: sessionId,
|
||||
};
|
||||
devicesByUser[userId].push(deviceInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesWithoutSession.length === 0) {
|
||||
return q(result);
|
||||
}
|
||||
|
||||
// TODO: this has a race condition - if we try to send another message
|
||||
// while we are claiming a key, we will end up claiming two and setting up
|
||||
// two sessions.
|
||||
//
|
||||
// That should eventually resolve itself, but it's poor form.
|
||||
|
||||
var self = this;
|
||||
return this._baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession
|
||||
).then(function(res) {
|
||||
for (var i = 0; i < devicesWithoutSession.length; ++i) {
|
||||
var device = devicesWithoutSession[i];
|
||||
var userId = device[0];
|
||||
var deviceId = device[1];
|
||||
var deviceInfo = device[2];
|
||||
|
||||
var userRes = res.one_time_keys[userId] || {};
|
||||
var deviceRes = userRes[deviceId];
|
||||
var oneTimeKey = null;
|
||||
for (var keyId in deviceRes) {
|
||||
if (keyId.indexOf("curve25519:") === 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);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
return olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -807,10 +888,9 @@ Crypto.prototype.ensureOlmSessionsForUsers = function(users) {
|
||||
* @return {bool} whether encryption is enabled.
|
||||
*/
|
||||
Crypto.prototype.isRoomEncrypted = function(roomId) {
|
||||
return Boolean(this._roomAlgorithms[roomId]);
|
||||
return Boolean(this._roomEncryptors[roomId]);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an event according to the configuration of the room, if necessary.
|
||||
*
|
||||
@@ -830,18 +910,13 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.getType() !== "m.room.message") {
|
||||
// we only encrypt m.room.message
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!room) {
|
||||
throw new Error("Cannot send encrypted messages in unknown rooms");
|
||||
}
|
||||
|
||||
var roomId = event.getRoomId();
|
||||
|
||||
var alg = this._roomAlgorithms[roomId];
|
||||
var alg = this._roomEncryptors[roomId];
|
||||
if (!alg) {
|
||||
// not encrypting messages in this room
|
||||
|
||||
@@ -871,117 +946,17 @@ Crypto.prototype.encryptEventIfNeeded = function(event, room) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} module:crypto.DecryptionResult
|
||||
*
|
||||
* @property {Object} payload decrypted payload (with properties 'type',
|
||||
* 'content').
|
||||
*
|
||||
* @property {Object<string, string>} keysClaimed keys that the sender of the
|
||||
* event claims ownership of: map from key type to base64-encoded key
|
||||
*
|
||||
* @property {Object<string, string>} keysProved keys that the sender of the
|
||||
* event is known to have ownership of: map from key type to base64-encoded
|
||||
* key
|
||||
*/
|
||||
|
||||
/**
|
||||
* Decrypt a received event
|
||||
*
|
||||
* @param {object} event raw event
|
||||
*
|
||||
* @return {module:crypto.DecryptionResult} decryption result
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* @raises {algorithms.DecryptionError} if there is a problem decrypting the event
|
||||
*/
|
||||
Crypto.prototype.decryptEvent = function(event) {
|
||||
var content = event.content;
|
||||
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
|
||||
if (!AlgClass) {
|
||||
throw new algorithms.DecryptionError("Unable to decrypt " + content.algorithm);
|
||||
}
|
||||
var alg = new AlgClass({
|
||||
olmDevice: this._olmDevice,
|
||||
});
|
||||
var r = alg.decryptEvent(event);
|
||||
|
||||
if (r !== null) {
|
||||
return r;
|
||||
} else {
|
||||
// We've got a message for a session we don't have. Maybe the sender
|
||||
// forgot to tell us about the session. Remind the sender that we
|
||||
// exist so that they might tell us about the session on their next
|
||||
// send.
|
||||
//
|
||||
// (Alternatively, it might be that we are just looking at
|
||||
// scrollback... at least we rate-limit the m.new_device events :/)
|
||||
//
|
||||
// XXX: this is a band-aid which masks symptoms of other bugs. It would
|
||||
// be nice to get rid of it.
|
||||
if (event.room_id !== undefined && event.sender !== undefined) {
|
||||
var device_id = event.content.device_id;
|
||||
if (device_id === undefined) {
|
||||
// if the sending device didn't tell us its device_id, fall
|
||||
// back to all devices.
|
||||
device_id = null;
|
||||
}
|
||||
this._sendPingToDevice(event.sender, device_id, event.room_id);
|
||||
}
|
||||
|
||||
throw new algorithms.DecryptionError("Unknown inbound session id");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a "m.new_device" message to remind it that we exist and are a member
|
||||
* of a room.
|
||||
*
|
||||
* This is rate limited to send a message at most once an hour per desination.
|
||||
*
|
||||
* @param {string} userId The ID of the user to ping.
|
||||
* @param {string?} deviceId The ID of the device to ping. If null, all
|
||||
* devices.
|
||||
* @param {string} roomId The ID of the room we want to remind them about.
|
||||
*/
|
||||
Crypto.prototype._sendPingToDevice = function(userId, deviceId, roomId) {
|
||||
if (deviceId === null) {
|
||||
deviceId = "*";
|
||||
}
|
||||
|
||||
var lastMessageTsMap = this._lastNewDeviceMessageTsByUserDeviceRoom;
|
||||
|
||||
var lastTsByDevice = lastMessageTsMap[userId];
|
||||
if (!lastTsByDevice) {
|
||||
lastTsByDevice = lastMessageTsMap[userId] = {};
|
||||
}
|
||||
|
||||
var lastTsByRoom = lastTsByDevice[deviceId];
|
||||
if (!lastTsByRoom) {
|
||||
lastTsByRoom = lastTsByDevice[deviceId] = {};
|
||||
}
|
||||
|
||||
var lastTs = lastTsByRoom[roomId];
|
||||
var timeNowMs = Date.now();
|
||||
var oneHourMs = 1000 * 60 * 60;
|
||||
|
||||
if (lastTs !== undefined && lastTs + oneHourMs > timeNowMs) {
|
||||
// rate-limiting
|
||||
return;
|
||||
}
|
||||
|
||||
var content = {};
|
||||
content[userId] = {};
|
||||
content[userId][deviceId] = {
|
||||
device_id: this._deviceId,
|
||||
rooms: [roomId],
|
||||
};
|
||||
|
||||
this._baseApis.sendToDevice(
|
||||
"m.new_device", // OH HAI!
|
||||
content
|
||||
).done();
|
||||
|
||||
lastTsByRoom[roomId] = timeNowMs;
|
||||
var content = event.getWireContent();
|
||||
var alg = this._getRoomDecryptor(event.getRoomId(), content.algorithm);
|
||||
alg.decryptEvent(event);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1011,6 +986,11 @@ Crypto.prototype._onCryptoEvent = function(event) {
|
||||
* @param {module:models/room[]} rooms list of rooms the client knows about
|
||||
*/
|
||||
Crypto.prototype._onInitialSyncCompleted = function(rooms) {
|
||||
this._initialSyncCompleted = true;
|
||||
|
||||
// catch up on any m.new_device events which arrived during the initial sync.
|
||||
this._flushNewDeviceRequests();
|
||||
|
||||
if (this._sessionStore.getDeviceAnnounced()) {
|
||||
return;
|
||||
}
|
||||
@@ -1023,7 +1003,7 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
|
||||
var room = rooms[i];
|
||||
|
||||
// check for rooms with encryption enabled
|
||||
var alg = this._roomAlgorithms[room.roomId];
|
||||
var alg = this._roomEncryptors[room.roomId];
|
||||
if (!alg) {
|
||||
continue;
|
||||
}
|
||||
@@ -1077,15 +1057,13 @@ Crypto.prototype._onInitialSyncCompleted = function(rooms) {
|
||||
*/
|
||||
Crypto.prototype._onRoomKeyEvent = function(event) {
|
||||
var content = event.getContent();
|
||||
var AlgClass = algorithms.DECRYPTION_CLASSES[content.algorithm];
|
||||
if (!AlgClass) {
|
||||
throw new algorithms.DecryptionError(
|
||||
"Unable to handle keys for " + content.algorithm
|
||||
);
|
||||
|
||||
if (!content.room_id || !content.algorithm) {
|
||||
console.error("key event is missing fields");
|
||||
return;
|
||||
}
|
||||
var alg = new AlgClass({
|
||||
olmDevice: this._olmDevice,
|
||||
});
|
||||
|
||||
var alg = this._getRoomDecryptor(content.room_id, content.algorithm);
|
||||
alg.onRoomKeyEvent(event);
|
||||
};
|
||||
|
||||
@@ -1109,7 +1087,7 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
|
||||
|
||||
var roomId = member.roomId;
|
||||
|
||||
var alg = this._roomAlgorithms[roomId];
|
||||
var alg = this._roomEncryptors[roomId];
|
||||
if (!alg) {
|
||||
// not encrypting in this room
|
||||
return;
|
||||
@@ -1139,26 +1117,121 @@ Crypto.prototype._onNewDeviceEvent = function(event) {
|
||||
console.log("m.new_device event from " + userId + ":" + deviceId +
|
||||
" for rooms " + rooms);
|
||||
|
||||
if (this.getStoredDevice(userId, deviceId)) {
|
||||
console.log("Known device; ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendingUsersWithNewDevices[userId] = true;
|
||||
|
||||
// we delay handling these until the intialsync has completed, so that we
|
||||
// can do all of them together.
|
||||
if (this._initialSyncCompleted) {
|
||||
this._flushNewDeviceRequests();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Start device queries for any users who sent us an m.new_device recently
|
||||
*/
|
||||
Crypto.prototype._flushNewDeviceRequests = function() {
|
||||
var self = this;
|
||||
this.downloadKeys(
|
||||
[userId], true
|
||||
).then(function() {
|
||||
for (var i = 0; i < rooms.length; i++) {
|
||||
var roomId = rooms[i];
|
||||
var alg = self._roomAlgorithms[roomId];
|
||||
if (!alg) {
|
||||
// not encrypting in this room
|
||||
continue;
|
||||
}
|
||||
alg.onNewDevice(userId, deviceId);
|
||||
|
||||
var users = utils.keys(this._pendingUsersWithNewDevices);
|
||||
|
||||
if (users.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var r = this._doKeyDownloadForUsers(users);
|
||||
|
||||
// we've kicked off requests to these users: remove their
|
||||
// pending flag for now.
|
||||
this._pendingUsersWithNewDevices = {};
|
||||
|
||||
users.map(function(u) {
|
||||
r[u] = r[u].catch(function(e) {
|
||||
console.error(
|
||||
'Error updating device keys for user ' + u + ':', e
|
||||
);
|
||||
|
||||
// reinstate the pending flags on any users which failed; this will
|
||||
// mean that we will do another download in the future, but won't
|
||||
// tight-loop.
|
||||
//
|
||||
self._pendingUsersWithNewDevices[u] = true;
|
||||
});
|
||||
});
|
||||
|
||||
q.all(utils.values(r)).done();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a decryptor for a given room and algorithm.
|
||||
*
|
||||
* If we already have a decryptor for the given room and algorithm, return
|
||||
* it. Otherwise try to instantiate it.
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @param {string?} roomId room id for decryptor. If undefined, a temporary
|
||||
* decryptor is instantiated.
|
||||
*
|
||||
* @param {string} algorithm crypto algorithm
|
||||
*
|
||||
* @return {module:crypto.algorithms.base.DecryptionAlgorithm}
|
||||
*
|
||||
* @raises {module:crypto.algorithms.DecryptionError} if the algorithm is
|
||||
* unknown
|
||||
*/
|
||||
Crypto.prototype._getRoomDecryptor = function(roomId, algorithm) {
|
||||
var decryptors;
|
||||
var alg;
|
||||
|
||||
roomId = roomId || null;
|
||||
if (roomId) {
|
||||
decryptors = this._roomDecryptors[roomId];
|
||||
if (!decryptors) {
|
||||
this._roomDecryptors[roomId] = decryptors = {};
|
||||
}
|
||||
}).catch(function(e) {
|
||||
console.error(
|
||||
"Error updating device keys for new device " + userId + ":" +
|
||||
deviceId,
|
||||
e
|
||||
|
||||
alg = decryptors[algorithm];
|
||||
if (alg) {
|
||||
return alg;
|
||||
}
|
||||
}
|
||||
|
||||
var AlgClass = algorithms.DECRYPTION_CLASSES[algorithm];
|
||||
if (!AlgClass) {
|
||||
throw new algorithms.DecryptionError(
|
||||
'Unknown encryption algorithm "' + algorithm + '".'
|
||||
);
|
||||
}).done();
|
||||
}
|
||||
alg = new AlgClass({
|
||||
userId: this._userId,
|
||||
crypto: this,
|
||||
olmDevice: this._olmDevice,
|
||||
roomId: roomId,
|
||||
});
|
||||
|
||||
if (decryptors) {
|
||||
decryptors[algorithm] = alg;
|
||||
}
|
||||
return alg;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+211
-32
@@ -20,6 +20,9 @@ limitations under the License.
|
||||
* Utilities common to olm encryption algorithms
|
||||
*/
|
||||
|
||||
var q = require('q');
|
||||
var anotherjson = require('another-json');
|
||||
|
||||
var utils = require("../utils");
|
||||
|
||||
/**
|
||||
@@ -34,22 +37,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 +82,188 @@ 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)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Try to make sure we have established olm sessions for the given devices.
|
||||
*
|
||||
* @param {module:crypto/OlmDevice} olmDevice
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis
|
||||
*
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices
|
||||
*
|
||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
module.exports.ensureOlmSessionsForDevices = function(
|
||||
olmDevice, baseApis, devicesByUser
|
||||
) {
|
||||
var devicesWithoutSession = [
|
||||
// [userId, deviceId], ...
|
||||
];
|
||||
var result = {};
|
||||
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
|
||||
result[userId] = {};
|
||||
var devices = devicesByUser[userId];
|
||||
for (var j = 0; j < devices.length; j++) {
|
||||
var deviceInfo = devices[j];
|
||||
var deviceId = deviceInfo.deviceId;
|
||||
var key = deviceInfo.getIdentityKey();
|
||||
var sessionId = olmDevice.getSessionIdForDevice(key);
|
||||
if (sessionId === null) {
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
result[userId][deviceId] = {
|
||||
device: deviceInfo,
|
||||
sessionId: sessionId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesWithoutSession.length === 0) {
|
||||
return q(result);
|
||||
}
|
||||
|
||||
// TODO: this has a race condition - if we try to send another message
|
||||
// while we are claiming a key, we will end up claiming two and setting up
|
||||
// two sessions.
|
||||
//
|
||||
// That should eventually resolve itself, but it's poor form.
|
||||
|
||||
var oneTimeKeyAlgorithm = "signed_curve25519";
|
||||
return baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm
|
||||
).then(function(res) {
|
||||
var otk_res = res.one_time_keys || {};
|
||||
for (var userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
|
||||
var userRes = otk_res[userId] || {};
|
||||
var devices = devicesByUser[userId];
|
||||
for (var j = 0; j < devices.length; j++) {
|
||||
var deviceInfo = devices[j];
|
||||
var deviceId = deviceInfo.deviceId;
|
||||
if (result[userId][deviceId].sessionId) {
|
||||
// we already have a result for this device
|
||||
continue;
|
||||
}
|
||||
|
||||
var deviceRes = userRes[deviceId] || {};
|
||||
var oneTimeKey = null;
|
||||
for (var keyId in deviceRes) {
|
||||
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
|
||||
oneTimeKey = deviceRes[keyId];
|
||||
}
|
||||
}
|
||||
|
||||
if (!oneTimeKey) {
|
||||
console.warn(
|
||||
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
|
||||
") for device " + userId + ":" + deviceId
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var sid = _verifyKeyAndStartSession(
|
||||
olmDevice, oneTimeKey, userId, deviceInfo
|
||||
);
|
||||
result[userId][deviceId].sessionId = sid;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
|
||||
var deviceId = deviceInfo.deviceId;
|
||||
try {
|
||||
_verifySignature(
|
||||
olmDevice, oneTimeKey, userId, deviceId,
|
||||
deviceInfo.getFingerprint()
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Unable to verify signature on one-time key for device " +
|
||||
userId + ":" + deviceId + ":", e
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
var sid;
|
||||
try {
|
||||
sid = olmDevice.createOutboundSession(
|
||||
deviceInfo.getIdentityKey(), oneTimeKey.key
|
||||
);
|
||||
} catch (e) {
|
||||
// possibly a bad key
|
||||
console.error("Error starting session with device " +
|
||||
userId + ":" + deviceId + ": " + e);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("Started new sessionid " + sid +
|
||||
" for device " + userId + ":" + deviceId);
|
||||
return sid;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Verify the signature on an object
|
||||
*
|
||||
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
|
||||
*
|
||||
* @param {Object} obj object to check signature on. Note that this will be
|
||||
* stripped of its 'signatures' and 'unsigned' properties.
|
||||
*
|
||||
* @param {string} signingUserId ID of the user whose signature should be checked
|
||||
*
|
||||
* @param {string} signingDeviceId ID of the device whose signature should be checked
|
||||
*
|
||||
* @param {string} signingKey base64-ed ed25519 public key
|
||||
*/
|
||||
var _verifySignature = module.exports.verifySignature = function(
|
||||
olmDevice, obj, signingUserId, signingDeviceId, signingKey
|
||||
) {
|
||||
var signKeyId = "ed25519:" + signingDeviceId;
|
||||
var signatures = obj.signatures || {};
|
||||
var userSigs = signatures[signingUserId] || {};
|
||||
var signature = userSigs[signKeyId];
|
||||
if (!signature) {
|
||||
throw Error("No signature");
|
||||
}
|
||||
|
||||
// prepare the canonical json: remove unsigned and signatures, and stringify with
|
||||
// anotherjson
|
||||
delete obj.unsigned;
|
||||
delete obj.signatures;
|
||||
var json = anotherjson.stringify(obj);
|
||||
|
||||
olmDevice.verifySignature(
|
||||
signingKey, json, signature
|
||||
);
|
||||
};
|
||||
|
||||
+194
-51
@@ -63,13 +63,17 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
|
||||
* requests. This function must look like function(opts, callback){ ... }.
|
||||
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
|
||||
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
|
||||
* @param {bool} opts.onlyData True to return only the 'data' component of the
|
||||
* response (e.g. the parsed HTTP body). If false, requests will return status
|
||||
* codes and headers in addition to data. Default: false.
|
||||
*
|
||||
* @param {bool=} opts.onlyData True to return only the 'data' component of the
|
||||
* response (e.g. the parsed HTTP body). If false, requests will return an
|
||||
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
|
||||
*
|
||||
* @param {string} opts.accessToken The access_token to send with requests. Can be
|
||||
* null to not send an access token.
|
||||
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
|
||||
* @param {Object=} opts.extraParams Optional. Extra query parameters to send on
|
||||
* requests.
|
||||
* @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait
|
||||
* before timing out the request. If not specified, there is no timeout.
|
||||
*/
|
||||
module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
|
||||
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
|
||||
@@ -99,20 +103,87 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
|
||||
/**
|
||||
* Upload content to the Home Server
|
||||
* @param {File} file A File object (in a browser) or in Node,
|
||||
an object with properties:
|
||||
name: The file's name
|
||||
stream: A read stream
|
||||
* @param {Function} callback Optional. The callback to invoke on
|
||||
* success/failure. See the promise return values for more information.
|
||||
* @return {module:client.Promise} Resolves to <code>{data: {Object},
|
||||
*
|
||||
* @param {object} file The object to upload. On a browser, something that
|
||||
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
|
||||
* a Buffer, String or ReadStream.
|
||||
*
|
||||
* @param {object} opts options object
|
||||
*
|
||||
* @param {string=} opts.name Name to give the file on the server. Defaults
|
||||
* to <tt>file.name</tt>.
|
||||
*
|
||||
* @param {string=} opts.type Content-type for the upload. Defaults to
|
||||
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
|
||||
*
|
||||
* @param {boolean=} opts.rawResponse Return the raw body, rather than
|
||||
* parsing the JSON. Defaults to false (except on node.js, where it
|
||||
* defaults to true for backwards compatibility).
|
||||
*
|
||||
* @param {boolean=} opts.onlyContentUri Just return the content URI,
|
||||
* rather than the whole body. Defaults to false (except on browsers,
|
||||
* where it defaults to true for backwards compatibility). Ignored if
|
||||
* opts.rawResponse is true.
|
||||
*
|
||||
* @param {Function=} opts.callback Deprecated. Optional. The callback to
|
||||
* invoke on success/failure. See the promise return values for more
|
||||
* information.
|
||||
*
|
||||
* @return {module:client.Promise} Resolves to response object, as
|
||||
* determined by this.opts.onlyData, opts.rawResponse, and
|
||||
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
|
||||
*/
|
||||
uploadContent: function(file, callback) {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
"Expected callback to be a function but got " + typeof callback
|
||||
);
|
||||
uploadContent: function(file, opts) {
|
||||
if (utils.isFunction(opts)) {
|
||||
// opts used to be callback
|
||||
opts = {
|
||||
callback: opts,
|
||||
};
|
||||
} else if (opts === undefined) {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
// if the file doesn't have a mime type, use a default since
|
||||
// the HS errors if we don't supply one.
|
||||
var contentType = opts.type || file.type || 'application/octet-stream';
|
||||
var fileName = opts.name || file.name;
|
||||
|
||||
// we used to recommend setting file.stream to the thing to upload on
|
||||
// nodejs.
|
||||
var body = file.stream ? file.stream : file;
|
||||
|
||||
// backwards-compatibility hacks where we used to do different things
|
||||
// between browser and node.
|
||||
var rawResponse = opts.rawResponse;
|
||||
if (rawResponse === undefined) {
|
||||
if (global.XMLHttpRequest) {
|
||||
rawResponse = false;
|
||||
} else {
|
||||
console.warn(
|
||||
"Returning the raw JSON from uploadContent(). Future " +
|
||||
"versions of the js-sdk will change this default, to " +
|
||||
"return the parsed object. Set opts.rawResponse=false " +
|
||||
"to change this behaviour now."
|
||||
);
|
||||
rawResponse = true;
|
||||
}
|
||||
}
|
||||
|
||||
var onlyContentUri = opts.onlyContentUri;
|
||||
if (!rawResponse && onlyContentUri === undefined) {
|
||||
if (global.XMLHttpRequest) {
|
||||
console.warn(
|
||||
"Returning only the content-uri from uploadContent(). " +
|
||||
"Future versions of the js-sdk will change this " +
|
||||
"default, to return the whole response object. Set " +
|
||||
"opts.onlyContentUri=false to change this behaviour now."
|
||||
);
|
||||
onlyContentUri = true;
|
||||
} else {
|
||||
onlyContentUri = false;
|
||||
}
|
||||
}
|
||||
|
||||
// browser-request doesn't support File objects because it deep-copies
|
||||
// the options using JSON.parse(JSON.stringify(options)). Instead of
|
||||
// loading the whole file into memory as a string and letting
|
||||
@@ -123,40 +194,59 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
|
||||
var upload = { loaded: 0, total: 0 };
|
||||
var promise;
|
||||
|
||||
// XMLHttpRequest doesn't parse JSON for us. request normally does, but
|
||||
// we're setting opts.json=false so that it doesn't JSON-encode the
|
||||
// request, which also means it doesn't JSON-decode the response. Either
|
||||
// way, we have to JSON-parse the response ourselves.
|
||||
var bodyParser = null;
|
||||
if (!rawResponse) {
|
||||
bodyParser = function(rawBody) {
|
||||
var body = JSON.parse(rawBody);
|
||||
if (onlyContentUri) {
|
||||
body = body.content_uri;
|
||||
if (body === undefined) {
|
||||
throw Error('Bad response');
|
||||
}
|
||||
}
|
||||
return body;
|
||||
};
|
||||
}
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
var defer = q.defer();
|
||||
var xhr = new global.XMLHttpRequest();
|
||||
upload.xhr = xhr;
|
||||
var cb = requestCallback(defer, callback, this.opts.onlyData);
|
||||
var cb = requestCallback(defer, opts.callback, this.opts.onlyData);
|
||||
|
||||
var timeout_fn = function() {
|
||||
xhr.abort();
|
||||
cb(new Error('Timeout'));
|
||||
};
|
||||
|
||||
// set an initial timeout of 30s; we'll advance it each time we get
|
||||
// a progress notification
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
var err;
|
||||
if (!xhr.responseText) {
|
||||
err = new Error('No response body.');
|
||||
var resp;
|
||||
try {
|
||||
if (!xhr.responseText) {
|
||||
throw new Error('No response body.');
|
||||
}
|
||||
resp = xhr.responseText;
|
||||
if (bodyParser) {
|
||||
resp = bodyParser(resp);
|
||||
}
|
||||
} catch (err) {
|
||||
err.http_status = xhr.status;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
if (resp.content_uri === undefined) {
|
||||
err = Error('Bad response');
|
||||
err.http_status = xhr.status;
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
cb(undefined, xhr, resp.content_uri);
|
||||
cb(undefined, xhr, resp);
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -169,30 +259,26 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
});
|
||||
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
|
||||
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
|
||||
url += "&filename=" + encodeURIComponent(file.name);
|
||||
url += "&filename=" + encodeURIComponent(fileName);
|
||||
|
||||
xhr.open("POST", url);
|
||||
if (file.type) {
|
||||
xhr.setRequestHeader("Content-Type", file.type);
|
||||
} else {
|
||||
// if the file doesn't have a mime type, use a default since
|
||||
// the HS errors if we don't supply one.
|
||||
xhr.setRequestHeader("Content-Type", 'application/octet-stream');
|
||||
}
|
||||
xhr.send(file);
|
||||
xhr.setRequestHeader("Content-Type", contentType);
|
||||
xhr.send(body);
|
||||
promise = defer.promise;
|
||||
|
||||
// dirty hack (as per _request) to allow the upload to be cancelled.
|
||||
promise.abort = xhr.abort.bind(xhr);
|
||||
} else {
|
||||
var queryParams = {
|
||||
filename: file.name,
|
||||
filename: fileName,
|
||||
};
|
||||
|
||||
promise = this.authedRequest(
|
||||
callback, "POST", "/upload", queryParams, file.stream, {
|
||||
opts.callback, "POST", "/upload", queryParams, body, {
|
||||
prefix: "/_matrix/media/v1",
|
||||
localTimeoutMs: 30000,
|
||||
headers: {"Content-Type": file.type},
|
||||
headers: {"Content-Type": contentType},
|
||||
json: false,
|
||||
bodyParser: bodyParser,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -351,7 +437,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
*/
|
||||
request: function(callback, method, path, queryParams, data, opts) {
|
||||
opts = opts || {};
|
||||
var prefix = opts.prefix || this.opts.prefix;
|
||||
var prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
|
||||
var fullUri = this.opts.baseUrl + prefix + path;
|
||||
|
||||
return this.requestOtherUrl(
|
||||
@@ -493,6 +579,32 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
return this.opts.baseUrl + prefix + path + queryString;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {function} callback
|
||||
* @param {string} method
|
||||
* @param {string} uri
|
||||
* @param {object} queryParams
|
||||
* @param {object|string} data
|
||||
* @param {object=} opts
|
||||
*
|
||||
* @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.)
|
||||
*
|
||||
* @param {object=} opts.headers extra request headers
|
||||
*
|
||||
* @param {number=} opts.localTimeoutMs client-side timeout for the
|
||||
* request. Default timeout if falsy.
|
||||
*
|
||||
* @param {function=} opts.bodyParser function to parse the body of the
|
||||
* response before passing it to the promise and callback.
|
||||
*
|
||||
* @return {module:client.Promise} a promise which resolves to either the
|
||||
* response object (if this.opts.onlyData is truthy), or the parsed
|
||||
* body. Rejects
|
||||
*/
|
||||
_request: function(callback, method, uri, queryParams, data, opts) {
|
||||
if (callback !== undefined && !utils.isFunction(callback)) {
|
||||
throw Error(
|
||||
@@ -508,12 +620,15 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
queryParams[key] = this.opts.extraParams[key];
|
||||
}
|
||||
}
|
||||
|
||||
var json = opts.json === undefined ? true : opts.json;
|
||||
|
||||
var defer = q.defer();
|
||||
|
||||
var timeoutId;
|
||||
var timedOut = false;
|
||||
var req;
|
||||
var localTimeoutMs = opts.localTimeoutMs;
|
||||
var localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
|
||||
if (localTimeoutMs) {
|
||||
timeoutId = callbacks.setTimeout(function() {
|
||||
timedOut = true;
|
||||
@@ -538,7 +653,7 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
withCredentials: false,
|
||||
qs: queryParams,
|
||||
body: data,
|
||||
json: true,
|
||||
json: json,
|
||||
timeout: localTimeoutMs,
|
||||
headers: opts.headers || {},
|
||||
_matrix_opts: this.opts
|
||||
@@ -550,7 +665,15 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
return; // already rejected promise
|
||||
}
|
||||
}
|
||||
var handlerFn = requestCallback(defer, callback, self.opts.onlyData);
|
||||
|
||||
// if json is falsy, we won't parse any error response, so need
|
||||
// to do so before turning it into a MatrixError
|
||||
var parseErrorJson = !json;
|
||||
var handlerFn = requestCallback(
|
||||
defer, callback, self.opts.onlyData,
|
||||
parseErrorJson,
|
||||
opts.bodyParser
|
||||
);
|
||||
handlerFn(err, response, body);
|
||||
}
|
||||
);
|
||||
@@ -577,14 +700,34 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
*
|
||||
* If onlyData is true, the defer/callback is invoked with the body of the
|
||||
* response, otherwise the result code.
|
||||
*
|
||||
* If parseErrorJson is true, we will JSON.parse the body if we get a 4xx error.
|
||||
*
|
||||
*/
|
||||
var requestCallback = function(defer, userDefinedCallback, onlyData) {
|
||||
var requestCallback = function(
|
||||
defer, userDefinedCallback, onlyData,
|
||||
parseErrorJson, bodyParser
|
||||
) {
|
||||
userDefinedCallback = userDefinedCallback || function() {};
|
||||
|
||||
return function(err, response, body) {
|
||||
if (!err && response.statusCode >= 400) {
|
||||
err = new module.exports.MatrixError(body);
|
||||
err.httpStatus = response.statusCode;
|
||||
if (!err) {
|
||||
try {
|
||||
if (response.statusCode >= 400) {
|
||||
if (parseErrorJson) {
|
||||
// we won't have json-decoded the response.
|
||||
body = JSON.parse(body);
|
||||
}
|
||||
err = new module.exports.MatrixError(body);
|
||||
} else if (bodyParser) {
|
||||
body = bodyParser(body);
|
||||
}
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
if (err) {
|
||||
err.httpStatus = response.statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
|
||||
@@ -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.
|
||||
@@ -79,6 +82,27 @@ module.exports.request = function(r) {
|
||||
request = r;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the currently-set request function.
|
||||
* @return {requestFunction} The current request function.
|
||||
*/
|
||||
module.exports.getRequest = function() {
|
||||
return request;
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply wrapping code around the request function. The wrapper function is
|
||||
* installed as the new request handler, and when invoked it is passed the
|
||||
* previous value, along with the options and callback arguments.
|
||||
* @param {requestWrapperFunction} wrapper The wrapping function.
|
||||
*/
|
||||
module.exports.wrapRequest = function(wrapper) {
|
||||
var origRequest = request;
|
||||
request = function(options, callback) {
|
||||
return wrapper(origRequest, options, callback);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
|
||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||
@@ -126,6 +150,16 @@ module.exports.createClient = function(opts) {
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A wrapper for the request function interface.
|
||||
* @callback requestWrapperFunction
|
||||
* @param {requestFunction} origRequest The underlying request function being
|
||||
* wrapped
|
||||
* @param {Object} opts The options for this HTTP request, given in the same
|
||||
* form as {@link requestFunction}.
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The request callback interface for performing HTTP requests. This matches the
|
||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||
|
||||
+71
-17
@@ -21,6 +21,10 @@ limitations under the License.
|
||||
* @module models/event
|
||||
*/
|
||||
|
||||
var EventEmitter = require("events").EventEmitter;
|
||||
|
||||
var utils = require('../utils.js');
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
* @readonly
|
||||
@@ -51,15 +55,6 @@ module.exports.EventStatus = {
|
||||
*
|
||||
* @param {Object} event The raw event to be wrapped in this DAO
|
||||
*
|
||||
* @param {Object=} clearEvent For encrypted events, the plaintext payload for
|
||||
* the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*
|
||||
* @param {Object=} keysProved Keys owned by the sender of this event.
|
||||
* See {@link module:models/event.MatrixEvent#getKeysProved}.
|
||||
*
|
||||
* @param {Object=} keysClaimed Keys the sender of this event claims.
|
||||
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
|
||||
*
|
||||
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
|
||||
* this property</b> directly unless you absolutely have to. Prefer the getter
|
||||
* methods defined on this class. Using the getter methods shields your app
|
||||
@@ -75,22 +70,25 @@ module.exports.EventStatus = {
|
||||
* Default: true. <strong>This property is experimental and may change.</strong>
|
||||
*/
|
||||
module.exports.MatrixEvent = function MatrixEvent(
|
||||
event, clearEvent, keysProved, keysClaimed
|
||||
event
|
||||
) {
|
||||
this.event = event || {};
|
||||
this.sender = null;
|
||||
this.target = null;
|
||||
this.status = null;
|
||||
this.forwardLooking = true;
|
||||
|
||||
this._clearEvent = clearEvent || {};
|
||||
this._pushActions = null;
|
||||
this._date = this.event.origin_server_ts ?
|
||||
new Date(this.event.origin_server_ts) : null;
|
||||
|
||||
this._keysProved = keysProved || {};
|
||||
this._keysClaimed = keysClaimed || {};
|
||||
this._clearEvent = {};
|
||||
this._keysProved = {};
|
||||
this._keysClaimed = {};
|
||||
};
|
||||
utils.inherits(module.exports.MatrixEvent, EventEmitter);
|
||||
|
||||
module.exports.MatrixEvent.prototype = {
|
||||
|
||||
utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
|
||||
/**
|
||||
* Get the event_id for this event.
|
||||
@@ -146,6 +144,14 @@ module.exports.MatrixEvent.prototype = {
|
||||
return this.event.origin_server_ts;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the timestamp of this event, as a Date object.
|
||||
* @return {Date} The event date, e.g. <code>new Date(1433502692297)</code>
|
||||
*/
|
||||
getDate: function() {
|
||||
return this._date;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the (decrypted, if necessary) event content JSON.
|
||||
*
|
||||
@@ -239,12 +245,37 @@ module.exports.MatrixEvent.prototype = {
|
||||
this._keysClaimed = keys;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the cleartext data on this event.
|
||||
*
|
||||
* (This is used after decrypting an event; it should not be used by applications).
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @fires module:models/event.MatrixEvent#"Event.decrypted"
|
||||
*
|
||||
* @param {Object} clearEvent The plaintext payload for the event
|
||||
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*
|
||||
* @param {Object=} keysProved Keys owned by the sender of this event.
|
||||
* See {@link module:models/event.MatrixEvent#getKeysProved}.
|
||||
*
|
||||
* @param {Object=} keysClaimed Keys the sender of this event claims.
|
||||
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
|
||||
*/
|
||||
setClearData: function(clearEvent, keysProved, keysClaimed) {
|
||||
this._clearEvent = clearEvent;
|
||||
this._keysProved = keysProved || {};
|
||||
this._keysClaimed = keysClaimed || {};
|
||||
this.emit("Event.decrypted", this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the event is encrypted.
|
||||
* @return {boolean} True if this event is encrypted.
|
||||
*/
|
||||
isEncrypted: function() {
|
||||
return Boolean(this._clearEvent.type);
|
||||
return this.event.type === "m.room.encrypted";
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -353,7 +384,18 @@ module.exports.MatrixEvent.prototype = {
|
||||
setPushActions: function(pushActions) {
|
||||
this._pushActions = pushActions;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace the `event` property and recalculate any properties based on it.
|
||||
* @param {Object} event the object to assign to the `event` property
|
||||
*/
|
||||
handleRemoteEcho: function(event) {
|
||||
this.event = event;
|
||||
// successfully sent.
|
||||
this.status = null;
|
||||
this._date = new Date(this.event.origin_server_ts);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
|
||||
@@ -394,3 +436,15 @@ var _REDACT_KEEP_CONTENT_MAP = {
|
||||
},
|
||||
'm.room.aliases': {'aliases': 1},
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Fires when an event is decrypted
|
||||
*
|
||||
* @event module:models/event.MatrixEvent#"Event.decrypted"
|
||||
*
|
||||
* @param {module:models/event.MatrixEvent} event
|
||||
* The matrix event which has been decrypted
|
||||
*/
|
||||
|
||||
+1
-4
@@ -674,10 +674,7 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
|
||||
|
||||
// replace the event source (this will preserve the plaintext payload if
|
||||
// any, which is good, because we don't want to try decoding it again).
|
||||
localEvent.event = remoteEvent.event;
|
||||
|
||||
// successfully sent.
|
||||
localEvent.status = null;
|
||||
localEvent.handleRemoteEcho(remoteEvent.event);
|
||||
|
||||
for (var i = 0; i < this._timelineSets.length; i++) {
|
||||
var timelineSet = this._timelineSets[i];
|
||||
|
||||
+49
-36
@@ -60,13 +60,14 @@ function debuglog() {
|
||||
function SyncApi(client, opts) {
|
||||
this.client = client;
|
||||
opts = opts || {};
|
||||
opts.initialSyncLimit = opts.initialSyncLimit || 8;
|
||||
opts.initialSyncLimit = (
|
||||
opts.initialSyncLimit === undefined ? 8 : opts.initialSyncLimit
|
||||
);
|
||||
opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
|
||||
opts.pollTimeout = opts.pollTimeout || (30 * 1000);
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||
this.opts = opts;
|
||||
this._peekRoomId = null;
|
||||
this._syncConnectionLost = false;
|
||||
this._currentSyncRequest = null;
|
||||
this._syncState = null;
|
||||
this._running = false;
|
||||
@@ -392,8 +393,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
|
||||
@@ -491,7 +497,7 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
qps._cacheBuster = Date.now();
|
||||
}
|
||||
|
||||
if (self._syncConnectionLost) {
|
||||
if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') {
|
||||
// we think the connection is dead. If it comes back up, we won't know
|
||||
// about it till /sync returns. If the timeout= is high, this could
|
||||
// be a long time. Set it to 0 when doing retries so we don't have to wait
|
||||
@@ -507,8 +513,6 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
);
|
||||
|
||||
this._currentSyncRequest.done(function(data) {
|
||||
self._syncConnectionLost = false;
|
||||
|
||||
// set the sync token NOW *before* processing the events. We do this so
|
||||
// if something barfs on an event we can skip it rather than constantly
|
||||
// polling with the same token.
|
||||
@@ -540,33 +544,25 @@ SyncApi.prototype._sync = function(syncOptions) {
|
||||
self._connectionReturnedDefer.reject();
|
||||
self._connectionReturnedDefer = null;
|
||||
}
|
||||
self._updateSyncState("STOPPED");
|
||||
return;
|
||||
}
|
||||
console.error("/sync error %s", err);
|
||||
console.error(err);
|
||||
|
||||
if (!self._syncConnectionLost) {
|
||||
// This is the first failure, which may be spurious. To avoid unnecessary
|
||||
// connection error warnings we simply retry the /sync immediately. Only
|
||||
// if *that* one fails too do we say the connection has been lost.
|
||||
// Examples of when this may happen are:
|
||||
// - Restarting backend servers. (In an HA world backends may be
|
||||
// restarted all the time, and its easiest just to make the
|
||||
// client retry).
|
||||
// - Intermediate proxies restarting.
|
||||
// - Device network changes.
|
||||
// Should we emit a state like "MAYBE_CONNETION_LOST"?
|
||||
self._syncConnectionLost = true;
|
||||
debuglog("Starting keep-alive");
|
||||
// Note that we do *not* mark the sync connection as
|
||||
// lost yet: we only do this if a keepalive poke
|
||||
// fails, since long lived HTTP connections will
|
||||
// go away sometimes and we shouldn't treat this as
|
||||
// erroneous. We set the state to 'reconnecting'
|
||||
// instead, so that clients can onserve this state
|
||||
// if they wish.
|
||||
self._startKeepAlives().done(function() {
|
||||
self._sync(syncOptions);
|
||||
} else {
|
||||
debuglog("Starting keep-alive");
|
||||
self._syncConnectionLost = true;
|
||||
self._startKeepAlives().done(function() {
|
||||
self._sync(syncOptions);
|
||||
});
|
||||
self._currentSyncRequest = null;
|
||||
self._updateSyncState("ERROR", { error: err });
|
||||
}
|
||||
});
|
||||
self._currentSyncRequest = null;
|
||||
self._updateSyncState("RECONNECTING");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -859,17 +855,21 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
|
||||
*/
|
||||
SyncApi.prototype._startKeepAlives = function(delay) {
|
||||
if (delay === undefined) {
|
||||
delay = 5000 + Math.floor(Math.random() * 5000);
|
||||
delay = 2000 + Math.floor(Math.random() * 5000);
|
||||
}
|
||||
|
||||
if (this._keepAliveTimer !== null) {
|
||||
clearTimeout(this._keepAliveTimer);
|
||||
}
|
||||
var self = this;
|
||||
self._keepAliveTimer = setTimeout(
|
||||
self._pokeKeepAlive.bind(self),
|
||||
delay
|
||||
);
|
||||
if (delay > 0) {
|
||||
self._keepAliveTimer = setTimeout(
|
||||
self._pokeKeepAlive.bind(self),
|
||||
delay
|
||||
);
|
||||
} else {
|
||||
self._pokeKeepAlive();
|
||||
}
|
||||
if (!this._connectionReturnedDefer) {
|
||||
this._connectionReturnedDefer = q.defer();
|
||||
}
|
||||
@@ -889,9 +889,15 @@ SyncApi.prototype._pokeKeepAlive = function() {
|
||||
}
|
||||
}
|
||||
|
||||
this.client._http.requestWithPrefix(
|
||||
undefined, "GET", "/_matrix/client/versions", undefined,
|
||||
undefined, "", 15 * 1000
|
||||
this.client._http.request(
|
||||
undefined, // callback
|
||||
"GET", "/_matrix/client/versions",
|
||||
undefined, // queryParams
|
||||
undefined, // data
|
||||
{
|
||||
prefix: '',
|
||||
localTimeoutMs: 15 * 1000,
|
||||
}
|
||||
).done(function() {
|
||||
success();
|
||||
}, function(err) {
|
||||
@@ -907,6 +913,13 @@ SyncApi.prototype._pokeKeepAlive = function() {
|
||||
self._pokeKeepAlive.bind(self),
|
||||
5000 + Math.floor(Math.random() * 5000)
|
||||
);
|
||||
// A keepalive has failed, so we emit the
|
||||
// error state (whether or not this is the
|
||||
// first failure).
|
||||
// Note we do this after setting the timer:
|
||||
// this lets the unit tests advance the mock
|
||||
// clock when the get the error.
|
||||
self._updateSyncState("ERROR", { error: err });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -234,7 +234,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
// remove some events from the other end, if necessary
|
||||
var excess = this._eventCount - this._windowLimit;
|
||||
if (excess > 0) {
|
||||
this._unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
|
||||
}
|
||||
return q(true);
|
||||
}
|
||||
@@ -287,15 +287,13 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
|
||||
|
||||
|
||||
/**
|
||||
* Trim the window to the windowlimit
|
||||
* Remove `delta` events from the start or end of the timeline.
|
||||
*
|
||||
* @param {number} delta number of events to remove from the timeline
|
||||
* @param {boolean} startOfTimeline if events should be removed from the start
|
||||
* of the timeline.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
TimelineWindow.prototype._unpaginate = function(delta, startOfTimeline) {
|
||||
TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
|
||||
var tl = startOfTimeline ? this._start : this._end;
|
||||
|
||||
// sanity-check the delta
|
||||
|
||||
+5
-1
@@ -204,7 +204,8 @@ module.exports.isFunction = function(value) {
|
||||
* @return {boolean} True if it is an array.
|
||||
*/
|
||||
module.exports.isArray = function(value) {
|
||||
return Boolean(value && value.constructor === Array);
|
||||
return Array.isArray ? Array.isArray(value) :
|
||||
Boolean(value && value.constructor === Array);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -337,6 +338,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
|
||||
*
|
||||
|
||||
+27
-10
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.6.2",
|
||||
"version": "0.7.3",
|
||||
"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,14 +47,15 @@
|
||||
"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"
|
||||
"olm": "https://matrix.org/packages/npm/olm/olm-2.1.0.tgz"
|
||||
}
|
||||
}
|
||||
|
||||
+115
-8
@@ -1,7 +1,6 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a release of matrix-js-sdk. Performs the steps documented
|
||||
# in RELEASING.md
|
||||
# Script to perform a release of matrix-js-sdk.
|
||||
#
|
||||
# Requires:
|
||||
# github-changelog-generator; to install, do
|
||||
@@ -10,6 +9,9 @@
|
||||
|
||||
set -e
|
||||
|
||||
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
|
||||
hub --version > /dev/null || (echo "hub is required: please install it"; kill $$)
|
||||
|
||||
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
|
||||
|
||||
help() {
|
||||
@@ -56,11 +58,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 +88,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
|
||||
@@ -85,12 +110,94 @@ if [ -z "$skip_changelog" ]; then
|
||||
git commit "$changelog_file" -m "Prepare changelog for $tag"
|
||||
fi
|
||||
fi
|
||||
latest_changes=`mktemp`
|
||||
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
|
||||
|
||||
set -x
|
||||
|
||||
# Bump package.json, build the dist, and tag
|
||||
# Bump package.json and build the dist
|
||||
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 it can only sign with the default key, but we can
|
||||
# only turn off both of these behaviours, so we have to
|
||||
# manually commit the result.
|
||||
npm version --no-git-tag-version "$release"
|
||||
git commit package.json -m "$tag"
|
||||
|
||||
|
||||
# figure out if we should be signing this release
|
||||
signing_id=
|
||||
if [ -f release_config.yaml ]; then
|
||||
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
|
||||
fi
|
||||
|
||||
|
||||
# If there is a 'dist' script in the package.json,
|
||||
# run it in a separate checkout of the project, then
|
||||
# upload any files in the 'dist' directory as release
|
||||
# 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 checkout "$rel_branch"
|
||||
npm install
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
DIST_VERSION="$tag" npm run dist
|
||||
|
||||
popd
|
||||
|
||||
for i in "$builddir"/dist/*; do
|
||||
assets="$assets -a $i"
|
||||
if [ -n "$signing_id" ]
|
||||
then
|
||||
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
|
||||
assets="$assets -a $i.asc"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# push the release branch (github can't release from
|
||||
# a branch it doesn't have)
|
||||
git push origin "$rel_branch"
|
||||
|
||||
if [ -n "$signing_id" ]; then
|
||||
# make a signed tag
|
||||
# gnupg seems to fail to get the right tty device unless we set it here
|
||||
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
|
||||
else
|
||||
git tag -a -F "${latest_changes}" "$tag"
|
||||
fi
|
||||
|
||||
# push the tag
|
||||
git push origin "$tag"
|
||||
|
||||
hubflags=''
|
||||
if [ $prerelease -eq 1 ]; then
|
||||
hubflags='-p'
|
||||
fi
|
||||
|
||||
release_text=`mktemp`
|
||||
echo "$tag" > "${release_text}"
|
||||
echo >> "${release_text}"
|
||||
cat "${latest_changes}" >> "${release_text}"
|
||||
hub release create $hubflags $assets -f "${release_text}" "$tag"
|
||||
|
||||
if [ $dodist -eq 0 ]; then
|
||||
rm -rf "$builddir"
|
||||
fi
|
||||
rm "${release_text}"
|
||||
rm "${latest_changes}"
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
@@ -113,8 +220,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
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
Outputs the body of the first entry of changelog file on stdin
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
found_first_header = False
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if re.match(r"^Changes in \[.*\]", line):
|
||||
if found_first_header:
|
||||
break
|
||||
found_first_header = True
|
||||
elif not re.match(r"^=+$", line) and len(line) > 0:
|
||||
print line
|
||||
@@ -5,21 +5,6 @@ var HttpBackend = require("../mock-request");
|
||||
var utils = require("../../lib/utils");
|
||||
var test_utils = require("../test-utils");
|
||||
|
||||
function MockStorageApi() {
|
||||
this.data = {};
|
||||
}
|
||||
MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
}
|
||||
};
|
||||
|
||||
var aliHttpBackend;
|
||||
var bobHttpBackend;
|
||||
var aliClient;
|
||||
@@ -36,7 +21,6 @@ var aliDeviceKeys;
|
||||
var bobDeviceKeys;
|
||||
var bobDeviceCurve25519Key;
|
||||
var bobDeviceEd25519Key;
|
||||
var aliLocalStore;
|
||||
var aliStorage;
|
||||
var bobStorage;
|
||||
var aliMessages;
|
||||
@@ -61,7 +45,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 +60,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 +160,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 +323,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 +346,8 @@ function recvMessage(httpBackend, client, message) {
|
||||
test_utils.mkEvent({
|
||||
type: "m.room.encrypted",
|
||||
room: roomId,
|
||||
content: message
|
||||
content: message,
|
||||
sender: sender,
|
||||
})
|
||||
]
|
||||
}
|
||||
@@ -397,6 +383,15 @@ function recvMessage(httpBackend, client, message) {
|
||||
|
||||
function aliStartClient() {
|
||||
expectAliKeyUpload().catch(test_utils.failTest);
|
||||
|
||||
// ali will try to query her own keys on start
|
||||
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[aliUserId] = {};
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
startClient(aliHttpBackend, aliClient);
|
||||
return aliHttpBackend.flush().then(function() {
|
||||
console.log("Ali client started");
|
||||
@@ -405,6 +400,15 @@ function aliStartClient() {
|
||||
|
||||
function bobStartClient() {
|
||||
expectBobKeyUpload().catch(test_utils.failTest);
|
||||
|
||||
// bob will try to query his own keys on start
|
||||
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual({});
|
||||
var result = {};
|
||||
result[bobUserId] = {};
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
startClient(bobHttpBackend, bobClient);
|
||||
return bobHttpBackend.flush().then(function() {
|
||||
console.log("Bob client started");
|
||||
@@ -459,11 +463,9 @@ describe("MatrixClient crypto", function() {
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
aliLocalStore = new MockStorageApi();
|
||||
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
|
||||
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
|
||||
test_utils.beforeEach(this);
|
||||
|
||||
aliStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
|
||||
aliHttpBackend = new HttpBackend();
|
||||
aliClient = sdk.createClient({
|
||||
baseUrl: "http://alis.server",
|
||||
@@ -474,6 +476,7 @@ describe("MatrixClient crypto", function() {
|
||||
request: aliHttpBackend.requestFn,
|
||||
});
|
||||
|
||||
bobStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
|
||||
bobHttpBackend = new HttpBackend();
|
||||
bobClient = sdk.createClient({
|
||||
baseUrl: "http://bobs.server",
|
||||
@@ -498,6 +501,27 @@ describe("MatrixClient crypto", function() {
|
||||
bobClient.stopClient();
|
||||
});
|
||||
|
||||
it('Ali knows the difference between a new user and one with no devices',
|
||||
function(done) {
|
||||
aliHttpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:id': {},
|
||||
}
|
||||
});
|
||||
|
||||
var p1 = aliClient.downloadKeys(['@bob:id']);
|
||||
var p2 = aliHttpBackend.flush('/keys/query', 1);
|
||||
|
||||
q.all([p1, p2]).then(function() {
|
||||
var devices = aliStorage.getEndToEndDevicesForUser('@bob:id');
|
||||
expect(utils.keys(devices).length).toEqual(0);
|
||||
|
||||
// request again: should be no more requests
|
||||
return aliClient.downloadKeys(['@bob:id']);
|
||||
}).nodeify(done);
|
||||
}
|
||||
);
|
||||
|
||||
it("Bob uploads without one-time keys and with one-time keys", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
@@ -529,6 +553,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 +652,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)
|
||||
@@ -605,4 +757,31 @@ describe("MatrixClient crypto", function() {
|
||||
}).then(aliRecvMessage)
|
||||
.catch(test_utils.failTest).done(done);
|
||||
});
|
||||
|
||||
|
||||
it("Ali does a key query when she gets a new_device event", function(done) {
|
||||
q()
|
||||
.then(bobUploadsKeys)
|
||||
.then(aliStartClient)
|
||||
.then(function() {
|
||||
var syncData = {
|
||||
next_batch: '2',
|
||||
to_device: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
content: {
|
||||
device_id: 'TEST_DEVICE',
|
||||
rooms: [],
|
||||
},
|
||||
sender: bobUserId,
|
||||
type: 'm.new_device',
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
aliHttpBackend.when('GET', '/sync').respond(200, syncData);
|
||||
return aliHttpBackend.flush('/sync', 1);
|
||||
}).then(expectAliQueryKeys)
|
||||
.nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,117 @@ describe("MatrixClient", function() {
|
||||
httpBackend.verifyNoOutstandingExpectation();
|
||||
});
|
||||
|
||||
describe("uploadContent", function() {
|
||||
var buf = new Buffer('hello world');
|
||||
it("should upload the file", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload"
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual(buf);
|
||||
expect(req.queryParams.filename).toEqual("hi.txt");
|
||||
expect(req.queryParams.access_token).toEqual(accessToken);
|
||||
expect(req.headers["Content-Type"]).toEqual("text/plain");
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
expect(req.opts.timeout).toBe(undefined);
|
||||
}).respond(200, "content");
|
||||
|
||||
var prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
expect(prom).toBeDefined();
|
||||
|
||||
var uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(1);
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
// for backwards compatibility, we return the raw JSON
|
||||
expect(response).toEqual("content");
|
||||
|
||||
var uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should parse the response if rawResponse=false", function(done) {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload"
|
||||
).check(function(req) {
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(200, JSON.stringify({ "content_uri": "uri" }));
|
||||
|
||||
client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
}, {
|
||||
rawResponse: false,
|
||||
}).then(function(response) {
|
||||
expect(response.content_uri).toEqual("uri");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should parse errors into a MatrixError", function(done) {
|
||||
// opts.json is false, so request returns unparsed json.
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/v1/upload"
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual(buf);
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(400, JSON.stringify({
|
||||
"errcode": "M_SNAFU",
|
||||
"error": "broken",
|
||||
}));
|
||||
|
||||
client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
}).then(function(response) {
|
||||
throw Error("request not failed");
|
||||
}, function(error) {
|
||||
expect(error.httpStatus).toEqual(400);
|
||||
expect(error.errcode).toEqual("M_SNAFU");
|
||||
expect(error.message).toEqual("broken");
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
it("should return a promise which can be cancelled", function(done) {
|
||||
var prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
});
|
||||
|
||||
var uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(1);
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
throw Error("request not aborted");
|
||||
}, function(error) {
|
||||
expect(error).toEqual("aborted");
|
||||
|
||||
var uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
var r = client.cancelUpload(prom);
|
||||
expect(r).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("joinRoom", function() {
|
||||
it("should no-op if you've already joined a room", function() {
|
||||
var roomId = "!foo:bar";
|
||||
@@ -184,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 },
|
||||
@@ -249,25 +367,24 @@ describe("MatrixClient", function() {
|
||||
algorithms: ["2"],
|
||||
unsigned: { "ghi": "def" },
|
||||
});
|
||||
|
||||
// dave's key fails validation.
|
||||
expect(res.dave).toEqual({});
|
||||
}).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return a rejected promise if the request fails", function(done) {
|
||||
httpBackend.when("POST", "/keys/query").respond(400);
|
||||
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);
|
||||
|
||||
var exceptionThrown;
|
||||
client.downloadKeys(["bottom"]).then(function() {
|
||||
fail("download didn't fail");
|
||||
}, function(err) {
|
||||
exceptionThrown = err;
|
||||
}).then(function() {
|
||||
expect(exceptionThrown).toBeTruthy();
|
||||
}).catch(utils.failTest).done(done);
|
||||
client.deleteDevice(
|
||||
"my_device", auth
|
||||
).catch(utils.failTest).done(done);
|
||||
|
||||
httpBackend.flush();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,893 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
|
||||
try {
|
||||
var Olm = require('olm');
|
||||
} catch (e) {}
|
||||
|
||||
var anotherjson = require('another-json');
|
||||
var q = require('q');
|
||||
|
||||
var sdk = require('../..');
|
||||
var utils = require('../../lib/utils');
|
||||
var test_utils = require('../test-utils');
|
||||
var MockHttpBackend = require('../mock-request');
|
||||
|
||||
var ROOM_ID = "!room:id";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} userId
|
||||
* @param {string} deviceId
|
||||
* @param {string} accessToken
|
||||
*/
|
||||
function TestClient(userId, deviceId, accessToken) {
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
this.storage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
this.client = sdk.createClient({
|
||||
baseUrl: "http://test.server",
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
deviceId: deviceId,
|
||||
sessionStore: this.storage,
|
||||
request: this.httpBackend.requestFn,
|
||||
});
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*
|
||||
* @param {object?} deviceQueryResponse the list of our existing devices to return from
|
||||
* the /query request. Defaults to empty device list
|
||||
* @return {Promise}
|
||||
*/
|
||||
TestClient.prototype.start = function(existingDevices) {
|
||||
var self = this;
|
||||
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
this.httpBackend.when('POST', '/keys/query').respond(200, function(path, content) {
|
||||
expect(content.device_keys[self.userId]).toEqual({});
|
||||
var res = existingDevices;
|
||||
if (!res) {
|
||||
res = { device_keys: {} };
|
||||
res.device_keys[self.userId] = {};
|
||||
}
|
||||
return res;
|
||||
});
|
||||
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
|
||||
expect(content.one_time_keys).not.toBeDefined();
|
||||
expect(content.device_keys).toBeDefined();
|
||||
self.deviceKeys = content.device_keys;
|
||||
return {one_time_key_counts: {signed_curve25519: 0}};
|
||||
});
|
||||
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
|
||||
expect(content.device_keys).not.toBeDefined();
|
||||
expect(content.one_time_keys).toBeDefined();
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
self.oneTimeKeys = content.one_time_keys;
|
||||
return {one_time_key_counts: {
|
||||
signed_curve25519: utils.keys(self.oneTimeKeys).length
|
||||
}};
|
||||
});
|
||||
|
||||
this.client.startClient();
|
||||
|
||||
return this.httpBackend.flush();
|
||||
};
|
||||
|
||||
/**
|
||||
* stop the client
|
||||
*/
|
||||
TestClient.prototype.stop = function() {
|
||||
this.client.stopClient();
|
||||
};
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getDeviceKey = function() {
|
||||
var key_id = 'curve25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[key_id];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
* @return {string} base64 device key
|
||||
*/
|
||||
TestClient.prototype.getSigningKey = function() {
|
||||
var key_id = 'ed25519:' + this.deviceId;
|
||||
return this.deviceKeys.keys[key_id];
|
||||
};
|
||||
|
||||
/**
|
||||
* start an Olm session with a given recipient
|
||||
*
|
||||
* @param {Olm.Account} olmAccount
|
||||
* @param {TestClient} recipientTestClient
|
||||
* @return {Olm.Session}
|
||||
*/
|
||||
function createOlmSession(olmAccount, recipientTestClient) {
|
||||
var otk_id = utils.keys(recipientTestClient.oneTimeKeys)[0];
|
||||
var otk = recipientTestClient.oneTimeKeys[otk_id];
|
||||
|
||||
var session = new Olm.Session();
|
||||
session.create_outbound(
|
||||
olmAccount, recipientTestClient.getDeviceKey(), otk.key
|
||||
);
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* encrypt an event with olm
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string=} opts.sender
|
||||
* @param {string} opts.senderKey
|
||||
* @param {Olm.Session} opts.p2pSession
|
||||
* @param {TestClient} opts.recipient
|
||||
* @param {object=} opts.plaincontent
|
||||
* @param {string=} opts.plaintype
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function encryptOlmEvent(opts) {
|
||||
expect(opts.senderKey).toBeDefined();
|
||||
expect(opts.p2pSession).toBeDefined();
|
||||
expect(opts.recipient).toBeDefined();
|
||||
|
||||
var plaintext = {
|
||||
content: opts.plaincontent || {},
|
||||
recipient: opts.recipient.userId,
|
||||
recipient_keys: {
|
||||
ed25519: opts.recipient.getSigningKey(),
|
||||
},
|
||||
sender: opts.sender || '@bob:xyz',
|
||||
type: opts.plaintype || 'm.test',
|
||||
};
|
||||
|
||||
var event = {
|
||||
content: {
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
ciphertext: {},
|
||||
sender_key: opts.senderKey,
|
||||
},
|
||||
sender: opts.sender || '@bob:xyz',
|
||||
type: 'm.room.encrypted',
|
||||
};
|
||||
event.content.ciphertext[opts.recipient.getDeviceKey()] =
|
||||
opts.p2pSession.encrypt(JSON.stringify(plaintext));
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* encrypt an event with megolm
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.senderKey
|
||||
* @param {Olm.OutboundGroupSession} opts.groupSession
|
||||
* @param {object=} opts.plaintext
|
||||
* @param {string=} opts.room_id
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function encryptMegolmEvent(opts) {
|
||||
expect(opts.senderKey).toBeDefined();
|
||||
expect(opts.groupSession).toBeDefined();
|
||||
|
||||
var plaintext = opts.plaintext || {};
|
||||
if (!plaintext.content) {
|
||||
plaintext.content = {
|
||||
body: '42',
|
||||
msgtype: "m.text",
|
||||
};
|
||||
}
|
||||
if (!plaintext.type) {
|
||||
plaintext.type = "m.room.message";
|
||||
}
|
||||
if (!plaintext.room_id) {
|
||||
expect(opts.room_id).toBeDefined();
|
||||
plaintext.room_id = opts.room_id;
|
||||
}
|
||||
|
||||
return {
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
|
||||
device_id: "testDevice",
|
||||
sender_key: opts.senderKey,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
},
|
||||
type: "m.room.encrypted",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* build an encrypted room_key event to share a group session
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.senderKey
|
||||
* @param {TestClient} opts.recipient
|
||||
* @param {Olm.Session} opts.p2pSession
|
||||
* @param {Olm.OutboundGroupSession} opts.groupSession
|
||||
* @param {string=} opts.room_id
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function encryptGroupSessionKey(opts) {
|
||||
return encryptOlmEvent({
|
||||
senderKey: opts.senderKey,
|
||||
recipient: opts.recipient,
|
||||
p2pSession: opts.p2pSession,
|
||||
plaincontent: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
room_id: opts.room_id,
|
||||
session_id: opts.groupSession.session_id(),
|
||||
session_key: opts.groupSession.session_key(),
|
||||
},
|
||||
plaintype: 'm.room_key',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* get a /sync response which contains a single room (ROOM_ID),
|
||||
* with the members given
|
||||
*
|
||||
* @param {string[]} roomMembers
|
||||
*
|
||||
* @return {object} event
|
||||
*/
|
||||
function getSyncResponse(roomMembers) {
|
||||
var roomResponse = {
|
||||
state: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i < roomMembers.length; i++) {
|
||||
roomResponse.state.events.push(
|
||||
test_utils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: roomMembers[i],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
var syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = roomResponse;
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("megolm", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
var testOlmAccount;
|
||||
var testSenderKey;
|
||||
var aliceTestClient;
|
||||
|
||||
/**
|
||||
* Get the device keys for testOlmAccount in a format suitable for a
|
||||
* response to /keys/query
|
||||
*/
|
||||
function getTestKeysQueryResponse(userId) {
|
||||
var testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
var testDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'DEVICE_ID',
|
||||
keys: {
|
||||
'curve25519:DEVICE_ID': testE2eKeys.curve25519,
|
||||
'ed25519:DEVICE_ID': testE2eKeys.ed25519,
|
||||
},
|
||||
user_id: userId,
|
||||
};
|
||||
var j = anotherjson.stringify(testDeviceKeys);
|
||||
var sig = testOlmAccount.sign(j);
|
||||
testDeviceKeys.signatures = {};
|
||||
testDeviceKeys.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
var queryResponse = {
|
||||
device_keys: {},
|
||||
};
|
||||
|
||||
queryResponse.device_keys[userId] = {
|
||||
'DEVICE_ID': testDeviceKeys,
|
||||
};
|
||||
|
||||
return queryResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a one-time key for testOlmAccount in a format suitable for a
|
||||
* response to /keys/claim
|
||||
*/
|
||||
function getTestKeysClaimResponse(userId) {
|
||||
testOlmAccount.generate_one_time_keys(1);
|
||||
var testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
var keyId = utils.keys(testOneTimeKeys.curve25519)[0];
|
||||
var oneTimeKey = testOneTimeKeys.curve25519[keyId];
|
||||
var keyResult = {
|
||||
'key': oneTimeKey,
|
||||
};
|
||||
var j = anotherjson.stringify(keyResult);
|
||||
var sig = testOlmAccount.sign(j);
|
||||
keyResult.signatures = {};
|
||||
keyResult.signatures[userId] = {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
var claimResponse = {one_time_keys: {}};
|
||||
claimResponse.one_time_keys[userId] = {
|
||||
'DEVICE_ID': {},
|
||||
};
|
||||
claimResponse.one_time_keys[userId].DEVICE_ID['signed_curve25519:' + keyId] =
|
||||
keyResult;
|
||||
return claimResponse;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
test_utils.beforeEach(this);
|
||||
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs"
|
||||
);
|
||||
|
||||
testOlmAccount = new Olm.Account();
|
||||
testOlmAccount.create();
|
||||
var testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
|
||||
testSenderKey = testE2eKeys.curve25519;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
aliceTestClient.stop();
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message", function(done) {
|
||||
return aliceTestClient.start().then(function() {
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
var roomKeyEncrypted = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
var messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// Alice gets both the events in a single sync
|
||||
var syncResponse = {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted],
|
||||
},
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.httpBackend.flush("/sync", 1);
|
||||
}).then(function() {
|
||||
var room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
var event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("Alice gets a second room_key message", function(done) {
|
||||
return aliceTestClient.start().then(function() {
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
// make the room_key event
|
||||
var roomKeyEncrypted1 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// encrypt a message with the group session
|
||||
var messageEncrypted = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// make a second room_key event now that we have advanced the group
|
||||
// session.
|
||||
var roomKeyEncrypted2 = encryptGroupSessionKey({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
|
||||
// on the first sync, send the best room key
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
|
||||
next_batch: 1,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted1],
|
||||
},
|
||||
});
|
||||
|
||||
// on the second sync, send the advanced room key, along with the
|
||||
// message. This simulates the situation where Alice has been sent a
|
||||
// later copy of the room key and is reloading the client.
|
||||
var syncResponse2 = {
|
||||
next_batch: 2,
|
||||
to_device: {
|
||||
events: [roomKeyEncrypted2],
|
||||
},
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse2.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [messageEncrypted],
|
||||
},
|
||||
};
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
|
||||
|
||||
return aliceTestClient.httpBackend.flush("/sync", 2);
|
||||
}).then(function() {
|
||||
var room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
var event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it('Alice sends a megolm message', function(done) {
|
||||
var p2pSession;
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
var inboundGroupSession;
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
var m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, function(path, content) {
|
||||
var ct = content.ciphertext;
|
||||
var r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
|
||||
expect(r.message_index).toEqual(0);
|
||||
var decrypted = JSON.parse(r.plaintext);
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function(done) {
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
console.log("Forcing alice to download our device keys");
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
|
||||
device_keys: {
|
||||
'@bob:xyz': {},
|
||||
}
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log("Telling alice to send a megolm message");
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
|
||||
it("We shouldn't attempt to send to blocked devices", function(done) {
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
console.log('Forcing alice to download our device keys');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.downloadKeys(['@bob:xyz']),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
console.log('Telling alice to send a megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
it("We should start a new megolm session when a device is blocked", function(done) {
|
||||
var p2pSession;
|
||||
var megolmSessionId;
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to send a megolm message');
|
||||
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log('sendToDevice: ', content);
|
||||
var m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(1); // normal message
|
||||
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
console.log('decrypted sendToDevice:', decrypted);
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
megolmSessionId = decrypted.content.session_id;
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log('/send:', content);
|
||||
expect(content.session_id).toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).then(function() {
|
||||
console.log('Telling alice to block our device');
|
||||
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
|
||||
|
||||
console.log('Telling alice to send another megolm message');
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log('/send:', content);
|
||||
expect(content.session_id).not.toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/2676
|
||||
it("Alice should send to her other devices", function(done) {
|
||||
// for this test, we make the testOlmAccount be another of Alice's devices.
|
||||
// it ought to get include in messages Alice sends.
|
||||
|
||||
var p2pSession;
|
||||
var inboundGroupSession;
|
||||
var decrypted;
|
||||
|
||||
return aliceTestClient.start(
|
||||
getTestKeysQueryResponse(aliceTestClient.userId)
|
||||
).then(function() {
|
||||
// an encrypted room with just alice
|
||||
var syncResponse = {
|
||||
next_batch: 1,
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncResponse.rooms.join[ROOM_ID] = {
|
||||
state: {
|
||||
events: [
|
||||
test_utils.mkEvent({
|
||||
type: 'm.room.encryption',
|
||||
skey: '',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
},
|
||||
}),
|
||||
test_utils.mkMembership({
|
||||
mship: 'join',
|
||||
sender: aliceTestClient.userId,
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
|
||||
return aliceTestClient.httpBackend.flush();
|
||||
}).then(function() {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
|
||||
200, function(path, content)
|
||||
{
|
||||
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
|
||||
.toEqual("signed_curve25519");
|
||||
return getTestKeysClaimResponse(aliceTestClient.userId);
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
console.log("sendToDevice: ", content);
|
||||
var m = content.messages[aliceTestClient.userId].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
expect(ct.type).toEqual(0); // pre-key message
|
||||
|
||||
p2pSession = new Olm.Session();
|
||||
p2pSession.create_inbound(testOlmAccount, ct.body);
|
||||
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, function(path, content) {
|
||||
var ct = content.ciphertext;
|
||||
var r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
decrypted = JSON.parse(r.plaintext);
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return q.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush(),
|
||||
]);
|
||||
}).then(function() {
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
|
||||
it('Alice should wait for device list to complete when sending a megolm message',
|
||||
function(done) {
|
||||
var p2pSession;
|
||||
var inboundGroupSession;
|
||||
|
||||
var downloadPromise;
|
||||
var sendPromise;
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/m.room.encrypted/'
|
||||
).respond(200, function(path, content) {
|
||||
var m = content.messages['@bob:xyz'].DEVICE_ID;
|
||||
var ct = m.ciphertext[testSenderKey];
|
||||
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
|
||||
|
||||
expect(decrypted.type).toEqual('m.room_key');
|
||||
inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(decrypted.content.session_key);
|
||||
return {};
|
||||
});
|
||||
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/send/'
|
||||
).respond(200, function(path, content) {
|
||||
var ct = content.ciphertext;
|
||||
var r = inboundGroupSession.decrypt(ct);
|
||||
console.log('Decrypted received megolm message', r);
|
||||
|
||||
expect(r.message_index).toEqual(0);
|
||||
var decrypted = JSON.parse(r.plaintext);
|
||||
expect(decrypted.type).toEqual('m.room.message');
|
||||
expect(decrypted.content.body).toEqual('test');
|
||||
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
|
||||
return aliceTestClient.start().then(function() {
|
||||
var syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
|
||||
// establish an olm session with alice
|
||||
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
|
||||
|
||||
var olmEvent = encryptOlmEvent({
|
||||
senderKey: testSenderKey,
|
||||
recipient: aliceTestClient,
|
||||
p2pSession: p2pSession,
|
||||
});
|
||||
|
||||
syncResponse.to_device = { events: [olmEvent] };
|
||||
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
return aliceTestClient.httpBackend.flush('/sync', 1);
|
||||
}).then(function() {
|
||||
console.log('Forcing alice to download our device keys');
|
||||
|
||||
// this will block
|
||||
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(function() {
|
||||
|
||||
// so will this.
|
||||
sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
|
||||
}).then(function() {
|
||||
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
|
||||
200, getTestKeysQueryResponse('@bob:xyz')
|
||||
);
|
||||
|
||||
return aliceTestClient.httpBackend.flush();
|
||||
}).then(function() {
|
||||
return q.all([downloadPromise, sendPromise]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
+58
-11
@@ -11,18 +11,17 @@ function HttpBackend() {
|
||||
var self = this;
|
||||
// the request function dependency that the SDK needs.
|
||||
this.requestFn = function(opts, callback) {
|
||||
var realReq = new Request(opts.method, opts.uri, opts.body, opts.qs);
|
||||
realReq.callback = callback;
|
||||
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
|
||||
self.requests.push(realReq);
|
||||
var req = new Request(opts, callback);
|
||||
console.log("HTTP backend received request: %s", req);
|
||||
self.requests.push(req);
|
||||
|
||||
var abort = function() {
|
||||
var idx = self.requests.indexOf(realReq);
|
||||
var idx = self.requests.indexOf(req);
|
||||
if (idx >= 0) {
|
||||
console.log("Aborting HTTP request: %s %s", opts.method,
|
||||
opts.uri);
|
||||
self.requests.splice(idx, 1);
|
||||
realReq.callback("aborted");
|
||||
req.callback("aborted");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -161,22 +160,32 @@ HttpBackend.prototype = {
|
||||
* @return {Request} An expected request.
|
||||
*/
|
||||
when: function(method, path, data) {
|
||||
var pendingReq = new Request(method, path, data);
|
||||
var pendingReq = new ExpectedRequest(method, path, data);
|
||||
this.expectedRequests.push(pendingReq);
|
||||
return pendingReq;
|
||||
}
|
||||
};
|
||||
|
||||
function Request(method, path, data, queryParams) {
|
||||
/**
|
||||
* Represents the expectation of a request.
|
||||
*
|
||||
* <p>Includes the conditions to be matched against, the checks to be made,
|
||||
* and the response to be returned.
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} method
|
||||
* @param {string} path
|
||||
* @param {object?} data
|
||||
*/
|
||||
function ExpectedRequest(method, path, data) {
|
||||
this.method = method;
|
||||
this.path = path;
|
||||
this.data = data;
|
||||
this.queryParams = queryParams;
|
||||
this.callback = null;
|
||||
this.response = null;
|
||||
this.checks = [];
|
||||
}
|
||||
Request.prototype = {
|
||||
|
||||
ExpectedRequest.prototype = {
|
||||
/**
|
||||
* Execute a check when this request has been satisfied.
|
||||
* @param {Function} fn The function to execute.
|
||||
@@ -221,6 +230,44 @@ Request.prototype = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a request made by the app.
|
||||
*
|
||||
* @constructor
|
||||
* @param {object} opts opts passed to request()
|
||||
* @param {function} callback
|
||||
*/
|
||||
function Request(opts, callback) {
|
||||
this.opts = opts;
|
||||
this.callback = callback;
|
||||
|
||||
Object.defineProperty(this, 'method', {
|
||||
get: function() { return opts.method; }
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'path', {
|
||||
get: function() { return opts.uri; }
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'data', {
|
||||
get: function() { return opts.body; }
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'queryParams', {
|
||||
get: function() { return opts.qs; }
|
||||
});
|
||||
|
||||
Object.defineProperty(this, 'headers', {
|
||||
get: function() { return opts.headers || {}; }
|
||||
});
|
||||
}
|
||||
|
||||
Request.prototype = {
|
||||
toString: function() {
|
||||
return this.method + " " + this.path;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The HttpBackend class.
|
||||
*/
|
||||
|
||||
+34
-4
@@ -65,7 +65,7 @@ module.exports.mkEvent = function(opts) {
|
||||
content: opts.content,
|
||||
event_id: "$" + Math.random() + "-" + Math.random()
|
||||
};
|
||||
if (opts.skey) {
|
||||
if (opts.skey !== undefined) {
|
||||
event.state_key = opts.skey;
|
||||
}
|
||||
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
|
||||
@@ -159,7 +159,16 @@ module.exports.mkMessage = function(opts) {
|
||||
* <p>This is useful for use with integration tests which use asyncronous
|
||||
* methods: it can be added as a 'catch' handler in a promise chain.
|
||||
*
|
||||
* @param {Error} error exception to be reported
|
||||
* @param {Error} err exception to be reported
|
||||
*
|
||||
* @deprecated
|
||||
* It turns out there are easier ways of doing this. Just use nodeify():
|
||||
*
|
||||
* it("should not throw", function(done) {
|
||||
* asynchronousMethod().then(function() {
|
||||
* // some tests
|
||||
* }).nodeify(done);
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* it("should not throw", function(done) {
|
||||
@@ -168,6 +177,27 @@ module.exports.mkMessage = function(opts) {
|
||||
* }).catch(utils.failTest).done(done);
|
||||
* });
|
||||
*/
|
||||
module.exports.failTest = function(error) {
|
||||
expect(error.stack).toBe(null);
|
||||
module.exports.failTest = function(err) {
|
||||
expect(true).toBe(false, "Testfunc threw: " + err.stack);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
module.exports.MockStorageApi = function() {
|
||||
this.data = {};
|
||||
};
|
||||
module.exports.MockStorageApi.prototype = {
|
||||
setItem: function(k, v) {
|
||||
this.data[k] = v;
|
||||
},
|
||||
getItem: function(k) {
|
||||
return this.data[k] || null;
|
||||
},
|
||||
removeItem: function(k) {
|
||||
delete this.data[k];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,6 @@ describe("Crypto", function() {
|
||||
}
|
||||
|
||||
it("Crypto exposes the correct olm library version", function() {
|
||||
expect(Crypto.getOlmVersion()).toEqual([1, 3, 0]);
|
||||
expect(Crypto.getOlmVersion()[0]).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -51,9 +51,10 @@ describe("MatrixClient", function() {
|
||||
// }
|
||||
// items are popped off when processed and block if no items left.
|
||||
];
|
||||
var accept_keepalives;
|
||||
var pendingLookup = null;
|
||||
function httpReq(cb, method, path, qp, data, prefix) {
|
||||
if (path === KEEP_ALIVE_PATH) {
|
||||
if (path === KEEP_ALIVE_PATH && accept_keepalives) {
|
||||
return q();
|
||||
}
|
||||
var next = httpLookups.shift();
|
||||
@@ -103,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
|
||||
@@ -143,8 +145,10 @@ describe("MatrixClient", function() {
|
||||
client._http.authedRequest.andCallFake(httpReq);
|
||||
client._http.authedRequestWithPrefix.andCallFake(httpReq);
|
||||
client._http.requestWithPrefix.andCallFake(httpReq);
|
||||
client._http.request.andCallFake(httpReq);
|
||||
|
||||
// set reasonable working defaults
|
||||
accept_keepalives = true;
|
||||
pendingLookup = null;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
@@ -204,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();
|
||||
@@ -250,6 +292,8 @@ describe("MatrixClient", function() {
|
||||
true, "retryImmediately returned false"
|
||||
);
|
||||
jasmine.Clock.tick(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
jasmine.Clock.tick(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -329,21 +373,25 @@ describe("MatrixClient", function() {
|
||||
it("should transition ERROR -> PREPARED after /sync if prev failed",
|
||||
function(done) {
|
||||
var expectedStates = [];
|
||||
accept_keepalives = false;
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
// We fail twice since the SDK ignores the first error.
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE2" }
|
||||
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: KEEP_ALIVE_PATH, data: {}
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", data: SYNC_DATA
|
||||
});
|
||||
|
||||
expectedStates.push(["ERROR", null]);
|
||||
expectedStates.push(["RECONNECTING", null]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["PREPARED", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
@@ -358,18 +406,19 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
|
||||
accept_keepalives = false;
|
||||
var expectedStates = [];
|
||||
// We fail twice since the SDK ignores the first error.
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO2" }
|
||||
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
});
|
||||
@@ -402,22 +451,23 @@ describe("MatrixClient", function() {
|
||||
client.startClient();
|
||||
});
|
||||
|
||||
it("should transition ERROR -> ERROR if multiple /sync fails", function(done) {
|
||||
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
|
||||
accept_keepalives = false;
|
||||
var expectedStates = [];
|
||||
// We fail twice since the SDK ignores the first error.
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
|
||||
});
|
||||
httpLookups.push({
|
||||
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
|
||||
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
|
||||
});
|
||||
|
||||
expectedStates.push(["PREPARED", null]);
|
||||
expectedStates.push(["SYNCING", "PREPARED"]);
|
||||
expectedStates.push(["ERROR", "SYNCING"]);
|
||||
expectedStates.push(["RECONNECTING", "SYNCING"]);
|
||||
expectedStates.push(["ERROR", "RECONNECTING"]);
|
||||
expectedStates.push(["ERROR", "ERROR"]);
|
||||
client.on("sync", syncChecker(expectedStates, done));
|
||||
client.startClient();
|
||||
|
||||
Reference in New Issue
Block a user