Compare commits

..

218 Commits

Author SHA1 Message Date
RiotRobot 8ee99a0616 v5.0.0-rc.1 2020-02-13 15:41:46 +00:00
RiotRobot 3ace1d04cd Prepare changelog for v5.0.0-rc.1 2020-02-13 15:41:45 +00:00
Bruno Windels 6684574bdf Merge pull request #1206 from matrix-org/bwindels/dontpassmethodstoverify
Remove methods argument to verification
2020-02-13 08:51:27 +00:00
Hubert Chathi 36a945f8e2 Merge pull request #1207 from matrix-org/uhoreg/fix_opts_request
don't do a dynamic import of request
2020-02-11 13:54:12 -05:00
Hubert Chathi 6a3d322033 don't do a dynamic import of request 2020-02-11 13:02:34 -05:00
Bruno Windels 00c003ec65 remove methods arg to requestVerification(DM)
as it's easy to have this argument be out of sync from all
the places this is called from the js-sdk. There is also little point,
as you can already specify the methods a consumer of the js-sdk
wants to provide through the verificationMethods option when creating
the client object.
2020-02-11 17:42:49 +01:00
Bruno Windels f4d335c161 use default methods if none are provided to the client 2020-02-11 17:42:17 +01:00
Bruno Windels 659f42139b Merge pull request #1201 from matrix-org/travis/wip/qr
QR self-verification fixes
2020-02-11 15:17:02 +00:00
Bruno Windels 0e791ed022 Merge pull request #1204 from matrix-org/bwindels/logverif
Log every verification event
2020-02-11 13:17:23 +00:00
Bruno Windels 48655aa1a3 log every verification event 2020-02-11 10:08:17 +01:00
Bruno Windels 83fa80cfda Merge pull request #1203 from matrix-org/bwindels/dontrequiredoneconfirmation
dont require .done event from other party
2020-02-11 08:18:46 +00:00
Bruno Windels cf5b5ee085 dont require .done event from other party 2020-02-10 18:00:24 +01:00
Bruno Windels 429a4e3526 fix lint 2020-02-10 17:21:22 +01:00
Zoe d66d4c1cd9 Merge pull request #1202 from matrix-org/foldleft/12221-reset-cross-signing
New option to fully reset Secret Storage keys in boostrapSecretStorage
2020-02-10 09:59:28 +00:00
Zoe 7a1bbdf2dd oops 2020-02-07 15:51:27 +00:00
Travis Ralston 29c1459568 Merge pull request #1190 from matrix-org/travis/qr-code-request-based
Add function to estimate target device for a VerificationRequest
2020-02-07 15:37:49 +00:00
Travis Ralston efad46a8a4 Rename target device prop 2020-02-07 15:37:34 +00:00
Zoe a69c621305 New option to fully reset Secret Storage keys in boostrapSecretStorage 2020-02-07 14:45:10 +00:00
Bruno Windels ad6dde6f26 Merge pull request #1200 from matrix-org/bwindels/4sunlockpurpose
pass ssss item name to callback so we can differentiate UI on it
2020-02-07 08:58:43 +00:00
Bruno Windels 2627e46723 add jsdoc for new param 2020-02-06 18:43:46 +01:00
Bruno Windels 408d70b55e pass ssss item name to callback so we can differentiate UI on it 2020-02-06 16:54:12 +01:00
Hubert Chathi 3f369e528b Merge pull request #1167 from cedricvanrompay/1-olm-device-export-import
add export/import of Olm devices
2020-02-05 20:09:01 -05:00
Zoe 312976294b Merge pull request #1199 from matrix-org/foldleft/types-for-utils
Convert utils.js -> utils.ts
2020-02-05 12:41:17 +00:00
Zoe 77f42c479b Update src/utils.ts
Co-Authored-By: Travis Ralston <travpc@gmail.com>
2020-02-05 11:50:39 +00:00
Zoe d60bd22674 actually let's not get into the business of writing types for our deps 2020-02-05 11:43:11 +00:00
Zoe 2e67f77d3e compiler flags 2020-02-05 11:17:55 +00:00
Zoe 6d8e8e6bd7 fix tests 2020-02-05 11:07:55 +00:00
Zoe 9c01945a05 copyright notice *sigh* 2020-02-05 10:23:24 +00:00
Zoe 7ce5ddd380 lint 2020-02-05 10:14:26 +00:00
Zoe 2b5de914f5 review feedback 2020-02-05 09:57:46 +00:00
Zoe 18a2426707 Convert utils.js -> utils.ts 2020-02-04 19:09:48 +00:00
David Baker 367fac6d54 Merge pull request #1197 from matrix-org/dbkr/stop_signing_yourself
Don't sign ourselves as a user
2020-02-04 14:31:08 +00:00
David Baker 157cc9e5eb Merge remote-tracking branch 'origin/develop' into dbkr/stop_signing_yourself 2020-02-04 14:26:58 +00:00
David Baker 81daf12598 Merge pull request #1196 from matrix-org/dbkr/verfication_logging
Add a bunch of logging to verification
2020-02-04 14:21:08 +00:00
Bruno Windels 9249b0652f Merge pull request #1198 from matrix-org/bwindels/fixverifroomeventtype
Fix: always return a valid string from InRoomChannel.getEventType
2020-02-04 14:16:14 +00:00
Bruno Windels ee4c6b6265 Merge pull request #1195 from matrix-org/bwindels/logoncancel
add logging when a request is being cancelled
2020-02-04 13:08:13 +00:00
David Baker 68deab4a68 We still need to mark our master key locally verified 2020-02-04 12:27:53 +00:00
Bruno Windels c9c765b5b8 fix getEventType 2020-02-04 13:12:38 +01:00
David Baker 616f73d8c6 forgive me, o great linter 2020-02-04 12:12:02 +00:00
Bruno Windels 208c371afb add failing test for getEventType 2020-02-04 13:10:06 +01:00
David Baker 3a59cfa9c0 Don't sign ourselves as a user 2020-02-04 12:09:42 +00:00
David Baker cf94527bd5 Add a bunch of logging to verification
So we have a better idea of what's going on
2020-02-04 12:04:50 +00:00
Travis Ralston fa93479863 Merge pull request #1194 from matrix-org/travis/fix-type
Don't explode verification validation if we don't have an event type
2020-02-04 11:48:25 +00:00
Bruno Windels 8bc0ef8c27 add logging when a request is being cancelled
so we can more easily see (especially for to_device requests)
why something was cancelled
2020-02-04 12:48:02 +01:00
Travis Ralston bd403b6d87 Don't explode verification validation if we don't have an event type
I don't know why this is undefined at this point, or why membership events are ending up here, but this fixes develop for people.

See https://github.com/vector-im/riot-web/issues/12231
2020-02-04 11:46:31 +00:00
Bruno Windels 57a7328065 Merge pull request #1193 from matrix-org/bwindels/dontshowverifrequestnotforme
Fix: verification request appearing for users that are not the receiver or sender if they are in room
2020-02-04 09:39:45 +00:00
Bruno Windels 4945463beb fix lint 2020-02-03 20:12:21 +01:00
Bruno Windels dfafa791f2 fix getOtherPartyUserId 2020-02-03 19:17:40 +01:00
Bruno Windels 5f2cb6b3a4 only an m.room.message with msgtype can be a .request 2020-02-03 19:17:18 +01:00
Bruno Windels 5398fac348 add (failing) tests for getEventType and getOtherPartyUserId 2020-02-03 19:16:48 +01:00
Cédric Van Rompay b217f6aa81 minor doc update (with sign-off)
Signed-off-by: Cédric Van Rompay <cedric.vanrompay@gmail.com>
2020-02-03 10:32:32 +01:00
Cédric Van Rompay ec597bea93 fix new way of calling OlmDevice.init 2020-02-03 10:27:10 +01:00
Cédric Van Rompay 7a5c54fef7 set pickle key through OlmDevice.init 2020-02-03 09:58:18 +01:00
David Baker 4064f18de2 Merge pull request #1192 from matrix-org/dbkr/fix_passthrough_key_get
Fix getting secrets encoded with passthrough keys
2020-02-02 19:12:39 +00:00
David Baker 6d13457172 Fix getting secrets encoded with passthrough keys 2020-02-01 17:29:08 +00:00
Travis Ralston f39518ef93 Unreviewed crypto verification for self 2020-02-01 10:49:32 +00:00
Bruno Windels 4b1cecd246 also set the deviceId on .ready so we know who to send .start to 2020-01-31 14:50:48 +01:00
Cédric Van Rompay 352509fd3a Update src/crypto/OlmDevice.js
Co-Authored-By: Hubert Chathi <hubert@uhoreg.ca>
2020-01-31 11:53:20 +01:00
Cédric Van Rompay d0f08f8839 Update src/crypto/OlmDevice.js
Co-Authored-By: Hubert Chathi <hubert@uhoreg.ca>
2020-01-31 11:53:09 +01:00
Cédric Van Rompay efd38a3471 Update src/crypto/OlmDevice.js
Co-Authored-By: Hubert Chathi <hubert@uhoreg.ca>
2020-01-31 11:52:54 +01:00
Cédric Van Rompay a4e74fea94 fix linting errors 2020-01-31 11:51:17 +01:00
Travis Ralston fdb33b6189 Merge remote-tracking branch 'origin/bwindels/todevicereadystartdone' into travis/wip 2020-01-30 18:06:31 +00:00
Bruno Windels dcbb67838b for the right panel to work, the verifier should send .done events 2020-01-30 18:46:22 +01:00
Bruno Windels 1727d636a3 don't assume both parties have a different userId in verif ping-pong 2020-01-30 18:45:54 +01:00
Travis Ralston 9eadc7f868 Add function to estimate target device for a VerificationRequest
For https://github.com/matrix-org/matrix-react-sdk/pull/4001
2020-01-30 16:57:01 +00:00
Travis Ralston 620118af5f Merge pull request #1175 from matrix-org/travis/update-qr-code
Update QR code handling for new spec
2020-01-30 11:25:57 +00:00
Travis Ralston 3645764f9a Appease the linter 2020-01-30 11:15:25 +00:00
Travis Ralston 769bfeb10f Verify all the things 2020-01-30 11:10:25 +00:00
Travis Ralston 5fbaa9cfa7 Fix verification of the master key 2020-01-29 18:06:25 +00:00
Travis Ralston 007508ba12 Merge branch 'develop' into travis/update-qr-code 2020-01-29 16:57:57 +00:00
David Baker 0f1f18b232 Merge pull request #1188 from matrix-org/dbkr/dont_add_epemeral_events_to_timeline_when_peeking
Don't add ephemeral events to timeline when peeking
2020-01-29 15:17:27 +00:00
David Baker d6b754b133 Merge pull request #1189 from matrix-org/dbkr/be_prepaed
Fix typo
2020-01-29 15:16:56 +00:00
Travis Ralston 1b80c83676 Merge branch 'develop' into travis/update-qr-code 2020-01-29 15:11:06 +00:00
Travis Ralston ec4dc582b6 Remove tests for old QR code stuff 2020-01-29 15:10:35 +00:00
David Baker 65646ff9e2 Fix typo
This would probably just cause apps to wait until the first live
sync had finished rather than the one from the store, so slowing
them down / breaking offline support.
2020-01-29 15:06:19 +00:00
Travis Ralston 92f6ec918b Appease the linter 2020-01-29 15:06:13 +00:00
David Baker 62bd41d2e6 Don't add ephemeral events to timeline when peeking
As hopefully explained by comment.

Fixes https://github.com/vector-im/riot-web/issues/11120
2020-01-29 15:04:09 +00:00
Bruno Windels 9d864ffd60 Merge pull request #1187 from matrix-org/bwindels/fixstartrace-rebased
Verification: resolve race between .start events from both parties
2020-01-29 15:04:00 +00:00
Travis Ralston c45b38cece Actually do the verification 2020-01-29 14:56:28 +00:00
Travis Ralston 0d7aee2c36 Misc cleanup 2020-01-29 14:52:04 +00:00
Travis Ralston be345a523f Fix verification flow 2020-01-29 14:43:37 +00:00
Bruno Windels 470bdf8741 fix tests 2020-01-29 15:19:18 +01:00
Bruno Windels 59319fb55b use logger instead of console 2020-01-29 15:19:07 +01:00
Bruno Windels fb7695fdbc fix unrelated issue: errorFactory returns function, so call it 2020-01-29 15:18:48 +01:00
Bruno Windels 25b7552683 startEvent can always be passed to verifier
as we'll check the sender there to see on which side we are
2020-01-29 15:18:18 +01:00
Bruno Windels 21d520378f apply same algo to pick .start event initially when changing phase
smallest sender userid wins
2020-01-29 15:17:36 +01:00
Bruno Windels 9cd6607520 attempt to switch start event if we already have a verifier 2020-01-29 15:16:54 +01:00
Bruno Windels efd3550f53 support switching startEvent while waiting for .accept on initiator side
if we get a .start event from the other party and we've also sent one,
the .start event with the sender that is first in sorting order should
be taken, and the other one ignored.

At the point where we will receive it, the verifier has already
been returned from beginKeyVerification, so we'll need to switch
start event internally, and retry the verification, now on the
receiver (sending .accept) side instead of initiator side
(sending .start).
2020-01-29 15:13:59 +01:00
Travis Ralston 76402ec8d7 Lie to the verification handling 2020-01-29 13:45:02 +00:00
Travis Ralston f689142806 Define NAME as a property higher up 2020-01-29 10:52:26 +00:00
Travis Ralston fd563bda6a Remove irrelevant verification flows for QR codes
You can't actually get at these through our verification framework - they scan/show steps are pre-verification framework.
2020-01-29 09:26:29 +00:00
Travis Ralston 09a8f7122c Merge branch 'develop' into travis/update-qr-code 2020-01-29 00:18:15 +00:00
David Baker 608fb00844 Merge pull request #1184 from matrix-org/dbkr/new_keybackup_in_bootstrap
Add option to bootstrap to start new key backup
2020-01-28 22:02:10 +00:00
David Baker 5c45e9c306 Add option to bootstrap to start new key backup
The key backup needs to be signed by the cross-signing key so
doing it here allows us to do it before we blow the private part
out of memory.
2020-01-28 19:36:00 +00:00
Travis Ralston 950221dc13 Merge branch 'develop' into travis/update-qr-code 2020-01-28 17:27:38 +00:00
Travis Ralston f816679596 Merge pull request #1182 from matrix-org/travis/null-guards
Add a bunch of null guards to feature checks
2020-01-28 14:24:53 +00:00
Travis Ralston 80ccf18b16 Merge pull request #1183 from salzig/docs/fix_matrix_client_reference
docs: fix MatrixClient reference
2020-01-28 14:13:59 +00:00
Ben Rexin c7abd9062a docs: fix MatrixClient reference 2020-01-28 15:05:13 +01:00
Travis Ralston 4287f2229b Add a bunch of null guards to feature checks 2020-01-28 13:21:01 +00:00
Michael Telatynski 8408055137 Merge pull request #1180 from matrix-org/t3chguy/cs_verification_decoration
Add helper to obtain the cancellation code for a verification request
2020-01-28 11:23:24 +00:00
Michael Telatynski cc0965d703 s/^t/T/ 2020-01-28 11:19:05 +00:00
Michael Telatynski 94b3d9d3e1 Add helper to obtain the cancellation code for a verification request 2020-01-28 11:15:07 +00:00
J. Ryan Stinnett 772bf7d6ff Merge pull request #1178 from matrix-org/jryans/tag-prerelease-next
Publish pre-releases as a separate tag on npm
2020-01-27 22:32:56 +00:00
J. Ryan Stinnett 15c2e4bb07 Publish pre-releases as a separate tag on npm
npm will install the newest version a package has published to the `latest` tag,
including pre-releases, which is not ideal since those may not be ready for
production use yet.

This uses an alternate tag (`next` is a common convention, but it can be
anything) for pre-releases so the default installs only get stable versions.

Fixes https://github.com/vector-im/riot-web/issues/12029
2020-01-27 20:40:35 +00:00
Travis Ralston 419693023f Add untested reciprocate function 2020-01-27 11:41:52 -07:00
Travis Ralston 2d081f2c19 Merge branch 'develop' into travis/update-qr-code 2020-01-27 11:41:05 -07:00
David Baker c76ce1fd85 Merge pull request #1177 from matrix-org/dbkr/fix_passthrough_keys
Fix support for passthrough keys
2020-01-27 16:55:27 +00:00
David Baker f38b4d37e6 Check for the whole thing being null 2020-01-27 16:25:08 +00:00
David Baker 73c92dfc57 Merge pull request #1174 from matrix-org/dbkr/trust_cross_signing_on_verify
Trust our own cross-signing keys if we verify them with another device
2020-01-27 16:11:08 +00:00
David Baker 61c5430deb Fix support for passthrough keys
and add code to fix up ones mis-stored by the old code
2020-01-27 15:50:01 +00:00
J. Ryan Stinnett 21e4c597d9 Merge pull request #1176 from matrix-org/jryans/await-device-list
Ensure cross-signing keys are downloaded when checking trust
2020-01-27 15:34:19 +00:00
J. Ryan Stinnett 4dbeee8cb3 Ignore downloading for tests 2020-01-27 15:28:36 +00:00
J. Ryan Stinnett adc76c636e Merge pull request #1172 from matrix-org/bwindels/reduceveriflogging
Don't log verification validation errors for normal messages
2020-01-27 15:04:00 +00:00
J. Ryan Stinnett 0dbf89b2b4 Ensure cross-signing keys are downloaded when checking trust
When checking cross-signing trust during login, we may not have downloaded keys
yet. This ensures we make an attempt first if needed.

Fixes https://github.com/vector-im/riot-web/issues/12068
2020-01-27 14:55:20 +00:00
Travis Ralston 83241ac17d Update QR code handling for new URL
This doesn't have any meaningful change on the process, just makes it more in line with what we do.
2020-01-27 06:59:04 -07:00
Cédric Van Rompay 6aa5d39357 move new example to own directory 2020-01-27 14:28:50 +01:00
Cédric Van Rompay 1304ecbe03 factor out _initializeFromExportedDevice 2020-01-27 14:12:43 +01:00
RiotRobot aafc027812 Merge branch 'master' into develop 2020-01-27 11:31:09 +00:00
Cédric Van Rompay 3a4b6f0ea0 rename "kwargs" to "opts" 2020-01-27 11:48:28 +01:00
Cédric Van Rompay b3d10ace21 mention export method in import 2020-01-27 11:45:17 +01:00
Cédric Van Rompay c17df7a6f7 fix typo in comments 2020-01-27 11:42:15 +01:00
David Baker 1c13f5026e Merge pull request #1173 from matrix-org/dbkr/fix_bootstrap_cleanup
Fix bootstrap cleanup
2020-01-27 10:18:37 +00:00
David Baker b9cfede888 Trust our own cross-signing keys if we verify them with another device 2020-01-25 20:38:11 +00:00
David Baker 49fd9e90a0 this can be const now 2020-01-25 19:48:36 +00:00
David Baker e09038232e Fix bootstrap cleanup
As hopefully explained in the comment. The symptom of this was that
bootstrapping would work just fine the first time you called it
in any run of the app, but then if called a second time (eg. if you
cancelled by dismissing the password prompt) it would create keys and
upload the public parts but not store the private parts in SSSS,
leaving you with cross signing keys you don't have the private parts
of.

Also use object.assign in the save keys callback just in case we
ever reset a subset of the keys (and also because it makes it a
bit simpler to reason about what objects are where).
2020-01-25 19:42:02 +00:00
Travis Ralston 2cfe310e89 Merge pull request #1155 from matrix-org/travis/qr-verif-rp
QR code verification
2020-01-24 08:55:42 -07:00
Bruno Windels 973c7467e8 Merge pull request #1171 from matrix-org/bwindels/fixverifyowndevice
expose deviceId prop on device channel
2020-01-24 11:24:23 +00:00
Bruno Windels 583df7ed7d don't log verification validation errors for normal messages 2020-01-24 12:23:18 +01:00
Bruno Windels 6d05376f04 expose deviceId prop on device channel
used to check if a verification came through to_device in the toast
2020-01-24 12:01:20 +01:00
Cédric Van Rompay e1f832bfa7 fix linting errors 2020-01-24 09:20:43 +01:00
Travis Ralston b8092cd00b Make the tests pass 2020-01-23 20:41:52 -07:00
Travis Ralston 3c1dca6cef Generate a shared secret if we don't have one 2020-01-23 20:15:02 -07:00
Travis Ralston c0f7dd6fe9 Fix secret size 2020-01-23 20:06:04 -07:00
Travis Ralston 6af6e99480 Expose the request event more readily for consumers 2020-01-23 20:05:56 -07:00
Travis Ralston c5cbe48668 Remove docs too 2020-01-23 19:29:42 -07:00
Travis Ralston 15707956ef Remove private key accessors for cross-signing 2020-01-23 19:29:42 -07:00
Travis Ralston 4668fc87a1 Add cross-signing accessors and QR code stuff 2020-01-23 19:29:42 -07:00
Jack Works 468fb2cc41 chore: remove custom promise, use es6 standard
Signed-off-by: Jack Works <jackworks@protonmail.com>
2020-01-23 19:23:08 -07:00
Jack Works 7c79e7e836 fix: typos
Signed-off-by: Jack Works <jackworks@protonmail.com>
2020-01-23 19:21:19 -07:00
Travis Ralston 0bf1f48623 Merge pull request #1169 from matrix-org/travis/fix-build
Move & upgrade babel runtime into dependencies (like it wants)
2020-01-23 15:45:53 -07:00
Travis Ralston f286eb4d11 Move & upgrade babel runtime into dependencies (like it wants)
https://babeljs.io/docs/en/babel-runtime
2020-01-23 15:44:36 -07:00
Cédric Van Rompay 9346c83dc1 fix destructuration of potentially nil value 2020-01-23 18:53:42 +01:00
Bruno Windels a76267f5b0 Merge pull request #1166 from matrix-org/bwindels/verifyowndevicechecks
Add unit tests for verifying your own device, remove .event property on verification request
2020-01-23 17:01:43 +00:00
Cédric Van Rompay 1d3a7b3d52 add example for export/import in browser 2020-01-23 16:55:16 +01:00
Cédric Van Rompay f78f04d553 userId must be included in exported data 2020-01-23 16:55:16 +01:00
Cédric Van Rompay 7b6dabbe9c add high-level export/import methods
not sure how to test these high-level methods though
2020-01-23 16:55:16 +01:00
Cédric Van Rompay ed01b3b8cf stop checking structure of exported data
it should suffice that the exported data
allows to recreate a device that can do crypto
2020-01-23 16:55:16 +01:00
Cédric Van Rompay 7880a30e57 add importing in OlmDevice.init() 2020-01-23 16:55:16 +01:00
Cédric Van Rompay 3a3ff93450 improve export doc 2020-01-23 16:55:16 +01:00
Cédric Van Rompay 3a1cdd37a3 move export test with other Olm tests to have active sessions 2020-01-23 16:55:16 +01:00
Cédric Van Rompay 8db38f8e75 fix output of getAllEndToEndSessions 2020-01-23 16:55:16 +01:00
Cédric Van Rompay ff24ef4ee5 add OlmDevice.prototype.export
- only exporting account and P2P sessions
- test is halfway done:
  - it only prints the export result instead of running assertions on it
  - there are no sessions to export

Note: to run only the added test:

    node_modules/.bin/jest spec/unit/crypto/algorithms/olm.spec.js --testEnvironment node --testNamePattern OlmDevice
