Compare commits

...

379 Commits

Author SHA1 Message Date
David Baker a600432946 v0.14.3 2019-01-22 10:41:09 +00:00
David Baker cde77cfc92 Prepare changelog for v0.14.3 2019-01-22 10:41:08 +00:00
David Baker d1f8cbaade v0.14.3-rc.1 2019-01-17 10:44:15 +00:00
David Baker 5f29cd2c1c Prepare changelog for v0.14.3-rc.1 2019-01-17 10:44:14 +00:00
David Baker 59b80d8fbd Remove babel-eslint as we no longer need it 2019-01-10 10:42:43 +00:00
David Baker 68bb8182e4 Update ESLint
* Bump version
 * Add a couple of rules to match our existing precedent
 * Fix a few genuine lint errors
 * Ignore a guard-for-in (not sure why eslint doesn't like this?)
 * Update max warnings
2019-01-10 10:42:34 +00:00
David Baker f6c8687dc8 Merge pull request #812 from matrix-org/dbkr/getallsessions
Add a getAllEndToEndSessions to crypto store
2019-01-03 10:09:18 +00:00
J. Ryan Stinnett 472d8faace Merge pull request #668 from matrix-org/t3chguy/fix_displayname_logic
T3chguy/fix displayname logic
2019-01-02 17:40:19 -06:00
Michael Telatynski fc5f3c2fcc re-add empty check after removing hidden chars 2018-12-30 00:20:33 +00:00
Travis Ralston fb756208d8 Merge pull request #814 from trashhalo/rebase-tip
Contributing: Note that rebase lets you mass signoff commits

replace find loop + slice with a single filter. More readable

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

keep public APIs consistent with previous behaviour now that its fuzzy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

make tests not fail, because of order of occurrence

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

do falsey displayname check regardless of whether we have roomstate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

add tests for the fuzzy disambiguation between members in a room

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

remove duplicated call
2018-12-30 00:18:05 +00:00
Travis Ralston 6a98e93845 Merge pull request #814 from trashhalo/rebase-tip
Contributing: Note that rebase lets you mass signoff commits
2018-12-28 19:40:41 -07:00
Stephen Solka 79e155acfb Note that rebase lets you mass signoff commits
Signed-off-by: Stephen Solka <stephen0q@gmail.com>
2018-12-28 21:36:06 -05:00
David Baker 59ae6e3dc0 Missing bracket 2018-12-21 19:21:40 +00:00
David Baker e628ed3ef4 Add a getAllEndToEndSessions to crypto store
So we can migrate them from place to place
2018-12-21 19:12:01 +00:00
J. Ryan Stinnett 11d40e9daa Merge pull request #672 from matrix-org/t3chguy/unhomoglyph
take into account homoglyphs when calculating similar display names
2018-12-20 14:38:48 +00:00
Michael Telatynski a07f0631b7 Include unhomoglyph in package-lock.json - it must have felt left out :L
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-12-19 22:28:40 +00:00
Michael Telatynski 790d1dd8f7 Merge branches 'develop' and 't3chguy/unhomoglyph' of github.com:matrix-org/matrix-js-sdk into t3chguy/unhomoglyph 2018-12-19 22:21:09 +00:00
J. Ryan Stinnett c92e510a4d Pin to base-x 3.0.4
This avoids API changes in 3.0.5 (requiring `Buffer` instances), but more
importantly also avoids dealing with ES6 in dependencies for another day.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-18 19:47:17 +00:00
David Baker c48a6c0601 Merge pull request #809 from jryans/key-backup-version-change
Emit for key backup failures
2018-12-18 09:34:43 +00:00
J. Ryan Stinnett 383f3f9834 Merge branch 'develop' into t3chguy/unhomoglyph 2018-12-18 01:01:41 +00:00
J. Ryan Stinnett 3c1e9ba6e9 Merge pull request #646 from leonlianght/develop
emit oldEventId on "updatePendingEvent"
2018-12-18 00:41:52 +00:00
J. Ryan Stinnett 26893b9877 Merge pull request #589 from johbo/add-thirdparty-user
Add getThirdpartyUser to base api
2018-12-18 00:38:09 +00:00
J. Ryan Stinnett 2b734b8e69 Emit for key backup failures
Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-14 15:17:22 +00:00
J. Ryan Stinnett c5f6f87a6c Add await to ensure we wait for backup errors
Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-14 15:17:22 +00:00
David Baker 66cdb62a3d Reorganize key backup flow
This will allow the key backup flow to propagate errors for things like version
mismatches more easily.

In addition, it raises the limit of keys sent per request from 10 to 200 to cut
down on the number of requests.
2018-12-14 15:17:10 +00:00
Travis Ralston f53e33723b Merge pull request #805 from matrix-org/travis/custom-status
Support custom status messages
2018-12-13 10:24:37 -07:00
Travis Ralston 06bc6e7568 Update jsdoc to match _unstable_ prefix 2018-12-13 09:42:22 -07:00
J. Ryan Stinnett 5e3f42ec5a Use olm-loader in all crypto tests
Standardize on importing `olm-loader` rather than pasting the same boilerplate
in different tests.  Importantly, `spec/unit/crypto.spec.js` did not include any
loading approach, so it would only find Olm if some other test loaded it first.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-13 08:59:57 +00:00
Travis Ralston 08b3dfa3b5 Prefix the status message API with _unstable
It's not a formal feature of Matrix yet, so we should try and avoid people relying on it. This makes it appear as a private API and is very clearly labeled as not intended for use.
2018-12-12 23:05:03 -07:00
J. Ryan Stinnett 6cf9563441 Avoid checking key backup status if guest
Fixes vector-im/riot-web#7839.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-12 22:26:01 +00:00
Travis Ralston fb65c7f4ba Support setting status message in rooms that look like 1:1s
Part of https://github.com/vector-im/riot-web/issues/1528
2018-12-12 13:21:13 -07:00
Travis Ralston c4452909e7 Support .well-known autodiscovery in the js-sdk (#799)
* Support .well-known autodiscovery in the js-sdk

It's much more useful here than in the react-sdk as it can be reused by more applications. This is also required to make the react-sdk a little easier to manage .well-known lookups as soon it'll be doing it in several places. 

Automatic discovery is an abstract concept in the spec and could include more than .well-known in the future, so this is made to be generic enough to support future mechanisms and other resources to discover. There's also a ton of comments (more than normally needed) as people may wish to use this as a reference in their own implementation and it doesn't hurt to explain what everything is doing.

Many of the functions are air lifted from the react-sdk and modified to work within the confines of the js-sdk.

* Swap out uglify-js for uglify-es

So we can start using ES6 dependencies without figuring out how to update babel. 

`uglify-es` is compatible with `uglify-js@3` (we were using `@2`) , which is why the same command is used. This commit includes changes to the command line to make the thing run the same as before too.

* Appease the linter

* Appease the linter some more

* Appease the linter: the tiebreaker

* Appease the linter yet again

* Switch to using the already available URL libraries

* Remove excess logging
2018-12-12 08:38:05 -07:00
Bruno Windels 2d3669b03b Merge pull request #749 from matrix-org/bwindels/releaseimprovements
Extra checks to avoid release script blowing up mid-process.
2018-12-12 09:07:12 +00:00
Travis Ralston 848e6e5897 Support reading custom status messages
Part of https://github.com/vector-im/riot-web/issues/1528
2018-12-11 21:41:15 -07:00
David Baker c723b76138 Merge branch 'master' into develop 2018-12-10 13:37:15 +00:00
David Baker 57f6b0af09 v0.14.2 2018-12-10 13:31:37 +00:00
David Baker 1c4082af45 Prepare changelog for v0.14.2 2018-12-10 13:31:37 +00:00
David Baker eece5d318e Merge pull request #800 from matrix-org/travis/mto-via
Move glob regex utilities out of the pushprocessor and into a more generic place
2018-12-07 17:47:56 +00:00
David Baker bb6ade2165 v0.14.2-rc.1 2018-12-06 11:11:29 +00:00
David Baker 586b010811 Prepare changelog for v0.14.2-rc.1 2018-12-06 11:11:28 +00:00
Travis Ralston 84ab0fde51 Appease the linter 2018-12-05 18:13:28 -07:00
Travis Ralston ec18df2c2a Move glob regex utilities out of the pushprocessor and into a more generic place 2018-12-05 18:01:12 -07:00
David Baker f50503e7c1 Merge pull request #794 from uhoreg/e2e_backup-test-fix
fix some assertions in e2e backup unit test
2018-12-04 11:22:20 +00:00
Travis Ralston c619e5c381 Merge pull request #798 from matrix-org/hs/fix-media-config
Config should be called with auth
2018-12-03 18:29:56 -07:00
Will Hunt e7c4a74ed6 Config should be called with auth 2018-12-04 01:27:08 +00:00
Hubert Chathi f8ea019f02 fix some assertions in e2e backup unit test 2018-11-29 11:52:19 -05:00
David Baker 6db8dd620d Fix https://github.com/matrix-org/matrix-js-sdk/pull/792 2018-11-28 16:11:28 +00:00
David Baker bdc1fa4c03 Merge pull request #792 from matrix-org/dbkr/dont_reestablish_unknwon_device
Don't re-establish sessions with unknown devices
2018-11-28 16:04:31 +00:00
David Baker 4e66a2d436 Don't re-establish sessions with unknown devices
as it won't work
2018-11-28 15:53:13 +00:00
David Baker 0fa948448e Merge branch 'master' into develop 2018-11-22 16:48:36 +00:00
David Baker 76c675cd09 v0.14.1 2018-11-22 16:46:26 +00:00
David Baker 85a4a594c5 Prepare changelog for v0.14.1 2018-11-22 16:46:25 +00:00
David Baker f70746c50f Handle crypto db version upgrades 2018-11-22 16:38:10 +00:00
David Baker 712490b671 Use a vaguely recent node 2018-11-21 18:44:21 +00:00
David Baker b580e68469 Merge pull request #736 from matrix-org/dbkr/e2e_backups
Support for e2e key backups
2018-11-21 18:27:45 +00:00
David Baker bd2cf18fbc Merge pull request #786 from matrix-org/dbkr/e2e_backups_passphrase
Passphrase Support for e2e backups
2018-11-21 18:24:59 +00:00
David Baker 092f4217b0 docs 2018-11-21 17:56:02 +00:00
David Baker abd2ac7168 Rename backup API call in test 2018-11-20 16:34:04 +00:00
David Baker eeea70640e Add randomString factored out from client secret 2018-11-20 16:28:29 +00:00
David Baker 6047838f53 lint 2018-11-20 16:17:58 +00:00
David Baker cb51799246 Make backup restore work 2018-11-20 16:15:29 +00:00
David Baker 44d99277fe Support passphrase-based e2e key backups 2018-11-20 13:09:59 +00:00
David Baker 5b8e643541 Merge branch 'master' into develop 2018-11-19 15:11:40 +00:00
David Baker ae85c209ab v0.14.0 2018-11-19 15:09:43 +00:00
David Baker 2306caa62f Prepare changelog for v0.14.0 2018-11-19 15:09:42 +00:00
Richard van der Hoff 17c11ae23f Merge pull request #783 from matrix-org/rav/sso_login_type
Add 'getSsoLoginUrl' function
2018-11-19 14:13:52 +01:00
Richard van der Hoff 5b51096e37 fix default login type for getSsoLoginUrl 2018-11-19 13:08:41 +00:00
Travis Ralston ac79d6bcee Merge pull request #784 from matrix-org/bwindels/missingheroescrash
Fix: don't set the room name to null when heroes are missing.
2018-11-16 13:23:57 -07:00
David Baker b6e056f832 Merge pull request #785 from matrix-org/dbkr/crypto_db_version_upgrade
Handle crypto db version upgrades
2018-11-16 17:32:40 +00:00
David Baker d99a22d68d Update to new API
Also fix test & remove debug logging from test
2018-11-16 14:46:18 +00:00
David Baker 2602c155d0 Handle crypto db version upgrades 2018-11-16 11:31:08 +00:00
Bruno Windels 80f562643f never return null as a name 2018-11-16 10:38:44 +01:00
Richard van der Hoff 578cb4e268 Add 'getSsoLoginUrl' function 2018-11-15 19:00:37 +00:00
David Baker c53c6a94d7 Update package-lock
so versions are consistent
2018-11-15 16:38:35 +00:00
David Baker 907cf19f05 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-11-15 16:35:52 +00:00
David Baker 88682e1c3b Merge pull request #780 from matrix-org/dbkr/olm_session_unwedge
Restart broken Olm sessions
2018-11-15 16:34:10 +00:00
David Baker 20a4edf899 Merge pull request #776 from matrix-org/dbkr/use_session_last_received_message
Use the last olm session that got a message
2018-11-15 16:33:59 +00:00
David Baker 3222b11346 v0.14.0-rc.1 2018-11-15 15:06:39 +00:00
David Baker fc9d6a6d47 Prepare changelog for v0.14.0-rc.1 2018-11-15 15:06:39 +00:00
David Baker c9917e4079 lint 2018-11-15 10:03:16 +00:00
Bruno Windels b233ab87bb Merge branch 'master' into develop 2018-11-14 18:21:01 +01:00
Bruno Windels 73c3a709de Merge branch 'release-v0.13.1' 2018-11-14 18:19:24 +01:00
Bruno Windels 6ce7b30b72 v0.13.1 2018-11-14 18:18:12 +01:00
Bruno Windels 980d55a2f3 Prepare changelog for v0.13.1 2018-11-14 18:18:11 +01:00
Richard Lewis 988be62804 Add function to get currently joined room members. 2018-11-14 18:15:14 +01:00
Richard Lewis 23efd0850d Add function to get currently joined rooms. 2018-11-14 18:15:08 +01:00
David Baker 17e0f1d9ab Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:58:21 +00:00
David Baker 3c85bd55d3 Time goes forwards 2018-11-14 14:57:48 +00:00
David Baker 2298d72ab9 Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:35:33 +00:00
David Baker 408407b33d Fix typo 2018-11-14 14:34:36 +00:00
David Baker ab426384e1 Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:30:10 +00:00
David Baker 5bc68c0c6d Handle last received message ts being undefined 2018-11-14 14:29:03 +00:00
David Baker ebf20d5b2c Don't force more than one new session per device per hour 2018-11-14 14:20:55 +00:00
David Baker 93d9c40323 Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:20:42 +00:00
Bruno Windels c6ea976d7f Merge branch 'master' into develop 2018-11-14 10:55:13 +01:00
Bruno Windels 5f24915300 Merge branch 'release-v0.13.0' 2018-11-14 10:54:52 +01:00
Bruno Windels fbe174fb64 v0.13.0 2018-11-14 10:53:42 +01:00
Richard Lewis 977d5331c0 Update CHANGELOG. 2018-11-14 10:50:05 +01:00
Richard Lewis d40d7e18f5 Update CHANGELOG. 2018-11-14 10:49:54 +01:00
Richard Lewis 11be68ad49 Refactor code to base 'login' method. 2018-11-14 10:49:44 +01:00
Richard Lewis b0d0782a72 Linting. 2018-11-14 10:49:36 +01:00
Richard Lewis dbb6d8ac71 Set access_token and user_id after login in with username and password. 2018-11-14 10:49:25 +01:00
David Baker a30845f9ce lint 2018-11-14 08:03:23 +00:00
David Baker 379f290b8b Add package-lock.json
to force base-x to version 3.0.4 because 3.0.5 breaks the build
by exporting ES6.
2018-11-13 14:49:56 +00:00
David Baker 6c413bba48 Typo + exit if session was never shared 2018-11-13 12:15:33 +00:00
David Baker e17a39d446 PR feedback 2018-11-13 12:10:26 +00:00
David Baker fcadf6ec4a Store last received message ts on olm session 2018-11-12 18:12:43 +00:00
David Baker 231fde219c Store last received message ts on olm session 2018-11-12 18:10:11 +00:00
David Baker 2774bd238b Merge pull request #781 from matrix-org/travis/block-send-if-failed
Prevent messages from being sent if other messages have failed to send
2018-11-09 10:54:38 +00:00
Travis Ralston fed67192bc Fix test: Actually resend the event after unknown devices are found
This test didn't actually test that resending would work, despite its name.
2018-11-08 17:17:30 -07:00
Travis Ralston 16db970558 Appease the linter 2018-11-08 16:51:21 -07:00
Travis Ralston c9a79bf32e Prevent messages from being sent if other messages have failed to send
Fixes https://github.com/vector-im/riot-web/issues/5408
2018-11-08 16:46:03 -07:00
David Baker d74ed508f9 Restart broken Olm sessions
* Start a new Olm sessions with a device when we get an undecryptable
   message on it.
 * Send a dummy message on that sessions such that the other end knows
   about it.
 * Re-send any outstanding keyshare requests for that device.

Also includes a unit test for megolm that isn't very related but came
out as a result anyway.

Includes https://github.com/matrix-org/matrix-js-sdk/pull/776
Fixes https://github.com/vector-im/riot-web/issues/3822
2018-11-08 19:09:28 +00:00
David Baker eafba9c7ef Merge pull request #777 from matrix-org/dbkr/olm_unit_test
A unit test for olm
2018-11-06 12:04:55 +00:00
Richard Lewis 610923af89 Merge pull request #778 from matrix-org/rxl881/loginWithPassword
Set access_token and user_id after login in with username and password.
2018-11-05 17:25:31 +00:00
Richard Lewis 23dfeb13df Update CHANGELOG. 2018-11-05 17:17:22 +00:00
Richard Lewis f4abd7d027 Update CHANGELOG. 2018-11-05 17:06:39 +00:00
Richard Lewis b716e71784 Refactor code to base 'login' method. 2018-11-05 16:40:11 +00:00
Richard Lewis 094598196a Linting. 2018-11-05 16:02:30 +00:00
David Baker db1d1c49a0 Merge pull request #779 from matrix-org/rxl881/getRooms
Add function to get currently joined rooms.
2018-11-05 12:14:12 +00:00
David Baker ff4125c11e Remove unused stuff & comments 2018-11-05 09:39:46 +00:00
Richard Lewis a0d51803ed Add function to get currently joined room members. 2018-11-05 00:08:04 +00:00
Richard Lewis 3aabd63975 Add function to get currently joined rooms. 2018-11-04 21:49:17 +00:00
Richard Lewis 394e37f9ea Set access_token and user_id after login in with username and password. 2018-11-04 19:43:18 +00:00
Travis Ralston 369b88d6f8 Merge pull request #775 from matrix-org/travis/cleanup-request
Remove the request-only stuff we don't need anymore
2018-11-02 09:39:23 -06:00
David Baker ec8b3ae515 Lint (and also name the test right) 2018-11-02 12:33:46 +00:00
David Baker c94382b46c A unit test for olm
Megolm has plenty but none for just the olm layer by itself. I
was playing with getting session re-establishment to work and a
unit test came out.
2018-11-02 12:05:13 +00:00
David Baker 2a6a67c6cc Inbound session creation counts as a received message 2018-11-01 17:08:43 +00:00
David Baker 37f0a9ad7b Try tests on node 10 2018-11-01 13:54:41 +00:00
David Baker 28540ad50a Use the last olm session that got a message
Implements https://github.com/matrix-org/matrix-doc/pull/1596
For https://github.com/vector-im/riot-web/issues/3822
Requires https://github.com/matrix-org/olm-backup/pull/77 (+release)
2018-11-01 12:59:38 +00:00
David Baker 29d92d3e81 Lint 2018-10-31 20:05:21 +00:00
David Baker 0477f354c9 Fix key forwarded count
It's exported in snake case
2018-10-31 19:45:29 +00:00
David Baker c7a0c1402c refer to getAllEndToEndInboundGroupSessions for magic numbers 2018-10-31 19:39:07 +00:00
David Baker 2af5643243 Clarify comment 2018-10-31 19:37:19 +00:00
David Baker 5e9885946f random double linebreak 2018-10-31 19:36:30 +00:00
Travis Ralston e89879d8a6 Remove the request-only stuff we don't need anymore
This was introduced in https://github.com/matrix-org/matrix-react-sdk/pull/2250 but can be pulled out due to https://github.com/matrix-org/matrix-js-sdk/pull/770. See https://github.com/vector-im/riot-web/issues/7634 for more information about the future.
2018-10-31 13:07:31 -06:00
David Baker 2f219f83db Catch exceptions from backupGroupSession() 2018-10-31 18:46:02 +00:00
David Baker 63e9f794c7 Remove unnecessary if 2018-10-31 18:37:38 +00:00
David Baker 7c0b910d7a remove unnecessary isFinite check 2018-10-31 18:36:55 +00:00
David Baker c77ecad9a5 clarify comment 2018-10-31 18:34:49 +00:00
David Baker db2897cf1e Remove spurious interlopers 2018-10-31 18:33:31 +00:00
David Baker 5c5ce0dfe3 Typo 2018-10-31 18:32:48 +00:00
David Baker 6de213483c Change getDeviceByIdentityKey() to just the 2 arg version 2018-10-31 18:31:56 +00:00
David Baker f5846b89ea More modern loop syntax 2018-10-31 18:18:41 +00:00
David Baker c5e7bedb37 Conclusion: no, it shouldn't 2018-10-31 18:08:53 +00:00
David Baker 2b46c560c7 Add crypto. prefix to keyBackupStatus event 2018-10-31 18:07:12 +00:00
David Baker c6ad0665b5 factor out duplicated test code 2018-10-31 18:03:40 +00:00
David Baker 8ab84dee16 PR feedback 1/n 2018-10-31 17:40:17 +00:00
Travis Ralston 62b2c07be2 Merge pull request #770 from matrix-org/travis/fix-request
Manually construct query strings for browser-request instances
2018-10-30 15:18:33 -06:00
Travis Ralston 2fb29ae8fd Use the right query string lib 2018-10-30 14:59:57 -06:00
Travis Ralston b57e858ad1 We can't use arrow functions, apparently. 2018-10-30 14:27:51 -06:00
Travis Ralston 054aac17aa Just use the interface provided to us rather than hack in a flag 2018-10-30 14:25:25 -06:00
Travis Ralston 68b65dd357 Because uglify-js breaks everything 2018-10-30 14:12:46 -06:00
Travis Ralston f2881126cd Manually construct query strings for browser-request instances
Because `request` just doesn't work for us in the browser, but `browser-request` is fine despite us having to do our own query strings.

Fixes https://github.com/vector-im/riot-web/issues/7620
2018-10-30 13:59:29 -06:00
Bruno Windels 11968a5888 Merge pull request #769 from matrix-org/bwindels/initcryptoll
Fix: correctly check for crypto being present
2018-10-30 16:13:32 +00:00
Bruno Windels ad279dc566 correctly check for crypto being present 2018-10-30 16:05:44 +01:00
David Baker 2814932845 lint 2018-10-30 12:36:03 +00:00
David Baker a2430dbc53 Fix DeviceList index of users by identity key
Was causing all keys to be send as unverified
2018-10-30 12:29:44 +00:00
David Baker e51d2dd36a Fix a few e2e backup bits
* Don't _maybeSendKeyBackup() as soon as we enable them: we shouldn't
   have anything to send anyway until we mark all sessions for backup,
   which we do just afterwards, so leave that to trigger the upload
   (otherwise the uploading triggered by backupAll just returns
   straight away because a backup is already in progress).
 * Pass delay & retry params to _maybeSendKeyBackup(): we want the
   all-key upload to happen straight away so pass in delay=0, and
   we also don't want to retry on a timer if the the user is waiting.
 * If we fail due to an HTTP 400 or similar, don't swallow the error.
 * Use the right indexeddb store
2018-10-30 11:45:19 +00:00
David Baker 604af1ac8c Merge branch 'master' into develop 2018-10-29 14:01:26 +00:00
David Baker 68c6393eb2 v0.12.1 2018-10-29 13:59:39 +00:00
David Baker 4cbf9c7f47 Prepare changelog for v0.12.1 2018-10-29 13:59:39 +00:00
Travis Ralston 8bb3b75b1d Merge pull request #768 from aaronraimist/lint
Update babel-eslint to 8.1.1
2018-10-26 21:13:04 -06:00
Aaron Raimist a76f0c7cb4 Update babel-eslint to 8.1.1
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2018-10-26 22:07:40 -05:00
Travis Ralston 01e31afcbd Merge pull request #764 from matrix-org/travis/permalink-routing
Support `request` in the browser and support supplying servers to try in joinRoom()
2018-10-26 14:22:51 -06:00
David Baker 3b2f2f922e Bump db version 2018-10-26 18:49:05 +01:00
Travis Ralston 64b83b3245 Merge branch 'develop' into travis/permalink-routing 2018-10-26 09:20:54 -06:00
David Baker 563e6b3cdd Fix jsdoc 2018-10-26 13:23:37 +01:00
David Baker 6518bff2ac Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-26 12:09:38 +01:00
David Baker 0e26247b53 Speed up time rather than increasing timeouts 2018-10-26 12:08:55 +01:00
David Baker e69f7dbc5f Merge pull request #767 from matrix-org/dbkr/loglevel_dep
loglevel should be a normal dependency
2018-10-26 11:26:15 +01:00
David Baker 4d0f6df89a alphabetical order 2018-10-26 11:23:05 +01:00
David Baker 6b184363a1 loglevel should be a normal dependency
rather than a dev dependency
2018-10-26 11:16:20 +01:00
David Baker b519069634 Merge pull request #766 from matrix-org/dbkr/stop_devicelist
Stop devicelist when client is stopped
2018-10-26 11:14:36 +01:00
Travis Ralston 1bd44a7427 Merge remote-tracking branch 'origin/develop' into travis/permalink-routing 2018-10-25 14:36:46 -06:00
Travis Ralston 568ff5a3f5 Appease the linter 2018-10-25 14:36:14 -06:00
David Baker a6bf40d4e2 We can always import these now 2018-10-25 19:21:29 +01:00
David Baker b3bb99d76a Stop client after backup tests 2018-10-25 19:11:43 +01:00
David Baker 243bab7036 Merge branch 'dbkr/stop_devicelist' into dbkr/e2e_backups 2018-10-25 19:03:57 +01:00
David Baker 88b39f4b67 Stop devicelist when client is stopped
To avoid the devicelist trying to save after the client has been
stopped

Hopefully will fix random test failures on node 11.
2018-10-25 19:00:03 +01:00
David Baker 5e8061f846 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-25 17:33:48 +01:00
David Baker 870e96a1df Merge pull request #743 from matrix-org/dbkr/wasm
Update to WebAssembly-powered Olm
2018-10-25 15:58:18 +01:00
David Baker 59070c2af6 Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-25 15:29:41 +01:00
David Baker cec8936728 Merge pull request #763 from Ryuno-Ki/logging-lib
Logging lib. Fixes #332
2018-10-25 15:28:47 +01:00
David Baker 14071b0d31 Merge pull request #765 from matrix-org/dbkr/mock_request_stop
Use new stop() method on matrix-mock-request
2018-10-25 15:16:08 +01:00
David Baker 57173e4385 Use mock-request 1.2.2 2018-10-25 15:01:05 +01:00
David Baker 997caad985 jsdoc 2018-10-25 14:43:17 +01:00
David Baker 2b752c0c02 Use new stop() method on matrix-mock-request
To finish all pending flushes between tests. This stops the unit
tests from hanging on node 11 when run in certain combinations.

Requires https://github.com/matrix-org/matrix-mock-request/pull/6
(so will need a release of matrix-mock-request before merging)
2018-10-25 14:29:25 +01:00
Travis Ralston 2cccb8b450 Install memfs because webpack is made of fail 2018-10-24 16:57:36 -06:00
Travis Ralston 0c540ac8de Re-add the querystring options 2018-10-24 16:36:12 -06:00
Travis Ralston 6033b7b886 Update request and browserify; Use request in the browser 2018-10-24 16:36:00 -06:00
Travis Ralston b67f8d1389 Merge branch 'develop' into travis/permalink-routing 2018-10-24 16:22:18 -06:00
André Jaenisch ae645ad9f0 Use Node.js module export, since ES6 export breaks build.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:52:50 +02:00
André Jaenisch 5b72509dac Fix broken build.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:12:51 +02:00
André Jaenisch 3ce42a096b Add Apache license banner.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:06:04 +02:00
André Jaenisch 8331c2f267 Use ES6 export instead of CommonJS.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:05:21 +02:00
André Jaenisch b3c9570b0f Remove 'use strict'.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 20:56:29 +02:00
André Jaenisch 9d5c877df9 Set loglevel to DEBUG to remain current behaviour.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 20:55:54 +02:00
André Jaenisch a8e2727473 Set level of logger instead of all of them.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 20:55:22 +02:00
David Baker 4b9c6e6bd2 Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-24 19:15:04 +01:00
David Baker d29ac088c0 retest 2018-10-24 18:55:04 +01:00
David Baker 3a316de9ef Update to Olm 3 here too 2018-10-24 17:40:58 +01:00
David Baker 40cb37e824 Update to Olm 3 2018-10-24 17:37:33 +01:00
David Baker f165b55a1d Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups 2018-10-24 17:15:36 +01:00
David Baker 84b91d4575 Update to Olm 3 2018-10-24 16:58:48 +01:00
David Baker f5832423f4 v0.12.1-rc.1 2018-10-24 11:14:59 +01:00
David Baker 73dd07aadf Prepare changelog for v0.12.1-rc.1 2018-10-24 11:14:59 +01:00
André Jaenisch 0f39a45734 Fixing module export of logger. Refs #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 01:44:10 +02:00
André Jaenisch f41060c39a Replace console.log with loglevel logger. Fixes #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 00:48:57 +02:00
André Jaenisch bbb8e12bac Create logger module. Refs #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 00:48:57 +02:00
André Jaenisch d0e1471c91 Added loglevel library. Refs #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 00:48:52 +02:00
Hubert Chathi 322ef1fd63 update backup algorithm name to agree with the proposal 2018-10-22 11:28:16 -04:00
Travis Ralston 47cb97bc60 Merge pull request #762 from aaronraimist/valid-package-json
Add repository type to package.json to make it valid
2018-10-20 21:55:48 -06:00
Aaron Raimist 8d35bea830 Add repository type to package.json to make it valid
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2018-10-20 21:38:04 -05:00
Travis Ralston d8bcc4e3f1 Initial support for specifying which servers to try in joinRoom
This has a bug when using browser-request where the query string for `server_name: [a, b]` comes out as `?server_name=a,b` instead of `?server_name=a&server_name=b`. This is due to browser-request not supporting the same qs options as request, so the qsStringifyOptions do nothing.
2018-10-19 13:34:22 -06:00
Hubert Chathi 434ac86090 properly fill out the is_verified and first_message_index fields 2018-10-19 10:51:19 -04:00
Travis Ralston 1061026f96 Merge pull request #761 from Half-Shot/hs/upload-limits
Add getMediaConfig()
2018-10-16 09:04:08 -06:00
Will Hunt e638c49160 Merge remote-tracking branch 'upstream/develop' into hs/upload-limits 2018-10-16 11:32:21 +01:00
David Baker 5d84db9fb7 Merge branch 'release-v0.12.0' 2018-10-16 10:49:14 +01:00
David Baker 874bdea634 v0.12.0 2018-10-16 10:47:33 +01:00
David Baker 68497d3a1f Prepare changelog for v0.12.0 2018-10-16 10:47:32 +01:00
David Baker b9e198c172 Oops: remove debug logging 2018-10-15 11:39:39 +01:00
Hubert Chathi 40d0a82342 remove accidental change to eslintrc 2018-10-12 15:45:48 -04:00
Hubert Chathi d49c0a1bcb more de-linting and fixing 2018-10-12 14:28:31 -04:00
Hubert Chathi 91fb7b0a7c fix unit tests for backup recovery 2018-10-12 12:03:51 -04:00
Hubert Chathi 9b12c22823 de-lint plus some minor fixes 2018-10-12 10:38:10 -04:00
Hubert Chathi 3957006fae Merge remote-tracking branch 'upstream/dbkr/e2e_backups' into e2e_backups 2018-10-11 14:01:26 -04:00
David Baker 874029dff0 oops - fix changelog format in retrospect 2018-10-11 15:38:34 +01:00
David Baker 6aff3ed407 v0.12.0-rc.1 2018-10-11 14:42:29 +01:00
David Baker c0ae78ae82 Prepare changelog for v0.12.0-rc.1 2018-10-11 14:42:29 +01:00
David Baker 8b22f01ecd Merge pull request #760 from matrix-org/bwindels/fixleavesinmemberlist
never replace /sync'ed memberships with OOB ones
2018-10-11 14:01:38 +01:00
Bruno Windels 2ed694b041 remove supersedes OOB logic 2018-10-11 14:32:03 +02:00
Bruno Windels a0ef6ab811 typo 2018-10-11 14:14:42 +02:00
Bruno Windels d098b39024 never replace /sync'ed memberships with OOB ones 2018-10-11 14:11:40 +02:00
David Baker 3cf23f8a5c Document breaking change 2018-10-11 12:13:25 +01:00
Hubert Chathi fc59bc2992 add localstorage support for key backups 2018-10-10 19:32:07 -04:00
Hubert Chathi da65f43983 wrap backup sending in a try, and add delays 2018-10-10 19:31:28 -04:00
Bruno Windels 72e77d237a Merge pull request #759 from matrix-org/dbkr/lazy_load_check_before_sync
Don't fail to start up if lazy load check fails
2018-10-10 18:28:47 +02:00
David Baker ecc3e18e85 typo 2018-10-10 17:27:06 +01:00
David Baker dea70af889 remove debug logging 2018-10-10 17:06:26 +01:00
David Baker 30362091e5 Don't fail to start up if lazy load check fails
Do the lazy loading check in the batch of things we do before
starting a sync rather than at client start time, so we don't fail
to start the client if we can't hit the HS to determine LL support.

Fixes https://github.com/vector-im/riot-web/issues/7455
2018-10-10 16:59:36 +01:00
David Baker ada4b6ef16 Lint 2018-10-09 15:46:12 +01:00
David Baker 59e6066579 Replace base58check with a simple parity check
base58check seems way overcomplicated for this purpose (plus the
module was exporting an es6 file, breaking the js-sdk build). A
parity check empirically detects single substitution and transposition
errors. Another option would be Luhn's algorithm.
2018-10-09 14:15:03 +01:00
David Baker 0aa3362671 Merge pull request #754 from matrix-org/dbkr/e2e_on_interslice
Make e2e work on Edge
2018-10-09 10:48:24 +01:00
David Baker 5873db7331 Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-09 10:47:11 +01:00
David Baker b3fe05ec81 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-09 10:35:15 +01:00
David Baker 92fbf58b13 Merge pull request #758 from matrix-org/bwindels/betteridberrors
throw error with same name and message over idb worker boundary
2018-10-09 09:59:55 +01:00
Bruno Windels a4b2cc84c7 Merge pull request #755 from matrix-org/travis/fix-vuln-warning
Default to a room version of 1 when there is no room create event
2018-10-08 09:55:10 +02:00
David Baker 89c3f6fa0e Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-05 14:01:43 +01:00
David Baker 1395da694e Merge pull request #757 from matrix-org/dbkr/gone_away_is_the_bluebird
Silence bluebird warnings
2018-10-05 13:25:16 +01:00
David Baker 264b20535e Silence bluebird warnings 2018-10-05 12:13:05 +01:00
Bruno Windels caba350b33 throw error with same name and message over idb worker boundary
instead of string currently thrown. This allows handling error
from the main thread.
2018-10-05 12:48:12 +02:00
Travis Ralston d9fe194111 Default to a room version of 1 when there is no room create event
Fixes https://github.com/vector-im/riot-web/issues/7331

There is something to be worried about when there is no room create event, however. This should always be available, although due to cache problems or servers that don't provide the event we can't be sure of this.
2018-10-04 13:42:07 -06:00
Hubert Chathi 258adda67c retry key backups when they fail 2018-10-04 15:19:20 -04:00
David Baker 40dc13b2e2 lint try 2 2018-10-04 15:38:08 +01:00
David Baker 4cda54ca1c lint 2018-10-04 15:15:30 +01:00
David Baker 8116c5b3f7 Make e2e work on Edge
We were sucessfully opening indexeddb but any queries using compound
indicies were failing because Edge doesn't support them, so messages
were failing to decrypt with 'DataError'.

Try a dummy query at startup, so if it fails we fall back to a
different store (ie. end up using localstorage on Edge).
2018-10-04 13:49:32 +01:00
David Baker 35d584c67b Remove outdated comment 2018-10-04 13:05:45 +01:00
David Baker 9504cbcc4f Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-04 11:19:37 +01:00
Bruno Windels 1dcc5127d0 Merge pull request #750 from matrix-org/bwindels/allownonffmerge
allow non-ff merge from release branch into master
2018-10-04 10:22:02 +02:00
Bruno Windels 6790699279 Merge pull request #739 from matrix-org/ben/new-examples
add new examples, to be expanded into a post
2018-10-04 10:21:36 +02:00
David Baker 85e3d7083c Merge pull request #751 from matrix-org/dbkr/indexeddb_errors
Reject with the actual error on indexeddb error
2018-10-03 16:22:26 +01:00
David Baker 262ace1773 commit the recovery key util file 2018-10-03 10:20:57 +01:00
David Baker 7cd101d8cb Fix recovery key format 2018-10-02 19:22:10 +01:00
David Baker ce2058aea9 Merge branch 'dbkr/wasm' into dbkr/e2e_backups 2018-10-02 16:54:36 +01:00
David Baker e9b0acaa8e Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-02 16:50:37 +01:00
David Baker bd2da08c4e Reject with the actual error on indexeddb error
Rather than the event
2018-10-02 16:48:27 +01:00
Bruno Windels 0a88d419c6 allow non-ff merge from release branch into master 2018-10-01 17:21:01 +02:00
Bruno Windels 80c190db36 Merge branch 'master' into develop 2018-10-01 15:40:16 +02:00
Bruno Windels 550cf00ee7 Merge branch 'release-v0.11.1' 2018-10-01 15:36:37 +02:00
Ben Parsons fbca7951fc improvements suggested by Bruno 2018-10-01 14:29:24 +01:00
Bruno Windels 1e1358fcef v0.11.1 2018-10-01 15:23:37 +02:00
Bruno Windels fd1b3329f5 Prepare changelog for v0.11.1 2018-10-01 15:23:37 +02:00
Bruno Windels 9c9d8468a5 Merge pull request #744 from matrix-org/dbkr/update_mocha
Update mocha to v5
2018-09-28 12:57:49 +01:00
Bruno Windels 55ca03f100 make release compatible with latest release of hub (2.5) 2018-09-28 12:51:31 +02:00
Bruno Windels 83708725b2 check youre logged in with correct npm user when releasing 2018-09-28 11:16:00 +02:00
David Baker 6f59d62e5c Merge pull request #748 from matrix-org/bwindels/nollforguests
disable lazy loading for guests as they cant create filters
2018-09-27 19:29:54 +01:00
Bruno Windels 1c348f0cdb disable lazy loading for guests as they cant create filters 2018-09-27 18:55:21 +01:00
Bruno Windels 634596257d v0.11.1-rc.1 2018-09-27 11:47:28 +01:00
Bruno Windels 5e4973a1dc changelog 2018-09-27 11:45:33 +01:00
Bruno Windels 19f023e0ee Revert "v0.11.1-rc.1"
This reverts commit 090c15fe19.
2018-09-27 11:42:19 +01:00
Bruno Windels 090c15fe19 v0.11.1-rc.1 2018-09-27 11:38:25 +01:00
Bruno Windels e8b307dc4f Prepare changelog for v0.11.1-rc.1 2018-09-27 11:38:25 +01:00
Bruno Windels 056479d450 Revert "v0.11.1-rc.1"
This reverts commit 847d40e567.
2018-09-27 11:35:52 +01:00
Bruno Windels e5ebe2f888 Merge pull request #747 from matrix-org/bwindels/releasehubversion
make usage of hub compatible with latest version (2.5)
2018-09-27 11:30:17 +01:00
Bruno Windels e8e1b431ad make usage of hub compatible with latest version (2.5) 2018-09-27 11:28:01 +01:00
Bruno Windels 847d40e567 v0.11.1-rc.1 2018-09-27 11:20:21 +01:00
Bruno Windels cf6c555e6a Prepare changelog for v0.11.1-rc.1 2018-09-27 11:20:21 +01:00
Bruno Windels b508aa9ebc Merge pull request #746 from matrix-org/bwindels/resynconlltoggle
Detect when lazy loading has been toggled in client.startClient
2018-09-27 09:49:01 +01:00
Bruno Windels 5e7634506e Merge branch 'develop' into bwindels/resynconlltoggle 2018-09-26 18:23:07 +01:00
Bruno Windels ba39b64ced re-enable test 2018-09-26 18:00:40 +01:00
David Baker 33ad65a105 Don't assume Olm will be available from start
By doing `Olm = global.Olm` on script load, we require that Olm is
available right from the start, which isn't really necessary. As
long as it appears some time before we actually want to use it,
this is fine (we can probably assume it's not going to go away
again..?)

This means Riot doesn't need to faff about making sure olm is
loaded before starting anything else.
2018-09-26 16:39:22 +01:00
Travis Ralston fcebe89f6c Merge branch 'hs/upload-limits' into develop 2018-09-26 09:14:12 -06:00
Bruno Windels 2d5eb920b8 pass lazy loading flag into error, to format message based on it 2018-09-26 16:12:30 +01:00
Travis Ralston 26de2c86ed Merge pull request #745 from matrix-org/revert-644-hs/upload-limits
Revert "Add getMediaLimits to client"
2018-09-26 09:12:03 -06:00
Travis Ralston cba1e95d0a Revert "Add getMediaLimits to client" 2018-09-26 09:11:28 -06:00
Bruno Windels 78a5a88638 fix jsdoc lint and better naming 2018-09-26 12:49:33 +01:00
Bruno Windels b7b9c67259 fix lint 2018-09-26 12:49:26 +01:00
Bruno Windels 54bff81470 clear sync data on toggling LL,also throw spec. error and delegate clear
the sync data needs to be cleared toggling in both directions:
 not LL -> LL: you want to get rid of all the excess state in the
               sync data to get the RAM benefits
 LL -> not LL: you want to fill the sync data state again
               because getOutOfBandMembers won't be called
2018-09-26 12:39:56 +01:00
David Baker fe21972f4a Update mocha to v5
Mostly to get the non-vulnerable version of node-growl
2018-09-26 11:36:06 +01:00
Bruno Windels 58e3c72446 only store serializable options (string, boolean, number) 2018-09-26 11:34:58 +01:00
Bruno Windels 6dd5c6c317 fix existing missing this 2018-09-26 11:33:19 +01:00
Bruno Windels 4e0af3eafe don't return the IDBEvent from storeClientOptions
as it's not needed and not cloneable
2018-09-26 11:32:43 +01:00
Bruno Windels 1d0791142c all store methods should return a promise 2018-09-26 11:32:11 +01:00
Bruno Windels 2560ba2980 dont clear the store if its a brand new one 2018-09-26 10:37:52 +01:00
Bruno Windels 19be3dd852 fix lint 2018-09-26 10:13:40 +01:00
Travis Ralston 40f2a6978b Merge pull request #644 from Half-Shot/hs/upload-limits
Add getMediaLimits to client
2018-09-25 18:12:55 -06:00
Bruno Windels 1fd8c43d94 fix tests 2018-09-25 18:50:09 +01:00
David Baker 63cc3fd890 lint 2018-09-25 18:14:11 +01:00
David Baker c556ca40b1 Support Olm with WebAssembly
wasm Olm has a new interface: it now has an init method that needs
to be called and the promise it returns waited on before the Olm
module is used. Support that, and allow Crypto etc to be imported
whether Olm is enabled or not. Change whether olm is enabled to
be async since now it will be unavailable if the async module init
fails. Don't call getOlmVersion() until the Olm.init() is done.
2018-09-25 17:49:54 +01:00
Matthew Hodgson 3e32f47903 Merge pull request #742 from matrix-org/travis/build-process
Split npm start into an init and watch script
2018-09-25 17:31:23 +01:00
Travis Ralston 8f2824186a Split npm start into an init and watch script
This is to better support riot-web's build process without losing the functionality supplied by `npm start`. The watch script no longer performs an initial build and thus `start:init` has been created for this purpose.
2018-09-25 10:09:58 -06:00
Bruno Windels b0dbb20e22 fixup of in memory stores 2018-09-25 15:53:40 +01:00
Bruno Windels 0519c4c6b1 await startClient and use promises also so error gets shown 2018-09-25 15:53:14 +01:00
Bruno Windels 28184b4a29 check if lazy loading was enabled before in startClient 2018-09-25 15:32:10 +01:00
Bruno Windels 76175abea2 allow storing client options in indexeddb
so we can tell what options the sync data was created with
2018-09-25 15:30:35 +01:00
Ben Parsons d28f829b1c add new examples, to be expanded into a post 2018-09-20 13:12:50 +01:00
Bruno Windels 184c3dce15 Merge pull request #738 from matrix-org/revert-733-bwindels/roomnamealias
Revert "room name should only take canonical alias into account"
2018-09-20 12:23:38 +02:00
David Baker a08a3078da Revert "room name should only take canonical alias into account" 2018-09-20 11:20:49 +01:00
David Baker c2100d7622 Merge pull request #737 from matrix-org/bwindels/fixnamedisambiguation
fix display name disambiguation with LL
2018-09-20 10:22:38 +01:00
Bruno Windels a91fa59174 fix display name disambiguation with LL 2018-09-19 18:14:18 +02:00
Bruno Windels 574a6b68ae Merge pull request #735 from matrix-org/bwindels/fixstalerr
Introduce Room.myMembership event
2018-09-19 13:35:11 +02:00
David Baker 2f4c1dfcc4 Test all 3 code paths on backup restore 2018-09-18 17:33:47 +01:00
David Baker 1b62a21dbd Free PkEncryption/Decryption objects 2018-09-18 16:12:37 +01:00
David Baker a78825eff9 Bump to Olm 2.3.0 for PkEncryption 2018-09-18 15:06:28 +01:00
David Baker 0bad7b213e Fix lint
Remove commented code block as it's not immediately obvious it makes
sense or is the right way of suggesting a key restore.
2018-09-18 14:56:11 +01:00
David Baker e4bb37b1a8 Fix lint mostly 2018-09-18 14:53:59 +01:00
David Baker 54c443ac68 Make tests pass 2018-09-18 14:48:02 +01:00
David Baker 3af9af96ea More linting 2018-09-17 19:31:37 +01:00
David Baker f75d188131 Soe progress on linting 2018-09-17 19:25:42 +01:00
Bruno Windels fc3a00054f add test for new event 2018-09-17 19:33:36 +02:00
Bruno Windels 84e41c2ade fix tests 2018-09-17 18:28:07 +02:00
Bruno Windels 4630733b55 don't fall back anymore to member, as this is more reliable 2018-09-17 18:23:48 +02:00
Bruno Windels eb690e14e4 introduce Room.myMembership event
As you don't always have your own member with lazy loading
of members enabled, looking at the sync response section
where a room appears is the most reliable way of determining
the syncing user's membership in a room.

Before we already used this to read the
current room membership with `room.getMyMembership()`,
but we were still using the `RoomMember.membership` event
to detect when the syncing user's membership changed.

This event will help make those checks work well with LL enabled.
2018-09-17 18:20:49 +02:00
David Baker 009430e829 Add isValidRecoveryKey
Add method to check if a given string is a valid recovery key
2018-09-17 17:04:29 +01:00
David Baker 073fb73ff3 Make multi-room key restore work 2018-09-17 15:59:37 +01:00
David Baker e789747834 Check sigs on e2e backup & enable it if we can 2018-09-14 17:06:27 +01:00
David Baker 833002f846 Merge pull request #733 from matrix-org/bwindels/roomnamealias
room name should only take canonical alias into account
2018-09-14 11:38:26 +01:00
David Baker 3838fab788 WIP e2e key backup support
Continues from uhoreg's branch
2018-09-13 17:01:05 +01:00
Bruno Windels 907e9fc476 fix lint 2018-09-13 10:04:31 +02:00
Bruno Windels b829a39cd2 fix tests 2018-09-13 09:59:20 +02:00
Bruno Windels daa7af0605 room name should only take canonical alias into account 2018-09-13 09:52:21 +02:00
David Baker 47a1adc864 Merge pull request #732 from matrix-org/bwindels/fixcontextstatenotwrapped
state events from context response were not wrapped in a MatrixEvent
2018-09-11 13:27:03 +01:00
Bruno Windels 98e448acdd state events from context response were not wrapped in a MatrixEvent 2018-09-11 14:13:35 +02:00
David Baker 72bd51f26e Merge remote-tracking branch 'origin/develop' into uhoreg-e2e_backups 2018-09-11 12:02:47 +01:00
David Baker 29db856322 Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups 2018-09-11 12:00:46 +01:00
David Baker cd5cda916f Merge branch 'master' into develop 2018-09-10 11:35:25 +01:00
David Baker b809d1c263 Merge pull request #724 from matrix-org/bwindels/fastermemberinsert
Reduce amount of promises created when inserting members
2018-09-07 13:57:44 +01:00
David Baker 7bb6675abf Merge pull request #726 from matrix-org/bwindels/dontwaitforstorage
dont wait for LL members to be stored to resolve the members
2018-09-07 13:57:20 +01:00
David Baker b91bea94f4 Merge pull request #728 from matrix-org/bwindels/wrongemitargs
RoomState.members emitted with wrong argument order for OOB members
2018-09-07 13:57:09 +01:00
Bruno Windels 9e02049b05 RoomState.members emitted with wrong argument order for OOB members 2018-09-07 14:11:27 +02:00
Bruno Windels ba4dc6c60a dont wait for LL members to be stored to resolve the members
this can easily add up to 100ms / 1000 of members
2018-09-07 10:44:06 +02:00
Bruno Windels 1511a27f4c update/remove comments 2018-09-06 18:09:46 +02:00
Bruno Windels 7ae6c147fa lint doesnt like async 2018-09-06 18:02:19 +02:00
Bruno Windels f51630eb07 dont create a promise for every inserted member but await the transaction instead 2018-09-06 18:01:20 +02:00
Hubert Chathi bf873bde42 split the backup version creation into two different methods 2018-08-24 22:13:13 -04:00
Hubert Chathi 017f81e430 fix some bugs 2018-08-24 16:39:22 -04:00
David Baker ec5fff2046 Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups 2018-08-24 13:29:29 +01:00
Hubert Chathi 73e294b1bd add copyright header to backup.spec 2018-08-23 00:29:29 -04:00
Hubert Chathi e5ec479923 check that crypto is enabled 2018-08-23 00:27:30 -04:00
Hubert Chathi 75107f99b2 pass in key rather than decryption object to restoreKeyBackups 2018-08-23 00:26:21 -04:00
Hubert Chathi fb8efe368a initial draft of API for working with backup versions 2018-08-23 00:03:36 -04:00
Hubert Chathi 1faf477537 fix formatting and fix authedRequest usage 2018-08-22 23:58:59 -04:00
Hubert Chathi d55618921b initial implementation of e2e key backup and restore 2018-08-07 23:10:55 -04:00
Michael Telatynski b34716f7e9 take into account homoglyphs when calculating similar display names
to prevent homoglyph attacks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-07-26 14:55:35 +01:00
Will Hunt aec7ef6f9c getMediaLimits -> getMediaConfig 2018-06-23 14:38:39 +01:00
Will Hunt 329f09ce0a Media/limits => /config 2018-06-23 14:38:39 +01:00
Will Hunt 68c23af5ae Remove extra return 2018-06-23 14:38:39 +01:00
Will Hunt a54f30c02f Add getMediaLimits to client 2018-06-23 14:38:39 +01:00
Will Hunt fde00b1c62 getMediaLimits -> getMediaConfig 2018-06-23 12:57:29 +01:00
Will Hunt 8dd425f8ff Media/limits => /config 2018-06-20 17:24:45 +01:00
Matthew Hodgson 69204d4fb3 Merge branch 'develop' into matthew/e2e_backups 2018-05-28 00:44:49 +01:00
Leon 9cdcbf6bf8 emit oldEventId on "updatePendingEvent"
It should fire `Room.localEchoUpdated` event and returns the oldEventId
2018-05-22 11:27:05 +08:00
Will Hunt 9596087959 Remove extra return 2018-05-03 17:57:57 +01:00
Will Hunt 6570402b95 Add getMediaLimits to client 2018-05-03 13:43:37 +01:00
Matthew Hodgson e0c9b990e7 blindly move crypto.suggestKeyRestore over to /sync 2018-01-18 20:59:08 +00:00
Matthew Hodgson fb1b554b86 initial pseudocode WIP for e2e online backups 2018-01-15 01:50:24 +00:00
Johannes Bornhold 81de2b3afc Add getThirdpartyUser
Signed-off-by: Johannes Bornhold <johannes@bornhold.name>
2017-12-31 22:46:18 +01:00
Johannes Bornhold 9a53fa3876 Fix typo around getThirdpartyLocation 2017-12-31 01:24:27 +01:00
68 changed files with 11474 additions and 599 deletions
+7 -2
View File
@@ -1,7 +1,6 @@
module.exports = {
parser: "babel-eslint",
parserOptions: {
ecmaVersion: 6,
ecmaVersion: 2017,
sourceType: "module",
ecmaFeatures: {
}
@@ -67,5 +66,11 @@ module.exports = {
"padded-blocks": ["warn"],
"no-extend-native": ["warn"],
"camelcase": ["warn"],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always",
}],
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
- "10.11.0"
script:
- ./travis.sh
+236
View File
@@ -1,3 +1,239 @@
Changes in [0.14.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.3) (2019-01-22)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.3-rc.1...v0.14.3)
* No changes since rc.1
Changes in [0.14.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.3-rc.1) (2019-01-17)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.2...v0.14.3-rc.1)
* Merge develop into experimental
[\#815](https://github.com/matrix-org/matrix-js-sdk/pull/815)
* Add a getAllEndToEndSessions to crypto store
[\#812](https://github.com/matrix-org/matrix-js-sdk/pull/812)
* T3chguy/fix displayname logic
[\#668](https://github.com/matrix-org/matrix-js-sdk/pull/668)
* Contributing: Note that rebase lets you mass signoff commits
[\#814](https://github.com/matrix-org/matrix-js-sdk/pull/814)
* take into account homoglyphs when calculating similar display names
[\#672](https://github.com/matrix-org/matrix-js-sdk/pull/672)
* Emit for key backup failures
[\#809](https://github.com/matrix-org/matrix-js-sdk/pull/809)
* emit oldEventId on "updatePendingEvent"
[\#646](https://github.com/matrix-org/matrix-js-sdk/pull/646)
* Add getThirdpartyUser to base api
[\#589](https://github.com/matrix-org/matrix-js-sdk/pull/589)
* Support custom status messages
[\#805](https://github.com/matrix-org/matrix-js-sdk/pull/805)
* Extra checks to avoid release script blowing up mid-process.
[\#749](https://github.com/matrix-org/matrix-js-sdk/pull/749)
* Move glob regex utilities out of the pushprocessor and into a more generic
place
[\#800](https://github.com/matrix-org/matrix-js-sdk/pull/800)
Changes in [0.14.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.2) (2018-12-10)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.2-rc.1...v0.14.2)
* No changes since rc.1
Changes in [0.14.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.2-rc.1) (2018-12-06)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.1...v0.14.2-rc.1)
* fix some assertions in e2e backup unit test
[\#794](https://github.com/matrix-org/matrix-js-sdk/pull/794)
* Config should be called with auth
[\#798](https://github.com/matrix-org/matrix-js-sdk/pull/798)
* Don't re-establish sessions with unknown devices
[\#792](https://github.com/matrix-org/matrix-js-sdk/pull/792)
* e2e key backups
[\#684](https://github.com/matrix-org/matrix-js-sdk/pull/684)
* WIP: online incremental megolm backups
[\#595](https://github.com/matrix-org/matrix-js-sdk/pull/595)
* Support for e2e key backups
[\#736](https://github.com/matrix-org/matrix-js-sdk/pull/736)
* Passphrase Support for e2e backups
[\#786](https://github.com/matrix-org/matrix-js-sdk/pull/786)
* Add 'getSsoLoginUrl' function
[\#783](https://github.com/matrix-org/matrix-js-sdk/pull/783)
* Fix: don't set the room name to null when heroes are missing.
[\#784](https://github.com/matrix-org/matrix-js-sdk/pull/784)
* Handle crypto db version upgrades
[\#785](https://github.com/matrix-org/matrix-js-sdk/pull/785)
* Restart broken Olm sessions
[\#780](https://github.com/matrix-org/matrix-js-sdk/pull/780)
* Use the last olm session that got a message
[\#776](https://github.com/matrix-org/matrix-js-sdk/pull/776)
Changes in [0.14.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.1) (2018-11-22)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.0...v0.14.1)
* Warning when crypto DB is too new to use.
Changes in [0.14.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.0) (2018-11-19)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.0-rc.1...v0.14.0)
* No changes since rc.1
Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.0-rc.1) (2018-11-15)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.13.1...v0.14.0-rc.1)
BREAKING CHANGE
----------------
* js-sdk now uses Olm 3.0. Apps using Olm must update to 3.0 to
continue using Olm with the js-sdk. The js-sdk will call Olm's
init() method when the client is started.
All Changes
-----------
* Prevent messages from being sent if other messages have failed to send
[\#781](https://github.com/matrix-org/matrix-js-sdk/pull/781)
* A unit test for olm
[\#777](https://github.com/matrix-org/matrix-js-sdk/pull/777)
* Set access_token and user_id after login in with username and password.
[\#778](https://github.com/matrix-org/matrix-js-sdk/pull/778)
* Add function to get currently joined rooms.
[\#779](https://github.com/matrix-org/matrix-js-sdk/pull/779)
* Remove the request-only stuff we don't need anymore
[\#775](https://github.com/matrix-org/matrix-js-sdk/pull/775)
* Manually construct query strings for browser-request instances
[\#770](https://github.com/matrix-org/matrix-js-sdk/pull/770)
* Fix: correctly check for crypto being present
[\#769](https://github.com/matrix-org/matrix-js-sdk/pull/769)
* Update babel-eslint to 8.1.1
[\#768](https://github.com/matrix-org/matrix-js-sdk/pull/768)
* Support `request` in the browser and support supplying servers to try in
joinRoom()
[\#764](https://github.com/matrix-org/matrix-js-sdk/pull/764)
* loglevel should be a normal dependency
[\#767](https://github.com/matrix-org/matrix-js-sdk/pull/767)
* Stop devicelist when client is stopped
[\#766](https://github.com/matrix-org/matrix-js-sdk/pull/766)
* Update to WebAssembly-powered Olm
[\#743](https://github.com/matrix-org/matrix-js-sdk/pull/743)
* Logging lib. Fixes #332
[\#763](https://github.com/matrix-org/matrix-js-sdk/pull/763)
* Use new stop() method on matrix-mock-request
[\#765](https://github.com/matrix-org/matrix-js-sdk/pull/765)
Changes in [0.13.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.1) (2018-11-14)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.13.0...v0.13.1)
* Add function to get currently joined rooms.
[\#779](https://github.com/matrix-org/matrix-js-sdk/pull/779)
Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.0) (2018-11-15)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1...v0.13.0)
BREAKING CHANGE
----------------
* `MatrixClient::login` now sets client `access_token` and `user_id` following successful login with username and password.
Changes in [0.12.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1) (2018-10-29)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1-rc.1...v0.12.1)
* No changes since rc.1
Changes in [0.12.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1-rc.1) (2018-10-24)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0...v0.12.1-rc.1)
* Add repository type to package.json to make it valid
[\#762](https://github.com/matrix-org/matrix-js-sdk/pull/762)
* Add getMediaConfig()
[\#761](https://github.com/matrix-org/matrix-js-sdk/pull/761)
* add new examples, to be expanded into a post
[\#739](https://github.com/matrix-org/matrix-js-sdk/pull/739)
Changes in [0.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0) (2018-10-16)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0-rc.1...v0.12.0)
* No changes since rc.1
Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0-rc.1) (2018-10-11)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1...v0.12.0-rc.1)
BREAKING CHANGES
----------------
* If js-sdk finds data in the store that is incompatible with the options currently being used,
it will emit sync state ERROR with an error of type InvalidStoreError. It will also stop trying
to sync in this situation: the app must stop the client and then either clear the store or
change the options (in this case, enable or disable lazy loading of members) and then start
the client again.
All Changes
-----------
* never replace /sync'ed memberships with OOB ones
[\#760](https://github.com/matrix-org/matrix-js-sdk/pull/760)
* Don't fail to start up if lazy load check fails
[\#759](https://github.com/matrix-org/matrix-js-sdk/pull/759)
* Make e2e work on Edge
[\#754](https://github.com/matrix-org/matrix-js-sdk/pull/754)
* throw error with same name and message over idb worker boundary
[\#758](https://github.com/matrix-org/matrix-js-sdk/pull/758)
* Default to a room version of 1 when there is no room create event
[\#755](https://github.com/matrix-org/matrix-js-sdk/pull/755)
* Silence bluebird warnings
[\#757](https://github.com/matrix-org/matrix-js-sdk/pull/757)
* allow non-ff merge from release branch into master
[\#750](https://github.com/matrix-org/matrix-js-sdk/pull/750)
* Reject with the actual error on indexeddb error
[\#751](https://github.com/matrix-org/matrix-js-sdk/pull/751)
* Update mocha to v5
[\#744](https://github.com/matrix-org/matrix-js-sdk/pull/744)
* disable lazy loading for guests as they cant create filters
[\#748](https://github.com/matrix-org/matrix-js-sdk/pull/748)
* Revert "Add getMediaLimits to client"
[\#745](https://github.com/matrix-org/matrix-js-sdk/pull/745)
Changes in [0.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1) (2018-10-01)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1-rc.1...v0.11.1)
* No changes since rc.1
Changes in [0.11.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1-rc.1) (2018-09-27)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0...v0.11.1-rc.1)
* make usage of hub compatible with latest version (2.5)
[\#747](https://github.com/matrix-org/matrix-js-sdk/pull/747)
* Detect when lazy loading has been toggled in client.startClient
[\#746](https://github.com/matrix-org/matrix-js-sdk/pull/746)
* Add getMediaLimits to client
[\#644](https://github.com/matrix-org/matrix-js-sdk/pull/644)
* Split npm start into an init and watch script
[\#742](https://github.com/matrix-org/matrix-js-sdk/pull/742)
* Revert "room name should only take canonical alias into account"
[\#738](https://github.com/matrix-org/matrix-js-sdk/pull/738)
* fix display name disambiguation with LL
[\#737](https://github.com/matrix-org/matrix-js-sdk/pull/737)
* Introduce Room.myMembership event
[\#735](https://github.com/matrix-org/matrix-js-sdk/pull/735)
* room name should only take canonical alias into account
[\#733](https://github.com/matrix-org/matrix-js-sdk/pull/733)
* state events from context response were not wrapped in a MatrixEvent
[\#732](https://github.com/matrix-org/matrix-js-sdk/pull/732)
* Reduce amount of promises created when inserting members
[\#724](https://github.com/matrix-org/matrix-js-sdk/pull/724)
* dont wait for LL members to be stored to resolve the members
[\#726](https://github.com/matrix-org/matrix-js-sdk/pull/726)
* RoomState.members emitted with wrong argument order for OOB members
[\#728](https://github.com/matrix-org/matrix-js-sdk/pull/728)
Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (2018-09-10)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0-rc.1...v0.11.0)
+5
View File
@@ -113,3 +113,8 @@ include the line in your commit or pull request comment::
can't be accepted. Git makes this trivial - just use the -s flag when you do
``git commit``, having first set ``user.name`` and ``user.email`` git configs
(which you should have done anyway :)
If you forgot to sign off your commits before making your pull request and are on git 2.17+
you can mass signoff using rebase::
git rebase --signoff origin/develop
+54 -2
View File
@@ -30,9 +30,61 @@ In Node.js
console.log("Public Rooms: %s", JSON.stringify(data));
});
```
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node) for a more complex example.
To start the client:
```javascript
await client.startClient({initialSyncLimit: 10});
```
You can perform a call to `/sync` to get the current state of the client:
```javascript
client.once('sync', function(state, prevState, res) {
if(state === 'PREPARED') {
console.log("prepared");
} else {
console.log(state);
process.exit(1);
}
});
```
To send a message:
```javascript
var content = {
"body": "message text",
"msgtype": "m.text"
};
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
console.log(err);
});
```
To listen for message events:
```javascript
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
if (event.getType() !== "m.room.message") {
return; // only use messages
}
console.log(event.event.content.body);
});
```
By default, the `matrix-js-sdk` client uses the `MatrixInMemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
```javascript
Object.keys(client.store.rooms).forEach((roomId) => {
client.getRoom(roomId).timeline.forEach(t => {
console.log(t.event);
});
});
```
What does this SDK do?
----------------------
@@ -267,13 +319,13 @@ To provide the Olm library in a browser application:
To provide the Olm library in a node.js application:
* ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz``
* ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``
(replace the URL with the latest version you want to use from
https://matrix.org/packages/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
If you want to package Olm as dependency for your node.js application, you
can use ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
can use ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz
--save-optional`` (if your application also works without e2e crypto enabled)
or ``--save`` (if it doesn't) to do so.
+13 -1
View File
@@ -1,5 +1,17 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("browser-request"));
const request = require('browser-request');
const queryString = require('qs');
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
// we mimic `request`'s query string interface to make it all work
// as expected.
// browser-request will happily take the constructed string as the
// query string without trying to modify it further.
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
return request(opts, fn);
});
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
+1 -1
View File
@@ -5,7 +5,7 @@ set -x
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 6 || exit $?
nvm use 10 || exit $?
npm install || exit $?
RC=0
+6772
View File
File diff suppressed because it is too large Load Diff
+18 -11
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "0.11.0",
"version": "0.14.3",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
@@ -10,15 +10,18 @@
"test": "npm run test:build && npm run test:run",
"check": "npm run test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "babel -s -w -d lib src",
"start": "npm run start:init && npm run start:watch",
"start:watch": "babel -s -w --skip-initial-build -d lib src",
"start:init": "babel -s -d lib src",
"clean": "rimraf lib dist",
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
"dist": "npm run build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 102 src spec",
"lint": "eslint --max-warnings 101 src spec",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-js-sdk"
},
"keywords": [
@@ -51,34 +54,38 @@
"dependencies": {
"another-json": "^0.2.0",
"babel-runtime": "^6.26.0",
"base-x": "3.0.4",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.2",
"request": "^2.53.0"
"loglevel": "1.6.1",
"qs": "^6.5.2",
"request": "^2.88.0",
"unhomoglyph": "^1.0.2"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-eslint": "^7.1.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0",
"browserify": "^14.0.0",
"browserify": "^16.2.3",
"browserify-shim": "^3.8.13",
"eslint": "^3.13.1",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
"jsdoc": "^3.5.5",
"lolex": "^1.5.2",
"matrix-mock-request": "^1.2.0",
"mocha": "^3.2.0",
"mocha-jenkins-reporter": "^0.3.6",
"matrix-mock-request": "^1.2.2",
"mocha": "^5.2.0",
"mocha-jenkins-reporter": "^0.4.0",
"rimraf": "^2.5.4",
"source-map-support": "^0.4.11",
"sourceify": "^0.1.0",
"uglify-js": "^2.8.26",
"watchify": "^3.2.1"
"watchify": "^3.11.0"
},
"browserify": {
"transform": [
+24 -4
View File
@@ -11,7 +11,17 @@
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 $$)
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
exit
fi
else
echo "hub is required: please install it"
exit
fi
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
@@ -45,7 +55,8 @@ fi
skip_changelog=
skip_jsdoc=
changelog_file="CHANGELOG.md"
while getopts hc:xz f; do
expected_npm_user="matrixdotorg"
while getopts hc:u:xz f; do
case $f in
h)
help
@@ -60,6 +71,9 @@ while getopts hc:xz f; do
z)
skip_jsdoc=1
;;
u)
expected_npm_user="$OPTARG"
;;
esac
done
shift `expr $OPTIND - 1`
@@ -74,6 +88,12 @@ if [ -z "$skip_changelog" ]; then
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
# ignore leading v on release
release="${1#v}"
tag="v${release}"
@@ -245,7 +265,7 @@ release_text=`mktemp`
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag"
hub release create $hubflags $assets -F "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
@@ -281,7 +301,7 @@ fi
echo "updating master branch"
git checkout master
git pull
git merge --ff-only "$rel_branch"
git merge "$rel_branch"
# push master and docs (if generated) to github
git push origin master
+2
View File
@@ -102,9 +102,11 @@ TestClient.prototype.start = function() {
/**
* stop the client
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
return this.httpBackend.stop();
};
/**
+1 -1
View File
@@ -97,7 +97,7 @@ describe("DeviceList management:", function() {
});
afterEach(function() {
aliceTestClient.stop();
return aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
+2 -2
View File
@@ -410,10 +410,10 @@ describe("MatrixClient crypto", function() {
});
afterEach(function() {
aliTestClient.stop();
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
bobTestClient.stop();
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
});
it("Bob uploads device keys", function() {
@@ -30,6 +30,7 @@ describe("MatrixClient events", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("emissions", function() {
@@ -111,6 +111,7 @@ describe("getEventTimeline support", function() {
if (client) {
client.stopClient();
}
return httpBackend.stop();
});
it("timeline support must be enabled to work", function(done) {
+1
View File
@@ -41,6 +41,7 @@ describe("MatrixClient", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
describe("uploadContent", function() {
+12 -15
View File
@@ -64,6 +64,7 @@ describe("MatrixClient opts", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
describe("without opts.store", function() {
@@ -94,7 +95,7 @@ describe("MatrixClient opts", function() {
httpBackend.flush("/txn1", 1);
});
it("should be able to sync / get new events", function(done) {
it("should be able to sync / get new events", async function() {
const expectedEventTypes = [ // from /initialSync
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
"m.room.create",
@@ -110,20 +111,16 @@ describe("MatrixClient opts", function() {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush("/pushrules", 1).then(function() {
return httpBackend.flush("/filter", 1);
}).then(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
);
done();
});
await client.startClient();
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
);
});
});
@@ -36,6 +36,7 @@ describe("MatrixClient retrying", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
xit("should retry according to MatrixScheduler.retryFn", function() {
@@ -130,6 +130,7 @@ describe("MatrixClient room timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("local echo events", function() {
+1
View File
@@ -38,6 +38,7 @@ describe("MatrixClient syncing", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("startClient", function() {
+8 -2
View File
@@ -296,7 +296,7 @@ describe("megolm", function() {
});
afterEach(function() {
aliceTestClient.stop();
return aliceTestClient.stop();
});
it("Alice receives a megolm message", function() {
@@ -817,8 +817,14 @@ describe("megolm", function() {
};
});
// Grab the event that we'll need to resend
const room = aliceTestClient.client.getRoom(ROOM_ID);
const pendingEvents = room.getPendingEvents();
expect(pendingEvents.length).toEqual(1);
const unsentEvent = pendingEvents[0];
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.client.resendEvent(unsentEvent, room),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
+670
View File
@@ -0,0 +1,670 @@
/*
Copyright 2018 New Vector 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";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const utils = require("../test-utils");
const AutoDiscovery = sdk.AutoDiscovery;
import expect from 'expect';
import MockHttpBackend from "matrix-mock-request";
describe("AutoDiscovery", function() {
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new MockHttpBackend();
sdk.request(httpBackend.requestFn);
});
it("should throw an error when no domain is specified", function() {
return Promise.all([
AutoDiscovery.findClientConfig(/* no args */).then(() => {
throw new Error("Expected a failure, not success with no args");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig("").then(() => {
throw new Error("Expected a failure, not success with an empty string");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(null).then(() => {
throw new Error("Expected a failure, not success with null");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(true).then(() => {
throw new Error("Expected a failure, not success with a non-string");
}, () => {
return true;
}),
]);
});
it("should return PROMPT when .well-known 404s", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "PROMPT",
error: null,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns a 500 error", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns a 400 error", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns an empty body", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (empty string)", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (no property)", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (disallowed scheme)", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "mxc://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 404)", function() {
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 500)", function() {
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 200 but wrong content)", function() {
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
not_matrix_versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " +
"m.homeserver", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri).toEqual("https://example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS with the right homeserver URL", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(missing base_url)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
not_base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(empty base_url)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(validation error: 404)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(validation error: 500)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS when the identity server configuration is " +
"verifiably accurate", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "SUCCESS",
error: null,
base_url: "https://identity.example.org",
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS and preserve non-standard keys from the " +
".well-known response", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
"org.example.custom.property": {
cupcakes: "yes",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "SUCCESS",
error: null,
base_url: "https://identity.example.org",
},
"org.example.custom.property": {
cupcakes: "yes",
},
};
expect(conf).toEqual(expected);
}),
]);
});
});
+110 -8
View File
@@ -1,20 +1,122 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
let Crypto;
if (sdk.CRYPTO_ENABLED) {
Crypto = require("../../lib/crypto");
}
import '../olm-loader';
import Crypto from '../../lib/crypto';
import expect from 'expect';
import WebStorageSessionStore from '../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../MockStorageApi';
const EventEmitter = require("events").EventEmitter;
const sdk = require("../..");
const Olm = global.Olm;
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
beforeEach(function(done) {
Olm.init().then(done);
});
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(2);
expect(Crypto.getOlmVersion()[0]).toEqual(3);
});
describe('Session management', function() {
const otkResponse = {
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:FLIBBLE': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally a valid signature',
},
},
},
},
},
},
};
let crypto;
let mockBaseApis;
let mockRoomList;
let fakeEmitter;
beforeEach(async function() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
cryptoStore.storeEndToEndDeviceData({
devices: {
'@bob:home.server': {
'BOBDEVICE': {
keys: {
'curve25519:BOBDEVICE': 'this is a key',
},
},
},
},
trackingStatus: {},
});
mockBaseApis = {
sendToDevice: expect.createSpy(),
getKeyBackupVersion: expect.createSpy(),
isGuest: expect.createSpy(),
};
mockRoomList = {};
fakeEmitter = new EventEmitter();
crypto = new Crypto(
mockBaseApis,
sessionStore,
"@alice:home.server",
"FLIBBLE",
sessionStore,
cryptoStore,
mockRoomList,
);
crypto.registerEventHandlers(fakeEmitter);
await crypto.init();
});
afterEach(async function() {
await crypto.stop();
});
it("restarts wedged Olm sessions", async function() {
const prom = new Promise((resolve) => {
mockBaseApis.claimOneTimeKeys = function() {
resolve();
return otkResponse;
};
});
fakeEmitter.emit('toDeviceEvent', {
getType: expect.createSpy().andReturn('m.room.message'),
getContent: expect.createSpy().andReturn({
msgtype: 'm.bad.encrypted',
}),
getWireContent: expect.createSpy().andReturn({
algorithm: 'm.olm.v1.curve25519-aes-sha2',
sender_key: 'this is a key',
}),
getSender: expect.createSpy().andReturn('@bob:home.server'),
});
await prom;
});
});
});
+12 -1
View File
@@ -59,16 +59,25 @@ describe('DeviceList', function() {
let downloadSpy;
let sessionStore;
let cryptoStore;
let deviceLists = [];
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
deviceLists = [];
downloadSpy = expect.createSpy();
const mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore();
});
afterEach(function() {
for (const dl of deviceLists) {
dl.stop();
}
});
function createTestDeviceList() {
const baseApis = {
downloadKeysForUsers: downloadSpy,
@@ -76,7 +85,9 @@ describe('DeviceList', function() {
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
return new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
const dl = new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
deviceLists.push(dl);
return dl;
}
it("should successfully download and store device keys", function() {
+98 -16
View File
@@ -1,8 +1,4 @@
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run megolm tests: libolm not available");
}
import '../../../olm-loader';
import expect from 'expect';
import Promise from 'bluebird';
@@ -13,20 +9,17 @@ import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
// Crypto and OlmDevice won't import unless we have global.Olm
let OlmDevice;
let Crypto;
if (global.Olm) {
OlmDevice = require('../../../../lib/crypto/OlmDevice');
Crypto = require('../../../../lib/crypto');
}
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import Crypto from '../../../../lib/crypto';
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
const Olm = global.Olm;
describe("MegolmDecryption", function() {
if (!global.Olm) {
console.warn('Not running megolm unit tests: libolm not present');
@@ -38,9 +31,11 @@ describe("MegolmDecryption", function() {
let mockCrypto;
let mockBaseApis;
beforeEach(function() {
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
await Olm.init();
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockBaseApis = {};
@@ -69,7 +64,7 @@ describe("MegolmDecryption", function() {
describe('receives some keys:', function() {
let groupSession;
beforeEach(function() {
beforeEach(async function() {
groupSession = new global.Olm.OutboundGroupSession();
groupSession.create();
@@ -98,7 +93,7 @@ describe("MegolmDecryption", function() {
},
};
return event.attemptDecryption(mockCrypto).then(() => {
await event.attemptDecryption(mockCrypto).then(() => {
megolmDecryption.onRoomKeyEvent(event);
});
});
@@ -266,5 +261,92 @@ describe("MegolmDecryption", function() {
// test is successful if no exception is thrown
});
});
it("re-uses sessions for sequential messages", async function() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
olmDevice.verifySignature = expect.createSpy();
await olmDevice.init();
mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
},
},
},
},
},
},
}));
mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve());
mockCrypto.downloadKeys.andReturn(Promise.resolve({
'@alice:home.server': {
aliceDevice: {
deviceId: 'aliceDevice',
isBlocked: expect.createSpy().andReturn(false),
isUnverified: expect.createSpy().andReturn(false),
getIdentityKey: expect.createSpy().andReturn(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: expect.createSpy().andReturn(''),
},
},
}));
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
const mockRoom = {
getEncryptionTargetMembers: expect.createSpy().andReturn(
[{userId: "@alice:home.server"}],
),
getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false),
};
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
);
mockBaseApis.claimOneTimeKeys.reset();
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled();
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
});
});
});
+86
View File
@@ -0,0 +1,86 @@
/*
Copyright 2018 New Vector 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.
*/
import '../../../olm-loader';
import expect from 'expect';
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
function makeOlmDevice() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
return olmDevice;
}
async function setupSession(initiator, opponent) {
await opponent.generateOneTimeKeys(1);
const keys = await opponent.getOneTimeKeys();
const firstKey = Object.values(keys['curve25519'])[0];
const sid = await initiator.createOutboundSession(
opponent.deviceCurve25519Key, firstKey,
);
return sid;
}
describe("OlmDecryption", function() {
if (!global.Olm) {
console.warn('Not running megolm unit tests: libolm not present');
return;
}
let aliceOlmDevice;
let bobOlmDevice;
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
await global.Olm.init();
aliceOlmDevice = makeOlmDevice();
bobOlmDevice = makeOlmDevice();
await aliceOlmDevice.init();
await bobOlmDevice.init();
});
describe('olm', function() {
it("can decrypt messages", async function() {
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
sid,
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
ciphertext.type,
ciphertext.body,
);
expect(result.payload).toEqual(
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
});
});
});
+467
View File
@@ -0,0 +1,467 @@
/*
Copyright 2018 New Vector 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.
*/
import '../../olm-loader';
import expect from 'expect';
import Promise from 'bluebird';
import sdk from '../../..';
import algorithms from '../../../lib/crypto/algorithms';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../MockStorageApi';
import testUtils from '../../test-utils';
import OlmDevice from '../../../lib/crypto/OlmDevice';
import Crypto from '../../../lib/crypto';
const Olm = global.Olm;
const MatrixClient = sdk.MatrixClient;
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
const ENCRYPTED_EVENT = new MatrixEvent({
type: 'm.room.encrypted',
room_id: '!ROOM:ID',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: 'SENDER_CURVE25519',
session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
},
event_id: '$event1',
origin_server_ts: 1507753886000,
});
const KEY_BACKUP_DATA = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
},
};
function makeTestClient(sessionStore, cryptoStore) {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
const store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
return new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
});
}
describe("MegolmBackup", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
return;
}
let olmDevice;
let mockOlmLib;
let mockCrypto;
let mockStorage;
let sessionStore;
let cryptoStore;
let megolmDecryption;
beforeEach(async function() {
await Olm.init();
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = {
algorithm: "m.megolm_backup.v1",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore(mockStorage);
olmDevice = new OlmDevice(sessionStore, cryptoStore);
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
mockOlmLib.encryptMessageForDevice =
expect.createSpy().andReturn(Promise.resolve());
});
describe("backup", function() {
let mockBaseApis;
let realSetTimeout;
beforeEach(function() {
mockBaseApis = {};
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
// clobber the setTimeout function to run 100x faster.
// ideally we would use lolex, but we have no oportunity
// to tick the clock between the first try and the retry.
realSetTimeout = global.setTimeout;
global.setTimeout = function(f, n) {
return realSetTimeout(f, n/100);
};
});
afterEach(function() {
global.setTimeout = realSetTimeout;
});
it('automatically calls the key back up', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// construct a fake decrypted key event via the use of a mocked
// 'crypto' implementation.
const event = new MatrixEvent({
type: 'm.room.encrypted',
});
const decryptedData = {
clearEvent: {
type: 'm.room_key',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
room_id: ROOM_ID,
session_id: groupSession.session_id(),
session_key: groupSession.session_key(),
},
},
senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519",
};
mockCrypto.decryptEvent = function() {
return Promise.resolve(decryptedData);
};
mockCrypto.cancelRoomKeyRequest = function() {};
mockCrypto.backupGroupSession = expect.createSpy();
return event.attemptDecryption(mockCrypto).then(() => {
return megolmDecryption.onRoomKeyEvent(event);
}).then(() => {
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
});
});
it('sends backups to the server', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const client = makeTestClient(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "m.megolm_backup.v1",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqualTo(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toExist();
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
groupSession.session_id(),
);
resolve();
return Promise.resolve({});
};
client._crypto.backupGroupSession(
"roomId",
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(1);
});
});
});
it('retries when a backup fails', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
const store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
});
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "foobar",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqualTo(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toExist();
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({});
} else {
return Promise.reject(
new Error("this is an expected failure"),
);
}
};
client._crypto.backupGroupSession(
"roomId",
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(2);
});
});
});
});
describe("restore", function() {
let client;
beforeEach(function() {
client = makeTestClient(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
});
afterEach(function() {
client.stopClient();
});
it('can restore from backup', function() {
client._http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
it('can restore backup by room', function() {
client._http.authedRequest = function() {
return Promise.resolve({
rooms: {
[ROOM_ID]: {
sessions: {
[SESSION_ID]: KEY_BACKUP_DATA,
},
},
},
});
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
});
});
+29 -19
View File
@@ -139,6 +139,9 @@ describe("MatrixClient", function() {
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
@@ -182,7 +185,7 @@ describe("MatrixClient", function() {
});
});
it("should not POST /filter if a matching filter already exists", function(done) {
it("should not POST /filter if a matching filter already exists", async function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -191,15 +194,19 @@ describe("MatrixClient", function() {
const filter = new sdk.Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.andReturn(filter);
client.startClient();
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
done();
}
const syncPromise = new Promise((resolve, reject) => {
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
resolve();
} else if (state === "ERROR") {
reject(new Error("sync error"));
}
});
});
await client.startClient();
await syncPromise;
});
describe("getSyncState", function() {
@@ -207,15 +214,18 @@ describe("MatrixClient", function() {
expect(client.getSyncState()).toBe(null);
});
it("should return the same sync state as emitted sync events", function(done) {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
done();
}
it("should return the same sync state as emitted sync events", async function() {
const syncingPromise = new Promise((resolve) => {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
resolve();
}
});
});
client.startClient();
await client.startClient();
await syncingPromise;
});
});
@@ -258,8 +268,8 @@ describe("MatrixClient", function() {
});
describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() {
client.startClient();
it("should return false if there is no request waiting", async function() {
await client.startClient();
expect(client.retryImmediately()).toBe(false);
});
+47 -16
View File
@@ -201,22 +201,6 @@ describe("RoomMember", function() {
});
});
describe("supersedesOutOfBand", function() {
it("should be set by markSupersedesOutOfBand", function() {
const member = new RoomMember();
expect(member.supersedesOutOfBand()).toEqual(false);
member.markSupersedesOutOfBand();
expect(member.supersedesOutOfBand()).toEqual(true);
});
it("should be cleared by clearSupersedesOutOfBand", function() {
const member = new RoomMember();
member.markSupersedesOutOfBand();
expect(member.supersedesOutOfBand()).toEqual(true);
member.clearSupersedesOutOfBand();
expect(member.supersedesOutOfBand()).toEqual(false);
});
});
describe("setMembershipEvent", function() {
const joinEvent = utils.mkMembership({
event: true,
@@ -301,5 +285,52 @@ describe("RoomMember", function() {
member.setMembershipEvent(joinEvent); // no-op
expect(emitCount).toEqual(1);
});
it("should set 'name' to user_id if it is just whitespace", function() {
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
user: userA,
room: roomId,
name: " \u200b ",
});
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual(userA); // it should fallback because all whitespace
});
it("should disambiguate users on a fuzzy displayname match", function() {
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
user: userA,
room: roomId,
name: "Alíce\u200b", // note diacritic and zero width char
});
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
};
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).toNotEqual("Alíce"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).toNotEqual(-1);
});
});
});
+5 -57
View File
@@ -299,41 +299,25 @@ describe("RoomState", function() {
expect(eventReceived).toEqual(true);
});
it("should overwrite existing members", function() {
it("should never overwrite existing members", function() {
const oobMemberEvent = utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const memberA = state.getMember(userA);
expect(memberA.events.member.getId()).toEqual(oobMemberEvent.getId());
expect(memberA.isOutOfBand()).toEqual(true);
});
it("should allow later state events to overwrite", function() {
const oobMemberEvent = utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
});
const memberEvent = utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
state.setStateEvents([memberEvent]);
const memberA = state.getMember(userA);
expect(memberA.events.member.getId()).toEqual(memberEvent.getId());
expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId());
expect(memberA.isOutOfBand()).toEqual(false);
});
it("should emit members when updating a member", function() {
const doesntExistYetUserId = "@doesntexistyet:hs";
const oobMemberEvent = utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.members', (_, __, member) => {
expect(member.userId).toEqual(userA);
expect(member.userId).toEqual(doesntExistYetUserId);
eventReceived = true;
});
@@ -341,28 +325,6 @@ describe("RoomState", function() {
state.setOutOfBandMembers([oobMemberEvent]);
expect(eventReceived).toEqual(true);
});
it("should not overwrite members updated since starting loading oob",
function() {
const oobMemberEvent = utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
});
const existingMemberEvent = utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
});
state.markOutOfBandMembersStarted();
state.setStateEvents([existingMemberEvent]);
expect(state.getMember(userA).supersedesOutOfBand()).toEqual(true);
state.setOutOfBandMembers([oobMemberEvent]);
const memberA = state.getMember(userA);
expect(memberA.events.member.getId()).toEqual(existingMemberEvent.getId());
expect(memberA.isOutOfBand()).toEqual(false);
expect(memberA.supersedesOutOfBand()).toEqual(false);
});
});
describe("clone", function() {
@@ -386,20 +348,6 @@ describe("RoomState", function() {
expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount());
});
it("should copy supersedes flag when OOB loading is progress",
function() {
// include OOB members in copy
state.markOutOfBandMembersStarted();
state.setStateEvents([utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
})]);
const copy = state.clone();
const memberA = state.getMember(userA);
const memberACopy = copy.getMember(userA);
expect(memberA.supersedesOutOfBand()).toEqual(true);
expect(memberACopy.supersedesOutOfBand()).toEqual(true);
});
it("should mark old copy as not waiting for out of band anymore", function() {
state.markOutOfBandMembersStarted();
const copy = state.clone();
+20 -8
View File
@@ -1402,13 +1402,25 @@ describe("Room", function() {
it("should return synced membership if membership isn't available yet",
function() {
const room = new Room(roomId, null, userA);
room.setSyncedMembership("invite");
room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite");
room.addLiveEvents([utils.mkMembership({
user: userA, mship: "join",
room: roomId, event: true,
})]);
});
it("should emit a Room.myMembership event on a change",
function() {
const room = new Room(roomId, null, userA);
const events = [];
room.on("Room.myMembership", (_room, membership, oldMembership) => {
events.push({membership, oldMembership});
});
room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite");
expect(events[0]).toEqual({membership: "invite", oldMembership: null});
events.splice(0); //clear
room.updateMyMembership("invite");
expect(events.length).toEqual(0);
room.updateMyMembership("join");
expect(room.getMyMembership()).toEqual("join");
expect(events[0]).toEqual({membership: "join", oldMembership: "invite"});
});
});
@@ -1439,11 +1451,11 @@ describe("Room", function() {
it("should return false if synced membership not join",
function() {
const room = new Room(roomId, null, userA);
room.setSyncedMembership("invite");
room.updateMyMembership("invite");
expect(room.maySendMessage()).toEqual(false);
room.setSyncedMembership("leave");
room.updateMyMembership("leave");
expect(room.maySendMessage()).toEqual(false);
room.setSyncedMembership("join");
room.updateMyMembership("join");
expect(room.maySendMessage()).toEqual(true);
});
});
+389
View File
@@ -0,0 +1,389 @@
/*
Copyright 2018 New Vector 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.
*/
/** @module auto-discovery */
import Promise from 'bluebird';
const logger = require("./logger");
import { URL as NodeURL } from "url";
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
/**
* Description for what an automatically discovered client configuration
* would look like. Although this is a class, it is recommended that it
* be treated as an interface definition rather than as a class.
*
* Additional properties than those defined here may be present, and
* should follow the Java package naming convention.
*/
class DiscoveredClientConfig { // eslint-disable-line no-unused-vars
// Dev note: this is basically a copy/paste of the .well-known response
// object as defined in the spec. It does have additional information,
// however. Overall, this exists to serve as a place for documentation
// and not functionality.
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
constructor() {
/**
* The homeserver configuration the client should use. This will
* always be present on the object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.homeserver"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
* if this is not AutoDiscovery.SUCCESS then the client should
* assume the other properties in the client config (such as
* the identity server configuration) are not valid.
*/
state: AutoDiscovery.PROMPT,
/**
* If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
* then this will contain a human-readable (English) message
* for what went wrong. If the state is none of those previously
* mentioned, this will be falsey.
*/
error: "Something went wrong",
/**
* The base URL clients should use to talk to the homeserver,
* particularly for the login process. May be falsey if the
* state is not AutoDiscovery.SUCCESS.
*/
base_url: "https://matrix.org",
};
/**
* The identity server configuration the client should use. This
* will always be present on teh object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.identity_server"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey.
*/
state: AutoDiscovery.PROMPT,
/**
* The base URL clients should use for interacting with the
* identity server. May be falsey if the state is not
* AutoDiscovery.SUCCESS.
*/
base_url: "https://vector.im",
};
}
}
/**
* Utilities for automatically discovery resources, such as homeservers
* for users to log in to.
*/
export class AutoDiscovery {
// Dev note: the constants defined here are related to but not
// exactly the same as those in the spec. This is to hopefully
// translate the meaning of the states in the spec, but also
// support our own if needed.
/**
* The auto discovery failed. The client is expected to communicate
* the error to the user and refuse logging in.
* @return {string}
* @constructor
*/
static get FAIL_ERROR() { return "FAIL_ERROR"; }
/**
* The auto discovery failed, however the client may still recover
* from the problem. The client is recommended to that the same
* action it would for PROMPT while also warning the user about
* what went wrong. The client may also treat this the same as
* a FAIL_ERROR state.
* @return {string}
* @constructor
*/
static get FAIL_PROMPT() { return "FAIL_PROMPT"; }
/**
* The auto discovery didn't fail but did not find anything of
* interest. The client is expected to prompt the user for more
* information, or fail if it prefers.
* @return {string}
* @constructor
*/
static get PROMPT() { return "PROMPT"; }
/**
* The auto discovery was successful.
* @return {string}
* @constructor
*/
static get SUCCESS() { return "SUCCESS"; }
/**
* Attempts to automatically discover client configuration information
* prior to logging in. Such information includes the homeserver URL
* and identity server URL the client would want. Additional details
* may also be discovered, and will be transparently included in the
* response object unaltered.
* @param {string} domain The homeserver domain to perform discovery
* on. For example, "matrix.org".
* @return {Promise<DiscoveredClientConfig>} Resolves to the discovered
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
static async findClientConfig(domain) {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
// We use a .well-known lookup for all cases. According to the spec, we
// can do other discovery mechanisms if we want such as custom lookups
// however we won't bother with that here (mostly because the spec only
// supports .well-known right now).
//
// By using .well-known, we need to ensure we at least pull out a URL
// for the homeserver. We don't really need an identity server configuration
// but will return one anyways (with state PROMPT) to make development
// easier for clients. If we can't get a homeserver URL, all bets are
// off on the rest of the config and we'll assume it is invalid too.
// We default to an error state to make the first few checks easier to
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
// Technically, we don't have a problem with the identity server
// config at this point.
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
},
};
// Step 1: Actually request the .well-known JSON file and make sure it
// at least has a homeserver definition.
const wellknown = await this._fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!wellknown || wellknown.action !== "SUCCESS"
|| !wellknown.raw["m.homeserver"]
|| !wellknown.raw["m.homeserver"]["base_url"]) {
logger.error("No m.homeserver key in well-known response");
if (wellknown.reason) logger.error(wellknown.reason);
if (wellknown.action === "IGNORE") {
clientConfig["m.homeserver"] = {
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
};
} else {
// this can only ever be FAIL_PROMPT at this point.
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
}
return Promise.resolve(clientConfig);
}
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl(
wellknown.raw["m.homeserver"]["base_url"],
);
if (!hsUrl) {
logger.error("Invalid base_url for m.homeserver");
return Promise.resolve(clientConfig);
}
// Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this._fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!hsVersions || !hsVersions.raw["versions"]) {
logger.error("Invalid /versions response");
return Promise.resolve(clientConfig);
}
// Step 4: Now that the homeserver looks valid, update our client config.
clientConfig["m.homeserver"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: hsUrl,
};
// Step 5: Try to pull out the identity server configuration
let isUrl = "";
if (wellknown.raw["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch. Note that we also fail the homeserver check in the
// object because according to the spec we're supposed to FAIL_ERROR
// if *anything* goes wrong with the IS validation, including invalid
// format. This means we're supposed to stop discovery completely.
const failingClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid identity server discovery response",
// We'll provide the base_url that was previously valid for
// debugging purposes.
base_url: clientConfig["m.homeserver"].base_url,
},
"m.identity_server": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid identity server discovery response",
base_url: null,
},
};
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl(
wellknown.raw["m.identity_server"]["base_url"],
);
if (!isUrl) {
logger.error("Invalid base_url for m.identity_server");
return Promise.resolve(failingClientConfig);
}
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this._fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
logger.error("Invalid /api/v1 response");
return Promise.resolve(failingClientConfig);
}
}
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: isUrl,
};
}
// Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown.raw)
.filter((k) => k !== "m.homeserver" && k !== "m.identity_server")
.map((k) => clientConfig[k] = wellknown.raw[k]);
// Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
}
/**
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
* is suitable for the requirements laid out by .well-known auto discovery.
* If valid, the URL will also be stripped of any trailing slashes.
* @param {string} url The potentially invalid URL to sanitize.
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private
*/
static _sanitizeWellKnownUrl(url) {
if (!url) return false;
try {
// We have to try and parse the URL using the NodeJS URL
// library if we're on NodeJS and use the browser's URL
// library when we're in a browser. To accomplish this, we
// try the NodeJS version first and fall back to the browser.
let parsed = null;
try {
if (NodeURL) parsed = new NodeURL(url);
else parsed = new URL(url);
} catch (e) {
parsed = new URL(url);
}
if (!parsed || !parsed.hostname) return false;
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
const port = parsed.port ? `:${parsed.port}` : "";
const path = parsed.pathname ? parsed.pathname : "";
let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`;
if (saferUrl.endsWith("/")) {
saferUrl = saferUrl.substring(0, saferUrl.length - 1);
}
return saferUrl;
} catch (e) {
logger.error(e);
return false;
}
}
/**
* Fetches a JSON object from a given URL, as expected by all .well-known
* related lookups. If the server gives a 404 then the `action` will be
* IGNORE. If the server returns something that isn't JSON, the `action`
* will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
*
* The returned object will be a result of the call in object form with
* the following properties:
* raw: The JSON object returned by the server.
* action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
* reason: Relatively human readable description of what went wrong.
* error: The actual Error, if one exists.
* @param {string} url The URL to fetch a JSON object from.
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
static async _fetchWellKnownObject(url) {
return new Promise(function(resolve, reject) {
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
{ method: "GET", uri: url },
(err, response, body) => {
if (err || response.statusCode < 200 || response.statusCode >= 300) {
let action = "FAIL_PROMPT";
let reason = (err ? err.message : null) || "General failure";
if (response.statusCode === 404) {
action = "IGNORE";
reason = "No .well-known JSON file found";
}
resolve({raw: {}, action: action, reason: reason, error: err});
return;
}
try {
resolve({raw: JSON.parse(body), action: "SUCCESS"});
} catch (e) {
let reason = "General failure";
if (e.name === "SyntaxError") reason = "Invalid JSON";
resolve({
raw: {},
action: "FAIL_PROMPT",
reason: reason, error: e,
});
}
},
);
});
}
}
+71 -4
View File
@@ -262,7 +262,19 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
utils.extend(login_data, data);
return this._http.authedRequest(
callback, "POST", "/login", undefined, login_data,
(error, response) => {
if (loginType === "m.login.password" && response &&
response.access_token && response.user_id) {
this._http.opts.accessToken = response.access_token;
this.credentials = {
userId: response.user_id,
};
}
if (callback) {
callback(error, response);
}
}, "POST", "/login", undefined, login_data,
);
};
@@ -298,9 +310,23 @@ MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
* @return {string} The HS URL to hit to begin the CAS login process.
*/
MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
return this._http.getUrl("/login/cas/redirect", {
return this.getSsoLoginUrl(redirectUrl, "cas");
};
/**
* @param {string} redirectUrl The URL to redirect to after the HS
* authenticates with the SSO.
* @param {string} loginType The type of SSO login we are doing (sso or cas).
* Defaults to 'sso'.
* @return {string} The HS URL to hit to begin the SSO login process.
*/
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
if (loginType === undefined) {
loginType = "sso";
}
return this._http.getUrl("/login/"+loginType+"/redirect", {
"redirectUrl": redirectUrl,
}, httpApi.PREFIX_UNSTABLE);
}, httpApi.PREFIX_R0);
};
/**
@@ -927,6 +953,28 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
);
};
/**
* @return {module:client.Promise} Resolves: A list of the user's current rooms
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedRooms = function() {
const path = utils.encodeUri("/joined_rooms");
return this._http.authedRequest(undefined, "GET", path);
};
/**
* Retrieve membership info. for a room.
* @param {string} roomId ID of the room to get membership for
* @return {module:client.Promise} Resolves: A list of currently joined users
* and their profile data.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) {
const path = utils.encodeUri("/rooms/$roomId/joined_members", {
$roomId: roomId,
});
return this._http.authedRequest(undefined, "GET", path);
};
// Room Directory operations
// =========================
@@ -1785,7 +1833,7 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
* Get information on how a specific place on a third party protocol
* may be reached.
* @param {string} protocol The protocol given in getThirdpartyProtocols()
* @param {object} params Protocol-specific parameters, as given in th
* @param {object} params Protocol-specific parameters, as given in the
* response to getThirdpartyProtocols()
* @return {module:client.Promise} Resolves to the result object
*/
@@ -1800,6 +1848,25 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
);
};
/**
* Get information on how a specific user on a third party protocol
* may be reached.
* @param {string} protocol The protocol given in getThirdpartyProtocols()
* @param {object} params Protocol-specific parameters, as given in the
* response to getThirdpartyProtocols()
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
const path = utils.encodeUri("/thirdparty/user/$protocol", {
$protocol: protocol,
});
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE,
);
};
/**
* MatrixBaseApis object
*/
+472 -55
View File
@@ -41,30 +41,46 @@ const SyncApi = require("./sync");
const MatrixBaseApis = require("./base-apis");
const MatrixError = httpApi.MatrixError;
const ContentHelpers = require("./content-helpers");
const olmlib = require("./crypto/olmlib");
import ReEmitter from './ReEmitter';
import RoomList from './crypto/RoomList';
import Crypto from './crypto';
import { isCryptoAvailable } from './crypto';
import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey';
import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password';
import { randomString } from './randomstring';
const LAZY_LOADING_MESSAGES_FILTER = {
lazy_load_members: true,
};
const LAZY_LOADING_SYNC_FILTER = {
room: {
state: LAZY_LOADING_MESSAGES_FILTER,
},
};
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
Promise.config({warnings: false});
const SCROLLBACK_DELAY_MS = 3000;
let CRYPTO_ENABLED = false;
const CRYPTO_ENABLED = isCryptoAvailable();
try {
var Crypto = require("./crypto");
CRYPTO_ENABLED = true;
} catch (e) {
console.warn("Unable to load crypto module: crypto will be disabled: " + e);
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
const keys = [];
for (const [sessionId, sessionData] of Object.entries(sessions)) {
try {
const decrypted = keyFromRecoverySession(sessionData, decryptionKey);
decrypted.session_id = sessionId;
decrypted.room_id = roomId;
keys.push(decrypted);
} catch (e) {
console.log("Failed to decrypt session from backup");
}
}
return keys;
}
function keyFromRecoverySession(session, decryptionKey) {
return JSON.parse(decryptionKey.decrypt(
session.session_data.ephemeral,
session.session_data.mac,
session.session_data.ciphertext,
));
}
/**
@@ -140,6 +156,8 @@ function MatrixClient(opts) {
MatrixBaseApis.call(this, opts);
this.olmVersion = null; // Populated after initCrypto is done
this.reEmitter = new ReEmitter(this);
this.store = opts.store || new StubStore();
@@ -192,10 +210,6 @@ function MatrixClient(opts) {
this._forceTURN = opts.forceTURN || false;
if (CRYPTO_ENABLED) {
this.olmVersion = Crypto.getOlmVersion();
}
// List of which rooms have encryption enabled: separate from crypto because
// we still want to know which rooms are encrypted even if crypto is disabled:
// we don't want to start sending unencrypted events to them.
@@ -385,6 +399,13 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
* successfully initialised.
*/
MatrixClient.prototype.initCrypto = async function() {
if (!isCryptoAvailable()) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
`you remember to load the olm library?`,
);
}
if (this._crypto) {
console.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
@@ -402,13 +423,6 @@ MatrixClient.prototype.initCrypto = async function() {
// initialise the list of encrypted rooms (whether or not crypto is enabled)
await this._roomList.init();
if (!CRYPTO_ENABLED) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
`you remember to load the olm library?`,
);
}
const userId = this.getUserId();
if (userId === null) {
throw new Error(
@@ -433,6 +447,7 @@ MatrixClient.prototype.initCrypto = async function() {
);
this.reEmitter.reEmit(crypto, [
"crypto.keyBackupFailed",
"crypto.roomKeyRequest",
"crypto.roomKeyRequestCancellation",
"crypto.warning",
@@ -440,6 +455,9 @@ MatrixClient.prototype.initCrypto = async function() {
await crypto.init();
this.olmVersion = Crypto.getOlmVersion();
// if crypto initialisation was successful, tell it to attach its event
// handlers.
crypto.registerEventHandlers(this);
@@ -543,7 +561,15 @@ MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified)
if (verified === undefined) {
verified = true;
}
return _setDeviceVerification(this, userId, deviceId, verified, null);
const prom = _setDeviceVerification(this, userId, deviceId, verified, null);
// if one of the user's own devices is being marked as verified / unverified,
// check the key backup status, since whether or not we use this depends on
// whether it has a signature from a verified device
if (userId == this.credentials.userId) {
this._crypto.checkKeyBackup();
}
return prom;
};
/**
@@ -747,6 +773,333 @@ MatrixClient.prototype.importRoomKeys = function(keys) {
return this._crypto.importRoomKeys(keys);
};
/**
* Get information about the current key backup.
* @returns {Promise} Information object from API or null
*/
MatrixClient.prototype.getKeyBackupVersion = function() {
return this._http.authedRequest(
undefined, "GET", "/room_keys/version",
).then((res) => {
if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
const err = "Unknown backup algorithm: " + res.algorithm;
return Promise.reject(err);
} else if (!(typeof res.auth_data === "object")
|| !res.auth_data.public_key) {
const err = "Invalid backup data returned";
return Promise.reject(err);
} else {
return res;
}
}).catch((e) => {
if (e.errcode === 'M_NOT_FOUND') {
return null;
} else {
throw e;
}
});
};
/**
* @param {object} info key backup info dict from getKeyBackupVersion()
* @return {object} {
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
* sigs: [
* valid: [bool],
* device: [DeviceInfo],
* ]
* }
*/
MatrixClient.prototype.isKeyBackupTrusted = function(info) {
return this._crypto.isKeyBackupTrusted(info);
};
/**
* @returns {bool} true if the client is configured to back up keys to
* the server, otherwise false.
*/
MatrixClient.prototype.getKeyBackupEnabled = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return Boolean(this._crypto.backupKey);
};
/**
* Enable backing up of keys, using data previously returned from
* getKeyBackupVersion.
*
* @param {object} info Backup information object as returned by getKeyBackupVersion
*/
MatrixClient.prototype.enableKeyBackup = function(info) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.backupInfo = info;
if (this._crypto.backupKey) this._crypto.backupKey.free();
this._crypto.backupKey = new global.Olm.PkEncryption();
this._crypto.backupKey.set_recipient_key(info.auth_data.public_key);
this.emit('crypto.keyBackupStatus', true);
};
/**
* Disable backing up of keys.
*/
MatrixClient.prototype.disableKeyBackup = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.backupInfo = null;
if (this._crypto.backupKey) this._crypto.backupKey.free();
this._crypto.backupKey = null;
this.emit('crypto.keyBackupStatus', false);
};
/**
* Set up the data required to create a new backup version. The backup version
* will not be created and enabled until createKeyBackupVersion is called.
*
* @param {string} password Passphrase string that can be entered by the user
* when restoring the backup as an alternative to entering the recovery key.
* Optional.
*
* @returns {Promise<object>} Object that can be passed to createKeyBackupVersion and
* additionally has a 'recovery_key' member with the user-facing recovery key string.
*/
MatrixClient.prototype.prepareKeyBackupVersion = async function(password) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const decryption = new global.Olm.PkDecryption();
try {
let publicKey;
const authData = {};
if (password) {
const keyInfo = await keyForNewBackup(password);
publicKey = decryption.init_with_private_key(keyInfo.key);
authData.private_key_salt = keyInfo.salt;
authData.private_key_iterations = keyInfo.iterations;
} else {
publicKey = decryption.generate_key();
}
authData.public_key = publicKey;
return {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: authData,
recovery_key: encodeRecoveryKey(decryption.get_private_key()),
};
} finally {
decryption.free();
}
};
/**
* Create a new key backup version and enable it, using the information return
* from prepareKeyBackupVersion.
*
* @param {object} info Info object from prepareKeyBackupVersion
* @returns {Promise<object>} Object with 'version' param indicating the version created
*/
MatrixClient.prototype.createKeyBackupVersion = function(info) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const data = {
algorithm: info.algorithm,
auth_data: info.auth_data,
};
return this._crypto._signObject(data.auth_data).then(() => {
return this._http.authedRequest(
undefined, "POST", "/room_keys/version", undefined, data,
);
}).then((res) => {
this.enableKeyBackup({
algorithm: info.algorithm,
auth_data: info.auth_data,
version: res.version,
});
return res;
});
};
MatrixClient.prototype.deleteKeyBackupVersion = function(version) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
// If we're currently backing up to this backup... stop.
// (We start using it automatically in createKeyBackupVersion
// so this is symmetrical).
if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) {
this.disableKeyBackup();
}
const path = utils.encodeUri("/room_keys/version/$version", {
$version: version,
});
return this._http.authedRequest(
undefined, "DELETE", path, undefined, undefined,
);
};
MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) {
let path;
if (sessionId !== undefined) {
path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", {
$roomId: roomId,
$sessionId: sessionId,
});
} else if (roomId !== undefined) {
path = utils.encodeUri("/room_keys/keys/$roomId", {
$roomId: roomId,
});
} else {
path = "/room_keys/keys";
}
const queryData = version === undefined ? undefined : { version: version };
return {
path: path,
queryData: queryData,
};
};
/**
* Back up session keys to the homeserver.
* @param {string} roomId ID of the room that the keys are for Optional.
* @param {string} sessionId ID of the session that the keys are for Optional.
* @param {integer} version backup version Optional.
* @param {object} data Object keys to send
* @return {module:client.Promise} a promise that will resolve when the keys
* are uploaded
*/
MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
undefined, "PUT", path.path, path.queryData, data,
);
};
MatrixClient.prototype.backupAllGroupSessions = function(version) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.backupAllGroupSessions(version);
};
MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
try {
decodeRecoveryKey(recoveryKey);
return true;
} catch (e) {
return false;
}
};
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
password, targetRoomId, targetSessionId, version,
) {
const backupInfo = await this.getKeyBackupVersion();
const privKey = await keyForExistingBackup(backupInfo, password);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, version,
);
};
MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
recoveryKey, targetRoomId, targetSessionId, version,
) {
const privKey = decodeRecoveryKey(recoveryKey);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, version,
);
};
MatrixClient.prototype._restoreKeyBackup = function(
privKey, targetRoomId, targetSessionId, version,
) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
let totalKeyCount = 0;
let keys = [];
const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version);
const decryption = new global.Olm.PkDecryption();
try {
decryption.init_with_private_key(privKey);
} catch(e) {
decryption.free();
throw e;
}
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData,
).then((res) => {
if (res.rooms) {
for (const [roomId, roomData] of Object.entries(res.rooms)) {
if (!roomData.sessions) continue;
totalKeyCount += Object.keys(roomData.sessions).length;
const roomKeys = keysFromRecoverySession(
roomData.sessions, decryption, roomId, roomKeys,
);
for (const k of roomKeys) {
k.room_id = roomId;
keys.push(k);
}
}
} else if (res.sessions) {
totalKeyCount = Object.keys(res.sessions).length;
keys = keysFromRecoverySession(
res.sessions, decryption, targetRoomId, keys,
);
} else {
totalKeyCount = 1;
try {
const key = keyFromRecoverySession(res, decryption);
key.room_id = targetRoomId;
key.session_id = targetSessionId;
keys.push(key);
} catch (e) {
console.log("Failed to decrypt session from backup");
}
}
return this.importRoomKeys(keys);
}).then(() => {
return {total: totalKeyCount, imported: keys.length};
}).finally(() => {
decryption.free();
});
};
MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
undefined, "DELETE", path.path, path.queryData,
);
};
// Group ops
// =========
// Operations on groups that come down the sync stream (ie. ones the
@@ -771,6 +1124,17 @@ MatrixClient.prototype.getGroups = function() {
return this.store.getGroups();
};
/**
* Get the config for the media repository.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves with an object containing the config.
*/
MatrixClient.prototype.getMediaConfig = function(callback) {
return this._http.authedRequestWithPrefix(
callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0,
);
};
// Room ops
// ========
@@ -917,6 +1281,8 @@ MatrixClient.prototype.isUserIgnored = function(userId) {
* </strong> Default: true.
* @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite,
* the signing URL is passed in this parameter.
* @param {string[]} opts.viaServers The server names to try and join through in
* addition to those that are automatically chosen.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Room object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
@@ -945,6 +1311,13 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
);
}
const queryString = {};
if (opts.viaServers) {
queryString["server_name"] = opts.viaServers;
}
const reqOpts = {qsStringifyOptions: {arrayFormat: 'repeat'}};
const defer = Promise.defer();
const self = this;
@@ -955,7 +1328,8 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
}
const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias});
return self._http.authedRequest(undefined, "POST", path, undefined, data);
return self._http.authedRequest(
undefined, "POST", path, queryString, data, reqOpts);
}).then(function(res) {
const roomId = res.room_id;
const syncApi = new SyncApi(self, self._clientOpts);
@@ -1175,6 +1549,13 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
room.addPendingEvent(localEvent, txnId);
}
// addPendingEvent can change the state to NOT_SENT if it believes
// that there's other events that have failed. We won't bother to
// try sending the event if the state has changed as such.
if (localEvent.status === EventStatus.NOT_SENT) {
return Promise.reject(new Error("Event blocked by other events not yet sent"));
}
return _sendEvent(this, room, localEvent, callback);
};
@@ -1882,6 +2263,27 @@ MatrixClient.prototype.mxcUrlToHttp =
);
};
/**
* Sets a new status message for the user. The message may be null/falsey
* to clear the message.
* @param {string} newMessage The new message to set.
* @return {module:client.Promise} Resolves: to nothing
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) {
return Promise.all(this.getRooms().map((room) => {
const isJoined = room.getMyMembership() === "join";
const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2;
if (isJoined && looksLikeDm) {
return this.sendStateEvent(room.roomId, "im.vector.user_status", {
status: newMessage,
}, this.getUserId());
} else {
return Promise.resolve();
}
}));
};
/**
* @param {Object} opts Options to apply
* @param {string} opts.presence One of "online", "offline" or "unavailable"
@@ -2067,7 +2469,7 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
let params = undefined;
if (this._clientOpts.lazyLoadMembers) {
params = {filter: JSON.stringify(LAZY_LOADING_MESSAGES_FILTER)};
params = {filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)};
}
// TODO: we should implement a backoff (as per scrollback()) to deal more
@@ -2102,7 +2504,8 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
self.getEventMapper()));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
} else {
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state);
const stateEvents = utils.map(res.state, self.getEventMapper());
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents);
}
timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start);
@@ -2147,7 +2550,7 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) {
if (this._clientOpts.lazyLoadMembers) {
// create a shallow copy of LAZY_LOADING_MESSAGES_FILTER,
// so the timelineFilter doesn't get written into it below
filter = Object.assign({}, LAZY_LOADING_MESSAGES_FILTER);
filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER);
}
if (timelineFilter) {
// XXX: it's horrific that /messages' filter parameter doesn't match
@@ -3104,21 +3507,6 @@ MatrixClient.prototype.startClient = async function(opts) {
// shallow-copy the opts dict before modifying and storing it
opts = Object.assign({}, opts);
if (opts.lazyLoadMembers) {
const supported = await this.doesServerSupportLazyLoading();
if (supported) {
opts.filter = await this.createFilter(LAZY_LOADING_SYNC_FILTER);
} else {
console.log("LL: lazy loading requested but not supported " +
"by server, so disabling");
opts.lazyLoadMembers = false;
}
}
if (opts.lazyLoadMembers && this._crypto) {
this._crypto.enableLazyLoading();
}
opts.crypto = this._crypto;
opts.canResetEntireTimeline = (roomId) => {
if (!this._canResetTimelineCallback) {
@@ -3127,11 +3515,29 @@ MatrixClient.prototype.startClient = async function(opts) {
return this._canResetTimelineCallback(roomId);
};
this._clientOpts = opts;
this._syncApi = new SyncApi(this, opts);
this._syncApi.sync();
};
/**
* store client options with boolean/string/numeric values
* to know in the next session what flags the sync data was
* created with (e.g. lazy loading)
* @param {object} opts the complete set of client options
* @return {Promise} for store operation */
MatrixClient.prototype._storeClientOptions = function() {
const primTypes = ["boolean", "string", "number"];
const serializableOpts = Object.entries(this._clientOpts)
.filter(([key, value]) => {
return primTypes.includes(typeof value);
})
.reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
return this.store.storeClientOptions(serializableOpts);
};
/**
* High level helper method to stop the client from polling and allow a
* clean shutdown.
@@ -3479,14 +3885,7 @@ MatrixClient.prototype.getEventMapper = function() {
* @return {string} A new client secret
*/
MatrixClient.prototype.generateClientSecret = function() {
let ret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
ret += chars.charAt(Math.floor(Math.random() * chars.length));
}
return ret;
return randomString(32);
};
/** */
@@ -3730,6 +4129,24 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* });
*/
/**
* Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
* @event module:client~MatrixClient#"crypto.keyBackupStatus"
* @param {bool} enabled true if key backup has been enabled, otherwise false
* @example
* matrixClient.on("crypto.keyBackupStatus", function(enabled){
* if (enabled) {
* [...]
* }
* });
*/
/**
* Fires when we want to suggest to the user that they restore their megolm keys
* from backup or by cross-signing the device.
*
* @event module:client~MatrixClient#"crypto.suggestKeyRestore"
*/
// EventEmitter JSDocs
+80 -24
View File
@@ -24,6 +24,7 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../logger';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
@@ -71,6 +72,9 @@ export default class DeviceList {
// }
this._devices = {};
// map of identity keys to the user who owns it
this._userByIdentityKey = {};
// which users we are tracking device status for.
// userId -> TRACKING_STATUS_*
this._deviceTrackingStatus = {}; // loaded from storage in load()
@@ -110,7 +114,7 @@ export default class DeviceList {
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
if (deviceData === null) {
console.log("Migrating e2e device data...");
logger.log("Migrating e2e device data...");
this._devices = this._sessionStore.getAllEndToEndDevices() || {};
this._deviceTrackingStatus = (
this._sessionStore.getEndToEndDeviceTrackingStatus() || {}
@@ -128,6 +132,16 @@ export default class DeviceList {
deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
}
this._userByIdentityKey = {};
for (const user of Object.keys(this._devices)) {
const userDevices = this._devices[user];
for (const device of Object.keys(userDevices)) {
const idKey = userDevices[device].keys['curve25519:'+device];
if (idKey !== undefined) {
this._userByIdentityKey[idKey] = user;
}
}
}
});
},
);
@@ -145,6 +159,12 @@ export default class DeviceList {
}
}
stop() {
if (this._saveTimer !== null) {
clearTimeout(this._saveTimer);
}
}
/**
* Save the device tracking state to storage, if any changes are
* pending other than updating the sync token
@@ -190,7 +210,7 @@ export default class DeviceList {
const resolveSavePromise = this._resolveSavePromise;
this._savePromiseTime = targetTime;
this._saveTimer = setTimeout(() => {
console.log('Saving device tracking data at token ' + this._syncToken);
logger.log('Saving device tracking data at token ' + this._syncToken);
// null out savePromise now (after the delay but before the write),
// otherwise we could return the existing promise when the save has
// actually already happened. Likewise for the dirty flag.
@@ -258,7 +278,7 @@ export default class DeviceList {
if (this._keyDownloadsInProgressByUser[u]) {
// already a key download in progress/queued for this user; its results
// will be good enough for us.
console.log(
logger.log(
`downloadKeys: already have a download in progress for ` +
`${u}: awaiting its result`,
);
@@ -269,13 +289,13 @@ export default class DeviceList {
});
if (usersToDownload.length != 0) {
console.log("downloadKeys: downloading for", usersToDownload);
logger.log("downloadKeys: downloading for", usersToDownload);
const downloadPromise = this._doKeyDownload(usersToDownload);
promises.push(downloadPromise);
}
if (promises.length === 0) {
console.log("downloadKeys: already have all necessary keys");
logger.log("downloadKeys: already have all necessary keys");
}
return Promise.all(promises).then(() => {
@@ -357,13 +377,17 @@ export default class DeviceList {
/**
* Find a device by curve25519 identity key
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
*
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(userId, algorithm, senderKey) {
getDeviceByIdentityKey(algorithm, senderKey) {
const userId = this._userByIdentityKey[senderKey];
if (!userId) {
return null;
}
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
@@ -408,7 +432,23 @@ export default class DeviceList {
* @param {Object} devs New device info for user
*/
storeDevicesForUser(u, devs) {
// remove previous devices from _userByIdentityKey
if (this._devices[u] !== undefined) {
for (const [deviceId, dev] of Object.entries(this._devices[u])) {
const identityKey = dev.keys['curve25519:'+deviceId];
delete this._userByIdentityKey[identityKey];
}
}
this._devices[u] = devs;
// add new ones
for (const [deviceId, dev] of Object.entries(devs)) {
const identityKey = dev.keys['curve25519:'+deviceId];
this._userByIdentityKey[identityKey] = u;
}
this._dirty = true;
}
@@ -433,7 +473,7 @@ export default class DeviceList {
throw new Error('userId must be a string; was '+userId);
}
if (!this._deviceTrackingStatus[userId]) {
console.log('Now tracking device list for ' + userId);
logger.log('Now tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
// we don't yet persist the tracking status, since there may be a lot
@@ -452,7 +492,7 @@ export default class DeviceList {
*/
stopTrackingDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log('No longer tracking device list for ' + userId);
logger.log('No longer tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
// we don't yet persist the tracking status, since there may be a lot
@@ -487,7 +527,7 @@ export default class DeviceList {
*/
invalidateUserDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log("Marking device list outdated for", userId);
logger.log("Marking device list outdated for", userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
// we don't yet persist the tracking status, since there may be a lot
@@ -525,7 +565,23 @@ export default class DeviceList {
* @param {Object} devices deviceId->{object} the new devices
*/
_setRawStoredDevicesForUser(userId, devices) {
// remove old devices from _userByIdentityKey
if (this._devices[userId] !== undefined) {
for (const [deviceId, dev] of Object.entries(this._devices[userId])) {
const identityKey = dev.keys['curve25519:'+deviceId];
delete this._userByIdentityKey[identityKey];
}
}
this._devices[userId] = devices;
// add new devices into _userByIdentityKey
for (const [deviceId, dev] of Object.entries(devices)) {
const identityKey = dev.keys['curve25519:'+deviceId];
this._userByIdentityKey[identityKey] = userId;
}
}
/**
@@ -550,7 +606,7 @@ export default class DeviceList {
).then(() => {
finished(true);
}, (e) => {
console.error(
logger.error(
'Error downloading keys for ' + users + ":", e,
);
finished(false);
@@ -573,7 +629,7 @@ export default class DeviceList {
// since we started this request. If that happens, we should
// ignore the completion of the first one.
if (this._keyDownloadsInProgressByUser[u] !== prom) {
console.log('Another update in the queue for', u,
logger.log('Another update in the queue for', u,
'- not marking up-to-date');
return;
}
@@ -584,7 +640,7 @@ export default class DeviceList {
// we didn't get any new invalidations since this download started:
// this user's device list is now up to date.
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
console.log("Device list for", u, "now up to date");
logger.log("Device list for", u, "now up to date");
} else {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
@@ -659,7 +715,7 @@ class DeviceListUpdateSerialiser {
if (this._downloadInProgress) {
// just queue up these users
console.log('Queued key download for', users);
logger.log('Queued key download for', users);
return this._queuedQueryDeferred.promise;
}
@@ -679,7 +735,7 @@ class DeviceListUpdateSerialiser {
const deferred = this._queuedQueryDeferred;
this._queuedQueryDeferred = null;
console.log('Starting key download for', downloadUsers);
logger.log('Starting key download for', downloadUsers);
this._downloadInProgress = true;
const opts = {};
@@ -706,7 +762,7 @@ class DeviceListUpdateSerialiser {
return prom;
}).done(() => {
console.log('Completed key download for ' + downloadUsers);
logger.log('Completed key download for ' + downloadUsers);
this._downloadInProgress = false;
deferred.resolve();
@@ -716,7 +772,7 @@ class DeviceListUpdateSerialiser {
this._doQueuedQueries();
}
}, (e) => {
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
logger.warn('Error downloading keys for ' + downloadUsers + ':', e);
this._downloadInProgress = false;
deferred.reject(e);
});
@@ -725,7 +781,7 @@ class DeviceListUpdateSerialiser {
}
async _processQueryResponseForUser(userId, response) {
console.log('got keys for ' + userId + ':', response);
logger.log('got keys for ' + userId + ':', response);
// map from deviceid -> deviceinfo for this user
const userStore = {};
@@ -763,7 +819,7 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
}
if (!(deviceId in userResult)) {
console.log("Device " + userId + ":" + deviceId +
logger.log("Device " + userId + ":" + deviceId +
" has been removed");
delete userStore[deviceId];
updated = true;
@@ -780,12 +836,12 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
// 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 +
logger.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 +
logger.warn("Mismatched device_id " + deviceResult.device_id +
" in keys from " + userId + ":" + deviceId);
continue;
}
@@ -815,7 +871,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
const signKeyId = "ed25519:" + deviceId;
const signKey = deviceResult.keys[signKeyId];
if (!signKey) {
console.warn("Device " + userId + ":" + deviceId +
logger.warn("Device " + userId + ":" + deviceId +
" has no ed25519 key");
return false;
}
@@ -825,7 +881,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
try {
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) {
console.warn("Unable to verify signature on device " +
logger.warn("Unable to verify signature on device " +
userId + ":" + deviceId + ":" + e);
return false;
}
@@ -842,7 +898,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
console.warn("Ed25519 key for device " + userId + ":" +
logger.warn("Ed25519 key for device " + userId + ":" +
deviceId + " has changed");
return false;
}
+99 -61
View File
@@ -15,19 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../logger';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/**
* olm.js wrapper
*
* @module crypto/OlmDevice
*/
const Olm = global.Olm;
if (!Olm) {
throw new Error("global.Olm is not defined");
}
// 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.
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
@@ -127,7 +117,7 @@ OlmDevice.prototype.init = async function() {
await this._migrateFromSessionStore();
let e2eKeys;
const account = new Olm.Account();
const account = new global.Olm.Account();
try {
await _initialiseAccount(
this._sessionStore, this._cryptoStore, this._pickleKey, account,
@@ -161,7 +151,7 @@ async function _initialiseAccount(sessionStore, cryptoStore, pickleKey, account)
* @return {array} The version of Olm.
*/
OlmDevice.getOlmVersion = function() {
return Olm.get_library_version();
return global.Olm.get_library_version();
};
OlmDevice.prototype._migrateFromSessionStore = async function() {
@@ -173,7 +163,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
// Migrate from sessionStore
pickledAccount = this._sessionStore.getEndToEndAccount();
if (pickledAccount !== null) {
console.log("Migrating account from session store");
logger.log("Migrating account from session store");
this._cryptoStore.storeAccount(txn, pickledAccount);
}
}
@@ -195,7 +185,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
// has run against the same localstorage and created some spurious sessions.
this._cryptoStore.countEndToEndSessions(txn, (count) => {
if (count) {
console.log("Crypto store already has sessions: not migrating");
logger.log("Crypto store already has sessions: not migrating");
return;
}
let numSessions = 0;
@@ -207,7 +197,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
);
}
}
console.log(
logger.log(
"Migrating " + numSessions + " sessions from session store",
);
});
@@ -236,14 +226,14 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
), txn,
);
} catch (e) {
console.warn(
logger.warn(
"Failed to migrate session " + s.senderKey + "/" +
s.sessionId + ": " + e.stack || e,
);
}
++numIbSessions;
}
console.log(
logger.log(
"Migrated " + numIbSessions +
" inbound group sessions from session store",
);
@@ -268,7 +258,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
*/
OlmDevice.prototype._getAccount = function(txn, func) {
this._cryptoStore.getAccount(txn, (pickledAccount) => {
const account = new Olm.Account();
const account = new global.Olm.Account();
try {
account.unpickle(this._pickleKey, pickledAccount);
func(account);
@@ -305,8 +295,8 @@ OlmDevice.prototype._storeAccount = function(txn, account) {
*/
OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) {
this._cryptoStore.getEndToEndSession(
deviceKey, sessionId, txn, (pickledSession) => {
this._unpickleSession(pickledSession, func);
deviceKey, sessionId, txn, (sessionInfo) => {
this._unpickleSession(sessionInfo, func);
},
);
};
@@ -316,15 +306,17 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) {
* function with it. The session object is destroyed once the function
* returns.
*
* @param {string} pickledSession
* @param {object} sessionInfo
* @param {function} func
* @private
*/
OlmDevice.prototype._unpickleSession = function(pickledSession, func) {
const session = new Olm.Session();
OlmDevice.prototype._unpickleSession = function(sessionInfo, func) {
const session = new global.Olm.Session();
try {
session.unpickle(this._pickleKey, pickledSession);
func(session);
session.unpickle(this._pickleKey, sessionInfo.session);
const unpickledSessInfo = Object.assign({}, sessionInfo, {session});
func(unpickledSessInfo);
} finally {
session.free();
}
@@ -334,14 +326,17 @@ OlmDevice.prototype._unpickleSession = function(pickledSession, func) {
* store our OlmSession in the session store
*
* @param {string} deviceKey
* @param {OlmSession} session
* @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int}
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @private
*/
OlmDevice.prototype._saveSession = function(deviceKey, session, txn) {
const pickledSession = session.pickle(this._pickleKey);
OlmDevice.prototype._saveSession = function(deviceKey, sessionInfo, txn) {
const sessionId = sessionInfo.session.session_id();
const pickledSessionInfo = Object.assign(sessionInfo, {
session: sessionInfo.session.pickle(this._pickleKey),
});
this._cryptoStore.storeEndToEndSession(
deviceKey, session.session_id(), pickledSession, txn,
deviceKey, sessionId, pickledSessionInfo, txn,
);
};
@@ -354,7 +349,7 @@ OlmDevice.prototype._saveSession = function(deviceKey, session, txn) {
* @private
*/
OlmDevice.prototype._getUtility = function(func) {
const utility = new Olm.Utility();
const utility = new global.Olm.Utility();
try {
return func(utility);
} finally {
@@ -466,12 +461,19 @@ OlmDevice.prototype.createOutboundSession = async function(
],
(txn) => {
this._getAccount(txn, (account) => {
const session = new Olm.Session();
const session = new global.Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
newSessionId = session.session_id();
this._storeAccount(txn, account);
this._saveSession(theirIdentityKey, session, txn);
const sessionInfo = {
session,
// Pretend we've received a message at this point, otherwise
// if we try to send a message to the device, it won't use
// this session
lastReceivedMessageTs: Date.now(),
};
this._saveSession(theirIdentityKey, sessionInfo, txn);
} finally {
session.free();
}
@@ -510,7 +512,7 @@ OlmDevice.prototype.createInboundSession = async function(
],
(txn) => {
this._getAccount(txn, (account) => {
const session = new Olm.Session();
const session = new global.Olm.Session();
try {
session.create_inbound_from(
account, theirDeviceIdentityKey, ciphertext,
@@ -520,7 +522,13 @@ OlmDevice.prototype.createInboundSession = async function(
const payloadString = session.decrypt(messageType, ciphertext);
this._saveSession(theirDeviceIdentityKey, session, txn);
const sessionInfo = {
session,
// this counts as a received message: set last received message time
// to now
lastReceivedMessageTs: Date.now(),
};
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
result = {
payload: payloadString,
@@ -568,13 +576,30 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
* @return {Promise<?string>} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) {
const sessionIds = await this.getSessionIdsForDevice(theirDeviceIdentityKey);
if (sessionIds.length === 0) {
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey);
if (sessionInfos.length === 0) {
return null;
}
// Use the session with the lowest ID.
sessionIds.sort();
return sessionIds[0];
// Use the session that has most recently received a message
let idxOfBest = 0;
for (let i = 1; i < sessionInfos.length; i++) {
const thisSessInfo = sessionInfos[i];
const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ?
0 : thisSessInfo.lastReceivedMessageTs;
const bestSessInfo = sessionInfos[idxOfBest];
const bestLastReceived = bestSessInfo.lastReceivedMessageTs === undefined ?
0 : bestSessInfo.lastReceivedMessageTs;
if (
thisLastReceived > bestLastReceived || (
thisLastReceived === bestLastReceived &&
thisSessInfo.sessionId < bestSessInfo.sessionId
)
) {
idxOfBest = i;
}
}
return sessionInfos[idxOfBest].sessionId;
};
/**
@@ -597,9 +622,10 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey)
this._cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, (sessions) => {
const sessionIds = Object.keys(sessions).sort();
for (const sessionId of sessionIds) {
this._unpickleSession(sessions[sessionId], (session) => {
this._unpickleSession(sessions[sessionId], (sessInfo) => {
info.push({
hasReceivedMessage: session.has_received_message(),
lastReceivedMessageTs: sessInfo.lastReceivedMessageTs,
hasReceivedMessage: sessInfo.session.has_received_message(),
sessionId: sessionId,
});
});
@@ -630,9 +656,9 @@ OlmDevice.prototype.encryptMessage = async function(
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => {
res = session.encrypt(payloadString);
this._saveSession(theirDeviceIdentityKey, session, txn);
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
res = sessionInfo.session.encrypt(payloadString);
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
);
@@ -657,9 +683,10 @@ OlmDevice.prototype.decryptMessage = async function(
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => {
payloadString = session.decrypt(messageType, ciphertext);
this._saveSession(theirDeviceIdentityKey, session, txn);
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
payloadString = sessionInfo.session.decrypt(messageType, ciphertext);
sessionInfo.lastReceivedMessageTs = Date.now();
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
);
@@ -689,8 +716,8 @@ OlmDevice.prototype.matchesSession = async function(
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => {
matches = session.matches_inbound(ciphertext);
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
matches = sessionInfo.session.matches_inbound(ciphertext);
});
},
);
@@ -724,11 +751,11 @@ OlmDevice.prototype._saveOutboundGroupSession = function(session) {
*/
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
const pickled = this._outboundGroupSessionStore[sessionId];
if (pickled === null) {
if (pickled === undefined) {
throw new Error("Unknown outbound group session " + sessionId);
}
const session = new Olm.OutboundGroupSession();
const session = new global.Olm.OutboundGroupSession();
try {
session.unpickle(this._pickleKey, pickled);
return func(session);
@@ -744,7 +771,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundGroupSession = function() {
const session = new Olm.OutboundGroupSession();
const session = new global.Olm.OutboundGroupSession();
try {
session.create();
this._saveOutboundGroupSession(session);
@@ -816,7 +843,7 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
* @return {*} result of func
*/
OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) {
const session = new Olm.InboundGroupSession();
const session = new global.Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, sessionData.session);
return func(session);
@@ -889,7 +916,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
roomId, senderKey, sessionId, txn,
(existingSession, existingSessionData) => {
if (existingSession) {
console.log(
logger.log(
"Update for megolm session " + senderKey + "/" + sessionId,
);
// for now we just ignore updates. TODO: implement something here
@@ -897,7 +924,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
}
// new session.
const session = new Olm.InboundGroupSession();
const session = new global.Olm.InboundGroupSession();
try {
if (exportFormat) {
session.import_session(sessionKey);
@@ -1034,7 +1061,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
}
if (roomId !== sessionData.room_id) {
console.warn(
logger.warn(
`requested keys for inbound group session ${senderKey}|` +
`${sessionId}, with incorrect room_id ` +
`(expected ${sessionData.room_id}, ` +
@@ -1058,6 +1085,8 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {integer} chainIndex The chain index at which to export the session.
* If omitted, export at the first index we know about.
*
* @returns {Promise<{chain_index: number, key: string,
* forwarding_curve25519_key_chain: Array<string>,
@@ -1065,9 +1094,12 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
* }>}
* details of the session key. The key is a base64-encoded megolm key in
* export format.
*
* @throws Error If the given chain index could not be obtained from the known
* index (ie. the given chain index is before the first we have).
*/
OlmDevice.prototype.getInboundGroupSessionKey = async function(
roomId, senderKey, sessionId,
roomId, senderKey, sessionId, chainIndex,
) {
let result;
await this._cryptoStore.doTxn(
@@ -1078,14 +1110,19 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
result = null;
return;
}
const messageIndex = session.first_known_index();
if (chainIndex === undefined) {
chainIndex = session.first_known_index();
}
const exportedSession = session.export_session(chainIndex);
const claimedKeys = sessionData.keysClaimed || {};
const senderEd25519Key = claimedKeys.ed25519 || null;
result = {
"chain_index": messageIndex,
"key": session.export_session(messageIndex),
"chain_index": chainIndex,
"key": exportedSession,
"forwarding_curve25519_key_chain":
sessionData.forwardingCurve25519KeyChain || [],
"sender_claimed_ed25519_key": senderEd25519Key,
@@ -1119,6 +1156,7 @@ OlmDevice.prototype.exportInboundGroupSession = function(
"session_id": sessionId,
"session_key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
"first_known_index": session.first_known_index(),
};
});
};
+26 -10
View File
@@ -16,6 +16,7 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../logger';
import utils from '../utils';
/**
@@ -108,7 +109,7 @@ export default class OutgoingRoomKeyRequestManager {
* Called when the client is stopped. Stops any running background processes.
*/
stop() {
console.log('stopping OutgoingRoomKeyRequestManager');
logger.log('stopping OutgoingRoomKeyRequestManager');
// stop the timer on the next run
this._clientRunning = false;
}
@@ -173,7 +174,7 @@ export default class OutgoingRoomKeyRequestManager {
// may have seen it, so we still need to send a cancellation
// in that case :/
console.log(
logger.log(
'deleting unnecessary room key request for ' +
stringifyRequestBody(requestBody),
);
@@ -201,7 +202,7 @@ export default class OutgoingRoomKeyRequestManager {
// the request cancelled. There is no point in
// sending another cancellation since the other tab
// will do it.
console.log(
logger.log(
'Tried to cancel room key request for ' +
stringifyRequestBody(requestBody) +
' but it was already cancelled in another tab',
@@ -222,7 +223,7 @@ export default class OutgoingRoomKeyRequestManager {
updatedReq,
andResend,
).catch((e) => {
console.error(
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
@@ -243,6 +244,21 @@ export default class OutgoingRoomKeyRequestManager {
});
}
/**
* Look for room key requests by target device and state
*
* @param {string} userId Target user ID
* @param {string} deviceId Target device ID
*
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingSentRoomKeyRequest(userId, deviceId) {
return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT],
);
}
// start the background timer to send queued requests, if the timer isn't
// already running
_startTimer() {
@@ -261,7 +277,7 @@ export default class OutgoingRoomKeyRequestManager {
}).catch((e) => {
// this should only happen if there is an indexeddb error,
// in which case we're a bit stuffed anyway.
console.warn(
logger.warn(
`error in OutgoingRoomKeyRequestManager: ${e}`,
);
}).done();
@@ -282,7 +298,7 @@ export default class OutgoingRoomKeyRequestManager {
return Promise.resolve();
}
console.log("Looking for queued outgoing room key requests");
logger.log("Looking for queued outgoing room key requests");
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
@@ -290,7 +306,7 @@ export default class OutgoingRoomKeyRequestManager {
ROOM_KEY_REQUEST_STATES.UNSENT,
]).then((req) => {
if (!req) {
console.log("No more outgoing room key requests");
logger.log("No more outgoing room key requests");
this._sendOutgoingRoomKeyRequestsTimer = null;
return;
}
@@ -312,7 +328,7 @@ export default class OutgoingRoomKeyRequestManager {
// go around the loop again
return this._sendOutgoingRoomKeyRequests();
}).catch((e) => {
console.error("Error sending room key request; will retry later.", e);
logger.error("Error sending room key request; will retry later.", e);
this._sendOutgoingRoomKeyRequestsTimer = null;
this._startTimer();
}).done();
@@ -321,7 +337,7 @@ export default class OutgoingRoomKeyRequestManager {
// given a RoomKeyRequest, send it and update the request record
_sendOutgoingRoomKeyRequest(req) {
console.log(
logger.log(
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
` from ${stringifyRecipientList(req.recipients)}` +
`(id ${req.requestId})`,
@@ -347,7 +363,7 @@ export default class OutgoingRoomKeyRequestManager {
// Given a RoomKeyRequest, cancel it and delete the request record unless
// andResend is set, in which case transition to UNSENT.
_sendOutgoingRoomKeyRequestCancellation(req, andResend) {
console.log(
logger.log(
`Sending cancellation for key request for ` +
`${stringifyRequestBody(req.requestBody)} to ` +
`${stringifyRecipientList(req.recipients)} ` +
+156 -18
View File
@@ -24,6 +24,7 @@ limitations under the License.
import Promise from 'bluebird';
const logger = require("../../logger");
const utils = require("../../utils");
const olmlib = require("../olmlib");
const base = require("./base");
@@ -65,7 +66,7 @@ OutboundSessionInfo.prototype.needsRotation = function(
if (this.useCount >= rotationPeriodMsgs ||
sessionLifetime >= rotationPeriodMs
) {
console.log(
logger.log(
"Rotating megolm session after " + this.useCount +
" messages, " + sessionLifetime + "ms",
);
@@ -103,7 +104,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
}
if (!devicesInRoom.hasOwnProperty(userId)) {
console.log("Starting new session because we shared with " + userId);
logger.log("Starting new session because we shared with " + userId);
return true;
}
@@ -113,7 +114,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
}
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
console.log(
logger.log(
"Starting new session because we shared with " +
userId + ":" + deviceId,
);
@@ -143,6 +144,11 @@ function MegolmEncryption(params) {
// room).
this._setupPromise = Promise.resolve();
// Map of outbound sessions by sessions ID. Used if we need a particular
// session (the session we're currently using to send is always obtained
// using _setupPromise).
this._outboundSessions = {};
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
@@ -182,7 +188,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
) {
console.log("Starting new megolm session because we need to rotate.");
logger.log("Starting new megolm session because we need to rotate.");
session = null;
}
@@ -192,8 +198,9 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
}
if (!session) {
console.log(`Starting new megolm session for room ${self._roomId}`);
logger.log(`Starting new megolm session for room ${self._roomId}`);
session = await self._prepareNewSession();
self._outboundSessions[session.sessionId] = session;
}
// now check if we need to share with any devices
@@ -263,6 +270,18 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
);
if (this._crypto.backupInfo) {
// don't wait for it to complete
this._crypto.backupGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [],
sessionId, key.key,
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
console.log("Failed to back up group session", e);
});
}
return new OutboundSessionInfo(sessionId);
};
@@ -319,7 +338,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
continue;
}
console.log(
logger.log(
"share keys with device " + userId + ":" + deviceId,
);
@@ -408,8 +427,98 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
};
/**
* @private
* Re-shares a megolm session key with devices if the key has already been
* sent to them.
*
* @param {string} senderKey The key of the originating device for the session
* @param {string} sessionId ID of the outbound session to share
* @param {string} userId ID of the user who owns the target device
* @param {module:crypto/deviceinfo} device The target device
*/
MegolmEncryption.prototype.reshareKeyWithDevice = async function(
senderKey, sessionId, userId, device,
) {
const obSessionInfo = this._outboundSessions[sessionId];
if (!obSessionInfo) {
logger.debug("Session ID " + sessionId + " not found: not re-sharing keys");
return;
}
// The chain index of the key we previously sent this device
if (obSessionInfo.sharedWithDevices[userId] === undefined) {
logger.debug("Session ID " + sessionId + " never shared with user " + userId);
return;
}
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sentChainIndex === undefined) {
logger.debug(
"Session ID " + sessionId + " never shared with device " +
userId + ":" + device.deviceId,
);
return;
}
// get the key from the inbound session: the outbound one will already
// have been ratcheted to the next chain index.
const key = await this._olmDevice.getInboundGroupSessionKey(
this._roomId, senderKey, sessionId, sentChainIndex,
);
if (!key) {
logger.warn(
"No outbound session key found for " + sessionId + ": not re-sharing keys",
);
return;
}
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {
[userId]: {
[device.deviceId]: device,
},
},
);
const payload = {
type: "m.forwarded_room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: sessionId,
session_key: key.key,
chain_index: key.chain_index,
sender_key: senderKey,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
},
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
userId,
device,
payload,
),
await this._baseApis.sendToDevice("m.room.encrypted", {
[userId]: {
[device.deviceId]: encryptedContent,
},
});
logger.debug(
`Re-shared key for session ${sessionId} with ${userId}:${device.deviceId}`,
);
};
/**
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
@@ -441,10 +550,10 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
await this._encryptAndSendKeysToDevices(
session, key.chain_index, userDeviceMaps[i], payload,
);
console.log(`Completed megolm keyshare in ${this._roomId} `
logger.log(`Completed megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length})`);
} catch (e) {
console.log(`megolm keyshare in ${this._roomId} `
logger.log(`megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
throw e;
@@ -463,7 +572,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
const self = this;
console.log(`Starting to encrypt event for ${this._roomId}`);
logger.log(`Starting to encrypt event for ${this._roomId}`);
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
// check if any of these devices are not yet known to the user.
@@ -782,12 +891,12 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
!sessionId ||
!content.session_key
) {
console.error("key event is missing fields");
logger.error("key event is missing fields");
return;
}
if (!senderKey) {
console.error("key event has no sender key (not encrypted?)");
logger.error("key event has no sender key (not encrypted?)");
return;
}
@@ -804,13 +913,13 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
senderKey = content.sender_key;
if (!senderKey) {
console.error("forwarded_room_key event is missing sender_key field");
logger.error("forwarded_room_key event is missing sender_key field");
return;
}
const ed25519Key = content.sender_claimed_ed25519_key;
if (!ed25519Key) {
console.error(
logger.error(
`forwarded_room_key_event is missing sender_claimed_ed25519_key field`,
);
return;
@@ -823,8 +932,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed();
}
console.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
this._olmDevice.addInboundGroupSession(
logger.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
exportFormat,
@@ -839,8 +948,21 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId);
}).then(() => {
if (this._crypto.backupInfo) {
// don't wait for the keys to be backed up for the server
this._crypto.backupGroupSession(
content.room_id, senderKey, forwardingKeyChain,
content.session_id, content.session_key, keysClaimed,
exportFormat,
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
console.log("Failed to back up group session", e);
});
}
}).catch((e) => {
console.error(`Error handling m.room_key_event: ${e}`);
logger.error(`Error handling m.room_key_event: ${e}`);
});
};
@@ -882,7 +1004,7 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) {
return null;
}
console.log(
logger.log(
"sharing keys for session " + body.sender_key + "|"
+ body.session_id + " with device "
+ userId + ":" + deviceId,
@@ -956,6 +1078,22 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
session.sender_claimed_keys,
true,
).then(() => {
if (this._crypto.backupInfo) {
// don't wait for it to complete
this._crypto.backupGroupSession(
session.room_id,
session.sender_key,
session.forwarding_curve25519_key_chain,
session.session_id,
session.session_key,
session.sender_claimed_keys,
true,
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
console.log("Failed to back up group session", e);
});
}
// have another go at decrypting events sent with this session.
this._retryDecryption(session.sender_key, session.session_id);
});
+3 -2
View File
@@ -22,6 +22,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
const logger = require("../../logger");
const utils = require("../../utils");
const olmlib = require("../olmlib");
const DeviceInfo = require("../deviceinfo");
@@ -273,7 +274,7 @@ OlmDecryption.prototype._decryptMessage = async function(
const payload = await this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
console.log(
logger.log(
"Decrypted Olm message from " + theirDeviceIdentityKey +
" with session " + sessionId,
);
@@ -328,7 +329,7 @@ OlmDecryption.prototype._decryptMessage = async function(
);
}
console.log(
logger.log(
"created new inbound Olm session ID " +
res.session_id + " with " + theirDeviceIdentityKey,
);
+81
View File
@@ -0,0 +1,81 @@
/*
Copyright 2018 New Vector 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.
*/
import { randomString } from '../randomstring';
const DEFAULT_ITERATIONS = 500000;
export async function keyForExistingBackup(backupData, password) {
if (!global.Olm) {
throw new Error("Olm is not available");
}
const authData = backupData.auth_data;
if (!authData.private_key_salt || !authData.private_key_iterations) {
throw new Error(
"Salt and/or iterations not found: " +
"this backup cannot be restored with a passphrase",
);
}
return await deriveKey(
password, backupData.auth_data.private_key_salt,
backupData.auth_data.private_key_iterations,
);
}
export async function keyForNewBackup(password) {
if (!global.Olm) {
throw new Error("Olm is not available");
}
const salt = randomString(32);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS);
return { key, salt, iterations: DEFAULT_ITERATIONS };
}
async function deriveKey(password, salt, iterations) {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
// TODO: Implement this for node
throw new Error("Password-based backup is not avaiable on this platform");
}
const key = await subtleCrypto.importKey(
'raw',
new TextEncoder().encode(password),
{name: 'PBKDF2'},
false,
['deriveBits'],
);
const keybits = await subtleCrypto.deriveBits(
{
name: 'PBKDF2',
salt: new TextEncoder().encode(salt),
iterations: iterations,
hash: 'SHA-512',
},
key,
global.Olm.PRIVATE_KEY_LENGTH * 8,
);
return new Uint8Array(keybits);
}
+434 -32
View File
@@ -25,6 +25,7 @@ const anotherjson = require('another-json');
import Promise from 'bluebird';
import {EventEmitter} from 'events';
const logger = require("../logger");
const utils = require("../utils");
const OlmDevice = require("./OlmDevice");
const olmlib = require("./olmlib");
@@ -36,6 +37,13 @@ const DeviceList = require('./DeviceList').default;
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
export function isCryptoAvailable() {
return Boolean(global.Olm);
}
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
/**
* Cryptography bits
*
@@ -62,7 +70,7 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
*
* @param {RoomList} roomList An initialised RoomList object
*/
function Crypto(baseApis, sessionStore, userId, deviceId,
export default function Crypto(baseApis, sessionStore, userId, deviceId,
clientStore, cryptoStore, roomList) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
@@ -72,6 +80,14 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
this._cryptoStore = cryptoStore;
this._roomList = roomList;
// track whether this device's megolm keys are being backed up incrementally
// to the server or not.
// XXX: this should probably have a single source of truth from OlmAccount
this.backupInfo = null; // The info dict from /room_keys/version
this.backupKey = null; // The encryption key object
this._checkedForBackup = false; // Have we checked the server for a backup we can use?
this._sendingBackups = false; // Are we currently sending backups?
this._olmDevice = new OlmDevice(sessionStore, cryptoStore);
this._deviceList = new DeviceList(
baseApis, cryptoStore, sessionStore, this._olmDevice,
@@ -115,6 +131,15 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
// has happened for a given room. This is delayed
// to avoid loading room members as long as possible.
this._roomDeviceTrackingState = {};
// The timestamp of the last time we forced establishment
// of a new session for each device, in milliseconds.
// {
// userId: {
// deviceId: 1234567890000,
// },
// }
this._lastNewSessionForced = {};
}
utils.inherits(Crypto, EventEmitter);
@@ -124,6 +149,8 @@ utils.inherits(Crypto, EventEmitter);
* Returns a promise which resolves once the crypto module is ready for use.
*/
Crypto.prototype.init = async function() {
await global.Olm.init();
const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount());
let cryptoStoreHasAccount;
await this._cryptoStore.doTxn(
@@ -174,6 +201,120 @@ Crypto.prototype.init = async function() {
);
this._deviceList.saveIfDirty();
}
this._checkAndStartKeyBackup();
};
/**
* Check the server for an active key backup and
* if one is present and has a valid signature from
* one of the user's verified devices, start backing up
* to it.
*/
Crypto.prototype._checkAndStartKeyBackup = async function() {
console.log("Checking key backup status...");
if (this._baseApis.isGuest()) {
console.log("Skipping key backup check since user is guest");
this._checkedForBackup = true;
return;
}
let backupInfo;
try {
backupInfo = await this._baseApis.getKeyBackupVersion();
} catch (e) {
console.log("Error checking for active key backup", e);
if (e.httpStatus / 100 === 4) {
// well that's told us. we won't try again.
this._checkedForBackup = true;
}
return;
}
this._checkedForBackup = true;
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
if (trustInfo.usable && !this.backupInfo) {
console.log("Found usable key backup: enabling key backups");
this._baseApis.enableKeyBackup(backupInfo);
} else if (!trustInfo.usable && this.backupInfo) {
console.log("No usable key backup: disabling key backup");
this._baseApis.disableKeyBackup();
} else if (!trustInfo.usable && !this.backupInfo) {
console.log("No usable key backup: not enabling key backup");
}
};
/**
* Forces a re-check of the key backup and enables/disables it
* as appropriate
*
* @param {object} backupInfo Backup info from /room_keys/version endpoint
*/
Crypto.prototype.checkKeyBackup = async function(backupInfo) {
this._checkedForBackup = false;
await this._checkAndStartKeyBackup();
};
/**
* @param {object} backupInfo key backup info dict from /room_keys/version
* @return {object} {
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
* sigs: [
* valid: [bool],
* device: [DeviceInfo],
* ]
* }
*/
Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
const ret = {
usable: false,
sigs: [],
};
if (
!backupInfo ||
!backupInfo.algorithm ||
!backupInfo.auth_data ||
!backupInfo.auth_data.public_key ||
!backupInfo.auth_data.signatures
) {
console.log("Key backup is absent or missing required data");
return ret;
}
const mySigs = backupInfo.auth_data.signatures[this._userId];
if (!mySigs || mySigs.length === 0) {
console.log("Ignoring key backup because it lacks any signatures from this user");
return ret;
}
for (const keyId of Object.keys(mySigs)) {
const device = this._deviceList.getStoredDevice(
this._userId, keyId.split(':')[1], // XXX: is this how we're supposed to get the device ID?
);
if (!device) {
console.log("Ignoring signature from unknown key " + keyId);
continue;
}
const sigInfo = { device };
try {
await olmlib.verifySignature(
this._olmDevice,
backupInfo.auth_data,
this._userId,
device.deviceId,
device.getFingerprint(),
);
sigInfo.valid = true;
} catch (e) {
console.log("Bad signature from device " + device.deviceId, e);
sigInfo.valid = false;
}
ret.sigs.push(sigInfo);
}
ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified());
return ret;
};
/**
@@ -196,7 +337,7 @@ Crypto.prototype.registerEventHandlers = function(eventEmitter) {
try {
crypto._onRoomMembership(event, member, oldMembership);
} catch (e) {
console.error("Error handling membership change:", e);
logger.error("Error handling membership change:", e);
}
});
@@ -214,6 +355,7 @@ Crypto.prototype.start = function() {
/** Stop background processes related to crypto */
Crypto.prototype.stop = function() {
this._outgoingRoomKeyRequestManager.stop();
this._deviceList.stop();
};
/**
@@ -381,7 +523,7 @@ function _maybeUploadOneTimeKeys(crypto) {
// create any more keys.
return uploadLoop(keyCount);
}).catch((e) => {
console.error("Error uploading one-time keys", e.stack || e);
logger.error("Error uploading one-time keys", e.stack || e);
}).finally(() => {
// reset _oneTimeKeyCount to prevent start uploading based on old data.
// it will be set again on the next /sync-response
@@ -588,7 +730,7 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
// identity key of the device which set up the Megolm session.
const device = this._deviceList.getDeviceByIdentityKey(
event.getSender(), algorithm, senderKey,
algorithm, senderKey,
);
if (device === null) {
@@ -606,13 +748,13 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
const claimedKey = event.getClaimedEd25519Key();
if (!claimedKey) {
console.warn("Event " + event.getId() + " claims no ed25519 key: " +
logger.warn("Event " + event.getId() + " claims no ed25519 key: " +
"cannot verify sending device");
return null;
}
if (claimedKey !== device.getFingerprint()) {
console.warn(
logger.warn(
"Event " + event.getId() + " claims ed25519 key " + claimedKey +
"but sender device has key " + device.getFingerprint());
return null;
@@ -658,7 +800,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
const existingConfig = this._roomList.getRoomEncryption(roomId);
if (existingConfig) {
if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
console.error("Ignoring m.room.encryption event which requests " +
logger.error("Ignoring m.room.encryption event which requests " +
"a change of config in " + roomId);
return;
}
@@ -705,7 +847,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
}
if (!this._lazyLoadMembers) {
console.log("Enabling encryption in " + roomId + "; " +
logger.log("Enabling encryption in " + roomId + "; " +
"starting to track device lists for all users therein");
await this.trackRoomDevices(roomId);
@@ -717,7 +859,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
this._deviceList.refreshOutdatedDeviceLists();
}
} else {
console.log("Enabling encryption in " + roomId);
logger.log("Enabling encryption in " + roomId);
}
};
@@ -738,7 +880,7 @@ Crypto.prototype.trackRoomDevices = function(roomId) {
if (!room) {
throw new Error(`Unable to start tracking devices in unknown room ${roomId}`);
}
console.log(`Starting to track devices for room ${roomId} ...`);
logger.log(`Starting to track devices for room ${roomId} ...`);
const members = await room.getEncryptionTargetMembers();
members.forEach((m) => {
this._deviceList.startTrackingDeviceList(m.userId);
@@ -815,6 +957,7 @@ Crypto.prototype.exportRoomKeys = async function() {
const sess = this._olmDevice.exportInboundGroupSession(
s.senderKey, s.sessionId, s.sessionData,
);
delete sess.first_known_index;
sess.algorithm = olmlib.MEGOLM_ALGORITHM;
exportedSessions.push(sess);
});
@@ -834,7 +977,7 @@ Crypto.prototype.importRoomKeys = function(keys) {
return Promise.map(
keys, (key) => {
if (!key.room_id || !key.algorithm) {
console.warn("ignoring room key entry with missing fields", key);
logger.warn("ignoring room key entry with missing fields", key);
return null;
}
@@ -843,6 +986,153 @@ Crypto.prototype.importRoomKeys = function(keys) {
},
);
};
/**
* Schedules sending all keys waiting to be sent to the backup, if not already
* scheduled. Retries if necessary.
*/
Crypto.prototype._scheduleKeyBackupSend = async function() {
if (this._sendingBackups) return;
try {
// wait between 0 and 10 seconds, to avoid backup
// requests from different clients hitting the server all at
// the same time when a new key is sent
const delay = Math.random() * 10000;
await Promise.delay(delay);
let numFailures = 0; // number of consecutive failures
while (1) {
if (!this.backupKey) {
return;
}
try {
const numBackedUp =
await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
if (numBackedUp === 0) {
// no sessions left needing backup: we're done
return;
}
numFailures = 0;
} catch (err) {
numFailures++;
console.log("Key backup request failed", err);
if (err.data) {
if (
err.data.errcode == 'M_NOT_FOUND' ||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
) {
// Backup version has changed or this backup version
// has been deleted
this.emit("crypto.keyBackupFailed", err.data.errcode);
throw err;
}
}
}
if (numFailures) {
// exponential backoff if we have failures
await Promise.delay(1000 * Math.pow(2, Math.min(numFailures - 1, 4)));
}
}
} finally {
this._sendingBackups = false;
}
};
/**
* Take some e2e keys waiting to be backed up and send them
* to the backup.
*
* @param {integer} limit Maximum number of keys to back up
* @returns {integer} Number of sessions backed up
*/
Crypto.prototype._backupPendingKeys = async function(limit) {
const sessions = await this._cryptoStore.getSessionsNeedingBackup(limit);
if (!sessions.length) {
return 0;
}
const data = {};
for (const session of sessions) {
const roomId = session.sessionData.room_id;
if (data[roomId] === undefined) {
data[roomId] = {sessions: {}};
}
const sessionData = await this._olmDevice.exportInboundGroupSession(
session.senderKey, session.sessionId, session.sessionData,
);
sessionData.algorithm = olmlib.MEGOLM_ALGORITHM;
delete sessionData.session_id;
delete sessionData.room_id;
const firstKnownIndex = sessionData.first_known_index;
delete sessionData.first_known_index;
const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData));
const forwardedCount =
(sessionData.forwarding_curve25519_key_chain || []).length;
const device = this._deviceList.getDeviceByIdentityKey(
olmlib.MEGOLM_ALGORITHM, session.senderKey,
);
data[roomId]['sessions'][session.sessionId] = {
first_message_index: firstKnownIndex,
forwarded_count: forwardedCount,
is_verified: !!(device && device.isVerified()),
session_data: encrypted,
};
}
await this._baseApis.sendKeyBackup(
undefined, undefined, this.backupInfo.version,
{rooms: data},
);
await this._cryptoStore.unmarkSessionsNeedingBackup(sessions);
return sessions.length;
};
Crypto.prototype.backupGroupSession = async function(
roomId, senderKey, forwardingCurve25519KeyChain,
sessionId, sessionKey, keysClaimed,
exportFormat,
) {
if (!this.backupInfo) {
throw new Error("Key backups are not enabled");
}
await this._cryptoStore.markSessionsNeedingBackup([{
senderKey: senderKey,
sessionId: sessionId,
}]);
// don't wait for this to complete: it will delay so
// happens in the background
this._scheduleKeyBackupSend();
};
Crypto.prototype.backupAllGroupSessions = async function(version) {
await this._cryptoStore.doTxn(
'readwrite',
[
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_BACKUP,
],
(txn) => {
this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
if (session !== null) {
this._cryptoStore.markSessionsNeedingBackup([session], txn);
}
});
},
);
let numKeysBackedUp;
do {
numKeysBackedUp = await this._backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
} while (numKeysBackedUp > 0);
};
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
/**
* Encrypt an event according to the configuration of the room.
@@ -963,7 +1253,7 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
requestBody, recipients,
).catch((e) => {
// this normally means we couldn't talk to the store
console.error(
logger.error(
'Error requesting key for event', e,
);
}).done();
@@ -980,7 +1270,7 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) {
Crypto.prototype.cancelRoomKeyRequest = function(requestBody, andResend) {
this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody, andResend)
.catch((e) => {
console.warn("Error clearing pending room key requests", e);
logger.warn("Error clearing pending room key requests", e);
}).done();
};
@@ -998,7 +1288,7 @@ Crypto.prototype.onCryptoEvent = async function(event) {
// finished processing the sync, in onSyncCompleted.
await this.setRoomEncryption(roomId, content, true);
} catch (e) {
console.error("Error configuring encryption in room " + roomId +
logger.error("Error configuring encryption in room " + roomId +
":", e);
}
};
@@ -1014,7 +1304,7 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) {
// scratch, so mark everything as untracked. onCryptoEvent will
// be called for all e2e rooms during the processing of the sync,
// at which point we'll start tracking all the users of that room.
console.log("Initial sync performed - resetting device tracking state");
logger.log("Initial sync performed - resetting device tracking state");
this._deviceList.stopTrackingAllDeviceLists();
this._roomDeviceTrackingState = {};
}
@@ -1125,6 +1415,8 @@ Crypto.prototype._onToDeviceEvent = function(event) {
this._onRoomKeyEvent(event);
} else if (event.getType() == "m.room_key_request") {
this._onRoomKeyRequestEvent(event);
} else if (event.getContent().msgtype === "m.bad.encrypted") {
this._onToDeviceBadEncrypted(event);
} else if (event.isBeingDecrypted()) {
// once the event has been decrypted, try again
event.once('Event.decrypted', (ev) => {
@@ -1132,7 +1424,7 @@ Crypto.prototype._onToDeviceEvent = function(event) {
});
}
} catch (e) {
console.error("Error handling toDeviceEvent:", e);
logger.error("Error handling toDeviceEvent:", e);
}
};
@@ -1146,14 +1438,109 @@ Crypto.prototype._onRoomKeyEvent = function(event) {
const content = event.getContent();
if (!content.room_id || !content.algorithm) {
console.error("key event is missing fields");
logger.error("key event is missing fields");
return;
}
if (!this._checkedForBackup) {
// don't bother awaiting on this - the important thing is that we retry if we
// haven't managed to check before
this._checkAndStartKeyBackup();
}
const alg = this._getRoomDecryptor(content.room_id, content.algorithm);
alg.onRoomKeyEvent(event);
};
/**
* Handle a toDevice event that couldn't be decrypted
*
* @private
* @param {module:models/event.MatrixEvent} event undecryptable event
*/
Crypto.prototype._onToDeviceBadEncrypted = async function(event) {
const content = event.getWireContent();
const sender = event.getSender();
const algorithm = content.algorithm;
const deviceKey = content.sender_key;
if (sender === undefined || deviceKey === undefined || deviceKey === undefined) {
return;
}
// check when we last forced a new session with this device: if we've already done so
// recently, don't do it again.
this._lastNewSessionForced[sender] = this._lastNewSessionForced[sender] || {};
const lastNewSessionForced = this._lastNewSessionForced[sender][deviceKey] || 0;
if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) {
logger.debug(
"New session already forced with device " + sender + ":" + deviceKey +
" at " + lastNewSessionForced + ": not forcing another",
);
return;
}
// establish a new olm session with this device since we're failing to decrypt messages
// on a current session.
// Note that an undecryptable message from another device could easily be spoofed -
// is there anything we can do to mitigate this?
const device = this._deviceList.getDeviceByIdentityKey(algorithm, deviceKey);
if (!device) {
logger.info(
"Couldn't find device for identity key " + deviceKey +
": not re-establishing session",
);
return;
}
const devicesByUser = {};
devicesByUser[sender] = [device];
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser, true,
);
this._lastNewSessionForced[sender][deviceKey] = Date.now();
// Now send a blank message on that session so the other side knows about it.
// (The keyshare request is sent in the clear so that won't do)
// We send this first such that, as long as the toDevice messages arrive in the
// same order we sent them, the other end will get this first, set up the new session,
// then get the keyshare request and send the key over this new session (because it
// is the session it has most recently received a message on).
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
sender,
device,
{type: "m.dummy"},
);
await this._baseApis.sendToDevice("m.room.encrypted", {
[sender]: {
[device.deviceId]: encryptedContent,
},
});
// Most of the time this probably won't be necessary since we'll have queued up a key request when
// we failed to decrypt the message and will be waiting a bit for the key to arrive before sending
// it. This won't always be the case though so we need to re-send any that have already been sent
// to avoid races.
const requestsToResend =
await this._outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest(
sender, device.deviceId,
);
for (const keyReq of requestsToResend) {
this.cancelRoomKeyRequest(keyReq.requestBody, true);
}
};
/**
* Handle a change in the membership state of a member of a room
*
@@ -1184,12 +1571,12 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
// by calling _trackRoomDevices
if (this._roomDeviceTrackingState[roomId]) {
if (member.membership == 'join') {
console.log('Join event for ' + member.userId + ' in ' + roomId);
logger.log('Join event for ' + member.userId + ' in ' + roomId);
// make sure we are tracking the deviceList for this user
this._deviceList.startTrackingDeviceList(member.userId);
} else if (member.membership == 'invite' &&
this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) {
console.log('Invite event for ' + member.userId + ' in ' + roomId);
logger.log('Invite event for ' + member.userId + ' in ' + roomId);
this._deviceList.startTrackingDeviceList(member.userId);
}
}
@@ -1256,7 +1643,7 @@ Crypto.prototype._processReceivedRoomKeyRequests = async function() {
this._processReceivedRoomKeyRequestCancellation(cancellation),
);
} catch (e) {
console.error(`Error processing room key requsts: ${e}`);
logger.error(`Error processing room key requsts: ${e}`);
} finally {
this._processingRoomKeyRequests = false;
}
@@ -1275,13 +1662,31 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
const roomId = body.room_id;
const alg = body.algorithm;
console.log(`m.room_key_request from ${userId}:${deviceId}` +
logger.log(`m.room_key_request from ${userId}:${deviceId}` +
` for ${roomId} / ${body.session_id} (id ${req.requestId})`);
if (userId !== this._userId) {
// TODO: determine if we sent this device the keys already: in
// which case we can do so again.
console.log("Ignoring room key request from other user for now");
if (!this._roomEncryptors[roomId]) {
logger.debug(`room key request for unencrypted room ${roomId}`);
return;
}
const encryptor = this._roomEncryptors[roomId];
const device = this._deviceList.getStoredDevice(userId, deviceId);
if (!device) {
logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`);
return;
}
try {
await encryptor.reshareKeyWithDevice(
body.sender_key, body.session_id, userId, device,
);
} catch (e) {
logger.warn(
"Failed to re-share keys for session " + body.session_id +
" with device " + userId + ":" + device.deviceId, e,
);
}
return;
}
@@ -1291,18 +1696,18 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
// if we don't have a decryptor for this room/alg, we don't have
// the keys for the requested events, and can drop the requests.
if (!this._roomDecryptors[roomId]) {
console.log(`room key request for unencrypted room ${roomId}`);
logger.log(`room key request for unencrypted room ${roomId}`);
return;
}
const decryptor = this._roomDecryptors[roomId][alg];
if (!decryptor) {
console.log(`room key request for unknown alg ${alg} in room ${roomId}`);
logger.log(`room key request for unknown alg ${alg} in room ${roomId}`);
return;
}
if (!await decryptor.hasKeysForKeyRequest(req)) {
console.log(
logger.log(
`room key request for unknown session ${roomId} / ` +
body.session_id,
);
@@ -1316,7 +1721,7 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
// if the device is is verified already, share the keys
const device = this._deviceList.getStoredDevice(userId, deviceId);
if (device && device.isVerified()) {
console.log('device is already verified: sharing keys');
logger.log('device is already verified: sharing keys');
req.share();
return;
}
@@ -1333,7 +1738,7 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) {
Crypto.prototype._processReceivedRoomKeyRequestCancellation = async function(
cancellation,
) {
console.log(
logger.log(
`m.room_key_request cancellation for ${cancellation.userId}:` +
`${cancellation.deviceId} (id ${cancellation.requestId})`,
);
@@ -1518,6 +1923,3 @@ class IncomingRoomKeyRequestCancellation {
* @event module:client~MatrixClient#"crypto.warning"
* @param {string} type One of the strings listed above
*/
/** */
module.exports = Crypto;
+18 -9
View File
@@ -23,6 +23,7 @@ limitations under the License.
import Promise from 'bluebird';
const anotherjson = require('another-json');
const logger = require("../logger");
const utils = require("../utils");
/**
@@ -35,6 +36,11 @@ module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* matrix algorithm tag for megolm backups
*/
module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
/**
* Encrypt an event payload for an Olm device
@@ -65,7 +71,7 @@ module.exports.encryptMessageForDevice = async function(
return;
}
console.log(
logger.log(
"Using sessionid " + sessionId + " for device " +
recipientUserId + ":" + recipientDevice.deviceId,
);
@@ -115,14 +121,17 @@ module.exports.encryptMessageForDevice = async function(
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
* map from userid to list of devices to ensure sessions for
*
* @param {bolean} force If true, establish a new session even if one already exists.
* Optional.
*
* @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 = async function(
olmDevice, baseApis, devicesByUser,
olmDevice, baseApis, devicesByUser, force,
) {
const devicesWithoutSession = [
// [userId, deviceId], ...
@@ -140,7 +149,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
const sessionId = await olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
if (sessionId === null || force) {
devicesWithoutSession.push([userId, deviceId]);
}
result[userId][deviceId] = {
@@ -176,7 +185,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
if (result[userId][deviceId].sessionId && !force) {
// we already have a result for this device
continue;
}
@@ -190,7 +199,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
}
if (!oneTimeKey) {
console.warn(
logger.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId,
);
@@ -219,7 +228,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
deviceInfo.getFingerprint(),
);
} catch (e) {
console.error(
logger.error(
"Unable to verify signature on one-time key for device " +
userId + ":" + deviceId + ":", e,
);
@@ -233,12 +242,12 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
);
} catch (e) {
// possibly a bad key
console.error("Error starting session with device " +
logger.error("Error starting session with device " +
userId + ":" + deviceId + ": " + e);
return null;
}
console.log("Started new sessionid " + sid +
logger.log("Started new sessionid " + sid +
" for device " + userId + ":" + deviceId);
return sid;
}
+66
View File
@@ -0,0 +1,66 @@
/*
Copyright 2018 New Vector 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.
*/
import bs58 from 'bs58';
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
export function encodeRecoveryKey(key) {
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
let parity = 0;
for (let i = 0; i < buf.length - 1; ++i) {
parity ^= buf[i];
}
buf[buf.length - 1] = parity;
const base58key = bs58.encode(buf);
return base58key.match(/.{1,4}/g).join(" ");
}
export function decodeRecoveryKey(recoverykey) {
const result = bs58.decode(recoverykey.replace(/ /g, ''));
let parity = 0;
for (const b of result) {
parity ^= b;
}
if (parity !== 0) {
throw new Error("Incorrect parity");
}
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
throw new Error("Incorrect prefix");
}
}
if (
result.length !==
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1
) {
throw new Error("Incorrect length");
}
return result.slice(
OLM_RECOVERY_KEY_PREFIX.length,
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
);
}
@@ -16,9 +16,11 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../logger';
import utils from '../../utils';
export const VERSION = 6;
export const VERSION = 7;
/**
* Implementation of a CryptoStore which is backed by an existing
@@ -38,7 +40,7 @@ export class Backend {
// attempts to delete the database will block (and subsequent
// attempts to re-create it will also block).
db.onversionchange = (ev) => {
console.log(`versionchange for indexeddb ${this._dbName}: closing`);
logger.log(`versionchange for indexeddb ${this._dbName}: closing`);
db.close();
};
}
@@ -64,7 +66,7 @@ export class Backend {
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
if (existing) {
// this entry matches the request - return it.
console.log(
logger.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
@@ -75,7 +77,7 @@ export class Backend {
// we got to the end of the list without finding a match
// - add the new request.
console.log(
logger.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
@@ -204,6 +206,42 @@ export class Backend {
return promiseifyTxn(txn).then(() => result);
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
let stateIndex = 0;
const results = [];
function onsuccess(ev) {
const cursor = ev.target.result;
if (cursor) {
const keyReq = cursor.value;
if (keyReq.recipients.includes({userId, deviceId})) {
results.push(keyReq);
}
cursor.continue();
} else {
// try the next state in the list
stateIndex++;
if (stateIndex >= wantedStates.length) {
// no matches
return;
}
const wantedState = wantedStates[stateIndex];
const cursorReq = ev.target.source.openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
}
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex];
const cursorReq = store.index("state").openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => results);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
@@ -226,7 +264,7 @@ export class Backend {
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
logger.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${data.state}`,
);
@@ -264,7 +302,7 @@ export class Backend {
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
logger.warn(
`Cannot delete room key request in state ${data.state} `
+ `(expected ${expectedState})`,
);
@@ -312,7 +350,10 @@ export class Backend {
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
results[cursor.value.sessionId] = cursor.value.session;
results[cursor.value.sessionId] = {
session: cursor.value.session,
lastReceivedMessageTs: cursor.value.lastReceivedMessageTs,
};
cursor.continue();
} else {
try {
@@ -330,7 +371,10 @@ export class Backend {
getReq.onsuccess = function() {
try {
if (getReq.result) {
func(getReq.result.session);
func({
session: getReq.result.session,
lastReceivedMessageTs: getReq.result.lastReceivedMessageTs,
});
} else {
func(null);
}
@@ -340,9 +384,32 @@ export class Backend {
};
}
storeEndToEndSession(deviceKey, sessionId, session, txn) {
getAllEndToEndSessions(txn, func) {
const objectStore = txn.objectStore("sessions");
objectStore.put({deviceKey, sessionId, session});
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
func(cursor.value);
cursor.continue();
} else {
try {
func(null);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
const objectStore = txn.objectStore("sessions");
objectStore.put({
deviceKey,
sessionId,
session: sessionInfo.session,
lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs,
});
}
// Inbound group sessions
@@ -400,7 +467,7 @@ export class Backend {
ev.stopPropagation();
// ...and this stops it from aborting the transaction
ev.preventDefault();
console.log(
logger.log(
"Ignoring duplicate inbound group session: " +
senderCurve25519Key + " / " + sessionId,
);
@@ -460,6 +527,71 @@ export class Backend {
};
}
// session backups
getSessionsNeedingBackup(limit) {
return new Promise((resolve, reject) => {
const sessions = [];
const txn = this._db.transaction(
["sessions_needing_backup", "inbound_group_sessions"],
"readonly",
);
txn.onerror = reject;
txn.oncomplete = function() {
resolve(sessions);
};
const objectStore = txn.objectStore("sessions_needing_backup");
const sessionStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
const sessionGetReq = sessionStore.get(cursor.key);
sessionGetReq.onsuccess = function() {
sessions.push({
senderKey: sessionGetReq.result.senderCurve25519Key,
sessionId: sessionGetReq.result.sessionId,
sessionData: sessionGetReq.result.session,
});
};
if (!limit || sessions.length < limit) {
cursor.continue();
}
}
};
});
}
unmarkSessionsNeedingBackup(sessions) {
const txn = this._db.transaction("sessions_needing_backup", "readwrite");
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.delete([session.senderKey, session.sessionId]);
req.onsuccess = resolve;
req.onerror = reject;
});
}));
}
markSessionsNeedingBackup(sessions, txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.put({
senderCurve25519Key: session.senderKey,
sessionId: session.sessionId,
});
req.onsuccess = resolve;
req.onerror = reject;
});
}));
}
doTxn(mode, stores, func) {
const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn);
@@ -471,7 +603,7 @@ export class Backend {
}
export function upgradeDatabase(db, oldVersion) {
console.log(
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
@@ -498,6 +630,11 @@ export function upgradeDatabase(db, oldVersion) {
if (oldVersion < 6) {
db.createObjectStore("rooms");
}
if (oldVersion < 7) {
db.createObjectStore("sessions_needing_backup", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
// Expand as needed.
}
+116 -19
View File
@@ -17,9 +17,11 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../../logger';
import LocalStorageCryptoStore from './localStorage-crypto-store';
import MemoryCryptoStore from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import {InvalidCryptoStoreError} from '../../errors';
/**
* Internal module. indexeddb storage for e2e.
@@ -64,7 +66,7 @@ export default class IndexedDBCryptoStore {
return;
}
console.log(`connecting to indexeddb ${this._dbName}`);
logger.log(`connecting to indexeddb ${this._dbName}`);
const req = this._indexedDB.open(
this._dbName, IndexedDBCryptoStoreBackend.VERSION,
@@ -77,7 +79,7 @@ export default class IndexedDBCryptoStore {
};
req.onblocked = () => {
console.log(
logger.log(
`can't yet open IndexedDBCryptoStore because it is open elsewhere`,
);
};
@@ -89,20 +91,42 @@ export default class IndexedDBCryptoStore {
req.onsuccess = (r) => {
const db = r.target.result;
console.log(`connected to indexeddb ${this._dbName}`);
logger.log(`connected to indexeddb ${this._dbName}`);
resolve(new IndexedDBCryptoStoreBackend.Backend(db));
};
}).then((backend) => {
// Edge has IndexedDB but doesn't support compund keys which we use fairly extensively.
// Try a dummy query which will fail if the browser doesn't support compund keys, so
// we can fall back to a different backend.
return backend.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
(txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => {
return backend;
},
);
}).catch((e) => {
console.warn(
if (e.name === 'VersionError') {
logger.warn("Crypto DB is too new for us to use!", e);
// don't fall back to a different store: the user has crypto data
// in this db so we should use it or nothing at all.
throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW);
}
logger.warn(
`unable to connect to indexeddb ${this._dbName}` +
`: falling back to localStorage store: ${e}`,
);
return new LocalStorageCryptoStore(global.localStorage);
}).catch((e) => {
console.warn(
`unable to open localStorage: falling back to in-memory store: ${e}`,
);
return new MemoryCryptoStore();
try {
return new LocalStorageCryptoStore(global.localStorage);
} catch (e) {
logger.warn(
`unable to open localStorage: falling back to in-memory store: ${e}`,
);
return new MemoryCryptoStore();
}
});
return this._backendPromise;
@@ -120,11 +144,11 @@ export default class IndexedDBCryptoStore {
return;
}
console.log(`Removing indexeddb instance: ${this._dbName}`);
logger.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this._indexedDB.deleteDatabase(this._dbName);
req.onblocked = () => {
console.log(
logger.log(
`can't yet delete IndexedDBCryptoStore because it is open elsewhere`,
);
};
@@ -134,14 +158,14 @@ export default class IndexedDBCryptoStore {
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${this._dbName}`);
logger.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
}).catch((e) => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that people can
// still use the app.
console.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
});
}
@@ -193,6 +217,24 @@ export default class IndexedDBCryptoStore {
});
}
/**
* Look for room key requests by target device and state
*
* @param {string} userId Target user ID
* @param {string} deviceId Target device ID
* @param {Array<Number>} wantedStates list of acceptable states
*
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
});
}
/**
* Look for an existing room key request by id and state, and update it if
* found
@@ -270,7 +312,10 @@ export default class IndexedDBCryptoStore {
* @param {string} sessionId The ID of the session to retrieve
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSession(deviceKey, sessionId, txn, func) {
this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func);
@@ -282,22 +327,36 @@ export default class IndexedDBCryptoStore {
* @param {string} deviceKey The public key of the other device.
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func);
}
/**
* Retrieve all end-to-end sessions
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called one for each session with
* an object with, deviceKey, lastReceivedMessageTs, sessionId
* and session keys.
*/
getAllEndToEndSessions(txn, func) {
this._backendPromise.value().getAllEndToEndSessions(txn, func);
}
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID for this end-to-end session.
* @param {string} session Base64 encoded end-to-end session.
* @param {string} sessionInfo Session information object
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndSession(deviceKey, sessionId, session, txn) {
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
this._backendPromise.value().storeEndToEndSession(
deviceKey, sessionId, session, txn,
deviceKey, sessionId, sessionInfo, txn,
);
}
@@ -407,6 +466,43 @@ export default class IndexedDBCryptoStore {
this._backendPromise.value().getEndToEndRooms(txn, func);
}
// session backups
/**
* Get the inbound group sessions that need to be backed up.
* @param {integer} limit The maximum number of sessions to retrieve. 0
* for no limit.
* @returns {Promise} resolves to an array of inbound group sessions
*/
getSessionsNeedingBackup(limit) {
return this._connect().then((backend) => {
return backend.getSessionsNeedingBackup(limit);
});
}
/**
* Unmark sessions as needing to be backed up.
* @param {Array<object>} sessions The sessions that need to be backed up.
* @returns {Promise} resolves when the sessions are unmarked
*/
unmarkSessionsNeedingBackup(sessions) {
return this._connect().then((backend) => {
return backend.unmarkSessionsNeedingBackup(sessions);
});
}
/**
* Mark sessions as needing to be backed up.
* @param {Array<object>} sessions The sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are marked
*/
markSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.markSessionsNeedingBackup(sessions, txn);
});
}
/**
* Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may
@@ -440,3 +536,4 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
+85 -5
View File
@@ -15,6 +15,8 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../logger';
import MemoryCryptoStore from './memory-crypto-store.js';
/**
@@ -32,6 +34,7 @@ const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
@@ -65,7 +68,21 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
}
_getEndToEndSessions(deviceKey, txn, func) {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
const fixedSessions = {};
// fix up any old sessions to be objects rather than just the base64 pickle
for (const [sid, val] of Object.entries(sessions || {})) {
if (typeof val === 'string') {
fixedSessions[sid] = {
session: val,
};
} else {
fixedSessions[sid] = val;
}
}
return fixedSessions;
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
@@ -77,9 +94,20 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
func(this._getEndToEndSessions(deviceKey) || {});
}
storeEndToEndSession(deviceKey, sessionId, session, txn) {
getAllEndToEndSessions(txn, func) {
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) {
const deviceKey = this.store.key(i).split('/')[1];
for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
func(sess);
}
}
}
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
const sessions = this._getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = session;
sessions[sessionId] = sessionInfo;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions,
);
@@ -165,6 +193,58 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
func(result);
}
getSessionsNeedingBackup(limit) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessions = [];
for (const session in sessionsNeedingBackup) {
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
// see getAllEndToEndInboundGroupSessions for the magic number explanations
const senderKey = session.substr(0, 43);
const sessionId = session.substr(44);
this.getEndToEndInboundGroupSession(
senderKey, sessionId, null,
(sessionData) => {
sessions.push({
senderKey: senderKey,
sessionId: sessionId,
sessionData: sessionData,
});
},
);
if (limit && session.length >= limit) {
break;
}
}
}
return Promise.resolve(sessions);
}
unmarkSessionsNeedingBackup(sessions) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId];
}
setJsonItem(
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
);
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true;
}
setJsonItem(
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
);
return Promise.resolve();
}
/**
* Delete all data from this store.
*
@@ -199,8 +279,8 @@ function getJsonItem(store, key) {
// JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key));
} catch (e) {
console.log("Error: Failed to get key %s: %s", key, e.stack || e);
console.log(e.stack);
logger.log("Error: Failed to get key %s: %s", key, e.stack || e);
logger.log(e.stack);
}
return null;
}
+65 -6
View File
@@ -17,6 +17,7 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../../logger';
import utils from '../../utils';
/**
@@ -41,6 +42,8 @@ export default class MemoryCryptoStore {
this._deviceData = null;
// roomId -> Opaque roomInfo object
this._rooms = {};
// Set of {senderCurve25519Key+'/'+sessionId}
this._sessionsNeedingBackup = {};
}
/**
@@ -71,7 +74,7 @@ export default class MemoryCryptoStore {
if (existing) {
// this entry matches the request - return it.
console.log(
logger.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
@@ -81,7 +84,7 @@ export default class MemoryCryptoStore {
// we got to the end of the list without finding a match
// - add the new request.
console.log(
logger.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
@@ -144,6 +147,19 @@ export default class MemoryCryptoStore {
return Promise.resolve(null);
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
const results = [];
for (const req of this._outgoingRoomKeyRequests) {
for (const state of wantedStates) {
if (req.state === state && req.recipients.includes({userId, deviceId})) {
results.push(req);
}
}
}
return Promise.resolve(results);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
@@ -163,7 +179,7 @@ export default class MemoryCryptoStore {
}
if (req.state != expectedState) {
console.warn(
logger.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${req.state}`,
);
@@ -194,7 +210,7 @@ export default class MemoryCryptoStore {
}
if (req.state != expectedState) {
console.warn(
logger.warn(
`Cannot delete room key request in state ${req.state} `
+ `(expected ${expectedState})`,
);
@@ -233,13 +249,21 @@ export default class MemoryCryptoStore {
func(this._sessions[deviceKey] || {});
}
storeEndToEndSession(deviceKey, sessionId, session, txn) {
getAllEndToEndSessions(txn, func) {
for (const deviceSessions of Object.values(this._sessions)) {
for (const sess of Object.values(deviceSessions)) {
func(sess);
}
}
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
let deviceSessions = this._sessions[deviceKey];
if (deviceSessions === undefined) {
deviceSessions = {};
this._sessions[deviceKey] = deviceSessions;
}
deviceSessions[sessionId] = session;
deviceSessions[sessionId] = sessionInfo;
}
// Inbound Group Sessions
@@ -295,6 +319,41 @@ export default class MemoryCryptoStore {
func(this._rooms);
}
getSessionsNeedingBackup(limit) {
const sessions = [];
for (const session in this._sessionsNeedingBackup) {
if (this._inboundGroupSessions[session]) {
sessions.push({
senderKey: session.substr(0, 43),
sessionId: session.substr(44),
sessionData: this._inboundGroupSessions[session],
});
if (limit && session.length >= limit) {
break;
}
}
}
return Promise.resolve(sessions);
}
unmarkSessionsNeedingBackup(sessions) {
for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId;
delete this._sessionsNeedingBackup[sessionKey];
}
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId;
this._sessionsNeedingBackup[sessionKey] = true;
}
return Promise.resolve();
}
// Session key backups
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
+46
View File
@@ -0,0 +1,46 @@
// can't just do InvalidStoreError extends Error
// because of http://babeljs.io/docs/usage/caveats/#classes
export function InvalidStoreError(reason, value) {
const message = `Store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.value = value;
return instance;
}
InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING";
InvalidStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidStoreError, Error);
export function InvalidCryptoStoreError(reason) {
const message = `Crypto store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.name = 'InvalidCryptoStoreError';
return instance;
}
InvalidCryptoStoreError.TOO_NEW = "TOO_NEW";
InvalidCryptoStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidCryptoStoreError, Error);
+11
View File
@@ -51,6 +51,17 @@ function Filter(userId, filterId) {
this.definition = {};
}
Filter.LAZY_LOADING_MESSAGES_FILTER = {
lazy_load_members: true,
};
Filter.LAZY_LOADING_SYNC_FILTER = {
room: {
state: Filter.LAZY_LOADING_MESSAGES_FILTER,
},
};
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
+2
View File
@@ -752,6 +752,8 @@ module.exports.MatrixHttpApi.prototype = {
method: method,
withCredentials: false,
qs: queryParams,
qsStringifyOptions: opts.qsStringifyOptions,
useQuerystring: true,
body: data,
json: false,
timeout: localTimeoutMs,
+36
View File
@@ -0,0 +1,36 @@
/*
Copyright 2018 André Jaenisch
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.
*/
/**
* @module logger
*/
const log = require("loglevel");
// This is to demonstrate, that you can use any namespace you want.
// Namespaces allow you to turn on/off the logging for specific parts of the
// application.
// An idea would be to control this via an environment variable (on Node.js).
// See https://www.npmjs.com/package/debug to see how this could be implemented
// Part of #332 is introducing a logging library in the first place.
const DEFAULT_NAME_SPACE = "matrix";
const logger = log.getLogger(DEFAULT_NAME_SPACE);
logger.setLevel(log.levels.DEBUG);
/**
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
* Can be tailored down to specific use cases if needed.
*/
module.exports = logger;
+4
View File
@@ -34,6 +34,8 @@ module.exports.SyncAccumulator = require("./sync-accumulator");
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
/** The {@link module:http-api.MatrixError|MatrixError} class. */
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:errors.InvalidStoreError|InvalidStoreError} class. */
module.exports.InvalidStoreError = require("./errors").InvalidStoreError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room|Room} class. */
@@ -65,6 +67,8 @@ module.exports.Filter = require("./filter");
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
/** The {@link module:interactive-auth} class. */
module.exports.InteractiveAuth = require("./interactive-auth");
/** The {@link module:auto-discovery|AutoDiscovery} class. */
module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery;
module.exports.MemoryCryptoStore =
+8 -36
View File
@@ -59,7 +59,6 @@ function RoomMember(roomId, userId) {
member: null,
};
this._isOutOfBand = false;
this._supersedesOutOfBand = false;
this._updateModifiedTime();
}
utils.inherits(RoomMember, EventEmitter);
@@ -80,31 +79,6 @@ RoomMember.prototype.isOutOfBand = function() {
return this._isOutOfBand;
};
/**
* Does the member supersede an incoming out-of-band
* member? If so the out-of-band member should be ignored.
* @return {bool}
*/
RoomMember.prototype.supersedesOutOfBand = function() {
return this._supersedesOutOfBand;
};
/**
* Mark the member as superseding the future incoming
* out-of-band members.
*/
RoomMember.prototype.markSupersedesOutOfBand = function() {
this._supersedesOutOfBand = true;
};
/**
* Clear the member superseding the future incoming
* out-of-band members, as loading finished or failed.
*/
RoomMember.prototype.clearSupersedesOutOfBand = function() {
this._supersedesOutOfBand = false;
};
/**
* Update this room member's membership event. May fire "RoomMember.name" if
* this event updates this member's name.
@@ -324,26 +298,24 @@ function calculateDisplayName(selfUserId, displayName, roomState) {
return selfUserId;
}
if (!roomState) {
return displayName;
}
// First check if the displayname is something we consider truthy
// after stripping it of zero width characters and padding spaces
const strippedDisplayName = utils.removeHiddenChars(displayName);
if (!strippedDisplayName) {
if (!utils.removeHiddenChars(displayName)) {
return selfUserId;
}
if (!roomState) {
return displayName;
}
// Next check if the name contains something that look like a mxid
// If it does, it may be someone trying to impersonate someone else
// Show full mxid in this case
// Also show mxid if there are other people with the same displayname
// ignoring any zero width chars (unicode 200B-200D)
// if their displayname is made up of just zero width chars, show full mxid
// Also show mxid if there are other people with the same or similar
// displayname, after hidden character removal.
let disambiguate = /@.+:.+/.test(displayName);
if (!disambiguate) {
const userIds = roomState.getUserIdsWithDisplayName(strippedDisplayName);
const userIds = roomState.getUserIdsWithDisplayName(displayName);
disambiguate = userIds.some((u) => u !== selfUserId);
}
+22 -42
View File
@@ -75,6 +75,8 @@ function RoomState(roomId, oobMemberFlags = undefined) {
// userId: RoomMember
};
this._updateModifiedTime();
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
this._displayNameToUserIds = {};
this._userIdsToDisplayNames = {};
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
@@ -154,6 +156,16 @@ RoomState.prototype.getMembers = function() {
return utils.values(this.members);
};
/**
* Get all RoomMembers in this room, excluding the user IDs provided.
* @param {Array<string>} excludedIds The user IDs to exclude.
* @return {Array<RoomMember>} A list of RoomMembers.
*/
RoomState.prototype.getMembersExcept = function(excludedIds) {
return utils.values(this.members)
.filter((m) => !excludedIds.includes(m.userId));
};
/**
* Get a room member by their user ID.
* @param {string} userId The room member's user ID.
@@ -180,7 +192,7 @@ RoomState.prototype.getSentinelMember = function(userId) {
sentinel = new RoomMember(this.roomId, userId);
const member = this.members[userId];
if (member) {
sentinel.setMembershipEvent(member.events.member);
sentinel.setMembershipEvent(member.events.member, this);
}
this._sentinels[userId] = sentinel;
}
@@ -220,8 +232,7 @@ RoomState.prototype.clone = function() {
// if loading is in progress (through _oobMemberFlags)
// since these are not new members, we're merely copying them
// set the status to not started
// after copying, we set back the status and
// copy the superseding flag from the current state
// after copying, we set back the status
const status = this._oobMemberFlags.status;
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
@@ -249,14 +260,6 @@ RoomState.prototype.clone = function() {
copyMember.markOutOfBand();
}
});
} else if (this._oobMemberFlags.status == OOB_STATUS_INPROGRESS) {
// copy markSupersedesOutOfBand flags
this.getMembers().forEach((member) => {
if (member.supersedesOutOfBand()) {
const copyMember = copy.getMember(member.userId);
copyMember.markSupersedesOutOfBand();
}
});
}
return copy;
@@ -341,11 +344,6 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
const member = self._getOrCreateMember(userId, event);
member.setMembershipEvent(event, self);
// if out of band members are loading,
// mark the member as more recent
if (self._oobMemberFlags.status == OOB_STATUS_INPROGRESS) {
member.markSupersedesOutOfBand();
}
self._updateMember(member);
self.emit("RoomState.members", event, self, member);
@@ -434,12 +432,6 @@ RoomState.prototype.markOutOfBandMembersFailed = function() {
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
// the request failed, there is nothing to supersede
// in case of a retry, these event would not supersede the
// retry anymore.
this.getMembers().forEach((m) => {
m.clearSupersedesOutOfBand();
});
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
@@ -483,27 +475,15 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) {
}
const userId = stateEvent.getStateKey();
const existingMember = this.getMember(userId);
if (existingMember) {
const existingMemberEvent = existingMember.events.member;
// ignore out of band members with events we are
// already aware of.
if (existingMemberEvent.getId() === stateEvent.getId()) {
return;
}
// this member was updated since we started
// loading the out of band members.
// Ignore the out of band member and clear
// the "supersedes" flag as the out of members are now loaded
if (existingMember.supersedesOutOfBand()) {
existingMember.clearSupersedesOutOfBand();
return;
}
// never replace members received as part of the sync
if (existingMember && !existingMember.isOutOfBand()) {
return;
}
const member = this._getOrCreateMember(userId, stateEvent);
member.setMembershipEvent(stateEvent);
member.setMembershipEvent(stateEvent, this);
// needed to know which members need to be stored seperately
// as the are not part of the sync accumulator
// as they are not part of the sync accumulator
// this is cleared by setMembershipEvent so when it's updated through /sync
member.markOutOfBand();
@@ -511,7 +491,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) {
this._setStateEvent(stateEvent);
this._updateMember(member);
this.emit("RoomState.members", {}, stateEvent, member);
this.emit("RoomState.members", stateEvent, this, member);
};
/**
@@ -551,12 +531,12 @@ RoomState.prototype.getLastModifiedTime = function() {
};
/**
* Get user IDs with the specified display name.
* Get user IDs with the specified or similar display names.
* @param {string} displayName The display name to get user IDs from.
* @return {string[]} An array of user IDs or an empty array.
*/
RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[displayName] || [];
return this._displayNameToUserIds[utils.removeHiddenChars(displayName)] || [];
};
/**
+42 -26
View File
@@ -178,7 +178,7 @@ function Room(roomId, client, myUserId, opts) {
// read by megolm; boolean value - null indicates "use global value"
this._blacklistUnverifiedDevices = null;
this._syncedMembership = null;
this._selfMembership = null;
this._summaryHeroes = null;
// awaited by getEncryptionTargetMembers while room members are loading
@@ -198,7 +198,10 @@ utils.inherits(Room, EventEmitter);
*/
Room.prototype.getVersion = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) return null;
if (!createEvent) {
console.warn("Room " + this.room_id + " does not have an m.room.create event");
return '1';
}
const ver = createEvent.getContent()['room_version'];
if (ver === undefined) return '1';
return ver;
@@ -257,13 +260,7 @@ Room.prototype.getLiveTimeline = function() {
* @return {string} the membership type (join | leave | invite) for the logged in user
*/
Room.prototype.getMyMembership = function() {
if (this.myUserId) {
const me = this.getMember(this.myUserId);
if (me) {
return me.membership;
}
}
return this._syncedMembership;
return this._selfMembership;
};
/**
@@ -278,7 +275,7 @@ Room.prototype.getDMInviter = function() {
return me.getDMInviter();
}
}
if (this._syncedMembership === "invite") {
if (this._selfMembership === "invite") {
// fall back to summary information
const memberCount = this.getInvitedAndJoinedMemberCount();
if (memberCount == 2 && this._summaryHeroes.length) {
@@ -362,8 +359,15 @@ Room.prototype.getAvatarFallbackMember = function() {
* Sets the membership this room was received as during sync
* @param {string} membership join | leave | invite
*/
Room.prototype.setSyncedMembership = function(membership) {
this._syncedMembership = membership;
Room.prototype.updateMyMembership = function(membership) {
const prevMembership = this._selfMembership;
this._selfMembership = membership;
if (prevMembership !== membership) {
if (membership === "leave") {
this._cleanupAfterLeaving();
}
this.emit("Room.myMembership", this, membership, prevMembership);
}
};
Room.prototype._loadMembersFromServer = async function() {
@@ -413,8 +417,21 @@ Room.prototype.loadMembersIfNeeded = function() {
// the OOB members
this.currentState.markOutOfBandMembersStarted();
const promise = this._loadMembers().then(({memberEvents, fromServer}) => {
this.currentState.setOutOfBandMembers(memberEvents);
const inMemoryUpdate = this._loadMembers().then((result) => {
this.currentState.setOutOfBandMembers(result.memberEvents);
// now the members are loaded, start to track the e2e devices if needed
if (this._client.isRoomEncrypted(this.roomId)) {
this._client._crypto.trackRoomDevices(this.roomId);
}
return result.fromServer;
}).catch((err) => {
// allow retries on fail
this._membersPromise = null;
this.currentState.markOutOfBandMembersFailed();
throw err;
});
// update members in storage, but don't wait for it
inMemoryUpdate.then((fromServer) => {
if (fromServer) {
const oobMembers = this.currentState.getMembers()
.filter((m) => m.isOutOfBand())
@@ -431,17 +448,12 @@ Room.prototype.loadMembersIfNeeded = function() {
});
}
}).catch((err) => {
// allow retries on fail
this._membersPromise = null;
this.currentState.markOutOfBandMembersFailed();
throw err;
// as this is not awaited anywhere,
// at least show the error in the console
console.error(err);
});
this._membersPromise = promise;
// now the members are loaded, start to track the e2e devices if needed
if (this._client.isRoomEncrypted(this.roomId)) {
this._client._crypto.trackRoomDevices(this.roomId);
}
this._membersPromise = inMemoryUpdate;
return this._membersPromise;
};
@@ -462,7 +474,7 @@ Room.prototype.clearLoadedMembersIfNeeded = async function() {
* called when sync receives this room in the leave section
* to do cleanup after leaving a room. Possibly called multiple times.
*/
Room.prototype.onLeft = function() {
Room.prototype._cleanupAfterLeaving = function() {
this.clearLoadedMembersIfNeeded().catch((err) => {
console.error(`error after clearing loaded members from ` +
`room ${this.roomId} after leaving`);
@@ -987,6 +999,10 @@ Room.prototype.addPendingEvent = function(event, txnId) {
this._txnToEvent[txnId] = event;
if (this._opts.pendingEventOrdering == "detached") {
if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
console.warn("Setting event as NOT_SENT due to messages in the same state");
event.status = EventStatus.NOT_SENT;
}
this._pendingEventList.push(event);
} else {
for (let i = 0; i < this._timelineSets.length; i++) {
@@ -1151,7 +1167,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
this.removeEvent(oldEventId);
}
this.emit("Room.localEchoUpdated", event, this, event.getId(), oldStatus);
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
};
@@ -1594,7 +1610,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
function memberNamesToRoomName(names, count = (names.length + 1)) {
const countWithoutMe = count - 1;
if (!names.length) {
return count <= 1 ? "Empty room" : null;
return "Empty room";
} else if (names.length === 1 && countWithoutMe <= 1) {
return names[0];
} else if (names.length === 2 && countWithoutMe <= 2) {
+14
View File
@@ -39,6 +39,9 @@ limitations under the License.
* when a user was last active.
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
* an approximation and that the user should be seen as active 'now'
* @prop {string} _unstable_statusMessage The status message for the user, if known. This is
* different from the presenceStatusMsg in that this is not tied to
* the user's presence, and should be represented differently.
* @prop {Object} events The events describing this user.
* @prop {MatrixEvent} events.presence The m.presence event for this user.
*/
@@ -46,6 +49,7 @@ function User(userId) {
this.userId = userId;
this.presence = "offline";
this.presenceStatusMsg = null;
this._unstable_statusMessage = "";
this.displayName = userId;
this.rawDisplayName = userId;
this.avatarUrl = null;
@@ -179,6 +183,16 @@ User.prototype.getLastActiveTs = function() {
return this.lastPresenceTs - this.lastActiveAgo;
};
/**
* Manually set the user's status message.
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
*/
User.prototype._unstable_updateStatusMessage = function(event) {
if (!event.getContent()) this._unstable_statusMessage = "";
else this._unstable_statusMessage = event.getContent()["status"];
this._updateModifiedTime();
};
/**
* The User class.
*/
+3 -20
View File
@@ -14,6 +14,9 @@ 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.
*/
import {escapeRegExp, globToRegexp} from "./utils";
/**
* @module pushprocessor
*/
@@ -26,10 +29,6 @@ const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'
* @param {Object} client The Matrix client object to use
*/
function PushProcessor(client) {
const escapeRegExp = function(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const cachedGlobToRegex = {
// $glob: RegExp,
};
@@ -244,22 +243,6 @@ function PushProcessor(client) {
return cachedGlobToRegex[glob];
};
const globToRegexp = function(glob) {
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\\-/, '-');
return '[' + first + second + ']';
});
return pat;
};
const valueForDottedKey = function(key, ev) {
const parts = key.split('.');
let val;
+26
View File
@@ -0,0 +1,26 @@
/*
Copyright 2018 New Vector 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.
*/
export function randomString(len) {
let ret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < len; ++i) {
ret += chars.charAt(Math.floor(Math.random() * chars.length));
}
return ret;
}
+58 -35
View File
@@ -19,7 +19,7 @@ import Promise from 'bluebird';
import SyncAccumulator from "../sync-accumulator";
import utils from "../utils";
const VERSION = 2;
const VERSION = 3;
function createDatabase(db) {
// Make user store, clobber based on user ID. (userId property of User objects)
@@ -41,6 +41,12 @@ function upgradeSchemaV2(db) {
oobMembersStore.createIndex("room", "room_id");
}
function upgradeSchemaV3(db) {
db.createObjectStore("client_options",
{ keyPath: ["clobber"]});
}
/**
* Helper method to collect results from a Cursor and promiseify it.
* @param {ObjectStore|Index} store The store to perform openCursor on.
@@ -77,7 +83,7 @@ function txnAsPromise(txn) {
resolve(event);
};
txn.onerror = function(event) {
reject(event);
reject(event.target.error);
};
});
}
@@ -88,7 +94,7 @@ function reqAsEventPromise(req) {
resolve(event);
};
req.onerror = function(event) {
reject(event);
reject(event.target.error);
};
});
}
@@ -123,6 +129,7 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
this.db = null;
this._disconnected = true;
this._syncAccumulator = new SyncAccumulator();
this._isNewlyCreated = false;
};
@@ -153,11 +160,15 @@ LocalIndexedDBStoreBackend.prototype = {
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
);
if (oldVersion < 1) { // The database did not previously exist.
this._isNewlyCreated = true;
createDatabase(db);
}
if (oldVersion < 2) {
upgradeSchemaV2(db);
}
if (oldVersion < 3) {
upgradeSchemaV3(db);
}
// Expand as needed.
};
@@ -185,6 +196,10 @@ LocalIndexedDBStoreBackend.prototype = {
return this._init();
});
},
/** @return {bool} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(this._isNewlyCreated);
},
/**
* Having connected, load initial data from the database and prepare for use
@@ -265,42 +280,28 @@ LocalIndexedDBStoreBackend.prototype = {
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
setOutOfBandMembers: function(roomId, membershipEvents) {
setOutOfBandMembers: async function(roomId, membershipEvents) {
console.log(`LL: backend about to store ${membershipEvents.length}` +
` members for ${roomId}`);
function ignoreResult() {}
// run everything in a promise so anything that throws will reject
return new Promise((resolve) =>{
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
const store = tx.objectStore("oob_membership_events");
const eventPuts = membershipEvents.map((e) => {
const putPromise = reqAsEventPromise(store.put(e));
// ignoring the result makes sure we discard the IDB success event
// ASAP, and not create a potentially big array containing them
// unneccesarily later on by calling Promise.all.
return putPromise.then(ignoreResult);
});
// aside from all the events, we also write a marker object to the store
// to mark the fact that OOB members have been written for this room.
// It's possible that 0 members need to be written as all where previously know
// but we still need to know whether to return null or [] from getOutOfBandMembers
// where null means out of band members haven't been stored yet for this room
const markerObject = {
room_id: roomId,
oob_written: true,
state_key: 0,
};
const markerPut = reqAsEventPromise(store.put(markerObject));
const allPuts = eventPuts.concat(markerPut);
// ignore the empty array Promise.all creates
// as this method should just resolve
// to undefined on success
resolve(Promise.all(allPuts).then(ignoreResult));
}).then(() => {
console.log(`LL: backend done storing for ${roomId}!`);
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
const store = tx.objectStore("oob_membership_events");
membershipEvents.forEach((e) => {
store.put(e);
});
// aside from all the events, we also write a marker object to the store
// to mark the fact that OOB members have been written for this room.
// It's possible that 0 members need to be written as all where previously know
// but we still need to know whether to return null or [] from getOutOfBandMembers
// where null means out of band members haven't been stored yet for this room
const markerObject = {
room_id: roomId,
oob_written: true,
state_key: 0,
};
store.put(markerObject);
await txnAsPromise(tx);
console.log(`LL: backend done storing for ${roomId}!`);
},
clearOutOfBandMembers: async function(roomId) {
@@ -543,6 +544,28 @@ LocalIndexedDBStoreBackend.prototype = {
});
});
},
getClientOptions: function() {
return Promise.resolve().then(() => {
const txn = this.db.transaction(["client_options"], "readonly");
const store = txn.objectStore("client_options");
return selectQuery(store, undefined, (cursor) => {
if (cursor.value && cursor.value && cursor.value.options) {
return cursor.value.options;
}
}).then((results) => results[0]);
});
},
storeClientOptions: async function(options) {
const txn = this.db.transaction(["client_options"], "readwrite");
const store = txn.objectStore("client_options");
store.put({
clobber: "-", // constant key so will always clobber
options: options,
}); // put == UPSERT
await txnAsPromise(txn);
},
};
export default LocalIndexedDBStoreBackend;
+15 -2
View File
@@ -65,7 +65,10 @@ RemoteIndexedDBStoreBackend.prototype = {
clearDatabase: function() {
return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
},
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return this._doCmd('isNewlyCreated');
},
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
@@ -114,6 +117,14 @@ RemoteIndexedDBStoreBackend.prototype = {
return this._doCmd('clearOutOfBandMembers', [roomId]);
},
getClientOptions: function() {
return this._doCmd('getClientOptions');
},
storeClientOptions: function(options) {
return this._doCmd('storeClientOptions', [options]);
},
/**
* Load all user presence events from the database. This is not cached.
* @return {Promise<Object[]>} A list of presence events in their raw form.
@@ -173,7 +184,9 @@ RemoteIndexedDBStoreBackend.prototype = {
if (msg.command == 'cmd_success') {
def.resolve(msg.result);
} else {
def.reject(msg.error);
const error = new Error(msg.error.message);
error.name = msg.error.name;
def.reject(error);
}
} else {
console.warn("Unrecognised message from worker: " + msg);
+14 -2
View File
@@ -67,6 +67,9 @@ class IndexedDBStoreWorker {
case 'connect':
prom = this.backend.connect();
break;
case 'isNewlyCreated':
prom = this.backend.isNewlyCreated();
break;
case 'clearDatabase':
prom = this.backend.clearDatabase().then((result) => {
// This returns special classes which can't be cloned
@@ -101,10 +104,16 @@ class IndexedDBStoreWorker {
case 'setOutOfBandMembers':
prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]);
break;
case 'getClientOptions':
prom = this.backend.getClientOptions();
break;
case 'storeClientOptions':
prom = this.backend.storeClientOptions(msg.args[0]);
break;
}
if (prom === undefined) {
postMessage({
this.postMessage({
command: 'cmd_fail',
seq: msg.seq,
// Can't be an Error because they're not structured cloneable
@@ -126,7 +135,10 @@ class IndexedDBStoreWorker {
command: 'cmd_fail',
seq: msg.seq,
// Just send a string because Error objects aren't cloneable
error: "Error running command",
error: {
message: err.message,
name: err.name,
},
});
});
}
+13
View File
@@ -146,6 +146,11 @@ IndexedDBStore.prototype.getSavedSync = function() {
return this.backend.getSavedSync();
};
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
IndexedDBStore.prototype.isNewlyCreated = function() {
return this.backend.isNewlyCreated();
};
/**
* @return {Promise} If there is a saved sync, the nextBatch token
* for this sync, otherwise null.
@@ -246,4 +251,12 @@ IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
return this.backend.clearOutOfBandMembers(roomId);
};
IndexedDBStore.prototype.getClientOptions = function() {
return this.backend.getClientOptions();
};
IndexedDBStore.prototype.storeClientOptions = function(options) {
return this.backend.storeClientOptions(options);
};
module.exports.IndexedDBStore = IndexedDBStore;
+14
View File
@@ -55,6 +55,7 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
this._oobMembers = {
// roomId: [member events]
};
this._clientOptions = {};
};
module.exports.MatrixInMemoryStore.prototype = {
@@ -67,6 +68,10 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.syncToken;
},
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(true);
},
/**
* Set the token to stream from.
@@ -402,4 +407,13 @@ module.exports.MatrixInMemoryStore.prototype = {
this._oobMembers[roomId] = membershipEvents;
return Promise.resolve();
},
getClientOptions: function() {
return Promise.resolve(this._clientOptions);
},
storeClientOptions: function(options) {
this._clientOptions = Object.assign({}, options);
return Promise.resolve();
},
};
+13
View File
@@ -32,6 +32,11 @@ function StubStore() {
StubStore.prototype = {
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(true);
},
/**
* Get the sync token.
* @return {string}
@@ -276,6 +281,14 @@ StubStore.prototype = {
clearOutOfBandMembers: function() {
return Promise.resolve();
},
getClientOptions: function() {
return Promise.resolve();
},
storeClientOptions: function() {
return Promise.resolve();
},
};
/** Stub Store class. */
-1
View File
@@ -34,7 +34,6 @@ import utils from "./utils";
* rather than asking the server to do an initial sync on startup.
*/
class SyncAccumulator {
/**
* @param {Object} opts
* @param {Number=} opts.maxTimelineEntries The ideal maximum number of
+87 -8
View File
@@ -33,6 +33,8 @@ const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
import {InvalidStoreError} from './errors';
const DEBUG = true;
// /sync requests allow you to set a timeout= but the request may continue
@@ -100,6 +102,7 @@ function SyncApi(client, opts) {
this._connectionReturnedDefer = null;
this._notifEvents = []; // accumulator of sync events in the current sync response
this._failedSyncCount = 0; // Number of consecutive failed /sync requests
this._storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
if (client.getNotifTimelineSet()) {
client.reEmitter.reEmit(client.getNotifTimelineSet(),
@@ -123,6 +126,7 @@ SyncApi.prototype.createRoom = function(roomId) {
"Room.timelineReset",
"Room.localEchoUpdated",
"Room.accountData",
"Room.myMembership",
]);
this._registerStateListeners(room);
return room;
@@ -421,6 +425,26 @@ SyncApi.prototype.recoverFromSyncStartupError = async function(savedSyncPromise,
await keepaliveProm;
};
/**
* Is the lazy loading option different than in previous session?
* @param {bool} lazyLoadMembers current options for lazy loading
* @return {bool} whether or not the option has changed compared to the previous session */
SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) {
lazyLoadMembers = !!lazyLoadMembers;
// assume it was turned off before
// if we don't know any better
let lazyLoadMembersBefore = false;
const isStoreNewlyCreated = await this.client.store.isNewlyCreated();
if (!isStoreNewlyCreated) {
const prevClientOptions = await this.client.store.getClientOptions();
if (prevClientOptions) {
lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers;
}
return lazyLoadMembersBefore !== lazyLoadMembers;
}
return false;
};
/**
* Main entry point
*/
@@ -443,6 +467,8 @@ SyncApi.prototype.sync = function() {
// 1) We need to get push rules so we can check if events should bing as we get
// them from /sync.
// 2) We need to get/create a filter which we can use for /sync.
// 3) We need to check the lazy loading option matches what was used in the
// stored sync. If it doesn't, we can't use the stored sync.
async function getPushRules() {
try {
@@ -457,9 +483,47 @@ SyncApi.prototype.sync = function() {
getPushRules();
return;
}
getFilter(); // Now get the filter and start syncing
checkLazyLoadStatus(); // advance to the next stage
}
const checkLazyLoadStatus = async () => {
if (this.opts.lazyLoadMembers && client.isGuest()) {
this.opts.lazyLoadMembers = false;
}
if (this.opts.lazyLoadMembers) {
const supported = await client.doesServerSupportLazyLoading();
if (supported) {
this.opts.filter = await client.createFilter(
Filter.LAZY_LOADING_SYNC_FILTER,
);
} else {
console.log("LL: lazy loading requested but not supported " +
"by server, so disabling");
this.opts.lazyLoadMembers = false;
}
}
// need to vape the store when enabling LL and wasn't enabled before
const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers);
if (shouldClear) {
this._storeIsInvalid = true;
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING;
const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers);
this._updateSyncState("ERROR", { error });
// bail out of the sync loop now: the app needs to respond to this error.
// we leave the state as 'ERROR' which isn't great since this normally means
// we're retrying. The client must be stopped before clearing the stores anyway
// so the app should stop the client, clear the store and start it again.
console.warn("InvalidStoreError: store is not usable: stopping sync.");
return;
}
if (this.opts.lazyLoadMembers && this.opts.crypto) {
this.opts.crypto.enableLazyLoading();
}
await this.client._storeClientOptions();
getFilter(); // Now get the filter and start syncing
};
async function getFilter() {
let filter;
if (self.opts.filter) {
@@ -587,7 +651,12 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
console.error("Error processing cached sync", e.stack || e);
}
this._updateSyncState("PREPARED", syncEventData);
// Don't emit a prepared if we've bailed because the store is invalid:
// in this case the client will not be usable until stopped & restarted
// so this would be useless and misleading.
if (!this._storeIsInvalid) {
this._updateSyncState("PREPARED", syncEventData);
}
};
/**
@@ -976,9 +1045,10 @@ SyncApi.prototype._processSyncResponse = async function(
// Handle invites
inviteRooms.forEach(function(inviteObj) {
const room = inviteObj.room;
room.setSyncedMembership("invite");
const stateEvents =
self._mapSyncEventsFormat(inviteObj.invite_state, room);
room.updateMyMembership("invite");
self._processRoomEvents(room, stateEvents);
if (inviteObj.isBrandNewRoom) {
room.recalculate();
@@ -993,7 +1063,6 @@ SyncApi.prototype._processSyncResponse = async function(
// Handle joins
await Promise.mapSeries(joinRooms, async function(joinObj) {
const room = joinObj.room;
room.setSyncedMembership("join");
const stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
@@ -1009,6 +1078,8 @@ SyncApi.prototype._processSyncResponse = async function(
);
}
room.updateMyMembership("join");
joinObj.timeline = joinObj.timeline || {};
if (joinObj.isBrandNewRoom) {
@@ -1101,6 +1172,16 @@ SyncApi.prototype._processSyncResponse = async function(
if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) {
await self.opts.crypto.onCryptoEvent(e);
}
if (e.isState() && e.getType() === "im.vector.user_status") {
let user = client.store.getUser(e.getStateKey());
if (user) {
user._unstable_updateStatusMessage(e);
} else {
user = createNewUser(client, e.getStateKey());
user._unstable_updateStatusMessage(e);
client.store.storeUser(user);
}
}
}
await Promise.mapSeries(stateEvents, processRoomEvent);
@@ -1116,8 +1197,6 @@ SyncApi.prototype._processSyncResponse = async function(
// Handle leaves (e.g. kicked rooms)
leaveRooms.forEach(function(leaveObj) {
const room = leaveObj.room;
room.setSyncedMembership("leave");
const stateEvents =
self._mapSyncEventsFormat(leaveObj.state, room);
const timelineEvents =
@@ -1125,6 +1204,8 @@ SyncApi.prototype._processSyncResponse = async function(
const accountDataEvents =
self._mapSyncEventsFormat(leaveObj.account_data);
room.updateMyMembership("leave");
self._processRoomEvents(room, stateEvents, timelineEvents);
room.addAccountData(accountDataEvents);
@@ -1145,8 +1226,6 @@ SyncApi.prototype._processSyncResponse = async function(
accountDataEvents.forEach(function(e) {
client.emit("event", e);
});
room.onLeft();
});
// update the notification timeline, if appropriate.
+29 -2
View File
@@ -19,6 +19,8 @@ limitations under the License.
* @module utils
*/
const unhomoglyph = require('unhomoglyph');
/**
* Encode a dictionary of query parameters.
* @param {Object} params A dict of key/values to encode e.g.
@@ -328,7 +330,7 @@ const deepCompare = module.exports.deepCompare = function(x, y) {
}
// finally, compare each of x's keys with y
for (p in y) {
for (p in y) { // eslint-disable-line guard-for-in
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
@@ -665,10 +667,35 @@ module.exports.isNumber = function(value) {
/**
* Removes zero width chars, diacritics and whitespace from the string
* Also applies an unhomoglyph on the string, to prevent similar looking chars
* @param {string} str the string to remove hidden characters from
* @return {string} a string with the hidden characters removed
*/
module.exports.removeHiddenChars = function(str) {
return str.normalize('NFD').replace(removeHiddenCharsRegex, '');
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
};
const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g;
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
module.exports.escapeRegExp = escapeRegExp;
module.exports.globToRegexp = function(glob, extended) {
extended = typeof(extended) === 'boolean' ? extended : true;
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
if (extended) {
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\-/, '-');
return '[' + first + second + ']';
});
}
return pat;
};
+1 -1
View File
@@ -5,7 +5,7 @@ set -ex
npm run lint
# install Olm so that we can run the crypto tests.
npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz
npm run test