Compare commits

..

227 Commits

Author SHA1 Message Date
RiotRobot 43fee73924 v4.0.0-rc.1 2020-01-20 14:24:36 +00:00
RiotRobot b72e9cb36c Prepare changelog for v4.0.0-rc.1 2020-01-20 14:24:35 +00:00
J. Ryan Stinnett 737dcc1d29 Merge pull request #1154 from matrix-org/jryans/complete-sec-confused
Convert secret storage to new account data API
2020-01-20 11:30:55 +00:00
Travis Ralston dba08d230e Merge pull request #1157 from aaronraimist/v5-safe
Add v5 as a safe room version
2020-01-18 19:29:44 -07:00
Aaron Raimist 15fb363874 Add v5 as a safe room version
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2020-01-18 16:37:19 -06:00
J. Ryan Stinnett ceb4581f91 Convert secret storage to new account data API
This converts all secret storage to use a newer account data API which uses
cached data in stored when available, but also knows how to ask the homeserver
in case it's invoked during early client startup before the initial sync.

As a consequence, it means most secret storage APIs are now async.

Part of https://github.com/vector-im/riot-web/issues/11901
2020-01-17 17:56:05 +00:00
J. Ryan Stinnett 8217c0f05f Merge pull request #1153 from matrix-org/jryans/cross-signing-setup
Add API to get account data from server
2020-01-16 16:42:12 +00:00
J. Ryan Stinnett c5c27b3cb0 Add API to get account data from server
This adds an API account data getter that bypasses the local store and goes
directly to the homeserver.

Part of https://github.com/vector-im/riot-web/issues/11214
2020-01-15 21:17:53 +00:00
Travis Ralston 04bbfae08e Merge pull request #1151 from matrix-org/travis/sourcemaps
Fix sourcemaps by refactoring the build system
2020-01-15 10:11:25 -07:00
Travis Ralston b3efa73eda Fix conflict in megolm.js 2020-01-15 09:06:30 -07:00
Travis Ralston f3efac059c Merge branch 'develop' into travis/sourcemaps 2020-01-15 09:03:21 -07:00
Hubert Chathi 9fb4ed2ec0 Merge pull request #1146 from uhoreg/reporting_olm_error
record, report, and notify about olm errors
2020-01-15 09:37:43 -05:00
Hubert Chathi f19013143a fix indexedDB storage and retry decryption when we get an olm error 2020-01-14 23:47:05 -05:00
Hubert Chathi ea3ee9bea5 Merge pull request #1148 from matrix-org/erikj/per_user_device_messages
Send device messages for the same user in same API call.
2020-01-14 21:33:46 -05:00
Travis Ralston ccca6f4b6d Re-add dist script usage to js-sdk and release script 2020-01-14 14:44:06 -07:00
David Baker 6a583d2ba6 Merge pull request #1150 from matrix-org/dbkr/dont_error_on_unknown_devices
Add an option to ignore unverified devices
2020-01-14 17:11:47 +00:00
Travis Ralston 4049a32871 Fix imports in crypto index post-merge 2020-01-14 10:08:26 -07:00
Travis Ralston 331c9ce1ff [CONFLICT CHUNKS] Merge branch 'develop' into travis/sourcemaps 2020-01-14 10:04:36 -07:00
David Baker 81ab2aca37 spelling
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-01-14 14:10:17 +00:00
David Baker 564b8276bf Merge pull request #1144 from matrix-org/dbkr/key_backup_resign
Sign key backup with cross-signing key on upgrade
2020-01-14 11:30:06 +00:00
Erik Johnston b4a93d2dc3 Also apply cahnge to '_splitBlockedDevices' 2020-01-14 10:09:19 +00:00
Erik Johnston 260040b919 Rename var to match new function 2020-01-14 10:07:54 +00:00
J. Ryan Stinnett 8dbef8b68e Merge pull request #1145 from aaronraimist/lock
Emoji verification: Change name of 🔒 to lock
2020-01-13 21:32:49 +00:00
Travis Ralston 458b2d422d Merge branch 'develop' into travis/sourcemaps 2020-01-13 11:17:27 -07:00
David Baker ee51357dbc Add an option to ignore unverified devices
Hopefully all necessary information is on the docstring.

Default behaviour remains unchanged.
2020-01-13 17:37:38 +00:00
Hubert Chathi fa679e873d Merge pull request #1147 from uhoreg/separate_encrypted_content
use a separate object for each encrypted content
2020-01-13 10:17:32 -05:00
Erik Johnston ed3fded8e8 Send device messages for the same user in same API call.
Currently we split the device messages up to limit the number per call,
but that can end up splitting messages to a given users device over
separate API calls. This is fine, but means that the server can't e.g.
bundle them into a single EDU for remote users or sanity check that the
client is sending to the right set of devices (i.e. its device list
cache isn't wrong).
2020-01-13 13:43:00 +00:00
RiotRobot 92df82bfa9 Merge branch 'master' into develop 2020-01-13 12:55:18 +00:00
RiotRobot 0dc9c27651 v3.0.0 2020-01-13 12:52:23 +00:00
RiotRobot f6f54c35a3 Prepare changelog for v3.0.0 2020-01-13 12:52:22 +00:00
Hubert Chathi 0a9959bffb use a separate object for each encrypted content
so that we don't duplicate the ciphertext for everyone
2020-01-11 15:49:01 -05:00
Hubert Chathi b3a16cb852 lint (and add a comment) 2020-01-10 13:36:11 -05:00
Aaron Raimist 9beb259333 Emoji verification: Change name of 🔒 to lock
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2020-01-09 21:55:43 -06:00
Hubert Chathi 63c57e8e02 record, report, and notify about olm errors 2020-01-09 22:19:35 -05:00
David Baker 0448a7ea68 Sign key backup with cross-signing key on upgrade
Add a signature from the cross-signing master key to the key
backup when upgrading the key backup into cross-signing.

For https://github.com/vector-im/riot-web/issues/11747
2020-01-09 20:46:36 +00:00
Travis Ralston 5bd005b28a Merge pull request #1143 from matrix-org/travis/sourcemaps-dev
Sourcemaps: develop -> feature branch
2020-01-07 15:30:56 -07:00
Travis Ralston 3aec6367d1 Fix OlmDevice import for "algorithms" to fix tests 2020-01-07 15:09:46 -07:00
Travis Ralston cea3831c20 Fix merge conflicts
This is done outside of the merge to highlight the changes, hopefully.
2020-01-07 14:43:36 -07:00
Travis Ralston 18ccceca2d [CONFLICT CHUNKS] Merge branch 'develop' into travis/sourcemaps-dev 2020-01-07 14:37:17 -07:00
Travis Ralston fffcdcb514 Merge pull request #1134 from matrix-org/travis/babel7-wp-media
Use a safer import/export scheme for the ContentRepo utilities
2020-01-07 14:27:36 -07:00
Hubert Chathi efadf374d6 Merge pull request #1142 from uhoreg/reporting_no_key_fix
Fix error handling in decryptGroupMessage
2020-01-07 11:36:53 -05:00
Hubert Chathi 55ecb40190 don't keep processing if we have an error 2020-01-07 11:25:30 -05:00
Hubert Chathi 01f6b3dfc6 notify devices when we don't send them keys (#1135)
and handle incoming notifications
2020-01-06 17:47:22 -05:00
RiotRobot 786590eadc v3.0.0-rc.1 2020-01-06 13:54:48 +00:00
RiotRobot c9174188ba Prepare changelog for v3.0.0-rc.1 2020-01-06 13:54:47 +00:00
Michael Telatynski 64fb79e0be Merge pull request #1141 from matrix-org/t3chguy/fuzzier_disambiguate
Make displayName disambiguation more fuzzy especially against RTL/LTR content
2020-01-06 13:12:39 +00:00
Michael Telatynski 088ff5d0aa Merge pull request #1129 from matrix-org/t3chguy/m_too_large
stop trying to resend event if we get M_TOO_LARGE
2020-01-06 11:42:27 +00:00
Michael Telatynski 99e58b0297 Make displayName disambiguation more fuzzy especially against RTL/LTR content
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-01-05 19:40:04 +00:00
Travis Ralston f4d1c5c006 Switch back to plain export functions instead of class 2020-01-03 12:16:54 -07:00
David Baker f8a1e98de1 Merge pull request #1139 from matrix-org/dbkr/fix_create_key_backup_crosssigning_disabled
Fix creating a key backup with cross signing diabled
2020-01-03 14:52:16 +00:00
David Baker 5487cf2070 Fix callback check
We need to check for getCrossSisgningKey but that was added
unconditionally elsewhere - only add it if we actually have a
getSecretStorageKey callback to use.
2020-01-03 14:36:04 +00:00
J. Ryan Stinnett e998be3a9b Fix typos in comments 2020-01-03 14:21:10 +00:00
David Baker d70767ef3a Merge pull request #1138 from matrix-org/dbkr/key_backup_checkdevicetrust
Use checkDeviceTrust with key backup
2020-01-03 14:03:50 +00:00
David Baker fbb355c5c9 Thank you once again, o great linter, for saving our lines from being too long 2020-01-03 14:02:38 +00:00
David Baker 20bc8071fc Fix creating a key backup with cross signing diabled
It broke if no scret key callback was supplied but a cross-signing
identity did exist (as hopefully explained in comment).

Fixes https://github.com/vector-im/riot-web/issues/11763
2020-01-03 13:52:36 +00:00
David Baker 0438c6c51c Oh great linter, your wisdom knows no bounds. 2020-01-03 13:37:55 +00:00
David Baker b39abba41e Use checkDeviceTrust with key backup
We did check if it was signed with a cross signing key which should
be the norm going forward, but for completeness, use the proper
cross-signing ernabled check for sigs from individual devices too.

Also adds a deviceTrust member to the signature so the app can see
the cross-signing trust status ofthe device without having to
recalculate it.
2020-01-03 13:32:31 +00:00
Travis Ralston e52e2f10bf Merge pull request #1131 from matrix-org/travis/babel7-wp-main
Add additional properties to package.json for riot-web's webpack
2020-01-02 10:51:52 -07:00
Travis Ralston dfc19e79f1 Merge pull request #1133 from matrix-org/travis/babel7-wp-idb
Fix import for indexeddb crypto store
2019-12-27 12:04:11 -07:00
Travis Ralston f59bd3da7a Merge pull request #1132 from matrix-org/travis/babel7-wp-request
Use the right request when creating clients
2019-12-27 11:56:52 -07:00
Travis Ralston 50791e3aa7 Make ContentRepo a class for easier importing
Exporting it the way we were was causing problems for webpack way down
the line, so we export it differently here to get around that. We also
have to fix all the import references so we import the right thing.
2019-12-22 20:51:30 -07:00
Travis Ralston 8211b2358f Fix import for indexeddb crypto store 2019-12-22 20:48:24 -07:00
Travis Ralston f2e1f3393d Add additional properties to package.json for riot-web's webpack
See https://github.com/vector-im/riot-web/pull/11679/commits/a1c9551bc8a1a6d61afed7e87ff7cebb3042a5ac
2019-12-22 20:47:45 -07:00
Travis Ralston 0ffec0a32d Use the right request when creating clients 2019-12-22 20:45:04 -07:00
Travis Ralston 1e5e705458 Regen lockfile 2019-12-19 17:10:32 -07:00
Travis Ralston f2af6ea60d Merge pull request #1127 from matrix-org/travis/babel7-btargets
Target NodeJS 10, minified browser bundle, and other publishing/package things
2019-12-19 17:08:57 -07:00
Travis Ralston de9187fee2 Merge branch 'travis/sourcemaps' into travis/babel7-btargets 2019-12-19 17:08:50 -07:00
Travis Ralston 5eed091185 Merge pull request #1126 from matrix-org/travis/babel7-updated-sourcemaps
Re-focus sourcemap generation
2019-12-19 17:08:03 -07:00
Travis Ralston 06644b5748 Merge pull request #1125 from matrix-org/travis/babel7-cleanup
Remove ancient polyfill for prototype inheritance
2019-12-19 17:07:56 -07:00
Travis Ralston bb853f65e0 Merge pull request #1124 from matrix-org/travis/babel7-test-sourcemaps
Remove "source-map-support" from tests because it makes sourcemaps worse
2019-12-19 17:07:49 -07:00
Travis Ralston eb830dd014 Merge pull request #1123 from matrix-org/travis/babel7-strict
Remove ancient "use strict" annotations
2019-12-19 17:07:41 -07:00
Travis Ralston de82d1e90c Merge pull request #1122 from matrix-org/travis/babel7-src-es6
Use ES6 imports/exports instead of older CommonJS ones
2019-12-19 17:07:29 -07:00
Travis Ralston 53e838083c Fix terser for new sourcemap handling 2019-12-19 14:04:01 -07:00
Travis Ralston 975368de8f Externalize our sourcemaps for the browser bundle 2019-12-19 13:58:08 -07:00
Travis Ralston 89173be055 Update README to describe build targets better 2019-12-19 13:42:47 -07:00
David Baker fe2bdd027e Merge pull request #1128 from matrix-org/dbkr/keybackup_migrate
Add support for passthrough SSSS secrets
2019-12-19 19:55:39 +00:00
Travis Ralston b376a7c399 Improve minification
uglifify was being applied inline, which only resulted in one asset. With this we've gone back to using `terser` to generate the minified version of the browser bundle, which also exports sourcemaps interpretted from browserify. 

The babelify options have changed to ensure that browser-safe code gets published so terser can more effectively parse it. It doesn't like things like classes it seems, but is fine with let/const and such. The preset-env preset automatically knows it is targeting a browser.
2019-12-19 12:25:28 -07:00
Travis Ralston 2df262d877 Reword concerns about default exports 2019-12-19 11:56:47 -07:00
David Baker 320ab050fe Stray p 2019-12-19 17:28:07 +00:00
David Baker 1816d7aa4c comment 2019-12-19 17:27:15 +00:00
David Baker 41b763f331 Just get the private key from the decryption object 2019-12-19 17:25:28 +00:00
Michael Telatynski 36db57615d stop retrying to send event if we get M_TOO_LARGE 2019-12-19 13:21:05 +00:00
David Baker 8f7ed1dc15 Lint 2019-12-19 11:50:25 +00:00
David Baker 83a8a0cf21 Add support for passthrough SSSS secrets
So we can migrate key backup keys

Adding a passthrough secret itself isn't exposed outside of the
js-sdk: hopefully this should only ever be necessary for this
bootstrap process which the js-sdk handles.
2019-12-19 11:23:57 +00:00
Travis Ralston ffb0e27efa We don't need no dist 2019-12-18 11:37:21 -07:00
Travis Ralston e71c4b3bc4 Publish src to npm as well
So downstream projects can use it via /src if they really want to.
2019-12-17 19:35:00 -07:00
Travis Ralston 85a0adb004 Reorder babel arguments to leave more of it to the config 2019-12-17 19:31:53 -07:00
Travis Ralston f1475cd3d7 Target NodeJS 10 and minified browser bundle
We release with Node 10 currently, so we should use that. The browser bundle is minfied because we want to keep it, so we might as well shave about 1mb off of it.
2019-12-17 16:18:11 -07:00
Travis Ralston 8c14812537 Re-focus sourcemap generation
We'll let babel decide where best to put the sourcemaps. We previously needed inline sourcemaps for browserify to work, though `babelify` takes care of this now that we use `src/` (without sourcemaps in `lib/` being inline, the transform wouldn't work).

Typescript sourcemaps have also been enabled as a mental reminder that they will be exported. Babel is the only thing that uses the tsconfig for generation right now, and it appears to ignore the sourcemaps field.
2019-12-17 16:14:14 -07:00
Travis Ralston 27aedf0563 Remove ancient polyfill for prototype inheritance
None of our targets care about this.
2019-12-17 16:07:56 -07:00
Travis Ralston 95c2c1643e Remove "source-map-support" from tests because it makes sourcemaps worse
Now that we're pointing at `src/` for tests, we can stop trying to load source maps from random places. With this dependency used, source maps are off by a few lines.
2019-12-17 15:45:15 -07:00
Travis Ralston f952f6742f Remove ancient "use strict" annotations
We don't need these anymore. Theoretically this commit could go to develop, but for safety it's going to `travis/sourcemaps` first.
2019-12-17 15:43:02 -07:00
Travis Ralston f3a10a8166 Appease the linter's line length limit 2019-12-17 15:27:17 -07:00
Travis Ralston 0790201cca Add tsify so the browser bundle can use src/ 2019-12-17 15:21:22 -07:00
Travis Ralston 5938c49453 Move index files for outputs and update pipeline
Having them in `src/` helps IDEs do autocomplete a bit more nicely, and helps us not get confused about which one is referencing which. They have also been converted to TypeScript for typings to be generated.
2019-12-17 15:16:37 -07:00
Travis Ralston 14fb080f80 Document ES6 changes 2019-12-17 15:16:37 -07:00
Travis Ralston 034b8db070 Convert tests to ES6
The earlier commit, d3ce0cb82f, has most of the juicy details on this. In addition to d3ce's changes, we also:
* Use `TestClient` in many integration tests due to subtle behaviour changes in imports when switching to ES6. Namely the behaviour where setting the request function is less reliable in the way we did it, but `TestClient` is very reliable.
* We now use the Olm loader more often to avoid having to maintain so much duplicate code. This makes the imports slightly easier to read.
2019-12-17 15:16:37 -07:00
Travis Ralston d3ce0cb82f Convert src to ES6
The bulk of this is just export/import changes, though there's a couple pieces to highlight:
* We no longer use default exports. This is because it's discouraged by the JS community, though not in any official capacity.
* We now use `polyfillSuper` for some prototype inheritance because the tests, and sometimes webpack, break on "cannot call EncryptionAlgorithm without 'new'". It's very much a workaround, and definitely not needed when we use real classes.