2020-01-23 16:55:16 +01:00
Bruno Windels 3faeec4add fix lint 2020-01-23 15:59:47 +01:00
Bruno Windels 7d56ee5084 with the change in the linked react-sdk PR, event isn't used anymore 2020-01-23 15:52:23 +01:00
Bruno Windels b2afaabb8c add unit tests for verifying your own device over to_device messages 2020-01-23 15:52:07 +01:00
Bruno Windels 3efaf90bc8 Merge pull request #1163 from matrix-org/bwindels/verificationaccceptedbyotherdevice
For dm-verification, also consider events sent by other devices of same user as "our" events
2020-01-23 13:27:15 +00:00
Bruno Windels 0c52887688 copyright year
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-01-23 10:51:19 +00:00
David Baker d5e9155a33 Merge pull request #1161 from matrix-org/dbkr/prepublish
Add a prepare script
2020-01-22 20:56:59 +00:00
Michael Telatynski 5def5ab074 Merge pull request #1162 from matrix-org/t3chguy/crypto/keys/upload/deviceId
Remove :deviceId from /keys/upload/:deviceId as not spec-compliant
2020-01-22 17:13:21 +00:00
Bruno Windels 1b242e636b remove obsolete comment 2020-01-22 17:39:21 +01:00
Bruno Windels 05f05c889a don't verify in observeOnly mode 2020-01-22 17:39:21 +01:00
Bruno Windels 1367e285c8 have channel decide what is considered "sent by us"
for in room verification, if another client accepts the request,
we still want to observe so those events should still be
considered ours, so looking at from_device doesn't work there.
2020-01-22 17:39:21 +01:00
Bruno Windels 45ec3e0bb9 also emit if the phase didn't change but observeOnly did 2020-01-22 17:39:21 +01:00
Bruno Windels dc38f78da2 add unit tests for verification request 2020-01-22 17:39:21 +01:00
Michael Telatynski 1b6a74fd93 Remove :deviceId from /keys/upload/:deviceId as not spec-compliant 2020-01-22 15:20:13 +00:00
David Baker 9d8a1494aa Turns out prepublish is deprecated and should be prepare 2020-01-22 14:14:24 +00:00
David Baker 08465cf236 Add a prepublish script
So we actually build the lib directory before publishing it
2020-01-22 14:09:01 +00:00
Damir Jelić 7016848401 Merge branch 'poljar/timeline-window-refactor' into develop 2020-01-21 17:01:16 +01:00
poljar bdd2a9e7e8 timeline-window: Small docfix.
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-01-21 16:55:12 +01:00
David Baker 80256e6782 Merge pull request #1158 from matrix-org/dbkr/upload_device_keys_empty_auth
Allow a device key upload request without auth
2020-01-21 15:03:58 +00:00
Damir Jelić 7907ef44f8 timeline-window: Refactor out and expose the logic to extend the window. 2020-01-21 15:42:55 +01:00
Damir Jelić 3a97a24686 timeline-window: Refactor out the TimelineIndex getting logic. 2020-01-21 15:21:10 +01:00
David Baker 7f208ed44e Allow a device key upload request without auth
This is useful for querying the supported auth methods.
2020-01-21 11:35:49 +00:00
Bruno Windels 22e6cfaebb Merge pull request #1140 from matrix-org/bwindels/verification-right-panel
Support for .ready verification event (MSC2366) & other things
2020-01-20 17:17:51 +00:00
Bruno Windels 9d6f873048 remove obsolete and now broken method
a request should be accepted by calling accept() on the request.
2020-01-20 18:13:18 +01:00
Bruno Windels d526229a0f update jsdoc of requestVerificationDM
which now returns a Promise of VerificationRequest instead of verifier
2020-01-20 18:12:52 +01:00
Bruno Windels aac68290ac remove obsolete comment 2020-01-20 17:56:28 +01:00
Bruno Windels bd9a2c13eb implement API change in sas test for requestVerificationDM 2020-01-20 17:55:48 +01:00
Bruno Windels e5c65d53f8 set transaction_id for remote echos in TestClient
as InRoomChannel looks at this to decide whether an event is
a remote echo (and to pass it to the verifier or not)
2020-01-20 17:54:26 +01:00
Bruno Windels 121e9d0225 don't overwrite a request when the remote echo arrives before event_id 2020-01-20 17:39:18 +01:00
Bruno Windels c12a3b6610 more fixup: make sure remote echo doesn't arrive earlier for TestClient 2020-01-20 17:35:44 +01:00
Bruno Windels 77d0a76186 fixup: another timeout 2020-01-20 14:52:34 +01:00
Bruno Windels e89528315d enable fake timers for consistency
although it doesn't make or break the test
2020-01-20 14:04:32 +01:00
Bruno Windels c34ccc9d53 adjust test: requestVerification returns the request instead of verifier 2020-01-20 14:03:43 +01:00
Bruno Windels e51ba795f3 to make this work while using fake timers, don't use setTimeout
but instead use Promise.resolved() as then always runs in the next tick.
2020-01-20 13:56:39 +01:00
Bruno Windels cbe2965849 mention reason in cancellation error 2020-01-17 19:01:30 +01:00
Bruno Windels 59bfc45856 use setTimeout of setInterval 2020-01-17 19:01:08 +01:00
Bruno Windels 07cc93cca2 fix lint 2020-01-17 16:58:19 +01:00
Travis Ralston 1205178e26 Merge branch 'develop' into bwindels/verification-right-panel 2020-01-16 13:13:00 -07:00
Bruno Windels 72fd1e4e7c add note to fix bug later 2020-01-03 18:21:33 +01:00
Bruno Windels f44e0a8e12 parenthesis in wrong place broke logic 2020-01-03 18:21:18 +01:00
Bruno Windels 9338d9c2a6 commit logging 2020-01-03 18:20:59 +01:00
Bruno Windels 75fc25feb5 fix method names 2020-01-03 18:20:50 +01:00
Bruno Windels 5919874f6f check !unsent instead of requested for emitting the crypto.request event 2020-01-03 18:20:16 +01:00
Bruno Windels 213bb9dba2 allow to move straight from UNSENT to STARTED
this was one of the things breaking to_device verification
2020-01-03 18:19:49 +01:00
Bruno Windels 3a9dc37d02 new state machine relies on having remote echos, so fake for to_device 2020-01-03 18:18:39 +01:00
Bruno Windels 423c8a886d use isRemoteEcho to determine if the event is theirs or not
rather than the sender and from_device (which is not always set)

as this was one of the things breaking to_device verification
of ones own devices.
2020-01-03 18:16:25 +01:00
Bruno Windels 3ec8233a2d fixes & implement timeout 2020-01-03 13:42:06 +01:00
Bruno Windels 8ed51c806e don't cancel or timeout when verify isn't called 2020-01-03 13:42:06 +01:00
Bruno Windels 57135a898f don't mark events loaded from cache as live events
this makes the verifier want to interact with the other party
when just reloading the session.
2020-01-03 13:42:06 +01:00
Bruno Windels 0d3d27a519 fixes and cleanup for historical 2020-01-03 13:42:06 +01:00
Bruno Windels cf42ad83da WIP historical 2020-01-03 13:42:06 +01:00
Bruno Windels e7bcb61a3b attempt at only creating verifier for live events
but doesn't work yet? data where liveEvent is fished out is undefined
2020-01-03 13:42:06 +01:00
Bruno Windels 883b83f1da move blocking non-participating users back to InRoomChannel
as it doesn't need to happen for ToDeviceChannel
2020-01-03 13:42:06 +01:00
Bruno Windels 48977e6eaa get other party user id by inspecting initial event sender/to fields
also fail validation with any event not sent by or directed to us
2020-01-03 13:42:06 +01:00
Bruno Windels efe2488155 get other user id from channel
next up is inspecting the .request event to
determine it reliably in InRoomChannel
2020-01-03 13:42:06 +01:00
Bruno Windels 29c04b6f9c only move to PHASE_DONE when both .done events are received
as once in done, the request is removed from the request map
and the second .done event that comes in will not find the request
anymore, so the request wouldn't be attached to the event anymore,
breaking rendering it in the timeline.
2020-01-03 13:42:06 +01:00
Bruno Windels 984b6234d2 don't block remote echos to VerificationRequests
also put logic to block non-participating senders in VerificationRequest
so it is shared between both channels.

Remote echo's should not be passed to the verifier though.
2020-01-03 13:42:06 +01:00
Bruno Windels dac4a5452d make this a public prop 2020-01-03 13:42:06 +01:00
Bruno Windels 5f9e82204a more ready and remote echo support 2020-01-03 13:42:06 +01:00
Bruno Windels c4142d93c3 store in-room verification requests by roomId, txnId
as it's harder to determine the other side of a request, given
the in-room code also processes remote echos for own events.
2020-01-03 13:42:06 +01:00
Bruno Windels b34a2c7ee2 WIP 2020-01-03 13:42:06 +01:00
Bruno Windels cd7cc1b71f set verification request on event 2020-01-03 13:42:06 +01:00
Bruno Windels 4c6dd564a4 filter verification methods from argument 2020-01-03 13:42:06 +01:00
Bruno Windels 28e46a82ea expose common phases as properties
so we don't need to import the PHASE_ constants where we need to check
2020-01-03 13:42:06 +01:00
Bruno Windels 10e294784e waitForVerifier is unused now, make it more broadly useful with callback 2020-01-03 13:42:06 +01:00
Bruno Windels 2da725340c return request instead of verifier from verification methods
as MSC2366 adds an extra interactive step to the verification process,
we can't wait for the verifier after sending the request.

This is a breaking change in the js-sdk as it changes the return type
of an existing method.
2020-01-03 13:42:06 +01:00
Bruno Windels 882d3a765d support .ready event in VerificationRequest 2020-01-03 13:42:06 +01:00
47 changed files with 2487 additions and 1029 deletions
+98
View File
@@ -1,3 +1,101 @@
Changes in [5.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.0-rc.1) (2020-02-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v4.0.0...v5.0.0-rc.1)
BREAKING CHANGES
---
* The verification methods API has removed an argument ([\#1206](https://github.com/matrix-org/matrix-js-sdk/pull/1206))
All Changes
---
* Remove methods argument to verification
[\#1206](https://github.com/matrix-org/matrix-js-sdk/pull/1206)
* don't do a dynamic import of request
[\#1207](https://github.com/matrix-org/matrix-js-sdk/pull/1207)
* QR self-verification fixes
[\#1201](https://github.com/matrix-org/matrix-js-sdk/pull/1201)
* Log every verification event
[\#1204](https://github.com/matrix-org/matrix-js-sdk/pull/1204)
* dont require .done event from other party
[\#1203](https://github.com/matrix-org/matrix-js-sdk/pull/1203)
* New option to fully reset Secret Storage keys in boostrapSecretStorage
[\#1202](https://github.com/matrix-org/matrix-js-sdk/pull/1202)
* Add function to estimate target device for a VerificationRequest
[\#1190](https://github.com/matrix-org/matrix-js-sdk/pull/1190)
* pass ssss item name to callback so we can differentiate UI on it
[\#1200](https://github.com/matrix-org/matrix-js-sdk/pull/1200)
* add export/import of Olm devices
[\#1167](https://github.com/matrix-org/matrix-js-sdk/pull/1167)
* Convert utils.js -> utils.ts
[\#1199](https://github.com/matrix-org/matrix-js-sdk/pull/1199)
* Don't sign ourselves as a user
[\#1197](https://github.com/matrix-org/matrix-js-sdk/pull/1197)
* Add a bunch of logging to verification
[\#1196](https://github.com/matrix-org/matrix-js-sdk/pull/1196)
* Fix: always return a valid string from InRoomChannel.getEventType
[\#1198](https://github.com/matrix-org/matrix-js-sdk/pull/1198)
* add logging when a request is being cancelled
[\#1195](https://github.com/matrix-org/matrix-js-sdk/pull/1195)
* Don't explode verification validation if we don't have an event type
[\#1194](https://github.com/matrix-org/matrix-js-sdk/pull/1194)
* Fix: verification request appearing for users that are not the receiver or
sender if they are in room
[\#1193](https://github.com/matrix-org/matrix-js-sdk/pull/1193)
* Fix getting secrets encoded with passthrough keys
[\#1192](https://github.com/matrix-org/matrix-js-sdk/pull/1192)
* Update QR code handling for new spec
[\#1175](https://github.com/matrix-org/matrix-js-sdk/pull/1175)
* Don't add ephemeral events to timeline when peeking
[\#1188](https://github.com/matrix-org/matrix-js-sdk/pull/1188)
* Fix typo
[\#1189](https://github.com/matrix-org/matrix-js-sdk/pull/1189)
* Verification: resolve race between .start events from both parties
[\#1187](https://github.com/matrix-org/matrix-js-sdk/pull/1187)
* Add option to bootstrap to start new key backup
[\#1184](https://github.com/matrix-org/matrix-js-sdk/pull/1184)
* Add a bunch of null guards to feature checks
[\#1182](https://github.com/matrix-org/matrix-js-sdk/pull/1182)
* docs: fix MatrixClient reference
[\#1183](https://github.com/matrix-org/matrix-js-sdk/pull/1183)
* Add helper to obtain the cancellation code for a verification request
[\#1180](https://github.com/matrix-org/matrix-js-sdk/pull/1180)
* Publish pre-releases as a separate tag on npm
[\#1178](https://github.com/matrix-org/matrix-js-sdk/pull/1178)
* Fix support for passthrough keys
[\#1177](https://github.com/matrix-org/matrix-js-sdk/pull/1177)
* Trust our own cross-signing keys if we verify them with another device
[\#1174](https://github.com/matrix-org/matrix-js-sdk/pull/1174)
* Ensure cross-signing keys are downloaded when checking trust
[\#1176](https://github.com/matrix-org/matrix-js-sdk/pull/1176)
* Don't log verification validation errors for normal messages
[\#1172](https://github.com/matrix-org/matrix-js-sdk/pull/1172)
* Fix bootstrap cleanup
[\#1173](https://github.com/matrix-org/matrix-js-sdk/pull/1173)
* QR code verification
[\#1155](https://github.com/matrix-org/matrix-js-sdk/pull/1155)
* expose deviceId prop on device channel
[\#1171](https://github.com/matrix-org/matrix-js-sdk/pull/1171)
* Move & upgrade babel runtime into dependencies (like it wants)
[\#1169](https://github.com/matrix-org/matrix-js-sdk/pull/1169)
* Add unit tests for verifying your own device, remove .event property on
verification request
[\#1166](https://github.com/matrix-org/matrix-js-sdk/pull/1166)
* For dm-verification, also consider events sent by other devices of same user
as "our" events
[\#1163](https://github.com/matrix-org/matrix-js-sdk/pull/1163)
* Add a prepare script
[\#1161](https://github.com/matrix-org/matrix-js-sdk/pull/1161)
* Remove :deviceId from /keys/upload/:deviceId as not spec-compliant
[\#1162](https://github.com/matrix-org/matrix-js-sdk/pull/1162)
* Refactor and expose some logic publicly for the TimelineWindow class.
[\#1159](https://github.com/matrix-org/matrix-js-sdk/pull/1159)
* Allow a device key upload request without auth
[\#1158](https://github.com/matrix-org/matrix-js-sdk/pull/1158)
* Support for .ready verification event (MSC2366) & other things
[\#1140](https://github.com/matrix-org/matrix-js-sdk/pull/1140)
Changes in [4.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v4.0.0) (2020-01-27)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v4.0.0-rc.1...v4.0.0)
+2
View File
@@ -0,0 +1,2 @@
olm.js
olm.wasm
+1
View File
@@ -0,0 +1 @@
../../../dist/browser-matrix.js
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Test Crypto in Browser</title>
<script src="lib/olm.js"></script>
<script src="lib/matrix.js"></script>
</head>
<body>
<h1>Testing export/import of Olm devices in the browser</h1>
<ul>
<li>
Make sure you built the current version of the Matrix JS SDK
(<code>yarn build</code>)
</li>
<li>
copy <code>olm.js</code> and <code>olm.wasm</code>
from a recent release of Olm (was tested with version 3.1.4)
in directory <code>lib/</code>
</li>
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
<li>
in the JS console, do:
<pre>
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
await aliceMatrixClient.exportDevice();
await aliceMatrixClient.getAccessToken();
</pre>
</li>
<li>
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page)
</li>
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
<li>
Do the following, replacing <code>ALICE_ID</code>
with the user ID of Alice (you can find it in the exported data)
<pre>
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
</pre>
</li>
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
<li>
Now do the following, using the exported data and the access token you saved previously:
<pre>
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
</pre>
</li>
<li>You should see the message sent by Bob printed in the console.</li>
</ul>
<script src="olm-device-export-import.js"></script>
</body>
</html>
@@ -0,0 +1,122 @@
if (!Olm) {
console.error(
"global.Olm does not seem to be present."
+ " Did you forget to add olm in the lib/ directory?"
);
}
const BASE_URL = 'http://localhost:8008';
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' };
const PASSWORD = 'password';
// useful to create new usernames
window.randomHex = () => Math.floor(Math.random() * (10**6)).toString(16);
window.newMatrixClient = async function (username) {
const registrationClient = matrixcs.createClient(BASE_URL);
const userRegisterResult = await registrationClient.register(
username,
PASSWORD,
null,
{ type: 'm.login.dummy' }
);
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
userId: userRegisterResult.user_id,
accessToken: userRegisterResult.access_token,
deviceId: userRegisterResult.device_id,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
}
window.importMatrixClient = async function (exportedDevice, accessToken) {
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
deviceToImport: exportedDevice,
accessToken,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
}
function extendMatrixClient(matrixClient) {
// automatic join
matrixClient.on('RoomMember.membership', async (event, member) => {
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) {
await matrixClient.joinRoom(member.roomId);
// setting up of room encryption seems to be triggered automatically
// but if we don't wait for it the first messages we send are unencrypted
await matrixClient.setRoomEncryption(member.roomId, { algorithm: 'm.megolm.v1.aes-sha2' })
}
});
matrixClient.onDecryptedMessage = message => {
console.log('Got encrypted message: ', message);
}
matrixClient.on('Event.decrypted', (event) => {
if (event.getType() === 'm.room.message'){
matrixClient.onDecryptedMessage(event.getContent().body);
} else {
console.log('decrypted an event of type', event.getType());
console.log(event);
}
});
matrixClient.createEncryptedRoom = async function(usersToInvite) {
const {
room_id: roomId,
} = await this.createRoom({
visibility: 'private',
invite: usersToInvite,
});
// matrixClient.setRoomEncryption() only updates local state
// but does not send anything to the server
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
// so we do it ourselves with 'sendStateEvent'
await this.sendStateEvent(
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG,
);
await this.setRoomEncryption(
roomId, ROOM_CRYPTO_CONFIG,
);
// Marking all devices as verified
let room = this.getRoom(roomId);
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
let memberkeys = await this.downloadKeys(members);
for (const userId in memberkeys) {
for (const deviceId in memberkeys[userId]) {
await this.setDeviceVerified(userId, deviceId);
}
}
return roomId;
}
matrixClient.sendTextMessage = async function(message, roomId) {
return matrixClient.sendMessage(
roomId,
{
body: message,
msgtype: 'm.text',
}
)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "4.0.0",
"version": "5.0.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
+6 -1
View File
@@ -294,9 +294,14 @@ fi
rm "${release_text}"
rm "${latest_changes}"
npm_publish_flags=''
if [ $prerelease -eq 1 ]; then
# Tag prereleases as `next` so the last stable release remains the default
npm_publish_flags='--tag next'
fi
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
npm publish
npm publish $npm_publish_flags
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
+55 -1
View File
@@ -41,7 +41,7 @@ async function setupSession(initiator, opponent) {
return sid;
}
describe("OlmDecryption", function() {
describe("OlmDevice", function() {
if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present');
return;
@@ -81,6 +81,60 @@ describe("OlmDecryption", function() {
);
});
it('exports picked account and olm sessions', async function() {
const sessionId = await setupSession(aliceOlmDevice, bobOlmDevice);
const exported = await bobOlmDevice.export();
// At this moment only Alice (the “initiator” in setupSession) has a session
expect(exported.sessions).toEqual([]);
const MESSAGE = (
"The olm or proteus is an aquatic salamander"
+ " in the family Proteidae"
);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
sessionId,
MESSAGE,
);
const bobRecreatedOlmDevice = makeOlmDevice();
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
const decrypted = await bobRecreatedOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
ciphertext.type,
ciphertext.body,
);
expect(decrypted.payload).toEqual(MESSAGE);
const exportedAgain = await bobRecreatedOlmDevice.export();
// this time we expect Bob to have a session to export
expect(exportedAgain.sessions).toHaveLength(1);
const MESSAGE_2 = (
"In contrast to most amphibians,"
+ " the olm is entirely aquatic"
);
const ciphertext2 = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
sessionId,
MESSAGE_2,
);
const bobRecreatedAgainOlmDevice = makeOlmDevice();
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
// Note: "decrypted_2" does not have the same structure as "decrypted"
const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage(
aliceOlmDevice.deviceCurve25519Key,
decrypted.session_id,
ciphertext2.type,
ciphertext2.body,
);
expect(decrypted2).toEqual(MESSAGE_2);
});
it("creates only one session at a time", async function() {
// if we call ensureOlmSessionsForDevices multiple times, it should
// only try to create one session at a time, even if the server is
+4 -4
View File
@@ -174,7 +174,7 @@ describe("Cross Signing", function() {
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
@@ -237,7 +237,7 @@ describe("Cross Signing", function() {
},
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
@@ -429,7 +429,7 @@ describe("Cross Signing", function() {
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
@@ -487,7 +487,7 @@ describe("Cross Signing", function() {
},
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
+3
View File
@@ -34,6 +34,9 @@ async function makeTestClient(userInfo, options) {
await client.initCrypto();
// No need to download keys for these tests
client._crypto.downloadKeys = async function() {};
return client;
}
@@ -0,0 +1,98 @@
/*
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.
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 {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
"../../../../src/crypto/verification/request/ToDeviceChannel";
import {MatrixEvent} from "../../../../src/models/event";
describe("InRoomChannel tests", function() {
const ALICE = "@alice:hs.tld";
const BOB = "@bob:hs.tld";
const MALORY = "@malory:hs.tld";
const client = {
getUserId() { return ALICE; },
};
it("getEventType only returns .request for a message with a msgtype", function() {
const invalidEvent = new MatrixEvent({
type: "m.key.verification.request",
});
expect(InRoomChannel.getEventType(invalidEvent)).toStrictEqual("");
const validEvent = new MatrixEvent({
type: "m.room.message",
content: { msgtype: "m.key.verification.request" },
});
expect(InRoomChannel.getEventType(validEvent)).
toStrictEqual("m.key.verification.request");
const validFooEvent = new MatrixEvent({ type: "m.foo" });
expect(InRoomChannel.getEventType(validFooEvent)).
toStrictEqual("m.foo");
});
it("getEventType should return m.room.message for messages", function() {
const messageEvent = new MatrixEvent({
type: "m.room.message",
content: { msgtype: "m.text" },
});
// XXX: The event type doesn't matter too much, just as long as it's not a verification event
expect(InRoomChannel.getEventType(messageEvent)).
toStrictEqual("m.room.message");
});
it("getEventType should return actual type for non-message events", function() {
const event = new MatrixEvent({
type: "m.room.member",
content: { },
});
expect(InRoomChannel.getEventType(event)).
toStrictEqual("m.room.member");
});
it("getOtherPartyUserId should not return anything for a request not " +
"directed at me", function() {
const event = new MatrixEvent({
sender: BOB,
type: "m.room.message",
content: { msgtype: "m.key.verification.request", to: MALORY },
});
expect(InRoomChannel.getOtherPartyUserId(event, client)).toStrictEqual(undefined);
});
it("getOtherPartyUserId should not return anything an event that is not of a valid " +
"request type", function() {
// invalid because this should be a room message with msgtype
const invalidRequest = new MatrixEvent({
sender: BOB,
type: "m.key.verification.request",
content: { to: ALICE },
});
expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client))
.toStrictEqual(undefined);
const startEvent = new MatrixEvent({
sender: BOB,
type: "m.key.verification.start",
content: { to: ALICE },
});
expect(InRoomChannel.getOtherPartyUserId(startEvent, client))
.toStrictEqual(undefined);
const fooEvent = new MatrixEvent({
sender: BOB,
type: "m.foo",
content: { to: ALICE },
});
expect(InRoomChannel.getOtherPartyUserId(fooEvent, client))
.toStrictEqual(undefined);
});
});
+7 -120
View File
@@ -16,8 +16,6 @@ limitations under the License.
*/
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;
@@ -31,124 +29,13 @@ describe("QR code verification", function() {
return Olm.init();
});
describe("showing", function() {
it("should emit an event to show a QR code", async function() {
const channel = {
send: jest.fn(),
};
const qrCode = new ShowQRCode(channel, {
getUserId: () => "@alice:example.com",
deviceId: "ABCDEFG",
getDeviceEd25519Key: function() {
return "device+ed25519+key";
},
});
const spy = jest.fn((e) => {
qrCode.done();
});
qrCode.on("show_qr_code", spy);
await qrCode.verify();
expect(spy).toHaveBeenCalledWith({
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
});
});
});
describe("scanning", function() {
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
it("should verify when a QR code is sent", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: jest.fn().mockReturnValue(device),
setDeviceVerified: jest.fn(),
};
const channel = {
send: jest.fn(),
};
const qrCode = new ScanQRCode(channel, client);
qrCode.on("confirm_user_id", ({userId, confirm}) => {
if (userId === "@alice:example.com") {
confirm();
} else {
qrCode.cancel(new Error("Incorrect user"));
}
});
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
await qrCode.verify();
expect(client.getStoredDevice)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
expect(client.setDeviceVerified)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
});
it("should error when the user ID doesn't match", async function() {
const client = {
getStoredDevice: jest.fn(),
setDeviceVerified: jest.fn(),
};
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();
});
it("should error if the key doesn't match", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: jest.fn().mockReturnValue(device),
setDeviceVerified: jest.fn(),
};
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();
describe("reciprocate", () => {
it("should verify the secret", () => {
// TODO: Actually write a test for this.
// Tests are hard because we are running before the verification
// process actually begins, and are largely UI-driven rather than
// logic-driven (compared to something like SAS). In the interest
// of time, tests are currently excluded.
});
});
});
+12 -3
View File
@@ -18,20 +18,27 @@ import "../../../olm-loader";
import {verificationMethods} from "../../../../src/crypto";
import {logger} from "../../../../src/logger";
import {SAS} from "../../../../src/crypto/verification/SAS";
import {makeTestClients} from './util';
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
const Olm = global.Olm;
describe("verification request", function() {
jest.useFakeTimers();
describe("verification request integration tests with crypto layer", function() {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
return;
}
beforeAll(function() {
setupWebcrypto();
return Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("should request and accept a verification", async function() {
const [alice, bob] = await makeTestClients(
[
@@ -64,7 +71,9 @@ describe("verification request", function() {
// XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer();
});
const aliceVerifier = await alice.client.requestVerification("@bob:example.com");
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
await aliceRequest.waitFor(r => r.started);
const aliceVerifier = aliceRequest.verifier;
expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay)
+10 -3
View File
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../../olm-loader";
import {makeTestClients} from './util';
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
import {MatrixEvent} from "../../../../src/models/event";
import {SAS} from "../../../../src/crypto/verification/SAS";
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
@@ -35,9 +35,14 @@ describe("SAS verification", function() {
}
beforeAll(function() {
setupWebcrypto();
return Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("should error on an unexpected event", async function() {
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
sas.handleEvent(new MatrixEvent({
@@ -442,9 +447,11 @@ describe("SAS verification", function() {
});
});
aliceVerifier = await alice.client.requestVerificationDM(
bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
const aliceRequest = await alice.client.requestVerificationDM(
bob.client.getUserId(), "!room_id",
);
await aliceRequest.waitFor(r => r.started);
aliceVerifier = aliceRequest.verifier;
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
+41 -18
View File
@@ -17,6 +17,7 @@ limitations under the License.
import {TestClient} from '../../../TestClient';
import {MatrixEvent} from "../../../../src/models/event";
import nodeCrypto from "crypto";
export async function makeTestClients(userInfos, options) {
const clients = [];
@@ -33,15 +34,13 @@ export async function makeTestClients(userInfos, options) {
content: msg,
});
const client = clientMap[userId][deviceId];
if (event.isEncrypted()) {
event.attemptDecryption(client._crypto)
.then(() => client.emit("toDeviceEvent", event));
} else {
setTimeout(
() => client.emit("toDeviceEvent", event),
0,
);
}
const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client._crypto) :
Promise.resolve();
decryptionPromise.then(
() => client.emit("toDeviceEvent", event),
);
}
}
}
@@ -50,21 +49,33 @@ export async function makeTestClients(userInfos, options) {
const sendEvent = function(room, type, content) {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
const event = new MatrixEvent({
const rawEvent = {
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: content,
room_id: room,
event_id: eventId,
});
for (const tc of clients) {
setTimeout(
() => tc.client.emit("Room.timeline", event),
0,
);
}
origin_server_ts: Date.now(),
};
const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
unsigned: {
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
},
}));
return {event_id: eventId};
setImmediate(() => {
for (const tc of clients) {
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
console.log("sending remote echo!!");
tc.client.emit("Room.timeline", remoteEcho);
} else {
tc.client.emit("Room.timeline", event);
}
}
});
return Promise.resolve({event_id: eventId});
};
for (const userInfo of userInfos) {
@@ -92,3 +103,15 @@ export async function makeTestClients(userInfos, options) {
return clients;
}
export function setupWebcrypto() {
global.crypto = {
getRandomValues: (buf) => {
return nodeCrypto.randomFillSync(buf);
},
};
}
export function teardownWebcrypto() {
global.crypto = undefined;
}
@@ -0,0 +1,250 @@
/*
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.
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 {VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE} from
"../../../../src/crypto/verification/request/VerificationRequest";
import {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
import {ToDeviceChannel} from
"../../../../src/crypto/verification/request/ToDeviceChannel";
import {MatrixEvent} from "../../../../src/models/event";
import {setupWebcrypto, teardownWebcrypto} from "./util";
function makeMockClient(userId, deviceId) {
let counter = 1;
let events = [];
const deviceEvents = {};
return {
getUserId() { return userId; },
getDeviceId() { return deviceId; },
sendEvent(roomId, type, content) {
counter = counter + 1;
const eventId = `$${userId}-${deviceId}-${counter}`;
events.push(new MatrixEvent({
sender: userId,
event_id: eventId,
room_id: roomId,
type,
content,
origin_server_ts: Date.now(),
}));
return Promise.resolve({event_id: eventId});
},
sendToDevice(type, msgMap) {
for (const userId of Object.keys(msgMap)) {
const deviceMap = msgMap[userId];
for (const deviceId of Object.keys(deviceMap)) {
const content = deviceMap[deviceId];
const event = new MatrixEvent({content, type});
deviceEvents[userId] = deviceEvents[userId] || {};
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
deviceEvents[userId][deviceId].push(event);
}
}
return Promise.resolve();
},
popEvents() {
const e = events;
events = [];
return e;
},
popDeviceEvents(userId, deviceId) {
const forDevice = deviceEvents[userId];
const events = forDevice && forDevice[deviceId];
const result = events || [];
if (events) {
delete forDevice[deviceId];
}
return result;
},
};
}
const MOCK_METHOD = "mock-verify";
class MockVerifier {
constructor(channel, client, userId, deviceId, startEvent) {
this._channel = channel;
this._startEvent = startEvent;
}
get events() {
return [DONE_TYPE];
}
async start() {
if (this._startEvent) {
await this._channel.send(DONE_TYPE, {});
} else {
await this._channel.send(START_TYPE, {method: MOCK_METHOD});
}
}
async handleEvent(event) {
if (event.getType() === DONE_TYPE && !this._startEvent) {
await this._channel.send(DONE_TYPE, {});
}
}
canSwitchStartEvent() {
return false;
}
}
function makeRemoteEcho(event) {
return new MatrixEvent(Object.assign({}, event.event, {
unsigned: {
transaction_id: "abc",
},
}));
}
async function distributeEvent(ownRequest, theirRequest, event) {
await ownRequest.channel.handleEvent(
makeRemoteEcho(event), ownRequest, true);
await theirRequest.channel.handleEvent(event, theirRequest, true);
}
describe("verification request unit tests", function() {
beforeAll(function() {
setupWebcrypto();
});
afterAll(() => {
teardownWebcrypto();
});
it("transition from UNSENT to DONE through happy path", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([[MOCK_METHOD, MockVerifier]]), alice);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([[MOCK_METHOD, MockVerifier]]), bob);
expect(aliceRequest.invalid).toBe(true);
expect(bobRequest.invalid).toBe(true);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
expect(requestEvent.getType()).toBe("m.room.message");
await distributeEvent(aliceRequest, bobRequest, requestEvent);
expect(aliceRequest.requested).toBe(true);
expect(bobRequest.requested).toBe(true);
await bobRequest.accept();
const [readyEvent] = bob.popEvents();
expect(readyEvent.getType()).toBe(READY_TYPE);
await distributeEvent(bobRequest, aliceRequest, readyEvent);
expect(bobRequest.ready).toBe(true);
expect(aliceRequest.ready).toBe(true);
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
await verifier.start();
const [startEvent] = alice.popEvents();
expect(startEvent.getType()).toBe(START_TYPE);
await distributeEvent(aliceRequest, bobRequest, startEvent);
expect(aliceRequest.started).toBe(true);
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
expect(bobRequest.started).toBe(true);
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
await bobRequest.verifier.start();
const [bobDoneEvent] = bob.popEvents();
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
const [aliceDoneEvent] = alice.popEvents();
expect(aliceDoneEvent.getType()).toBe(DONE_TYPE);
await distributeEvent(aliceRequest, bobRequest, aliceDoneEvent);
expect(aliceRequest.done).toBe(true);
expect(bobRequest.done).toBe(true);
});
it("methods only contains common methods", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([["c", function() {}], ["a", function() {}]]), alice);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([["c", function() {}], ["b", function() {}]]), bob);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await distributeEvent(aliceRequest, bobRequest, requestEvent);
await bobRequest.accept();
const [readyEvent] = bob.popEvents();
await distributeEvent(bobRequest, aliceRequest, readyEvent);
expect(aliceRequest.methods).toStrictEqual(["c"]);
expect(bobRequest.methods).toStrictEqual(["c"]);
});
it("other client accepting request puts it in observeOnly mode", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bob1Request = new VerificationRequest(
new InRoomChannel(bob1, "!room"), new Map(), bob1);
const bob2Request = new VerificationRequest(
new InRoomChannel(bob2, "!room"), new Map(), bob2);
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
await bob1Request.accept();
const [readyEvent] = bob1.popEvents();
expect(bob2Request.observeOnly).toBe(false);
await bob2Request.channel.handleEvent(readyEvent, bob2Request, true);
expect(bob2Request.observeOnly).toBe(true);
});
it("verify own device with to_device messages", async function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const bob1Request = new VerificationRequest(
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
ToDeviceChannel.makeTransactionId(), "device2"),
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
const to = {userId: "@bob:matrix.tld", deviceId: "device2"};
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
expect(verifier).toBeInstanceOf(MockVerifier);
await verifier.start();
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
expect(startEvent.getType()).toBe(START_TYPE);
const bob2Request = new VerificationRequest(
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
await bob2Request.verifier.start();
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
expect(doneEvent1.getType()).toBe(DONE_TYPE);
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
const [doneEvent2] = bob1.popDeviceEvents("@bob:matrix.tld", "device2");
expect(doneEvent2.getType()).toBe(DONE_TYPE);
await bob2Request.channel.handleEvent(doneEvent2, bob2Request, true);
expect(bob1Request.done).toBe(true);
expect(bob2Request.done).toBe(true);
});
});
+97 -108
View File
@@ -172,7 +172,7 @@ MatrixBaseApis.prototype.makeTxnId = function() {
* Check whether a username is available prior to registration. An error response
* indicates an invalid/unavailable username.
* @param {string} username The username to check the availability of.
* @return {module:client.Promise} Resolves: to `true`.
* @return {Promise} Resolves: to `true`.
*/
MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
return this._http.authedRequest(
@@ -193,7 +193,7 @@ MatrixBaseApis.prototype.isUsernameAvailable = function(username) {
* @param {string} guestAccessToken
* @param {string} inhibitLogin
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.register = function(
@@ -258,7 +258,7 @@ MatrixBaseApis.prototype.register = function(
* @param {Object=} opts Registration options
* @param {Object} opts.body JSON HTTP body to provide.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
@@ -271,7 +271,7 @@ MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
* @param {Object} data parameters for registration request
* @param {string=} kind type of user to register. may be "guest"
* @param {module:client.callback=} callback
* @return {module:client.Promise} Resolves: to the /register response
* @return {Promise} Resolves: to the /register response
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerRequest = function(data, kind, callback) {
@@ -287,7 +287,7 @@ MatrixBaseApis.prototype.registerRequest = function(data, kind, callback) {
/**
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.loginFlows = function(callback) {
@@ -298,7 +298,7 @@ MatrixBaseApis.prototype.loginFlows = function(callback) {
* @param {string} loginType
* @param {Object} data
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.login = function(loginType, data, callback) {
@@ -329,7 +329,7 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
* @param {string} user
* @param {string} password
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) {
@@ -342,7 +342,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback)
/**
* @param {string} relayState URL Callback after SAML2 Authentication
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
@@ -379,7 +379,7 @@ MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
/**
* @param {string} token Login token previously received from homeserver
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
@@ -396,7 +396,7 @@ MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
* it is up to the caller to either reset or destroy the MatrixClient after
* this method succeeds.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: On success, the empty object
* @return {Promise} Resolves: On success, the empty object
*/
MatrixBaseApis.prototype.logout = function(callback) {
return this._http.authedRequest(
@@ -414,7 +414,7 @@ MatrixBaseApis.prototype.logout = function(callback) {
* @param {boolean} erase Optional. If set, send as `erase` attribute in the
* JSON request body, indicating whether the account should be erased. Defaults
* to false.
* @return {module:client.Promise} Resolves: On success, the empty object
* @return {Promise} Resolves: On success, the empty object
*/
MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) {
if (typeof(erase) === 'function') {
@@ -467,7 +467,7 @@ MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId)
* @param {string} options.name The name to give this room.
* @param {string} options.topic The topic to give this room.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: <code>{room_id: {string},
* @return {Promise} Resolves: <code>{room_id: {string},
* room_alias: {string(opt)}}</code>
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
@@ -512,7 +512,7 @@ MatrixBaseApis.prototype.fetchRelations =
/**
* @param {string} roomId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.roomState = function(roomId, callback) {
@@ -545,7 +545,7 @@ MatrixBaseApis.prototype.fetchRoomEvent = function(roomId, eventId, callback) {
* @param {string} excludeMembership the membership type to exclude from the response
* @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: dictionary of userid to profile information
* @return {Promise} Resolves: dictionary of userid to profile information
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.members =
@@ -572,7 +572,7 @@ function(roomId, includeMembership, excludeMembership, atEventId, callback) {
* Upgrades a room to a new protocol version
* @param {string} roomId
* @param {string} newVersion The target version to upgrade to
* @return {module:client.Promise} Resolves: Object with key 'replacement_room'
* @return {Promise} Resolves: Object with key 'replacement_room'
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.upgradeRoom = function(roomId, newVersion) {
@@ -585,7 +585,7 @@ MatrixBaseApis.prototype.upgradeRoom = function(roomId, newVersion) {
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group summary object
* @return {Promise} Resolves: Group summary object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupSummary = function(groupId) {
@@ -595,7 +595,7 @@ MatrixBaseApis.prototype.getGroupSummary = function(groupId) {
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group profile object
* @return {Promise} Resolves: Group profile object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupProfile = function(groupId) {
@@ -610,7 +610,7 @@ MatrixBaseApis.prototype.getGroupProfile = function(groupId) {
* @param {string=} profile.avatar_url MXC avatar URL
* @param {string=} profile.short_description A short description of the room
* @param {string=} profile.long_description A longer HTML description of the room
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setGroupProfile = function(groupId, profile) {
@@ -626,7 +626,7 @@ MatrixBaseApis.prototype.setGroupProfile = function(groupId, profile) {
* least a 'type' field which is 'open' if anyone can join the group
* the group without prior approval, or 'invite' if an invite is
* required to join.
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setGroupJoinPolicy = function(groupId, policy) {
@@ -643,7 +643,7 @@ MatrixBaseApis.prototype.setGroupJoinPolicy = function(groupId, policy) {
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group users list object
* @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupUsers = function(groupId) {
@@ -653,7 +653,7 @@ MatrixBaseApis.prototype.getGroupUsers = function(groupId) {
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group users list object
* @return {Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupInvitedUsers = function(groupId) {
@@ -663,7 +663,7 @@ MatrixBaseApis.prototype.getGroupInvitedUsers = function(groupId) {
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group rooms list object
* @return {Promise} Resolves: Group rooms list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupRooms = function(groupId) {
@@ -674,7 +674,7 @@ MatrixBaseApis.prototype.getGroupRooms = function(groupId) {
/**
* @param {string} groupId
* @param {string} userId
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.inviteUserToGroup = function(groupId, userId) {
@@ -688,7 +688,7 @@ MatrixBaseApis.prototype.inviteUserToGroup = function(groupId, userId) {
/**
* @param {string} groupId
* @param {string} userId
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeUserFromGroup = function(groupId, userId) {
@@ -703,7 +703,7 @@ MatrixBaseApis.prototype.removeUserFromGroup = function(groupId, userId) {
* @param {string} groupId
* @param {string} userId
* @param {string} roleId Optional.
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addUserToGroupSummary = function(groupId, userId, roleId) {
@@ -719,7 +719,7 @@ MatrixBaseApis.prototype.addUserToGroupSummary = function(groupId, userId, roleI
/**
* @param {string} groupId
* @param {string} userId
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeUserFromGroupSummary = function(groupId, userId) {
@@ -734,7 +734,7 @@ MatrixBaseApis.prototype.removeUserFromGroupSummary = function(groupId, userId)
* @param {string} groupId
* @param {string} roomId
* @param {string} categoryId Optional.
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addRoomToGroupSummary = function(groupId, roomId, categoryId) {
@@ -750,7 +750,7 @@ MatrixBaseApis.prototype.addRoomToGroupSummary = function(groupId, roomId, categ
/**
* @param {string} groupId
* @param {string} roomId
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeRoomFromGroupSummary = function(groupId, roomId) {
@@ -765,7 +765,7 @@ MatrixBaseApis.prototype.removeRoomFromGroupSummary = function(groupId, roomId)
* @param {string} groupId
* @param {string} roomId
* @param {bool} isPublic Whether the room-group association is visible to non-members
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addRoomToGroup = function(groupId, roomId, isPublic) {
@@ -786,7 +786,7 @@ MatrixBaseApis.prototype.addRoomToGroup = function(groupId, roomId, isPublic) {
* @param {string} groupId
* @param {string} roomId
* @param {bool} isPublic Whether the room-group association is visible to non-members
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.updateGroupRoomVisibility = function(groupId, roomId, isPublic) {
@@ -806,7 +806,7 @@ MatrixBaseApis.prototype.updateGroupRoomVisibility = function(groupId, roomId, i
/**
* @param {string} groupId
* @param {string} roomId
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeRoomFromGroup = function(groupId, roomId) {
@@ -820,7 +820,7 @@ MatrixBaseApis.prototype.removeRoomFromGroup = function(groupId, roomId) {
/**
* @param {string} groupId
* @param {Object} opts Additional options to send alongside the acceptance.
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.acceptGroupInvite = function(groupId, opts = null) {
@@ -833,7 +833,7 @@ MatrixBaseApis.prototype.acceptGroupInvite = function(groupId, opts = null) {
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.joinGroup = function(groupId) {
@@ -846,7 +846,7 @@ MatrixBaseApis.prototype.joinGroup = function(groupId) {
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.leaveGroup = function(groupId) {
@@ -858,7 +858,7 @@ MatrixBaseApis.prototype.leaveGroup = function(groupId) {
};
/**
* @return {module:client.Promise} Resolves: The groups to which the user is joined
* @return {Promise} Resolves: The groups to which the user is joined
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedGroups = function() {
@@ -870,7 +870,7 @@ MatrixBaseApis.prototype.getJoinedGroups = function() {
* @param {Object} content Request content
* @param {string} content.localpart The local part of the desired group ID
* @param {Object} content.profile Group profile object
* @return {module:client.Promise} Resolves: Object with key group_id: id of the created group
* @return {Promise} Resolves: Object with key group_id: id of the created group
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.createGroup = function(content) {
@@ -882,7 +882,7 @@ MatrixBaseApis.prototype.createGroup = function(content) {
/**
* @param {string[]} userIds List of user IDs
* @return {module:client.Promise} Resolves: Object as exmaple below
* @return {Promise} Resolves: Object as exmaple below
*
* {
* "users": {
@@ -903,7 +903,7 @@ MatrixBaseApis.prototype.getPublicisedGroups = function(userIds) {
/**
* @param {string} groupId
* @param {bool} isPublic Whether the user's membership of this group is made public
* @return {module:client.Promise} Resolves: Empty object
* @return {Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setGroupPublicity = function(groupId, isPublic) {
@@ -922,7 +922,7 @@ MatrixBaseApis.prototype.setGroupPublicity = function(groupId, isPublic) {
* @param {string} eventType
* @param {string} stateKey
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, callback) {
@@ -946,7 +946,7 @@ MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, c
* @param {Object} content
* @param {string} stateKey
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
@@ -969,7 +969,7 @@ MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, s
* @param {string} roomId
* @param {Number} limit
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
@@ -1000,7 +1000,7 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
* @param {object} opts Options for the read markers.
* @param {object} opts.hidden True to hide the read receipt from other users. <b>This
* property is currently unstable and may change in the future.</b>
* @return {module:client.Promise} Resolves: the empty object, {}.
* @return {Promise} Resolves: the empty object, {}.
*/
MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
function(roomId, rmEventId, rrEventId, opts) {
@@ -1020,7 +1020,7 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
};
/**
* @return {module:client.Promise} Resolves: A list of the user's current rooms
* @return {Promise} Resolves: A list of the user's current rooms
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedRooms = function() {
@@ -1031,7 +1031,7 @@ MatrixBaseApis.prototype.getJoinedRooms = function() {
/**
* Retrieve membership info. for a room.
* @param {string} roomId ID of the room to get membership for
* @return {module:client.Promise} Resolves: A list of currently joined users
* @return {Promise} Resolves: A list of currently joined users
* and their profile data.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
@@ -1055,7 +1055,7 @@ MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) {
* @param {object} options.filter Filter parameters
* @param {string} options.filter.generic_search_term String to search for
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.publicRooms = function(options, callback) {
@@ -1087,7 +1087,7 @@ MatrixBaseApis.prototype.publicRooms = function(options, callback) {
* @param {string} alias The room alias to create.
* @param {string} roomId The room ID to link the alias to.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO.
* @return {Promise} Resolves: TODO.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) {
@@ -1107,7 +1107,7 @@ MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) {
* and you must have sufficient access to do this operation.
* @param {string} alias The room alias to delete.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO.
* @return {Promise} Resolves: TODO.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
@@ -1123,7 +1123,7 @@ MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
* Get room info for the given alias.
* @param {string} alias The room alias to resolve.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Object with room_id and servers.
* @return {Promise} Resolves: Object with room_id and servers.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) {
@@ -1139,7 +1139,7 @@ MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) {
/**
* @param {string} roomAlias
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) {
@@ -1152,7 +1152,7 @@ MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) {
* Get the visibility of a room in the current HS's room directory
* @param {string} roomId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getRoomDirectoryVisibility =
@@ -1170,7 +1170,7 @@ MatrixBaseApis.prototype.getRoomDirectoryVisibility =
* in the public directory, or "private" to make
* it invisible.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setRoomDirectoryVisibility =
@@ -1193,7 +1193,7 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibility =
* in the public directory, or "private" to make
* it invisible.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
@@ -1216,7 +1216,7 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
* @param {string} opts.term the term with which to search.
* @param {number} opts.limit the maximum number of results to return. The server will
* apply a limit if unspecified.
* @return {module:client.Promise} Resolves: an array of results.
* @return {Promise} Resolves: an array of results.
*/
MatrixBaseApis.prototype.searchUserDirectory = function(opts) {
const body = {
@@ -1272,7 +1272,7 @@ MatrixBaseApis.prototype.searchUserDirectory = function(opts) {
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*
* @return {module:client.Promise} Resolves to response object, as
* @return {Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
@@ -1282,7 +1282,7 @@ MatrixBaseApis.prototype.uploadContent = function(file, opts) {
/**
* Cancel a file upload in progress
* @param {module:client.Promise} promise The promise returned from uploadContent
* @param {Promise} promise The promise returned from uploadContent
* @return {boolean} true if canceled, otherwise false
*/
MatrixBaseApis.prototype.cancelUpload = function(promise) {
@@ -1310,7 +1310,7 @@ MatrixBaseApis.prototype.getCurrentUploads = function() {
* @param {string} info The kind of info to retrieve (e.g. 'displayname',
* 'avatar_url').
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) {
@@ -1332,7 +1332,7 @@ MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) {
/**
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getThreePids = function(callback) {
@@ -1352,7 +1352,7 @@ MatrixBaseApis.prototype.getThreePids = function(callback) {
* @param {Object} creds
* @param {boolean} bind
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: on success
* @return {Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
@@ -1375,7 +1375,7 @@ MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
*
* @param {Object} data A object with 3PID validation data from having called
* `account/3pid/<medium>/requestToken` on the homeserver.
* @return {module:client.Promise} Resolves: on success
* @return {Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addThreePidOnly = async function(data) {
@@ -1398,7 +1398,7 @@ MatrixBaseApis.prototype.addThreePidOnly = async function(data) {
* @param {Object} data A object with 3PID validation data from having called
* `validate/<medium>/requestToken` on the identity server. It should also
* contain `id_server` and `id_access_token` fields as well.
* @return {module:client.Promise} Resolves: on success
* @return {Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.bindThreePid = async function(data) {
@@ -1418,7 +1418,7 @@ MatrixBaseApis.prototype.bindThreePid = async function(data) {
* @param {string} medium The threepid medium (eg. 'email')
* @param {string} address The threepid address (eg. 'bob@example.com')
* this must be as returned by getThreePids.
* @return {module:client.Promise} Resolves: on success
* @return {Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.unbindThreePid = async function(medium, address) {
@@ -1439,7 +1439,7 @@ MatrixBaseApis.prototype.unbindThreePid = async function(medium, address) {
* @param {string} medium The threepid medium (eg. 'email')
* @param {string} address The threepid address (eg. 'bob@example.com')
* this must be as returned by getThreePids.
* @return {module:client.Promise} Resolves: The server response on success
* @return {Promise} Resolves: The server response on success
* (generally the empty JSON object)
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
@@ -1457,7 +1457,7 @@ MatrixBaseApis.prototype.deleteThreePid = function(medium, address) {
* @param {Object} authDict
* @param {string} newPassword The new desired password.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback) {
@@ -1478,7 +1478,7 @@ MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback)
/**
* Gets all devices recorded for the logged-in user
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getDevices = function() {
@@ -1492,7 +1492,7 @@ MatrixBaseApis.prototype.getDevices = function() {
*
* @param {string} device_id device to update
* @param {Object} body body of request
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
@@ -1508,7 +1508,7 @@ MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
*
* @param {string} device_id device to delete
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
@@ -1530,7 +1530,7 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
*
* @param {string[]} devices IDs of the devices to delete
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
@@ -1552,7 +1552,7 @@ MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
* Gets all pushers registered for the logged-in user
*
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Array of objects representing pushers
* @return {Promise} Resolves: Array of objects representing pushers
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getPushers = function(callback) {
@@ -1567,7 +1567,7 @@ MatrixBaseApis.prototype.getPushers = function(callback) {
*
* @param {Object} pusher Object representing a pusher
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Empty json object on success
* @return {Promise} Resolves: Empty json object on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
@@ -1579,7 +1579,7 @@ MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
/**
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getPushRules = function(callback) {
@@ -1594,7 +1594,7 @@ MatrixBaseApis.prototype.getPushRules = function(callback) {
* @param {string} ruleId
* @param {Object} body
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callback) {
@@ -1613,7 +1613,7 @@ MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callb
* @param {string} kind
* @param {string} ruleId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback) {
@@ -1632,7 +1632,7 @@ MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback
* @param {string} ruleId
* @param {boolean} enabled
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind,
@@ -1653,7 +1653,7 @@ MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind,
* @param {string} ruleId
* @param {array} actions
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind,
@@ -1677,7 +1677,7 @@ MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind,
* @param {string} opts.next_batch the batch token to pass in the query string
* @param {Object} opts.body the JSON object to pass to the request body.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.search = function(opts, callback) {
@@ -1698,28 +1698,16 @@ MatrixBaseApis.prototype.search = function(opts, callback) {
*
* @param {Object} content body of upload request
*
* @param {Object=} opts
*
* @param {string=} opts.device_id explicit device_id to use for upload
* (default is to use the same as that used during auth).
* @param {Object=} opts this method no longer takes any opts,
* used to take opts.device_id but this was not removed from the spec as a redundant parameter
*
* @param {module:client.callback=} callback
*
* @return {module:client.Promise} Resolves: result object. Rejects: with
* @return {Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
opts = opts || {};
const deviceId = opts.device_id;
let path;
if (deviceId) {
path = utils.encodeUri("/keys/upload/$deviceId", {
$deviceId: deviceId,
});
} else {
path = "/keys/upload";
}
return this._http.authedRequest(callback, "POST", path, undefined, content);
return this._http.authedRequest(callback, "POST", "/keys/upload", undefined, content);
};
MatrixBaseApis.prototype.uploadKeySignatures = function(content) {
@@ -1741,7 +1729,7 @@ MatrixBaseApis.prototype.uploadKeySignatures = function(content) {
* @param {string=} opts.token sync token to pass in the query request, to help
* the HS give the most recent results
*
* @return {module:client.Promise} Resolves: result object. Rejects: with
* @return {Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
@@ -1773,7 +1761,7 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
*
* @param {string} [key_algorithm = signed_curve25519] desired key type
*
* @return {module:client.Promise} Resolves: result object. Rejects: with
* @return {Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
@@ -1802,7 +1790,7 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
* @param {string} oldToken
* @param {string} newToken
*
* @return {module:client.Promise} Resolves: result object. Rejects: with
* @return {Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
@@ -1816,7 +1804,8 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
};
MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
const data = Object.assign({}, keys, {auth});
const data = Object.assign({}, keys);
if (auth) Object.assign(data, {auth});
return this._http.authedRequest(
undefined, "POST", "/keys/device_signing/upload", undefined, data, {
prefix: PREFIX_UNSTABLE,
@@ -1836,7 +1825,7 @@ MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
* general) was added as part of the v2 API version.
*
* @param {object} hsOpenIdToken
* @return {module:client.Promise} Resolves: with object containing an Identity
* @return {Promise} Resolves: with object containing an Identity
* Server access token.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
@@ -1872,7 +1861,7 @@ MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
* @param {string} identityAccessToken The `access_token` field of the identity
* server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if no identity server is set
*/
@@ -1938,7 +1927,7 @@ MatrixBaseApis.prototype.requestEmailToken = async function(
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if no identity server is set
*/
@@ -1998,7 +1987,7 @@ MatrixBaseApis.prototype.requestMsisdnToken = async function(
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set
*/
@@ -2049,7 +2038,7 @@ MatrixBaseApis.prototype.submitMsisdnToken = async function(
* This must be the same value submitted in the requestToken call.
* @param {string} msisdnToken The MSISDN token, as enetered by the user.
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.submitMsisdnTokenOtherUrl = function(
@@ -2175,7 +2164,7 @@ MatrixBaseApis.prototype.identityHashedLookup = async function(
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: A threepid mapping
* @return {Promise} Resolves: A threepid mapping
* object or the empty object if no mapping
* exists
* @return {module:http-api.MatrixError} Rejects: with an error response.
@@ -2243,7 +2232,7 @@ MatrixBaseApis.prototype.lookupThreePid = async function(
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: Lookup results from IS.
* @return {Promise} Resolves: Lookup results from IS.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.bulkLookupThreePids = async function(
@@ -2300,7 +2289,7 @@ MatrixBaseApis.prototype.bulkLookupThreePids = async function(
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: an object with account info.
* @return {Promise} Resolves: an object with account info.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getIdentityAccount = function(
@@ -2323,7 +2312,7 @@ MatrixBaseApis.prototype.getIdentityAccount = function(
* content to send. Map from user_id to device_id to content object.
* @param {string=} txnId transaction id. One will be made up if not
* supplied.
* @return {module:client.Promise} Resolves to the result object
* @return {Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.sendToDevice = function(
eventType, contentMap, txnId,
@@ -2352,7 +2341,7 @@ MatrixBaseApis.prototype.sendToDevice = function(
/**
* Get the third party protocols that can be reached using
* this HS
* @return {module:client.Promise} Resolves to the result object
* @return {Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
return this._http.authedRequest(
@@ -2374,7 +2363,7 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
* @param {string} protocol The protocol given in getThirdpartyProtocols()
* @param {object} params Protocol-specific parameters, as given in the
* response to getThirdpartyProtocols()
* @return {module:client.Promise} Resolves to the result object
* @return {Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
const path = utils.encodeUri("/thirdparty/location/$protocol", {
@@ -2390,7 +2379,7 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
* @param {string} protocol The protocol given in getThirdpartyProtocols()
* @param {object} params Protocol-specific parameters, as given in the
* response to getThirdpartyProtocols()
* @return {module:client.Promise} Resolves to the result object
* @return {Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
const path = utils.encodeUri("/thirdparty/user/$protocol", {
@@ -2425,7 +2414,7 @@ MatrixBaseApis.prototype.agreeToTerms = function(
* @param {string} eventId The event to report.
* @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive.
* @param {string} reason The reason the content is being reported. May be blank.
* @returns {module:client.Promise} Resolves to an empty object if successful
* @returns {Promise} Resolves to an empty object if successful
*/
MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason) {
const path = utils.encodeUri("/rooms/$roomId/report/$eventId", {
+136 -118
View File
@@ -98,6 +98,12 @@ function keyFromRecoverySession(session, decryptionKey) {
*
* @param {string} opts.userId The user ID for this user.
*
* @param {Object} opts.deviceToImport Device data exported with
* "exportDevice" method that must be imported to recreate this device.
* Should only be useful for devices with end-to-end crypto enabled.
* If provided, opts.deviceId and opts.userId should **NOT** be provided
* (they are present in the exported data).
*
* @param {IdentityServerProvider} [opts.identityServer]
* Optional. A provider object with one function `getAccessToken`, which is a
* callback that returns a Promise<String> of an identity access token to supply
@@ -215,6 +221,7 @@ function keyFromRecoverySession(session, decryptionKey) {
* pubkey: {UInt8Array}
* }
* }
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
*
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
* Optional. Function called when a request for a secret is received from another
@@ -248,6 +255,29 @@ export function MatrixClient(opts) {
userId: userId,
};
if (opts.deviceToImport) {
if (this.deviceId) {
logger.warn(
'not importing device because'
+ ' device ID is provided to constructor'
+ ' independently of exported data',
);
} else if (this.credentials.userId) {
logger.warn(
'not importing device because'
+ ' user ID is provided to constructor'
+ ' independently of exported data',
);
} else if (!(opts.deviceToImport.deviceId)) {
logger.warn('not importing device because no device ID in exported data');
} else {
this.deviceId = opts.deviceToImport.deviceId;
this.credentials.userId = opts.deviceToImport.userId;
// will be used during async initialization of the crypto
this._exportedOlmDeviceToImport = opts.deviceToImport.olmDevice;
}
}
this.scheduler = opts.scheduler;
if (this.scheduler) {
const self = this;
@@ -391,6 +421,18 @@ export function MatrixClient(opts) {
utils.inherits(MatrixClient, EventEmitter);
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
MatrixClient.prototype.exportDevice = async function() {
if (!(this._crypto)) {
logger.warn('not exporting device if crypto is not enabled');
return;
}
return {
userId: this.credentials.userId,
deviceId: this.deviceId,
olmDevice: await this._crypto._olmDevice.export(),
};
};
/**
* Clear any data out of the persistent stores used by the client.
*
@@ -507,7 +549,7 @@ MatrixClient.prototype.isInitialSyncComplete = function() {
if (!state) {
return false;
}
return state === "PREPAED" || state === "SYNCING";
return state === "PREPARED" || state === "SYNCING";
};
/**
@@ -570,7 +612,7 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
* Gets the capabilities of the homeserver. Always returns an object of
* capability keys and their options, which may be empty.
* @param {boolean} fresh True to ignore any cached values.
* @return {module:client.Promise} Resolves to the capabilities of the homeserver
* @return {Promise} Resolves to the capabilities of the homeserver
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getCapabilities = function(fresh=false) {
@@ -684,7 +726,10 @@ MatrixClient.prototype.initCrypto = async function() {
]);
logger.log("Crypto: initialising crypto object...");
await crypto.init();
await crypto.init({
exportedOlmDevice: this._exportedOlmDeviceToImport,
});
delete this._exportedOlmDeviceToImport;
this.olmVersion = Crypto.getOlmVersion();
@@ -751,7 +796,7 @@ MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
*
* @param {string} userId the user to list keys for.
*
* @return {Promise<module:crypto-deviceinfo[]>} list of devices
* @return {Promise<module:crypto/deviceinfo[]>} list of devices
*/
MatrixClient.prototype.getStoredDevicesForUser = async function(userId) {
if (this._crypto === null) {
@@ -766,7 +811,7 @@ MatrixClient.prototype.getStoredDevicesForUser = async function(userId) {
* @param {string} userId the user to list keys for.
* @param {string} deviceId unique identifier for the device
*
* @return {Promise<?module:crypto-deviceinfo>} device or null
* @return {Promise<?module:crypto/deviceinfo>} device or null
*/
MatrixClient.prototype.getStoredDevice = async function(userId, deviceId) {
if (this._crypto === null) {
@@ -862,52 +907,32 @@ async function _setDeviceVerification(
*
* @param {string} userId the user to request verification with
* @param {string} roomId the room to use for verification
* @param {Array} methods array of verification methods to use. Defaults to
* all known methods
*
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
* when the request is accepted by the other user
* @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
* when the request has been sent to the other party.
*/
MatrixClient.prototype.requestVerificationDM = function(userId, roomId, methods) {
MatrixClient.prototype.requestVerificationDM = function(userId, roomId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.requestVerificationDM(userId, roomId, methods);
};
/**
* Accept a key verification request from a DM.
*
* @param {module:models/event~MatrixEvent} event the verification request
* that is accepted
* @param {string} method the verification mmethod to use
*
* @returns {module:crypto/verification/Base} a verifier
*/
MatrixClient.prototype.acceptVerificationDM = function(event, method) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.acceptVerificationDM(event, method);
return this._crypto.requestVerificationDM(userId, roomId);
};
/**
* Request a key verification from another user.
*
* @param {string} userId the user to request verification with
* @param {Array} methods array of verification methods to use. Defaults to
* all known methods
* @param {Array} devices array of device IDs to send requests to. Defaults to
* all devices owned by the user
*
* @returns {Promise<module:crypto/verification/Base>} resolves to a verifier
* when the request is accepted by the other user
* @returns {Promise<module:crypto/verification/request/VerificationRequest>} resolves to a VerificationRequest
* when the request has been sent to the other party.
*/
MatrixClient.prototype.requestVerification = function(userId, methods, devices) {
MatrixClient.prototype.requestVerification = function(userId, devices) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.requestVerification(userId, methods, devices);
return this._crypto.requestVerification(userId, devices);
};
/**
@@ -1336,7 +1361,7 @@ MatrixClient.prototype.forceDiscardSession = function(roomId) {
*
* This should be encrypted before returning it to the user.
*
* @return {module:client.Promise} a promise which resolves to a list of
* @return {Promise} a promise which resolves to a list of
* session export objects
*/
MatrixClient.prototype.exportRoomKeys = function() {
@@ -1351,7 +1376,7 @@ MatrixClient.prototype.exportRoomKeys = function() {
*
* @param {Object[]} keys a list of session export objects
*
* @return {module:client.Promise} a promise which resolves when the keys
* @return {Promise} a promise which resolves when the keys
* have been imported
*/
MatrixClient.prototype.importRoomKeys = function(keys) {
@@ -1616,7 +1641,7 @@ MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version)
* @param {string} sessionId ID of the session that the keys are for Optional.
* @param {integer} version backup version Optional.
* @param {object} data Object keys to send
* @return {module:client.Promise} a promise that will resolve when the keys
* @return {Promise} a promise that will resolve when the keys
* are uploaded
*/
MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data) {
@@ -1842,7 +1867,7 @@ MatrixClient.prototype.getGroups = function() {
/**
* Get the config for the media repository.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves with an object containing the config.
* @return {Promise} Resolves with an object containing the config.
*/
MatrixClient.prototype.getMediaConfig = function(callback) {
return this._http.authedRequest(
@@ -1932,7 +1957,7 @@ MatrixClient.prototype.getUsers = function() {
* @param {string} eventType The event type
* @param {Object} contents the contents object for the event
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setAccountData = function(eventType, contents, callback) {
@@ -1959,7 +1984,7 @@ MatrixClient.prototype.getAccountData = function(eventType) {
* 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
* @return {Promise} Resolves: The contents of the given account
* data event.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
@@ -1996,7 +2021,7 @@ MatrixClient.prototype.getIgnoredUsers = function() {
* Sets the users that the current user should ignore.
* @param {string[]} userIds the user IDs to ignore
* @param {module:client.callback} [callback] Optional.
* @return {module:client.Promise} Resolves: Account data event
* @return {Promise} Resolves: Account data event
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setIgnoredUsers = function(userIds, callback) {
@@ -2029,7 +2054,7 @@ MatrixClient.prototype.isUserIgnored = function(userId) {
* @param {string[]} opts.viaServers The server names to try and join through in
* addition to those that are automatically chosen.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Room object.
* @return {Promise} Resolves: Room object.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
@@ -2097,7 +2122,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
* @param {MatrixEvent} event The event to resend.
* @param {Room} room Optional. The room the event is in. Will update the
* timeline entry if provided.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.resendEvent = function(event, room) {
@@ -2131,7 +2156,7 @@ MatrixClient.prototype.cancelPendingEvent = function(event) {
* @param {string} roomId
* @param {string} name
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setRoomName = function(roomId, name, callback) {
@@ -2143,7 +2168,7 @@ MatrixClient.prototype.setRoomName = function(roomId, name, callback) {
* @param {string} roomId
* @param {string} topic
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setRoomTopic = function(roomId, topic, callback) {
@@ -2154,7 +2179,7 @@ MatrixClient.prototype.setRoomTopic = function(roomId, topic, callback) {
/**
* @param {string} roomId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getRoomTags = function(roomId, callback) {
@@ -2172,7 +2197,7 @@ MatrixClient.prototype.getRoomTags = function(roomId, callback) {
* @param {string} tagName name of room tag to be set
* @param {object} metadata associated with that tag to be stored
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setRoomTag = function(roomId, tagName, metadata, callback) {
@@ -2190,7 +2215,7 @@ MatrixClient.prototype.setRoomTag = function(roomId, tagName, metadata, callback
* @param {string} roomId
* @param {string} tagName name of room tag to be removed
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.deleteRoomTag = function(roomId, tagName, callback) {
@@ -2209,7 +2234,7 @@ MatrixClient.prototype.deleteRoomTag = function(roomId, tagName, callback) {
* @param {string} eventType event type to be set
* @param {object} content event content
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setRoomAccountData = function(roomId, eventType,
@@ -2231,7 +2256,7 @@ MatrixClient.prototype.setRoomAccountData = function(roomId, eventType,
* @param {Number} powerLevel
* @param {MatrixEvent} event
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel,
@@ -2259,7 +2284,7 @@ MatrixClient.prototype.setPowerLevel = function(roomId, userId, powerLevel,
* @param {Object} content
* @param {string} txnId Optional.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
@@ -2274,7 +2299,7 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
* @param {object} eventObject An object with the partial structure of an event, to which event_id, user_id, room_id and origin_server_ts will be added.
* @param {string} txnId the txnId.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
@@ -2414,7 +2439,7 @@ function _sendEvent(client, room, event, callback) {
* @param {module:models/room?} room destination room. Null if the destination
* is not a room we have seen over the sync pipe.
*
* @return {module:client.Promise?} Promise which resolves when the event has been
* @return {Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/
@@ -2523,7 +2548,7 @@ function _sendEventHttpRequest(client, event) {
* @param {string} [txnId] transaction id. One will be made up if not
* supplied.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callback) {
@@ -2539,7 +2564,7 @@ MatrixClient.prototype.redactEvent = function(roomId, eventId, txnId, callback)
* @param {Object} content
* @param {string} txnId Optional.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendMessage = function(roomId, content, txnId, callback) {
@@ -2556,7 +2581,7 @@ MatrixClient.prototype.sendMessage = function(roomId, content, txnId, callback)
* @param {string} body
* @param {string} txnId Optional.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendTextMessage = function(roomId, body, txnId, callback) {
@@ -2569,7 +2594,7 @@ MatrixClient.prototype.sendTextMessage = function(roomId, body, txnId, callback)
* @param {string} body
* @param {string} txnId Optional.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendNotice = function(roomId, body, txnId, callback) {
@@ -2582,7 +2607,7 @@ MatrixClient.prototype.sendNotice = function(roomId, body, txnId, callback) {
* @param {string} body
* @param {string} txnId Optional.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendEmoteMessage = function(roomId, body, txnId, callback) {
@@ -2596,7 +2621,7 @@ MatrixClient.prototype.sendEmoteMessage = function(roomId, body, txnId, callback
* @param {Object} info
* @param {string} text
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendImageMessage = function(roomId, url, info, text, callback) {
@@ -2621,7 +2646,7 @@ MatrixClient.prototype.sendImageMessage = function(roomId, url, info, text, call
* @param {Object} info
* @param {string} text
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendStickerMessage = function(roomId, url, info, text, callback) {
@@ -2646,7 +2671,7 @@ MatrixClient.prototype.sendStickerMessage = function(roomId, url, info, text, ca
* @param {string} body
* @param {string} htmlBody
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendHtmlMessage = function(roomId, body, htmlBody, callback) {
@@ -2659,7 +2684,7 @@ MatrixClient.prototype.sendHtmlMessage = function(roomId, body, htmlBody, callba
* @param {string} body
* @param {string} htmlBody
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callback) {
@@ -2672,7 +2697,7 @@ MatrixClient.prototype.sendHtmlNotice = function(roomId, body, htmlBody, callbac
* @param {string} body
* @param {string} htmlBody
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback) {
@@ -2686,7 +2711,7 @@ MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback
* @param {string} receiptType The kind of receipt e.g. "m.read"
* @param {object} opts Additional content to send alongside the receipt.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendReceipt = function(event, receiptType, opts, callback) {
@@ -2723,7 +2748,7 @@ MatrixClient.prototype.sendReceipt = function(event, receiptType, opts, callback
* other users and homeservers. Default false (send to everyone). <b>This
* property is unstable and may change in the future.</b>
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendReadReceipt = async function(event, opts, callback) {
@@ -2759,7 +2784,7 @@ MatrixClient.prototype.sendReadReceipt = async function(event, opts, callback) {
* @param {object} opts Options for the read markers
* @param {object} opts.hidden True to hide the receipt from other users and homeservers.
* <b>This property is unstable and may change in the future.</b>
* @return {module:client.Promise} Resolves: the empty object, {}.
* @return {Promise} Resolves: the empty object, {}.
*/
MatrixClient.prototype.setRoomReadMarkers = async function(
roomId, rmEventId, rrEvent, opts,
@@ -2795,7 +2820,7 @@ MatrixClient.prototype.setRoomReadMarkers = async function(
* recent one preceding this timestamp if available, or failing that the next
* most recent available preview.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Object of OG metadata.
* @return {Promise} Resolves: Object of OG metadata.
* @return {module:http-api.MatrixError} Rejects: with an error response.
* May return synthesized attributes if the URL lacked OG meta.
*/
@@ -2826,7 +2851,7 @@ MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
* @param {boolean} isTyping
* @param {Number} timeoutMs
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.sendTyping = function(roomId, isTyping, timeoutMs, callback) {
@@ -2934,7 +2959,7 @@ MatrixClient.prototype.getRoomUpgradeHistory = function(roomId, verifyLinks=fals
* @param {string} roomId
* @param {string} userId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.invite = function(roomId, userId, callback) {
@@ -2947,7 +2972,7 @@ MatrixClient.prototype.invite = function(roomId, userId, callback) {
* @param {string} roomId The room to invite the user to.
* @param {string} email The email address to invite.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.inviteByEmail = function(roomId, email, callback) {
@@ -2962,7 +2987,7 @@ MatrixClient.prototype.inviteByEmail = function(roomId, email, callback) {
* @param {string} medium The medium to invite the user e.g. "email".
* @param {string} address The address for the specified medium.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.inviteByThreePid = async function(
@@ -3006,7 +3031,7 @@ MatrixClient.prototype.inviteByThreePid = async function(
/**
* @param {string} roomId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.leave = function(roomId, callback) {
@@ -3022,7 +3047,7 @@ MatrixClient.prototype.leave = function(roomId, callback) {
* @param {string} roomId The room ID to start leaving at
* @param {boolean} includeFuture If true, the whole chain (past and future) of
* upgraded rooms will be left.
* @return {module:client.Promise} Resolves when completed with an object keyed
* @return {Promise} Resolves when completed with an object keyed
* by room ID and value of the error encountered when leaving or null.
*/
MatrixClient.prototype.leaveRoomChain = function(roomId, includeFuture=true) {
@@ -3063,7 +3088,7 @@ MatrixClient.prototype.leaveRoomChain = function(roomId, includeFuture=true) {
* @param {string} userId
* @param {string} reason Optional.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.ban = function(roomId, userId, reason, callback) {
@@ -3076,7 +3101,7 @@ MatrixClient.prototype.ban = function(roomId, userId, reason, callback) {
* @param {boolean} deleteRoom True to delete the room from the store on success.
* Default: true.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.forget = function(roomId, deleteRoom, callback) {
@@ -3100,7 +3125,7 @@ MatrixClient.prototype.forget = function(roomId, deleteRoom, callback) {
* @param {string} roomId
* @param {string} userId
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Object (currently empty)
* @return {Promise} Resolves: Object (currently empty)
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.unban = function(roomId, userId, callback) {
@@ -3125,7 +3150,7 @@ MatrixClient.prototype.unban = function(roomId, userId, callback) {
* @param {string} userId
* @param {string} reason Optional.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.kick = function(roomId, userId, reason, callback) {
@@ -3142,7 +3167,7 @@ MatrixClient.prototype.kick = function(roomId, userId, reason, callback) {
* @param {string} membershipValue
* @param {string} reason
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
function _setMembershipState(client, roomId, userId, membershipValue, reason,
@@ -3170,7 +3195,7 @@ function _setMembershipState(client, roomId, userId, membershipValue, reason,
* @param {string} membership
* @param {string} reason
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
function _membershipChange(client, roomId, userId, membership, reason, callback) {
@@ -3210,7 +3235,7 @@ MatrixClient.prototype.getPushActionsForEvent = function(event) {
* @param {string} info The kind of info to set (e.g. 'avatar_url')
* @param {Object} data The JSON object to set.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setProfileInfo = function(info, data, callback) {
@@ -3226,7 +3251,7 @@ MatrixClient.prototype.setProfileInfo = function(info, data, callback) {
/**
* @param {string} name
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setDisplayName = function(name, callback) {
@@ -3238,7 +3263,7 @@ MatrixClient.prototype.setDisplayName = function(name, callback) {
/**
* @param {string} url
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setAvatarUrl = function(url, callback) {
@@ -3271,7 +3296,7 @@ MatrixClient.prototype.mxcUrlToHttp =
* Sets a new status message for the user. The message may be null/falsey
* to clear the message.
* @param {string} newMessage The new message to set.
* @return {module:client.Promise} Resolves: to nothing
* @return {Promise} Resolves: to nothing
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) {
@@ -3298,7 +3323,7 @@ MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) {
* @param {string} opts.presence One of "online", "offline" or "unavailable"
* @param {string} opts.status_msg The status message to attach.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws If 'presence' isn't a valid presence enum value.
*/
@@ -3330,7 +3355,7 @@ function _presenceList(callback, client, opts, method) {
/**
* Retrieve current user presence list.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getPresenceList = function(callback) {
@@ -3341,7 +3366,7 @@ MatrixClient.prototype.getPresenceList = function(callback) {
* Add users to the current user presence list.
* @param {module:client.callback} callback Optional.
* @param {string[]} userIds
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.inviteToPresenceList = function(callback, userIds) {
@@ -3353,7 +3378,7 @@ MatrixClient.prototype.inviteToPresenceList = function(callback, userIds) {
* Drop users from the current user presence list.
* @param {module:client.callback} callback Optional.
* @param {string[]} userIds
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
**/
MatrixClient.prototype.dropFromPresenceList = function(callback, userIds) {
@@ -3373,7 +3398,7 @@ MatrixClient.prototype.dropFromPresenceList = function(callback, userIds) {
* @param {Integer} limit Optional. The maximum number of previous events to
* pull in. Default: 30.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: Room. If you are at the beginning
* @return {Promise} Resolves: Room. If you are at the beginning
* of the timeline, <code>Room.oldState.paginationToken</code> will be
* <code>null</code>.
* @return {module:http-api.MatrixError} Rejects: with an error response.
@@ -3456,7 +3481,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
* @param {EventTimelineSet} timelineSet The timelineSet to look for the event in
* @param {string} eventId The ID of the event to look for
*
* @return {module:client.Promise} Resolves:
* @return {Promise} Resolves:
* {@link module:models/event-timeline~EventTimeline} including the given
* event
*/
@@ -3586,7 +3611,7 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) {
* false to go forwards
* @param {number} [opts.limit = 30] number of events to request
*
* @return {module:client.Promise} Resolves to a boolean: false if there are no
* @return {Promise} Resolves to a boolean: false if there are no
* events and we reached either end of the timeline; else true.
*/
MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
@@ -3738,7 +3763,7 @@ MatrixClient.prototype.resetNotifTimelineSet = function() {
* Peek into a room and receive updates about the room. This only works if the
* history visibility for the room is world_readable.
* @param {String} roomId The room to attempt to peek into.
* @return {module:client.Promise} Resolves: Room object
* @return {Promise} Resolves: Room object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.peekInRoom = function(roomId) {
@@ -3769,7 +3794,7 @@ MatrixClient.prototype.stopPeeking = function() {
* @param {boolean} opts.allowRead True to set history visibility to
* be world_readable. This gives guests read access *from this point forward*.
* If false or not given, history visibility is not modified.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
@@ -3802,7 +3827,7 @@ MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
* @return {Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestRegisterEmailToken = function(email, clientSecret,
sendAttempt, nextLink) {
@@ -3829,7 +3854,7 @@ MatrixClient.prototype.requestRegisterEmailToken = function(email, clientSecret,
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
* @return {Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestRegisterMsisdnToken = function(phoneCountry, phoneNumber,
clientSecret, sendAttempt, nextLink) {
@@ -3860,7 +3885,7 @@ MatrixClient.prototype.requestRegisterMsisdnToken = function(phoneCountry, phone
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
* @return {Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestAdd3pidEmailToken = function(email, clientSecret,
sendAttempt, nextLink) {
@@ -3887,7 +3912,7 @@ MatrixClient.prototype.requestAdd3pidEmailToken = function(email, clientSecret,
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
* @return {Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestAdd3pidMsisdnToken = function(phoneCountry, phoneNumber,
clientSecret, sendAttempt, nextLink) {
@@ -3920,7 +3945,7 @@ MatrixClient.prototype.requestAdd3pidMsisdnToken = function(phoneCountry, phoneN
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @param {module:client.callback} callback Optional. As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
* @return {Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestPasswordEmailToken = function(email, clientSecret,
sendAttempt, nextLink) {
@@ -3946,7 +3971,7 @@ MatrixClient.prototype.requestPasswordEmailToken = function(email, clientSecret,
* @param {string} clientSecret As requestEmailToken
* @param {number} sendAttempt As requestEmailToken
* @param {string} nextLink As requestEmailToken
* @return {module:client.Promise} Resolves: As requestEmailToken
* @return {Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype.requestPasswordMsisdnToken = function(phoneCountry, phoneNumber,
clientSecret, sendAttempt, nextLink) {
@@ -3968,7 +3993,7 @@ MatrixClient.prototype.requestPasswordMsisdnToken = function(phoneCountry, phone
*
* @param {string} endpoint The endpoint to send the request to
* @param {object} params Parameters for the POST request
* @return {module:client.Promise} Resolves: As requestEmailToken
* @return {Promise} Resolves: As requestEmailToken
*/
MatrixClient.prototype._requestTokenFromEndpoint = async function(endpoint, params) {
const postParams = Object.assign({}, params);
@@ -4033,7 +4058,7 @@ MatrixClient.prototype.getRoomPushRule = function(scope, roomId) {
* @param {string} scope "global" or device-specific.
* @param {string} roomId the id of the room.
* @param {string} mute the mute state.
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
@@ -4113,7 +4138,7 @@ MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
* @param {string=} opts.keys The keys to search on. Defaults to all keys. One
* of "content.body", "content.name", "content.topic".
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.searchMessageText = function(opts, callback) {
@@ -4151,7 +4176,7 @@ MatrixClient.prototype.searchMessageText = function(opts, callback) {
* @param {Object} opts
* @param {string} opts.term the term to search for
* @param {Object} opts.filter a JSON filter object to pass in the request
* @return {module:client.Promise} Resolves: result object
* @return {Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.searchRoomEvents = function(opts) {
@@ -4187,7 +4212,7 @@ MatrixClient.prototype.searchRoomEvents = function(opts) {
* Take a result from an earlier searchRoomEvents call, and backfill results.
*
* @param {object} searchResults the results object to be updated
* @return {module:client.Promise} Resolves: updated result object
* @return {Promise} Resolves: updated result object
* @return {Error} Rejects: with an error response.
*/
MatrixClient.prototype.backPaginateRoomEventsSearch = function(searchResults) {
@@ -4257,7 +4282,7 @@ MatrixClient.prototype._processRoomEventsSearch = function(searchResults, respon
/**
* Populate the store with rooms the user has left.
* @return {module:client.Promise} Resolves: TODO - Resolved when the rooms have
* @return {Promise} Resolves: TODO - Resolved when the rooms have
* been added to the data store.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
@@ -4316,7 +4341,7 @@ MatrixClient.prototype.createFilter = function(content) {
* @param {string} filterId The filter ID to retrieve
* @param {boolean} allowCached True to allow cached filters to be returned.
* Default: True.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
@@ -4415,7 +4440,7 @@ MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
* Gets a bearer token from the Home Server that the user can
* present to a third party in order to prove their ownership
* of the Matrix account they are logged into.
* @return {module:client.Promise} Resolves: Token object
* @return {Promise} Resolves: Token object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getOpenIdToken = function() {
@@ -4434,7 +4459,7 @@ MatrixClient.prototype.getOpenIdToken = function() {
/**
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.turnServer = function(callback) {
@@ -4691,6 +4716,7 @@ MatrixClient.prototype.isVersionSupported = async function(version) {
*/
MatrixClient.prototype.doesServerSupportLazyLoading = async function() {
const response = await this.getVersions();
if (!response) return false;
const versions = response["versions"];
const unstableFeatures = response["unstable_features"];
@@ -4706,6 +4732,7 @@ MatrixClient.prototype.doesServerSupportLazyLoading = async function() {
*/
MatrixClient.prototype.doesServerRequireIdServerParam = async function() {
const response = await this.getVersions();
if (!response) return true;
const versions = response["versions"];
@@ -4715,6 +4742,7 @@ MatrixClient.prototype.doesServerRequireIdServerParam = async function() {
}
const unstableFeatures = response["unstable_features"];
if (!unstableFeatures) return true;
if (unstableFeatures["m.require_identity_server"] === undefined) {
return true;
} else {
@@ -4730,10 +4758,10 @@ MatrixClient.prototype.doesServerRequireIdServerParam = async function() {
*/
MatrixClient.prototype.doesServerAcceptIdentityAccessToken = async function() {
const response = await this.getVersions();
if (!response) return false;
const versions = response["versions"];
const unstableFeatures = response["unstable_features"];
return (versions && versions.includes("r0.6.0"))
|| (unstableFeatures && unstableFeatures["m.id_access_token"]);
};
@@ -4746,6 +4774,7 @@ MatrixClient.prototype.doesServerAcceptIdentityAccessToken = async function() {
*/
MatrixClient.prototype.doesServerSupportSeparateAddAndBind = async function() {
const response = await this.getVersions();
if (!response) return false;
const versions = response["versions"];
const unstableFeatures = response["unstable_features"];
@@ -5530,14 +5559,3 @@ MatrixClient.prototype.generateClientSecret = function() {
* @param {Object} err The error value, the "rejected" value or null.
* @param {Object} data The data returned, the "resolved" value.
*/
/**
* {@link https://github.com/kriskowal/q|A promise implementation (Q)}. Functions
* which return this will specify 2 return arguments. These arguments map to the
* "onFulfilled" and "onRejected" values of the Promise.
* @typedef {Object} Promise
* @static
* @property {Function} then promise.then(onFulfilled, onRejected, onProgress)
* @property {Function} catch promise.catch(onRejected)
* @property {Function} finally promise.finally(callback)
*/
+2
View File
@@ -348,6 +348,7 @@ export class CrossSigningInfo extends EventEmitter {
async signUser(key) {
if (!this.keys.user_signing) {
logger.info("No user signing key: not signing user");
return;
}
return this.signObject(key.keys.master, "user_signing");
@@ -360,6 +361,7 @@ export class CrossSigningInfo extends EventEmitter {
);
}
if (!this.keys.self_signing) {
logger.info("No self signing key: not signing device");
return;
}
return this.signObject(
+2 -2
View File
@@ -594,7 +594,7 @@ export class DeviceList extends EventEmitter {
*
* @param {String[]} users list of userIds
*
* @return {module:client.Promise} resolves when all the users listed have
* @return {Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
@@ -699,7 +699,7 @@ class DeviceListUpdateSerialiser {
* @param {String} syncToken sync token to pass in the query request, to
* help the HS give the most recent results
*
* @return {module:client.Promise} resolves when all the users listed have
* @return {Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
+131 -14
View File
@@ -111,16 +111,52 @@ export function OlmDevice(cryptoStore) {
* Initialise the OlmAccount. This must be called before any other operations
* on the OlmDevice.
*
* Data from an exported Olm device can be provided
* in order to re-create this device.
*
* Attempts to load the OlmAccount from the crypto store, or creates one if none is
* found.
*
* Reads the device keys from the OlmAccount object.
*
* @param {object} opts
* @param {object} opts.fromExportedDevice (Optional) data from exported device
* that must be re-created.
* If present, opts.pickleKey is ignored
* (exported data already provides a pickle key)
* @param {object} opts.pickleKey (Optional) pickle key to set instead of default one
*/
OlmDevice.prototype.init = async function() {
OlmDevice.prototype.init = async function(opts = {}) {
let e2eKeys;
const account = new global.Olm.Account();
const { pickleKey, fromExportedDevice } = opts;
try {
await _initialiseAccount(this._cryptoStore, this._pickleKey, account);
if (fromExportedDevice) {
if (pickleKey) {
console.warn(
'ignoring opts.pickleKey'
+ ' because opts.fromExportedDevice is present.',
);
}
this._pickleKey = fromExportedDevice.pickleKey;
await _initialiseFromExportedDevice(
fromExportedDevice,
this._cryptoStore,
this._pickleKey,
account,
);
} else {
if (pickleKey) {
this._pickleKey = pickleKey;
}
await _initialiseAccount(
this._cryptoStore,
this._pickleKey,
account,
);
}
e2eKeys = JSON.parse(account.identity_keys());
this._maxOneTimeKeys = account.max_number_of_one_time_keys();
@@ -132,18 +168,67 @@ OlmDevice.prototype.init = async function() {
this.deviceEd25519Key = e2eKeys.ed25519;
};
async function _initialiseAccount(cryptoStore, pickleKey, account) {
await cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
cryptoStore.getAccount(txn, (pickledAccount) => {
if (pickledAccount !== null) {
account.unpickle(pickleKey, pickledAccount);
} else {
account.create();
pickledAccount = account.pickle(pickleKey);
cryptoStore.storeAccount(txn, pickledAccount);
}
});
/**
* Populates the crypto store using data that was exported from an existing device.
* Note that for now only the account and sessions stores are populated;
* Other stores will be as with a new device.
*
* @param {Object} exportedData Data exported from another device
* through the export method.
* @param {module:crypto/store/base~CryptoStore} cryptoStore storage for the crypto layer
* @param {string} pickleKey the key that was used to pickle the exported data
* @param {Olm.Account} account an olm account to initialize
*/
async function _initialiseFromExportedDevice(
exportedData,
cryptoStore,
pickleKey,
account,
) {
await cryptoStore.doTxn(
'readwrite',
[
IndexedDBCryptoStore.STORE_ACCOUNT,
IndexedDBCryptoStore.STORE_SESSIONS,
],
(txn) => {
cryptoStore.storeAccount(txn, exportedData.pickledAccount);
exportedData.sessions.forEach((session) => {
const {
deviceKey,
sessionId,
} = session;
const sessionInfo = {
session: session.session,
lastReceivedMessageTs: session.lastReceivedMessageTs,
};
cryptoStore.storeEndToEndSession(
deviceKey,
sessionId,
sessionInfo,
txn,
);
});
});
account.unpickle(pickleKey, exportedData.pickledAccount);
}
async function _initialiseAccount(cryptoStore, pickleKey, account) {
await cryptoStore.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
cryptoStore.getAccount(txn, (pickledAccount) => {
if (pickledAccount !== null) {
account.unpickle(pickleKey, pickledAccount);
} else {
account.create();
pickledAccount = account.pickle(pickleKey);
cryptoStore.storeAccount(txn, pickledAccount);
}
});
},
);
}
/**
@@ -191,6 +276,38 @@ OlmDevice.prototype._storeAccount = function(txn, account) {
this._cryptoStore.storeAccount(txn, account.pickle(this._pickleKey));
};
/**
* Export data for re-creating the Olm device later.
* TODO export data other than just account and (P2P) sessions.
*
* @return {Promise<object>} The exported data
*/
OlmDevice.prototype.export = async function() {
const result = {
pickleKey: this._pickleKey,
};
await this._cryptoStore.doTxn(
'readonly',
[
IndexedDBCryptoStore.STORE_ACCOUNT,
IndexedDBCryptoStore.STORE_SESSIONS,
],
(txn) => {
this._cryptoStore.getAccount(txn, (pickledAccount) => {
result.pickledAccount = pickledAccount;
});
result.sessions = [];
// Note that the pickledSession object we get in the callback
// is not exactly the same thing you get in method _getSession
// see documentation of IndexedDBCryptoStore.getAllEndToEndSessions
this._cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => {
result.sessions.push(pickledSession);
});
},
);
return result;
};
/**
* extract an OlmSession from the session store and call the given function
* The session is useable only within the callback passed to this
@@ -1110,7 +1227,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {sring} sessionId session identifier
* @param {string} sessionId session identifier
*
* @returns {Promise<boolean>} true if we have the keys to this session
*/
+59 -12
View File
@@ -253,12 +253,41 @@ export class SecretStorage extends EventEmitter {
*/
storePassthrough(name, keyId) {
return this._baseApis.setAccountData(name, {
[keyId]: {
passthrough: true,
encrypted: {
[keyId]: {
passthrough: true,
},
},
});
}
/**
* Temporary method to fix up existing accounts where secrets
* are incorrectly stored without the 'encrypted' level
*
* @param {string} name The name of the secret
* @param {object} secretInfo The account data object
* @returns {object} The fixed object or null if no fix was performed
*/
async _fixupStoredSecret(name, secretInfo) {
// We assume the secret was only stored passthrough for 1
// key - this was all the broken code supported.
const keys = Object.keys(secretInfo);
if (
keys.length === 1 && keys[0] !== 'encrypted' &&
secretInfo[keys[0]].passthrough
) {
const hasKey = await this.hasKey(keys[0]);
if (hasKey) {
console.log("Fixing up passthrough secret: " + name);
await this.storePassthrough(name, keys[0]);
const newData = await this._baseApis.getAccountDataFromServer(name);
return newData;
}
}
return null;
}
/**
* Get a secret from storage.
*
@@ -267,12 +296,16 @@ export class SecretStorage extends EventEmitter {
* @return {string} the contents of the secret
*/
async get(name) {
const secretInfo = await this._baseApis.getAccountDataFromServer(name);
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo) {
return;
}
if (!secretInfo.encrypted) {
throw new Error("Content is not encrypted!");
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
throw new Error("Content is not encrypted!");
}
}
// get possible keys to decrypt
@@ -285,8 +318,12 @@ export class SecretStorage extends EventEmitter {
const encInfo = secretInfo.encrypted[keyId];
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
&& encInfo.ephemeral) {
if (
keyInfo.pubkey && (
(encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) ||
encInfo.passthrough
)
) {
keys[keyId] = keyInfo;
}
break;
@@ -299,7 +336,7 @@ export class SecretStorage extends EventEmitter {
let decryption;
try {
// fetch private key from app
[keyId, decryption] = await this._getSecretStorageKey(keys);
[keyId, decryption] = await this._getSecretStorageKey(keys, name);
const encInfo = secretInfo.encrypted[keyId];
@@ -329,9 +366,14 @@ export class SecretStorage extends EventEmitter {
*/
async isStored(name, checkKey) {
// check if secret exists
const secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo || !secretInfo.encrypted) {
return false;
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo) return false;
if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
return false;
}
}
if (checkKey === undefined) checkKey = true;
@@ -352,6 +394,11 @@ export class SecretStorage extends EventEmitter {
this._crossSigningInfo.userId,
);
}
// 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 true;
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
@@ -540,12 +587,12 @@ export class SecretStorage extends EventEmitter {
}
}
async _getSecretStorageKey(keys) {
async _getSecretStorageKey(keys, name) {
if (!this._cryptoCallbacks.getSecretStorageKey) {
throw new Error("No getSecretStorageKey callback supplied");
}
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys });
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys }, name);
if (!returned) {
throw new Error("getSecretStorageKey callback returned falsey");
+1 -1
View File
@@ -70,7 +70,7 @@ export class EncryptionAlgorithm {
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
* @return {Promise} Promise which resolves to the new event body
*/
/**
+5 -5
View File
@@ -186,7 +186,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
* @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
* @return {Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = async function(
@@ -497,7 +497,7 @@ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
*
* @param {object} payload fields to include in the encrypted payload
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* @return {Promise} Promise which resolves once the key sharing
* for the given userDeviceMap is generated and has been sent.
*/
MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
@@ -558,7 +558,7 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
*
* @param {object} payload fields to include in the notification payload
*
* @return {module:client.Promise} Promise which resolves once the notifications
* @return {Promise} Promise which resolves once the notifications
* for the given userDeviceMap is generated and has been sent.
*/
MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
@@ -779,7 +779,7 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
* @param {string} eventType
* @param {object} content plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
* @return {Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
const self = this;
@@ -866,7 +866,7 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to an array whose
* @return {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
+2 -2
View File
@@ -54,7 +54,7 @@ utils.inherits(OlmEncryption, EncryptionAlgorithm);
* @private
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
* @return {Promise} Promise which resolves when setup is complete
*/
OlmEncryption.prototype._ensureSession = function(roomMembers) {
if (this._prepPromise) {
@@ -85,7 +85,7 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
* @param {string} eventType
* @param {object} content plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
* @return {Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
// pick the list of recipients based on the membership list.
+179 -148
View File
@@ -41,29 +41,38 @@ import {
import {SECRET_STORAGE_ALGORITHM_V1, SecretStorage} from './SecretStorage';
import {OutgoingRoomKeyRequestManager} from './OutgoingRoomKeyRequestManager';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import {ScanQRCode, ShowQRCode} from './verification/QRCode';
import {
ReciprocateQRCode,
SCAN_QR_CODE_METHOD,
SHOW_QR_CODE_METHOD,
} from './verification/QRCode';
import {SAS} from './verification/SAS';
import {keyFromPassphrase} from './key_passphrase';
import {encodeRecoveryKey} from './recoverykey';
import {VerificationRequest} from "./verification/request/VerificationRequest";
import {InRoomChannel} from "./verification/request/InRoomChannel";
import {ToDeviceChannel} from "./verification/request/ToDeviceChannel";
import {InRoomChannel, InRoomRequests} from "./verification/request/InRoomChannel";
import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDeviceChannel";
import * as httpApi from "../http-api";
import {IllegalMethod} from "./verification/IllegalMethod";
const DeviceVerification = DeviceInfo.DeviceVerification;
const defaultVerificationMethods = {
[ScanQRCode.NAME]: ScanQRCode,
[ShowQRCode.NAME]: ShowQRCode,
[ReciprocateQRCode.NAME]: ReciprocateQRCode,
[SAS.NAME]: SAS,
// These two can't be used for actual verification, but we do
// need to be able to define them here for the verification flows
// to start.
[SHOW_QR_CODE_METHOD]: IllegalMethod,
[SCAN_QR_CODE_METHOD]: IllegalMethod,
};
/**
* verification method names
*/
export const verificationMethods = {
QR_CODE_SCAN: ScanQRCode.NAME,
QR_CODE_SHOW: ShowQRCode.NAME,
RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME,
SAS: SAS.NAME,
};
@@ -117,8 +126,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
this._clientStore = clientStore;
this._cryptoStore = cryptoStore;
this._roomList = roomList;
this._verificationMethods = new Map();
if (verificationMethods) {
this._verificationMethods = new Map();
for (const method of verificationMethods) {
if (typeof method === "string") {
if (defaultVerificationMethods[method]) {
@@ -132,10 +141,13 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
method.NAME,
method,
);
} else {
console.warn(`Excluding unknown verification method ${method}`);
}
}
} else {
this._verificationMethods = defaultVerificationMethods;
}
// track whether this device's megolm keys are being backed up incrementally
// to the server or not.
// XXX: this should probably have a single source of truth from OlmAccount
@@ -204,8 +216,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
// }
this._lastNewSessionForced = {};
this._toDeviceVerificationRequests = new Map();
this._inRoomVerificationRequests = new Map();
this._toDeviceVerificationRequests = new ToDeviceRequests();
this._inRoomVerificationRequests = new InRoomRequests();
const cryptoCallbacks = this._baseApis._cryptoCallbacks || {};
@@ -228,12 +240,24 @@ utils.inherits(Crypto, EventEmitter);
* Initialise the crypto module so that it is ready for use
*
* Returns a promise which resolves once the crypto module is ready for use.
*
* @param {Object} opts keyword arguments.
* @param {string} opts.exportedOlmDevice (Optional) data from exported device
* that must be re-created.
*/
Crypto.prototype.init = async function() {
Crypto.prototype.init = async function(opts) {
const {
exportedOlmDevice,
} = opts || {};
logger.log("Crypto: initialising Olm...");
await global.Olm.init();
logger.log("Crypto: initialising Olm device...");
await this._olmDevice.init();
logger.log(
exportedOlmDevice
? "Crypto: initialising Olm device from exported device..."
: "Crypto: initialising Olm device...",
);
await this._olmDevice.init({ fromExportedDevice: exportedOlmDevice });
logger.log("Crypto: loading device list...");
await this._deviceList.load();
@@ -336,6 +360,10 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
* called to await a secret storage key creation flow.
* @param {object} [opts.keyBackupInfo] The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
* @param {bool} [opts.setupNewKeyBackup] If true, a new key backup version will be
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
* @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist.
* Returns:
* {Promise} A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
@@ -344,6 +372,8 @@ Crypto.prototype.bootstrapSecretStorage = async function({
authUploadDeviceSigningKeys,
createSecretStorageKey = async () => { },
keyBackupInfo,
setupNewKeyBackup,
setupNewSecretStorage,
} = {}) {
logger.log("Bootstrapping Secure Secret Storage");
@@ -351,7 +381,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
// key with the cross-signing master key. The cross-signing master key is also used
// to verify the signature on the SSSS default key when adding secrets, so we
// effectively need it for both reading and writing secrets.
let crossSigningPrivateKeys = {};
const crossSigningPrivateKeys = {};
// If we happen to reset cross-signing keys here, then we want access to the
// cross-signing private keys, but only for the scope of this method, so we
@@ -359,7 +389,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks);
try {
const inStorage =
const inStorage = !setupNewSecretStorage &&
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
if (!this._crossSigningInfo.getId() || !inStorage) {
logger.log(
@@ -375,7 +405,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
"creating new keys",
);
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
keys => crossSigningPrivateKeys = keys;
keys => Object.assign(crossSigningPrivateKeys, keys);
this._baseApis._cryptoCallbacks.getCrossSigningKey =
name => crossSigningPrivateKeys[name];
await this.resetCrossSigningKeys(
@@ -389,7 +419,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
// Check if Secure Secret Storage has a default key. If we don't have one, create
// the default key (which will also be signed by the cross-signing master key).
if (!await this.hasSecretStorageKey()) {
if (setupNewSecretStorage || !await this.hasSecretStorageKey()) {
let newKeyId;
if (keyBackupInfo) {
logger.log("Secret storage default key not found, using key backup key");
@@ -467,8 +497,24 @@ Crypto.prototype.bootstrapSecretStorage = async function({
);
}
}
if (setupNewKeyBackup && !keyBackupInfo) {
const info = await this._baseApis.prepareKeyBackupVersion(
null /* random key */,
{ secureSecretStorage: true },
);
await this._baseApis.createKeyBackupVersion(info);
}
} finally {
this._baseApis._cryptoCallbacks = appCallbacks;
// Restore the original callbacks. NB. we must do this by manipulating
// the same object since the CrossSigning class has a reference to the
// object, so if we assign the object here then our callbacks will change
// but the instances of the CrossSigning class will be left with our
// random, otherwise dead closures.
for (const cb of Object.keys(this._baseApis._cryptoCallbacks)) {
delete this._baseApis._cryptoCallbacks[cb];
}
Object.assign(this._baseApis._cryptoCallbacks, appCallbacks);
}
logger.log("Secure Secret Storage ready");
@@ -811,6 +857,10 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
Crypto.prototype.checkOwnCrossSigningTrust = async function() {
const userId = this._userId;
// Before proceeding, ensure our cross-signing public keys have been
// downloaded via the device list.
await this.downloadKeys([this._userId]);
// If we see an update to our own master key, check it against the master
// key we have and, if it matches, mark it as verified
@@ -850,13 +900,7 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
const oldUserSigningId = this._crossSigningInfo.getId("user_signing");
// Update the version of our keys in our cross-signing object and the local store
this._crossSigningInfo.setKeys(newCrossSigning.keys);
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
},
);
this._storeTrustedSelfKeys(newCrossSigning.keys);
const keySignatures = {};
@@ -896,6 +940,21 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
// the backup with the new key (if not already signed)?
};
/**
* Store a set of keys as our own, trusted, cross-signing keys.
*
* @param {object} keys The new trusted set of keys
*/
Crypto.prototype._storeTrustedSelfKeys = async function(keys) {
this._crossSigningInfo.setKeys(keys);
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
},
);
};
/**
* Check if the master key is signed by a verified device, and if so, prompt
* the application to mark it as verified.
@@ -1147,17 +1206,13 @@ Crypto.prototype.registerEventHandlers = function(eventEmitter) {
}
});
eventEmitter.on("toDeviceEvent", function(event) {
crypto._onToDeviceEvent(event);
});
eventEmitter.on("toDeviceEvent", crypto._onToDeviceEvent.bind(crypto));
eventEmitter.on("Room.timeline", function(event) {
crypto._onTimelineEvent(event);
});
const timelineHandler = crypto._onTimelineEvent.bind(crypto);
eventEmitter.on("Event.decrypted", function(event) {
crypto._onTimelineEvent(event);
});
eventEmitter.on("Room.timeline", timelineHandler);
eventEmitter.on("Event.decrypted", timelineHandler);
};
@@ -1248,10 +1303,6 @@ Crypto.prototype.uploadDeviceKeys = function() {
return crypto._signObject(deviceKeys).then(() => {
return crypto._baseApis.uploadKeysRequest({
device_keys: deviceKeys,
}, {
// for now, we set the device id explicitly, as we may not be using the
// same one as used in login.
device_id: deviceId,
});
});
};
@@ -1348,9 +1399,7 @@ function _maybeUploadOneTimeKeys(crypto) {
return Promise.resolve(crypto._oneTimeKeyCount);
}
// ask the server how many keys we have
return crypto._baseApis.uploadKeysRequest({}, {
device_id: crypto._deviceId,
}).then((res) => {
return crypto._baseApis.uploadKeysRequest({}).then((res) => {
return res.one_time_key_counts.signed_curve25519 || 0;
});
}).then((keyCount) => {
@@ -1390,10 +1439,6 @@ async function _uploadOneTimeKeys(crypto) {
const res = await crypto._baseApis.uploadKeysRequest({
one_time_keys: oneTimeJson,
}, {
// for now, we set the device id explicitly, as we may not be using the
// same one as used in login.
device_id: crypto._deviceId,
});
await crypto._olmDevice.markKeysAsPublished();
@@ -1492,17 +1537,32 @@ Crypto.prototype.setDeviceVerification = async function(
if (!verified) {
throw new Error("Cannot set a cross-signing key as unverified");
}
const device = await this._crossSigningInfo.signUser(xsk);
if (device) {
await this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
// This will emit events when it comes back down the sync
// (we could do local echo to speed things up)
if (!this._crossSigningInfo.getId() && userId === this._crossSigningInfo.userId) {
this._storeTrustedSelfKeys(xsk.keys);
}
// Now sign the master key with our user signing key (unless it's ourself)
if (userId !== this._userId) {
logger.info(
"Master key " + xsk.getId() + " for " + userId +
" marked verified. Signing...",
);
const device = await this._crossSigningInfo.signUser(xsk);
if (device) {
logger.info("Uploading signature for " + userId + "...");
await this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
// This will emit events when it comes back down the sync
// (we could do local echo to speed things up)
}
return device;
} else {
return xsk;
}
return device;
}
const devices = this._deviceList.getRawStoredDevicesForUser(userId);
@@ -1539,10 +1599,12 @@ Crypto.prototype.setDeviceVerification = async function(
// do cross-signing
if (verified && userId === this._userId) {
logger.info("Own device " + deviceId + " marked verified: signing");
const device = await this._crossSigningInfo.signDevice(
userId, DeviceInfo.fromStorage(dev, deviceId),
);
if (device) {
logger.info("Uploading signature for " + deviceId);
await this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
@@ -1557,98 +1619,65 @@ Crypto.prototype.setDeviceVerification = async function(
return deviceObj;
};
Crypto.prototype.requestVerificationDM = async function(userId, roomId, methods) {
Crypto.prototype.requestVerificationDM = function(userId, roomId) {
const channel = new InRoomChannel(this._baseApis, roomId, userId);
const request = await this._requestVerificationWithChannel(
return this._requestVerificationWithChannel(
userId,
methods,
channel,
this._inRoomVerificationRequests,
);
return await request.waitForVerifier();
};
Crypto.prototype.acceptVerificationDM = function(event, method) {
if(!InRoomChannel.validateEvent(event, this._baseApis)) {
return;
}
const sender = event.getSender();
const requestsByTxnId = this._inRoomVerificationRequests.get(sender);
if (!requestsByTxnId) {
return;
}
const transactionId = InRoomChannel.getTransactionId(event);
const request = requestsByTxnId.get(transactionId);
if (!request) {
return;
}
return request.beginKeyVerification(method);
};
Crypto.prototype.requestVerification = async function(userId, methods, devices) {
Crypto.prototype.requestVerification = function(userId, devices) {
if (!devices) {
devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(userId));
}
const channel = new ToDeviceChannel(this._baseApis, userId, devices);
const request = await this._requestVerificationWithChannel(
return this._requestVerificationWithChannel(
userId,
methods,
channel,
this._toDeviceVerificationRequests,
);
return await request.waitForVerifier();
};
Crypto.prototype._requestVerificationWithChannel = async function(
userId, methods, channel, requestsMap,
userId, channel, requestsMap,
) {
if (!methods) {
// .keys() returns an iterator, so we need to explicitly turn it into an array
methods = [...this._verificationMethods.keys()];
}
// TODO: filter by given methods
const request = new VerificationRequest(
channel, this._verificationMethods, userId, this._baseApis);
let request = new VerificationRequest(
channel, this._verificationMethods, this._baseApis);
await request.sendRequest();
let requestsByTxnId = requestsMap.get(userId);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
requestsMap.set(userId, requestsByTxnId);
// don't replace the request created by a racing remote echo
const racingRequest = requestsMap.getRequestByChannel(channel);
if (racingRequest) {
request = racingRequest;
} else {
logger.log(`Crypto: adding new request to ` +
`requestsByTxnId with id ${channel.transactionId} ${channel.roomId}`);
requestsMap.setRequestByChannel(channel, request);
}
// TODO: we're only adding the request to the map once it has been sent
// but if the other party is really fast they could potentially respond to the
// request before the server tells us the event got sent, and we would probably
// create a new request object
requestsByTxnId.set(channel.transactionId, request);
return request;
};
Crypto.prototype.beginKeyVerification = function(
method, userId, deviceId, transactionId = null,
) {
let requestsByTxnId = this._toDeviceVerificationRequests.get(userId);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._toDeviceVerificationRequests.set(userId, requestsByTxnId);
}
let request;
if (transactionId) {
request = requestsByTxnId.get(transactionId);
request = this._toDeviceVerificationRequests.getRequestBySenderAndTxnId(
userId, transactionId);
if (!request) {
throw new Error(
`No request found for user ${userId} with ` +
`transactionId ${transactionId}`);
}
} else {
transactionId = ToDeviceChannel.makeTransactionId();
const channel = new ToDeviceChannel(
this._baseApis, userId, [deviceId], transactionId, deviceId);
request = new VerificationRequest(
channel, this._verificationMethods, userId, this._baseApis);
requestsByTxnId.set(transactionId, request);
}
if (!request) {
throw new Error(
`No request found for user ${userId} with transactionId ${transactionId}`);
channel, this._verificationMethods, this._baseApis);
this._toDeviceVerificationRequests.setRequestBySenderAndTxnId(
userId, transactionId, request);
}
return request.beginKeyVerification(method, {userId, deviceId});
};
@@ -1899,7 +1928,7 @@ Crypto.prototype.trackRoomDevices = function(roomId) {
*
* @param {string[]} users list of user ids
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* @return {Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
@@ -1962,7 +1991,7 @@ Crypto.prototype.exportRoomKeys = async function() {
* Import a list of room keys previously exported by exportRoomKeys
*
* @param {Object[]} keys a list of session export objects
* @return {module:client.Promise} a promise which resolves once the keys have been imported
* @return {Promise} a promise which resolves once the keys have been imported
*/
Crypto.prototype.importRoomKeys = function(keys) {
return Promise.all(keys.map((key) => {
@@ -2158,7 +2187,7 @@ Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
*
* @param {module:models/room} room destination room.
*
* @return {module:client.Promise?} Promise which resolves when the event has been
* @return {Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed
*/
/* eslint-enable valid-jsdoc */
@@ -2534,7 +2563,6 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
if (!ToDeviceChannel.validateEvent(event, this._baseApis)) {
return;
}
const transactionId = ToDeviceChannel.getTransactionId(event);
const createRequest = event => {
if (!ToDeviceChannel.canCreateRequest(ToDeviceChannel.getEventType(event))) {
return;
@@ -2551,10 +2579,13 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
[deviceId],
);
return new VerificationRequest(
channel, this._verificationMethods, userId, this._baseApis);
channel, this._verificationMethods, this._baseApis);
};
this._handleVerificationEvent(event, transactionId,
this._toDeviceVerificationRequests, createRequest);
this._handleVerificationEvent(
event,
this._toDeviceVerificationRequests,
createRequest,
);
};
/**
@@ -2562,65 +2593,65 @@ Crypto.prototype._onKeyVerificationMessage = function(event) {
*
* @private
* @param {module:models/event.MatrixEvent} event the timeline event
* @param {module:models/Room} room not used
* @param {bool} atStart not used
* @param {bool} removed not used
* @param {bool} data.liveEvent whether this is a live event
*/
Crypto.prototype._onTimelineEvent = function(event) {
Crypto.prototype._onTimelineEvent = function(
event, room, atStart, removed, {liveEvent} = {},
) {
if (!InRoomChannel.validateEvent(event, this._baseApis)) {
return;
}
const transactionId = InRoomChannel.getTransactionId(event);
const createRequest = event => {
if (!InRoomChannel.canCreateRequest(InRoomChannel.getEventType(event))) {
return;
}
const userId = event.getSender();
const channel = new InRoomChannel(
this._baseApis,
event.getRoomId(),
userId,
);
return new VerificationRequest(
channel, this._verificationMethods, userId, this._baseApis);
channel, this._verificationMethods, this._baseApis);
};
this._handleVerificationEvent(event, transactionId,
this._inRoomVerificationRequests, createRequest);
this._handleVerificationEvent(
event,
this._inRoomVerificationRequests,
createRequest,
liveEvent,
);
};
Crypto.prototype._handleVerificationEvent = async function(
event, transactionId, requestsMap, createRequest,
event, requestsMap, createRequest, isLiveEvent = true,
) {
const sender = event.getSender();
let requestsByTxnId = requestsMap.get(sender);
let request = requestsMap.getRequest(event);
let isNewRequest = false;
let request = requestsByTxnId && requestsByTxnId.get(transactionId);
if (!request) {
request = createRequest(event);
// a request could not be made from this event, so ignore event
if (!request) {
logger.log(`Crypto: could not find VerificationRequest for ` +
`${event.getType()}, and could not create one, so ignoring.`);
return;
}
isNewRequest = true;
if (!requestsByTxnId) {
requestsByTxnId = new Map();
requestsMap.set(sender, requestsByTxnId);
}
requestsByTxnId.set(transactionId, request);
requestsMap.setRequest(event, request);
}
event.setVerificationRequest(request);
try {
const hadVerifier = !!request.verifier;
await request.channel.handleEvent(event, request);
await request.channel.handleEvent(event, request, isLiveEvent);
// emit start event when verifier got set
if (!hadVerifier && request.verifier) {
this._baseApis.emit("crypto.verification.start", request.verifier);
}
} catch (err) {
console.error("error while handling verification event", event, err);
logger.error("error while handling verification event: " + err.message);
}
if (!request.pending) {
requestsByTxnId.delete(transactionId);
if (requestsByTxnId.size === 0) {
requestsMap.delete(sender);
}
} else if (isNewRequest && !request.initiatedByMe) {
const shouldEmit = isNewRequest &&
!request.initiatedByMe &&
!request.invalid && // check it has enough events to pass the UNSENT stage
!request.observeOnly;
if (shouldEmit) {
this._baseApis.emit("crypto.verification.request", request);
}
};
+2 -2
View File
@@ -123,10 +123,10 @@ export async function encryptMessageForDevice(
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
*
* @param {bolean} force If true, establish a new session even if one already exists.
* @param {boolean} force If true, establish a new session even if one already exists.
* Optional.
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* @return {Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
+9 -5
View File
@@ -263,11 +263,15 @@ export class MemoryCryptoStore {
}
getAllEndToEndSessions(txn, func) {
for (const deviceSessions of Object.values(this._sessions)) {
for (const sess of Object.values(deviceSessions)) {
func(sess);
}
}
Object.entries(this._sessions).forEach(([deviceKey, deviceSessions]) => {
Object.entries(deviceSessions).forEach(([sessionId, session]) => {
func({
...session,
deviceKey,
sessionId,
});
});
});
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
+51 -4
View File
@@ -27,6 +27,13 @@ import {newTimeoutError} from "./Error";
const timeoutException = new Error("Verification timed out");
export class SwitchStartEventError extends Error {
constructor(startEvent) {
super();
this.startEvent = startEvent;
}
}
export class VerificationBase extends EventEmitter {
/**
* Base class for verification methods.
@@ -67,9 +74,19 @@ export class VerificationBase extends EventEmitter {
this._done = false;
this._promise = null;
this._transactionTimeoutTimer = null;
}
// At this point, the verification request was received so start the timeout timer.
this._resetTimer();
get initiatedByMe() {
// if there is no start event yet,
// we probably want to send it,
// which happens if we initiate
if (!this.startEvent) {
return true;
}
const sender = this.startEvent.getSender();
const content = this.startEvent.getContent();
return sender === this._baseApis.getUserId() &&
content.from_device === this._baseApis.getDeviceId();
}
_resetTimer() {
@@ -107,6 +124,22 @@ export class VerificationBase extends EventEmitter {
});
}
canSwitchStartEvent() {
return false;
}
switchStartEvent(event) {
if (this.canSwitchStartEvent(event)) {
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
reject(new SwitchStartEventError(event));
} else {
this.startEvent = event;
}
}
}
handleEvent(e) {
if (this._done) {
return;
@@ -122,8 +155,15 @@ export class VerificationBase extends EventEmitter {
} else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject;
this._reject = undefined;
reject(new Error("Other side cancelled verification"));
} else {
const content = e.getContent();
const {reason, code} = content;
reject(new Error(`Other side cancelled verification ` +
`because ${reason} (${code})`));
} else if (this._expectedEvent) {
// only cancel if there is an event expected.
// if there is no event expected, it means verify() wasn't called
// and we're just replaying the timeline events when syncing
// after a refresh when the events haven't been stored in the cache yet.
const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent
+ " but got " + e.getType(),
@@ -263,6 +303,13 @@ export class VerificationBase extends EventEmitter {
throw new Error("No devices could be verified");
}
logger.info(
"Verification completed! Marking devices verified: ",
verifiedDevices,
);
// TODO: There should probably be a batch version of this, otherwise it's going
// to upload each signature in a separate API call which is silly because the
// API supports as many signatures as you like.
for (const deviceId of verifiedDevices) {
await this._baseApis.setDeviceVerified(userId, deviceId);
}
+2 -4
View File
@@ -23,12 +23,10 @@ limitations under the License.
import {MatrixEvent} from "../../models/event";
export function newVerificationError(code, reason, extradata) {
extradata = extradata || {};
extradata.code = code;
extradata.reason = reason;
const content = Object.assign({}, {code, reason}, extradata);
return new MatrixEvent({
type: "m.key.verification.cancel",
content: extradata,
content,
});
}
+43
View File
@@ -0,0 +1,43 @@
/*
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.
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.
*/
/**
* Verification method that is illegal to have (cannot possibly
* do verification with this method).
* @module crypto/verification/IllegalMethod
*/
import {VerificationBase as Base} from "./Base";
/**
* @class crypto/verification/IllegalMethod/IllegalMethod
* @extends {module:crypto/verification/Base}
*/
export class IllegalMethod extends Base {
static factory(...args) {
return new IllegalMethod(...args);
}
static get NAME() {
// Typically the name will be something else, but to complete
// the contract we offer a default one here.
return "org.matrix.illegal_method";
}
async _doVerification() {
throw new Error("Verification is not possible with this method");
}
}
+61 -71
View File
@@ -1,5 +1,6 @@
/*
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.
@@ -21,103 +22,92 @@ limitations under the License.
import {VerificationBase as Base} from "./Base";
import {
errorFactory,
newKeyMismatchError,
newUserCancelledError,
newUserMismatchError,
} from './Error';
const MATRIXTO_REGEXP = /^(?:https?:\/\/)?(?:www\.)?matrix\.to\/#\/([#@!+][^?]+)\?(.+)$/;
const KEY_REGEXP = /^key_([^:]+:.+)$/;
const newQRCodeError = errorFactory("m.qr_code.invalid", "Invalid QR code");
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
/**
* @class crypto/verification/QRCode/ShowQRCode
* @class crypto/verification/QRCode/ReciprocateQRCode
* @extends {module:crypto/verification/Base}
*/
export class ShowQRCode extends Base {
_doVerification() {
if (!this._done) {
const url = "https://matrix.to/#/" + this._baseApis.getUserId()
+ "?device=" + encodeURIComponent(this._baseApis.deviceId)
+ "&action=verify&key_ed25519%3A"
+ encodeURIComponent(this._baseApis.deviceId) + "="
+ encodeURIComponent(this._baseApis.getDeviceEd25519Key());
this.emit("show_qr_code", {
url: url,
});
}
}
}
ShowQRCode.NAME = "m.qr_code.show.v1";
/**
* @class crypto/verification/QRCode/ScanQRCode
* @extends {module:crypto/verification/Base}
*/
export class ScanQRCode extends Base {
export class ReciprocateQRCode extends Base {
static factory(...args) {
return new ScanQRCode(...args);
return new ReciprocateQRCode(...args);
}
static get NAME() {
return "m.reciprocate.v1";
}
async _doVerification() {
const code = await new Promise((resolve, reject) => {
this.emit("scan", {
done: resolve,
cancel: () => reject(newUserCancelledError()),
});
});
const match = code.match(MATRIXTO_REGEXP);
let deviceId;
const keys = {};
if (!match) {
throw newQRCodeError();
}
const userId = match[1];
const params = match[2].split("&").map(
(x) => x.split("=", 2).map(decodeURIComponent),
);
let action;
for (const [name, value] of params) {
if (name === "device") {
deviceId = value;
} else if (name === "action") {
action = value;
} else {
const keyMatch = name.match(KEY_REGEXP);
if (keyMatch) {
keys[keyMatch[1]] = value;
}
}
}
if (!deviceId || action !== "verify" || Object.keys(keys).length === 0) {
throw newQRCodeError();
if (!this.startEvent) {
// TODO: Support scanning QR codes
throw new Error("It is not currently possible to start verification" +
"with this method yet.");
}
const targetUserId = this.startEvent.getSender();
if (!this.userId) {
await new Promise((resolve, reject) => {
console.log("Asking to confirm user ID");
this.userId = await new Promise((resolve, reject) => {
this.emit("confirm_user_id", {
userId: userId,
confirm: resolve,
userId: targetUserId,
confirm: resolve, // takes a userId
cancel: () => reject(newUserMismatchError()),
});
});
} else if (this.userId !== userId) {
} else if (targetUserId !== this.userId) {
throw newUserMismatchError({
expected: this.userId,
actual: userId,
actual: targetUserId,
});
}
await this._verifyKeys(userId, keys, (keyId, device, key) => {
if (device.keys[keyId] !== key) {
if (this.startEvent.getContent()['secret'] !== this.request.encodedSharedSecret) {
throw newKeyMismatchError();
}
// If we've gotten this far, verify the user's master cross signing key
const xsignInfo = this._baseApis.getStoredCrossSigningForUser(this.userId);
if (!xsignInfo) throw new Error("Missing cross signing info");
const masterKey = xsignInfo.getId("master");
const masterKeyId = `ed25519:${masterKey}`;
const keys = {[masterKeyId]: masterKey};
const devices = (await this._baseApis.getStoredDevicesForUser(this.userId)) || [];
const targetDevice = devices.find(d => {
return d.deviceId === this.request.estimatedTargetDevice.deviceId;
});
if (!targetDevice) throw new Error("Device not found, somehow");
keys[`ed25519:${targetDevice.deviceId}`] = targetDevice.getFingerprint();
if (this.request.requestingUserId === this.request.receivingUserId) {
delete keys[masterKeyId];
}
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
const targetKey = keys[keyId];
if (!targetKey) throw newKeyMismatchError();
if (keyInfo !== targetKey) {
console.error("key ID from key info does not match");
throw newKeyMismatchError();
}
for (const deviceKeyId in device.keys) {
if (!deviceKeyId.startsWith("ed25519")) continue;
const deviceTargetKey = keys[deviceKeyId];
if (!deviceTargetKey) throw newKeyMismatchError();
if (device.keys[deviceKeyId] !== deviceTargetKey) {
console.error("master key does not match");
throw newKeyMismatchError();
}
}
// Otherwise it is probably fine
});
}
}
ScanQRCode.NAME = "m.qr_code.scan.v1";
+59 -14
View File
@@ -19,7 +19,7 @@ limitations under the License.
* @module crypto/verification/SAS
*/
import {VerificationBase as Base} from "./Base";
import {VerificationBase as Base, SwitchStartEventError} from "./Base";
import anotherjson from 'another-json';
import {
errorFactory,
@@ -29,6 +29,8 @@ import {
newUserCancelledError,
} from './Error';
const START_TYPE = "m.key.verification.start";
const EVENTS = [
"m.key.verification.accept",
"m.key.verification.key",
@@ -201,16 +203,37 @@ export class SAS extends Base {
// make sure user's keys are downloaded
await this._baseApis.downloadKeys([this.userId]);
if (this.startEvent) {
return await this._doRespondVerification();
} else {
return await this._doSendVerification();
}
let retry = false;
do {
try {
if (this.initiatedByMe) {
return await this._doSendVerification();
} else {
return await this._doRespondVerification();
}
} catch (err) {
if (err instanceof SwitchStartEventError) {
// this changes what initiatedByMe returns
this.startEvent = err.startEvent;
retry = true;
} else {
throw err;
}
}
} while (retry);
}
async _doSendVerification() {
const type = "m.key.verification.start";
const initialMessage = this._channel.completeContent(type, {
canSwitchStartEvent(event) {
if (event.getType() !== START_TYPE) {
return false;
}
const content = event.getContent();
return content && content.method === SAS.NAME &&
this._waitingForAccept;
}
async _sendStart() {
const startContent = this._channel.completeContent(START_TYPE, {
method: SAS.NAME,
from_device: this._baseApis.deviceId,
key_agreement_protocols: KEY_AGREEMENT_LIST,
@@ -219,11 +242,33 @@ export class SAS extends Base {
// FIXME: allow app to specify what SAS methods can be used
short_authentication_string: SAS_LIST,
});
// add the transaction id to the message beforehand because
// it needs to be included in the commitment hash later on
this._channel.sendCompleted(type, initialMessage);
await this._channel.sendCompleted(START_TYPE, startContent);
return startContent;
}
let e = await this._waitForEvent("m.key.verification.accept");
async _doSendVerification() {
this._waitingForAccept = true;
let startContent;
if (this.startEvent) {
startContent = this._channel.completedContentFromEvent(this.startEvent);
} else {
startContent = await this._sendStart();
}
// we might have switched to a different start event,
// but was we didn't call _waitForEvent there was no
// call that could throw yet. So check manually that
// we're still on the initiator side
if (!this.initiatedByMe) {
throw new SwitchStartEventError(this.startEvent);
}
let e;
try {
e = await this._waitForEvent("m.key.verification.accept");
} finally {
this._waitingForAccept = false;
}
let content = e.getContent();
const sasMethods
= intersection(content.short_authentication_string, SAS_SET);
@@ -248,7 +293,7 @@ export class SAS extends Base {
e = await this._waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
const commitmentStr = content.key + anotherjson.stringify(startContent);
// TODO: use selected hash function (when we support multiple)
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
throw newMismatchedCommitmentError();
+138 -18
View File
@@ -15,7 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {REQUEST_TYPE, START_TYPE, VerificationRequest} from "./VerificationRequest";
import {
VerificationRequest,
REQUEST_TYPE,
READY_TYPE,
START_TYPE,
} from "./VerificationRequest";
import {logger} from '../../../logger';
const MESSAGE_TYPE = "m.room.message";
const M_REFERENCE = "m.reference";
@@ -31,10 +37,10 @@ export class InRoomChannel {
* @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) {
constructor(client, roomId, userId = null) {
this._client = client;
this._roomId = roomId;
this._userId = userId;
this.userId = userId;
this._requestEventId = null;
}
@@ -43,16 +49,41 @@ export class InRoomChannel {
return true;
}
get receiveStartFromOtherDevices() {
return true;
}
get roomId() {
return this._roomId;
}
/** The transaction id generated/used by this verification channel */
get transactionId() {
return this._requestEventId;
}
static getOtherPartyUserId(event, client) {
const type = InRoomChannel.getEventType(event);
if (type !== REQUEST_TYPE) {
return;
}
const ownUserId = client.getUserId();
const sender = event.getSender();
const content = event.getContent();
const receiver = content.to;
if (sender === ownUserId) {
return receiver;
} else if (receiver === ownUserId) {
return sender;
}
}
/**
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
static getTimestamp(event) {
getTimestamp(event) {
return event.getTs();
}
@@ -97,19 +128,26 @@ export class InRoomChannel {
}
const type = InRoomChannel.getEventType(event);
const content = event.getContent();
// from here on we're fairly sure that this is supposed to be
// part of a verification request, so be noisy when rejecting something
if (type === REQUEST_TYPE) {
if (typeof content.to !== "string" || !content.to.length) {
if (!content || typeof content.to !== "string" || !content.to.length) {
logger.log("InRoomChannel: validateEvent: " +
"no valid to " + (content && content.to));
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) {
if (!InRoomChannel.getOtherPartyUserId(event, client)) {
logger.log("InRoomChannel: validateEvent: " +
`not directed to or sent by me: ${event.getSender()}` +
`, ${content && content.to}`);
return false;
}
}
return VerificationRequest.validateEvent(
type, event, InRoomChannel.getTimestamp(event), client);
return VerificationRequest.validateEvent(type, event, client);
}
/**
@@ -130,28 +168,54 @@ export class InRoomChannel {
}
}
}
return type;
if (type && type !== REQUEST_TYPE) {
return type;
} else {
return "";
}
}
/**
* 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
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request) {
async handleEvent(event, request, isLiveEvent) {
const type = InRoomChannel.getEventType(event);
// do validations that need state (roomId, userId),
// ignore if invalid
if (event.getRoomId() !== this._roomId || event.getSender() !== this._userId) {
if (event.getRoomId() !== this._roomId) {
return;
}
// set transactionId when receiving a .request
if (!this._requestEventId && type === REQUEST_TYPE) {
this._requestEventId = event.getId();
// set userId if not set already
if (this.userId === null) {
const userId = InRoomChannel.getOtherPartyUserId(event, this._client);
if (userId) {
this.userId = userId;
}
}
// ignore events not sent by us or the other party
const ownUserId = this._client.getUserId();
const sender = event.getSender();
if (this.userId !== null) {
if (sender !== ownUserId && sender !== this.userId) {
logger.log(`InRoomChannel: ignoring verification event from ` +
`non-participating sender ${sender}`);
return;
}
}
if (this._requestEventId === null) {
this._requestEventId = InRoomChannel.getTransactionId(event);
}
return await request.handleEvent(type, event, InRoomChannel.getTimestamp(event));
const isRemoteEcho = !!event.getUnsigned().transaction_id;
const isSentByUs = event.getSender() === this._client.getUserId();
return await request.handleEvent(
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
}
/**
@@ -180,7 +244,7 @@ export class InRoomChannel {
*/
completeContent(type, content) {
content = Object.assign({}, content);
if (type === REQUEST_TYPE || type === START_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
}
if (type === REQUEST_TYPE) {
@@ -191,7 +255,7 @@ export class InRoomChannel {
"verification. You will need to use legacy key " +
"verification to verify keys.",
msgtype: REQUEST_TYPE,
to: this._userId,
to: this.userId,
from_device: content.from_device,
methods: content.methods,
};
@@ -232,3 +296,59 @@ export class InRoomChannel {
}
}
}
export class InRoomRequests {
constructor() {
this._requestsByRoomId = new Map();
}
getRequest(event) {
const roomId = event.getRoomId();
const txnId = InRoomChannel.getTransactionId(event);
// console.log(`looking for request in room ${roomId} with txnId ${txnId} for an ${event.getType()} from ${event.getSender()}...`);
return this._getRequestByTxnId(roomId, txnId);
}
getRequestByChannel(channel) {
return this._getRequestByTxnId(channel.roomId, channel.transactionId);
}
_getRequestByTxnId(roomId, txnId) {
const requestsByTxnId = this._requestsByRoomId.get(roomId);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this._setRequest(
event.getRoomId(),
InRoomChannel.getTransactionId(event),
request,
);
}
setRequestByChannel(channel, request) {
this._setRequest(channel.roomId, channel.transactionId, request);
}
_setRequest(roomId, txnId, request) {
let requestsByTxnId = this._requestsByRoomId.get(roomId);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByRoomId.set(roomId, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
const roomId = event.getRoomId();
const requestsByTxnId = this._requestsByRoomId.get(roomId);
if (requestsByTxnId) {
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByRoomId.delete(roomId);
}
}
}
}
@@ -1,59 +0,0 @@
/*
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;
}
}
@@ -20,11 +20,14 @@ import {logger} from '../../../logger';
import {
CANCEL_TYPE,
PHASE_STARTED,
PHASE_READY,
REQUEST_TYPE,
READY_TYPE,
START_TYPE,
VerificationRequest,
} from "./VerificationRequest";
import {errorFromEvent, newUnexpectedMessageError} from "../Error";
import {MatrixEvent} from "../../../models/event";
/**
* A key verification channel that sends verification events over to_device messages.
@@ -34,12 +37,20 @@ 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.userId = userId;
this._devices = devices;
this.transactionId = transactionId;
this._deviceId = deviceId;
}
get deviceId() {
return this._deviceId;
}
get needsDoneMessage() {
return true;
}
static getEventType(event) {
return event.getType();
}
@@ -80,10 +91,12 @@ export class ToDeviceChannel {
}
const content = event.getContent();
if (!content) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
return false;
}
if (!content.transaction_id) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
return false;
}
@@ -91,6 +104,7 @@ export class ToDeviceChannel {
if (type === REQUEST_TYPE) {
if (!Number.isFinite(content.timestamp)) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
return false;
}
if (event.getSender() === client.getUserId() &&
@@ -98,19 +112,19 @@ export class ToDeviceChannel {
) {
// ignore requests from ourselves, because it doesn't make sense for a
// device to verify itself
logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
return false;
}
}
return VerificationRequest.validateEvent(
type, event, ToDeviceChannel.getTimestamp(event), client);
return VerificationRequest.validateEvent(type, event, client);
}
/**
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
static getTimestamp(event) {
getTimestamp(event) {
const content = event.getContent();
return content && content.timestamp;
}
@@ -119,12 +133,13 @@ export class ToDeviceChannel {
* 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
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request) {
async handleEvent(event, request, isLiveEvent) {
const type = event.getType();
const content = event.getContent();
if (type === REQUEST_TYPE || type === START_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
if (!this.transactionId) {
this.transactionId = content.transaction_id;
}
@@ -143,14 +158,17 @@ export class ToDeviceChannel {
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
}
}
const wasStarted = request.phase === PHASE_STARTED ||
request.phase === PHASE_READY;
const wasStarted = request.phase === PHASE_STARTED;
await request.handleEvent(
event.getType(), event, ToDeviceChannel.getTimestamp(event));
const isStarted = request.phase === PHASE_STARTED;
await request.handleEvent(event.getType(), event, isLiveEvent, false, false);
// the request has picked a start event, tell the other devices about it
if (type === START_TYPE && !wasStarted && isStarted && this._deviceId) {
const isStarted = request.phase === PHASE_STARTED ||
request.phase === PHASE_READY;
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
// the request has picked a ready or start event, tell the other devices about it
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
if (nonChosenDevices.length) {
const message = this.completeContent({
@@ -186,7 +204,7 @@ export class ToDeviceChannel {
if (this.transactionId) {
content.transaction_id = this.transactionId;
}
if (type === REQUEST_TYPE || type === START_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
}
if (type === REQUEST_TYPE) {
@@ -216,12 +234,28 @@ export class ToDeviceChannel {
* @param {object} content
* @returns {Promise} the promise of the request
*/
sendCompleted(type, content) {
async sendCompleted(type, content) {
let result;
if (type === REQUEST_TYPE) {
return this._sendToDevices(type, content, this._devices);
result = await this._sendToDevices(type, content, this._devices);
} else {
return this._sendToDevices(type, content, [this._deviceId]);
result = await this._sendToDevices(type, content, [this._deviceId]);
}
// the VerificationRequest state machine requires remote echos of the event
// the client sends itself, so we fake this for to_device messages
const remoteEchoEvent = new MatrixEvent({
sender: this._client.getUserId(),
content,
type,
});
await this._request.handleEvent(
type,
remoteEchoEvent,
/*isLiveEvent=*/true,
/*isRemoteEcho=*/true,
/*isSentByUs=*/true,
);
return result;
}
_sendToDevices(type, content, devices) {
@@ -231,7 +265,7 @@ export class ToDeviceChannel {
msgMap[deviceId] = content;
}
return this._client.sendToDevice(type, {[this._userId]: msgMap});
return this._client.sendToDevice(type, {[this.userId]: msgMap});
} else {
return Promise.resolve();
}
@@ -245,3 +279,60 @@ export class ToDeviceChannel {
return randomString(32);
}
}
export class ToDeviceRequests {
constructor() {
this._requestsByUserId = new Map();
}
getRequest(event) {
return this.getRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
);
}
getRequestByChannel(channel) {
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
}
getRequestBySenderAndTxnId(sender, txnId) {
const requestsByTxnId = this._requestsByUserId.get(sender);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this.setRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
request,
);
}
setRequestByChannel(channel, request) {
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
}
setRequestBySenderAndTxnId(sender, txnId, request) {
let requestsByTxnId = this._requestsByUserId.get(sender);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByUserId.set(sender, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
const userId = event.getSender();
const requestsByTxnId = this._requestsByUserId.get(userId);
if (requestsByTxnId) {
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByUserId.delete(userId);
}
}
}
}
@@ -16,7 +16,6 @@ limitations under the License.
*/
import {logger} from '../../../logger';
import {RequestCallbackChannel} from "./RequestCallbackChannel";
import {EventEmitter} from 'events';
import {
errorFactory,
@@ -24,6 +23,7 @@ import {
newUnexpectedMessageError,
newUnknownMethodError,
} from "../Error";
import * as olmlib from "../../olmlib";
// the recommended amount of time before a verification request
// should be (automatically) cancelled without user interaction
@@ -41,11 +41,11 @@ 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 READY_TYPE = EVENT_PREFIX + "ready";
export const PHASE_UNSENT = 1;
export const PHASE_REQUESTED = 2;
// const PHASE_READY = 3;
export const PHASE_READY = 3;
export const PHASE_STARTED = 4;
export const PHASE_CANCELLED = 5;
export const PHASE_DONE = 6;
@@ -58,17 +58,19 @@ export const PHASE_DONE = 6;
* @event "change" whenever the state of the request object has changed.
*/
export class VerificationRequest extends EventEmitter {
constructor(channel, verificationMethods, userId, client) {
constructor(channel, verificationMethods, client) {
super();
this.channel = channel;
this.channel._request = this;
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;
this._eventsByUs = new Map();
this._eventsByThem = new Map();
this._observeOnly = false;
this._timeoutTimer = null;
this._sharedSecret = null; // used for QR codes
}
/**
@@ -76,37 +78,38 @@ export class VerificationRequest extends EventEmitter {
* 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) {
static validateEvent(type, event, client) {
const content = event.getContent();
if (!type.startsWith(EVENT_PREFIX)) {
if (!type || !type.startsWith(EVENT_PREFIX)) {
return false;
}
if (type === REQUEST_TYPE) {
if (!Array.isArray(content.methods)) {
return false;
}
// from here on we're fairly sure that this is supposed to be
// part of a verification request, so be noisy when rejecting something
if (!content) {
logger.log("VerificationRequest: validateEvent: no content");
return false;
}
if (type === REQUEST_TYPE || type === START_TYPE) {
if (typeof content.from_device !== "string" ||
content.from_device.length === 0
) {
if (type === REQUEST_TYPE || type === READY_TYPE) {
if (!Array.isArray(content.methods)) {
logger.log("VerificationRequest: validateEvent: " +
"fail because methods");
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");
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
if (typeof content.from_device !== "string" ||
content.from_device.length === 0
) {
logger.log("VerificationRequest: validateEvent: "+
"fail because from_device");
return false;
}
}
@@ -114,20 +117,56 @@ export class VerificationRequest extends EventEmitter {
return true;
}
/** once the phase is PHASE_STARTED, common methods supported by both sides */
get invalid() {
return this.phase === PHASE_UNSENT;
}
/** returns whether the phase is PHASE_REQUESTED */
get requested() {
return this.phase === PHASE_REQUESTED;
}
/** returns whether the phase is PHASE_CANCELLED */
get cancelled() {
return this.phase === PHASE_CANCELLED;
}
/** returns whether the phase is PHASE_READY */
get ready() {
return this.phase === PHASE_READY;
}
/** returns whether the phase is PHASE_STARTED */
get started() {
return this.phase === PHASE_STARTED;
}
/** returns whether the phase is PHASE_DONE */
get done() {
return this.phase === PHASE_DONE;
}
/** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
get methods() {
return this._commonMethods;
}
/** the timeout of the request, provided for compatibility with previous verification code */
/** The current remaining amount of ms before the request should be automatically cancelled */
get timeout() {
const elapsed = Date.now() - this._startTimestamp;
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
const requestEvent = this._getEventByEither(REQUEST_TYPE);
if (requestEvent) {
const elapsed = Date.now() - this.channel.getTimestamp(requestEvent);
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
}
return 0;
}
/** the m.key.verification.request event that started this request, provided for compatibility with previous verification code */
get event() {
return this._requestEvent;
/**
* The key verification request event.
* @returns {MatrixEvent} The request event, or falsey if not found.
*/
get requestEvent() {
return this._getEventByEither(REQUEST_TYPE);
}
/** current phase of the request. Some properties might only be defined in a current phase. */
@@ -142,8 +181,7 @@ export class VerificationRequest extends EventEmitter {
/** 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
return this._phase !== PHASE_DONE
&& this._phase !== PHASE_CANCELLED;
}
@@ -152,27 +190,108 @@ export class VerificationRequest extends EventEmitter {
* For ToDeviceChannel, this is who sent the .start event
*/
get initiatedByMe() {
return this._initiatedByMe;
// event created by us but no remote echo has been received yet
const noEventsYet = (this._eventsByUs.size + this._eventsByThem.size) === 0;
if (this._phase === PHASE_UNSENT && noEventsYet) {
return true;
}
const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE);
const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE);
if (hasMyRequest && !hasTheirRequest) {
return true;
}
if (!hasMyRequest && hasTheirRequest) {
return false;
}
const hasMyStart = this._eventsByUs.has(START_TYPE);
const hasTheirStart = this._eventsByThem.has(START_TYPE);
if (hasMyStart && !hasTheirStart) {
return true;
}
return false;
}
/** the id of the user that initiated the request */
/** The id of the user that initiated the request */
get requestingUserId() {
if (this.initiatedByMe) {
return this._client.getUserId();
} else {
return this._otherUserId;
return this.otherUserId;
}
}
/** the id of the user that (will) receive(d) the request */
/** The id of the user that (will) receive(d) the request */
get receivingUserId() {
if (this.initiatedByMe) {
return this._otherUserId;
return this.otherUserId;
} else {
return this._client.getUserId();
}
}
/** The user id of the other party in this request */
get otherUserId() {
return this.channel.userId;
}
/**
* The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED
*/
get cancellingUserId() {
const myCancel = this._eventsByUs.get(CANCEL_TYPE);
const theirCancel = this._eventsByThem.get(CANCEL_TYPE);
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
return myCancel.getSender();
}
if (theirCancel) {
return theirCancel.getSender();
}
return undefined;
}
/**
* The cancellation code e.g m.user which is responsible for cancelling this verification
*/
get cancellationCode() {
const ev = this._getEventByEither(CANCEL_TYPE);
return ev ? ev.getContent().code : null;
}
get observeOnly() {
return this._observeOnly;
}
/**
* The unpadded base64 encoded shared secret. Primarily used for QR code
* verification.
*/
get encodedSharedSecret() {
if (!this._sharedSecret) this._generateSharedSecret();
return this._sharedSecret;
}
/**
* Gets which device the verification should be started with
* given the events sent so far in the verification. This is the
* same algorithm used to determine which device to send the
* verification to when no specific device is specified.
* @returns {{userId: *, deviceId: *}} The device information
*/
get targetDevice() {
const theirFirstEvent =
this._eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE) ||
this._eventsByThem.get(START_TYPE);
const theirFirstContent = theirFirstEvent.getContent();
const fromDevice = theirFirstContent.from_device;
return {
userId: this.otherUserId,
deviceId: fromDevice,
};
}
/* 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.
@@ -182,8 +301,13 @@ export class VerificationRequest extends EventEmitter {
*/
beginKeyVerification(method, targetDevice = null) {
// need to allow also when unsent in case of to_device
if (!this._verifier) {
if (this._hasValidPreStartPhase()) {
if (!this.observeOnly && !this._verifier) {
const validStartPhase =
this.phase === PHASE_REQUESTED ||
this.phase === PHASE_READY ||
(this.phase === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE));
if (validStartPhase) {
// 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)) {
@@ -203,12 +327,10 @@ export class VerificationRequest extends EventEmitter {
* @returns {Promise} resolves when the event has been sent.
*/
async sendRequest() {
if (this._phase === PHASE_UNSENT) {
this._initiatedByMe = true;
this._setPhase(PHASE_REQUESTED, false);
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
const methods = [...this._verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, {methods});
this.emit("change");
this._generateSharedSecret();
}
}
@@ -219,34 +341,63 @@ export class VerificationRequest extends EventEmitter {
* @returns {Promise} resolves when the event has been sent.
*/
async cancel({reason = "User declined", code = "m.user"} = {}) {
if (this._phase !== PHASE_CANCELLED) {
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason));
return this._verifier.cancel(errorFactory(code, reason)());
} else {
this._setPhase(PHASE_CANCELLED, false);
this._cancellingUserId = this._client.getUserId();
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);
});
/**
* Accepts the request, sending a .ready event to the other party
* @returns {Promise} resolves when the event has been sent.
*/
async accept() {
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
const methods = [...this._verificationMethods.keys()];
await this.channel.send(READY_TYPE, {methods});
this._generateSharedSecret();
}
}
_generateSharedSecret() {
const secretBytes = new Uint8Array(32); // 256bits
global.crypto.getRandomValues(secretBytes);
this._sharedSecret = olmlib.encodeBase64(secretBytes);
}
/**
* Can be used to listen for state changes until the callback returns true.
* @param {Function} fn callback to evaluate whether the request is in the desired state.
* Takes the request as an argument.
* @returns {Promise} that resolves once the callback returns true
* @throws {Error} when the request is cancelled
*/
waitFor(fn) {
return new Promise((resolve, reject) => {
const check = () => {
let handled = false;
if (fn(this)) {
resolve(this);
handled = true;
} else if (this.cancelled) {
reject(new Error("cancelled"));
handled = true;
}
if (handled) {
this.off("change", check);
}
return handled;
};
if (!check()) {
this.on("change", check);
}
});
}
_setPhase(phase, notify = true) {
this._phase = phase;
if (notify) {
@@ -254,155 +405,297 @@ export class VerificationRequest extends EventEmitter {
}
}
_getEventByEither(type) {
return this._eventsByThem.get(type) || this._eventsByUs.get(type);
}
_getEventBy(type, byThem) {
if (byThem) {
return this._eventsByThem.get(type);
} else {
return this._eventsByUs.get(type);
}
}
_calculatePhaseTransitions() {
const transitions = [{phase: PHASE_UNSENT}];
const phase = () => transitions[transitions.length - 1].phase;
// always pass by .request first to be sure channel.userId has been set
const hasRequestByThem = this._eventsByThem.has(REQUEST_TYPE);
const requestEvent = this._getEventBy(REQUEST_TYPE, hasRequestByThem);
if (requestEvent) {
transitions.push({phase: PHASE_REQUESTED, event: requestEvent});
}
const readyEvent =
requestEvent && this._getEventBy(READY_TYPE, !hasRequestByThem);
if (readyEvent && phase() === PHASE_REQUESTED) {
transitions.push({phase: PHASE_READY, event: readyEvent});
}
let startEvent;
if (readyEvent || !requestEvent) {
const theirStartEvent = this._eventsByThem.get(START_TYPE);
const ourStartEvent = this._eventsByUs.get(START_TYPE);
// any party can send .start after a .ready or unsent
if (theirStartEvent && ourStartEvent) {
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ?
theirStartEvent : ourStartEvent;
} else {
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
}
} else {
startEvent = this._getEventBy(START_TYPE, !hasRequestByThem);
}
if (startEvent) {
const fromRequestPhase = phase() === PHASE_REQUESTED &&
requestEvent.getSender() !== startEvent.getSender();
const fromUnsentPhase = phase() === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE);
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
transitions.push({phase: PHASE_STARTED, event: startEvent});
}
}
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
if (ourDoneEvent && phase() === PHASE_STARTED) {
transitions.push({phase: PHASE_DONE});
}
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
if (cancelEvent && phase() !== PHASE_DONE) {
transitions.push({phase: PHASE_CANCELLED, event: cancelEvent});
return transitions;
}
return transitions;
}
_transitionToPhase(transition) {
const {phase, event} = transition;
// get common methods
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
if (!this._wasSentByOwnDevice(event)) {
const content = event.getContent();
this._commonMethods =
content.methods.filter(m => this._verificationMethods.has(m));
}
}
// detect if we're not a party in the request, and we should just observe
if (!this.observeOnly) {
// if requested or accepted by one of my other devices
if (phase === PHASE_REQUESTED ||
phase === PHASE_STARTED ||
phase === PHASE_READY
) {
if (
this.channel.receiveStartFromOtherDevices &&
this._wasSentByOwnUser(event) &&
!this._wasSentByOwnDevice(event)
) {
this._observeOnly = true;
}
}
}
// create verifier
if (phase === PHASE_STARTED) {
const {method} = event.getContent();
if (!this._verifier && !this.observeOnly) {
this._verifier = this._createVerifier(method, event);
}
}
}
/**
* 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.
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device
* @param {bool} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
* For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
* @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);
async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
// if reached phase cancelled or done, ignore anything else that comes
if (!this.pending) {
return;
}
const wasObserveOnly = this._observeOnly;
if (this._verifier) {
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
this._verifier.handleEvent(event);
this._adjustObserveOnly(event, isLiveEvent);
if (!this.observeOnly && !isRemoteEcho) {
if (await this._cancelOnError(type, event)) {
return;
}
}
if (type === CANCEL_TYPE) {
this._handleCancel();
} else if (type === DONE_TYPE) {
this._handleDone();
const oldPhase = this.phase;
this._addEvent(type, event, isSentByUs);
const transitions = this._calculatePhaseTransitions();
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
// trim off phases we already went through, if any
const newTransitions = transitions.slice(existingIdx + 1);
// transition to all new phases
for (const transition of newTransitions) {
this._transitionToPhase(transition);
}
try {
// only pass events from the other side to the verifier,
// no remote echos of our own events
if (this._verifier && !this.observeOnly) {
const oldEvent = this._verifier.startEvent;
// if the verifier does not have a startEvent, it is because it's still sending and we are on the initator side
const oldSender = oldEvent ?
oldEvent.getSender() :
this._client.getUserId();
const newEventWinsRace = event.getSender() < oldSender;
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
this._verifier.switchStartEvent(event);
} else if (!isRemoteEcho) {
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
this._verifier.handleEvent(event);
}
}
}
if (newTransitions.length) {
const lastTransition = newTransitions[newTransitions.length - 1];
const {phase} = lastTransition;
this._setupTimeout(phase);
// set phase as last thing as this emits the "change" event
this._setPhase(phase);
} else if (this._observeOnly !== wasObserveOnly) {
this.emit("change");
}
} finally {
// log events we processed so we can see from rageshakes what events were added to a request
logger.log(`Verification request ${this.channel.transactionId}: ` +
`${type} event with ${JSON.stringify(event.getContent())} ` +
`deviceId:${this.channel.deviceId} ` +
`sender:${event.getSender()}, isSentByUs:${isSentByUs} ` +
`phase:${oldPhase}=>${this.phase}, ` +
`observeOnly:${wasObserveOnly}=>${this._observeOnly}`);
}
}
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()));
_setupTimeout(phase) {
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
phase === PHASE_REQUESTED && this.initiatedByMe;
if (shouldTimeout) {
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
}
if (this._timeoutTimer) {
const shouldClear = phase === PHASE_STARTED ||
phase === PHASE_READY ||
phase === PHASE_DONE ||
phase === PHASE_CANCELLED;
if (shouldClear) {
clearTimeout(this._timeoutTimer);
this._timeoutTimer = null;
}
}
}
_hasValidPreStartPhase() {
return this._phase === PHASE_REQUESTED ||
(
this.channel.constructor.canCreateRequest(START_TYPE) &&
this._phase === PHASE_UNSENT
);
}
_cancelOnTimeout = () => {
try {
this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
} catch (err) {
logger.error("Error while cancelling verification request", err);
}
};
async _handleStart(content, event) {
if (this._hasValidPreStartPhase()) {
const {method} = content;
async _cancelOnError(type, event) {
if (type === START_TYPE) {
const method = event.getContent().method;
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);
return true;
}
}
const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED;
if (isUnexpectedRequest || isUnexpectedReady) {
logger.warn(`Cancelling, unexpected ${type} verification ` +
`event from ${event.getSender()}`);
const reason = `Unexpected ${type} event in phase ${this.phase}`;
await this.cancel(errorFromEvent(newUnexpectedMessageError({reason})));
return true;
}
return false;
}
_adjustObserveOnly(event, isLiveEvent) {
// don't send out events for historical requests
if (!isLiveEvent) {
this._observeOnly = true;
}
// a timestamp is not provided on all to_device events
const timestamp = this.channel.getTimestamp(event);
if (Number.isFinite(timestamp)) {
const elapsed = Date.now() - timestamp;
// don't allow interaction on old requests
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)
) {
this._observeOnly = true;
}
}
}
_addEvent(type, event, isSentByUs) {
if (isSentByUs) {
this._eventsByUs.set(type, event);
} else {
this._eventsByThem.set(type, event);
}
// once we know the userId of the other party (from the .request event)
// see if any event by anyone else crept into this._eventsByThem
if (type === REQUEST_TYPE) {
for (const [type, event] of this._eventsByThem.entries()) {
if (event.getSender() !== this.otherUserId) {
this._eventsByThem.delete(type);
}
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);
if (!targetDevice) {
targetDevice = this.targetDevice;
}
const {userId, deviceId} = targetDevice;
const VerifierCtor = this._verificationMethods.get(method);
if (!VerifierCtor) {
console.warn("could not find verifier constructor for method", method);
logger.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.channel,
this._client,
userId,
deviceId,
startSentByMe ? null : startEvent,
startEvent,
this,
);
}
_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};
}
_wasSentByOwnUser(event) {
return event.getSender() === this._client.getUserId();
}
// only for .request and .start
_wasSentByMe(event) {
if (event.getSender() !== this._client.getUserId()) {
// only for .request, .ready or .start
_wasSentByOwnDevice(event) {
if (!this._wasSentByOwnUser(event)) {
return false;
}
const content = event.getContent();
+8 -8
View File
@@ -155,7 +155,7 @@ MatrixHttpApi.prototype = {
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*
* @return {module:client.Promise} Resolves to response object, as
* @return {Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
@@ -436,7 +436,7 @@ MatrixHttpApi.prototype = {
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Object} [data] The HTTP JSON body.
*
* @param {Object|Number=} opts additional options. If a number is specified,
* this is treated as `opts.localTimeoutMs`.
@@ -449,7 +449,7 @@ MatrixHttpApi.prototype = {
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
@@ -518,7 +518,7 @@ MatrixHttpApi.prototype = {
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Object} [data] The HTTP JSON body.
*
* @param {Object=} opts additional options
*
@@ -530,7 +530,7 @@ MatrixHttpApi.prototype = {
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
@@ -557,7 +557,7 @@ MatrixHttpApi.prototype = {
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Object} [data] The HTTP JSON body.
*
* @param {Object=} opts additional options
*
@@ -569,7 +569,7 @@ MatrixHttpApi.prototype = {
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
@@ -633,7 +633,7 @@ MatrixHttpApi.prototype = {
* @param {function=} opts.bodyParser function to parse the body of the
* response before passing it to the promise and callback.
*
* @return {module:client.Promise} a promise which resolves to either the
* @return {Promise} a promise which resolves to either the
* response object (if this.opts.onlyData is truthy), or the parsed
* body. Rejects
*/
+2 -1
View File
@@ -16,8 +16,9 @@ limitations under the License.
import * as matrixcs from "./matrix";
import * as utils from "./utils";
import request from "request";
matrixcs.request(import("request"));
matrixcs.request(request);
utils.runPolyfills();
export * from "./matrix";
+3 -3
View File
@@ -47,14 +47,14 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* @param {object?} opts.authData error response from the last request. If
* null, a request will be made with no auth before starting.
*
* @param {function(object?): module:client.Promise} opts.doRequest
* @param {function(object?): Promise} opts.doRequest
* called with the new auth dict to submit the request. Also passes a
* second deprecated arg which is a flag set to true if this request
* is a background request. The busyChanged callback should be used
* instead of the backfround flag. Should return a promise which resolves
* to the successful response or rejects with a MatrixError.
*
* @param {function(bool): module:client.Promise} opts.busyChanged
* @param {function(bool): Promise} opts.busyChanged
* called whenever the interactive auth logic becomes busy submitting
* information provided by the user or finsihes. After this has been
* called with true the UI should indicate that a request is in progress
@@ -132,7 +132,7 @@ InteractiveAuth.prototype = {
/**
* begin the authentication process.
*
* @return {module:client.Promise} which resolves to the response on success,
* @return {Promise} which resolves to the response on success,
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
* no suitable authentication flow can be found
*/
+3 -3
View File
@@ -103,11 +103,11 @@ export function setCryptoStoreFactory(fac) {
}
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
* @param {(Object|string)} opts The configuration options for this client. If
* this is a string, it is assumed to be the base URL. These configuration
* options will be passed directly to {@link module:client~MatrixClient}.
* options will be passed directly to {@link module:client.MatrixClient}.
* @param {Object} opts.store If not set, defaults to
* {@link module:store/memory.MemoryStore}.
* @param {Object} opts.scheduler If not set, defaults to
@@ -122,7 +122,7 @@ export function setCryptoStoreFactory(fac) {
* in-memory otherwise).
*
* @return {MatrixClient} A new matrix client.
* @see {@link module:client~MatrixClient} for the full list of options for
* @see {@link module:client.MatrixClient} for the full list of options for
* <code>opts</code>.
*/
export function createClient(opts) {
+7 -5
View File
@@ -490,8 +490,9 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
*/
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy, fromCache) {
if (this._filter) {
const events = this._filter.filterRoomTimeline([event]);
if (!events.length) {
@@ -529,7 +530,7 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
return;
}
this.addEventToTimeline(event, this._liveTimeline, false);
this.addEventToTimeline(event, this._liveTimeline, false, fromCache);
};
/**
@@ -541,11 +542,12 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
* @param {MatrixEvent} event
* @param {EventTimeline} timeline
* @param {boolean} toStartOfTimeline
* @param {boolean} fromCache whether the sync response came from cache
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
toStartOfTimeline) {
toStartOfTimeline, fromCache) {
const eventId = event.getId();
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
@@ -555,7 +557,7 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
const data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline && !fromCache,
};
this.emit("Room.timeline", event, this.room,
Boolean(toStartOfTimeline), false, data);
@@ -828,7 +830,7 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
*
* @param {object} data more data about the event
*
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
* @param {module:models/event-timeline.EventTimeline} data.timeline the timeline the
* event was added to/removed from
*
* @param {boolean} data.liveEvent true if the event was a real-time event
+11 -1
View File
@@ -22,7 +22,7 @@ limitations under the License.
*/
import {EventEmitter} from 'events';
import * as utils from '../utils.js';
import * as utils from '../utils';
import {logger} from '../logger';
/**
@@ -154,6 +154,12 @@ export const MatrixEvent = function(
* attempt may succeed)
*/
this._retryDecryption = false;
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
* `Crypto` will set this the `VerificationRequest` for the event
* so it can be easily accessed from the timeline.
*/
this.verificationRequest = null;
};
utils.inherits(MatrixEvent, EventEmitter);
@@ -1054,6 +1060,10 @@ utils.extend(MatrixEvent.prototype, {
encrypted: this.event,
};
},
setVerificationRequest: function(request) {
this.verificationRequest = request;
},
});
+6 -4
View File
@@ -1067,10 +1067,11 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) {
if (event.isRedaction()) {
const redactId = event.event.redacts;
@@ -1117,7 +1118,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
// add to our timeline sets
for (let i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
this._timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
}
// synthesize and inject implicit read receipts
@@ -1427,9 +1428,10 @@ Room.prototype._revertRedactionLocalEcho = function(redactionEvent) {
* this function will be ignored entirely, preserving the existing event in the
* timeline. Events are identical based on their event ID <b>only</b>.
*
* @param {boolean} fromCache whether the sync response came from cache
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
Room.prototype.addLiveEvents = function(events, duplicateStrategy, fromCache) {
let i;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
@@ -1455,7 +1457,7 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
for (i = 0; i < events.length; i++) {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this._addLiveEvent(events[i], duplicateStrategy);
this._addLiveEvent(events[i], duplicateStrategy, fromCache);
}
};
+9 -4
View File
@@ -388,8 +388,10 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
});
// strip out events which aren't for the given room_id (e.g presence)
// and also ephemeral events (which we're assuming is anything without
// and event ID because the /events API doesn't separate them).
const events = res.chunk.filter(function(e) {
return e.room_id === peekRoom.roomId;
return e.room_id === peekRoom.roomId && e.event_id;
}).map(self.client.getEventMapper());
peekRoom.addLiveEvents(events);
@@ -688,6 +690,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
oldSyncToken: null,
nextSyncToken,
catchingUp: false,
fromCache: true,
};
const data = {
@@ -1237,7 +1240,8 @@ SyncApi.prototype._processSyncResponse = async function(
}
}
self._processRoomEvents(room, stateEvents, timelineEvents);
self._processRoomEvents(room, stateEvents,
timelineEvents, syncEventData.fromCache);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1564,10 +1568,11 @@ SyncApi.prototype._resolveInvites = function(room) {
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
* at the *START* of the timeline list if it is supplied.
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
* @param {boolean} fromCache whether the sync response came from cache
* is earlier in time. Higher index is later.
*/
SyncApi.prototype._processRoomEvents = function(room, stateEventList,
timelineEventList) {
timelineEventList, fromCache) {
// If there are no events in the timeline yet, initialise it with
// the given state events
const liveTimeline = room.getLiveTimeline();
@@ -1621,7 +1626,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// if the timeline has any state events in it.
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
room.addLiveEvents(timelineEventList || []);
room.addLiveEvents(timelineEventList || [], null, fromCache);
};
/**
+61 -31
View File
@@ -89,7 +89,7 @@ export function TimelineWindow(client, timelineSet, opts) {
* given event
* @param {number} [initialWindowSize = 20] Size of the initial window
*
* @return {module:client.Promise}
* @return {Promise}
*/
TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
const self = this;
@@ -147,6 +147,62 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
}
};
/**
* Get the TimelineIndex of the window in the given direction.
*
* @param {string} direction EventTimeline.BACKWARDS to get the TimelineIndex
* at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at
* the end.
*
* @return {TimelineIndex} The requested timeline index if one exists, null
* otherwise.
*/
TimelineWindow.prototype.getTimelineIndex = function(direction) {
if (direction == EventTimeline.BACKWARDS) {
return this._start;
} else if (direction == EventTimeline.FORWARDS) {
return this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
};
/**
* Try to extend the window using events that are already in the underlying
* TimelineIndex.
*
* @param {string} direction EventTimeline.BACKWARDS to try extending it
* backwards; EventTimeline.FORWARDS to try extending it forwards.
* @param {number} size number of events to try to extend by.
*
* @return {boolean} true if the window was extended, false otherwise.
*/
TimelineWindow.prototype.extend = function(direction, size) {
const tl = this.getTimelineIndex(direction);
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
return false;
}
const count = (direction == EventTimeline.BACKWARDS) ?
tl.retreat(size) : tl.advance(size);
if (count) {
this._eventCount += count;
debuglog("TimelineWindow: increased cap by " + count +
" (now " + this._eventCount + ")");
// remove some events from the other end, if necessary
const excess = this._eventCount - this._windowLimit;
if (excess > 0) {
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
return true;
}
return false;
};
/**
* Check if this window can be extended
*
@@ -161,14 +217,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
* @return {boolean} true if we can paginate in the given direction
*/
TimelineWindow.prototype.canPaginate = function(direction) {
let tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
tl = this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
const tl = this.getTimelineIndex(direction);
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
@@ -208,7 +257,7 @@ TimelineWindow.prototype.canPaginate = function(direction) {
* @param {number} [requestLimit = 5] limit for the number of API requests we
* should make.
*
* @return {module:client.Promise} Resolves to a boolean which is true if more events
* @return {Promise} Resolves to a boolean which is true if more events
* were successfully retrieved.
*/
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
@@ -224,14 +273,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
}
let tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
tl = this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
const tl = this.getTimelineIndex(direction);
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
@@ -243,18 +285,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
}
// try moving the cap
const count = (direction == EventTimeline.BACKWARDS) ?
tl.retreat(size) : tl.advance(size);
if (count) {
this._eventCount += count;
debuglog("TimelineWindow: increased cap by " + count +
" (now " + this._eventCount + ")");
// remove some events from the other end, if necessary
const excess = this._eventCount - this._windowLimit;
if (excess > 0) {
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
if (this.extend(direction, size)) {
return Promise.resolve(true);
}
@@ -490,4 +521,3 @@ TimelineIndex.prototype.advance = function(delta) {
TimelineIndex.prototype.retreat = function(delta) {
return this.advance(delta * -1) * -1;
};
+55 -41
View File
@@ -28,7 +28,7 @@ import unhomoglyph from 'unhomoglyph';
* {"foo": "bar", "baz": "taz"}
* @return {string} The encoded string e.g. foo=bar&baz=taz
*/
export function encodeParams(params) {
export function encodeParams(params: Record<string, string>): string {
let qs = "";
for (const key in params) {
if (!params.hasOwnProperty(key)) {
@@ -48,7 +48,8 @@ export function encodeParams(params) {
* variables with. E.g. { "$bar": "baz" }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
export function encodeUri(pathTemplate, variables) {
export function encodeUri(pathTemplate: string,
variables: Record<string, string>): string {
for (const key in variables) {
if (!variables.hasOwnProperty(key)) {
continue;
@@ -67,7 +68,7 @@ export function encodeUri(pathTemplate, variables) {
* the array with the signature <code>fn(element){...}</code>
* @return {Array} A new array with the results of the function.
*/
export function map(array, fn) {
export function map<T, S>(array: T[], fn: (t: T) => S): S[] {
const results = new Array(array.length);
for (let i = 0; i < array.length; i++) {
results[i] = fn(array[i]);
@@ -83,8 +84,9 @@ export function map(array, fn) {
* looks like <code>fn(element, index, array){...}</code>.
* @return {Array} A new array with the results of the function.
*/
export function filter(array, fn) {
const results = [];
export function filter<T>(array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean): T[] {
const results: T[] = [];
for (let i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
results.push(array[i]);
@@ -98,15 +100,15 @@ export function filter(array, fn) {
* @param {Object} obj The object to get the keys for.
* @return {string[]} The keys of the object.
*/
export function keys(obj) {
const keys = [];
export function keys(obj: object): string[] {
const result = [];
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
keys.push(key);
result.push(key);
}
return keys;
return result;
}
/**
@@ -114,15 +116,15 @@ export function keys(obj) {
* @param {Object} obj The object to get the values for.
* @return {Array<*>} The values of the object.
*/
export function values(obj) {
const values = [];
export function values<T>(obj: Record<string, T>): T[] {
const result = [];
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
values.push(obj[key]);
result.push(obj[key]);
}
return values;
return result;
}
/**
@@ -131,7 +133,7 @@ export function values(obj) {
* @param {Function} fn The function to invoke for each element. Has the
* function signature <code>fn(element, index)</code>.
*/
export function forEach(array, fn) {
export function forEach<T>(array: T[], fn: (t: T, i: number) => void) {
for (let i = 0; i < array.length; i++) {
fn(array[i], i);
}
@@ -148,7 +150,11 @@ export function forEach(array, fn) {
* @return {*} The first value in the array which returns <code>true</code> for
* the given function.
*/
export function findElement(array, fn, reverse) {
export function findElement<T>(
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean
) {
let i;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
@@ -175,7 +181,11 @@ export function findElement(array, fn, reverse) {
* @param {boolean} reverse True to search in reverse order.
* @return {boolean} True if an element was removed.
*/
export function removeElement(array, fn, reverse) {
export function removeElement<T>(
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean
) {
let i;
let removed;
if (reverse) {
@@ -203,8 +213,8 @@ export function removeElement(array, fn, reverse) {
* @param {*} value The thing to check.
* @return {boolean} True if it is a function.
*/
export function isFunction(value) {
return Object.prototype.toString.call(value) == "[object Function]";
export function isFunction(value: any) {
return Object.prototype.toString.call(value) === "[object Function]";
}
/**
@@ -212,7 +222,7 @@ export function isFunction(value) {
* @param {*} value The thing to check.
* @return {boolean} True if it is an array.
*/
export function isArray(value) {
export function isArray(value: any) {
return Array.isArray ? Array.isArray(value) :
Boolean(value && value.constructor === Array);
}
@@ -223,10 +233,11 @@ export function isArray(value) {
* @param {string[]} keys The list of keys that 'obj' must have.
* @throws If the object is missing keys.
*/
export function checkObjectHasKeys(obj, keys) {
for (let i = 0; i < keys.length; i++) {
if (!obj.hasOwnProperty(keys[i])) {
throw new Error("Missing required key: " + keys[i]);
// note using 'keys' here would shadow the 'keys' function defined above
export function checkObjectHasKeys(obj: object, keys_: string[]) {
for (let i = 0; i < keys_.length; i++) {
if (!obj.hasOwnProperty(keys_[i])) {
throw new Error("Missing required key: " + keys_[i]);
}
}
}
@@ -237,7 +248,7 @@ export function checkObjectHasKeys(obj, keys) {
* @param {string[]} allowedKeys The list of allowed key names.
* @throws If there are extra keys.
*/
export function checkObjectHasNoAdditionalKeys(obj, allowedKeys) {
export function checkObjectHasNoAdditionalKeys(obj: object, allowedKeys: string[]): void {
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
@@ -254,7 +265,7 @@ export function checkObjectHasNoAdditionalKeys(obj, allowedKeys) {
* @param {Object} obj The object to deep copy.
* @return {Object} A copy of the object without any references to the original.
*/
export function deepCopy(obj) {
export function deepCopy(obj: object): object {
return JSON.parse(JSON.stringify(obj));
}
@@ -266,7 +277,7 @@ export function deepCopy(obj) {
*
* @return {boolean} true if the two objects are equal
*/
export function deepCompare(x, y) {
export function deepCompare(x: any, y: any): boolean {
// Inspired by
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
@@ -377,7 +388,7 @@ export function runPolyfills() {
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun/*, thisArg*/) {
Array.prototype.filter = function(fun: Function/*, thisArg*/) {
if (this === void 0 || this === null) {
throw new TypeError();
}
@@ -453,7 +464,7 @@ export function runPolyfills() {
// 8. Repeat, while k < len
while (k < len) {
var kValue, mappedValue;
let kValue, mappedValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
@@ -538,7 +549,7 @@ export function runPolyfills() {
// 7. Repeat, while k < len
while (k < len) {
var kValue;
let kValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
@@ -572,7 +583,7 @@ export function runPolyfills() {
* prototype.
* @param {function} superCtor Constructor function to inherit prototype from.
*/
export function inherits(ctor, superCtor) {
export function inherits(ctor: Function, superCtor: Function) {
// Add util.inherits from Node.js
// Source:
// https://github.com/joyent/node/blob/master/lib/util.js
@@ -596,7 +607,7 @@ export function inherits(ctor, superCtor) {
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
ctor.super_ = superCtor;
(ctor as any).super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
@@ -617,7 +628,7 @@ export function inherits(ctor, superCtor) {
* @param {any} SuperType The type to act as a super instance
* @param {any} params Arguments to supply to the super type's constructor
*/
export function polyfillSuper(thisArg, SuperType, ...params) {
export function polyfillSuper(thisArg: any, SuperType: any, ...params: any[]) {
try {
SuperType.call(thisArg, ...params);
} catch (e) {
@@ -633,7 +644,7 @@ export function polyfillSuper(thisArg, SuperType, ...params) {
* @param {*} value the value to test
* @return {boolean} whether or not value is a finite number without type-coercion
*/
export function isNumber(value) {
export function isNumber(value: any): boolean {
return typeof value === 'number' && isFinite(value);
}
@@ -643,7 +654,7 @@ export function isNumber(value) {
* @param {string} str the string to remove hidden characters from
* @return {string} a string with the hidden characters removed
*/
export function removeHiddenChars(str) {
export function removeHiddenChars(str: string): string {
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
}
@@ -656,11 +667,11 @@ export function removeHiddenChars(str) {
// Zero width no-break space (BOM) U+FEFF
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g;
export function escapeRegExp(string) {
export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function globToRegexp(glob, extended) {
export function globToRegexp(glob: string, extended: any): string {
extended = typeof(extended) === 'boolean' ? extended : true;
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
@@ -679,7 +690,7 @@ export function globToRegexp(glob, extended) {
return pat;
}
export function ensureNoTrailingSlash(url) {
export function ensureNoTrailingSlash(url: string): string {
if (url && url.endsWith("/")) {
return url.substr(0, url.length - 1);
} else {
@@ -688,13 +699,13 @@ export function ensureNoTrailingSlash(url) {
}
// Returns a promise which resolves with a given value after the given number of ms
export function sleep(ms, value) {
export function sleep<T>(ms: number, value: T): Promise<T> {
return new Promise((resolve => {
setTimeout(resolve, ms, value);
}));
}
export function isNullOrUndefined(val) {
export function isNullOrUndefined(val: any): boolean {
return val === null || val === undefined;
}
@@ -711,12 +722,15 @@ export function defer() {
return {resolve, reject, promise};
}
export async function promiseMapSeries(promises, fn) {
export async function promiseMapSeries<T>(
promises: Promise<T>[],
fn: (t: T) => void
): Promise<void> {
for (const o of await promises) {
await fn(await o);
}
}
export function promiseTry(fn) {
export function promiseTry<T>(fn: () => T): Promise<T> {
return new Promise((resolve) => resolve(fn()));
}