There is some import shuffling to help keep the imports clean - this was done by my IDE.
2019-12-17 15:14:22 -07:00
Travis Ralston 4dbda8dffd Merge pull request #1113 from matrix-org/travis/babel7-watcher
[BREAKING] Refactor the entire build process
2019-12-12 16:09:18 -07:00
Travis Ralston 01f32e0f45 Undo change to utils import 2019-12-12 14:14:19 -07:00
J. Ryan Stinnett 9a0de545b8 Merge pull request #1118 from matrix-org/jryans/4s-new-key-backup
Add support for key backups using secret storage
2019-12-12 17:33:05 +00:00
Travis Ralston 86c530e967 Leave the description alone 2019-12-12 10:27:02 -07:00
J. Ryan Stinnett 049b769f68 Add docs 2019-12-12 17:27:01 +00:00
Travis Ralston dcd6626fe6 Fix readme for new lack of minification 2019-12-12 10:26:44 -07:00
Travis Ralston 601cefe975 Fix the release script for new build process
It doesn't seem to care what the version is, so just build the SDK normally.
2019-12-12 10:25:19 -07:00
J. Ryan Stinnett 1fc2ab7f7d Fix backup tests 2019-12-12 16:06:46 +00:00
J. Ryan Stinnett f2c5b2bd49 Emit user trust via the client 2019-12-12 15:41:56 +00:00
J. Ryan Stinnett f31f88ce31 Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup 2019-12-12 15:38:32 +00:00
J. Ryan Stinnett d35f5152a9 Restore key backup from stored key 2019-12-12 15:11:48 +00:00
J. Ryan Stinnett d8e19db8bf Merge pull request #1117 from matrix-org/jryans/rm-user-verif-event
Remove unused user verification event
2019-12-12 15:06:13 +00:00
J. Ryan Stinnett 376e56d5fd Guard free calls 2019-12-12 14:49:49 +00:00
J. Ryan Stinnett 72f856eca4 Add util to check whether backup key is stored 2019-12-12 14:46:40 +00:00
J. Ryan Stinnett dbab75eae7 Report cross-signing sig as JS style boolean 2019-12-12 13:25:03 +00:00
J. Ryan Stinnett 7457da80e9 Clean up backup trust checks
There were several inaccurate comments and redundant code paths around backup
trust checks.
2019-12-12 13:18:34 +00:00
J. Ryan Stinnett 443e01d38c Always check backup validity, even during enabling
This ensure we run the full backup validity check even when enabling (rather
than assuming we've signed things correctly) to ensure any problem are reported
right away.
2019-12-12 13:15:32 +00:00
J. Ryan Stinnett 880438c5c1 Remove unused user verification event
This was added with cross-signing work, but nothing actually emits it. Let's
remove it until we find a need.
2019-12-12 12:13:40 +00:00
Travis Ralston 1984cf02cf Merge branch 'develop' into travis/babel7-watcher 2019-12-11 17:22:42 -07:00
J. Ryan Stinnett 5423d3ca61 Merge pull request #1116 from matrix-org/jryans/4s-new-key-backup
Fix check for private keys
2019-12-11 17:54:19 +00:00
J. Ryan Stinnett 3f448df1d3 Create key backup with secret storage
When secret storage is enable, create a random key for encrypting key backups
and store it in SSSS.
2019-12-11 16:29:02 +00:00
J. Ryan Stinnett a626b44bbe Fix check for private keys
This check for new keys was always true, instead of checking whether something
was added.
2019-12-11 14:58:57 +00:00
J. Ryan Stinnett 4c6e2fca91 Merge pull request #1115 from matrix-org/jryans/restore-watch
Restore watching mode for `start:watch`
2019-12-11 14:58:19 +00:00
J. Ryan Stinnett ab4d9ae4bc Restore watching mode for start:watch
Regressed by #1112
2019-12-11 14:50:28 +00:00
J. Ryan Stinnett fb3d075da2 Merge pull request #1079 from matrix-org/jryans/4s-new-key-backup
Add secret storage bootstrap flow
2019-12-11 11:04:22 +00:00
J. Ryan Stinnett 657e48de7e Fix grammar 2019-12-11 10:51:12 +00:00
J. Ryan Stinnett 1b63cb1406 Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup 2019-12-11 10:09:29 +00:00
Travis Ralston 4bdabbfbe9 [BREAKING] Refactor the entire build process
For https://github.com/vector-im/riot-web/issues/8880

Features:
* Export modern JS
* Export typings
* Export source maps that actually mean something
* No longer supporting minified builds

This is a step towards being a boring SDK and not anticipating an install location. 

This commit requires a major version bump of the SDK.
2019-12-10 13:25:07 -07:00
Travis Ralston 01f0dd4498 Merge pull request #1112 from matrix-org/travis/babel-7
Part 1 of many: Upgrade to babel@7 and TypeScript
2019-12-10 12:14:24 -07:00
Travis Ralston f59650d8a6 Use better-docs jsdoc template for TypeScript+JS documentation
See description of https://github.com/matrix-org/matrix-js-sdk/pull/1112#issue-351540830 for more info
2019-12-10 11:59:46 -07:00
Travis Ralston 0e444fd925 Re-add babel-eslint
This was accidentally removed.
2019-12-10 11:53:02 -07:00
Travis Ralston 9b8b57d186 Convert randomstring to typescript as a proof of concept 2019-12-10 11:50:01 -07:00
Travis Ralston ca6a52727c Fix logger imports in tests 2019-12-10 11:39:56 -07:00
Travis Ralston 3dfde6bf6a Setup for babel@7 + typescript
Refs:
* https://github.com/matrix-org/matrix-js-sdk/pull/1012
* https://github.com/matrix-org/matrix-js-sdk/pull/1106
2019-12-10 11:31:23 -07:00
J. Ryan Stinnett 780394b051 Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup 2019-12-10 17:54:02 +00:00
J. Ryan Stinnett 6942e3467b Rework to hold cross-signing keys in JS SDK as needed 2019-12-10 17:36:35 +00:00
Michael Telatynski 70eb8a7300 Merge pull request #1100 from matrix-org/t3chguy/remove_bluebird_13
Remove Bluebird: phase 2.5
2019-12-10 16:58:22 +00:00
Michael Telatynski 15a8c23cd0 Merge pull request #1088 from matrix-org/t3chguy/remove_bluebird_12
Remove Bluebird: phase 3
2019-12-10 16:55:23 +00:00
Hubert Chathi 49f0e368d0 Merge pull request #1104 from matrix-org/uhoreg/ignore_verification_done
ignore m.key.verification.done messages when we don't expect any more messages
2019-12-10 09:27:22 -05:00
Bruno Windels 590608a215 Merge pull request #1111 from matrix-org/bwindels/fix-verif-remote-echo-cancel
dont cancel on remote echo of own .request event
2019-12-10 14:09:47 +00:00
Bruno Windels 202fec2a35 dont cancel on remote echo of own .request event 2019-12-10 14:35:17 +01:00
Bruno Windels 817bfa35e5 Merge pull request #1109 from matrix-org/bwindels/accept-verif-request-rebased
Refactor verification request code
2019-12-10 11:10:01 +00:00
Michael Telatynski 110c9800f0 Merge branches 't3chguy/remove_bluebird_12' and 't3chguy/remove_bluebird_13' of github.com:matrix-org/matrix-js-sdk into t3chguy/remove_bluebird_12 2019-12-10 00:19:03 +00:00
Michael Telatynski 1a6dc973bb Merge branches 'develop' and 't3chguy/remove_bluebird_13' of github.com:matrix-org/matrix-js-sdk into t3chguy/remove_bluebird_13 2019-12-10 00:18:32 +00:00
J. Ryan Stinnett 44dd674dab Note about static potential 2019-12-09 17:57:24 +00:00
J. Ryan Stinnett 4a3ce640d7 Document verification methods accepts cross-signing key ID as well 2019-12-09 17:20:54 +00:00
Bruno Windels df6ebf83b4 fix tests 2019-12-09 17:45:01 +01:00
Bruno Windels e5dcc5a407 use verification request with channel from crypto 2019-12-09 17:45:01 +01:00
Bruno Windels 1ee8abb0e6 wrap channel passed to verifier to nofity request
so request is notified when verifier sends an event
2019-12-09 17:45:01 +01:00
Bruno Windels dd40435425 make verifier use channel instead of client straight away
so it is agnostic of the channel used
2019-12-09 17:45:01 +01:00
Bruno Windels 74cb57c761 extract DM verification specific things into InRoomChannel 2019-12-09 17:45:01 +01:00
Bruno Windels 86123f28f7 extract to_device verification specific things into ToDeviceChannel 2019-12-09 17:45:01 +01:00
Bruno Windels f97ab32e7c extract common logic between normal & DM verif into single request class 2019-12-09 17:45:01 +01:00
RiotRobot b0e2544e4b Merge branch 'master' into develop 2019-12-09 11:25:47 +00:00
J. Ryan Stinnett 2a2a40af7a Add separate check for secret storage keys
Decryption vs. signing keys are calculated differently and so require separate
check functions.
2019-12-06 17:51:22 +00:00
J. Ryan Stinnett 1df12d1677 Fix type docs for checkPrivateKey 2019-12-06 14:41:29 +00:00
J. Ryan Stinnett 14a2d7e860 Add docs for more exported cross-signing APIs 2019-12-06 13:20:57 +00:00
J. Ryan Stinnett 3f2c05664f More unstable notices 2019-12-06 13:13:20 +00:00
J. Ryan Stinnett 9b05d1d68e Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup 2019-12-06 12:08:33 +00:00
J. Ryan Stinnett 772d668389 Merge pull request #1105 from matrix-org/jryans/xs-dl-storage
Fix device list's cross-signing storage path
2019-12-06 12:06:49 +00:00
J. Ryan Stinnett 03360a663e Fix device list's cross-signing storage path
Some variables were changed during the course of the initial cross-signing PR
(https://github.com/matrix-org/matrix-js-sdk/pull/832) without updating the
storage path to match, so we weren't storing / loading cross-signing info for
devices in the end.

This updates storage and loading to match where the data now lives in memory.
2019-12-06 11:59:17 +00:00
Hubert Chathi e1e9f690c9 ignore m.key.verification.done messages when we don't expect any more messages 2019-12-05 12:53:59 -05:00
J. Ryan Stinnett 934e81d16c Clarify the key backup integration is unfinished 2019-12-05 16:36:17 +00:00
J. Ryan Stinnett 88bb31d3e6 Expose deriveKey from passphrase 2019-12-05 16:30:10 +00:00
J. Ryan Stinnett 33f5894547 Adjust secret key adding to consume instead of create
This changes `addKey` for secret storage to consume info about a pre-generated
key, rather than creating the key in middle of the method. This eases UI work
that want to have the public and private keys earlier on in the flow.
2019-12-05 16:30:10 +00:00
David Baker fa46d2bef8 Merge pull request #1103 from matrix-org/dbkr/yarn_upgrade_dec19
yarn upgrade
2019-12-05 13:58:50 +00:00
J. Ryan Stinnett 65f8556ee9 Include KDF params in recovery key info
This adjusts the metadata from `createRecoveryKeyFromPassphrase` to include KDF
info formatted in the way secret storage expects. Since
`prepareKeyBackupVersion` did something similar, we adjust it to use the new
function and reshape the objects.
2019-12-05 10:25:24 +00:00
Michael Telatynski ebe174fbef lets *not* get rid of pointless waits :D
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-04 23:47:53 +00:00
Michael Telatynski eaaeedbb37 Merge branches 'develop' and 't3chguy/remove_bluebird_12' of github.com:matrix-org/matrix-js-sdk into t3chguy/remove_bluebird_12 2019-12-04 23:44:04 +00:00
Michael Telatynski bf45c176a7 get rid of bunch of seemingly pointless waits
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-04 23:42:16 +00:00
Michael Telatynski 87a8e4c216 Apply uhoreg's patch (with jesty stuff changed out)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-04 23:37:49 +00:00
Michael Telatynski 30cc7d4f0f Fix one of the crypto.spec.js failures
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-04 23:31:20 +00:00
Michael Telatynski 4a47867e49 Down to two test failures 2019-12-04 19:17:58 +00:00
J. Ryan Stinnett 5fced642fa Allow getSecretStorageKey to be async 2019-12-04 17:30:27 +00:00
J. Ryan Stinnett 9fb559307b Add recovery key generation path 2019-12-04 15:32:22 +00:00
J. Ryan Stinnett 96c8c2b9c3 Test for cross-signing private key as well as public 2019-12-04 15:16:38 +00:00
J. Ryan Stinnett 145cdf6985 Remove extra space 2019-12-04 14:59:39 +00:00
J. Ryan Stinnett 5910fd95ff Treat secret as not stored if its key info is missing 2019-12-04 14:23:47 +00:00
J. Ryan Stinnett c0dbf2df7f Publicise method testing for secret storage key existence 2019-12-04 14:23:47 +00:00
David Baker cfaadef669 yarn upgrade 2019-12-04 14:19:38 +00:00
J. Ryan Stinnett eeffe208ec Clarify client-level method for adding secret storage keys 2019-12-04 13:45:56 +00:00
J. Ryan Stinnett 9dc61faa6f Add bootstrap option to specify storage key 2019-12-04 11:36:25 +00:00
J. Ryan Stinnett 2173ab3437 Add test for bootstrapping from scratch 2019-12-04 10:59:28 +00:00
J. Ryan Stinnett 66a6dd1f0c Switch to Node compatible base64 handling 2019-12-03 18:01:45 +00:00
Michael Telatynski 8a8109272a fix undef3
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-02 23:45:12 +00:00
Michael Telatynski 7ea30c449e Merge branch 't3chguy/remove_bluebird_13' of github.com:matrix-org/matrix-js-sdk into t3chguy/remove_bluebird_13 2019-12-02 23:43:32 +00:00
Michael Telatynski a6e4096773 fix
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-02 23:43:06 +00:00
Michael Telatynski c1e2d646b6 undo remove
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-02 23:42:10 +00:00
Michael Telatynski 710ac6847d fix undef2
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-02 23:39:47 +00:00
Michael Telatynski f0267eae36 fix undef
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-02 23:38:55 +00:00
Michael Telatynski 1632ee3537 fix order
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-02 23:18:26 +00:00
Michael Telatynski a16cdb948c Fix cross-signing.spec by waiting for right emit
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-02 23:12:45 +00:00
Michael Telatynski c4ae27dae6 Merge branches 'develop' and 't3chguy/remove_bluebird_13' of github.com:matrix-org/matrix-js-sdk into t3chguy/remove_bluebird_13 2019-12-02 22:56:13 +00:00
Michael Telatynski 053bc49738 simplify promiseTry
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-12-02 22:55:05 +00:00
Michael Telatynski 3a1de9fbdc Rip out more bluebirds AGAIN 2019-12-02 22:34:46 +00:00
Michael Telatynski efcaadd0b4 Rip out more bluebirds 2019-12-02 22:34:46 +00:00
Michael Telatynski 0170cb066d add another clean-up to sas.spec.js 2019-12-02 22:34:46 +00:00
Michael Telatynski 6bba5ca25a Rip out some more bluebird 2019-12-02 22:34:46 +00:00
Michael Telatynski edcdeb31ea Rip out bluebird of a bunch more places, not as much to go :| 2019-12-02 22:34:46 +00:00
Michael Telatynski 1286007b2e Rip out bluebird of a bunch of places, lots to go :( 2019-12-02 22:34:46 +00:00
Michael Telatynski 9faab093f7 delint 2019-12-02 22:34:46 +00:00
Michael Telatynski 64bf145e4b Replace rest of defers 2019-12-02 22:34:46 +00:00
Michael Telatynski 733008cfc4 delint and replace more defers 2019-12-02 22:34:46 +00:00
Michael Telatynski bab4582139 Replace more Bluebirdy stuffs 2019-12-02 22:34:46 +00:00
Michael Telatynski fddf2843b4 Replace Bluebird.try 2019-12-02 22:34:45 +00:00
Michael Telatynski f8d83f8273 Stop using Bluebird::mapSeries 2019-12-02 22:34:45 +00:00
J. Ryan Stinnett cfeaf188ed Encode cross-signing keys via base64 2019-12-02 14:39:21 +00:00
J. Ryan Stinnett 58ad1ecbfe Sign secret storage keys when cross-signing keys change 2019-12-02 13:50:43 +00:00
J. Ryan Stinnett 463538178d Clarify purpose of the after local key change helper 2019-12-02 13:02:21 +00:00
J. Ryan Stinnett 14907065d7 Rename device signing auth param 2019-11-29 17:50:59 +00:00
J. Ryan Stinnett ce2059a4b9 Add follow-up steps when restoring from secret storage
If we restore cross-signing keys from secret storage, we also need to run many
of the same follow-up steps from resetting the keys, such as saving to the
crypto store, upgrading device verifications, etc.
2019-11-29 15:20:59 +00:00
J. Ryan Stinnett 2bfc157e64 Clarify cross-signing reset variable 2019-11-29 15:15:18 +00:00
J. Ryan Stinnett fda7a2cf13 Add issue link for key verification 2019-11-29 13:59:09 +00:00
J. Ryan Stinnett e69de8c26f Merge remote-tracking branch 'origin/develop' into jryans/4s-new-key-backup 2019-11-29 11:23:48 +00:00
J. Ryan Stinnett f404c80714 Get cross-signing private keys from secret storage
If you've already set up cross-signing elsewhere and start using a new device,
this loads the private keys from secret storage and regenerates the public keys
to match.

We may also want to download the public keys from the homeserver's key sharing
and verify that they match the private keys, but for now that's left as future
work.
2019-11-29 11:11:45 +00:00
J. Ryan Stinnett e2b680c223 Document CrossSigningInfo#resetKeys 2019-11-28 14:31:53 +00:00
J. Ryan Stinnett bdaf2e3b4f Reflow comment 2019-11-28 12:13:48 +00:00
J. Ryan Stinnett 2190022e64 Add return type 2019-11-28 11:54:20 +00:00
J. Ryan Stinnett e000e2b9fd Move cross-signing storage to 4S into class 2019-11-28 11:54:20 +00:00
J. Ryan Stinnett d5000820fd Fix comment typo 2019-11-27 17:19:41 +00:00
J. Ryan Stinnett 9d91d197e4 Revert to previous cross-signing keys on error 2019-11-27 16:11:06 +00:00
J. Ryan Stinnett 5b767ae948 More comment tweaks to cross-signing keys 2019-11-27 15:51:00 +00:00
David Baker 49448fafaa Correct comment (it's not just a cache) 2019-11-25 15:05:10 +00:00
J. Ryan Stinnett 4116d89d5f Store cross-signing keys in secret storage 2019-11-20 17:48:36 +00:00
J. Ryan Stinnett cc192efe45 Create the SSSS default key when needed 2019-11-20 17:27:57 +00:00
J. Ryan Stinnett feef1a35b9 Add first pass at bootstrapping Secure Secret Storage
This adds a first chunk of bootstrapping Secure Secret Storage by creating
cross-signing keys and supporting interactive auth to upload them.

Part of https://github.com/vector-im/riot-web/issues/11212
2019-11-20 17:23:21 +00:00
J. Ryan Stinnett 55a2f46604 Remove doesCrossSigningHaveKeys, use getId instead 2019-11-20 14:42:46 +00:00
J. Ryan Stinnett ed8b303400 Simplify public key access for cross signing
This simplifies reading the code by removing the semi-magical `[1]` bit to
access the unprefixed version of the key.
2019-11-19 17:47:18 +00:00
J. Ryan Stinnett 4acd06eaba Fix typo in CrossSigning#resetKeys 2019-11-19 13:52:26 +00:00
129 changed files with 7474 additions and 4688 deletions
+16 -12
View File
@@ -1,15 +1,19 @@
{
"presets": ["es2015", "es2016"],
"plugins": [
"transform-class-properties",
// this transforms async functions into generator functions, which
// are then made to use the regenerator module by babel's
// transform-regnerator plugin (which is enabled by es2015).
"transform-async-to-bluebird",
// This makes sure that the regenerator runtime is available to
// the transpiled code.
"transform-runtime",
"sourceMaps": true,
"presets": [
["@babel/preset-env", {
"targets": {
"node": 10
},
"modules": "commonjs"
}],
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime"
]
}
+29 -5
View File
@@ -1,11 +1,35 @@
steps:
- label: ":eslint: Lint"
- label: ":eslint: JS Lint"
command:
- "yarn install"
- "yarn lint"
- "yarn lint:js"
plugins:
- docker#v3.0.1:
image: "node:10"
image: "node:12"
- label: ":tslint: TS Lint"
command:
- "yarn install"
- "yarn lint:ts"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":typescript: Types Lint"
command:
- "yarn install"
- "yarn lint:types"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: "🛠 Build"
command:
- "yarn install"
- "yarn build"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":jest: Tests"
command:
@@ -13,7 +37,7 @@ steps:
- "yarn test"
plugins:
- docker#v3.0.1:
image: "node:10"
image: "node:12"
- label: "📃 Docs"
command:
@@ -21,7 +45,7 @@ steps:
- "yarn gendoc"
plugins:
- docker#v3.0.1:
image: "node:10"
image: "node:12"
- wait
+118
View File
@@ -1,3 +1,121 @@
Changes in [4.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v4.0.0-rc.1) (2020-01-20)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0...v4.0.0-rc.1)
BREAKING CHANGES
================
* The js-sdk node module now exports ES6 rather than ES5. If you
wish to supports target that aren't compatible with ES6, you
will need to transpile the js-sdk to a suitable dialect.
All Changes
===========
* Convert secret storage to new account data API
[\#1154](https://github.com/matrix-org/matrix-js-sdk/pull/1154)
* Add v5 as a safe room version
[\#1157](https://github.com/matrix-org/matrix-js-sdk/pull/1157)
* Add API to get account data from server
[\#1153](https://github.com/matrix-org/matrix-js-sdk/pull/1153)
* Fix sourcemaps by refactoring the build system
[\#1151](https://github.com/matrix-org/matrix-js-sdk/pull/1151)
* record, report, and notify about olm errors
[\#1146](https://github.com/matrix-org/matrix-js-sdk/pull/1146)
* Send device messages for the same user in same API call.
[\#1148](https://github.com/matrix-org/matrix-js-sdk/pull/1148)
* Add an option to ignore unverified devices
[\#1150](https://github.com/matrix-org/matrix-js-sdk/pull/1150)
* Sign key backup with cross-signing key on upgrade
[\#1144](https://github.com/matrix-org/matrix-js-sdk/pull/1144)
* Emoji verification: Change name of 🔒 to lock
[\#1145](https://github.com/matrix-org/matrix-js-sdk/pull/1145)
* use a separate object for each encrypted content
[\#1147](https://github.com/matrix-org/matrix-js-sdk/pull/1147)
* Sourcemaps: develop -> feature branch
[\#1143](https://github.com/matrix-org/matrix-js-sdk/pull/1143)
* Use a safer import/export scheme for the ContentRepo utilities
[\#1134](https://github.com/matrix-org/matrix-js-sdk/pull/1134)
* Fix error handling in decryptGroupMessage
[\#1142](https://github.com/matrix-org/matrix-js-sdk/pull/1142)
* Add additional properties to package.json for riot-web's webpack
[\#1131](https://github.com/matrix-org/matrix-js-sdk/pull/1131)
* Fix import for indexeddb crypto store
[\#1133](https://github.com/matrix-org/matrix-js-sdk/pull/1133)
* Use the right request when creating clients
[\#1132](https://github.com/matrix-org/matrix-js-sdk/pull/1132)
* Target NodeJS 10, minified browser bundle, and other publishing/package
things
[\#1127](https://github.com/matrix-org/matrix-js-sdk/pull/1127)
* Re-focus sourcemap generation
[\#1126](https://github.com/matrix-org/matrix-js-sdk/pull/1126)
* Remove ancient polyfill for prototype inheritance
[\#1125](https://github.com/matrix-org/matrix-js-sdk/pull/1125)
* Remove "source-map-support" from tests because it makes sourcemaps worse
[\#1124](https://github.com/matrix-org/matrix-js-sdk/pull/1124)
* Remove ancient "use strict" annotations
[\#1123](https://github.com/matrix-org/matrix-js-sdk/pull/1123)
* Use ES6 imports/exports instead of older CommonJS ones
[\#1122](https://github.com/matrix-org/matrix-js-sdk/pull/1122)
* [BREAKING] Refactor the entire build process
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
* No changes from rc.1
Changes in [3.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0-rc.1) (2020-01-06)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.6...v3.0.0-rc.1)
BREAKING CHANGES
================
* matrix-js-sdk no longer uses bluebird promises, so promises returned
by the js-sdk no longer support the done() method. Code that calls
done() on promises returned by the js-sdk will break and will need
to be updated to remove the done() call.
All Changes
===========
* Make displayName disambiguation more fuzzy especially against RTL/LTR
content
[\#1141](https://github.com/matrix-org/matrix-js-sdk/pull/1141)
* stop trying to resend event if we get M_TOO_LARGE
[\#1129](https://github.com/matrix-org/matrix-js-sdk/pull/1129)
* Fix creating a key backup with cross signing diabled
[\#1139](https://github.com/matrix-org/matrix-js-sdk/pull/1139)
* Use checkDeviceTrust with key backup
[\#1138](https://github.com/matrix-org/matrix-js-sdk/pull/1138)
* Add support for passthrough SSSS secrets
[\#1128](https://github.com/matrix-org/matrix-js-sdk/pull/1128)
* Add support for key backups using secret storage
[\#1118](https://github.com/matrix-org/matrix-js-sdk/pull/1118)
* Remove unused user verification event
[\#1117](https://github.com/matrix-org/matrix-js-sdk/pull/1117)
* Fix check for private keys
[\#1116](https://github.com/matrix-org/matrix-js-sdk/pull/1116)
* Restore watching mode for `start:watch`
[\#1115](https://github.com/matrix-org/matrix-js-sdk/pull/1115)
* Add secret storage bootstrap flow
[\#1079](https://github.com/matrix-org/matrix-js-sdk/pull/1079)
* Part 1 of many: Upgrade to babel@7 and TypeScript
[\#1112](https://github.com/matrix-org/matrix-js-sdk/pull/1112)
* Remove Bluebird: phase 2.5
[\#1100](https://github.com/matrix-org/matrix-js-sdk/pull/1100)
* Remove Bluebird: phase 3
[\#1088](https://github.com/matrix-org/matrix-js-sdk/pull/1088)
* ignore m.key.verification.done messages when we don't expect any more
messages
[\#1104](https://github.com/matrix-org/matrix-js-sdk/pull/1104)
* dont cancel on remote echo of own .request event
[\#1111](https://github.com/matrix-org/matrix-js-sdk/pull/1111)
* Refactor verification request code
[\#1109](https://github.com/matrix-org/matrix-js-sdk/pull/1109)
* Fix device list's cross-signing storage path
[\#1105](https://github.com/matrix-org/matrix-js-sdk/pull/1105)
* yarn upgrade
[\#1103](https://github.com/matrix-org/matrix-js-sdk/pull/1103)
Changes in [2.4.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.6) (2019-12-09)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.6-rc.1...v2.4.6)
+10 -2
View File
@@ -36,8 +36,16 @@ minutes.
Code style
~~~~~~~~~~
The code-style for matrix-js-sdk is not formally documented, but contributors
are encouraged to read the code style document for matrix-react-sdk
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the code style document for matrix-react-sdk
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
and follow the principles set out there.
+24 -20
View File
@@ -9,12 +9,16 @@ Quickstart
In a browser
------------
Download either the full or minified version from
Download the browser version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
``<script>`` to your page. There will be a global variable ``matrixcs``
attached to ``window`` through which you can access the SDK. See below for how to
include libolm to enable end-to-end-encryption.
The browser bundle supports recent versions of browsers. Typically this is ES2015
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
[browserlists](https://github.com/browserslist/browserslist).
Please check [the working browser example](examples/browser) for more information.
In Node.js
@@ -22,13 +26,18 @@ In Node.js
Ensure you have the latest LTS version of Node.js installed.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/) if you do not have it already.
This SDK targets Node 10 for compatibility, which translates to ES6. If you're using
a bundler like webpack you'll likely have to transpile dependencies, including this
SDK, to match your target browsers.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/)
if you do not have it already.
``yarn add matrix-js-sdk``
```javascript
var sdk = require("matrix-js-sdk");
var client = sdk.createClient("https://matrix.org");
import * as sdk from "matrix-js-sdk";
const client = sdk.createClient("https://matrix.org");
client.publicRooms(function(err, data) {
console.log("Public Rooms: %s", JSON.stringify(data));
});
@@ -59,7 +68,7 @@ client.once('sync', function(state, prevState, res) {
To send a message:
```javascript
var content = {
const content = {
"body": "message text",
"msgtype": "m.text"
};
@@ -161,7 +170,7 @@ which will be fulfilled in the future.
The typical usage is something like:
```javascript
matrixClient.someMethod(arg1, arg2).done(function(result) {
matrixClient.someMethod(arg1, arg2).then(function(result) {
...
});
```
@@ -191,10 +200,10 @@ This section provides some useful code snippets which demonstrate the
core functionality of the SDK. These examples assume the SDK is setup like this:
```javascript
var sdk = require("matrix-js-sdk");
var myUserId = "@example:localhost";
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
var matrixClient = sdk.createClient({
import * as sdk from "matrix-js-sdk";
const myUserId = "@example:localhost";
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId
@@ -206,7 +215,7 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
```javascript
matrixClient.on("RoomMember.membership", function(event, member) {
if (member.membership === "invite" && member.userId === myUserId) {
matrixClient.joinRoom(member.roomId).done(function() {
matrixClient.joinRoom(member.roomId).then(function() {
console.log("Auto-joined %s", member.roomId);
});
}
@@ -247,11 +256,11 @@ Output:
```javascript
matrixClient.on("RoomState.members", function(event, state, member) {
var room = matrixClient.getRoom(state.roomId);
const room = matrixClient.getRoom(state.roomId);
if (!room) {
return;
}
var memberList = state.getMembers();
const memberList = state.getMembers();
console.log(room.name);
console.log(Array(room.name.length + 1).join("=")); // underline
for (var i = 0; i < memberList.length; i++) {
@@ -297,7 +306,7 @@ End-to-end encryption support
=============================
The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
application to make libolm available, via the ``Olm`` global.
It is also necessry to call ``matrixClient.initCrypto()`` after creating a new
@@ -319,7 +328,7 @@ To provide the Olm library in a browser application:
* download the transpiled libolm (from https://packages.matrix.org/npm/olm/).
* load ``olm.js`` as a ``<script>`` *before* ``browser-matrix.js``.
To provide the Olm library in a node.js application:
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
@@ -351,11 +360,6 @@ To build a browser version from scratch when developing::
$ yarn build
```
To constantly do builds when files are modified (using ``watchify``)::
```
$ yarn watch
```
To run tests (Jasmine)::
```
$ yarn test
-35
View File
@@ -1,35 +0,0 @@
var matrixcs = require("./lib/matrix");
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.
var indexedDB;
try {
indexedDB = global.indexedDB;
} catch(e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
matrixcs.setCryptoStoreFactory(
function() {
return new matrixcs.IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto"
);
}
);
}
module.exports = matrixcs; // keep export for browserify package deps
global.matrixcs = matrixcs;
-1
View File
@@ -1,4 +1,3 @@
"use strict";
console.log("Loading browser sdk");
var client = matrixcs.createClient("http://matrix.org");
+9 -11
View File
@@ -1,5 +1,3 @@
"use strict";
var myUserId = "@example:localhost";
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
var sdk = require("matrix-js-sdk");
@@ -56,7 +54,7 @@ rl.on('line', function(line) {
}
}
if (notSentEvent) {
matrixClient.resendEvent(notSentEvent, viewingRoom).done(function() {
matrixClient.resendEvent(notSentEvent, viewingRoom).then(function() {
printMessages();
rl.prompt();
}, function(err) {
@@ -70,7 +68,7 @@ rl.on('line', function(line) {
}
else if (line.indexOf("/more ") === 0) {
var amount = parseInt(line.split(" ")[1]) || 20;
matrixClient.scrollback(viewingRoom, amount).done(function(room) {
matrixClient.scrollback(viewingRoom, amount).then(function(room) {
printMessages();
rl.prompt();
}, function(err) {
@@ -79,7 +77,7 @@ rl.on('line', function(line) {
}
else if (line.indexOf("/invite ") === 0) {
var userId = line.split(" ")[1].trim();
matrixClient.invite(viewingRoom.roomId, userId).done(function() {
matrixClient.invite(viewingRoom.roomId, userId).then(function() {
printMessages();
rl.prompt();
}, function(err) {
@@ -92,7 +90,7 @@ rl.on('line', function(line) {
matrixClient.uploadContent({
stream: stream,
name: filename
}).done(function(url) {
}).then(function(url) {
var content = {
msgtype: "m.file",
body: filename,
@@ -116,7 +114,7 @@ rl.on('line', function(line) {
viewingRoom = roomList[roomIndex];
if (viewingRoom.getMember(myUserId).membership === "invite") {
// join the room first
matrixClient.joinRoom(viewingRoom.roomId).done(function(room) {
matrixClient.joinRoom(viewingRoom.roomId).then(function(room) {
setRoomList();
viewingRoom = room;
printMessages();
@@ -128,7 +126,7 @@ rl.on('line', function(line) {
else {
printMessages();
}
}
}
}
rl.prompt();
});
@@ -281,8 +279,8 @@ function printMemberList(room) {
member.membership + new Array(10 - member.membership.length).join(" ")
);
print(
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
membershipWithPadding, member.name,
"%s"+fmt(" :: ")+"%s"+fmt(" (")+"%s"+fmt(")"),
membershipWithPadding, member.name,
(member.userId === myUserId ? "Me" : member.userId),
fmt
);
@@ -295,7 +293,7 @@ function printRoomInfo(room) {
var sendHeader = " Sender ";
// pad content to 100
var restCount = (
100 - "Content".length - " | ".length - " | ".length -
100 - "Content".length - " | ".length - " | ".length -
eTypeHeader.length - sendHeader.length
);
var padSide = new Array(Math.floor(restCount/2)).join(" ");
-1
View File
@@ -1,4 +1,3 @@
"use strict";
console.log("Loading browser sdk");
var BASE_URL = "https://matrix.org";
var TOKEN = "accesstokengoeshere";
-6
View File
@@ -1,6 +0,0 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("request"));
module.exports = matrixcs;
var utils = require("./lib/utils");
utils.runPolyfills();
+23
View File
@@ -0,0 +1,23 @@
{
"tags": {
"allowUnknownTags": true
},
"plugins": [
"node_modules/better-docs/category",
"node_modules/better-docs/typescript"
],
"source": {
"include": [
"src"
],
"includePattern": ".(ts|js)$"
},
"opts": {
"encoding": "utf8",
"destination": ".jsdoc",
"readme": "README.md",
"recurse": true,
"verbose": true,
"template": "node_modules/better-docs"
}
}
+48 -48
View File
@@ -1,21 +1,23 @@
{
"name": "matrix-js-sdk",
"version": "2.4.6",
"version": "4.0.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test:watch": "jest spec/ --coverage --testEnvironment node --watch",
"test": "jest spec/ --coverage --testEnvironment node",
"gendoc": "babel --no-babelrc --plugins transform-class-properties -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "yarn start:init && yarn start:watch",
"start:watch": "babel -s -w --skip-initial-build -d lib src",
"start:init": "babel -s -d lib src",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"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 && terser -c -m -o dist/browser-matrix.min.js --source-map \"content='dist/browser-matrix.js.map'\" dist/browser-matrix.js",
"dist": "yarn build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 93 src spec",
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:compile-browser && yarn build:minify-browser && yarn build:types",
"build:types": "tsc --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:ts && yarn lint:js",
"lint:js": "eslint --max-warnings 93 src spec",
"lint:types": "tsc --noEmit",
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
"test": "jest spec/ --coverage --testEnvironment node",
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
},
"repository": {
"type": "git",
@@ -24,33 +26,26 @@
"keywords": [
"matrix-org"
],
"browser": "browser-index.js",
"main": "./lib/index.js",
"typings": "./lib/index.d.ts",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.js",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
".babelrc",
".eslintrc.js",
"spec/.eslintrc.js",
"lib",
"src",
"git-revision.txt",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"RELEASING.md",
"examples",
"git-hooks",
"git-revision.txt",
"index.js",
"browser-index.js",
"lib",
"package.json",
"release.sh",
"spec",
"src"
"release.sh"
],
"dependencies": {
"another-json": "^0.2.0",
"babel-runtime": "^6.26.0",
"bluebird": "3.5.5",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.2",
@@ -60,34 +55,39 @@
"unhomoglyph": "^1.0.2"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-es2016": "^6.24.1",
"browserify": "^16.2.3",
"browserify-shim": "^3.8.13",
"@babel/cli": "^7.7.5",
"@babel/core": "^7.7.5",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6",
"@babel/preset-typescript": "^7.7.4",
"@babel/register": "^7.7.4",
"@babel/runtime": "^7.7.6",
"@types/node": "12",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babelify": "^10.0.0",
"better-docs": "^1.4.7",
"browserify": "^16.5.0",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-jest": "^23.0.4",
"exorcist": "^1.0.1",
"jest": "^23.6.0",
"jest": "^24.9.0",
"jsdoc": "^3.5.5",
"matrix-mock-request": "^1.2.3",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
"rimraf": "^3.0.0",
"source-map-support": "^0.5.13",
"sourceify": "^1.0.0",
"terser": "^4.3.8",
"watchify": "^3.11.1"
"terser": "^4.4.3",
"tsify": "^4.0.1",
"tslint": "^5.20.1",
"typescript": "^3.7.3"
},
"browserify": {
"transform": [
"sourceify"
]
"jest": {
"testEnvironment": "node"
}
}
+3 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
#
# Script to perform a release of matrix-js-sdk.
# Script to perform a release of matrix-js-sdk and downstream projects.
#
# Requires:
# github-changelog-generator; install via:
@@ -9,6 +9,8 @@
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
#
# Note: this script is also used to release matrix-react-sdk and riot-web.
set -e
+2 -3
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -18,7 +19,7 @@ limitations under the License.
* A mock implementation of the webstorage api
* @constructor
*/
function MockStorageApi() {
export function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
@@ -52,5 +53,3 @@ MockStorageApi.prototype = {
},
};
/** */
module.exports = MockStorageApi;
+12 -13
View File
@@ -16,17 +16,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
// load olm before the sdk if possible
import './olm-loader';
import sdk from '..';
import testUtils from './test-utils';
import MockHttpBackend from 'matrix-mock-request';
import Promise from 'bluebird';
import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-store';
import logger from '../src/logger';
import {LocalStorageCryptoStore} from '../src/crypto/store/localStorage-crypto-store';
import {logger} from '../src/logger';
import {WebStorageSessionStore} from "../src/store/session/webstorage";
import {syncPromise} from "./test-utils";
import {createClient} from "../src/matrix";
import {MockStorageApi} from "./MockStorageApi";
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
@@ -40,16 +39,16 @@ import logger from '../src/logger';
* session store. If undefined, we will create a MockStorageApi.
* @param {object} options additional options to pass to the client
*/
export default function TestClient(
export function TestClient(
userId, deviceId, accessToken, sessionStoreBackend, options,
) {
this.userId = userId;
this.deviceId = deviceId;
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new testUtils.MockStorageApi();
sessionStoreBackend = new MockStorageApi();
}
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
@@ -66,7 +65,7 @@ export default function TestClient(
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
options.cryptoStore = this.cryptoStore;
}
this.client = sdk.createClient(options);
this.client = createClient(options);
this.deviceKeys = null;
this.oneTimeKeys = {};
@@ -98,7 +97,7 @@ TestClient.prototype.start = function() {
return Promise.all([
this.httpBackend.flushAllExpected(),
testUtils.syncPromise(this.client),
syncPromise(this.client),
]).then(() => {
logger.log(this + ': started');
});
@@ -226,7 +225,7 @@ TestClient.prototype.flushSync = function() {
logger.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
testUtils.syncPromise(this.client),
syncPromise(this.client),
]).then(() => {
logger.log(`${this}: flushSync completed`);
});
+4 -5
View File
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,11 +16,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import TestClient from '../TestClient';
import testUtils from '../test-utils';
import logger from '../../src/logger';
import {TestClient} from '../TestClient';
import * as testUtils from '../test-utils';
import {logger} from '../../src/logger';
const ROOM_ID = "!room:id";
+7 -10
View File
@@ -2,6 +2,7 @@
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,18 +25,14 @@ limitations under the License.
* See also `megolm.spec.js`.
*/
"use strict";
import 'source-map-support/register';
// load olm before the sdk if possible
import '../olm-loader';
const sdk = require("../..");
import Promise from 'bluebird';
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
const TestClient = require('../TestClient').default;
import logger from '../../src/logger';
import {logger} from '../../src/logger';
import * as testUtils from "../test-utils";
import * as utils from "../../src/utils";
import {TestClient} from "../TestClient";
import {CRYPTO_ENABLED} from "../../src/client";
let aliTestClient;
const roomId = "!room:localhost";
@@ -401,7 +398,7 @@ function firstSync(testClient) {
describe("MatrixClient crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
if (!CRYPTO_ENABLED) {
return;
}
+6 -16
View File
@@ -1,26 +1,16 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import Promise from 'bluebird';
import * as utils from "../test-utils";
import {TestClient} from "../TestClient";
describe("MatrixClient events", function() {
const baseUrl = "http://localhost.or.something";
let client;
let httpBackend;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: selfUserId,
accessToken: selfAccessToken,
});
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
@@ -162,7 +152,7 @@ describe("MatrixClient events", function() {
});
client.startClient();
httpBackend.flushAllExpected().done(function() {
httpBackend.flushAllExpected().then(function() {
expect(fired).toBe(true, "User.presence didn't fire.");
done();
});
+27 -38
View File
@@ -1,13 +1,8 @@
"use strict";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventTimeline = sdk.EventTimeline;
import logger from '../../src/logger';
import * as utils from "../test-utils";
import {EventTimeline} from "../../src/matrix";
import {logger} from "../../src/logger";
import {TestClient} from "../TestClient";
const baseUrl = "http://localhost.or.something";
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
@@ -104,8 +99,9 @@ describe("getEventTimeline support", function() {
let client;
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
const testClient = new TestClient(userId, "DEVICE", accessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
});
afterEach(function() {
@@ -116,12 +112,6 @@ describe("getEventTimeline support", function() {
});
it("timeline support must be enabled to work", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
return startClient(httpBackend, client).then(function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -132,12 +122,15 @@ describe("getEventTimeline support", function() {
});
it("timeline support works when enabled", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{timelineSupport: true},
);
client = testClient.client;
httpBackend = testClient.httpBackend;
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId);
@@ -152,11 +145,7 @@ describe("getEventTimeline support", function() {
it("scrollback should be able to scroll back to before a gappy /sync",
function() {
// need a client with timelineSupport disabled to make this work
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
let room;
return startClient(httpBackend, client).then(function() {
@@ -224,15 +213,15 @@ describe("MatrixClient event timelines", function() {
let httpBackend = null;
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{timelineSupport: true},
);
client = testClient.client;
httpBackend = testClient.httpBackend;
return startClient(httpBackend, client);
});
@@ -356,7 +345,7 @@ describe("MatrixClient event timelines", function() {
.toEqual("start_token");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
}).done(resolve, reject);
}).then(resolve, reject);
});
});
+13 -29
View File
@@ -1,39 +1,23 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const publicGlobals = require("../../lib/matrix");
const Room = publicGlobals.Room;
const MemoryStore = publicGlobals.MemoryStore;
const Filter = publicGlobals.Filter;
const utils = require("../test-utils");
const MockStorageApi = require("../MockStorageApi");
import * as utils from "../test-utils";
import {CRYPTO_ENABLED} from "../../src/client";
import {Filter, MemoryStore, Room} from "../../src/matrix";
import {TestClient} from "../TestClient";
describe("MatrixClient", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
let store = null;
let sessionStore = null;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
beforeEach(function() {
httpBackend = new HttpBackend();
store = new MemoryStore();
const mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
deviceId: "aliceDevice",
accessToken: accessToken,
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
store: store,
sessionStore: sessionStore,
});
httpBackend = testClient.httpBackend;
client = testClient.client;
});
afterEach(function() {
@@ -181,7 +165,7 @@ describe("MatrixClient", function() {
event_format: "client",
});
store.storeFilter(filter);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
client.getFilter(userId, filterId, true).then(function(gotFilter) {
expect(gotFilter).toEqual(filter);
done();
});
@@ -202,7 +186,7 @@ describe("MatrixClient", function() {
event_format: "client",
});
store.storeFilter(storeFilter);
client.getFilter(userId, filterId, false).done(function(gotFilter) {
client.getFilter(userId, filterId, false).then(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
done();
});
@@ -220,7 +204,7 @@ describe("MatrixClient", function() {
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
client.getFilter(userId, filterId, true).then(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store.getFilter(userId, filterId)).toBeTruthy();
done();
@@ -248,7 +232,7 @@ describe("MatrixClient", function() {
filter_id: filterId,
});
client.createFilter(filterDefinition).done(function(gotFilter) {
client.createFilter(filterDefinition).then(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
expect(store.getFilter(userId, filterId)).toEqual(gotFilter);
done();
@@ -295,7 +279,7 @@ describe("MatrixClient", function() {
});
}).respond(200, response);
httpBackend.flush().done(function() {
httpBackend.flush().then(function() {
done();
});
});
@@ -303,7 +287,7 @@ describe("MatrixClient", function() {
describe("downloadKeys", function() {
if (!sdk.CRYPTO_ENABLED) {
if (!CRYPTO_ENABLED) {
return;
}
+14 -17
View File
@@ -1,11 +1,8 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import Promise from 'bluebird';
import * as utils from "../test-utils";
import HttpBackend from "matrix-mock-request";
import {MatrixClient} from "../../src/matrix";
import {MatrixScheduler} from "../../src/scheduler";
import {MemoryStore} from "../../src/store/memory";
describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something";
@@ -73,7 +70,7 @@ describe("MatrixClient opts", function() {
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: new sdk.MatrixScheduler(),
scheduler: new MatrixScheduler(),
});
});
@@ -86,7 +83,7 @@ describe("MatrixClient opts", function() {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId,
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(res.event_id).toEqual(eventId);
done();
});
@@ -126,7 +123,7 @@ describe("MatrixClient opts", function() {
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
store: new sdk.MemoryStore(),
store: new MemoryStore(),
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -139,7 +136,7 @@ describe("MatrixClient opts", function() {
errcode: "M_SOMETHING",
error: "Ruh roh",
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
}, function(err) {
expect(err.errcode).toEqual("M_SOMETHING");
@@ -157,16 +154,16 @@ describe("MatrixClient opts", function() {
});
let sentA = false;
let sentB = false;
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
sentA = true;
expect(sentB).toBe(true);
});
client.sendTextMessage("!foo:bar", "b body", "txn2").done(function(res) {
client.sendTextMessage("!foo:bar", "b body", "txn2").then(function(res) {
sentB = true;
expect(sentA).toBe(false);
});
httpBackend.flush("/txn2", 1).done(function() {
httpBackend.flush("/txn1", 1).done(function() {
httpBackend.flush("/txn2", 1).then(function() {
httpBackend.flush("/txn1", 1).then(function() {
done();
});
});
@@ -176,7 +173,7 @@ describe("MatrixClient opts", function() {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "foo",
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(res.event_id).toEqual("foo");
done();
});
+15 -18
View File
@@ -1,13 +1,9 @@
"use strict";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const EventStatus = sdk.EventStatus;
import {EventStatus} from "../../src/matrix";
import {MatrixScheduler} from "../../src/scheduler";
import {Room} from "../../src/models/room";
import {TestClient} from "../TestClient";
describe("MatrixClient retrying", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
let scheduler;
@@ -17,16 +13,17 @@ describe("MatrixClient retrying", function() {
let room;
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
scheduler = new sdk.MatrixScheduler();
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: scheduler,
});
room = new sdk.Room(roomId);
scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{scheduler},
);
httpBackend = testClient.httpBackend;
client = testClient.client;
room = new Room(roomId);
client.store.storeRoom(room);
});
+24 -27
View File
@@ -1,14 +1,9 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const EventStatus = sdk.EventStatus;
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {EventStatus} from "../../src/models/event";
import {TestClient} from "../TestClient";
import Promise from 'bluebird';
describe("MatrixClient room timelines", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const userId = "@alice:localhost";
@@ -103,15 +98,17 @@ describe("MatrixClient room timelines", function() {
}
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
// these tests should work with or without timelineSupport
timelineSupport: true,
});
// these tests should work with or without timelineSupport
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{timelineSupport: true},
);
httpBackend = testClient.httpBackend;
client = testClient.client;
setNextSyncData();
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
@@ -151,7 +148,7 @@ describe("MatrixClient room timelines", function() {
expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
done();
});
});
@@ -177,10 +174,10 @@ describe("MatrixClient room timelines", function() {
return;
}
const room = client.getRoom(roomId);
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
client.sendTextMessage(roomId, "I am a fish", "txn1").then(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
@@ -210,10 +207,10 @@ describe("MatrixClient room timelines", function() {
}
const room = client.getRoom(roomId);
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(room.timeline.length).toEqual(2);
httpBackend.flush("/txn1", 1);
promise.done(function() {
promise.then(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getId()).toEqual(eventId);
done();
@@ -248,7 +245,7 @@ describe("MatrixClient room timelines", function() {
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
client.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBe(null);
@@ -312,7 +309,7 @@ describe("MatrixClient room timelines", function() {
// sync response
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
client.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(5);
const joinMsg = room.timeline[0];
expect(joinMsg.sender.name).toEqual("Old Alice");
@@ -350,7 +347,7 @@ describe("MatrixClient room timelines", function() {
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
client.scrollback(room).then(function() {
expect(room.timeline.length).toEqual(3);
expect(room.timeline[0].event).toEqual(sbEvents[1]);
expect(room.timeline[1].event).toEqual(sbEvents[0]);
@@ -381,11 +378,11 @@ describe("MatrixClient room timelines", function() {
const room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeTruthy();
client.scrollback(room, 1).done(function() {
client.scrollback(room, 1).then(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend.flush("/messages", 1).done(function() {
httpBackend.flush("/messages", 1).then(function() {
// still have a sync to flush
httpBackend.flush("/sync", 1).then(() => {
done();
+9 -19
View File
@@ -1,15 +1,9 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const MatrixEvent = sdk.MatrixEvent;
const EventTimeline = sdk.EventTimeline;
import Promise from 'bluebird';
import {MatrixEvent} from "../../src/models/event";
import {EventTimeline} from "../../src/models/event-timeline";
import * as utils from "../test-utils";
import {TestClient} from "../TestClient";
describe("MatrixClient syncing", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const selfUserId = "@alice:localhost";
@@ -22,13 +16,9 @@ describe("MatrixClient syncing", function() {
const roomTwo = "!bar:localhost";
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: selfUserId,
accessToken: selfAccessToken,
});
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
@@ -51,7 +41,7 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flushAllExpected().done(function() {
httpBackend.flushAllExpected().then(function() {
done();
});
});
@@ -65,7 +55,7 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flushAllExpected().done(function() {
httpBackend.flushAllExpected().then(function() {
done();
});
});
+12 -9
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,15 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
const anotherjson = require('another-json');
import Promise from 'bluebird';
const utils = require('../../lib/utils');
const testUtils = require('../test-utils');
const TestClient = require('../TestClient').default;
import logger from '../../src/logger';
import anotherjson from "another-json";
import * as utils from "../../src/utils";
import * as testUtils from "../test-utils";
import {TestClient} from "../TestClient";
import {logger} from "../../src/logger";
const ROOM_ID = "!room:id";
@@ -618,6 +615,9 @@ describe("megolm", function() {
).respond(200, {
event_id: '$event_id',
});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
@@ -715,6 +715,9 @@ describe("megolm", function() {
event_id: '$event_id',
};
});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
+2 -1
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../src/logger';
import {logger} from '../src/logger';
// try to load the olm library.
try {
+32 -36
View File
@@ -1,12 +1,8 @@
"use strict";
import Promise from 'bluebird';
// load olm before the sdk if possible
import './olm-loader';
import logger from '../src/logger';
import sdk from '..';
const MatrixEvent = sdk.MatrixEvent;
import {logger} from '../src/logger';
import {MatrixEvent} from "../src/models/event";
/**
* Return a promise that is resolved when the client next emits a
@@ -15,7 +11,7 @@ const MatrixEvent = sdk.MatrixEvent;
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
module.exports.syncPromise = function(client, count) {
export function syncPromise(client, count) {
if (count === undefined) {
count = 1;
}
@@ -26,7 +22,7 @@ module.exports.syncPromise = function(client, count) {
const p = new Promise((resolve, reject) => {
const cb = (state) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state == 'SYNCING') {
if (state === 'SYNCING') {
resolve();
} else {
client.once('sync', cb);
@@ -36,9 +32,9 @@ module.exports.syncPromise = function(client, count) {
});
return p.then(() => {
return module.exports.syncPromise(client, count-1);
return syncPromise(client, count-1);
});
};
}
/**
* Create a spy for an object and automatically spy its methods.
@@ -46,7 +42,7 @@ module.exports.syncPromise = function(client, count) {
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
*/
module.exports.mock = function(constr, name) {
export function mock(constr, name) {
// Based on
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
@@ -67,7 +63,7 @@ module.exports.mock = function(constr, name) {
}
}
return result;
};
}
/**
* Create an Event.
@@ -80,7 +76,7 @@ module.exports.mock = function(constr, name) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
*/
module.exports.mkEvent = function(opts) {
export function mkEvent(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
@@ -99,14 +95,14 @@ module.exports.mkEvent = function(opts) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
};
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkPresence = function(opts) {
export function mkPresence(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
@@ -122,7 +118,7 @@ module.exports.mkPresence = function(opts) {
},
};
return opts.event ? new MatrixEvent(event) : event;
};
}
/**
* Create an m.room.member event.
@@ -137,7 +133,7 @@ module.exports.mkPresence = function(opts) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkMembership = function(opts) {
export function mkMembership(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.sender || opts.user;
@@ -154,8 +150,8 @@ module.exports.mkMembership = function(opts) {
if (opts.url) {
opts.content.avatar_url = opts.url;
}
return module.exports.mkEvent(opts);
};
return mkEvent(opts);
}
/**
* Create an m.room.message event.
@@ -166,7 +162,7 @@ module.exports.mkMembership = function(opts) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkMessage = function(opts) {
export function mkMessage(opts) {
opts.type = "m.room.message";
if (!opts.msg) {
opts.msg = "Random->" + Math.random();
@@ -178,8 +174,8 @@ module.exports.mkMessage = function(opts) {
msgtype: "m.text",
body: opts.msg,
};
return module.exports.mkEvent(opts);
};
return mkEvent(opts);
}
/**
@@ -187,10 +183,10 @@ module.exports.mkMessage = function(opts) {
*
* @constructor
*/
module.exports.MockStorageApi = function() {
export function MockStorageApi() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
}
MockStorageApi.prototype = {
get length() {
return Object.keys(this.data).length;
},
@@ -215,7 +211,7 @@ module.exports.MockStorageApi.prototype = {
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
module.exports.awaitDecryption = function(event) {
export function awaitDecryption(event) {
if (!event.isBeingDecrypted()) {
return Promise.resolve(event);
}
@@ -228,19 +224,19 @@ module.exports.awaitDecryption = function(event) {
resolve(ev);
});
});
};
}
const HttpResponse = module.exports.HttpResponse = function(
export function HttpResponse(
httpLookups, acceptKeepalives, ignoreUnhandledSync,
) {
this.httpLookups = httpLookups;
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
this.ignoreUnhandledSync = ignoreUnhandledSync;
this.pendingLookup = null;
};
}
HttpResponse.prototype.request = function HttpResponse(
HttpResponse.prototype.request = function(
cb, method, path, qp, data, prefix,
) {
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
@@ -256,7 +252,7 @@ HttpResponse.prototype.request = function HttpResponse(
if (!next) { // no more things to return
if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
return Promise.defer().promise;
return new Promise(() => {});
}
if (this.pendingLookup) {
if (this.pendingLookup.method === method
@@ -271,7 +267,7 @@ HttpResponse.prototype.request = function HttpResponse(
);
}
this.pendingLookup = {
promise: Promise.defer().promise,
promise: new Promise(() => {}),
method: method,
path: path,
};
@@ -308,10 +304,10 @@ HttpResponse.prototype.request = function HttpResponse(
} else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
this.httpLookups.unshift(next);
return Promise.defer().promise;
return new Promise(() => {});
}
expect(true).toBe(false, "Expected different request. " + logLine);
return Promise.defer().promise;
return new Promise(() => {});
};
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
@@ -353,7 +349,7 @@ HttpResponse.defaultResponses = function(userId) {
];
};
module.exports.setHttpResponses = function setHttpResponses(
export function setHttpResponses(
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
) {
const httpResponseObj = new HttpResponse(
@@ -369,4 +365,4 @@ module.exports.setHttpResponses = function setHttpResponses(
client._http.authedRequestWithPrefix.mockImplementation(httpReq);
client._http.requestWithPrefix.mockImplementation(httpReq);
client._http.request.mockImplementation(httpReq);
};
}
+3 -8
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,16 +14,10 @@ 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 AutoDiscovery = sdk.AutoDiscovery;
import MockHttpBackend from "matrix-mock-request";
import * as sdk from "../../src";
import {AutoDiscovery} from "../../src/autodiscovery";
describe("AutoDiscovery", function() {
let httpBackend = null;
+12 -14
View File
@@ -1,6 +1,4 @@
"use strict";
import 'source-map-support/register';
const ContentRepo = require("../../lib/content-repo");
import {getHttpUriForMxc, getIdenticonUri} from "../../src/content-repo";
describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
@@ -9,7 +7,7 @@ describe("ContentRepo", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
const httpUrl = "http://example.com/image.jpeg";
expect(
ContentRepo.getHttpUriForMxc(
getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true,
),
).toEqual(httpUrl);
@@ -17,25 +15,25 @@ describe("ContentRepo", function() {
it("should return the empty string HTTP URLs by default", function() {
const httpUrl = "http://example.com/image.jpeg";
expect(ContentRepo.getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
});
it("should return a download URL if no width/height/resize are specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
);
});
it("should return the empty string for null input", function() {
expect(ContentRepo.getHttpUriForMxc(null)).toEqual("");
expect(getHttpUriForMxc(null)).toEqual("");
});
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop",
);
@@ -44,7 +42,7 @@ describe("ContentRepo", function() {
it("should put fragments from mxc:// URIs after any query parameters",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32#automade",
);
@@ -53,7 +51,7 @@ describe("ContentRepo", function() {
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
);
});
@@ -61,25 +59,25 @@ describe("ContentRepo", function() {
describe("getIdenticonUri", function() {
it("should do nothing for null input", function() {
expect(ContentRepo.getIdenticonUri(null)).toEqual(null);
expect(getIdenticonUri(null)).toEqual(null);
});
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
expect(getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=96&height=96",
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
expect(getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=32&height=64",
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
expect(getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
"?width=32&height=64",
);
+25 -50
View File
@@ -1,27 +1,20 @@
import 'source-map-support/register';
import '../olm-loader';
import Crypto from '../../lib/crypto';
import WebStorageSessionStore from '../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../MockStorageApi';
import TestClient from '../TestClient';
import {MatrixEvent} from '../../lib/models/event';
import Room from '../../lib/models/room';
import olmlib from '../../lib/crypto/olmlib';
const EventEmitter = require("events").EventEmitter;
const sdk = require("../..");
import {Crypto} from "../../src/crypto";
import {WebStorageSessionStore} from "../../src/store/session/webstorage";
import {MemoryCryptoStore} from "../../src/crypto/store/memory-crypto-store";
import {MockStorageApi} from "../MockStorageApi";
import {TestClient} from "../TestClient";
import {MatrixEvent} from "../../src/models/event";
import {Room} from "../../src/models/room";
import * as olmlib from "../../src/crypto/olmlib";
import {sleep} from "../../src/utils";
import {EventEmitter} from "events";
import {CRYPTO_ENABLED} from "../../src/client";
const Olm = global.Olm;
jest.useFakeTimers();
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
if (!CRYPTO_ENABLED) {
return;
}
@@ -270,7 +263,8 @@ describe("Crypto", function() {
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
// the room key request should be gone since we've now decypted everything
await sleep(1);
// the room key request should be gone since we've now decrypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toBeFalsy();
},
@@ -301,6 +295,8 @@ describe("Crypto", function() {
});
it("uses a new txnid for re-requesting keys", async function() {
jest.useFakeTimers();
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
@@ -310,52 +306,31 @@ describe("Crypto", function() {
sender_key: "senderkey",
},
});
/* return a promise and a function. When the function is called,
* the promise will be resolved.
*/
function awaitFunctionCall() {
let func;
const promise = new Promise((resolve, reject) => {
func = function(...args) {
resolve(args);
return new Promise((resolve, reject) => {
// give us some time to process the result before
// continuing
global.setTimeout(resolve, 1);
});
};
});
return {func, promise};
}
// replace Alice's sendToDevice function with a mock
aliceClient.sendToDevice = jest.fn().mockResolvedValue(undefined);
aliceClient.startClient();
let promise;
// make a room key request, and record the transaction ID for the
// sendToDevice call
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
jest.runAllTimers();
let args = await promise;
const txnId = args[2];
jest.runAllTimers();
await Promise.resolve();
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
const txnId = aliceClient.sendToDevice.mock.calls[0][2];
// give the room key request manager time to update the state
// of the request
await Promise.resolve();
// cancel and resend the room key request
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
jest.runAllTimers();
await Promise.resolve();
// cancelAndResend will call sendToDevice twice:
// the first call to sendToDevice will be the cancellation
args = await promise;
// the second call to sendToDevice will be the key request
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
jest.runAllTimers();
args = await promise;
jest.runAllTimers();
expect(args[2]).not.toBe(txnId);
expect(aliceClient.sendToDevice).toBeCalledTimes(3);
expect(aliceClient.sendToDevice.mock.calls[2][2]).not.toBe(txnId);
});
});
});
+9 -10
View File
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,12 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import DeviceList from '../../../lib/crypto/DeviceList';
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
import utils from '../../../lib/utils';
import logger from '../../../src/logger';
import Promise from 'bluebird';
import {logger} from "../../../src/logger";
import * as utils from "../../../src/utils";
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
import {DeviceList} from "../../../src/crypto/DeviceList";
const signedDeviceList = {
"failures": {},
@@ -87,7 +86,7 @@ describe('DeviceList', function() {
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = Promise.defer();
const queryDefer1 = utils.defer();
downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
@@ -106,7 +105,7 @@ describe('DeviceList', function() {
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = Promise.defer();
const queryDefer1 = utils.defer();
downloadSpy.mockReturnValue(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
@@ -114,7 +113,7 @@ describe('DeviceList', function() {
downloadSpy.mockReset();
// outdated notif arrives while the request is in flight.
const queryDefer2 = Promise.defer();
const queryDefer2 = utils.defer();
downloadSpy.mockReturnValue(queryDefer2.promise);
dl.invalidateUserDeviceList('@test1:sw1v.org');
@@ -134,7 +133,7 @@ describe('DeviceList', function() {
logger.log("Creating new devicelist to simulate app reload");
downloadSpy.mockReset();
const dl2 = createTestDeviceList();
const queryDefer3 = Promise.defer();
const queryDefer3 = utils.defer();
downloadSpy.mockReturnValue(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
+359 -18
View File
@@ -1,17 +1,16 @@
import '../../../olm-loader';
import * as algorithms from "../../../../src/crypto/algorithms";
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
import {MockStorageApi} from "../../../MockStorageApi";
import * as testUtils from "../../../test-utils";
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
import {Crypto} from "../../../../src/crypto";
import {logger} from "../../../../src/logger";
import {MatrixEvent} from "../../../../src/models/event";
import {TestClient} from "../../../TestClient";
import {Room} from "../../../../src/models/room";
import * as olmlib from "../../../../src/crypto/olmlib";
import Promise from 'bluebird';
import sdk from '../../../..';
import algorithms from '../../../../lib/crypto/algorithms';
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';
import logger from '../../../../src/logger';
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
@@ -56,7 +55,7 @@ describe("MegolmDecryption", function() {
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
mockOlmLib.encryptMessageForDevice =
jest.fn().mockReturnValue(Promise.resolve());
jest.fn().mockResolvedValue(undefined);
megolmDecryption.olmlib = mockOlmLib;
});
@@ -136,11 +135,11 @@ describe("MegolmDecryption", function() {
const deviceInfo = {};
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
mockOlmLib.ensureOlmSessionsForDevices.mockReturnValue(
Promise.resolve({'@alice:foo': {'alidevice': {
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
'@alice:foo': {'alidevice': {
sessionId: 'alisession',
}}}),
);
}},
});
const awaitEncryptForDevice = new Promise((res, rej) => {
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
@@ -282,7 +281,7 @@ describe("MegolmDecryption", function() {
},
},
}));
mockBaseApis.sendToDevice = jest.fn().mockReturnValue(Promise.resolve());
mockBaseApis.sendToDevice = jest.fn().mockResolvedValue(undefined);
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
'@alice:home.server': {
@@ -344,4 +343,346 @@ describe("MegolmDecryption", function() {
expect(ct2.session_id).toEqual(ct1.session_id);
});
});
it("notifies devices that have been blocked", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient1 = (new TestClient(
"@bob:example.com", "bobdevice1",
)).client;
const bobClient2 = (new TestClient(
"@bob:example.com", "bobdevice2",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient1.initCrypto(),
bobClient2.initCrypto(),
]);
const aliceDevice = aliceClient._crypto._olmDevice;
const bobDevice1 = bobClient1._crypto._olmDevice;
const bobDevice2 = bobClient2._crypto._olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
room.getEncryptionTargetMembers = async function() {
return [{userId: "@bob:example.com"}];
};
room.setBlacklistUnverifiedDevices(true);
aliceClient.store.storeRoom(room);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
const BOB_DEVICES = {
bobdevice1: {
user_id: "@bob:example.com",
device_id: "bobdevice1",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
},
verified: 0,
},
bobdevice2: {
user_id: "@bob:example.com",
device_id: "bobdevice2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
},
verified: -1,
},
};
aliceClient._crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds);
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice2.session_id;
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice1: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.unverified',
reason:
'The sender has disabled encrypting to unverified devices.',
sender_key: aliceDevice.deviceCurve25519Key,
},
bobdevice2: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.blacklisted',
reason: 'The sender has blocked you.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
};
const event = new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$event",
content: {
msgtype: "m.text",
body: "secret",
},
});
await aliceClient._crypto.encryptEvent(event, room);
expect(run).toBe(true);
aliceClient.stopClient();
bobClient1.stopClient();
bobClient2.stopClient();
});
it("notifies devices when unable to create olm session", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const aliceDevice = aliceClient._crypto._olmDevice;
const bobDevice = bobClient._crypto._olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
aliceRoom.getEncryptionTargetMembers = async () => {
return [
{
userId: "@alice:example.com",
membership: "join",
},
{
userId: "@bob:example.com",
membership: "join",
},
];
};
const BOB_DEVICES = {
bobdevice: {
user_id: "@bob:example.com",
device_id: "bobdevice",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:bobdevice": bobDevice.deviceEd25519Key,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key,
},
known: true,
verified: 1,
},
};
aliceClient._crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds);
};
aliceClient.claimOneTimeKeys = async () => {
// Bob has no one-time keys
return {
one_time_keys: {},
};
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
};
const event = new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$event",
content: {},
});
await aliceClient._crypto.encryptEvent(event, aliceRoom);
expect(run).toBe(true);
});
it("throws an error describing why it doesn't have a key", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
const roomId = "!someroom";
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.blacklisted",
reason: "You have been blocked",
},
}));
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
},
}))).rejects.toThrow("The sender has blocked you.");
});
it("throws an error describing the lack of an olm session", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
const roomId = "!someroom";
const now = Date.now();
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.no_olm",
reason: "Unable to establish a secure channel.",
},
}));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
},
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
});
it("throws an error to indicate a wedged olm session", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
const roomId = "!someroom";
const now = Date.now();
// pretend we got an event that we can't decrypt
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
content: {
msgtype: "m.bad.encrypted",
algorithm: "m.megolm.v1.aes-sha2",
session_id: "session_id",
sender_key: bobDevice.deviceCurve25519Key,
},
}));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
},
origin_server_ts: now,
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
});
});
+7 -8
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018,2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,14 +16,12 @@ limitations under the License.
*/
import '../../../olm-loader';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import logger from '../../../../src/logger';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import olmlib from '../../../../lib/crypto/olmlib';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
import {MockStorageApi} from "../../../MockStorageApi";
import {logger} from "../../../../src/logger";
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
import * as olmlib from "../../../../src/crypto/olmlib";
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
function makeOlmDevice() {
const mockStorage = new MockStorageApi();
+15 -19
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,25 +16,20 @@ limitations under the License.
*/
import '../../olm-loader';
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';
import logger from '../../../src/logger';
import olmlib from '../../../lib/crypto/olmlib';
import {logger} from "../../../src/logger";
import * as olmlib from "../../../src/crypto/olmlib";
import {MatrixClient} from "../../../src/client";
import {MatrixEvent} from "../../../src/models/event";
import * as algorithms from "../../../src/crypto/algorithms";
import {WebStorageSessionStore} from "../../../src/store/session/webstorage";
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
import {MockStorageApi} from "../../MockStorageApi";
import * as testUtils from "../../test-utils";
import {OlmDevice} from "../../../src/crypto/OlmDevice";
import {Crypto} from "../../../src/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';
@@ -157,7 +153,7 @@ describe("MegolmBackup", function() {
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
mockOlmLib.encryptMessageForDevice =
jest.fn().mockReturnValue(Promise.resolve());
jest.fn().mockResolvedValue(undefined);
});
describe("backup", function() {
@@ -338,7 +334,7 @@ describe("MegolmBackup", function() {
});
await client.resetCrossSigningKeys();
let numCalls = 0;
await new Promise(async (resolve, reject) => {
await new Promise((resolve, reject) => {
client._http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
@@ -363,7 +359,7 @@ describe("MegolmBackup", function() {
resolve();
return Promise.resolve({});
};
await client.createKeyBackupVersion({
client.createKeyBackupVersion({
algorithm: "m.megolm_backup.v1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
+5 -6
View File
@@ -16,13 +16,9 @@ limitations under the License.
*/
import '../../olm-loader';
import anotherjson from 'another-json';
import olmlib from '../../../lib/crypto/olmlib';
import TestClient from '../../TestClient';
import * as olmlib from "../../../src/crypto/olmlib";
import {TestClient} from '../../TestClient';
import {HttpResponse, setHttpResponses} from '../../test-utils';
async function makeTestClient(userInfo, options, keys) {
@@ -789,6 +785,9 @@ describe("Cross Signing", function() {
upgradeResolveFunc = resolve;
});
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
await new Promise((resolve) => {
alice._crypto.on("userTrustStatusChanged", resolve);
});
await upgradePromise;
const bobTrust3 = alice.checkUserTrust("@bob:example.com");
+100 -11
View File
@@ -15,20 +15,23 @@ limitations under the License.
*/
import '../../olm-loader';
import { MatrixEvent } from '../../../lib/models/event';
import { SECRET_STORAGE_ALGORITHM_V1 } from '../../../lib/crypto/SecretStorage';
import olmlib from '../../../lib/crypto/olmlib';
import TestClient from '../../TestClient';
import { makeTestClients } from './verification/util';
import * as olmlib from "../../../src/crypto/olmlib";
import {SECRET_STORAGE_ALGORITHM_V1} from "../../../src/crypto/SecretStorage";
import {MatrixEvent} from "../../../src/models/event";
import {TestClient} from '../../TestClient';
import {makeTestClients} from './verification/util';
async function makeTestClient(userInfo, options) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
)).client;
// Make it seem as if we've synced and thus the store can be trusted to
// contain valid account data.
client.isInitialSyncComplete = function() {
return true;
};
await client.initCrypto();
return client;
@@ -106,11 +109,11 @@ describe("Secrets", function() {
}),
]);
expect(secretStorage.isStored("foo")).toBe(false);
expect(await secretStorage.isStored("foo")).toBe(false);
await secretStorage.store("foo", "bar", ["abc"]);
expect(secretStorage.isStored("foo")).toBe(true);
expect(await secretStorage.isStored("foo")).toBe(true);
expect(await secretStorage.get("foo")).toBe("bar");
expect(getKey).toHaveBeenCalled();
@@ -163,7 +166,7 @@ describe("Secrets", function() {
};
alice.resetCrossSigningKeys();
const newKeyId = await alice.addSecretKey(
const newKeyId = await alice.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1,
);
// we don't await on this because it waits for the event to come down the sync
@@ -244,4 +247,90 @@ describe("Secrets", function() {
expect(secret).toBe("bar");
});
it("bootstraps when no storage or cross-signing keys locally", async function() {
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
await bob.bootstrapSecretStorage();
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
it("bootstraps when cross-signing keys in secret storage", async function() {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
},
},
},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob._crypto.checkKeyBackup = async () => {};
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({ pubkey: storagePublicKey }),
});
// Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {};
await bob.bootstrapSecretStorage();
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
});
+24 -15
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018-2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,17 +14,10 @@ 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 logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
import "../../../olm-loader";
import {logger} from "../../../../src/logger";
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
import {ScanQRCode, ShowQRCode} from "../../../../src/crypto/verification/QRCode";
const Olm = global.Olm;
@@ -39,7 +33,10 @@ describe("QR code verification", function() {
describe("showing", function() {
it("should emit an event to show a QR code", async function() {
const qrCode = new ShowQRCode({
const channel = {
send: jest.fn(),
};
const qrCode = new ShowQRCode(channel, {
getUserId: () => "@alice:example.com",
deviceId: "ABCDEFG",
getDeviceEd25519Key: function() {
@@ -79,7 +76,10 @@ describe("QR code verification", function() {
getStoredDevice: jest.fn().mockReturnValue(device),
setDeviceVerified: jest.fn(),
};
const qrCode = new ScanQRCode(client);
const channel = {
send: jest.fn(),
};
const qrCode = new ScanQRCode(channel, client);
qrCode.on("confirm_user_id", ({userId, confirm}) => {
if (userId === "@alice:example.com") {
confirm();
@@ -102,13 +102,17 @@ describe("QR code verification", function() {
getStoredDevice: jest.fn(),
setDeviceVerified: jest.fn(),
};
const qrCode = new ScanQRCode(client, "@bob:example.com", "ABCDEFG");
const channel = {
send: jest.fn(),
};
const qrCode = new ScanQRCode(channel, client, "@bob:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = jest.fn();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(channel.send).toHaveBeenCalled();
expect(client.getStoredDevice).not.toHaveBeenCalled();
expect(client.setDeviceVerified).not.toHaveBeenCalled();
});
@@ -131,13 +135,18 @@ describe("QR code verification", function() {
getStoredDevice: jest.fn().mockReturnValue(device),
setDeviceVerified: jest.fn(),
};
const qrCode = new ScanQRCode(client, "@alice:example.com", "ABCDEFG");
const channel = {
send: jest.fn(),
};
const qrCode = new ScanQRCode(
channel, client, "@alice:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = jest.fn();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(channel.send).toHaveBeenCalled();
expect(client.getStoredDevice).toHaveBeenCalled();
expect(client.setDeviceVerified).not.toHaveBeenCalled();
});
+6 -13
View File
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,22 +14,14 @@ 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 logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import {verificationMethods} from '../../../../lib/crypto';
import SAS from '../../../../lib/crypto/verification/SAS';
import "../../../olm-loader";
import {verificationMethods} from "../../../../src/crypto";
import {logger} from "../../../../src/logger";
import {SAS} from "../../../../src/crypto/verification/SAS";
import {makeTestClients} from './util';
const Olm = global.Olm;
import {makeTestClients} from './util';
describe("verification request", function() {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
+34 -45
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018-2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,29 +14,17 @@ 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 logger from '../../../../src/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import olmlib from '../../../../lib/crypto/olmlib';
import sdk from '../../../..';
import {verificationMethods} from '../../../../lib/crypto';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import SAS from '../../../../lib/crypto/verification/SAS';
import "../../../olm-loader";
import {makeTestClients} from './util';
import {MatrixEvent} from "../../../../src/models/event";
import {SAS} from "../../../../src/crypto/verification/SAS";
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
import {verificationMethods} from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib";
import {logger} from "../../../../src/logger";
const Olm = global.Olm;
const MatrixEvent = sdk.MatrixEvent;
import {makeTestClients} from './util';
let ALICE_DEVICES;
let BOB_DEVICES;
@@ -430,32 +419,26 @@ describe("SAS verification", function() {
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.client.on("event", async (event) => {
const content = event.getContent();
if (event.getType() === "m.room.message"
&& content.msgtype === "m.key.verification.request") {
expect(content.methods).toContain(SAS.NAME);
expect(content.to).toBe(bob.client.getUserId());
const verifier = bob.client.acceptVerificationDM(event, SAS.NAME);
verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
}
bob.client.on("crypto.verification.request", async (request) => {
const verifier = request.beginKeyVerification(SAS.NAME);
verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
}
});
await verifier.verify();
resolve();
}
}
});
await verifier.verify();
resolve();
});
});
@@ -479,6 +462,12 @@ describe("SAS verification", function() {
}
});
});
afterEach(async function() {
await Promise.all([
alice.stop(),
bob.stop(),
]);
});
it("should verify a key", async function() {
await Promise.all([
+4 -5
View File
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,10 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import TestClient from '../../../TestClient';
import sdk from '../../../..';
const MatrixEvent = sdk.MatrixEvent;
import {TestClient} from '../../../TestClient';
import {MatrixEvent} from "../../../../src/models/event";
export async function makeTestClients(userInfos, options) {
const clients = [];
@@ -60,7 +59,7 @@ export async function makeTestClients(userInfos, options) {
});
for (const tc of clients) {
setTimeout(
() => tc.client.emit("event", event),
() => tc.client.emit("Room.timeline", event),
0,
);
}
+5 -7
View File
@@ -1,12 +1,10 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {EventTimeline} from "../../src/models/event-timeline";
import {RoomState} from "../../src/models/room-state";
function mockRoomStates(timeline) {
timeline._startState = utils.mock(sdk.RoomState, "startState");
timeline._endState = utils.mock(sdk.RoomState, "endState");
timeline._startState = utils.mock(RoomState, "startState");
timeline._endState = utils.mock(RoomState, "endState");
}
describe("EventTimeline", function() {
+3 -5
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundaction C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,11 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import sdk from '../..';
const MatrixEvent = sdk.MatrixEvent;
import Promise from 'bluebird';
import logger from '../../src/logger';
import {logger} from "../../src/logger";
import {MatrixEvent} from "../../src/models/event";
describe("MatrixEvent", () => {
describe(".attemptDecryption", () => {
+1 -4
View File
@@ -1,7 +1,4 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const Filter = sdk.Filter;
import {Filter} from "../../src/filter";
describe("Filter", function() {
const filterId = "f1lt3ring15g00d4ursoul";
+4 -9
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,16 +14,10 @@ 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 InteractiveAuth = sdk.InteractiveAuth;
const MatrixError = sdk.MatrixError;
import logger from '../../src/logger';
import {logger} from "../../src/logger";
import {InteractiveAuth} from "../../src/interactive-auth";
import {MatrixError} from "../../src/http-api";
// Trivial client object to test interactive auth
// (we do not need TestClient here)
+1 -1
View File
@@ -1,4 +1,4 @@
import TestClient from '../TestClient';
import {TestClient} from '../TestClient';
describe('Login request', function() {
let client;
+8 -12
View File
@@ -1,10 +1,6 @@
"use strict";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
import logger from '../../src/logger';
import {logger} from "../../src/logger";
import {MatrixClient} from "../../src/client";
import {Filter} from "../../src/filter";
jest.useFakeTimers();
@@ -83,7 +79,7 @@ describe("MatrixClient", function() {
);
}
pendingLookup = {
promise: Promise.defer().promise,
promise: new Promise(() => {}),
method: method,
path: path,
};
@@ -119,7 +115,7 @@ describe("MatrixClient", function() {
return Promise.resolve(next.data);
}
expect(true).toBe(false, "Expected different request. " + logLine);
return Promise.defer().promise;
return new Promise(() => {});
}
beforeEach(function() {
@@ -171,7 +167,7 @@ describe("MatrixClient", function() {
// a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run.
client._http.authedRequest.mockImplementation(function() {
return Promise.defer().promise;
return new Promise(() => {});
});
});
@@ -181,7 +177,7 @@ describe("MatrixClient", function() {
httpLookups.push(SYNC_RESPONSE);
const filterId = "ehfewf";
store.getFilterIdByName.mockReturnValue(filterId);
const filter = new sdk.Filter(0, filterId);
const filter = new Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.mockReturnValue(filter);
const syncPromise = new Promise((resolve, reject) => {
@@ -248,7 +244,7 @@ describe("MatrixClient", function() {
const filterName = getFilterName(client.credentials.userId);
client.store.setFilterIdByName(filterName, invalidFilterId);
const filter = new sdk.Filter(client.credentials.userId);
const filter = new Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
+2 -4
View File
@@ -1,7 +1,5 @@
"use strict";
import 'source-map-support/register';
const PushProcessor = require("../../lib/pushprocessor");
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {PushProcessor} from "../../src/pushprocessor";
describe('NotificationService', function() {
const testUserId = "@ali:matrix.org";
+1 -4
View File
@@ -1,7 +1,4 @@
"use strict";
import 'source-map-support/register';
const callbacks = require("../../src/realtime-callbacks");
import * as callbacks from "../../src/realtime-callbacks";
let wallTime = 1234567890;
jest.useFakeTimers();
+2 -5
View File
@@ -1,8 +1,5 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {RoomMember} from "../../src/models/room-member";
describe("RoomMember", function() {
const roomId = "!foo:bar";
+3 -6
View File
@@ -1,9 +1,6 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const RoomState = sdk.RoomState;
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {RoomState} from "../../src/models/room-state";
import {RoomMember} from "../../src/models/room-member";
describe("RoomState", function() {
const roomId = "!foo:bar";
+7 -11
View File
@@ -1,12 +1,8 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const Room = sdk.Room;
const RoomState = sdk.RoomState;
const MatrixEvent = sdk.MatrixEvent;
const EventStatus = sdk.EventStatus;
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {EventStatus, MatrixEvent} from "../../src/models/event";
import {EventTimeline} from "../../src/models/event-timeline";
import {RoomState} from "../../src/models/room-state";
import {Room} from "../../src/models/room";
describe("Room", function() {
const roomId = "!foo:bar";
@@ -20,9 +16,9 @@ describe("Room", function() {
room = new Room(roomId);
// mock RoomStates
room.oldState = room.getLiveTimeline()._startState =
utils.mock(sdk.RoomState, "oldState");
utils.mock(RoomState, "oldState");
room.currentState = room.getLiveTimeline()._endState =
utils.mock(sdk.RoomState, "currentState");
utils.mock(RoomState, "currentState");
});
describe("getAvatarUrl", function() {
+18 -20
View File
@@ -1,12 +1,10 @@
// This file had a function whose name is all caps, which displeases eslint
/* eslint new-cap: "off" */
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const MatrixScheduler = sdk.MatrixScheduler;
const MatrixError = sdk.MatrixError;
const utils = require("../test-utils");
import {defer} from '../../src/utils';
import {MatrixError} from "../../src/http-api";
import {MatrixScheduler} from "../../src/scheduler";
import * as utils from "../test-utils";
jest.useFakeTimers();
@@ -14,7 +12,7 @@ describe("MatrixScheduler", function() {
let scheduler;
let retryFn;
let queueFn;
let defer;
let deferred;
const roomId = "!foo:bar";
const eventA = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true,
@@ -37,7 +35,7 @@ describe("MatrixScheduler", function() {
});
retryFn = null;
queueFn = null;
defer = Promise.defer();
deferred = defer();
});
it("should process events in a queue in a FIFO manner", async function() {
@@ -47,8 +45,8 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "one_big_queue";
};
const deferA = Promise.defer();
const deferB = Promise.defer();
const deferA = defer();
const deferB = defer();
let yieldedA = false;
scheduler.setProcessFunction(function(event) {
if (yieldedA) {
@@ -74,7 +72,7 @@ describe("MatrixScheduler", function() {
it("should invoke the retryFn on failure and wait the amount of time specified",
async function() {
const waitTimeMs = 1500;
const retryDefer = Promise.defer();
const retryDefer = defer();
retryFn = function() {
retryDefer.resolve();
return waitTimeMs;
@@ -88,9 +86,9 @@ describe("MatrixScheduler", function() {
procCount += 1;
if (procCount === 1) {
expect(ev).toEqual(eventA);
return defer.promise;
return deferred.promise;
} else if (procCount === 2) {
// don't care about this defer
// don't care about this deferred
return new Promise();
}
expect(procCount).toBeLessThan(3);
@@ -101,7 +99,7 @@ describe("MatrixScheduler", function() {
// wait just long enough before it does
await Promise.resolve();
expect(procCount).toEqual(1);
defer.reject({});
deferred.reject({});
await retryDefer.promise;
expect(procCount).toEqual(1);
jest.advanceTimersByTime(waitTimeMs);
@@ -121,8 +119,8 @@ describe("MatrixScheduler", function() {
return "yep";
};
const deferA = Promise.defer();
const deferB = Promise.defer();
const deferA = defer();
const deferB = defer();
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
@@ -177,14 +175,14 @@ describe("MatrixScheduler", function() {
const expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId(),
];
const deferA = Promise.defer();
const deferA = defer();
scheduler.setProcessFunction(function(event) {
const id = expectOrder.shift();
expect(id).toEqual(event.getId());
if (expectOrder.length === 0) {
done();
}
return id === eventA.getId() ? deferA.promise : defer.promise;
return id === eventA.getId() ? deferA.promise : deferred.promise;
});
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
@@ -298,7 +296,7 @@ describe("MatrixScheduler", function() {
scheduler.setProcessFunction(function(ev) {
procCount += 1;
expect(ev).toEqual(eventA);
return defer.promise;
return deferred.promise;
});
// as queueing doesn't start processing synchronously anymore (see commit bbdb5ac)
// wait just long enough before it does
@@ -314,7 +312,7 @@ describe("MatrixScheduler", function() {
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
return defer.promise;
return deferred.promise;
});
expect(procCount).toEqual(0);
});
+2 -5
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,11 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import 'source-map-support/register';
import sdk from "../..";
const SyncAccumulator = sdk.SyncAccumulator;
import {SyncAccumulator} from "../../src/sync-accumulator";
describe("SyncAccumulator", function() {
let sa;
+3 -9
View File
@@ -1,12 +1,6 @@
"use strict";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const TimelineWindow = sdk.TimelineWindow;
const TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
const utils = require("../test-utils");
import {EventTimeline} from "../../src/models/event-timeline";
import {TimelineIndex, TimelineWindow} from "../../src/timeline-window";
import * as utils from "../test-utils";
const ROOM_ID = "roomId";
const USER_ID = "userId";
+2 -5
View File
@@ -1,8 +1,5 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const User = sdk.User;
const utils = require("../test-utils");
import {User} from "../../src/models/user";
import * as utils from "../test-utils";
describe("User", function() {
const userId = "@alice:bar";
+1 -3
View File
@@ -1,6 +1,4 @@
"use strict";
import 'source-map-support/register';
const utils = require("../../lib/utils");
import * as utils from "../../src/utils";
describe("utils", function() {
describe("encodeParams", function() {
+1 -1
View File
@@ -20,7 +20,7 @@ limitations under the License.
* @module
*/
export default class Reemitter {
export class ReEmitter {
constructor(target) {
this.target = target;
+2 -3
View File
@@ -17,9 +17,8 @@ limitations under the License.
/** @module auto-discovery */
import Promise from 'bluebird';
import logger from './logger';
import { URL as NodeURL } from "url";
import {logger} from './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
+35 -35
View File
@@ -16,7 +16,6 @@ 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";
/**
* This is an internal module. MatrixBaseApis is currently only meant to be used
@@ -25,17 +24,22 @@ limitations under the License.
* @module base-apis
*/
import { SERVICE_TYPES } from './service-types';
import logger from './logger';
const httpApi = require("./http-api");
const utils = require("./utils");
const PushProcessor = require("./pushprocessor");
import {SERVICE_TYPES} from './service-types';
import {logger} from './logger';
import {PushProcessor} from "./pushprocessor";
import * as utils from "./utils";
import {
MatrixHttpApi,
PREFIX_IDENTITY_V1,
PREFIX_IDENTITY_V2,
PREFIX_R0,
PREFIX_UNSTABLE,
} from "./http-api";
function termsUrlForService(serviceType, baseUrl) {
switch (serviceType) {
case SERVICE_TYPES.IS:
return baseUrl + httpApi.PREFIX_IDENTITY_V2 + '/terms';
return baseUrl + PREFIX_IDENTITY_V2 + '/terms';
case SERVICE_TYPES.IM:
return baseUrl + '/_matrix/integrations/v1/terms';
default:
@@ -83,7 +87,7 @@ function termsUrlForService(serviceType, baseUrl) {
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*/
function MatrixBaseApis(opts) {
export function MatrixBaseApis(opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
this.baseUrl = opts.baseUrl;
@@ -95,13 +99,13 @@ function MatrixBaseApis(opts) {
idBaseUrl: opts.idBaseUrl,
accessToken: opts.accessToken,
request: opts.request,
prefix: httpApi.PREFIX_R0,
prefix: PREFIX_R0,
onlyData: true,
extraParams: opts.queryParams,
localTimeoutMs: opts.localTimeoutMs,
useAuthorizationHeader: opts.useAuthorizationHeader,
};
this._http = new httpApi.MatrixHttpApi(this, httpOpts);
this._http = new MatrixHttpApi(this, httpOpts);
this._txnCtr = 0;
}
@@ -369,7 +373,7 @@ MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
}
return this._http.getUrl("/login/"+loginType+"/redirect", {
"redirectUrl": redirectUrl,
}, httpApi.PREFIX_R0);
}, PREFIX_R0);
};
/**
@@ -447,7 +451,7 @@ MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId)
return this._http.getUrl(path, {
session: authSessionId,
}, httpApi.PREFIX_R0);
}, PREFIX_R0);
};
// Room operations
@@ -499,7 +503,7 @@ MatrixBaseApis.prototype.fetchRelations =
});
const response = await this._http.authedRequest(
undefined, "GET", path, null, null, {
prefix: httpApi.PREFIX_UNSTABLE,
prefix: PREFIX_UNSTABLE,
},
);
return response;
@@ -1377,7 +1381,7 @@ MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
MatrixBaseApis.prototype.addThreePidOnly = async function(data) {
const path = "/account/3pid/add";
const prefix = await this.isVersionSupported("r0.6.0") ?
httpApi.PREFIX_R0 : httpApi.PREFIX_UNSTABLE;
PREFIX_R0 : PREFIX_UNSTABLE;
return this._http.authedRequest(
undefined, "POST", path, null, data, { prefix },
);
@@ -1400,7 +1404,7 @@ MatrixBaseApis.prototype.addThreePidOnly = async function(data) {
MatrixBaseApis.prototype.bindThreePid = async function(data) {
const path = "/account/3pid/bind";
const prefix = await this.isVersionSupported("r0.6.0") ?
httpApi.PREFIX_R0 : httpApi.PREFIX_UNSTABLE;
PREFIX_R0 : PREFIX_UNSTABLE;
return this._http.authedRequest(
undefined, "POST", path, null, data, { prefix },
);
@@ -1425,7 +1429,7 @@ MatrixBaseApis.prototype.unbindThreePid = async function(medium, address) {
id_server: this.getIdentityServerUrl(true),
};
const prefix = await this.isVersionSupported("r0.6.0") ?
httpApi.PREFIX_R0 : httpApi.PREFIX_UNSTABLE;
PREFIX_R0 : PREFIX_UNSTABLE;
return this._http.authedRequest(
undefined, "POST", path, null, data, { prefix },
);
@@ -1722,7 +1726,7 @@ MatrixBaseApis.prototype.uploadKeySignatures = function(content) {
return this._http.authedRequest(
undefined, "POST", '/keys/signatures/upload', undefined,
content, {
prefix: httpApi.PREFIX_UNSTABLE,
prefix: PREFIX_UNSTABLE,
},
);
};
@@ -1815,7 +1819,7 @@ MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
const data = Object.assign({}, keys, {auth});
return this._http.authedRequest(
undefined, "POST", "/keys/device_signing/upload", undefined, data, {
prefix: httpApi.PREFIX_UNSTABLE,
prefix: PREFIX_UNSTABLE,
},
);
};
@@ -1841,7 +1845,7 @@ MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
throw new Error("No Identity Server base URL set");
}
const uri = this.idBaseUrl + httpApi.PREFIX_IDENTITY_V2 + "/account/register";
const uri = this.idBaseUrl + PREFIX_IDENTITY_V2 + "/account/register";
return this._http.requestOtherUrl(
undefined, "POST", uri,
null, hsOpenIdToken,
@@ -1890,7 +1894,7 @@ MatrixBaseApis.prototype.requestEmailToken = async function(
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
@@ -1903,7 +1907,7 @@ MatrixBaseApis.prototype.requestEmailToken = async function(
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
params, PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
@@ -1958,7 +1962,7 @@ MatrixBaseApis.prototype.requestMsisdnToken = async function(
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/requestToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
@@ -1971,7 +1975,7 @@ MatrixBaseApis.prototype.requestMsisdnToken = async function(
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/msisdn/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
params, PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
@@ -2013,7 +2017,7 @@ MatrixBaseApis.prototype.submitMsisdnToken = async function(
try {
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
@@ -2023,7 +2027,7 @@ MatrixBaseApis.prototype.submitMsisdnToken = async function(
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V1,
params, PREFIX_IDENTITY_V1,
);
}
throw err;
@@ -2074,7 +2078,7 @@ MatrixBaseApis.prototype.submitMsisdnTokenOtherUrl = function(
MatrixBaseApis.prototype.getIdentityHashDetails = function(identityAccessToken) {
return this._http.idServerRequest(
undefined, "GET", "/hash_details",
null, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
null, PREFIX_IDENTITY_V2, identityAccessToken,
);
};
@@ -2143,7 +2147,7 @@ MatrixBaseApis.prototype.identityHashedLookup = async function(
const response = await this._http.idServerRequest(
undefined, "POST", "/lookup",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
if (!response || !response['mappings']) return []; // no results
@@ -2223,7 +2227,7 @@ MatrixBaseApis.prototype.lookupThreePid = async function(
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V1,
params, PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err, undefined);
@@ -2281,7 +2285,7 @@ MatrixBaseApis.prototype.bulkLookupThreePids = async function(
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/bulk_lookup", params,
httpApi.PREFIX_IDENTITY_V1, identityAccessToken,
PREFIX_IDENTITY_V1, identityAccessToken,
);
}
throw err;
@@ -2304,7 +2308,7 @@ MatrixBaseApis.prototype.getIdentityAccount = function(
) {
return this._http.idServerRequest(
undefined, "GET", "/account",
undefined, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
undefined, PREFIX_IDENTITY_V2, identityAccessToken,
);
};
@@ -2432,7 +2436,3 @@ MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason)
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
};
/**
* MatrixBaseApis object
*/
module.exports = MatrixBaseApis;
+54
View File
@@ -0,0 +1,54 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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 * as matrixcs from "./matrix";
import request from "browser-request";
import queryString from "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.
let indexedDB;
try {
indexedDB = global.indexedDB;
} catch(e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
matrixcs.setCryptoStoreFactory(
function() {
return new matrixcs.IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto",
);
},
);
}
// We export 3 things to make browserify happy as well as downstream projects.
// It's awkward, but required.
export * from "./matrix";
export default matrixcs; // keep export for browserify package deps
global.matrixcs = matrixcs;
+306 -121
View File
@@ -16,52 +16,41 @@ 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";
const PushProcessor = require('./pushprocessor');
import {sleep} from './utils';
/**
* This is an internal module. See {@link MatrixClient} for the public class.
* @module client
*/
const EventEmitter = require("events").EventEmitter;
import Promise from 'bluebird';
const url = require('url');
const httpApi = require("./http-api");
const MatrixEvent = require("./models/event").MatrixEvent;
const EventStatus = require("./models/event").EventStatus;
const EventTimeline = require("./models/event-timeline");
const SearchResult = require("./models/search-result");
const StubStore = require("./store/stub");
const webRtcCall = require("./webrtc/call");
const utils = require("./utils");
const contentRepo = require("./content-repo");
const Filter = require("./filter");
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 logger from './logger';
import Crypto from './crypto';
import { isCryptoAvailable } from './crypto';
import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey';
import { keyFromPassphrase, keyFromAuthData } from './crypto/key_passphrase';
import { randomString } from './randomstring';
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
Promise.config({warnings: false});
import url from "url";
import {EventEmitter} from "events";
import {MatrixBaseApis} from "./base-apis";
import {Filter} from "./filter";
import {SyncApi} from "./sync";
import {EventStatus, MatrixEvent} from "./models/event";
import {EventTimeline} from "./models/event-timeline";
import {SearchResult} from "./models/search-result";
import {StubStore} from "./store/stub";
import {createNewMatrixCall} from "./webrtc/call";
import * as utils from './utils';
import {sleep} from './utils';
import {MatrixError, PREFIX_MEDIA_R0, PREFIX_UNSTABLE} from "./http-api";
import {getHttpUriForMxc} from "./content-repo";
import * as ContentHelpers from "./content-helpers";
import * as olmlib from "./crypto/olmlib";
import {ReEmitter} from './ReEmitter';
import {RoomList} from './crypto/RoomList';
import {logger} from './logger';
import {Crypto, isCryptoAvailable} from './crypto';
import {decodeRecoveryKey} from './crypto/recoverykey';
import {keyFromAuthData} from './crypto/key_passphrase';
import {randomString} from './randomstring';
import {PushProcessor} from "./pushprocessor";
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
const SCROLLBACK_DELAY_MS = 3000;
const CRYPTO_ENABLED = isCryptoAvailable();
export const CRYPTO_ENABLED = isCryptoAvailable();
const CAPABILITIES_CACHE_MS = 21600000; // 6 hours - an arbitrary value
function keysFromRecoverySession(sessions, decryptionKey, roomId) {
@@ -181,7 +170,8 @@ function keyFromRecoverySession(session, decryptionKey) {
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {function} [opts.cryptoCallbacks.getCrossSigningKey]
* Optional (required for cross-signing). Function to call when a cross-signing private key is needed.
* Optional. Function to call when a cross-signing private key is needed.
* Secure Secret Storage will be used by default if this is unset.
* Args:
* {string} type The type of key needed. Will be one of "master",
* "self_signing", or "user_signing"
@@ -193,8 +183,8 @@ function keyFromRecoverySession(session, decryptionKey) {
* UInt8Array or rejects with an error.
*
* @param {function} [opts.cryptoCallbacks.saveCrossSigningKeys]
* Optional (required for cross-signing). Called when new private keys
* for cross-signing need to be saved.
* Optional. Called when new private keys for cross-signing need to be saved.
* Secure Secret Storage will be used by default if this is unset.
* Args:
* {object} keys the private keys to save. Map of key name to private key
* as a UInt8Array. The getPrivateKey callback above will be called
@@ -215,7 +205,7 @@ function keyFromRecoverySession(session, decryptionKey) {
* @param {function} [opts.cryptoCallbacks.getSecretStorageKey]
* Optional. Function called when an encryption key for secret storage
* is required. One or more keys will be described in the keys object.
* The callback function should return with an array of:
* The callback function should return a promise with an array of:
* [<key name>, <UInt8Array private key>] or null if it cannot provide
* any of the keys.
* Args:
@@ -239,7 +229,7 @@ function keyFromRecoverySession(session, decryptionKey) {
* {DeviceTrustLevel} device_trust: The trust status of the device requesting
* the secret as returned by {@link module:client~MatrixClient#checkDeviceTrust}.
*/
function MatrixClient(opts) {
export function MatrixClient(opts) {
opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl);
opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl);
@@ -278,7 +268,7 @@ function MatrixClient(opts) {
// try constructing a MatrixCall to see if we are running in an environment
// which has WebRTC. If we are, listen for and handle m.call.* events.
const call = webRtcCall.createNewMatrixCall(this);
const call = createNewMatrixCall(this);
this._supportsVoip = false;
if (call) {
setupCallEventHandler(this);
@@ -298,7 +288,7 @@ function MatrixClient(opts) {
this._cryptoStore = opts.cryptoStore;
this._sessionStore = opts.sessionStore;
this._verificationMethods = opts.verificationMethods;
this._cryptoCallbacks = opts.cryptoCallbacks;
this._cryptoCallbacks = opts.cryptoCallbacks || {};
this._forceTURN = opts.forceTURN || false;
this._fallbackICEServerAllowed = opts.fallbackICEServerAllowed || false;
@@ -508,6 +498,18 @@ MatrixClient.prototype.getSyncStateData = function() {
return this._syncApi.getSyncStateData();
};
/**
* Whether the initial sync has completed.
* @return {boolean} True if at least on sync has happened.
*/
MatrixClient.prototype.isInitialSyncComplete = function() {
const state = this.getSyncState();
if (!state) {
return false;
}
return state === "PREPAED" || state === "SYNCING";
};
/**
* Return whether the client is configured for a guest account.
* @return {boolean} True if this is a guest access_token (or no token is supplied).
@@ -677,7 +679,7 @@ MatrixClient.prototype.initCrypto = async function() {
"crypto.warning",
"crypto.devicesUpdated",
"deviceVerificationChanged",
"userVerificationChanged",
"userTrustStatusChanged",
"crossSigning.keysChanged",
]);
@@ -777,7 +779,8 @@ MatrixClient.prototype.getStoredDevice = async function(userId, deviceId) {
* Mark the given device as verified
*
* @param {string} userId owner of the device
* @param {string} deviceId unique identifier for the device
* @param {string} deviceId unique identifier for the device or user's
* cross-signing public key ID.
*
* @param {boolean=} verified whether to mark the device as verified. defaults
* to 'true'.
@@ -805,7 +808,8 @@ MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified)
* Mark the given device as blocked/unblocked
*
* @param {string} userId owner of the device
* @param {string} deviceId unique identifier for the device
* @param {string} deviceId unique identifier for the device or user's
* cross-signing public key ID.
*
* @param {boolean=} blocked whether to mark the device as blocked. defaults
* to 'true'.
@@ -825,7 +829,8 @@ MatrixClient.prototype.setDeviceBlocked = function(userId, deviceId, blocked) {
* Mark the given device as known/unknown
*
* @param {string} userId owner of the device
* @param {string} deviceId unique identifier for the device
* @param {string} deviceId unique identifier for the device or user's
* cross-signing public key ID.
*
* @param {boolean=} known whether to mark the device as known. defaults
* to 'true'.
@@ -947,6 +952,35 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() {
return this._crypto.getGlobalBlacklistUnverifiedDevices();
};
/**
* Set whether sendMessage in a room with unknown and unverified devices
* should throw an error and not send them message. This has 'Global' for
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
* no room-level equivalent for this setting.
*
* This API is currently UNSTABLE and may change or be removed without notice.
*
* @param {boolean} value whether error on unknown devices
*/
MatrixClient.prototype.setGlobalErrorOnUnknownDevices = function(value) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.setGlobalErrorOnUnknownDevices(value);
};
/**
* @return {boolean} whether to error on unknown devices
*
* This API is currently UNSTABLE and may change or be removed without notice.
*/
MatrixClient.prototype.getGlobalErrorOnUnknownDevices = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getGlobalErrorOnUnknownDevices();
};
/**
* Add methods that call the corresponding method in this._crypto
*
@@ -965,14 +999,6 @@ function wrapCryptoFuncs(MatrixClient, names) {
}
}
/**
* Check whether we already have cross-signing keys for the current user.
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#doesCrossSigningHaveKeys
* @return {boolean} Whether we have keys.
*/
/**
* Generate new cross-signing keys.
* The cross-signing API is currently UNSTABLE and may change without notice.
@@ -1026,15 +1052,34 @@ function wrapCryptoFuncs(MatrixClient, names) {
* @returns {DeviceTrustLevel}
*/
/**
* Check the copy of our cross-signing key that we have in the device list and
* see if we can get the private key. If so, mark it as trusted.
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkOwnCrossSigningTrust
*/
/**
* Checks that a given cross-signing private key matches a given public key.
* This can be used by the getCrossSigningKey callback to verify that the
* private key it is about to supply is the one that was requested.
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkCrossSigningPrivateKey
* @param {Uint8Array} privateKey The private key
* @param {string} expectedPublicKey The public key
* @returns {boolean} true if the key matches, otherwise false
*/
wrapCryptoFuncs(MatrixClient, [
"doesCrossSigningHaveKeys",
"resetCrossSigningKeys",
"getCrossSigningId",
"getStoredCrossSigningForUser",
"checkUserTrust",
"checkDeviceTrust",
"checkOwnCrossSigningTrust",
"checkPrivateKey",
"checkCrossSigningPrivateKey",
]);
/**
@@ -1053,21 +1098,56 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
return await this._crypto.checkDeviceTrust(event.getSender(), device.deviceId);
};
/**
* Create a recovery key from a user-supplied passphrase.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#createRecoveryKeyFromPassphrase
* @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<String>} The user-facing recovery key string.
*/
/**
* Bootstrap Secure Secret Storage if needed by creating a default key and signing it with
* the cross-signing master key. If everything is already set up, then no
* changes are made, so this is safe to run to ensure secret storage is ready
* for use.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#bootstrapSecretStorage
* @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
* called to await an interactive auth flow when uploading device signing keys.
* Args:
* {function} A function that makes the request requiring auth. Receives the
* auth data as an object.
*/
/**
* Add a key for encrypting secrets.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#addSecretKey
* @function module:client~MatrixClient#addSecretStorageKey
* @param {string} algorithm the algorithm used by the key
* @param {object} opts the options for the algorithm. The properties used
* depend on the algorithm given. This object may be modified to pass
* information back about the key.
* depend on the algorithm given.
* @param {string} [keyName] the name of the key. If not given, a random
* name will be generated.
*
* @return {string} the name of the key
*/
/**
* Check whether we have a key with a given ID.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#hasSecretStorageKey
* @param {string} [keyId = default key's ID] The ID of the key to check
* for. Defaults to the default key ID if not provided.
* @return {boolean} Whether we have the key.
*/
/**
* Store an encrypted secret on the server
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
@@ -1129,14 +1209,30 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
* @param {string} keyId The new default key ID
*/
/**
* Checks that a given secret storage private key matches a given public key.
* This can be used by the getSecretStorageKey callback to verify that the
* private key it is about to supply is the one that was requested.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkSecretStoragePrivateKey
* @param {Uint8Array} privateKey The private key
* @param {string} expectedPublicKey The public key
* @returns {boolean} true if the key matches, otherwise false
*/
wrapCryptoFuncs(MatrixClient, [
"addSecretKey",
"createRecoveryKeyFromPassphrase",
"bootstrapSecretStorage",
"addSecretStorageKey",
"hasSecretStorageKey",
"storeSecret",
"getSecret",
"isSecretStored",
"requestSecret",
"getDefaultSecretStorageKeyId",
"setDefaultSecretStorageKeyId",
"checkSecretStoragePrivateKey",
]);
/**
@@ -1285,7 +1381,7 @@ MatrixClient.prototype.checkKeyBackup = function() {
MatrixClient.prototype.getKeyBackupVersion = function() {
return this._http.authedRequest(
undefined, "GET", "/room_keys/version", undefined, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
{prefix: PREFIX_UNSTABLE},
).then((res) => {
if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) {
const err = "Unknown backup algorithm: " + res.algorithm;
@@ -1376,38 +1472,50 @@ MatrixClient.prototype.disableKeyBackup = function() {
* @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.
* @param {boolean} [opts.secureSecretStorage = false] Whether to use Secure
* Secret Storage to store the key encrypting key backups.
* Optional, defaults to false.
*
* @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) {
MatrixClient.prototype.prepareKeyBackupVersion = async function(
password,
{ secureSecretStorage = false } = {},
) {
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 keyFromPassphrase(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();
}
const [keyInfo, encodedPrivateKey, privateKey] =
await this.createRecoveryKeyFromPassphrase(password);
authData.public_key = publicKey;
return {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: authData,
recovery_key: encodeRecoveryKey(decryption.get_private_key()),
};
} finally {
decryption.free();
if (secureSecretStorage) {
await this.storeSecret("m.megolm_backup.v1", encodeBase64(privateKey));
logger.info("Key backup private key stored in secret storage");
}
// Reshape objects into form expected for key backup
const authData = {
public_key: keyInfo.pubkey,
};
if (keyInfo.passphrase) {
authData.private_key_salt = keyInfo.passphrase.salt;
authData.private_key_iterations = keyInfo.passphrase.iterations;
}
return {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: authData,
recovery_key: encodedPrivateKey,
};
};
/**
* Check whether the key backup private key is stored in secret storage.
* @return {Promise<boolean>} Whether the backup key is stored.
*/
MatrixClient.prototype.isKeyBackupKeyStored = async function() {
return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */);
};
/**
@@ -1427,25 +1535,35 @@ MatrixClient.prototype.createKeyBackupVersion = async function(info) {
auth_data: info.auth_data,
};
// Now sign the backup auth data. Do it as this device first because crypto._signObject
// is dumb and bluntly replaces the whole signatures block...
// this can probably go away very soon in favour of just signing with the SSK.
// Sign the backup auth data with the device key for backwards compat with
// older devices with cross-signing. This can probably go away very soon in
// favour of just signing with the cross-singing master key.
await this._crypto._signObject(data.auth_data);
if (this._crypto._crossSigningInfo.getId()) {
// now also sign the auth data with the master key
if (
this._cryptoCallbacks.getCrossSigningKey &&
this._crypto._crossSigningInfo.getId()
) {
// now also sign the auth data with the cross-signing master key
// we check for the callback explicitly here because we still want to be able
// to create an un-cross-signed key backup if there is a cross-signing key but
// no callback supplied.
await this._crypto._crossSigningInfo.signObject(data.auth_data, "master");
}
const res = await this._http.authedRequest(
undefined, "POST", "/room_keys/version", undefined, data,
{prefix: httpApi.PREFIX_UNSTABLE},
{prefix: PREFIX_UNSTABLE},
);
this.enableKeyBackup({
algorithm: info.algorithm,
auth_data: info.auth_data,
version: res.version,
});
// We could assume everything's okay and enable directly, but this ensures
// we run the same signature verification that will be used for future
// sessions.
await this.checkKeyBackup();
if (!this.getKeyBackupEnabled()) {
logger.error("Key backup not usable even though we just created it");
}
return res;
};
@@ -1467,7 +1585,7 @@ MatrixClient.prototype.deleteKeyBackupVersion = function(version) {
return this._http.authedRequest(
undefined, "DELETE", path, undefined, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
{prefix: PREFIX_UNSTABLE},
);
};
@@ -1509,7 +1627,7 @@ MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data
const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
undefined, "PUT", path.path, path.queryData, data,
{prefix: httpApi.PREFIX_UNSTABLE},
{prefix: PREFIX_UNSTABLE},
);
};
@@ -1549,6 +1667,18 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
/**
* Restore from an existing key backup via a passphrase.
*
* @param {string} password Passphrase
* @param {string} [targetRoomId] Room ID to target a specific room.
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
password, targetRoomId, targetSessionId, backupInfo,
) {
@@ -1558,6 +1688,39 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
);
};
/**
* Restore from an existing key backup via a private key stored in secret
* storage.
*
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {string} [targetRoomId] Room ID to target a specific room.
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function(
backupInfo, targetRoomId, targetSessionId,
) {
const privKey = decodeBase64(await this.getSecret("m.megolm_backup.v1"));
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
);
};
/**
* Restore from an existing key backup via an encoded recovery key.
*
* @param {string} recoveryKey Encoded recovery key
* @param {string} [targetRoomId] Room ID to target a specific room.
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
recoveryKey, targetRoomId, targetSessionId, backupInfo,
) {
@@ -1598,7 +1761,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
{prefix: PREFIX_UNSTABLE},
).then((res) => {
if (res.rooms) {
for (const [roomId, roomData] of Object.entries(res.rooms)) {
@@ -1606,7 +1769,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
totalKeyCount += Object.keys(roomData.sessions).length;
const roomKeys = keysFromRecoverySession(
roomData.sessions, decryption, roomId, roomKeys,
roomData.sessions, decryption, roomId,
);
for (const k of roomKeys) {
k.room_id = roomId;
@@ -1648,7 +1811,7 @@ MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, versio
const path = this._makeKeyBackupPath(roomId, sessionId, version);
return this._http.authedRequest(
undefined, "DELETE", path.path, path.queryData, undefined,
{prefix: httpApi.PREFIX_UNSTABLE},
{prefix: PREFIX_UNSTABLE},
);
};
@@ -1684,7 +1847,7 @@ MatrixClient.prototype.getGroups = function() {
MatrixClient.prototype.getMediaConfig = function(callback) {
return this._http.authedRequest(
callback, "GET", "/config", undefined, undefined, {
prefix: httpApi.PREFIX_MEDIA_R0,
prefix: PREFIX_MEDIA_R0,
},
);
};
@@ -1791,6 +1954,34 @@ MatrixClient.prototype.getAccountData = function(eventType) {
return this.store.getAccountData(eventType);
};
/**
* Get account data event of given type for the current user. This variant
* gets account data directly from the homeserver if the local store is not
* ready, which can be useful very early in startup before the initial sync.
* @param {string} eventType The event type
* @return {module:client.Promise} Resolves: The contents of the given account
* data event.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getAccountDataFromServer = async function(eventType) {
if (this.isInitialSyncComplete()) {
const event = this.store.getAccountData(eventType);
if (!event) {
return null;
}
// The network version below returns just the content, so this branch
// does the same to match.
return event.getContent();
}
const path = utils.encodeUri("/user/$userId/account_data/$type", {
$userId: this.credentials.userId,
$type: eventType,
});
return this._http.authedRequest(
undefined, "GET", path, undefined,
);
};
/**
* Gets the users that are ignored by this client
* @returns {string[]} The array of users that are ignored (empty if none)
@@ -1892,7 +2083,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
// return syncApi.syncRoom(room);
}
return Promise.resolve(room);
}).done(function(room) {
}).then(function(room) {
_resolve(callback, resolve, room);
}, function(err) {
_reject(callback, reject, err);
@@ -2621,7 +2812,7 @@ MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
url: url,
ts: ts,
}, undefined, {
prefix: httpApi.PREFIX_MEDIA_R0,
prefix: PREFIX_MEDIA_R0,
},
).then(function(response) {
// TODO: expire cache occasionally
@@ -3071,7 +3262,7 @@ MatrixClient.prototype.setAvatarUrl = function(url, callback) {
*/
MatrixClient.prototype.mxcUrlToHttp =
function(mxcUrl, width, height, resizeMethod, allowDirectLinks) {
return contentRepo.getHttpUriForMxc(
return getHttpUriForMxc(
this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks,
);
};
@@ -3224,7 +3415,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
room.oldState.paginationToken,
limit,
'b');
}).done(function(res) {
}).then(function(res) {
const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
if (res.state) {
const stateEvents = utils.map(res.state, _PojoToMatrixEventMapper(self));
@@ -3872,10 +4063,10 @@ MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
// This is a workaround to SYN-590 (Push rule update fails)
deferred = utils.defer();
this.deletePushRule(scope, "room", roomPushRule.rule_id)
.done(function() {
.then(function() {
self.addPushRule(scope, "room", roomId, {
actions: ["dont_notify"],
}).done(function() {
}).then(function() {
deferred.resolve();
}, function(err) {
deferred.reject(err);
@@ -3891,8 +4082,8 @@ MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
if (deferred) {
return new Promise((resolve, reject) => {
// Update this.pushRules when the operation completes
deferred.done(function() {
self.getPushRules().done(function(result) {
deferred.then(function() {
self.getPushRules().then(function(result) {
self.pushRules = result;
resolve();
}, function(err) {
@@ -3901,7 +4092,7 @@ MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
}, function(err) {
// Update it even if the previous operation fails. This can help the
// app to recover when push settings has been modifed from another client
self.getPushRules().done(function(result) {
self.getPushRules().then(function(result) {
self.pushRules = result;
reject(err);
}, function(err2) {
@@ -4395,7 +4586,7 @@ MatrixClient.prototype.startClient = async function(opts) {
}
if (this._crypto) {
this._crypto.uploadDeviceKeys().done();
this._crypto.uploadDeviceKeys();
this._crypto.start();
}
@@ -4730,7 +4921,7 @@ function setupCallEventHandler(client) {
);
}
call = webRtcCall.createNewMatrixCall(client, event.getRoomId(), {
call = createNewMatrixCall(client, event.getRoomId(), {
forceTURN: client._forceTURN,
});
if (!call) {
@@ -4830,7 +5021,7 @@ function setupCallEventHandler(client) {
// if not live, store the fact that the call has ended because
// we're probably getting events backwards so
// the hangup will come before the invite
call = webRtcCall.createNewMatrixCall(client, event.getRoomId());
call = createNewMatrixCall(client, event.getRoomId());
if (call) {
call.callId = content.call_id;
call._initWithHangup(event);
@@ -4854,7 +5045,7 @@ function checkTurnServers(client) {
return; // guests can't access TURN servers
}
client.turnServer().done(function(res) {
client.turnServer().then(function(res) {
if (res.uris) {
logger.log("Got TURN URIs: " + res.uris + " refresh in " +
res.ttl + " secs");
@@ -4930,11 +5121,6 @@ MatrixClient.prototype.generateClientSecret = function() {
return randomString(32);
};
/** */
module.exports.MatrixClient = MatrixClient;
/** */
module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
// MatrixClient Event JSDocs
/**
@@ -5354,5 +5540,4 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* @property {Function} then promise.then(onFulfilled, onRejected, onProgress)
* @property {Function} catch promise.catch(onRejected)
* @property {Function} finally promise.finally(callback)
* @property {Function} done promise.done(onFulfilled, onRejected, onProgress)
*/
+77 -78
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,88 +14,86 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** @module ContentHelpers */
module.exports = {
/**
* Generates the content for a HTML Message event
* @param {string} body the plaintext body of the message
* @param {string} htmlBody the HTML representation of the message
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlMessage: function(body, htmlBody) {
return {
msgtype: "m.text",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Notice event
* @param {string} body the plaintext body of the notice
* @param {string} htmlBody the HTML representation of the notice
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlNotice: function(body, htmlBody) {
return {
msgtype: "m.notice",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Message event
* @param {string} body the plaintext body of the message
* @param {string} htmlBody the HTML representation of the message
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlMessage(body, htmlBody) {
return {
msgtype: "m.text",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
}
/**
* Generates the content for a HTML Emote event
* @param {string} body the plaintext body of the emote
* @param {string} htmlBody the HTML representation of the emote
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlEmote: function(body, htmlBody) {
return {
msgtype: "m.emote",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Notice event
* @param {string} body the plaintext body of the notice
* @param {string} htmlBody the HTML representation of the notice
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlNotice(body, htmlBody) {
return {
msgtype: "m.notice",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
}
/**
* Generates the content for a Plaintext Message event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
makeTextMessage: function(body) {
return {
msgtype: "m.text",
body: body,
};
},
/**
* Generates the content for a HTML Emote event
* @param {string} body the plaintext body of the emote
* @param {string} htmlBody the HTML representation of the emote
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlEmote(body, htmlBody) {
return {
msgtype: "m.emote",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
}
/**
* Generates the content for a Plaintext Notice event
* @param {string} body the plaintext body of the notice
* @returns {{msgtype: string, body: string}}
*/
makeNotice: function(body) {
return {
msgtype: "m.notice",
body: body,
};
},
/**
* Generates the content for a Plaintext Message event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
export function makeTextMessage(body) {
return {
msgtype: "m.text",
body: body,
};
}
/**
* Generates the content for a Plaintext Emote event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
makeEmoteMessage: function(body) {
return {
msgtype: "m.emote",
body: body,
};
},
};
/**
* Generates the content for a Plaintext Notice event
* @param {string} body the plaintext body of the notice
* @returns {{msgtype: string, body: string}}
*/
export function makeNotice(body) {
return {
msgtype: "m.notice",
body: body,
};
}
/**
* Generates the content for a Plaintext Emote event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
export function makeEmoteMessage(body) {
return {
msgtype: "m.emote",
body: body,
};
}
+84 -85
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,95 +17,93 @@ limitations under the License.
/**
* @module content-repo
*/
const utils = require("./utils");
/** Content Repo utility functions */
module.exports = {
/**
* Get the HTTP URL for an MXC URI.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
* directly. Fetching such URLs will leak information about the user to
* anyone they share a room with. If false, will return the emptry string
* for such URLs.
* @return {string} The complete URL to the content.
*/
getHttpUriForMxc: function(baseUrl, mxc, width, height,
resizeMethod, allowDirectLinks) {
if (typeof mxc !== "string" || !mxc) {
import * as utils from "./utils";
/**
* Get the HTTP URL for an MXC URI.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
* directly. Fetching such URLs will leak information about the user to
* anyone they share a room with. If false, will return the emptry string
* for such URLs.
* @return {string} The complete URL to the content.
*/
export function getHttpUriForMxc(baseUrl, mxc, width, height,
resizeMethod, allowDirectLinks) {
if (typeof mxc !== "string" || !mxc) {
return '';
}
if (mxc.indexOf("mxc://") !== 0) {
if (allowDirectLinks) {
return mxc;
} else {
return '';
}
if (mxc.indexOf("mxc://") !== 0) {
if (allowDirectLinks) {
return mxc;
} else {
return '';
}
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/r0/download/";
const params = {};
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/r0/download/";
const params = {};
if (width) {
params.width = Math.round(width);
}
if (height) {
params.height = Math.round(height);
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/r0/thumbnail/";
}
if (width) {
params.width = Math.round(width);
}
if (height) {
params.height = Math.round(height);
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/r0/thumbnail/";
}
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
},
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
}
/**
* Get an identicon URL from an arbitrary string.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
* @deprecated This is no longer in the specification.
*/
getIdenticonUri: function(baseUrl, identiconString, width, height) {
if (!identiconString) {
return null;
}
if (!width) {
width = 96;
}
if (!height) {
height = 96;
}
const params = {
width: width,
height: height,
};
/**
* Get an identicon URL from an arbitrary string.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
* @deprecated This is no longer in the specification.
*/
export function getIdenticonUri(baseUrl, identiconString, width, height) {
if (!identiconString) {
return null;
}
if (!width) {
width = 96;
}
if (!height) {
height = 96;
}
const params = {
width: width,
height: height,
};
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
},
};
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
}
+68 -16
View File
@@ -20,12 +20,15 @@ limitations under the License.
* @module crypto/CrossSigning
*/
import {pkSign, pkVerify} from './olmlib';
import {decodeBase64, encodeBase64, pkSign, pkVerify} from './olmlib';
import {EventEmitter} from 'events';
import logger from '../logger';
import {logger} from '../logger';
function publicKeyFromKeyInfo(keyInfo) {
return Object.entries(keyInfo.keys)[0];
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
// We assume only a single key, and we want the bare form without type
// prefix, so we select the values.
return Object.values(keyInfo.keys)[0];
}
export class CrossSigningInfo extends EventEmitter {
@@ -54,8 +57,9 @@ export class CrossSigningInfo extends EventEmitter {
/**
* Calls the app callback to ask for a private key
* @param {string} type The key type ("master", "self_signing", or "user_signing")
* @param {Uint8Array} expectedPubkey The matching public key or undefined to use
* @param {string} expectedPubkey The matching public key or undefined to use
* the stored public key for the given key type.
* @returns {Array} An array with [ public key, Olm.PkSigning ]
*/
async getCrossSigningKey(type, expectedPubkey) {
if (!this._callbacks.getCrossSigningKey) {
@@ -69,7 +73,7 @@ export class CrossSigningInfo extends EventEmitter {
const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
if (!privkey) {
throw new Error(
"getCrossSigningKey callback for " + type + " returned falsey",
"getCrossSigningKey callback for " + type + " returned falsey",
);
}
const signing = new global.Olm.PkSigning();
@@ -101,12 +105,54 @@ export class CrossSigningInfo extends EventEmitter {
};
}
hasKeys() {
return Object.keys(this.keys).length > 0;
/**
* Check whether the private keys exist in secret storage.
* XXX: This could be static, be we often seem to have an instance when we
* want to know this anyway...
*
* @param {SecretStorage} secretStorage The secret store using account data
* @returns {boolean} Whether all private keys were found in storage
*/
async isStoredInSecretStorage(secretStorage) {
let stored = true;
for (const type of ["master", "self_signing", "user_signing"]) {
stored &= await secretStorage.isStored(`m.cross_signing.${type}`, false);
}
return stored;
}
/**
* Get the ID used to identify the user
* Store private keys in secret storage for use by other devices. This is
* typically called in conjunction with the creation of new cross-signing
* keys.
*
* @param {object} keys The keys to store
* @param {SecretStorage} secretStorage The secret store using account data
*/
static async storeInSecretStorage(keys, secretStorage) {
for (const type of Object.keys(keys)) {
const encodedKey = encodeBase64(keys[type]);
await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
}
}
/**
* Get private keys from secret storage created by some other device. This
* also passes the private keys to the app-specific callback.
*
* @param {string} type The type of key to get. One of "master",
* "self_signing", or "user_signing".
* @param {SecretStorage} secretStorage The secret store using account data
* @return {Uint8Array} The private key
*/
static async getFromSecretStorage(type, secretStorage) {
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
return decodeBase64(encodedKey);
}
/**
* Get the ID used to identify the user. This can also be used to test for
* the existence of a given key type.
*
* @param {string} type The type of key to get the ID of. One of "master",
* "self_signing", or "user_signing". Defaults to "master".
@@ -117,10 +163,16 @@ export class CrossSigningInfo extends EventEmitter {
type = type || "master";
if (!this.keys[type]) return null;
const keyInfo = this.keys[type];
return publicKeyFromKeyInfo(keyInfo)[1];
return publicKeyFromKeyInfo(keyInfo);
}
/**
* Create new cross-signing keys for the given key types. The public keys
* will be held in this class, while the private keys are passed off to the
* `saveCrossSigningKeys` application callback.
*
* @param {CrossSigningLevel} level The key types to reset
*/
async resetKeys(level) {
if (!this._callbacks.saveCrossSigningKeys) {
throw new Error("No saveCrossSigningKeys callback supplied");
@@ -159,7 +211,7 @@ export class CrossSigningInfo extends EventEmitter {
},
};
} else {
[masterPub, masterSigning] = await this.getCrossSigningyKey("master");
[masterPub, masterSigning] = await this.getCrossSigningKey("master");
}
if (level & CrossSigningLevel.SELF_SIGNING) {
@@ -219,7 +271,7 @@ export class CrossSigningInfo extends EventEmitter {
if (!this.keys.master) {
// this is the first key we've seen, so first-use is true
this.firstUse = true;
} else if (publicKeyFromKeyInfo(keys.master)[1] !== this.getId()) {
} else if (publicKeyFromKeyInfo(keys.master) !== this.getId()) {
// this is a different key, so first-use is false
this.firstUse = false;
} // otherwise, same key, so no change
@@ -229,7 +281,7 @@ export class CrossSigningInfo extends EventEmitter {
} else {
throw new Error("Tried to set cross-signing keys without a master key");
}
const masterKey = publicKeyFromKeyInfo(signingKeys.master)[1];
const masterKey = publicKeyFromKeyInfo(signingKeys.master);
// verify signatures
if (keys.user_signing) {
@@ -268,8 +320,8 @@ export class CrossSigningInfo extends EventEmitter {
this.keys.master = keys.master;
// if the master key is set, then the old self-signing and
// user-signing keys are obsolete
delete this.keys.self_signing;
delete this.keys.user_signing;
this.keys.self_signing = null;
this.keys.user_signing = null;
}
if (keys.self_signing) {
this.keys.self_signing = keys.self_signing;
@@ -381,7 +433,7 @@ export class CrossSigningInfo extends EventEmitter {
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
// ...and this device's key from their SSK...
pkVerify(
deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId,
deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId,
);
// ...then we trust this device as much as far as we trust the user
return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust);
+11 -13
View File
@@ -15,7 +15,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/DeviceList
@@ -23,15 +22,13 @@ limitations under the License.
* Manages the list of other users' devices
*/
import Promise from 'bluebird';
import {EventEmitter} from 'events';
import logger from '../logger';
import DeviceInfo from './deviceinfo';
import {logger} from '../logger';
import {DeviceInfo} from './deviceinfo';
import {CrossSigningInfo} from './CrossSigning';
import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
import {sleep} from '../utils';
import * as olmlib from './olmlib';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import {defer, sleep} from '../utils';
/* State transition diagram for DeviceList._deviceTrackingStatus
@@ -64,7 +61,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
/**
* @alias module:crypto/DeviceList
*/
export default class DeviceList extends EventEmitter {
export class DeviceList extends EventEmitter {
constructor(baseApis, cryptoStore, olmDevice) {
super();
@@ -122,7 +119,8 @@ export default class DeviceList extends EventEmitter {
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
this._devices = deviceData ? deviceData.devices : {},
this._ssks = deviceData ? deviceData.self_signing_keys || {} : {};
this._crossSigningInfo = deviceData ?
deviceData.crossSigningInfo || {} : {};
this._deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
@@ -213,7 +211,7 @@ export default class DeviceList extends EventEmitter {
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices,
self_signing_keys: this._ssks,
crossSigningInfo: this._crossSigningInfo,
trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken,
}, txn);
@@ -711,7 +709,7 @@ class DeviceListUpdateSerialiser {
});
if (!this._queuedQueryDeferred) {
this._queuedQueryDeferred = Promise.defer();
this._queuedQueryDeferred = defer();
}
// We always take the new sync token and just use the latest one we've
@@ -776,7 +774,7 @@ class DeviceListUpdateSerialiser {
}
return prom;
}).done(() => {
}).then(() => {
logger.log('Completed key download for ' + downloadUsers);
this._downloadInProgress = false;
+127 -16
View File
@@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017, 2019 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,8 +16,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';
import {logger} from '../logger';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import * as algorithms from './algorithms';
// 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.
@@ -69,7 +71,7 @@ function checkPayloadLength(payloadString) {
* @property {string} deviceCurve25519Key Curve25519 key for the account
* @property {string} deviceEd25519Key Ed25519 key for the account
*/
function OlmDevice(cryptoStore) {
export function OlmDevice(cryptoStore) {
this._cryptoStore = cryptoStore;
this._pickleKey = "DEFAULT_KEY";
@@ -671,6 +673,18 @@ OlmDevice.prototype.matchesSession = async function(
return matches;
};
OlmDevice.prototype.recordSessionProblem = async function(deviceKey, type, fixed) {
await this._cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
};
OlmDevice.prototype.sessionMayHaveProblems = async function(deviceKey, timestamp) {
return await this._cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
};
OlmDevice.prototype.filterOutNotifiedErrorDevices = async function(devices) {
return await this._cryptoStore.filterOutNotifiedErrorDevices(devices);
};
// Outbound group session
// ======================
@@ -818,9 +832,9 @@ OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, txn, func,
) {
this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => {
senderKey, sessionId, txn, (sessionData, withheld) => {
if (sessionData === null) {
func(null);
func(null, null, withheld);
return;
}
@@ -834,7 +848,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
}
this._unpickleInboundGroupSession(sessionData, (session) => {
func(session, sessionData);
func(session, sessionData, withheld);
});
},
);
@@ -859,7 +873,10 @@ OlmDevice.prototype.addInboundGroupSession = async function(
exportFormat,
) {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
/* if we already have this session, consider updating it */
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn,
@@ -914,6 +931,60 @@ OlmDevice.prototype.addInboundGroupSession = async function(
);
};
/**
* Record in the data store why an inbound group session was withheld.
*
* @param {string} roomId room that the session belongs to
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} code reason code
* @param {string} reason human-readable version of `code`
*/
OlmDevice.prototype.addInboundGroupSessionWithheld = async function(
roomId, senderKey, sessionId, code, reason,
) {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD],
(txn) => {
this._cryptoStore.storeEndToEndInboundGroupSessionWithheld(
senderKey, sessionId,
{
room_id: roomId,
code: code,
reason: reason,
},
txn,
);
},
);
};
export const WITHHELD_MESSAGES = {
"m.unverified": "The sender has disabled encrypting to unverified devices.",
"m.blacklisted": "The sender has blocked you.",
"m.unauthorised": "You are not authorised to read the message.",
"m.no_olm": "Unable to establish a secure channel.",
};
/**
* Calculate the message to use for the exception when a session key is withheld.
*
* @param {object} withheld An object that describes why the key was withheld.
*
* @return {string} the message
*
* @private
*/
function _calculateWithheldMessage(withheld) {
if (withheld.code && withheld.code in WITHHELD_MESSAGES) {
return WITHHELD_MESSAGES[withheld.code];
} else if (withheld.reason) {
return withheld.reason;
} else {
return "decryption key withheld";
}
}
/**
* Decrypt a received message with an inbound group session
*
@@ -934,16 +1005,49 @@ OlmDevice.prototype.decryptGroupMessage = async function(
roomId, senderKey, sessionId, body, eventId, timestamp,
) {
let result;
// when the localstorage crypto store is used as an indexeddb backend,
// exceptions thrown from within the inner function are not passed through
// to the top level, so we store exceptions in a variable and raise them at
// the end
let error;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => {
roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
if (session === null) {
if (withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
_calculateWithheldMessage(withheld),
{
session: senderKey + '|' + sessionId,
},
);
}
result = null;
return;
}
const res = session.decrypt(body);
let res;
try {
res = session.decrypt(body);
} catch (e) {
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
_calculateWithheldMessage(withheld),
{
session: senderKey + '|' + sessionId,
},
);
} else {
error = e;
}
return;
}
let plaintext = res.plaintext;
if (plaintext === undefined) {
@@ -965,10 +1069,11 @@ OlmDevice.prototype.decryptGroupMessage = async function(
msgInfo.id !== eventId ||
msgInfo.timestamp !== timestamp
) {
throw new Error(
error = new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey,
);
return;
}
}
this._inboundGroupSessionMessageIndexes[messageIndexKey] = {
@@ -994,6 +1099,9 @@ OlmDevice.prototype.decryptGroupMessage = async function(
},
);
if (error) {
throw error;
}
return result;
};
@@ -1009,7 +1117,10 @@ OlmDevice.prototype.decryptGroupMessage = async function(
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
let result;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
'readonly', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => {
if (sessionData === null) {
@@ -1060,7 +1171,10 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
) {
let result;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
'readonly', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => {
if (session === null) {
@@ -1139,6 +1253,3 @@ OlmDevice.prototype.verifySignature = function(
util.ed25519_verify(key, message, signature);
});
};
/** */
module.exports = OlmDevice;
+3 -5
View File
@@ -14,10 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../logger';
import utils from '../utils';
import {logger} from '../logger';
import * as utils from '../utils';
/**
* Internal module. Management of outgoing room key requests.
@@ -77,7 +75,7 @@ const ROOM_KEY_REQUEST_STATES = {
CANCELLATION_PENDING_AND_WILL_RESEND: 3,
};
export default class OutgoingRoomKeyRequestManager {
export class OutgoingRoomKeyRequestManager {
constructor(baseApis, deviceId, cryptoStore) {
this._baseApis = baseApis;
this._deviceId = deviceId;
+2 -2
View File
@@ -20,12 +20,12 @@ limitations under the License.
* Manages the list of encrypted rooms
*/
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
/**
* @alias module:crypto/RoomList
*/
export default class RoomList {
export class RoomList {
constructor(cryptoStore) {
this._cryptoStore = cryptoStore;
+117 -61
View File
@@ -15,12 +15,10 @@ limitations under the License.
*/
import {EventEmitter} from 'events';
import logger from '../logger';
import olmlib from './olmlib';
import { randomString } from '../randomstring';
import { keyFromPassphrase } from './key_passphrase';
import { encodeRecoveryKey } from './recoverykey';
import { pkVerify } from './olmlib';
import {logger} from '../logger';
import * as olmlib from './olmlib';
import {pkVerify} from './olmlib';
import {randomString} from '../randomstring';
export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-sha2";
@@ -28,7 +26,7 @@ export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-s
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
*/
export default class SecretStorage extends EventEmitter {
export class SecretStorage extends EventEmitter {
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
super();
this._baseApis = baseApis;
@@ -38,12 +36,12 @@ export default class SecretStorage extends EventEmitter {
this._incomingRequests = {};
}
getDefaultKeyId() {
const defaultKeyEvent = this._baseApis.getAccountData(
async getDefaultKeyId() {
const defaultKey = await this._baseApis.getAccountDataFromServer(
'm.secret_storage.default_key',
);
if (!defaultKeyEvent) return null;
return defaultKeyEvent.getContent().key;
if (!defaultKey) return null;
return defaultKey.key;
}
setDefaultKeyId(keyId) {
@@ -71,8 +69,7 @@ export default class SecretStorage extends EventEmitter {
*
* @param {string} algorithm the algorithm used by the key.
* @param {object} opts the options for the algorithm. The properties used
* depend on the algorithm given. This object may be modified to pass
* information back about the key.
* depend on the algorithm given.
* @param {string} [keyId] the ID of the key. If not given, a random
* ID will be generated.
*
@@ -92,21 +89,16 @@ export default class SecretStorage extends EventEmitter {
{
const decryption = new global.Olm.PkDecryption();
try {
if (opts.passphrase) {
const key = await keyFromPassphrase(opts.passphrase);
keyData.passphrase = {
algorithm: "m.pbkdf2",
iterations: key.iterations,
salt: key.salt,
};
opts.encodedkey = encodeRecoveryKey(key.key);
keyData.pubkey = decryption.init_with_private_key(key.key);
} else if (opts.privkey) {
keyData.pubkey = decryption.init_with_private_key(opts.privkey);
opts.encodedkey = encodeRecoveryKey(opts.privkey);
const { passphrase, pubkey } = opts;
// Copies in public key details of the form generated by
// the Crypto module's `createRecoveryKeyFromPassphrase`.
if (passphrase && pubkey) {
keyData.passphrase = passphrase;
keyData.pubkey = pubkey;
} else if (pubkey) {
keyData.pubkey = pubkey;
} else {
keyData.pubkey = decryption.generate_key();
opts.encodedkey = encodeRecoveryKey(decryption.get_private_key());
}
} finally {
decryption.free();
@@ -120,7 +112,11 @@ export default class SecretStorage extends EventEmitter {
if (!keyId) {
do {
keyId = randomString(32);
} while (this._baseApis.getAccountData(`m.secret_storage.key.${keyId}`));
} while (
await this._baseApis.getAccountDataFromServer(
`m.secret_storage.key.${keyId}`,
)
);
}
await this._crossSigningInfo.signObject(keyData, 'master');
@@ -132,7 +128,52 @@ export default class SecretStorage extends EventEmitter {
return keyId;
}
// TODO: need a function to get all the secret keys
/**
* Signs a given secret storage key with the cross-signing master key.
*
* @param {string} [keyId = default key's ID] The ID of the key to sign.
* Defaults to the default key ID if not provided.
*/
async signKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
throw new Error("signKey requires a key ID");
}
const keyInfo = await this._baseApis.getAccountDataFromServer(
`m.secret_storage.key.${keyId}`,
);
if (!keyInfo) {
throw new Error(`Key ${keyId} does not exist in account data`);
}
await this._crossSigningInfo.signObject(keyInfo, 'master');
await this._baseApis.setAccountData(
`m.secret_storage.key.${keyId}`, keyInfo,
);
}
/**
* Check whether we have a key with a given ID.
*
* @param {string} [keyId = default key's ID] The ID of the key to check
* for. Defaults to the default key ID if not provided.
* @return {boolean} Whether we have the key.
*/
async hasKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
return false;
}
return !!this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
}
/**
* Store an encrypted secret on the server
@@ -146,7 +187,7 @@ export default class SecretStorage extends EventEmitter {
const encrypted = {};
if (!keys) {
const defaultKeyId = this.getDefaultKeyId();
const defaultKeyId = await this.getDefaultKeyId();
if (!defaultKeyId) {
throw new Error("No keys specified and no default key present");
}
@@ -159,28 +200,27 @@ export default class SecretStorage extends EventEmitter {
for (const keyId of keys) {
// get key information from key storage
const keyInfo = this._baseApis.getAccountData(
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) {
throw new Error("Unknown key: " + keyId);
}
const keyInfoContent = keyInfo.getContent();
// check signature of key info
pkVerify(
keyInfoContent,
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
// encrypt secret, based on the algorithm
switch (keyInfoContent.algorithm) {
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
{
const encryption = new global.Olm.PkEncryption();
try {
encryption.set_recipient_key(keyInfoContent.pubkey);
encryption.set_recipient_key(keyInfo.pubkey);
encrypted[keyId] = encryption.encrypt(secret);
} finally {
encryption.free();
@@ -189,7 +229,7 @@ export default class SecretStorage extends EventEmitter {
}
default:
logger.warn("unknown algorithm for secret storage key " + keyId
+ ": " + keyInfoContent.algorithm);
+ ": " + keyInfo.algorithm);
// do nothing if we don't understand the encryption algorithm
}
}
@@ -198,6 +238,27 @@ export default class SecretStorage extends EventEmitter {
await this._baseApis.setAccountData(name, {encrypted});
}
/**
* Store a secret defined to be the same as the given key.
* No secret information will be stored, instead the secret will
* be stored with a marker to say that the contents of the secret is
* the value of the given key.
* This is useful for migration from systems that predate SSSS such as
* key backup.
*
* @param {string} name The name of the secret
* @param {string} keyId The ID of the key whose value will be the
* value of the secret
* @returns {Promise} resolved when account data is saved
*/
storePassthrough(name, keyId) {
return this._baseApis.setAccountData(name, {
[keyId]: {
passthrough: true,
},
});
}
/**
* Get a secret from storage.
*
@@ -206,25 +267,22 @@ export default class SecretStorage extends EventEmitter {
* @return {string} the contents of the secret
*/
async get(name) {
const secretInfo = this._baseApis.getAccountData(name);
const secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo) {
return;
}
const secretContent = secretInfo.getContent();
if (!secretContent.encrypted) {
if (!secretInfo.encrypted) {
throw new Error("Content is not encrypted!");
}
// get possible keys to decrypt
const keys = {};
for (const keyId of Object.keys(secretContent.encrypted)) {
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = this._baseApis.getAccountData(
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
).getContent();
const encInfo = secretContent.encrypted[keyId];
);
const encInfo = secretInfo.encrypted[keyId];
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
@@ -243,8 +301,13 @@ export default class SecretStorage extends EventEmitter {
// fetch private key from app
[keyId, decryption] = await this._getSecretStorageKey(keys);
const encInfo = secretInfo.encrypted[keyId];
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself.
if (encInfo.passthrough) return decryption.get_private_key();
// decrypt secret
const encInfo = secretContent.encrypted[keyId];
switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
return decryption.decrypt(
@@ -264,29 +327,24 @@ export default class SecretStorage extends EventEmitter {
*
* @return {boolean} whether or not the secret is stored
*/
isStored(name, checkKey) {
async isStored(name, checkKey) {
// check if secret exists
const secretInfo = this._baseApis.getAccountData(name);
if (!secretInfo) {
const secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo || !secretInfo.encrypted) {
return false;
}
if (checkKey === undefined) checkKey = true;
const secretContent = secretInfo.getContent();
if (!secretContent.encrypted) {
return false;
}
// check if secret is encrypted by a known/trusted secret and
// encryption looks sane
for (const keyId of Object.keys(secretContent.encrypted)) {
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = this._baseApis.getAccountData(
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
).getContent();
const encInfo = secretContent.encrypted[keyId];
);
if (!keyInfo) return false;
const encInfo = secretInfo.encrypted[keyId];
if (checkKey) {
pkVerify(
keyInfo,
@@ -487,9 +545,7 @@ export default class SecretStorage extends EventEmitter {
throw new Error("No getSecretStorageKey callback supplied");
}
const returned = await Promise.resolve(
this._cryptoCallbacks.getSecretStorageKey({keys}),
);
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys });
if (!returned) {
throw new Error("getSecretStorageKey callback returned falsey");
+13 -8
View File
@@ -20,8 +20,6 @@ limitations under the License.
* @module
*/
import Promise from 'bluebird';
/**
* map of registered encryption algorithm classes. A map from string to {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
@@ -52,7 +50,7 @@ export const DECRYPTION_CLASSES = {};
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
class EncryptionAlgorithm {
export class EncryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
@@ -86,7 +84,6 @@ class EncryptionAlgorithm {
onRoomMembership(event, member, oldMembership) {
}
}
export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* base type for decryption implementations
@@ -100,7 +97,7 @@ export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
class DecryptionAlgorithm {
export class DecryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._crypto = params.crypto;
@@ -161,8 +158,17 @@ class DecryptionAlgorithm {
shareKeysWithDevice(keyRequest) {
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
}
/**
* Retry decrypting all the events from a sender that haven't been
* decrypted yet.
*
* @param {string} senderKey the sender's key
*/
async retryDecryptionFromSender(senderKey) {
// ignore by default
}
}
export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* Exception thrown when decryption fails
@@ -175,7 +181,7 @@ export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
*
* @extends Error
*/
class DecryptionError extends Error {
export class DecryptionError extends Error {
constructor(code, msg, details) {
super(msg);
this.code = code;
@@ -183,7 +189,6 @@ class DecryptionError extends Error {
this.detailedString = _detailedStringForDecryptionError(this, details);
}
}
export {DecryptionError}; // https://github.com/jsdoc3/jsdoc/issues/1272
function _detailedStringForDecryptionError(err, details) {
let result = err.name + '[msg: ' + err.message;
+4 -19
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,28 +14,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/algorithms
*/
const base = require("./base");
import "./olm";
import "./megolm";
require("./olm");
require("./megolm");
/**
* @see module:crypto/algorithms/base.ENCRYPTION_CLASSES
*/
module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES;
/**
* @see module:crypto/algorithms/base.DECRYPTION_CLASSES
*/
module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES;
/**
* @see module:crypto/algorithms/base.DecryptionError
*/
module.exports.DecryptionError = base.DecryptionError;
export * from "./base";
+460 -84
View File
@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,7 +15,6 @@ 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";
/**
* Defines m.olm encryption/decryption
@@ -22,12 +22,19 @@ limitations under the License.
* @module crypto/algorithms/megolm
*/
import Promise from 'bluebird';
import logger from '../../logger';
import {logger} from '../../logger';
import * as utils from "../../utils";
import {polyfillSuper} from "../../utils";
import * as olmlib from "../olmlib";
import {
DecryptionAlgorithm,
DecryptionError,
EncryptionAlgorithm,
registerAlgorithm,
UnknownDeviceError,
} from "./base";
const utils = require("../../utils");
const olmlib = require("../olmlib");
const base = require("./base");
import {WITHHELD_MESSAGES} from '../OlmDevice';
/**
* @private
@@ -48,6 +55,7 @@ function OutboundSessionInfo(sessionId) {
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharedWithDevices = {};
this.blockedDevicesNotified = {};
}
@@ -85,6 +93,15 @@ OutboundSessionInfo.prototype.markSharedWithDevice = function(
this.sharedWithDevices[userId][deviceId] = chainIndex;
};
OutboundSessionInfo.prototype.markNotifiedBlockedDevice = function(
userId, deviceId,
) {
if (!this.blockedDevicesNotified[userId]) {
this.blockedDevicesNotified[userId] = {};
}
this.blockedDevicesNotified[userId][deviceId] = true;
};
/**
* Determine if this session has been shared with devices which it shouldn't
* have been.
@@ -129,13 +146,13 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
* Megolm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
* {@link module:crypto/algorithms/EncryptionAlgorithm}
*/
function MegolmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
polyfillSuper(this, EncryptionAlgorithm, params);
// the most recent attempt to set up a session. This is used to serialise
// the session setups, so that we have a race-free view of which session we
@@ -161,17 +178,20 @@ function MegolmEncryption(params) {
this._sessionRotationPeriodMsgs = params.config.rotation_period_msgs;
}
}
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
utils.inherits(MegolmEncryption, EncryptionAlgorithm);
/**
* @private
*
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
* @param {Object} blocked The devices that are blocked, indexed by user ID
*
* @return {module:client.Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked,
) {
const self = this;
let session;
@@ -238,9 +258,50 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
}
}
return self._shareKeyWithDevices(
session, shareMap,
const errorDevices = [];
await self._shareKeyWithDevices(
session, shareMap, errorDevices,
);
// are there any new blocked devices that we need to notify?
const blockedMap = {};
for (const userId in blocked) {
if (!blocked.hasOwnProperty(userId)) {
continue;
}
const userBlockedDevices = blocked[userId];
for (const deviceId in userBlockedDevices) {
if (!userBlockedDevices.hasOwnProperty(deviceId)) {
continue;
}
if (
!session.blockedDevicesNotified[userId] ||
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push(userBlockedDevices[deviceId]);
}
}
}
const filteredErrorDevices =
await self._olmDevice.filterOutNotifiedErrorDevices(errorDevices);
for (const {userId, deviceInfo} of filteredErrorDevices) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push({
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
});
}
// notify blocked devices that they're blocked
await self._notifyBlockedDevices(session, blockedMap);
}
// helper which returns the session prepared by prepareSession
@@ -288,6 +349,10 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
};
/**
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call. Also filters out devices we don't have
* a session with.
*
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
@@ -300,12 +365,16 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that can't get an
* olm session for
*
* @return {array<object<userid, deviceInfo>>}
*/
MegolmEncryption.prototype._splitUserDeviceMap = function(
session, chainIndex, devicemap, devicesByUser,
session, chainIndex, devicemap, devicesByUser, errorDevices,
) {
const maxToDeviceMessagesPerRequest = 20;
const maxUsersPerRequest = 20;
// use an array where the slices of a content map gets stored
const mapSlices = [];
@@ -335,6 +404,8 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
// to claim a one-time-key for dead devices on every message.
session.markSharedWithDevice(userId, deviceId, chainIndex);
errorDevices.push({userId, deviceInfo});
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
@@ -344,11 +415,6 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
"share keys with device " + userId + ":" + deviceId,
);
if (entriesInCurrentSlice > maxToDeviceMessagesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
if (!mapSlices[currentSliceId]) {
mapSlices[currentSliceId] = [];
}
@@ -360,6 +426,61 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
entriesInCurrentSlice++;
}
// We do this in the per-user loop as we prefer that all messages to the
// same user end up in the same API call to make it easier for the
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (entriesInCurrentSlice > maxUsersPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
}
return mapSlices;
};
/**
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call.
*
* @private
*
* @param {object} devicesByUser map from userid to list of devices
*
* @return {array<array<object>>} the blocked devices, split into chunks
*/
MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
const maxUsersPerRequest = 20;
// use an array where the slices of a content map gets stored
let currentSlice = [];
const mapSlices = [currentSlice];
for (const userId of Object.keys(devicesByUser)) {
const userBlockedDevicesToShareWith = devicesByUser[userId];
for (const blockedInfo of userBlockedDevicesToShareWith) {
currentSlice.push({
userId: userId,
blockedInfo: blockedInfo,
});
}
// We do this in the per-user loop as we prefer that all messages to the
// same user end up in the same API call to make it easier for the
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (currentSlice.length > maxUsersPerRequest) {
// the current slice is filled up. Start inserting into the next slice
currentSlice = [];
mapSlices.push(currentSlice);
}
}
if (currentSlice.length === 0) {
mapSlices.pop();
}
return mapSlices;
};
@@ -382,15 +503,15 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
session, chainIndex, userDeviceMap, payload,
) {
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
const contentMap = {};
const promises = [];
for (let i = 0; i < userDeviceMap.length; i++) {
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
const val = userDeviceMap[i];
const userId = val.userId;
const deviceInfo = val.deviceInfo;
@@ -428,6 +549,53 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
});
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {array<object>} userDeviceMap list of blocked devices to notify
*
* @param {object} payload fields to include in the notification payload
*
* @return {module:client.Promise} Promise which resolves once the notifications
* for the given userDeviceMap is generated and has been sent.
*/
MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
session, userDeviceMap, payload,
) {
const contentMap = {};
for (const val of userDeviceMap) {
const userId = val.userId;
const blockedInfo = val.blockedInfo;
const deviceInfo = blockedInfo.deviceInfo;
const deviceId = deviceInfo.deviceId;
const message = Object.assign({}, payload);
message.code = blockedInfo.code;
message.reason = blockedInfo.reason;
if (message.code === "m.no_olm") {
delete message.room_id;
delete message.session_id;
}
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = message;
}
await this._baseApis.sendToDevice("org.matrix.room_key.withheld", contentMap);
// store that we successfully uploaded the keys of the current slice
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
session.markNotifiedBlockedDevice(userId, deviceId);
}
}
};
/**
* Re-shares a megolm session key with devices if the key has already been
* sent to them.
@@ -508,7 +676,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
userId,
device,
payload,
),
);
await this._baseApis.sendToDevice("m.room.encrypted", {
[userId]: {
@@ -524,8 +692,14 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that we can't get an
* olm session for
*/
MegolmEncryption.prototype._shareKeyWithDevices = async function(session, devicesByUser) {
MegolmEncryption.prototype._shareKeyWithDevices = async function(
session, devicesByUser, errorDevices,
) {
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
@@ -543,7 +717,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
);
const userDeviceMaps = this._splitUserDeviceMap(
session, key.chain_index, devicemap, devicesByUser,
session, key.chain_index, devicemap, devicesByUser, errorDevices,
);
for (let i = 0; i < userDeviceMaps.length; i++) {
@@ -562,6 +736,42 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
}
};
/**
* Notify blocked devices that they have been blocked.
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, object>} devicesByUser
* map from userid to device ID to blocked data
*/
MegolmEncryption.prototype._notifyBlockedDevices = async function(
session, devicesByUser,
) {
const payload = {
room_id: this._roomId,
session_id: session.sessionId,
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
};
const userDeviceMaps = this._splitBlockedDevices(devicesByUser);
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
await this._sendBlockedNotificationsToDevices(
session, userDeviceMaps[i], payload,
);
logger.log(`Completed blacklist notification for ${session.sessionId} `
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
} catch (e) {
logger.log(`blacklist notification for ${session.sessionId} in `
+ `${this._roomId} (slice ${i + 1}/${userDeviceMaps.length}) failed`);
throw e;
}
}
};
/**
* @inheritdoc
*
@@ -571,42 +781,43 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
const self = this;
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.
// if so, warn the user so they can verify or ignore.
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
self._checkForUnknownDevices(devicesInRoom);
}
return self._ensureOutboundSession(devicesInRoom);
}).then(function(session) {
const payloadJson = {
room_id: self._roomId,
type: eventType,
content: content,
};
const session = await self._ensureOutboundSession(devicesInRoom, blocked);
const payloadJson = {
room_id: self._roomId,
type: eventType,
content: content,
};
const ciphertext = self._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
const ciphertext = self._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
};
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
};
session.useCount++;
return encryptedContent;
});
session.useCount++;
return encryptedContent;
};
/**
@@ -644,7 +855,7 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
if (Object.keys(unknownDevices).length) {
// it'd be kind to pass unknownDevices up to the user in this error
throw new base.UnknownDeviceError(
throw new UnknownDeviceError(
"This room contains unknown devices which have not been verified. " +
"We strongly recommend you verify them before continuing.", unknownDevices);
}
@@ -655,8 +866,11 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to a map
* from userId to deviceId to deviceInfo
* @return {module:client.Promise} Promise which resolves to an array whose
* first element is a map from userId to deviceId to deviceInfo indicating
* the devices that messages should be encrypted to, and whose second
* element is a map from userId to deviceId to data indicating the devices
* that are in the room but that have been blocked
*/
MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
const members = await room.getEncryptionTargetMembers();
@@ -677,6 +891,7 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
// using all the device_lists changes and left fields.
// See https://github.com/vector-im/riot-web/issues/2305 for details.
const devices = await this._crypto.downloadKeys(roomMembers, false);
const blocked = {};
// remove any blocked devices
for (const userId in devices) {
if (!devices.hasOwnProperty(userId)) {
@@ -691,26 +906,40 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting)
) {
) {
if (!blocked[userId]) {
blocked[userId] = {};
}
const blockedInfo = userDevices[deviceId].isBlocked()
? {
code: "m.blacklisted",
reason: WITHHELD_MESSAGES["m.blacklisted"],
}
: {
code: "m.unverified",
reason: WITHHELD_MESSAGES["m.unverified"],
};
blockedInfo.deviceInfo = userDevices[deviceId];
blocked[userId][deviceId] = blockedInfo;
delete userDevices[deviceId];
}
}
}
return devices;
return [devices, blocked];
};
/**
* Megolm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
* {@link module:crypto/algorithms/DecryptionAlgorithm}
*/
function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
polyfillSuper(this, DecryptionAlgorithm, params);
// events which we couldn't decrypt due to unknown sessions / indexes: map from
// senderKey|sessionId to Set of MatrixEvents
@@ -719,7 +948,12 @@ function MegolmDecryption(params) {
// this gets stubbed out by the unit tests.
this.olmlib = olmlib;
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
utils.inherits(MegolmDecryption, DecryptionAlgorithm);
const PROBLEM_DESCRIPTIONS = {
no_olm: "The sender was unable to establish a secure channel.",
unknown: "The secure channel with the sender was corrupted.",
};
/**
* @inheritdoc
@@ -737,7 +971,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
throw new base.DecryptionError(
throw new DecryptionError(
"MEGOLM_MISSING_FIELDS",
"Missing fields in input",
);
@@ -757,6 +991,11 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
event.getId(), event.getTs(),
);
} catch (e) {
if (e.name === "DecryptionError") {
// re-throw decryption errors as-is
throw e;
}
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
@@ -765,7 +1004,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
}
throw new base.DecryptionError(
throw new DecryptionError(
errorCode,
e ? e.toString() : "Unknown Error: Error is undefined", {
session: content.sender_key + '|' + content.session_id,
@@ -782,7 +1021,29 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
// event is still in the pending list; if not, a retry will have been
// scheduled, so we needn't send out the request here.)
this._requestKeysForEvent(event);
throw new base.DecryptionError(
// See if there was a problem with the olm session at the time the
// event was sent. Use a fuzz factor of 2 minutes.
const problem = await this._olmDevice.sessionMayHaveProblems(
content.sender_key, event.getTs() - 120000,
);
if (problem) {
let problemDescription = PROBLEM_DESCRIPTIONS[problem.type]
|| PROBLEM_DESCRIPTIONS.unknown;
if (problem.fixed) {
problemDescription +=
" Trying to create a new secure channel and re-requesting the keys.";
}
throw new DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
problemDescription,
{
session: content.sender_key + '|' + content.session_id,
},
);
}
throw new DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
"The sender's device has not sent us the keys for this message.",
{
@@ -801,7 +1062,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
// (this is somewhat redundant, since the megolm session is scoped to the
// room, so neither the sender nor a MITM can lie about the room_id).
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
throw new DecryptionError(
"MEGOLM_BAD_ROOM",
"Message intended for room " + payload.room_id,
);
@@ -837,11 +1098,16 @@ MegolmDecryption.prototype._requestKeysForEvent = function(event) {
*/
MegolmDecryption.prototype._addEventToPendingList = function(event) {
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
this._pendingEvents[k] = new Set();
const senderKey = content.sender_key;
const sessionId = content.session_id;
if (!this._pendingEvents[senderKey]) {
this._pendingEvents[senderKey] = new Map();
}
this._pendingEvents[k].add(event);
const senderPendingEvents = this._pendingEvents[senderKey];
if (!senderPendingEvents.has(sessionId)) {
senderPendingEvents.set(sessionId, new Set());
}
senderPendingEvents.get(sessionId).add(event);
};
/**
@@ -853,14 +1119,20 @@ MegolmDecryption.prototype._addEventToPendingList = function(event) {
*/
MegolmDecryption.prototype._removeEventFromPendingList = function(event) {
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
const senderKey = content.sender_key;
const sessionId = content.session_id;
const senderPendingEvents = this._pendingEvents[senderKey];
const pendingEvents = senderPendingEvents && senderPendingEvents.get(sessionId);
if (!pendingEvents) {
return;
}
this._pendingEvents[k].delete(event);
if (this._pendingEvents[k].size === 0) {
delete this._pendingEvents[k];
pendingEvents.delete(event);
if (pendingEvents.size === 0) {
senderPendingEvents.delete(senderKey);
}
if (senderPendingEvents.size === 0) {
delete this._pendingEvents[senderKey];
}
};
@@ -964,6 +1236,78 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
});
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
const content = event.getContent();
const senderKey = content.sender_key;
if (content.code === "m.no_olm") {
const sender = event.getSender();
// if the sender says that they haven't been able to establish an olm
// session, let's proactively establish one
// Note: after we record that the olm session has had a problem, we
// trigger retrying decryption for all the messages from the sender's
// key, so that we can update the error message to indicate the olm
// session problem.
if (await this._olmDevice.getSessionIdForDevice(senderKey)) {
// a session has already been established, so we don't need to
// create a new one.
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
this.retryDecryptionFromSender(senderKey);
return;
}
const device = this._crypto._deviceList.getDeviceByIdentityKey(
content.algorithm, senderKey,
);
if (!device) {
logger.info(
"Couldn't find device for identity key " + senderKey +
": not establishing session",
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false);
this.retryDecryptionFromSender(senderKey);
return;
}
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {[sender]: [device]}, false,
);
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._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
this.retryDecryptionFromSender(senderKey);
await this._baseApis.sendToDevice("m.room.encrypted", {
[sender]: {
[device.deviceId]: encryptedContent,
},
});
} else {
await this._olmDevice.addInboundGroupSessionWithheld(
content.room_id, senderKey, content.session_id, content.code,
content.reason,
);
}
};
/**
* @inheritdoc
*/
@@ -1036,7 +1380,7 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) {
// TODO: retries
return this._baseApis.sendToDevice("m.room.encrypted", contentMap);
});
}).done();
});
};
MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
@@ -1107,13 +1451,20 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
* @return {Boolean} whether all messages were successfully decrypted
*/
MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionId) {
const k = senderKey + "|" + sessionId;
const pending = this._pendingEvents[k];
const senderPendingEvents = this._pendingEvents[senderKey];
if (!senderPendingEvents) {
return true;
}
const pending = senderPendingEvents.get(sessionId);
if (!pending) {
return true;
}
delete this._pendingEvents[k];
pending.delete(sessionId);
if (pending.size === 0) {
this._pendingEvents[senderKey];
}
await Promise.all([...pending].map(async (ev) => {
try {
@@ -1123,9 +1474,34 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
}
}));
return !this._pendingEvents[k];
// ev.attemptDecryption will re-add to this._pendingEvents if an event
// couldn't be decrypted
return !((this._pendingEvents[senderKey] || {})[sessionId]);
};
base.registerAlgorithm(
MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) {
const senderPendingEvents = this._pendingEvents[senderKey];
logger.warn(senderPendingEvents);
if (!senderPendingEvents) {
return true;
}
delete this._pendingEvents[senderKey];
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
await Promise.all([...pending].map(async (ev) => {
try {
logger.warn(ev.getId());
await ev.attemptDecryption(this._crypto);
} catch (e) {
// don't die if something goes wrong
}
}));
}));
return !this._pendingEvents[senderKey];
};
registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
);
+28 -25
View File
@@ -13,39 +13,42 @@ 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";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/olm
*/
import Promise from 'bluebird';
import logger from '../../logger';
const utils = require("../../utils");
const olmlib = require("../olmlib");
const DeviceInfo = require("../deviceinfo");
import {logger} from '../../logger';
import * as utils from "../../utils";
import {polyfillSuper} from "../../utils";
import * as olmlib from "../olmlib";
import {DeviceInfo} from "../deviceinfo";
import {
DecryptionAlgorithm,
DecryptionError,
EncryptionAlgorithm,
registerAlgorithm,
} from "./base";
const DeviceVerification = DeviceInfo.DeviceVerification;
const base = require("./base");
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
* {@link module:crypto/algorithms/EncryptionAlgorithm}
*/
function OlmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
polyfillSuper(this, EncryptionAlgorithm, params);
this._sessionPrepared = false;
this._prepPromise = null;
}
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
utils.inherits(OlmEncryption, EncryptionAlgorithm);
/**
* @private
@@ -146,14 +149,14 @@ OlmEncryption.prototype.encryptMessage = async function(room, eventType, content
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
* {@link module:crypto/algorithms/DecryptionAlgorithm}
*/
function OlmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
polyfillSuper(this, DecryptionAlgorithm, params);
}
utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
utils.inherits(OlmDecryption, DecryptionAlgorithm);
/**
* @inheritdoc
@@ -171,14 +174,14 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_MISSING_CIPHERTEXT",
"Missing ciphertext",
);
}
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
"Not included in recipients",
);
@@ -189,7 +192,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
try {
payloadString = await this._decryptMessage(deviceKey, message);
} catch (e) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_BAD_ENCRYPTED_MESSAGE",
"Bad Encrypted Message", {
sender: deviceKey,
@@ -203,14 +206,14 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this._userId) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_BAD_RECIPIENT",
"Message was intented for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_BAD_RECIPIENT_KEY",
"Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
@@ -224,7 +227,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_FORWARDED_MESSAGE",
"Message forwarded from " + payload.sender, {
reported_sender: event.getSender(),
@@ -234,7 +237,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_BAD_ROOM",
"Message intended for room " + payload.room_id, {
reported_room: event.room_id,
@@ -337,4 +340,4 @@ OlmDecryption.prototype._decryptMessage = async function(
};
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
+2 -5
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,8 +14,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/deviceinfo
@@ -44,7 +43,7 @@ limitations under the License.
*
* @param {string} deviceId id of the device
*/
function DeviceInfo(deviceId) {
export function DeviceInfo(deviceId) {
// you can't change the deviceId
Object.defineProperty(this, 'deviceId', {
enumerable: true,
@@ -167,5 +166,3 @@ DeviceInfo.DeviceVerification = {
const DeviceVerification = DeviceInfo.DeviceVerification;
/** */
module.exports = DeviceInfo;
+573 -590
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { randomString } from '../randomstring';
import {randomString} from '../randomstring';
const DEFAULT_ITERATIONS = 500000;
@@ -49,7 +49,7 @@ export async function keyFromPassphrase(password) {
return { key, salt, iterations: DEFAULT_ITERATIONS };
}
async function deriveKey(password, salt, iterations) {
export async function deriveKey(password, salt, iterations) {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
+37 -21
View File
@@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -21,26 +22,24 @@ limitations under the License.
* Utilities common to olm encryption algorithms
*/
import Promise from 'bluebird';
const anotherjson = require('another-json');
import logger from '../logger';
const utils = require("../utils");
import {logger} from '../logger';
import * as utils from "../utils";
import anotherjson from "another-json";
/**
* matrix algorithm tag for olm
*/
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
export const OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* matrix algorithm tag for megolm
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
export const 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";
export const MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
/**
@@ -59,7 +58,7 @@ module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2
* Returns a promise which resolves (to undefined) when the payload
* has been encrypted into `resultsObject`
*/
module.exports.encryptMessageForDevice = async function(
export async function encryptMessageForDevice(
resultsObject,
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
payloadFields,
@@ -112,7 +111,7 @@ module.exports.encryptMessageForDevice = async function(
resultsObject[deviceKey] = await olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
);
};
}
/**
* Try to make sure we have established olm sessions for the given devices.
@@ -131,7 +130,7 @@ module.exports.encryptMessageForDevice = async function(
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
module.exports.ensureOlmSessionsForDevices = async function(
export async function ensureOlmSessionsForDevices(
olmDevice, baseApis, devicesByUser, force,
) {
const devicesWithoutSession = [
@@ -263,12 +262,12 @@ module.exports.ensureOlmSessionsForDevices = async function(
await Promise.all(promises);
return result;
};
}
async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
const deviceId = deviceInfo.deviceId;
try {
await _verifySignature(
await verifySignature(
olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint(),
);
@@ -303,8 +302,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
*
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
*
* @param {Object} obj object to check signature on. Note that this will be
* stripped of its 'signatures' and 'unsigned' properties.
* @param {Object} obj object to check signature on.
*
* @param {string} signingUserId ID of the user whose signature should be checked
*
@@ -315,7 +313,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
* Returns a promise which resolves (to undefined) if the the signature is good,
* or rejects with an Error if it is bad.
*/
const _verifySignature = module.exports.verifySignature = async function(
export async function verifySignature(
olmDevice, obj, signingUserId, signingDeviceId, signingKey,
) {
const signKeyId = "ed25519:" + signingDeviceId;
@@ -336,7 +334,7 @@ const _verifySignature = module.exports.verifySignature = async function(
olmDevice.verifySignature(
signingKey, json, signature,
);
};
}
/**
* Sign a JSON object using public key cryptography
@@ -348,7 +346,7 @@ const _verifySignature = module.exports.verifySignature = async function(
* @param {string} pubkey The public key (ignored if key is a seed)
* @returns {string} the signature for the object
*/
module.exports.pkSign = function(obj, key, userId, pubkey) {
export function pkSign(obj, key, userId, pubkey) {
let createdKey = false;
if (key instanceof Uint8Array) {
const keyObj = new global.Olm.PkSigning();
@@ -372,7 +370,7 @@ module.exports.pkSign = function(obj, key, userId, pubkey) {
key.free();
}
}
};
}
/**
* Verify a signed JSON object
@@ -380,7 +378,7 @@ module.exports.pkSign = function(obj, key, userId, pubkey) {
* @param {string} pubkey The public key to use to verify
* @param {string} userId The user ID who signed the object
*/
module.exports.pkVerify = function(obj, pubkey, userId) {
export function pkVerify(obj, pubkey, userId) {
const keyId = "ed25519:" + pubkey;
if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
throw new Error("No signature");
@@ -398,4 +396,22 @@ module.exports.pkVerify = function(obj, pubkey, userId) {
if (unsigned) obj.unsigned = unsigned;
util.free();
}
};
}
/**
* Encode a typed array of uint8 as base64.
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The base64.
*/
export function encodeBase64(uint8Array) {
return Buffer.from(uint8Array).toString("base64");
}
/**
* Decode a base64 string to a typed array of uint8.
* @param {string} base64 The base64 to decode.
* @return {Uint8Array} The decoded data.
*/
export function decodeBase64(base64) {
return Buffer.from(base64, "base64");
}
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,12 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import {logger} from '../../logger';
import * as utils from "../../utils";
import logger from '../../logger';
import utils from '../../utils';
export const VERSION = 7;
export const VERSION = 9;
/**
* Implementation of a CryptoStore which is backed by an existing
@@ -81,7 +80,7 @@ export class Backend {
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
txn.oncomplete = () => { resolve(request); };
txn.oncomplete = () => {resolve(request);};
const store = txn.objectStore("outgoingRoomKeyRequests");
store.add(request);
});
@@ -427,17 +426,107 @@ export class Backend {
});
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const txn = this._db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
objectStore.put({
deviceKey,
type,
fixed,
time: Date.now(),
});
return promiseifyTxn(txn);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
let result;
const txn = this._db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
const index = objectStore.index("deviceKey");
const req = index.getAll(deviceKey);
req.onsuccess = (event) => {
const problems = req.result;
if (!problems.length) {
result = null;
return;
}
problems.sort((a, b) => {
return a.time - b.time;
});
const lastProblem = problems[problems.length - 1];
for (const problem of problems) {
if (problem.time > timestamp) {
result = Object.assign({}, problem, {fixed: lastProblem.fixed});
return;
}
}
if (lastProblem.fixed) {
result = null;
} else {
result = lastProblem;
}
};
await promiseifyTxn(txn);
return result;
}
// FIXME: we should probably prune this when devices get deleted
async filterOutNotifiedErrorDevices(devices) {
const txn = this._db.transaction("notified_error_devices", "readwrite");
const objectStore = txn.objectStore("notified_error_devices");
const ret = [];
await Promise.all(devices.map((device) => {
return new Promise((resolve) => {
const {userId, deviceInfo} = device;
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
getReq.onsuccess = function() {
if (!getReq.result) {
objectStore.put({userId, deviceId: deviceInfo.deviceId});
ret.push(device);
}
resolve();
};
});
}));
return ret;
}
// Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
let session = false;
let withheld = false;
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() {
try {
if (getReq.result) {
func(getReq.result.session);
session = getReq.result.session;
} else {
func(null);
session = null;
}
if (withheld !== false) {
func(session, withheld);
}
} catch (e) {
abortWithException(txn, e);
}
};
const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
withheldGetReq.onsuccess = function() {
try {
if (withheldGetReq.result) {
withheld = withheldGetReq.result.session;
} else {
withheld = null;
}
if (session !== false) {
func(session, withheld);
}
} catch (e) {
abortWithException(txn, e);
@@ -501,6 +590,15 @@ export class Backend {
});
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
const objectStore = txn.objectStore("inbound_group_sessions_withheld");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
getEndToEndDeviceData(txn, func) {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
@@ -664,6 +762,21 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 8) {
db.createObjectStore("inbound_group_sessions_withheld", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 9) {
const problemsStore = db.createObjectStore("session_problems", {
keyPath: ["deviceKey", "time"],
});
problemsStore.createIndex("deviceKey", "deviceKey");
db.createObjectStore("notified_error_devices", {
keyPath: ["userId", "deviceId"],
});
}
// Expand as needed.
}
+39 -7
View File
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,11 +16,9 @@ See the License for the specific language governing permissions and
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 {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';
import * as IndexedDBHelpers from "../../indexeddb-helpers";
@@ -36,7 +35,7 @@ import * as IndexedDBHelpers from "../../indexeddb-helpers";
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class IndexedDBCryptoStore {
export class IndexedDBCryptoStore {
/**
* Create a new IndexedDBCryptoStore
*
@@ -106,7 +105,10 @@ export default class IndexedDBCryptoStore {
// we can fall back to a different backend.
return backend.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
[
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
],
(txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => {
@@ -407,6 +409,24 @@ export default class IndexedDBCryptoStore {
});
}
storeEndToEndSessionProblem(deviceKey, type, fixed) {
return this._backendPromise.then(async (backend) => {
await backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
});
}
getEndToEndSessionProblem(deviceKey, timestamp) {
return this._backendPromise.then(async (backend) => {
return await backend.getEndToEndSessionProblem(deviceKey, timestamp);
});
}
filterOutNotifiedErrorDevices(devices) {
return this._backendPromise.then(async (backend) => {
return await backend.filterOutNotifiedErrorDevices(devices);
});
}
// Inbound group sessions
/**
@@ -473,6 +493,16 @@ export default class IndexedDBCryptoStore {
});
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
this._backendPromise.then(backend => {
backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
}
// End-to-end device tracking
/**
@@ -609,6 +639,8 @@ export default class IndexedDBCryptoStore {
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
= 'inbound_group_sessions_withheld';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
+86 -9
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017, 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,10 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../logger';
import MemoryCryptoStore from './memory-crypto-store';
import {logger} from '../../logger';
import {MemoryCryptoStore} from './memory-crypto-store';
/**
* Internal module. Partial localStorage backed storage for e2e.
@@ -32,8 +31,10 @@ import MemoryCryptoStore from './memory-crypto-store';
const E2E_PREFIX = "crypto.";
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
const KEY_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
@@ -41,10 +42,18 @@ function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndSessionProblems(deviceKey) {
return E2E_PREFIX + "session.problems/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) {
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndRoomsPrefix(roomId) {
return KEY_ROOMS_PREFIX + roomId;
}
@@ -52,7 +61,7 @@ function keyEndToEndRoomsPrefix(roomId) {
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class LocalStorageCryptoStore extends MemoryCryptoStore {
export class LocalStorageCryptoStore extends MemoryCryptoStore {
constructor(webStore) {
super();
this.store = webStore;
@@ -124,13 +133,71 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
problems.push({type, fixed, time: Date.now()});
problems.sort((a, b) => {
return a.time - b.time;
});
setJsonItem(this.store, key, problems);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
if (!problems.length) {
return null;
}
const lastProblem = problems[problems.length - 1];
for (const problem of problems) {
if (problem.time > timestamp) {
return Object.assign({}, problem, {fixed: lastProblem.fixed});
}
}
if (lastProblem.fixed) {
return null;
} else {
return lastProblem;
}
}
async filterOutNotifiedErrorDevices(devices) {
const notifiedErrorDevices =
getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
const ret = [];
for (const device of devices) {
const {userId, deviceInfo} = device;
if (userId in notifiedErrorDevices) {
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
ret.push(device);
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
}
} else {
ret.push(device);
notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true };
}
}
setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices);
return ret;
}
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
));
func(
getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
),
getJsonItem(
this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
),
);
}
getAllEndToEndInboundGroupSessions(txn, func) {
@@ -172,6 +239,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
setJsonItem(
this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
sessionData,
);
}
getEndToEndDeviceData(txn, func) {
func(getJsonItem(
this.store, KEY_DEVICE_DATA,
+69 -7
View File
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,10 +16,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../logger';
import utils from '../../utils';
import {logger} from '../../logger';
import * as utils from "../../utils";
/**
* Internal module. in-memory storage for e2e.
@@ -29,7 +28,7 @@ import utils from '../../utils';
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class MemoryCryptoStore {
export class MemoryCryptoStore {
constructor() {
this._outgoingRoomKeyRequests = [];
this._account = null;
@@ -37,8 +36,13 @@ export default class MemoryCryptoStore {
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
// Map of {devicekey -> array of problems}
this._sessionProblems = {};
// Map of {userId -> deviceId -> true}
this._notifiedErrorDevices = {};
// Map of {senderCurve25519Key+'/'+sessionId -> session data object}
this._inboundGroupSessions = {};
this._inboundGroupSessionsWithheld = {};
// Opaque device data object
this._deviceData = null;
// roomId -> Opaque roomInfo object
@@ -69,7 +73,7 @@ export default class MemoryCryptoStore {
getOrAddOutgoingRoomKeyRequest(request) {
const requestBody = request.requestBody;
return Promise.try(() => {
return utils.promiseTry(() => {
// first see if we already have an entry for this request.
const existing = this._getOutgoingRoomKeyRequest(requestBody);
@@ -275,10 +279,61 @@ export default class MemoryCryptoStore {
deviceSessions[sessionId] = sessionInfo;
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const problems = this._sessionProblems[deviceKey]
= this._sessionProblems[deviceKey] || [];
problems.push({type, fixed, time: Date.now()});
problems.sort((a, b) => {
return a.time - b.time;
});
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
const problems = this._sessionProblems[deviceKey] || [];
if (!problems.length) {
return null;
}
const lastProblem = problems[problems.length - 1];
for (const problem of problems) {
if (problem.time > timestamp) {
return Object.assign({}, problem, {fixed: lastProblem.fixed});
}
}
if (lastProblem.fixed) {
return null;
} else {
return lastProblem;
}
}
async filterOutNotifiedErrorDevices(devices) {
const notifiedErrorDevices = this._notifiedErrorDevices;
const ret = [];
for (const device of devices) {
const {userId, deviceInfo} = device;
if (userId in notifiedErrorDevices) {
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
ret.push(device);
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
}
} else {
ret.push(device);
notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true };
}
}
return ret;
}
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null);
const k = senderCurve25519Key+'/'+sessionId;
func(
this._inboundGroupSessions[k] || null,
this._inboundGroupSessionsWithheld[k] || null,
);
}
getAllEndToEndInboundGroupSessions(txn, func) {
@@ -308,6 +363,13 @@ export default class MemoryCryptoStore {
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
const k = senderCurve25519Key+'/'+sessionId;
this._inboundGroupSessionsWithheld[k] = sessionData;
}
// Device Data
getEndToEndDeviceData(txn, func) {
+22 -103
View File
@@ -21,13 +21,13 @@ limitations under the License.
import {MatrixEvent} from '../../models/event';
import {EventEmitter} from 'events';
import logger from '../../logger';
import DeviceInfo from '../deviceinfo';
import {logger} from '../../logger';
import {DeviceInfo} from '../deviceinfo';
import {newTimeoutError} from "./Error";
const timeoutException = new Error("Verification timed out");
export default class VerificationBase extends EventEmitter {
export class VerificationBase extends EventEmitter {
/**
* Base class for verification methods.
*
@@ -40,51 +40,36 @@ export default class VerificationBase extends EventEmitter {
*
* @class
*
* @param {module:base-apis~Channel} channel the verification channel to send verification messages over.
*
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
*
* @param {string} userId the user ID that is being verified
*
* @param {string} deviceId the device ID that is being verified
*
* @param {string} transactionId the transaction ID to be used when sending events
*
* @param {string} [roomId] the room to use for verification
*
* @param {object} [startEvent] the m.key.verification.start event that
* initiated this verification, if any
*
* @param {object} [request] the key verification request object related to
* this verification, if any
*/
constructor(baseApis, userId, deviceId, transactionId, roomId, startEvent, request) {
constructor(channel, baseApis, userId, deviceId, startEvent, request) {
super();
this._channel = channel;
this._baseApis = baseApis;
this.userId = userId;
this.deviceId = deviceId;
this.transactionId = transactionId;
if (typeof(roomId) === "string" || roomId instanceof String) {
this.roomId = roomId;
this.startEvent = startEvent;
this.request = request;
} else {
// if room ID was omitted, but start event and request were not
this.startEvent= roomId;
this.request = startEvent;
}
this.startEvent = startEvent;
this.request = request;
this.cancelled = false;
this._done = false;
this._promise = null;
this._transactionTimeoutTimer = null;
this._eventsSubscription = null;
// At this point, the verification request was received so start the timeout timer.
this._resetTimer();
if (this.roomId) {
this._sendWithTxnId = this._sendMessage;
} else {
this._sendWithTxnId = this._sendToDevice;
}
}
_resetTimer() {
@@ -107,55 +92,8 @@ export default class VerificationBase extends EventEmitter {
}
}
_contentFromEventWithTxnId(event) {
if (this.roomId) { // verification as timeline event
// ensure m.related_to is included in e2ee rooms
// as the field is excluded from encryption
const content = Object.assign({}, event.getContent());
content["m.relates_to"] = event.getRelation();
return content;
} else { // verification as to_device event
return event.getContent();
}
}
/* creates a content object with the transaction id added to it */
_contentWithTxnId(content) {
const copy = Object.assign({}, content);
if (this.roomId) { // verification as timeline event
copy["m.relates_to"] = {
rel_type: "m.reference",
event_id: this.transactionId,
};
} else { // verification as to_device event
copy.transaction_id = this.transactionId;
}
return copy;
}
_send(type, contentWithoutTxnId) {
const content = this._contentWithTxnId(contentWithoutTxnId);
return this._sendWithTxnId(type, content);
}
/* send a message to the other participant, using to-device messages
*/
_sendToDevice(type, content) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
return this._baseApis.sendToDevice(type, {
[this.userId]: { [this.deviceId]: content },
});
}
/* send a message to the other participant, using in-roomm messages
*/
_sendMessage(type, content) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
return this._baseApis.sendEvent(this.roomId, type, content);
_send(type, uncompletedContent) {
return this._channel.send(type, uncompletedContent);
}
_waitForEvent(type) {
@@ -173,10 +111,14 @@ export default class VerificationBase extends EventEmitter {
if (this._done) {
return;
} else if (e.getType() === this._expectedEvent) {
this._expectedEvent = undefined;
this._rejectEvent = undefined;
this._resetTimer();
this._resolveEvent(e);
// if we receive an expected m.key.verification.done, then just
// ignore it, since we don't need to do anything about it
if (this._expectedEvent !== "m.key.verification.done") {
this._expectedEvent = undefined;
this._rejectEvent = undefined;
this._resetTimer();
this._resolveEvent(e);
}
} else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject;
this._reject = undefined;
@@ -199,7 +141,7 @@ export default class VerificationBase extends EventEmitter {
done() {
this._endTimer(); // always kill the activity timer
if (!this._done) {
if (this.roomId) {
if (this._channel.needsDoneMessage) {
// verification in DM requires a done message
this._send("m.key.verification.done", {});
}
@@ -211,7 +153,7 @@ export default class VerificationBase extends EventEmitter {
this._endTimer(); // always kill the activity timer
if (!this._done) {
this.cancelled = true;
if (this.userId && this.deviceId && this.transactionId) {
if (this.userId && this.deviceId) {
// send a cancellation to the other user (if it wasn't
// cancelled by the other user)
if (e === timeoutException) {
@@ -225,13 +167,11 @@ export default class VerificationBase extends EventEmitter {
content.code = content.code || "m.unknown";
content.reason = content.reason || content.body
|| "Unknown reason";
content.transaction_id = this.transactionId;
this._send("m.key.verification.cancel", content);
} else {
this._send("m.key.verification.cancel", {
code: "m.unknown",
reason: content.body || "Unknown reason",
transaction_id: this.transactionId,
});
}
}
@@ -239,7 +179,6 @@ export default class VerificationBase extends EventEmitter {
this._send("m.key.verification.cancel", {
code: "m.unknown",
reason: e.toString(),
transaction_id: this.transactionId,
});
}
}
@@ -248,10 +187,6 @@ export default class VerificationBase extends EventEmitter {
// but no reject function. If cancel is called again, we'd error.
if (this._reject) this._reject(e);
} else {
// unsubscribe from events, this happens in _reject usually but we don't have one here
if (this._eventsSubscription) {
this._eventsSubscription = this._eventsSubscription();
}
// FIXME: this causes an "Uncaught promise" console message
// if nothing ends up chaining this promise.
this._promise = Promise.reject(e);
@@ -275,23 +210,11 @@ export default class VerificationBase extends EventEmitter {
this._resolve = (...args) => {
this._done = true;
this._endTimer();
if (this.handler) {
// these listeners are attached in Crypto.acceptVerificationDM
if (this._eventsSubscription) {
this._eventsSubscription = this._eventsSubscription();
}
}
resolve(...args);
};
this._reject = (...args) => {
this._done = true;
this._endTimer();
if (this.handler) {
// these listeners are attached in Crypto.acceptVerificationDM
if (this._eventsSubscription) {
this._eventsSubscription = this._eventsSubscription();
}
}
reject(...args);
};
});
@@ -344,8 +267,4 @@ export default class VerificationBase extends EventEmitter {
await this._baseApis.setDeviceVerified(userId, deviceId);
}
}
setEventsSubscription(subscription) {
this._eventsSubscription = subscription;
}
}
+10
View File
@@ -85,3 +85,13 @@ export const newUserMismatchError = errorFactory("m.user_error", "User mismatch"
export const newInvalidMessageError = errorFactory(
"m.invalid_message", "Invalid message",
);
export function errorFromEvent(event) {
const content = event.getContent();
if (content) {
const {code, reason} = content;
return {code, reason};
} else {
return {code: "Unknown error", reason: "m.unknown"};
}
}
+2 -2
View File
@@ -19,11 +19,11 @@ limitations under the License.
* @module crypto/verification/QRCode
*/
import Base from "./Base";
import {VerificationBase as Base} from "./Base";
import {
errorFactory,
newUserCancelledError,
newKeyMismatchError,
newUserCancelledError,
newUserMismatchError,
} from './Error';
+34 -18
View File
@@ -19,14 +19,14 @@ limitations under the License.
* @module crypto/verification/SAS
*/
import Base from "./Base";
import {VerificationBase as Base} from "./Base";
import anotherjson from 'another-json';
import {
errorFactory,
newUserCancelledError,
newUnknownMethodError,
newKeyMismatchError,
newInvalidMessageError,
newKeyMismatchError,
newUnknownMethodError,
newUserCancelledError,
} from './Error';
const EVENTS = [
@@ -108,7 +108,7 @@ const emojiMapping = [
["✏️", "pencil"], // 43
["📎", "paperclip"], // 44
["✂️", "scissors"], // 45
["🔒", "padlock"], // 46
["🔒", "lock"], // 46
["🔑", "key"], // 47
["🔨", "hammer"], // 48
["☎️", "telephone"], // 49
@@ -185,7 +185,11 @@ function intersection(anArray, aSet) {
* @alias module:crypto/verification/SAS
* @extends {module:crypto/verification/Base}
*/
export default class SAS extends Base {
export class SAS extends Base {
static get NAME() {
return "m.sas.v1";
}
get events() {
return EVENTS;
}
@@ -205,7 +209,8 @@ export default class SAS extends Base {
}
async _doSendVerification() {
const initialMessage = this._contentWithTxnId({
const type = "m.key.verification.start";
const initialMessage = this._channel.completeContent(type, {
method: SAS.NAME,
from_device: this._baseApis.deviceId,
key_agreement_protocols: KEY_AGREEMENT_LIST,
@@ -216,8 +221,7 @@ export default class SAS extends Base {
});
// add the transaction id to the message beforehand because
// it needs to be included in the commitment hash later on
this._sendWithTxnId("m.key.verification.start", initialMessage);
this._channel.sendCompleted(type, initialMessage);
let e = await this._waitForEvent("m.key.verification.accept");
let content = e.getContent();
@@ -254,7 +258,7 @@ export default class SAS extends Base {
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this.transactionId;
+ this._channel.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
@@ -270,7 +274,14 @@ export default class SAS extends Base {
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac"),
this._waitForEvent("m.key.verification.mac")
.then((e) => {
// we don't expect any more messages from the other
// party, and they may send a m.key.verification.done
// when they're done on their end
this._expectedEvent = "m.key.verification.done";
return e;
}),
verifySAS,
]);
content = e.getContent();
@@ -283,7 +294,7 @@ export default class SAS extends Base {
async _doRespondVerification() {
// as m.related_to is not included in the encrypted content in e2e rooms,
// we need to make sure it is added
let content = this._contentFromEventWithTxnId(this.startEvent);
let content = this._channel.completedContentFromEvent(this.startEvent);
// Note: we intersect using our pre-made lists, rather than the sets,
// so that the result will be in our order of preference. Then
@@ -331,7 +342,7 @@ export default class SAS extends Base {
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.transactionId;
+ this._channel.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
@@ -347,7 +358,14 @@ export default class SAS extends Base {
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac"),
this._waitForEvent("m.key.verification.mac")
.then((e) => {
// we don't expect any more messages from the other
// party, and they may send a m.key.verification.done
// when they're done on their end
this._expectedEvent = "m.key.verification.done";
return e;
}),
verifySAS,
]);
content = e.getContent();
@@ -363,7 +381,7 @@ export default class SAS extends Base {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this.transactionId;
+ this._channel.transactionId;
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
mac[deviceKeyId] = olmSAS[macMethods[method]](
@@ -393,7 +411,7 @@ export default class SAS extends Base {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.transactionId;
+ this._channel.transactionId;
if (content.keys !== olmSAS[macMethods[method]](
Object.keys(content.mac).sort().join(","),
@@ -412,5 +430,3 @@ export default class SAS extends Base {
});
}
}
SAS.NAME = "m.sas.v1";
@@ -0,0 +1,234 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
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 {REQUEST_TYPE, START_TYPE, VerificationRequest} from "./VerificationRequest";
const MESSAGE_TYPE = "m.room.message";
const M_REFERENCE = "m.reference";
const M_RELATES_TO = "m.relates_to";
/**
* A key verification channel that sends verification events in the timeline of a room.
* Uses the event id of the initial m.key.verification.request event as a transaction id.
*/
export class InRoomChannel {
/**
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
*/
constructor(client, roomId, userId) {
this._client = client;
this._roomId = roomId;
this._userId = userId;
this._requestEventId = null;
}
/** Whether this channel needs m.key.verification.done messages to be sent after a successful verification */
get needsDoneMessage() {
return true;
}
/** The transaction id generated/used by this verification channel */
get transactionId() {
return this._requestEventId;
}
/**
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
static getTimestamp(event) {
return event.getTs();
}
/**
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
* @param {string} type the event type to check
* @returns {bool} boolean flag
*/
static canCreateRequest(type) {
return type === REQUEST_TYPE;
}
/**
* Extract the transaction id used by a given key verification event, if any
* @param {MatrixEvent} event the event
* @returns {string} the transaction id
*/
static getTransactionId(event) {
if (InRoomChannel.getEventType(event) === REQUEST_TYPE) {
return event.getId();
} else {
const relation = event.getRelation();
if (relation && relation.rel_type === M_REFERENCE) {
return relation.event_id;
}
}
}
/**
* Checks whether this event is a well-formed key verification event.
* This only does checks that don't rely on the current state of a potentially already channel
* so we can prevent channels being created by invalid events.
* `handleEvent` can do more checks and choose to ignore invalid events.
* @param {MatrixEvent} event the event to validate
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(event, client) {
const txnId = InRoomChannel.getTransactionId(event);
if (typeof txnId !== "string" || txnId.length === 0) {
return false;
}
const type = InRoomChannel.getEventType(event);
const content = event.getContent();
if (type === REQUEST_TYPE) {
if (typeof content.to !== "string" || !content.to.length) {
return false;
}
const ownUserId = client.getUserId();
// ignore requests that are not direct to or sent by the syncing user
if (event.getSender() !== ownUserId && content.to !== ownUserId) {
return false;
}
}
return VerificationRequest.validateEvent(
type, event, InRoomChannel.getTimestamp(event), client);
}
/**
* As m.key.verification.request events are as m.room.message events with the InRoomChannel
* to have a fallback message in non-supporting clients, we map the real event type
* to the symbolic one to keep things in unison with ToDeviceChannel
* @param {MatrixEvent} event the event to get the type of
* @returns {string} the "symbolic" event type
*/
static getEventType(event) {
const type = event.getType();
if (type === MESSAGE_TYPE) {
const content = event.getContent();
if (content) {
const {msgtype} = content;
if (msgtype === REQUEST_TYPE) {
return REQUEST_TYPE;
}
}
}
return type;
}
/**
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request) {
const type = InRoomChannel.getEventType(event);
// do validations that need state (roomId, userId),
// ignore if invalid
if (event.getRoomId() !== this._roomId || event.getSender() !== this._userId) {
return;
}
// set transactionId when receiving a .request
if (!this._requestEventId && type === REQUEST_TYPE) {
this._requestEventId = event.getId();
}
return await request.handleEvent(type, event, InRoomChannel.getTimestamp(event));
}
/**
* Adds the transaction id (relation) back to a received event
* so it has the same format as returned by `completeContent` before sending.
* The relation can not appear on the event content because of encryption,
* relations are excluded from encryption.
* @param {MatrixEvent} event the received event
* @returns {Object} the content object with the relation added again
*/
completedContentFromEvent(event) {
// ensure m.related_to is included in e2ee rooms
// as the field is excluded from encryption
const content = Object.assign({}, event.getContent());
content[M_RELATES_TO] = event.getRelation();
return content;
}
/**
* Add all the fields to content needed for sending it over this channel.
* This is public so verification methods (SAS uses this) can get the exact
* content that will be sent independent of the used channel,
* as they need to calculate the hash of it.
* @param {string} type the event type
* @param {object} content the (incomplete) content
* @returns {object} the complete content, as it will be sent.
*/
completeContent(type, content) {
content = Object.assign({}, content);
if (type === REQUEST_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
}
if (type === REQUEST_TYPE) {
// type is mapped to m.room.message in the send method
content = {
body: this._client.getUserId() + " is requesting to verify " +
"your key, but your client does not support in-chat key " +
"verification. You will need to use legacy key " +
"verification to verify keys.",
msgtype: REQUEST_TYPE,
to: this._userId,
from_device: content.from_device,
methods: content.methods,
};
} else {
content[M_RELATES_TO] = {
rel_type: M_REFERENCE,
event_id: this.transactionId,
};
}
return content;
}
/**
* Send an event over the channel with the content not having gone through `completeContent`.
* @param {string} type the event type
* @param {object} uncompletedContent the (incomplete) content
* @returns {Promise} the promise of the request
*/
send(type, uncompletedContent) {
const content = this.completeContent(type, uncompletedContent);
return this.sendCompleted(type, content);
}
/**
* Send an event over the channel with the content having gone through `completeContent` already.
* @param {string} type the event type
* @param {object} content
* @returns {Promise} the promise of the request
*/
async sendCompleted(type, content) {
let sendType = type;
if (type === REQUEST_TYPE) {
sendType = MESSAGE_TYPE;
}
const response = await this._client.sendEvent(this._roomId, sendType, content);
if (type === REQUEST_TYPE) {
this._requestEventId = response.event_id;
}
}
}
@@ -0,0 +1,59 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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.
*/
/** a key verification channel that wraps over an actual channel to pass it to a verifier,
* to notify the VerificationRequest when the verifier tries to send anything over the channel.
* This way, the VerificationRequest can update its state based on events sent by the verifier.
* Anything that is not sending is just routing through to the wrapped channel.
*/
export class RequestCallbackChannel {
constructor(request, channel) {
this._request = request;
this._channel = channel;
}
get transactionId() {
return this._channel.transactionId;
}
get needsDoneMessage() {
return this._channel.needsDoneMessage;
}
handleEvent(event, request) {
return this._channel.handleEvent(event, request);
}
completedContentFromEvent(event) {
return this._channel.completedContentFromEvent(event);
}
completeContent(type, content) {
return this._channel.completeContent(type, content);
}
async send(type, uncompletedContent) {
this._request.handleVerifierSend(type, uncompletedContent);
const result = await this._channel.send(type, uncompletedContent);
return result;
}
async sendCompleted(type, content) {
this._request.handleVerifierSend(type, content);
const result = await this._channel.sendCompleted(type, content);
return result;
}
}
@@ -0,0 +1,247 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
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';
import {logger} from '../../../logger';
import {
CANCEL_TYPE,
PHASE_STARTED,
REQUEST_TYPE,
START_TYPE,
VerificationRequest,
} from "./VerificationRequest";
import {errorFromEvent, newUnexpectedMessageError} from "../Error";
/**
* A key verification channel that sends verification events over to_device messages.
* Generates its own transaction ids.
*/
export class ToDeviceChannel {
// userId and devices of user we're about to verify
constructor(client, userId, devices, transactionId = null, deviceId = null) {
this._client = client;
this._userId = userId;
this._devices = devices;
this.transactionId = transactionId;
this._deviceId = deviceId;
}
static getEventType(event) {
return event.getType();
}
/**
* Extract the transaction id used by a given key verification event, if any
* @param {MatrixEvent} event the event
* @returns {string} the transaction id
*/
static getTransactionId(event) {
const content = event.getContent();
return content && content.transaction_id;
}
/**
* Checks whether the given event type should be allowed to initiate a new VerificationRequest over this channel
* @param {string} type the event type to check
* @returns {bool} boolean flag
*/
static canCreateRequest(type) {
return type === REQUEST_TYPE || type === START_TYPE;
}
/**
* Checks whether this event is a well-formed key verification event.
* This only does checks that don't rely on the current state of a potentially already channel
* so we can prevent channels being created by invalid events.
* `handleEvent` can do more checks and choose to ignore invalid events.
* @param {MatrixEvent} event the event to validate
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(event, client) {
if (event.isCancelled()) {
logger.warn("Ignoring flagged verification request from "
+ event.getSender());
return false;
}
const content = event.getContent();
if (!content) {
return false;
}
if (!content.transaction_id) {
return false;
}
const type = event.getType();
if (type === REQUEST_TYPE) {
if (!Number.isFinite(content.timestamp)) {
return false;
}
if (event.getSender() === client.getUserId() &&
content.from_device == client.getDeviceId()
) {
// ignore requests from ourselves, because it doesn't make sense for a
// device to verify itself
return false;
}
}
return VerificationRequest.validateEvent(
type, event, ToDeviceChannel.getTimestamp(event), client);
}
/**
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
static getTimestamp(event) {
const content = event.getContent();
return content && content.timestamp;
}
/**
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request) {
const type = event.getType();
const content = event.getContent();
if (type === REQUEST_TYPE || type === START_TYPE) {
if (!this.transactionId) {
this.transactionId = content.transaction_id;
}
const deviceId = content.from_device;
// adopt deviceId if not set before and valid
if (!this._deviceId && this._devices.includes(deviceId)) {
this._deviceId = deviceId;
}
// if no device id or different from addopted one, cancel with sender
if (!this._deviceId || this._deviceId !== deviceId) {
// also check that message came from the device we sent the request to earlier on
// and do send a cancel message to that device
// (but don't cancel the request for the device we should be talking to)
const cancelContent =
this.completeContent(errorFromEvent(newUnexpectedMessageError()));
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
}
}
const wasStarted = request.phase === PHASE_STARTED;
await request.handleEvent(
event.getType(), event, ToDeviceChannel.getTimestamp(event));
const isStarted = request.phase === PHASE_STARTED;
// the request has picked a start event, tell the other devices about it
if (type === START_TYPE && !wasStarted && isStarted && this._deviceId) {
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
if (nonChosenDevices.length) {
const message = this.completeContent({
code: "m.accepted",
reason: "Verification request accepted by another device",
});
await this._sendToDevices(CANCEL_TYPE, message, nonChosenDevices);
}
}
}
/**
* See {InRoomChannel.completedContentFromEvent} why this is needed.
* @param {MatrixEvent} event the received event
* @returns {Object} the content object
*/
completedContentFromEvent(event) {
return event.getContent();
}
/**
* Add all the fields to content needed for sending it over this channel.
* This is public so verification methods (SAS uses this) can get the exact
* content that will be sent independent of the used channel,
* as they need to calculate the hash of it.
* @param {string} type the event type
* @param {object} content the (incomplete) content
* @returns {object} the complete content, as it will be sent.
*/
completeContent(type, content) {
// make a copy
content = Object.assign({}, content);
if (this.transactionId) {
content.transaction_id = this.transactionId;
}
if (type === REQUEST_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
}
if (type === REQUEST_TYPE) {
content.timestamp = Date.now();
}
return content;
}
/**
* Send an event over the channel with the content not having gone through `completeContent`.
* @param {string} type the event type
* @param {object} uncompletedContent the (incomplete) content
* @returns {Promise} the promise of the request
*/
send(type, uncompletedContent = {}) {
// create transaction id when sending request
if ((type === REQUEST_TYPE || type === START_TYPE) && !this.transactionId) {
this.transactionId = ToDeviceChannel.makeTransactionId();
}
const content = this.completeContent(type, uncompletedContent);
return this.sendCompleted(type, content);
}
/**
* Send an event over the channel with the content having gone through `completeContent` already.
* @param {string} type the event type
* @param {object} content
* @returns {Promise} the promise of the request
*/
sendCompleted(type, content) {
if (type === REQUEST_TYPE) {
return this._sendToDevices(type, content, this._devices);
} else {
return this._sendToDevices(type, content, [this._deviceId]);
}
}
_sendToDevices(type, content, devices) {
if (devices.length) {
const msgMap = {};
for (const deviceId of devices) {
msgMap[deviceId] = content;
}
return this._client.sendToDevice(type, {[this._userId]: msgMap});
} else {
return Promise.resolve();
}
}
/**
* Allow Crypto module to create and know the transaction id before the .start event gets sent.
* @returns {string} the transaction id
*/
static makeTransactionId() {
return randomString(32);
}
}
@@ -0,0 +1,414 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
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 {logger} from '../../../logger';
import {RequestCallbackChannel} from "./RequestCallbackChannel";
import {EventEmitter} from 'events';
import {
errorFactory,
errorFromEvent,
newUnexpectedMessageError,
newUnknownMethodError,
} from "../Error";
// the recommended amount of time before a verification request
// should be (automatically) cancelled without user interaction
// and ignored.
const VERIFICATION_REQUEST_TIMEOUT = 10 * 60 * 1000; //10m
// to avoid almost expired verification notifications
// from showing a notification and almost immediately
// disappearing, also ignore verification requests that
// are this amount of time away from expiring.
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; //3s
export const EVENT_PREFIX = "m.key.verification.";
export const REQUEST_TYPE = EVENT_PREFIX + "request";
export const START_TYPE = EVENT_PREFIX + "start";
export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
export const DONE_TYPE = EVENT_PREFIX + "done";
// export const READY_TYPE = EVENT_PREFIX + "ready";
export const PHASE_UNSENT = 1;
export const PHASE_REQUESTED = 2;
// const PHASE_READY = 3;
export const PHASE_STARTED = 4;
export const PHASE_CANCELLED = 5;
export const PHASE_DONE = 6;
/**
* State machine for verification requests.
* Things that differ based on what channel is used to
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
* @event "change" whenever the state of the request object has changed.
*/
export class VerificationRequest extends EventEmitter {
constructor(channel, verificationMethods, userId, client) {
super();
this.channel = channel;
this._verificationMethods = verificationMethods;
this._client = client;
this._commonMethods = [];
this._setPhase(PHASE_UNSENT, false);
this._requestEvent = null;
this._otherUserId = userId;
this._initiatedByMe = null;
this._startTimestamp = null;
}
/**
* Stateless validation logic not specific to the channel.
* Invoked by the same static method in either channel.
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
* @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead.
* @param {number} timestamp the timestamp in milliseconds when this event was sent.
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(type, event, timestamp, client) {
const content = event.getContent();
if (!type.startsWith(EVENT_PREFIX)) {
return false;
}
if (type === REQUEST_TYPE) {
if (!Array.isArray(content.methods)) {
return false;
}
}
if (type === REQUEST_TYPE || type === START_TYPE) {
if (typeof content.from_device !== "string" ||
content.from_device.length === 0
) {
return false;
}
}
// a timestamp is not provided on all to_device events
if (Number.isFinite(timestamp)) {
const elapsed = Date.now() - timestamp;
// ignore if event is too far in the past or too far in the future
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)) {
logger.log("received verification that is too old or from the future");
return false;
}
}
return true;
}
/** once the phase is PHASE_STARTED, common methods supported by both sides */
get methods() {
return this._commonMethods;
}
/** the timeout of the request, provided for compatibility with previous verification code */
get timeout() {
const elapsed = Date.now() - this._startTimestamp;
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
}
/** the m.key.verification.request event that started this request, provided for compatibility with previous verification code */
get event() {
return this._requestEvent;
}
/** current phase of the request. Some properties might only be defined in a current phase. */
get phase() {
return this._phase;
}
/** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */
get verifier() {
return this._verifier;
}
/** whether this request has sent it's initial event and needs more events to complete */
get pending() {
return this._phase !== PHASE_UNSENT
&& this._phase !== PHASE_DONE
&& this._phase !== PHASE_CANCELLED;
}
/** Whether this request was initiated by the syncing user.
* For InRoomChannel, this is who sent the .request event.
* For ToDeviceChannel, this is who sent the .start event
*/
get initiatedByMe() {
return this._initiatedByMe;
}
/** the id of the user that initiated the request */
get requestingUserId() {
if (this.initiatedByMe) {
return this._client.getUserId();
} else {
return this._otherUserId;
}
}
/** the id of the user that (will) receive(d) the request */
get receivingUserId() {
if (this.initiatedByMe) {
return this._otherUserId;
} else {
return this._client.getUserId();
}
}
/* Start the key verification, creating a verifier and sending a .start event.
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
* @param {string} method the name of the verification method to use.
* @param {string?} targetDevice.userId the id of the user to direct this request to
* @param {string?} targetDevice.deviceId the id of the device to direct this request to
* @returns {VerifierBase} the verifier of the given method
*/
beginKeyVerification(method, targetDevice = null) {
// need to allow also when unsent in case of to_device
if (!this._verifier) {
if (this._hasValidPreStartPhase()) {
// when called on a request that was initiated with .request event
// check the method is supported by both sides
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
throw newUnknownMethodError();
}
this._verifier = this._createVerifier(method, null, targetDevice);
if (!this._verifier) {
throw newUnknownMethodError();
}
}
}
return this._verifier;
}
/**
* sends the initial .request event.
* @returns {Promise} resolves when the event has been sent.
*/
async sendRequest() {
if (this._phase === PHASE_UNSENT) {
this._initiatedByMe = true;
this._setPhase(PHASE_REQUESTED, false);
const methods = [...this._verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, {methods});
this.emit("change");
}
}
/**
* Cancels the request, sending a cancellation to the other party
* @param {string?} error.reason the error reason to send the cancellation with
* @param {string?} error.code the error code to send the cancellation with
* @returns {Promise} resolves when the event has been sent.
*/
async cancel({reason = "User declined", code = "m.user"} = {}) {
if (this._phase !== PHASE_CANCELLED) {
if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason));
} else {
this._setPhase(PHASE_CANCELLED, false);
await this.channel.send(CANCEL_TYPE, {code, reason});
}
this.emit("change");
}
}
/** @returns {Promise} with the verifier once it becomes available. Can be used after calling `sendRequest`. */
waitForVerifier() {
if (this.verifier) {
return Promise.resolve(this.verifier);
} else {
return new Promise(resolve => {
const checkVerifier = () => {
if (this.verifier) {
this.off("change", checkVerifier);
resolve(this.verifier);
}
};
this.on("change", checkVerifier);
});
}
}
_setPhase(phase, notify = true) {
this._phase = phase;
if (notify) {
this.emit("change");
}
}
/**
* Changes the state of the request and verifier in response to a key verification event.
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
* @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead.
* @param {number} timestamp the timestamp in milliseconds when this event was sent.
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(type, event, timestamp) {
const content = event.getContent();
if (type === REQUEST_TYPE || type === START_TYPE) {
if (this._startTimestamp === null) {
this._startTimestamp = timestamp;
}
}
if (type === REQUEST_TYPE) {
await this._handleRequest(content, event);
} else if (type === START_TYPE) {
await this._handleStart(content, event);
}
if (this._verifier) {
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
this._verifier.handleEvent(event);
}
}
if (type === CANCEL_TYPE) {
this._handleCancel();
} else if (type === DONE_TYPE) {
this._handleDone();
}
}
async _handleRequest(content, event) {
if (this._phase === PHASE_UNSENT) {
const otherMethods = content.methods;
this._commonMethods = otherMethods.
filter(m => this._verificationMethods.has(m));
this._requestEvent = event;
this._initiatedByMe = this._wasSentByMe(event);
this._setPhase(PHASE_REQUESTED);
} else if (this._phase !== PHASE_REQUESTED) {
logger.warn("Ignoring flagged verification request from " +
event.getSender());
await this.cancel(errorFromEvent(newUnexpectedMessageError()));
}
}
_hasValidPreStartPhase() {
return this._phase === PHASE_REQUESTED ||
(
this.channel.constructor.canCreateRequest(START_TYPE) &&
this._phase === PHASE_UNSENT
);
}
async _handleStart(content, event) {
if (this._hasValidPreStartPhase()) {
const {method} = content;
if (!this._verificationMethods.has(method)) {
await this.cancel(errorFromEvent(newUnknownMethodError()));
} else {
// if not in requested phase
if (this.phase === PHASE_UNSENT) {
this._initiatedByMe = this._wasSentByMe(event);
}
this._verifier = this._createVerifier(method, event);
this._setPhase(PHASE_STARTED);
}
}
}
/**
* Called by RequestCallbackChannel when the verifier sends an event
* @param {string} type the "symbolic" event type
* @param {object} content the completed or uncompleted content for the event to be sent
*/
handleVerifierSend(type, content) {
if (type === CANCEL_TYPE) {
this._handleCancel();
} else if (type === START_TYPE) {
if (this._phase === PHASE_UNSENT || this._phase === PHASE_REQUESTED) {
// if unsent, we're sending a (first) .start event and hence requesting the verification.
// in any other situation, the request was initiated by the other party.
this._initiatedByMe = this.phase === PHASE_UNSENT;
this._setPhase(PHASE_STARTED);
}
}
}
_handleCancel() {
if (this._phase !== PHASE_CANCELLED) {
this._setPhase(PHASE_CANCELLED);
}
}
_handleDone() {
if (this._phase === PHASE_STARTED) {
this._setPhase(PHASE_DONE);
}
}
_createVerifier(method, startEvent = null, targetDevice = null) {
const startSentByMe = startEvent && this._wasSentByMe(startEvent);
const {userId, deviceId} = this._getVerifierTarget(startEvent, targetDevice);
const VerifierCtor = this._verificationMethods.get(method);
if (!VerifierCtor) {
console.warn("could not find verifier constructor for method", method);
return;
}
// invokes handleVerifierSend when verifier sends something
const callbackMedium = new RequestCallbackChannel(this, this.channel);
return new VerifierCtor(
callbackMedium,
this._client,
userId,
deviceId,
startSentByMe ? null : startEvent,
);
}
_getVerifierTarget(startEvent, targetDevice) {
// targetDevice should be set when creating a verifier for to_device before the .start event has been sent,
// so the userId and deviceId are provided
if (targetDevice) {
return targetDevice;
} else {
let targetEvent;
if (startEvent && !this._wasSentByMe(startEvent)) {
targetEvent = startEvent;
} else if (this._requestEvent && !this._wasSentByMe(this._requestEvent)) {
targetEvent = this._requestEvent;
} else {
throw new Error(
"can't determine who the verifier should be targeted at. " +
"No .request or .start event and no targetDevice");
}
const userId = targetEvent.getSender();
const content = targetEvent.getContent();
const deviceId = content && content.from_device;
return {userId, deviceId};
}
}
// only for .request and .start
_wasSentByMe(event) {
if (event.getSender() !== this._client.getUserId()) {
return false;
}
const content = event.getContent();
if (!content || content.from_device !== this._client.getDeviceId()) {
return false;
}
return true;
}
}
+3 -5
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,7 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter-component
*/
@@ -45,7 +46,7 @@ function _matches_wildcard(actual_value, filter_value) {
* @constructor
* @param {Object} filter_json the definition of this filter JSON, e.g. { 'contains_url': true }
*/
function FilterComponent(filter_json) {
export function FilterComponent(filter_json) {
this.filter_json = filter_json;
this.types = filter_json.types || null;
@@ -141,6 +142,3 @@ FilterComponent.prototype.filter = function(events) {
FilterComponent.prototype.limit = function() {
return this.filter_json.limit !== undefined ? this.filter_json.limit : 10;
};
/** The FilterComponent class */
module.exports = FilterComponent;
+4 -6
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,12 +14,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter
*/
const FilterComponent = require("./filter-component");
import {FilterComponent} from "./filter-component";
/**
* @param {Object} obj
@@ -45,7 +46,7 @@ function setProp(obj, keyNesting, val) {
* @prop {string} userId The user ID of the filter
* @prop {?string} filterId The filter ID
*/
function Filter(userId, filterId) {
export function Filter(userId, filterId) {
this.userId = userId;
this.filterId = filterId;
this.definition = {};
@@ -198,6 +199,3 @@ Filter.fromJson = function(userId, filterId, jsonObj) {
filter.setDefinition(jsonObj);
return filter;
};
/** The Filter class */
module.exports = Filter;
+22 -24
View File
@@ -14,21 +14,20 @@ 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";
/**
* This is an internal module. See {@link MatrixHttpApi} for the public class.
* @module http-api
*/
import Promise from 'bluebird';
const parseContentType = require('content-type').parse;
const utils = require("./utils");
import logger from './logger';
import {parse as parseContentType} from "content-type";
import * as utils from "./utils";
import {logger} from './logger';
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
// waiting for the delay to elapse.
const callbacks = require("./realtime-callbacks");
import * as callbacks from "./realtime-callbacks";
/*
TODO:
@@ -39,27 +38,27 @@ TODO:
/**
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
*/
module.exports.PREFIX_R0 = "/_matrix/client/r0";
export const PREFIX_R0 = "/_matrix/client/r0";
/**
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
*/
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
export const PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for v1 of the the identity API
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
export const PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the v2 identity API
*/
module.exports.PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
export const PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
/**
* URI path for the media repo API
*/
module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
export const PREFIX_MEDIA_R0 = "/_matrix/media/r0";
/**
* Construct a MatrixHttpApi.
@@ -86,16 +85,16 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*/
module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
export function MatrixHttpApi(event_emitter, opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = opts.onlyData || false;
this.event_emitter = event_emitter;
this.opts = opts;
this.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
this.uploads = [];
};
}
module.exports.MatrixHttpApi.prototype = {
MatrixHttpApi.prototype = {
/**
* Sets the baase URL for the identity server
* @param {string} url The new base url
@@ -256,7 +255,7 @@ module.exports.MatrixHttpApi.prototype = {
}
if (global.XMLHttpRequest) {
const defer = Promise.defer();
const defer = utils.defer();
const xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
@@ -418,7 +417,7 @@ module.exports.MatrixHttpApi.prototype = {
opts.headers['Authorization'] = `Bearer ${accessToken}`;
}
const defer = Promise.defer();
const defer = utils.defer();
this.opts.request(
opts,
requestCallback(defer, callback, this.opts.onlyData),
@@ -682,7 +681,7 @@ module.exports.MatrixHttpApi.prototype = {
}
}
const defer = Promise.defer();
const defer = utils.defer();
let timeoutId;
let timedOut = false;
@@ -699,7 +698,7 @@ module.exports.MatrixHttpApi.prototype = {
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
defer.reject(new MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs,
@@ -837,7 +836,7 @@ function parseErrorResponse(response, body) {
if (contentType) {
if (contentType.type === 'application/json') {
const jsonBody = typeof(body) === 'object' ? body : JSON.parse(body);
err = new module.exports.MatrixError(jsonBody);
err = new MatrixError(jsonBody);
} else if (contentType.type === 'text/plain') {
err = new Error(`Server returned ${httpStatus} error: ${body}`);
}
@@ -892,13 +891,12 @@ function getResponseContentType(response) {
* @prop {Object} data The raw Matrix error JSON used to construct this object.
* @prop {integer} httpStatus The numeric HTTP status code given
*/
module.exports.MatrixError = function MatrixError(errorJson) {
export function MatrixError(errorJson) {
errorJson = errorJson || {};
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
this.data = errorJson;
};
module.exports.MatrixError.prototype = Object.create(Error.prototype);
/** */
module.exports.MatrixError.prototype.constructor = module.exports.MatrixError;
}
MatrixError.prototype = Object.create(Error.prototype);
MatrixError.prototype.constructor = MatrixError;
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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 * as matrixcs from "./matrix";
import * as utils from "./utils";
matrixcs.request(import("request"));
utils.runPolyfills();
export * from "./matrix";
export default matrixcs;
-2
View File
@@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
/**
* Check if an IndexedDB database exists. The only way to do so is to try opening it, so
* we do that and then delete it did not exist before.
+2 -1
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,5 +21,5 @@ limitations under the License.
*/
/** The {@link module:indexeddb-store-worker~IndexedDBStoreWorker} class. */
module.exports.IndexedDBStoreWorker = require("./store/indexeddb-store-worker.js");
export {IndexedDBStoreWorker} from "./store/indexeddb-store-worker";
+4 -9
View File
@@ -15,14 +15,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** @module interactive-auth */
import Promise from 'bluebird';
const url = require("url");
const utils = require("./utils");
import logger from './logger';
import url from "url";
import * as utils from "./utils";
import {logger} from './logger';
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
@@ -104,7 +102,7 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* attemptAuth promise.
*
*/
function InteractiveAuth(opts) {
export function InteractiveAuth(opts) {
this._matrixClient = opts.matrixClient;
this._data = opts.authData || {};
this._requestCallback = opts.doRequest;
@@ -511,6 +509,3 @@ InteractiveAuth.prototype = {
},
};
/** */
module.exports = InteractiveAuth;
+8 -6
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018 André Jaenisch
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,7 +18,8 @@ limitations under the License.
/**
* @module logger
*/
const log = require("loglevel");
import log from "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
@@ -25,12 +27,12 @@ const log = require("loglevel");
// 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);
const DEFAULT_NAMESPACE = "matrix";
/**
* 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;
*/
export const logger = log.getLogger(DEFAULT_NAMESPACE);
logger.setLevel(log.levels.DEBUG);
+56 -119
View File
@@ -15,129 +15,66 @@ 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";
/** The {@link module:ContentHelpers} object */
module.exports.ContentHelpers = require("./content-helpers");
/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */
module.exports.MatrixEvent = require("./models/event").MatrixEvent;
/** The {@link module:models/event.EventStatus|EventStatus} enum. */
module.exports.EventStatus = require("./models/event").EventStatus;
/** The {@link module:store/memory.MemoryStore|MemoryStore} class. */
module.exports.MemoryStore = require("./store/memory").MemoryStore;
/**
* The {@link module:store/memory.MemoryStore|MemoryStore} class was previously
* exported as `MatrixInMemoryStore`, so this is preserved for SDK consumers.
* @deprecated Prefer `MemoryStore` going forward.
*/
module.exports.MatrixInMemoryStore = module.exports.MemoryStore;
/** The {@link module:store/indexeddb.IndexedDBStore|IndexedDBStore} class. */
module.exports.IndexedDBStore = require("./store/indexeddb").IndexedDBStore;
/** The {@link module:store/indexeddb.IndexedDBStoreBackend|IndexedDBStoreBackend} class. */
module.exports.IndexedDBStoreBackend = require("./store/indexeddb").IndexedDBStoreBackend;
/** The {@link module:sync-accumulator.SyncAccumulator|SyncAccumulator} class. */
module.exports.SyncAccumulator = require("./sync-accumulator");
/** The {@link module:http-api.MatrixHttpApi|MatrixHttpApi} class. */
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. */
module.exports.Room = require("./models/room");
/** The {@link module:models/group|Group} class. */
module.exports.Group = require("./models/group");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
module.exports.EventTimelineSet = require("./models/event-timeline-set");
/** The {@link module:models/room-member|RoomMember} class. */
module.exports.RoomMember = require("./models/room-member");
/** The {@link module:models/room-state~RoomState|RoomState} class. */
module.exports.RoomState = require("./models/room-state");
/** The {@link module:models/user~User|User} class. */
module.exports.User = require("./models/user");
/** The {@link module:scheduler~MatrixScheduler|MatrixScheduler} class. */
module.exports.MatrixScheduler = require("./scheduler");
/** The {@link module:store/session/webstorage~WebStorageSessionStore|
* WebStorageSessionStore} class. <strong>Work in progress; unstable.</strong> */
module.exports.WebStorageSessionStore = require("./store/session/webstorage");
/** True if crypto libraries are being used on this client. */
module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED;
/** {@link module:content-repo|ContentRepo} utility functions. */
module.exports.ContentRepo = require("./content-repo");
/** The {@link module:filter~Filter|Filter} class. */
module.exports.Filter = require("./filter");
/** The {@link module:timeline-window~TimelineWindow} class. */
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
/** The {@link module:interactive-auth} class. */
module.exports.InteractiveAuth = require("./interactive-auth");
/** The {@link module:auto-discovery|AutoDiscovery} class. */
module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery;
import {MemoryCryptoStore} from "./crypto/store/memory-crypto-store";
import {MemoryStore} from "./store/memory";
import {MatrixScheduler} from "./scheduler";
import {MatrixClient} from "./client";
module.exports.SERVICE_TYPES = require('./service-types').SERVICE_TYPES;
module.exports.MemoryCryptoStore =
require("./crypto/store/memory-crypto-store").default;
module.exports.IndexedDBCryptoStore =
require("./crypto/store/indexeddb-crypto-store").default;
/**
* Create a new Matrix Call.
* @function
* @param {module:client.MatrixClient} client The MatrixClient instance to use.
* @param {string} roomId The room the call is in.
* @return {module:webrtc/call~MatrixCall} The Matrix call or null if the browser
* does not support WebRTC.
*/
module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCall;
/**
* Set a preferred audio output device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioOutput = require('./webrtc/call').setAudioOutput;
/**
* Set a preferred audio input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioInput = require('./webrtc/call').setAudioInput;
/**
* Set a preferred video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallVideoInput = require('./webrtc/call').setVideoInput;
export * from "./client";
export * from "./http-api";
export * from "./autodiscovery";
export * from "./sync-accumulator";
export * from "./errors";
export * from "./models/event";
export * from "./models/room";
export * from "./models/group";
export * from "./models/event-timeline";
export * from "./models/event-timeline-set";
export * from "./models/room-member";
export * from "./models/room-state";
export * from "./models/user";
export * from "./scheduler";
export * from "./filter";
export * from "./timeline-window";
export * from "./interactive-auth";
export * from "./service-types";
export * from "./store/memory";
export * from "./store/indexeddb";
export * from "./store/session/webstorage";
export * from "./crypto/store/memory-crypto-store";
export * from "./crypto/store/indexeddb-crypto-store";
export * from "./content-repo";
export const ContentHelpers = import("./content-helpers");
export {
createNewMatrixCall,
setAudioOutput as setMatrixCallAudioOutput,
setAudioInput as setMatrixCallAudioInput,
setVideoInput as setMatrixCallVideoInput,
} from "./webrtc/call";
// expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request)
let request;
let requestInstance;
/**
* The function used to perform HTTP requests. Only use this if you want to
* use a different HTTP library, e.g. Angular's <code>$http</code>. This should
* be set prior to calling {@link createClient}.
* @param {requestFunction} r The request function to use.
*/
module.exports.request = function(r) {
request = r;
};
export function request(r) {
requestInstance = r;
}
/**
* Return the currently-set request function.
* @return {requestFunction} The current request function.
*/
module.exports.getRequest = function() {
return request;
};
export function getRequest() {
return requestInstance;
}
/**
* Apply wrapping code around the request function. The wrapper function is
@@ -145,15 +82,15 @@ module.exports.getRequest = function() {
* previous value, along with the options and callback arguments.
* @param {requestWrapperFunction} wrapper The wrapping function.
*/
module.exports.wrapRequest = function(wrapper) {
const origRequest = request;
request = function(options, callback) {
export function wrapRequest(wrapper) {
const origRequest = requestInstance;
requestInstance = function(options, callback) {
return wrapper(origRequest, options, callback);
};
};
}
let cryptoStoreFactory = () => new module.exports.MemoryCryptoStore;
let cryptoStoreFactory = () => new MemoryCryptoStore;
/**
* Configure a different factory to be used for creating crypto stores
@@ -161,9 +98,9 @@ let cryptoStoreFactory = () => new module.exports.MemoryCryptoStore;
* @param {Function} fac a function which will return a new
* {@link module:crypto.store.base~CryptoStore}.
*/
module.exports.setCryptoStoreFactory = function(fac) {
export function setCryptoStoreFactory(fac) {
cryptoStoreFactory = fac;
};
}
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
@@ -188,20 +125,20 @@ module.exports.setCryptoStoreFactory = function(fac) {
* @see {@link module:client~MatrixClient} for the full list of options for
* <code>opts</code>.
*/
module.exports.createClient = function(opts) {
export function createClient(opts) {
if (typeof opts === "string") {
opts = {
"baseUrl": opts,
};
}
opts.request = opts.request || request;
opts.store = opts.store || new module.exports.MemoryStore({
opts.request = opts.request || requestInstance;
opts.store = opts.store || new MemoryStore({
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
opts.scheduler = opts.scheduler || new MatrixScheduler();
opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory();
return new module.exports.MatrixClient(opts);
};
return new MatrixClient(opts);
}
/**
* The request function interface for performing HTTP requests. This matches the
+2 -6
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,7 +14,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/event-context
@@ -33,7 +33,7 @@ limitations under the License.
*
* @constructor
*/
function EventContext(ourEvent) {
export function EventContext(ourEvent) {
this._timeline = [ourEvent];
this._ourEventIndex = 0;
this._paginateTokens = {b: null, f: null};
@@ -113,7 +113,3 @@ EventContext.prototype.addEvents = function(events, atStart) {
}
};
/**
* The EventContext class
*/
module.exports = EventContext;
+9 -12
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,16 +14,17 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/event-timeline-set
*/
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const EventTimeline = require("./event-timeline");
import {EventEmitter} from "events";
import {EventTimeline} from "./event-timeline";
import {EventStatus} from "./event";
import logger from '../logger';
import Relations from './relations';
import * as utils from "../utils";
import {logger} from '../logger';
import {Relations} from './relations';
// var DEBUG = false;
const DEBUG = true;
@@ -71,7 +73,7 @@ if (DEBUG) {
* via `getRelationsForEvent`.
* This feature is currently unstable and the API may change without notice.
*/
function EventTimelineSet(room, opts) {
export function EventTimelineSet(room, opts) {
this.room = room;
this._timelineSupport = Boolean(opts.timelineSupport);
@@ -815,11 +817,6 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
}
};
/**
* The EventTimelineSet class.
*/
module.exports = EventTimelineSet;
/**
* Fires whenever the timeline in a room is updated.
* @event module:client~MatrixClient#"Room.timeline"

Some files were not shown because too many files have changed in this diff Show More