Compare commits

...

570 Commits

Author SHA1 Message Date
RiotRobot db7848c9ba v6.2.2 2020-06-16 11:00:41 +01:00
RiotRobot 97497ed9d5 Prepare changelog for v6.2.2 2020-06-16 11:00:41 +01:00
J. Ryan Stinnett 60fcc652de Merge pull request #1407 from matrix-org/jryans/1403-release
Use existing session id for fetching flows as to not get a new session
2020-06-16 10:56:00 +01:00
Michael Telatynski cb57717424 undo that because {} sux 2020-06-16 10:38:41 +01:00
Michael Telatynski 590f7786eb clean up 2020-06-16 10:38:41 +01:00
Michael Telatynski 0024edcb7f Use existing session id for fetching flows as to not get a new session id 2020-06-16 10:38:41 +01:00
RiotRobot 12b573bc63 v6.2.1 2020-06-05 15:07:04 +01:00
RiotRobot cc8c163e0f Prepare changelog for v6.2.1 2020-06-05 15:07:04 +01:00
Bruno Windels abc7f76679 Merge pull request #1399 from matrix-org/bwindels/backupformatbug-rc
Bring back backup key format migration
2020-06-05 13:32:58 +00:00
Bruno Windels eee04895fe take into account key can be an object now 2020-06-05 15:26:17 +02:00
Bruno Windels f520b88f79 fix type check in migration code 2020-06-05 15:26:17 +02:00
Bruno Windels f0f1c113e4 Revert "remove key backup format migration"
This reverts commit 8d81240c58.
2020-06-05 15:26:17 +02:00
Bruno Windels 84a15761ad Revert "lint"
This reverts commit 9fe0e1e85f.
2020-06-05 15:26:17 +02:00
RiotRobot 30720bfdd7 v6.2.0 2020-06-04 14:57:14 +01:00
RiotRobot 234a18f016 Prepare changelog for v6.2.0 2020-06-04 14:57:14 +01:00
RiotRobot 2c2d531e7f v6.2.0-rc.1 2020-06-02 13:48:12 +01:00
RiotRobot 91e0f7fbc4 Prepare changelog for v6.2.0-rc.1 2020-06-02 13:48:12 +01:00
J. Ryan Stinnett bebeec7d84 Merge pull request #1304 from MTRNord/fix-register-auth-with-new-spec
Make auth argument in the register request compliant with r0.6.0
2020-06-02 10:13:57 +01:00
Travis Ralston 86d448c285 Merge pull request #1393 from matrix-org/travis/uia-3pid
Send the wrong auth params with the right auth params
2020-06-01 09:14:39 -06:00
Travis Ralston 73f8867a6f Send the wrong auth params with the right auth params
Followon from https://github.com/matrix-org/matrix-react-sdk/pull/4667
2020-06-01 09:08:58 -06:00
Hubert Chathi 0d7cb2bf25 Merge pull request #1387 from matrix-org/uhoreg/picklekey
encrypt cached keys with pickle key
2020-05-28 13:51:25 -04:00
Hubert Chathi 991f590056 another unit fix test 2020-05-27 23:32:23 -04:00
Hubert Chathi 0e6758ccbc more unit test fixes 2020-05-27 23:21:15 -04:00
Hubert Chathi 1d9c6520e5 fix unit tests 2020-05-27 20:02:40 -04:00
Hubert Chathi c81f11df0a encrypt/decrypt cached private keys with pickle key 2020-05-27 19:34:28 -04:00
Travis Ralston f39a1e70de Fix the tests 2020-05-27 13:32:28 -06:00
Travis Ralston f0fa249d36 Request fresh flows on the initial registration request 2020-05-27 13:16:10 -06:00
Travis Ralston 8f72197817 Merge branch 'develop' into fix-register-auth-with-new-spec 2020-05-27 12:14:42 -06:00
Hubert Chathi 1556ac84da add a pickle key option to the client 2020-05-27 12:57:00 -04:00
David Baker b6da6bb835 Merge pull request #1385 from Sorunome/patch-1
Fix replying to key share requests
2020-05-26 12:10:44 +01:00
Sorunome b1a882ea79 Fix replying to key share requests
This check was meant to filter out key sharing requests from ourselves. Accidentally, it filtered out sharing requests from anyone *but* ourselves.
2020-05-26 13:04:31 +02:00
Michael Telatynski 3305f2cc72 Merge pull request #1384 from matrix-org/t3chguy/cdn
Add dist to package.json files so CDNs can serve it
2020-05-25 15:56:26 +01:00
Michael Telatynski c589a5211b Add dist to package.json files so CDNs can serve it
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-25 14:29:50 +01:00
Michael Telatynski ff0d91979b Merge pull request #1382 from matrix-org/t3chguy/fix_getVersion_warning
Fix getVersion warning saying undefined room
2020-05-22 14:44:11 +01:00
Michael Telatynski 9d6888cf74 Fix getVersion warning saying undefined room
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-22 14:21:51 +01:00
David Baker a4a7097c10 Merge pull request #1379 from matrix-org/dbkr/combine_default_rules_processing
Combine the two places we processed client-level default push rules
2020-05-21 15:11:46 +01:00
David Baker cf2b058e7c Update comment on default override rules 2020-05-21 10:06:12 +01:00
Hubert Chathi 048d7a9b63 Merge pull request #1378 from matrix-org/uhoreg/fix_mac_check
make MAC check robust against unpadded vs padded base64 differences
2020-05-20 12:05:35 -04:00
David Baker d6e37d0288 Combine the two places we processed client-level default push rules
We did this in two places: updated existing rules when the rules
were stored and then added new ones every time we computed push
action for an event. Just do both when the rules are saved.
2020-05-20 14:52:37 +01:00
Bruno Windels 3cd562fa96 Merge pull request #1375 from matrix-org/bwindels/remove-keybackup-format-migration
Remove key backup format migration
2020-05-20 08:05:28 +00:00
Michael Telatynski bd569cb041 Merge pull request #1241 from matrix-org/t3chguy/fix_unhomoglyph_import
Add simple browserify browser-matrix.js tests
2020-05-19 11:58:35 +01:00
RiotRobot e3c6a0e1a0 Merge branch 'master' into develop 2020-05-19 11:04:15 +01:00
RiotRobot b29176507c v6.1.0 2020-05-19 11:01:13 +01:00
RiotRobot db25e9719b Prepare changelog for v6.1.0 2020-05-19 11:01:13 +01:00
Michael Telatynski 661901b00d tidy up
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-18 20:11:32 +01:00
Michael Telatynski b9352cfcc1 Fix tests
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-18 20:03:04 +01:00
Michael Telatynski e381b1901e clean up test
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-18 19:39:30 +01:00
Hubert Chathi ebdff505eb make MAC check robust against unpadded vs padded base64 differences 2020-05-15 16:52:53 -04:00
Michael Telatynski f806e4342e Add simple browserify sync test
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-15 18:56:09 +01:00
Michael Telatynski 7f32d7d320 revert
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-15 16:55:00 +01:00
Michael Telatynski b1b4b21d45 Merge branches 'develop' and 't3chguy/fix_unhomoglyph_import' of github.com:matrix-org/matrix-js-sdk into t3chguy/fix_unhomoglyph_import
 Conflicts:
	src/utils.ts
2020-05-15 16:50:23 +01:00
Hubert Chathi 486e8b5445 Merge pull request #1376 from matrix-org/uhoreg/sas2
support new key agreement method for SAS
2020-05-15 10:59:11 -04:00
Bruno Windels 9fe0e1e85f lint 2020-05-15 13:44:10 +02:00
Bruno Windels 8d81240c58 remove key backup format migration 2020-05-15 12:37:41 +02:00
Hubert Chathi bdadcd4532 support new key agreement method for SAS 2020-05-14 16:14:45 -04:00
RiotRobot 1de9a24677 v6.1.0-rc.1 2020-05-14 16:06:17 +01:00
RiotRobot 6335f14a34 Prepare changelog for v6.1.0-rc.1 2020-05-14 16:06:16 +01:00
Bruno Windels da2ef381ac Merge pull request #1373 from matrix-org/bwindels/remove-asym-4s
Remove support for asymmetric 4S encryption
2020-05-14 08:10:01 +00:00
Bruno Windels 7b173f4c74 don't sign 4S keys anymore 2020-05-13 10:38:03 +02:00
Bruno Windels d49eb0bbc4 remove upgrade path from asym to sym 4S algorithm 2020-05-13 10:38:03 +02:00
Bruno Windels 91556d5bcd fix lint 2020-05-13 10:38:03 +02:00
Bruno Windels 039abe1f75 remove tests for upgrade path 2020-05-12 14:53:50 +02:00
Bruno Windels bc32faf467 don't sign/verify 4s key with cross signing master key anymore 2020-05-12 14:49:20 +02:00
Bruno Windels 74a4dfeb67 remove support for asym 4s enc 2020-05-12 14:29:54 +02:00
RiotRobot f120533fad Merge branch 'master' into develop 2020-05-05 11:01:32 +01:00
RiotRobot a1baf39299 v6.0.0 2020-05-05 10:54:38 +01:00
RiotRobot 9f0f1bcc68 Prepare changelog for v6.0.0 2020-05-05 10:54:37 +01:00
J. Ryan Stinnett 246963e181 Merge pull request #1368 from matrix-org/foldleft/13167-spinner-progress-rc
Add progress callback for key backups
2020-05-04 16:34:38 +01:00
Zoe e3134ab0de satisfy linter 2, now with more self-hatred 2020-05-04 16:25:43 +01:00
Zoe b38d52da72 satisfy the linter 2020-05-04 16:25:43 +01:00
Zoe 411c4f40d9 docs 2020-05-04 16:25:43 +01:00
Zoe 694a85b652 lint 2020-05-04 16:25:43 +01:00
Zoe c84e72f53a Added a progressCallback for backup key loading 2020-05-04 16:25:43 +01:00
David Baker cb7e1a9d82 Merge pull request #1367 from matrix-org/dbkr/increase_olm_second_phase_timeout
Increase timeout for 2nd phase of Olm session creation
2020-05-01 18:32:25 +01:00
David Baker 3c9dfc195e Increase timeout for 2nd phase of Olm session creation
The timeouts on the two phases of olm session creation are 2 and 10
seconds respectively, so sessions will fail if servers take more
than 10s to respond. Now that we have two phases, we can afford to
wait longer on the second one because the user's isn't waiting for
it to finish before the message will send, so increase it to 30s
so servers have more of a chance to respond.
2020-05-01 18:25:25 +01:00
David Baker a5c13041c6 Merge pull request #1366 from matrix-org/dbkr/retry_decryption_logging
Add logging on decryption retries
2020-05-01 17:49:13 +01:00
David Baker d699e98346 Add logging on decryption retries
For https://github.com/vector-im/riot-web/issues/13473
2020-05-01 17:36:49 +01:00
RiotRobot 135a76febd v6.0.0-rc.2 2020-05-01 16:18:21 +01:00
RiotRobot fae2856e7e Prepare changelog for v6.0.0-rc.2 2020-05-01 16:18:21 +01:00
David Baker 98426b6186 Merge pull request #1365 from matrix-org/dbkr/emit_on_store_trusted_master_key_rel
Emit event when a trusted self-key is stored
2020-05-01 15:37:09 +01:00
David Baker a63d9d2ccd lint 2020-05-01 15:28:15 +01:00
David Baker c567139e28 Emit event when a trusted self-key is stored
This will cause our own user trust status to change, so emit an
event to say so.
2020-05-01 15:28:09 +01:00
David Baker 3d495b0753 Merge pull request #1364 from matrix-org/dbkr/emit_on_store_trusted_master_key
Emit event when a trusted self-key is stored
2020-05-01 15:27:36 +01:00
David Baker 4946b55c21 lint 2020-05-01 15:13:02 +01:00
David Baker ad7d7154f4 Emit event when a trusted self-key is stored
This will cause our own user trust status to change, so emit an
event to say so.
2020-05-01 15:01:13 +01:00
Zoe e5b6a9f8cb Merge pull request #1352 from matrix-org/foldleft/12252-large-err
Customize error payload for oversized messages
2020-05-01 09:44:43 +01:00
Zoe 04f27d36ef Update src/crypto/OlmDevice.js
Co-authored-by: Travis Ralston <travpc@gmail.com>
2020-05-01 09:40:11 +01:00
J. Ryan Stinnett 683fc98fdc Merge pull request #1363 from matrix-org/jryans/key-backup-network-fail
Return null for key backup state when we haven't checked yet
2020-04-30 17:21:09 +01:00
J. Ryan Stinnett c303e83444 Return null for key backup state when we haven't checked yet
This allows distinguishing the "unknown" case from "not present".

Part of https://github.com/vector-im/riot-web/issues/13404
2020-04-30 15:40:27 +01:00
J. Ryan Stinnett ae5a40d686 Only consider key backup checked in error if 404
Other error codes are likely to mean we never actually reached the server at
all, so we should not consider that as checked.

Part of https://github.com/vector-im/riot-web/issues/13404
2020-04-30 15:22:50 +01:00
Zoe 409b7068bb Merge pull request #1351 from matrix-org/foldleft/13167-spinner-progress
Added a progressCallback for backup key loading
2020-04-30 14:09:16 +01:00
Zoe f2a12c7154 satisfy linter 2, now with more self-hatred 2020-04-30 14:06:20 +01:00
Zoe efac2eac07 satisfy the linter 2020-04-30 12:04:29 +01:00
RiotRobot f3f6f3e39a v6.0.0-rc.1 2020-04-30 11:14:04 +01:00
RiotRobot f905586dbd Prepare changelog for v6.0.0-rc.1 2020-04-30 11:14:03 +01:00
David Baker f393cea2c2 Merge pull request #1362 from matrix-org/dbkr/sessions_there_on_login_are_old_rel
Add initialFetch param to willUpdateDevices / devicesUpdated
2020-04-29 18:14:13 +01:00
David Baker e937998b40 Add initialFetch param to willUpdateDevices / devicesUpdated
This indicates whether the device database is being populated initially
2020-04-29 17:44:25 +01:00
David Baker f85ec08886 Merge pull request #1360 from matrix-org/dbkr/sessions_there_on_login_are_old
Add initialFetch param to willUpdateDevices / devicesUpdated
2020-04-29 17:42:12 +01:00
J. Ryan Stinnett 7ff68f3844 Merge pull request #1361 from matrix-org/bwindels/fixrequestreadyrace-rc
Fix race between sending .request and receiving .ready over to_device
2020-04-29 17:26:11 +01:00
Bruno Windels 04529bd524 add to_device verif request to txnId -> request map before sending
so if the .ready event arrives before the request finishes,
it still ends up in the correct VerificationRequest
2020-04-29 18:20:16 +02:00
Bruno Windels 2c681d93d9 Merge pull request #1359 from matrix-org/bwindels/fixrequestreadyrace
Fix race between sending .request and receiving .ready over to_device
2020-04-29 16:19:31 +00:00
David Baker 1451fcb040 Add initialFetch param to willUpdateDevices / devicesUpdated
This indicates whether the device database is being populated initially
2020-04-29 17:01:50 +01:00
Zoe 4986f3c2ca docs 2020-04-29 16:55:27 +01:00
Bruno Windels e9edb85a32 add to_device verif request to txnId -> request map before sending
so if the .ready event arrives before the request finishes,
it still ends up in the correct VerificationRequest
2020-04-29 17:51:32 +02:00
J. Ryan Stinnett dca60ae4ae Merge pull request #1358 from matrix-org/bwindels/fixwaitforeventrace-rc
Handle race between sending and await next event from other party
2020-04-29 15:21:35 +01:00
Bruno Windels 025f964b0b Merge pull request #1357 from matrix-org/bwindels/fixwaitforeventrace
Handle race between sending and await next event from other party
2020-04-29 13:53:35 +00:00
Bruno Windels ff46a8fa9e handle race between sending and await next event from other party 2020-04-29 15:52:20 +02:00
Bruno Windels 59412aba51 handle race between sending and await next event from other party 2020-04-29 15:45:51 +02:00
David Baker a351ee9f76 Merge pull request #1356 from matrix-org/dbkr/another_round_of_toast_rel
Add crypto.willUpdateDevices event and make getStoredDevices/getStoredDevicesForUser synchronous
2020-04-29 12:50:00 +01:00
David Baker a5b14092cd Make getStoredDevice calls sync 2020-04-29 12:23:03 +01:00
David Baker 90f1de9c3f Add crypto.willUpdateDevices event and make getStoredDevices/getStoredDevicesForUser synchronous
Add an event fired before devices are updated, so apps can monitor
changes in devices by observing the state before and after.

BREAKING CHANGE:
Make getStoredDevices/getStoredDevicesForUser synchronous so they
can safely be called from the event listener without racing with
the data being updated. They didn't do any async work so didn't need
to be async.

Add doc for crypto.devicesUpdated
2020-04-29 12:23:00 +01:00
David Baker a516f06946 Merge pull request #1354 from matrix-org/dbkr/another_round_of_toast
Add crypto.willUpdateDevices event and make getStoredDevices/getStoredDevicesForUser synchronous
2020-04-29 12:09:11 +01:00
David Baker 7c1f3e4519 Make getStoredDevice calls sync 2020-04-29 11:50:18 +01:00
Michael Telatynski 3bb1a6336b Merge pull request #1350 from matrix-org/t3chguy/redaction_redesign
Fix sender of local echo events in unsigned redactions
2020-04-29 11:17:28 +01:00
J. Ryan Stinnett 7dc926596f Merge pull request #1355 from matrix-org/jryans/rm-extra-key-backup-setup-rc
Remove redundant key backup setup path
2020-04-29 09:46:17 +01:00
J. Ryan Stinnett e775515c38 Remove the other copy instead 2020-04-28 21:34:17 +01:00
J. Ryan Stinnett c116f2b1bc Remove redundant key backup setup path
Bootstrap was create key backup twice due to some code duplication we missed in
previous refactorings.

Fixes https://github.com/vector-im/riot-web/issues/13423
2020-04-28 21:34:17 +01:00
J. Ryan Stinnett f24993d6aa Merge pull request #1353 from matrix-org/jryans/rm-extra-key-backup-setup
Remove redundant key backup setup path
2020-04-28 21:33:49 +01:00
David Baker f053cf1fdb Add crypto.willUpdateDevices event and make getStoredDevices/getStoredDevicesForUser synchronous
Add an event fired before devices are updated, so apps can monitor
changes in devices by observing the state before and after.

BREAKING CHANGE:
Make getStoredDevices/getStoredDevicesForUser synchronous so they
can safely be called from the event listener without racing with
the data being updated. They didn't do any async work so didn't need
to be async.

Add doc for crypto.devicesUpdated
2020-04-28 18:31:04 +01:00
J. Ryan Stinnett 251318eaf0 Remove the other copy instead 2020-04-28 17:20:49 +01:00
J. Ryan Stinnett a66e7d79ad Remove redundant key backup setup path
Bootstrap was create key backup twice due to some code duplication we missed in
previous refactorings.

Fixes https://github.com/vector-im/riot-web/issues/13423
2020-04-28 16:56:37 +01:00
Zoe 38884083a2 Customize error payload for oversized messages 2020-04-28 16:39:35 +01:00
Zoe 0b1088d9a8 lint 2020-04-28 11:31:41 +01:00
Zoe 875f9ca388 Added a progressCallback for backup key loading 2020-04-28 11:28:39 +01:00
David Baker 25cd1f25f1 Merge pull request #1349 from matrix-org/dbkr/retryDecryption_remove_dead_code
Remove some dead code from _retryDecryption
2020-04-28 11:23:53 +01:00
Michael Telatynski 7152ece70a Fix sender of local echo events in unsigned redactions
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-04-27 23:52:22 +01:00
David Baker 896c76ce9d Merge pull request #1348 from matrix-org/dbkr/keyrequest_wait_sync_process
Don't send key requests until after sync processing is finished
2020-04-24 14:38:54 +01:00
David Baker 21d3dd4506 Remove some dead code from _retryDecryption
I think this was attempting to remove the events from _pendingEvents
but a) it wasn't and b) it probably shouldn't be because the retry
itself will re-add them when the decryption attempt starts and remove
them if it succeeds.

Also fix what was presumably a c+p fail.
2020-04-24 14:36:42 +01:00
David Baker 300b32c8d7 Let's keep starting the timer for the retries straight away
rather than leaving them until next time the queue is sent
2020-04-24 13:16:07 +01:00
David Baker 19e3b35fc7 Fix immediate key request sending 2020-04-24 11:26:48 +01:00
David Baker 320811f9ed Don't send key requests until after sync processing is finished
Key requests wait for a short time (500ms) before being sent as
an attempt to wait for the key to arrive before sending a request
for it, but client startup takes way longer than this so this
would still result in key requests being sent for keys that we'd
fetched but were still waiting to be read out of the sync response
and put into the database.
2020-04-24 11:19:52 +01:00
David Baker 991457496f Merge pull request #1346 from matrix-org/dbkr/dont_talk_to_ourself
Prevent attempts to send olm messages to ourselves
2020-04-23 16:10:36 +01:00
RiotRobot 1cb6134758 v5.3.1-rc.4 2020-04-23 15:41:07 +01:00
RiotRobot a255b8e450 Prepare changelog for v5.3.1-rc.4 2020-04-23 15:41:07 +01:00
Bruno Windels 324f036b35 Merge pull request #1347 from matrix-org/bwindels/retry4s-rc
Retry account data upload requests
2020-04-23 14:33:36 +00:00
Bruno Windels 479a284e10 move fn to http-api and add jsdoc 2020-04-23 16:29:08 +02:00
Bruno Windels e0660ce01c log while retrying 2020-04-23 16:29:07 +02:00
Bruno Windels 3a3f6cb7ca cleanup, return straight away 2020-04-23 16:29:07 +02:00
Bruno Windels 9797e6021b exclude aborted requests and matrix errors when creating connectionerror
- throw an AbortError when aborting is detected
 - don't turn AbortError's into a ConnectionError
 - also consider the string "aborted" an AbortError for unit tests
 - unit tests like to reject the request with a MatrixError,
   so exclude those too
2020-04-23 16:29:07 +02:00
Bruno Windels a9212d33b1 retry PUT account data requests 2020-04-23 16:29:07 +02:00
Bruno Windels d32d3fd864 retry util function 2020-04-23 16:29:07 +02:00
Bruno Windels a19cdd06cf throw ConnectionError when request fails 2020-04-23 16:29:07 +02:00
Bruno Windels f4d0f89fda cleanup: use class for MatrixError
this way we get stacktraces as well
2020-04-23 16:29:07 +02:00
Bruno Windels ca34cb951e Merge pull request #1345 from matrix-org/bwindels/retry4s
Retry account data upload requests
2020-04-23 14:28:31 +00:00
David Baker 4c2d1b0385 typo
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-04-23 14:40:17 +01:00
Bruno Windels 6b4fefc123 move fn to http-api and add jsdoc 2020-04-23 15:32:40 +02:00
Bruno Windels 86913dccb0 log while retrying 2020-04-23 15:25:10 +02:00
Bruno Windels 30101a4428 cleanup, return straight away 2020-04-23 15:22:46 +02:00
Bruno Windels 668e8f6f24 exclude aborted requests and matrix errors when creating connectionerror
- throw an AbortError when aborting is detected
 - don't turn AbortError's into a ConnectionError
 - also consider the string "aborted" an AbortError for unit tests
 - unit tests like to reject the request with a MatrixError,
   so exclude those too
2020-04-23 15:18:52 +02:00
David Baker 2f51e206c7 Prevent attempts to send olm messages to ourselves
The cause of this I've seen is us sending keyshare reqiests when
we didn't have keys but them arriving when we do, and us replying to
ourseles with keys, so exclude that explicity.

Also catch this lower down to catch any other code paths that
mistaklenly try this. Hopefully the comment explains enough as to why
it won't work.
2020-04-23 13:36:54 +01:00
Bruno Windels 78b8b36a87 retry PUT account data requests 2020-04-23 13:37:56 +02:00
Bruno Windels af3ef86d19 retry util function 2020-04-23 13:37:33 +02:00
Bruno Windels 50febaf477 throw ConnectionError when request fails 2020-04-23 13:37:10 +02:00
Bruno Windels 907f317b04 cleanup: use class for MatrixError
this way we get stacktraces as well
2020-04-23 13:36:40 +02:00
David Baker 6edb78d015 Merge pull request #1344 from matrix-org/dbkr/log_megolm_session_first_index
Log first known index with megolm session updates
2020-04-23 12:30:48 +01:00
David Baker b58846ab6e Log first known index with megolm session updates
Move the logging lower to a point where we know the first index.
2020-04-23 11:43:22 +01:00
David Baker 32233a2c7b Merge pull request #1343 from matrix-org/dbkr/prune_todevice_messages
Prune to_device messages to avoid sending empty messages
2020-04-22 13:54:29 +01:00
David Baker 8b2752441d Prune to_device messages to avoid sending empty messages
Fixes https://github.com/vector-im/riot-web/issues/13322
2020-04-22 13:48:57 +01:00
Michael Telatynski e6af29df94 Merge pull request #1335 from matrix-org/t3chguy/ts1
Convert bunch of things to TypeScript
2020-04-22 10:26:53 +01:00
David Baker 4ae5404fe4 Merge pull request #1342 from matrix-org/dbkr/log_new_olm_sessions
Add logging when making new Olm sessions
2020-04-22 09:47:44 +01:00
David Baker 7f0bdc8ddb Add logging when making new Olm sessions 2020-04-21 18:31:36 +01:00
Bruno Windels 39836f115b Merge pull request #1341 from matrix-org/bwindels/fixfilterlookup-rc
Fix: handle filter not found
2020-04-21 14:15:03 +00:00
Bruno Windels 4a699fe6a7 Merge pull request #1340 from matrix-org/bwindels/fixfilterlookup
Fix: handle filter not found
2020-04-21 14:13:33 +00:00
David Baker 7cc61a887c Merge pull request #1339 from matrix-org/dbkr/getAccountDataFromServer_404_rel
Make getAccountDataFromServer return null if not found
2020-04-21 15:10:20 +01:00
David Baker 8f73f1ed3c Merge pull request #1338 from matrix-org/dbkr/getAccountDataFromServer_404
Make getAccountDataFromServer return null if not found
2020-04-21 15:10:13 +01:00
Bruno Windels 67fb027d39 filter out invalid filter id values like 'undefined'
this way we don't attempt to fetch filters when validating them
with filterId==="undefined"
2020-04-21 16:00:45 +02:00
Bruno Windels c6b61ea0ea Fix unknown filters returning 400 rather than 404
This made getFilter in sync retrying continuously
I also refactored getOrCreateFilter to use await
2020-04-21 16:00:45 +02:00
Bruno Windels 32841234a7 filter out invalid filter id values like 'undefined'
this way we don't attempt to fetch filters when validating them
with filterId==="undefined"
2020-04-21 15:56:39 +02:00
Bruno Windels 6f6520ed0f Fix unknown filters returning 400 rather than 404
This made getFilter in sync retrying continuously
I also refactored getOrCreateFilter to use await
2020-04-21 15:54:50 +02:00
David Baker b3860f3754 Make getAccountDataFromServer return null if not found
Callers assume that if the account data isn't there, the method will
return null rather than throw an exception. This meant that this only
actually worked in these cases when the sync had completed and it was
using the locally stored account data, so this ceased to be a
problem in practice when we made it wait for the initial sync to finish
before entering the security setup flow.

Fixes https://github.com/vector-im/riot-web/issues/13169
2020-04-21 14:32:26 +01:00
David Baker 0958917317 Make getAccountDataFromServer return null if not found
Callers assume that if the account data isn't there, the method will
return null rather than throw an exception. This meant that this only
actually worked in these cases when the sync had completed and it was
using the locally stored account data, so this ceased to be a
problem in practice when we made it wait for the initial sync to finish
before entering the security setup flow.

Fixes https://github.com/vector-im/riot-web/issues/13169
2020-04-21 14:28:10 +01:00
David Baker f42fa7e791 Merge pull request #1337 from matrix-org/dbkr/setdefaultkey_fail_rel
Fix setDefaultKeyId to fail if the request fails
2020-04-21 14:21:42 +01:00
David Baker 7022d1f3bf Merge pull request #1336 from matrix-org/dbkr/setdefaultkey_fail
Fix setDefaultKeyId to fail if the request fails
2020-04-21 14:21:35 +01:00
David Baker b592c41f96 Fix setDefaultKeyId to fail if the request fails
It returned only when the echo came down the sync stream, but we
forgot to make it fail if the reuqest failed and it would just
never return in this case.

Fixes https://github.com/vector-im/riot-web/issues/13162
2020-04-21 13:52:48 +01:00
David Baker 62188571d7 Fix setDefaultKeyId to fail if the request fails
It returned only when the echo came down the sync stream, but we
forgot to make it fail if the reuqest failed and it would just
never return in this case.

Fixes https://github.com/vector-im/riot-web/issues/13162
2020-04-21 13:50:21 +01:00
Michael Telatynski 8cca3392f6 fix localStorage type
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-04-21 12:03:25 +01:00
Michael Telatynski 7082516664 Improve ts types and enable esModuleInterop
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-04-21 11:55:56 +01:00
Michael Telatynski 77a1fc9e60 Convert bunch of things to TypeScript
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-04-21 11:45:54 +01:00
Travis Ralston e2b1dd6532 Merge pull request #1328 from MichaelKohler/issue905-setRoomEncryption-doc
Document setRoomEncryption not modifying room state
2020-04-21 00:30:14 -06:00
Bruno Windels 7b7fdb0a65 Merge pull request #1333 from matrix-org/bwindels/onlyonefilterplease-rc
Fix: don't do extra /filter request when enabling lazy loading of members
2020-04-20 16:36:26 +00:00
Bruno Windels 414279f644 Merge pull request #1332 from matrix-org/bwindels/onlyonefilterplease
Fix: don't do extra /filter request when enabling lazy loading of members
2020-04-20 16:24:52 +00:00
Bruno Windels be91c8580d rename build function 2020-04-20 18:22:30 +02:00
Bruno Windels befaa782f6 when enabling LL, set the flag on Filter, rather than doing /filter 2020-04-20 18:22:30 +02:00
Bruno Windels 8e29dae36a rename build function 2020-04-20 18:06:10 +02:00
Bruno Windels d2438d10a6 when enabling LL, set the flag on Filter, rather than doing /filter 2020-04-20 17:26:19 +02:00
David Baker b3752bb2c6 Merge pull request #1331 from matrix-org/dbkr/fix_reject_no_auth_flow_found_rel
Reject attemptAuth promise if no auth flow found
2020-04-20 13:38:49 +01:00
Michael Kohler 02e145b0de Reject attemptAuth promise if no auth flow found 2020-04-20 13:34:18 +01:00
David Baker 0fcf8777a1 Merge pull request #1329 from MichaelKohler/fix-reject-no-auth-flow-found
Reject attemptAuth promise if no auth flow found
2020-04-20 13:06:44 +01:00
J. Ryan Stinnett 26cae5543b Merge pull request #1330 from matrix-org/dbkr/serialise_prekey_decryptions_rel
Serialise Olm prekey decryptions
2020-04-20 10:14:24 +01:00
David Baker b0573dec77 Add a catch so the error doesn't propagate to later decryptions 2020-04-20 10:08:13 +01:00
David Baker c5f4e762e5 Serialise Olm prekey decryptions
If they overlap, they can both try to create new sessions, but this
removes the OTK so it will only work once.

Fixes https://github.com/vector-im/riot-web/issues/13229
2020-04-20 10:08:07 +01:00
Michael Kohler f4c08477d0 Reject attemptAuth promise if no auth flow found 2020-04-19 14:04:10 +02:00
Michael Kohler 4342ee6d4a Document setRoomEncrption not modifying room state (fixes #905) 2020-04-18 19:49:05 +02:00
Michael Telatynski 7b73561923 Merge pull request #1327 from nchamo/fix-filter-component-check
Fix FilterComponent allowed_values check
2020-04-18 14:45:31 +01:00
Nicolas Chamo 87c0cf233c Update src/filter-component.js
Co-Authored-By: Michael Telatynski <7t3chguy@googlemail.com>
2020-04-17 23:59:38 -03:00
Nicolas Chamo 145cd7894b More lint fixes 2020-04-17 19:56:32 -03:00
Nicolas Chamo 6cf8a76c29 linting 2020-04-17 19:50:15 -03:00
nchamo 7ea09ebe4a Add fix and test
Signed-off-by: Nicolas Chamo <nicolas@chamo.com.ar>
2020-04-17 19:37:33 -03:00
David Baker 1dcb16365b Merge pull request #1326 from matrix-org/dbkr/serialise_prekey_decryptions
Serialise Olm prekey decryptions
2020-04-17 18:07:24 +01:00
David Baker 83b33d9d7a Add a catch so the error doesn't propagate to later decryptions 2020-04-17 18:03:15 +01:00
David Baker 83879fa945 Serialise Olm prekey decryptions
If they overlap, they can both try to create new sessions, but this
removes the OTK so it will only work once.

Fixes https://github.com/vector-im/riot-web/issues/13229
2020-04-17 17:47:46 +01:00
RiotRobot c3903f2796 v5.3.1-rc.3 2020-04-17 17:35:49 +01:00
RiotRobot 18564553ad Prepare changelog for v5.3.1-rc.3 2020-04-17 17:35:48 +01:00
J. Ryan Stinnett fe3e87a9d6 Merge pull request #1323 from matrix-org/dbkr/fix_ssss_reset_rel
Fix cross-signing/SSSS reset
2020-04-17 13:42:47 +01:00
Bruno Windels 78c734d2e9 Merge pull request #1325 from matrix-org/bwindels/fixkeybackupcrash-rc
Fix: crash when backup key needs fixing from corruption issue
2020-04-17 12:23:08 +00:00
Bruno Windels 5afa16d454 method is on crypto 2020-04-17 14:19:55 +02:00
Bruno Windels 57233416d9 Merge pull request #1324 from matrix-org/bwindels/fixkeybackupcrash
Fix: crash when backup key needs fixing from corruption issue
2020-04-17 12:11:07 +00:00
Bruno Windels f56ce29210 method is on crypto 2020-04-17 13:51:12 +02:00
David Baker 149f59e9be lint 2020-04-16 20:42:00 +01:00
David Baker 761892d6b1 Fix cross-signing/SSSS reset
We re-used the old SSSS key even when resetting, meaning we prompted
the user to create a new passphrase but then ignored it and kept using
the old one.

Fixes https://github.com/vector-im/riot-web/issues/13212
2020-04-16 20:41:55 +01:00
David Baker 4b639d5f9a Merge pull request #1322 from matrix-org/dbkr/fix_ssss_reset
Fix cross-signing/SSSS reset
2020-04-16 20:38:47 +01:00
David Baker da8ed77aae lint 2020-04-16 20:18:27 +01:00
David Baker 12f46909f7 Fix cross-signing/SSSS reset
We re-used the old SSSS key even when resetting, meaning we prompted
the user to create a new passphrase but then ignored it and kept using
the old one.

Fixes https://github.com/vector-im/riot-web/issues/13212
2020-04-16 20:13:18 +01:00
RiotRobot faf18b1996 v5.3.1-rc.2 2020-04-16 18:39:06 +01:00
RiotRobot 218deddd00 Prepare changelog for v5.3.1-rc.2 2020-04-16 18:39:06 +01:00
Bruno Windels d6fe650c4c Merge pull request #1321 from matrix-org/bwindels/untrustedqrcodereciprocate-rc
Implement QR code reciprocate for self-verification with untrusted MSK
2020-04-16 12:38:04 +00:00
Bruno Windels 919189db1a Merge pull request #1320 from matrix-org/bwindels/untrustedqrcodereciprocate
Implement QR code reciprocate for self-verification with untrusted MSK
2020-04-16 12:34:46 +00:00
Bruno Windels 951191d99a update comments 2020-04-16 14:31:44 +02:00
Bruno Windels f814a96eac update comments 2020-04-16 14:30:51 +02:00
Bruno Windels 2654f4bf0f implement qr code reciprocate for self-verif with untrusted MSK 2020-04-16 13:26:56 +02:00
Bruno Windels 218aa423c3 implement qr code reciprocate for self-verif with untrusted MSK 2020-04-16 13:01:46 +02:00
RiotRobot 195f3a7550 v5.3.1-rc.1 2020-04-15 19:05:12 +01:00
RiotRobot 1c9343ed8f Prepare changelog for v5.3.1-rc.1 2020-04-15 19:05:11 +01:00
RiotRobot 5c7d9981f8 Merge branch 'release-v5.3.0' into release-v5.3.1 2020-04-15 19:02:40 +01:00
J. Ryan Stinnett 63bc17a6b4 Merge pull request #1319 from matrix-org/jryans/release-script
Adapt release script for riot-desktop
2020-04-15 18:26:04 +01:00
J. Ryan Stinnett 629490c4ae Adapt release script for riot-desktop
This allows the bulk of the release script to be used for riot-desktop, while
skipping the NPM and develop branch parts that do not apply.

Part of https://github.com/vector-im/riot-web/issues/13176
2020-04-15 18:10:46 +01:00
Bruno Windels d59d62f96a Merge pull request #1318 from matrix-org/bwindels/fixindexernotifs
Fix: prevent spurious notifications from indexer
2020-04-15 16:50:18 +00:00
Bruno Windels 8a460c2368 fix jsdoc 2020-04-15 18:33:55 +02:00
Bruno Windels 6f60183316 allow creating event mappers that map events whose events are not reemitted 2020-04-15 17:08:25 +02:00
J. Ryan Stinnett a21c62519b Merge pull request #1317 from matrix-org/jryans/gen-user-obj
Always create our own user object
2020-04-14 16:19:45 +01:00
J. Ryan Stinnett 1b8f6d1739 Always create our own user object
This ensures our own user object is always available for tasks like
verification, even if you aren't in any rooms.

Fixes https://github.com/vector-im/riot-web/issues/13165
2020-04-14 16:02:00 +01:00
Hubert Chathi e087bce61a Merge pull request #1311 from matrix-org/uhoreg/fix_backup_key_format
Fix incorrect backup key format in SSSS
2020-04-13 17:38:45 -04:00
Hubert Chathi d2f24c3e87 cut long line to appease lint 2020-04-13 17:03:17 -04:00
Hubert Chathi 5d606bba66 add test for passthrough on backups 2020-04-13 16:54:02 -04:00
Hubert Chathi 864fe459b7 improve readability of tests 2020-04-13 16:27:46 -04:00
Marcel ea8362ed63 Keep the null in authData if this is the first stage. 2020-04-10 19:32:50 +02:00
Marcel 3a1508c2ab Fix "should make a request if no authdata is provided" test 2020-04-10 19:15:21 +02:00
Marcel f914391aaf Revert hack to set auth = null 2020-04-10 18:31:35 +02:00
Bruno Windels 8992438aa6 Merge pull request #1315 from matrix-org/bwindels/fixe2eecrash
Fix e2ee crash after refreshing after having received a cross-singing key reset
2020-04-10 10:20:20 +00:00
Bruno Windels 197d5ebb44 fix e2ee crash after refreshing after cross-singing key reset 2020-04-10 10:37:12 +02:00
Hubert Chathi 4039498eee use cached backup key if available 2020-04-09 17:05:01 -04:00
Bruno Windels a686231a5b Merge pull request #1314 from matrix-org/bwindels/catchverifysenderrors
Fix: catch send errors in SAS verifier
2020-04-09 16:32:09 +00:00
Bruno Windels 97cf4bff1f fix tests 2020-04-09 18:21:20 +02:00
Bruno Windels 11727833a2 mark request as cancelled immediately after verifier is cancelled
in case send errors prevent us from receiving remote echo
2020-04-09 17:54:45 +02:00
Hubert Chathi df38fde336 apply changes from code review 2020-04-09 11:48:58 -04:00
Bruno Windels 00233d610b pass on send errors so request gets cancelled 2020-04-09 17:23:44 +02:00
Bruno Windels d1c9030fac Merge pull request #1312 from matrix-org/bwindels/fixresetkeys
Clear cross-signing keys when detecting the keys have changed
2020-04-09 13:34:26 +00:00
Bruno Windels 70071eef41 also emit user trust has changed 2020-04-09 14:09:51 +02:00
Bruno Windels 65dd56f53a remove obsolete comment 2020-04-09 13:27:13 +02:00
Bruno Windels c8fb4af369 fix comment style 2020-04-09 13:13:17 +02:00
Bruno Windels 9e1ba992e3 Clear cross-signing keys when detecting the keys have changed 2020-04-09 12:24:29 +02:00
J. Ryan Stinnett bad09fed44 Merge pull request #1310 from matrix-org/jryans/upgrade-2020-04-08
Upgrade deps
2020-04-09 10:19:09 +01:00
Hubert Chathi 5bd146bb85 lint 2020-04-09 00:21:31 -04:00
Hubert Chathi 75703f273f fix unit tests 2020-04-09 00:18:21 -04:00
Hubert Chathi ca7b49d209 fix incorrect backup key format in SSSS 2020-04-09 00:08:17 -04:00
Marcel 379322db0d Revert setting authDict = null by default
Remove console.log
Revert null checks on this._data
2020-04-08 20:50:59 +02:00
J. Ryan Stinnett 30bce8a682 yarn upgrade 2020-04-08 14:41:36 +01:00
RiotRobot f413f0ee5f v5.3.0-rc.1 2020-04-08 13:46:11 +01:00
RiotRobot be6778a931 Prepare changelog for v5.3.0-rc.1 2020-04-08 13:46:10 +01:00
J. Ryan Stinnett 84637c6ebd Merge pull request #1308 from matrix-org/jryans/backup-key-cache-format
Store key backup key in cache as Uint8Array
2020-04-08 11:32:18 +01:00
J. Ryan Stinnett 2ed6e9ba2f Convert recovery key to Uint8Array explicitly 2020-04-08 00:43:52 +01:00
J. Ryan Stinnett 2e3cb54d38 Restore redundant conversion for tests 2020-04-07 19:21:25 +01:00
J. Ryan Stinnett 0efac7eae0 Store key backup key in cache as Uint8Array
Bootstrap was incorrectly storing the key backup key as base64 encoded, when
instead it should be a uint8Array.

Fixes https://github.com/vector-im/riot-web/issues/13057
2020-04-07 18:28:48 +01:00
J. Ryan Stinnett dc56f05d03 Throw error when trying to cache keys in the wrong format 2020-04-07 18:06:57 +01:00
Hubert Chathi 4d2625278c Merge pull request #1307 from MTRNord/fix-keys_query_endpoint
Use the correct request body for the /keys/query endpoint.
2020-04-07 12:01:06 -04:00
Marcel bff8a947a1 Fix expectBobQueryKeys test 2020-04-07 16:53:28 +02:00
Marcel 9cb7406ebd Fix downloadKeys test 2020-04-07 16:44:30 +02:00
Marcel 38681ca6ca Fix expectAliQueryKeys test 2020-04-07 16:31:15 +02:00
Marcel 916696c906 Fix expectKeyQuery test 2020-04-07 16:19:46 +02:00
Marcel 1b3c4c935e Use the correct request body for the /keys/query endpoint.
https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-keys-query
2020-04-07 14:08:49 +02:00
Marcel bf6d1f6555 Fix interactive-auth.js lint 2020-04-07 13:26:27 +02:00
Marcel 9faeac4c83 Merge branch 'develop' into fix-register-auth-with-new-spec 2020-04-07 13:15:51 +02:00
Marcel a863cf3d72 Fix auth linter error and tests 2020-04-07 13:13:49 +02:00
David Baker 66417e6742 Merge pull request #1305 from matrix-org/dbkr/uia_ignore_poll_after_success
Avoid creating two devices on registration
2020-04-07 10:32:08 +01:00
David Baker 6ad4d6dd62 also fix typo 2020-04-07 10:21:57 +01:00
David Baker 59607f8dbf Clear resolve / reject function after rejecting too 2020-04-07 10:20:58 +01:00
David Baker 62380a48d5 Merge pull request #1306 from matrix-org/dbkr/max_warnings_81
Lower max-warnings to 81
2020-04-07 10:19:45 +01:00
David Baker 4245372495 Lower max-warnings to 81
We apparently fixed 11 warnings at some point. Let's keep it that way.
2020-04-07 10:06:23 +01:00
David Baker 964a448470 Avoid creating two devices on registration
Ignore poll requests after a UIA session has finished. Sometimes
the app calls poll() even after the registration has succeeded
which causes the API call to happen again which, in the case of
registration, creates a phantom device.

Fixes https://github.com/vector-im/riot-web/issues/13040
2020-04-06 19:45:18 +01:00
J. Ryan Stinnett 2151f28d4c Merge pull request #1303 from matrix-org/jryans/backup-key-not-cached
Move key backup key creation before caching
2020-04-06 18:41:58 +01:00
Marcel 45a92bcf9c Fix auth check as auth == {} not works 2020-04-06 17:30:32 +02:00
Marcel 0a0441756b Fix auth check 2020-04-06 17:23:24 +02:00
Marcel c1de2ebbf9 Fix authdata to only be null if nothing had been added. 2020-04-06 17:14:43 +02:00
J. Ryan Stinnett 060549656e Move key backup key creation before caching
For some reason, we were trying to cache the key backup key before it was
created, and as a result we wouldn't cache the key until perhaps a second time
through bootstrapping.

Fixes https://github.com/vector-im/riot-web/issues/13023
2020-04-06 16:03:24 +01:00
Marcel 557c2db955 Handle Error during user-interactive auth: TypeError: "this._data is null" 2020-04-06 15:37:52 +02:00
Marcel ed8d064a13 Replace {} with null to be compliant with newer specs. While older also accepted null for the auth part of the registration request object 2020-04-06 15:22:04 +02:00
Zoe 7dabf507a2 Merge pull request #1298 from matrix-org/foldleft/12843-resend-requests
Expose function to force-reset outgoing room key requests
2020-04-06 13:43:06 +01:00
Zoe d69afa47a1 review feedback 2020-04-06 13:30:36 +01:00
Bruno Windels 275a8175aa Merge pull request #1302 from matrix-org/bwindels/selfverifux
Add isSelfVerification property to VerificationRequest
2020-04-03 16:02:05 +00:00
Bruno Windels 40bebf4cbd Merge pull request #1297 from matrix-org/bwindels/qr-reciprocate
QR code reciprocation
2020-04-03 12:28:34 +00:00
Zoe b8bc323c03 add a crypto-level function for resending key requests 2020-04-03 11:47:13 +01:00
Zoe c6d32ea2b0 Expose function to force-reset outgoing room key requests 2020-04-02 16:25:50 +01:00
Bruno Windels 2ace35ad6b add isSelfVerification helper 2020-04-02 17:16:06 +02:00
Bruno Windels b5ea8d3a78 Merge branch 'bwindels/qr-reciprocate' into bwindels/selfverifux 2020-04-02 17:15:32 +02:00
Bruno Windels f3a05f6ed0 fix jsdoc 2020-04-02 13:37:15 +02:00
Bruno Windels 43eae4929b remove ts annotations as I don't want to convert the whole file now 2020-04-02 13:31:17 +02:00
Bruno Windels 6144962c24 cancel with m.user code when user doesn't reciprocate 2020-04-02 12:53:52 +02:00
Bruno Windels 544cc36006 explain a bit why we're doing things here 2020-04-02 11:31:44 +02:00
Hubert Chathi 1834e6688f Merge pull request #1294 from matrix-org/uhoreg/check_sssss_passphrase
Add ability to check symmetric SSSS key before we try to use it
2020-04-01 16:01:34 -04:00
David Baker e1ee56aa43 Merge pull request #1296 from matrix-org/dbkr/debug_stuck_events
Add some debug logging for events stuck to bottom of timeline
2020-04-01 18:54:52 +01:00
David Baker a1779719be Add some debug logging for events stuck to bottom of timeline 2020-04-01 18:48:59 +01:00
Bruno Windels 0e464af627 Merge pull request #1295 from matrix-org/bwindels/fixdoublerequestexplosion
Fix: spontanous verification request cancellation under some circumstances
2020-04-01 17:17:24 +00:00
Bruno Windels fd7f0c3b5a Ignore phase while checking if other party supports scanning
(as the phase hasn't been set yet)

Also, set the qrCodeData in handleEvent as we need it to be async
2020-04-01 19:01:53 +02:00
Bruno Windels ed210a4fb1 Use static constructor that can be async (as looking up device is async)
Also fix static methods not to use this
2020-04-01 19:00:57 +02:00
Bruno Windels c2a0504980 update comments 2020-04-01 18:23:34 +02:00
Bruno Windels b6708871d3 fix olmlib import 2020-04-01 18:23:34 +02:00
Bruno Windels 275ea6aacb spec recommends 11 random bytes for shared secret 2020-04-01 18:23:34 +02:00
Bruno Windels f97d413603 expose chosen method after request has started
so we can show a different ux now that it's not just SAS anymore,
but also reciprocate
2020-04-01 18:23:34 +02:00
Bruno Windels 2c7ea0606b move shared secret to QRCodeData 2020-04-01 18:23:34 +02:00
Bruno Windels d7a7100912 reuse keys from qr code data to sign
so we don't sign the wrong keys if a malicious HS admin
forged a cross-sign identity reset
2020-04-01 18:23:34 +02:00
Bruno Windels 2c54b8d77e ask the user if the other user scanned the QR code 2020-04-01 18:23:34 +02:00
Bruno Windels b30f278e03 checking the userid isn't a thing anymore AFAIK 2020-04-01 18:23:34 +02:00
Bruno Windels b642030a34 keep a copy of the master/device key and mode, for use in reciprocate 2020-04-01 18:23:34 +02:00
Bruno Windels 725976d472 Add QR code buffer generation to VerificationRequest (from react-sdk)
Doing this so later we can keep the other user master signing key on it
so we can make sure we cross-sign that one once we decide to sign.
2020-04-01 18:23:34 +02:00
Bruno Windels 80d87e1bf1 prevent the same event being handled twice in the verification request, result in cancellation 2020-04-01 17:13:24 +02:00
Hubert Chathi d01f527e71 only try to set keyInfo if it exists 2020-03-31 16:54:06 -04:00
Hubert Chathi 3e68c82171 apply changes from code review 2020-03-31 16:34:41 -04:00
Hubert Chathi 51bde23207 Merge branch 'develop' into uhoreg/check_sssss_passphrase 2020-03-31 10:41:39 -04:00
J. Ryan Stinnett 934ed37fdc Merge pull request #1293 from matrix-org/jryans/4s-prompt-overload
Receive private key for caching from the app layer
2020-03-31 11:04:51 +01:00
J. Ryan Stinnett 4a965f5408 Convert secret storage key creation to object 2020-03-31 10:45:02 +01:00
J. Ryan Stinnett 6343da33c3 Merge pull request #1292 from matrix-org/jryans/id-change-red
Track whether we have verified a user before
2020-03-31 10:07:20 +01:00
Hubert Chathi e6edee863f add ability to check symmetric SSSS key before we try to use it 2020-03-30 17:25:03 -04:00
J. Ryan Stinnett 9a1d62438d Receive private key for caching from the app layer
This passes through the new secret storage key as well as the key info so that
bootstrap can use it immediately without triggering an immediate prompt.

Part of https://github.com/vector-im/riot-web/issues/12867
2020-03-30 21:46:22 +01:00
J. Ryan Stinnett d2ba3039c7 Add missing awaits in bootstrap path 2020-03-30 21:46:22 +01:00
J. Ryan Stinnett b6ad4c10bc Track whether we have verified a user before
This tracks whether we have ever cross-signing verified a user before (at least
as far as the current device has ever observed). This info helps to present an
alert in case the user subsequently becomes unverified.

Part of https://github.com/vector-im/riot-web/issues/12808
2020-03-30 14:51:49 +01:00
J. Ryan Stinnett 93954d314e Remove unused variable in device list 2020-03-30 14:51:49 +01:00
J. Ryan Stinnett 282f85f1dd Add missing await when changing key IDs 2020-03-30 14:51:49 +01:00
RiotRobot 223d37ffce Merge branch 'master' into develop 2020-03-30 13:23:17 +01:00
RiotRobot 3d20388ca0 v5.2.0 2020-03-30 13:20:20 +01:00
RiotRobot 198c9d934e Prepare changelog for v5.2.0 2020-03-30 13:20:20 +01:00
J. Ryan Stinnett d43005d91e Merge pull request #1290 from matrix-org/dbkr/send_is_verified_rel
Fix isVerified returning false
2020-03-30 10:28:51 +01:00
Bruno Windels 02264b4572 Merge pull request #1222 from matrix-org/bwindels/fixtestfailure
Fix: error during tests
2020-03-30 08:36:15 +00:00
Bruno Windels add652f18e Merge pull request #1288 from matrix-org/bwindels/alwayssenddone
Send .done event for to_device verification
2020-03-27 16:30:28 +00:00
David Baker 1b9146b9e7 Merge pull request #1291 from matrix-org/dbkr/request_key_backup_key
Request the key backup key & restore backup
2020-03-27 15:43:16 +00:00
J. Ryan Stinnett 5178819b51 Merge pull request #1276 from mjattiot/hotfix/screen_sharing
Make screen sharing works on Chrome using getDisplayMedia()
2020-03-27 15:40:53 +00:00
David Baker f57c25ec27 Make test pass again 2020-03-27 15:13:46 +00:00
David Baker 794429b68b Request the key backup key & restore backup
After a successful verification with ourselves, request the key
backup key too and restore a key backup if we get it.

Also cache the key backup key when we cache the SSK & USK so we have
it available to share.

Fixes https://github.com/vector-im/riot-web/issues/12704
2020-03-27 14:40:15 +00:00
David Baker 983a04bb00 Merge pull request #1289 from matrix-org/dbkr/send_is_verified
Fix isVerified returning false
2020-03-27 14:28:47 +00:00
David Baker adbef16b9d Also pass the parameter in 2020-03-27 14:26:58 +00:00
David Baker 157ea49328 Fix isVerified returning false
which would cause key backups uploads to be missing is_verified
because it was set to `undefined` which would cause the backup to
fail

Fixes https://github.com/vector-im/riot-web/issues/12901
2020-03-27 14:26:53 +00:00
David Baker 17386e7aae Also pass the parameter in 2020-03-27 14:14:45 +00:00
David Baker cb19cd673f Fix isVerified returning false
which would cause key backups uploads to be missing is_verified
because it was set to `undefined` which would cause the backup to
fail

Fixes https://github.com/vector-im/riot-web/issues/12901
2020-03-27 14:11:32 +00:00
Bruno Windels 4f0a297cf3 remove obsolete comment 2020-03-27 12:08:43 +01:00
Bruno Windels 6553e331cd also send .done event for to_device verification 2020-03-27 12:04:48 +01:00
Bruno Windels 21908aea6c Merge pull request #1286 from matrix-org/bwindels/verifignoredupes
Fix: verification gets cancelled when event gets duplicated
2020-03-26 17:07:09 +00:00
David Baker 7c40798ee0 Merge pull request #1287 from matrix-org/dbkr/secret_request_front_door
Use requestSecret on the client to request secrets
2020-03-26 16:16:02 +00:00
David Baker 8cdc635cad lint and aldo indent in a more traditional way 2020-03-26 16:12:11 +00:00
David Baker 7f5ac072e6 Use requestSecret on the client to request secrets
Rather than accessing private method to get the secret storage
object (this was a bit confusing when I grepped for 'requestSecret'
and didn't find anything).
2020-03-26 16:01:35 +00:00
Travis Ralston d69af72c7a Merge pull request #1277 from Awesome-Technologies/develop
Allow guests to fetch TURN servers
2020-03-26 09:33:41 -06:00
Bruno Windels ece1e202de indenting 2020-03-26 15:04:28 +01:00
Bruno Windels 91f38a362d ignore duplicate verification events 2020-03-26 15:04:13 +01:00
RiotRobot 5a3cc314be v5.2.0-rc.1 2020-03-26 12:55:17 +00:00
RiotRobot 3dfaafd177 Prepare changelog for v5.2.0-rc.1 2020-03-26 12:55:16 +00:00
David Baker bdba61975b Merge pull request #1285 from matrix-org/dbkr/trust_cross_signing_flag
Add a flag for whether cross signing signatures are trusted
2020-03-26 12:19:08 +00:00
David Baker 3b9023ec2b add comment 2020-03-26 12:04:16 +00:00
David Baker 4dfc7958b6 lint 2020-03-26 10:07:17 +00:00
David Baker 2fad318726 Make the flag only affect trust of other people's devices 2020-03-26 09:58:05 +00:00
David Baker 480b0e64a6 lint 2020-03-25 18:44:55 +00:00
David Baker 6ec7b5d404 Add a flag for whether cross signing signatures are trusted 2020-03-25 18:36:08 +00:00
J. Ryan Stinnett 0781d78da8 Merge pull request #1282 from matrix-org/jryans/robust-secret-share
Cache user and self signing keys during bootstrap
2020-03-25 17:50:58 +00:00
Zoe 513a256ec1 Merge pull request #1283 from matrix-org/foldleft/remove-extra-promise
remove unnecessary promise
2020-03-25 12:52:42 +00:00
Zoe 9372790666 remove unnecessary promise 2020-03-25 11:47:59 +00:00
J. Ryan Stinnett a6532b7881 Fix logging lints 2020-03-24 18:34:05 +00:00
J. Ryan Stinnett cea3582ed1 Always attempt caching via bootstrap 2020-03-24 18:28:31 +00:00
J. Ryan Stinnett 6bd22a3e9c Add logging to secret request side 2020-03-24 17:44:44 +00:00
J. Ryan Stinnett 7b93b99054 Cache USK and SSK private key during bootstrap 2020-03-24 17:35:59 +00:00
Michael Albert c6eb1525b5 Allow guests to fetch TURN servers
Signed-off-by: Michael Albert <michael.albert@awesome-technologies.de>
2020-03-24 18:04:26 +01:00
J. Ryan Stinnett a4b8ba0bb3 Add logging when replying to secret requests 2020-03-24 15:51:35 +00:00
Zoe 02216b15e5 Merge pull request #1281 from matrix-org/foldleft/12704-key-requests
Functions to cache session backups key automatically
2020-03-24 15:32:09 +00:00
David Baker 42efdf1e0a Merge pull request #1279 from matrix-org/dbkr/unify_cross_signing_checks
Add function for checking cross-signing is ready
2020-03-24 13:34:19 +00:00
David Baker 465f9e634e Merge pull request #1272 from matrix-org/dbkr/symmetric-ssss-migrate
Migration to symmetric SSSS
2020-03-24 13:12:17 +00:00
David Baker 7e92f0e5c8 OK, that really is all the comment formatting 2020-03-24 13:08:49 +00:00
David Baker 859a0d8db2 More comment formatting 2020-03-24 13:08:12 +00:00
David Baker 71740cabb5 comment formatting 2020-03-24 13:06:08 +00:00
David Baker 8f77680750 Typo
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-03-24 13:05:15 +00:00
David Baker 509e4b337d Update for new name 2020-03-24 13:01:46 +00:00
David Baker 942ff0c9fd Better name
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-03-24 13:00:53 +00:00
David Baker 24c3dd1f1a Merge pull request #1280 from matrix-org/uhoreg/reduce_olm_creation
reduce number of one-time-key requests
2020-03-24 10:30:23 +00:00
Hubert Chathi 4f58e9945b factor out failed device notif to a function, and record all failed devices
instead of filtering out already-notified devices
2020-03-24 00:15:04 -04:00
Hubert Chathi 547ded9155 handle failed devices that we aren't going to retry 2020-03-23 23:14:36 -04:00
Hubert Chathi 4f112e8379 only re-try creating olm sessions for servers that failed to respond
If the server responded, then retrying likely won't help.  Retrying is mainly
to help with slow servers.
2020-03-23 22:36:10 -04:00
Hubert Chathi 4d63f8ed04 don't always do second phase of olm creation
don't need to do the shorter timeout when doing preparation to encrypt, and
skip the second phase if the first phase already took longer than a normal
otk claim
2020-03-23 21:26:56 -04:00
Hubert Chathi 944d39c836 add some comments 2020-03-23 16:51:44 -04:00
Bruno Windels 433977b918 Merge pull request #1275 from matrix-org/bwindels/assumemethodswhentodevice
Fix: assume the requested method is supported by other party with to_device
2020-03-23 19:39:27 +00:00
David Baker d9796e3bec Fix indenting 2020-03-23 19:00:02 +00:00
David Baker 0a7b9109f0 Move aes functions to their own file 2020-03-23 18:56:32 +00:00
David Baker 89bf9ff65b doc style fix 2020-03-23 18:40:53 +00:00
David Baker 7f6e223c0c Add function for checking cross-signing is ready
Aggregate function that checks the various things are in place for
cross-signing to work.
2020-03-23 18:34:16 +00:00
David Baker c696e5238b Merge pull request #1278 from matrix-org/dbkr/blacklist_use_device_trust
Use checkDeviceTrust when computing untrusted devices
2020-03-23 14:58:06 +00:00
David Baker d303fd0c7c Fix test 2020-03-23 14:53:55 +00:00
David Baker e1ad2f8a21 Use checkDeviceTrust when computing untrusted devices
Apparently we missed using cross-signing trust in the js-sdk itself
2020-03-23 14:28:10 +00:00
Zoe 7053cf0182 Functions to cache session backups key automatically 2020-03-23 14:24:35 +00:00
mjattiot e25158975b formatting
Signed-off-by: mjattiot <mjattiot@opensense.fr>
2020-03-20 15:22:56 +01:00
mjattiot 7e028a82fc improved compatibility
Signed-off-by: mjattiot <mjattiot@opensense.fr>
2020-03-20 15:22:56 +01:00
mjattiot 17fe3e4dc1 init
Signed-off-by: mjattiot <mjattiot@opensense.fr>
2020-03-20 15:22:56 +01:00
Bruno Windels 4bd09c45a0 assume the requested method is supported by other party during to_device verification 2020-03-20 13:29:29 +01:00
Zoe 6a7a255081 Merge pull request #1271 from matrix-org/foldleft/12704-key-storage
Rename ssss cache functions to be more general
2020-03-20 11:17:21 +00:00
Zoe 6701fdd486 Rename ssss cache functions to be more general 2020-03-20 10:18:06 +00:00
David Baker ddce14b20b Use the typeof test to avoid undefined 2020-03-19 21:12:57 +00:00
David Baker f1317e824b Don't assume subtleCrypto exists if there's a window
Jest has a window object but doesn't have subtleCrypto
2020-03-19 21:04:36 +00:00
David Baker db285af0b5 Add callback to get the user's current key backup passphrase
And also add a null check
2020-03-19 20:36:00 +00:00
David Baker 0434bf5a48 Add functions to get the raw key backup key 2020-03-19 20:34:57 +00:00
Zoe 78d9111646 Add a store for backup keys 2020-03-19 15:30:28 +00:00
J. Ryan Stinnett 0f28a89c52 Merge pull request #1268 from matrix-org/jryans/send-only-new-key-sigs
Upload only new device signature of master key
2020-03-19 14:56:29 +00:00
Hubert Chathi 92db6599d8 Merge pull request #1270 from matrix-org/uhoreg/expose_prepare_to_encrypt
expose prepareToEncrypt in the client API
2020-03-19 10:56:16 -04:00
Hubert Chathi 70fb5dcaa4 Merge pull request #1269 from matrix-org/uhoreg/device_list_no_dying
don't kill the whole device download if one device gives an error
2020-03-19 10:56:00 -04:00
David Baker a265574da1 Merge remote-tracking branch 'origin/develop' into dbkr/symmetric-ssss-migrate 2020-03-19 14:27:25 +00:00
Hubert Chathi 9911766435 expose prepareToEncrypt in the client API 2020-03-18 18:53:26 -04:00
Hubert Chathi fb08ef9a9b don't kill the whole device download if one device gives an error 2020-03-18 15:28:54 -04:00
J. Ryan Stinnett 2fab06111c Upload only new device signature of master key
This changes bootstrap to only upload the new device signature of the master
key. We were previously _adding_ the new signature, but then uploading both old
and new device key signatures of the master key.

This was particularly bad when re-uploading signatures from deleted devices, as
that would cause the homeserver to reject the entire upload.

Fixes https://github.com/vector-im/riot-web/issues/12752
2020-03-18 18:35:37 +00:00
Bruno Windels 11e3b1ab53 Merge pull request #1267 from matrix-org/bwindels/handleselfverifstartrace
handle racing .start event during self verification
2020-03-18 14:06:45 +00:00
Zoe 3c78f7dbe1 Merge pull request #1266 from matrix-org/foldleft/fix-label-error
A crypto.keySignatureUploadFailure event reported the wrong source
2020-03-18 11:21:16 +00:00
Bruno Windels 999cebc304 handle racing .start event during self verification
by comparing the device id rather than the user id, as defined in the MSC
2020-03-17 17:51:32 +01:00
RiotRobot b2e154377a Merge branch 'master' into develop 2020-03-17 14:09:49 +00:00
RiotRobot d5c68139c0 v5.1.1 2020-03-17 14:07:01 +00:00
RiotRobot cbde77a5cd Prepare changelog for v5.1.1 2020-03-17 14:07:00 +00:00
David Baker 8120041ba7 Merge branch 'symmetric-ssss-migrate' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-symmetric-ssss-migrate 2020-03-17 13:11:01 +00:00
Michael Telatynski 68bc8edaae Merge pull request #1263 from matrix-org/t3chguy/fix_editing
Fix editing of unsent messages by waiting for actual event id
2020-03-17 13:00:55 +00:00
Zoe 7ec339985a a crypto.keySignatureUploadFailure event reported the wrong source 2020-03-17 11:42:03 +00:00
Bruno Windels 70c0abaef8 Merge pull request #1265 from matrix-org/bwindels/fixolmapierror-release
Fix: ensureOlmSessionsForDevices parameter format
2020-03-17 11:25:59 +00:00
Bruno Windels d4dcac93b1 devicesByUser should be userId => array of devices 2020-03-17 12:21:56 +01:00
Michael Telatynski 43889cfb31 use async/await instead
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-17 11:14:25 +00:00
Bruno Windels 9e4e14802d Merge pull request #1264 from matrix-org/bwindels/fixolmapierror
Fix: ensureOlmSessionsForDevices parameter format
2020-03-17 11:08:23 +00:00
Bruno Windels 9bebb22746 devicesByUser should be userId => array of devices 2020-03-17 09:51:28 +01:00
Hubert Chathi 3b06b0ffc1 fix lint 2020-03-16 17:22:12 -04:00
Hubert Chathi 1b24d55b24 misc fixes and cleanups 2020-03-16 17:20:54 -04:00
Hubert Chathi c8c6444f6a migrate backup key from asymmetric SSSS to symmetric SSSS 2020-03-16 11:05:07 -04:00
Hubert Chathi 45a88f0517 add function to check that secret storage needs upgrading 2020-03-16 11:00:11 -04:00
Michael Telatynski 53cb3ca79b return the additional promise to simplify the rejection chain
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 12:23:13 +00:00
Michael Telatynski 68526284f1 fix rejection handling
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 10:34:39 +00:00
Zoe 68cebc7ff9 If a key upload fails, throw an error and emit an event (#1254) 2020-03-16 10:24:31 +00:00
Michael Telatynski 38286b74e3 tidy up
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 10:10:22 +00:00
Michael Telatynski 86f56082f0 Make use of scheduler instead of an additional promise
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 10:09:17 +00:00
Michael Telatynski e87bbfc535 Fix editing of unsent messages by waiting for actual event id
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 09:29:37 +00:00
Travis Ralston 758e12d6dd Merge pull request #1261 from matrix-org/travis/yarn-cleanup
Remove stuff that yarn install doesn't think we need
2020-03-13 09:25:23 -06:00
Bruno Windels bff461081a Merge pull request #1262 from matrix-org/bwindels/nullcheckonreceipts-release
Fix: prevent error being thrown during sync in some cases
2020-03-13 12:47:52 +00:00
Bruno Windels 33d36395aa check if push actions has a tweaks object 2020-03-13 13:41:32 +01:00
Hubert Chathi e373508211 some fixes in SSSS migration 2020-03-12 18:08:54 -04:00
Bruno Windels 9051edad37 Merge pull request #1258 from matrix-org/bwindels/nullcheckonreceipts
Fix: prevent error being thrown during sync in some cases
2020-03-12 17:09:10 +00:00
Travis Ralston 678b268008 Remove stuff that yarn install doesn't think we need 2020-03-12 10:44:52 -06:00
J. Ryan Stinnett 0361bcf94f Merge pull request #1260 from matrix-org/jryans/verified-to-bool-release
Force `is_verified` for key backups to bool and fix computation
2020-03-12 15:48:07 +00:00
J. Ryan Stinnett b1f02d30c1 Check key backup trust for the right user ID
This corrects the key backup trust computation so that we use the user ID for
the device we're checking inside of always using the client's main user ID,
which would always resulted in false for other people.

Fixes https://github.com/vector-im/riot-web/issues/12693
2020-03-12 15:42:14 +00:00
J. Ryan Stinnett 2af0e5b176 Convert trustedLocally to a bool in all cases
This ensure we always have a boolean value, even when device is null.

Part of https://github.com/vector-im/riot-web/issues/12693
2020-03-12 15:42:14 +00:00
J. Ryan Stinnett c204812d9c Merge pull request #1259 from matrix-org/jryans/verified-to-bool
Force `is_verified` for key backups to bool and fix computation
2020-03-12 15:39:26 +00:00
J. Ryan Stinnett 3b7def880f Check key backup trust for the right user ID
This corrects the key backup trust computation so that we use the user ID for
the device we're checking inside of always using the client's main user ID,
which would always resulted in false for other people.

Fixes https://github.com/vector-im/riot-web/issues/12693
2020-03-12 14:47:28 +00:00
J. Ryan Stinnett e5ec2f03c2 Convert trustedLocally to a bool in all cases
This ensure we always have a boolean value, even when device is null.

Part of https://github.com/vector-im/riot-web/issues/12693
2020-03-12 14:21:46 +00:00
Bruno Windels a1b3e8055f check if push actions has a tweaks object 2020-03-12 12:59:43 +01:00
Bruno Windels 1e503261f2 Merge pull request #1257 from matrix-org/bwindels/devicelegacyverif
Add a method for legacy single device verification, returning a verification request
2020-03-12 11:30:08 +00:00
David Baker 9107a3e569 Merge pull request #1256 from matrix-org/dbkr/yarn_upgrade_20200311
yarn upgrade
2020-03-12 09:44:51 +00:00
RiotRobot c6070519ed v5.1.1-rc.1 2020-03-11 15:05:49 +00:00
RiotRobot 30ece1be70 Prepare changelog for v5.1.1-rc.1 2020-03-11 15:05:48 +00:00
Bruno Windels b66a1d30a0 method for legacy single device verification, returning a verification request rather than a verifier 2020-03-11 15:53:38 +01:00
David Baker 51e1f56873 yarn upgrade 2020-03-11 14:47:48 +00:00
Hubert Chathi 86304fd037 Merge pull request #1252 from matrix-org/uhoreg/megolm_speed
refactor megolm encryption to improve perceived speed
2020-03-10 20:09:41 -04:00
Hubert Chathi 04387e78cc some cleanups 2020-03-10 15:56:33 -04:00
Travis Ralston 2bfc44b947 Merge pull request #1253 from matrix-org/travis/remove-v1-identity
Remove v1 identity server fallbacks
2020-03-10 09:30:22 -06:00
Bruno Windels 33941eb37b Merge pull request #1251 from matrix-org/bwindels/altaliasesforname
Use alt_aliases instead of local ones for room names
2020-03-10 12:42:50 +00:00
J. Ryan Stinnett 0a45559276 Merge pull request #1250 from matrix-org/jryans/xsign-slow-login
Upload cross-signing key signatures in the background
2020-03-10 11:07:45 +00:00
Travis Ralston 800441e0ed Appease the linter 2020-03-09 17:10:37 -06:00
Travis Ralston 95164d08d5 Remove v1 identity server fallbacks
Fixes https://github.com/vector-im/riot-web/issues/10443

**Review with https://github.com/matrix-org/matrix-react-sdk/pull/4191**
2020-03-09 17:06:10 -06:00
Hubert Chathi 98d955ef1f refactor megolm encryption to improve perceived speed
- allow applications to pre-send decryption keys before the message is sent
- establish new olm sessions with a shorter timeout first, and then re-try in
  the background with a longer timeout without blocking message sending
2020-03-09 18:38:18 -04:00
Bruno Windels 950dadc14e fix tests 2020-03-09 18:33:20 +01:00
Bruno Windels 31d2f0135b use alt aliases instead of local ones for room names 2020-03-09 17:13:50 +01:00
J. Ryan Stinnett c02928f294 Upload cross-signing key signatures in the background
At the moment, uploading cross-signing key signatures is a slow process that can
potentially take many minutes (!) for large accounts / slow servers. This
changes to do the bootstrapping related versions of this in the background.

Note that key signature uploads for interactive flows like verification are
still blocking for now.

Fixes https://github.com/vector-im/riot-web/issues/12223
2020-03-09 15:08:14 +00:00
J. Ryan Stinnett 951fff45e6 Skip device verif upgrades when callback not present
This skips the upgrade when the upgrade callback is not present (which is
expected as no one sets it currently). This adds logging for around the upgrade
process.
2020-03-09 15:03:02 +00:00
J. Ryan Stinnett 4fdd817ff5 Add logging around key change post-processing 2020-03-09 14:46:10 +00:00
J. Ryan Stinnett acba31bd6d Merge pull request #1249 from matrix-org/jryans/sharing-names
Fix secret sharing names to match spec
2020-03-09 13:48:06 +00:00
J. Ryan Stinnett b5eea01848 Fix secret sharing names to match spec
When sharing keys, we should use `m.cross_signing` prefix.

Part of https://github.com/vector-im/riot-web/issues/12661
2020-03-09 13:40:02 +00:00
Bruno Windels 074e02ccf2 Merge pull request #1248 from matrix-org/bwindels/removecryptoverifstartevent
Cleanup: remove crypto.verification.start event
2020-03-06 16:47:34 +00:00
Bruno Windels 4b9bc67cb6 remove crypto.verification.start event
as it is not used anymore by the react-sdk
2020-03-06 16:48:44 +01:00
Zoe 936ef4116b For self-verifications, also request keys from the other device (#1245)
* For self-verifications, also request keys from the other device
* removed some XXX's so the editor doesn't think it's three issues
* add methods to access key cache callbacks
2020-03-06 09:56:56 +00:00
J. Ryan Stinnett 9883d6851a Merge pull request #1246 from matrix-org/jryans/xsign-trust-bool
Fix regression in key backup request params
2020-03-05 14:16:16 +00:00
J. Ryan Stinnett 4c08e126ca Fix regression in key backup request params
This converts the cross-signing trust to a boolean as required by the
homeserver.

Regressed by https://github.com/vector-im/riot-web/issues/12599
Fixes https://github.com/vector-im/riot-web/issues/12618
2020-03-05 12:17:42 +00:00
J. Ryan Stinnett bc53f8fdec Merge pull request #1244 from matrix-org/jryans/xsign-key-backup-verif
Use cross-signing trust to mark backups verified
2020-03-03 18:03:46 +00:00
J. Ryan Stinnett 0b76d3d7bd Merge pull request #1243 from matrix-org/jryans/xsign-auto-share
Check both cross-signing and local trust for key sharing
2020-03-03 18:03:38 +00:00
J. Ryan Stinnett abaf71418e Use cross-signing trust to mark backups verified
This changes to cross-signing trust as well as local trust when we decide
whether to tell the homeserver a session of room keys is verified.

Fixes https://github.com/vector-im/riot-web/issues/12599
2020-03-03 15:52:38 +00:00
J. Ryan Stinnett c96a906b39 Check both cross-signing and local trust for key sharing
When sharing room keys with our own devices, this ensure we check both
cross-signing and local trust.

Fixes https://github.com/vector-im/riot-web/issues/12596
2020-03-03 15:12:40 +00:00
RiotRobot da96765020 Merge branch 'master' into develop 2020-03-02 16:55:55 +00:00
RiotRobot f654c8a892 v5.1.0 2020-03-02 16:53:10 +00:00
RiotRobot 336fce55df Prepare changelog for v5.1.0 2020-03-02 16:53:10 +00:00
Zoe d11946d86b Merge pull request #1242 from matrix-org/foldleft/fix-bad-merge
Fixed up tests to match new way that crypto stores are created
2020-03-02 15:01:27 +00:00
Zoe 3a4c72ac08 actually, returning is unnecessary 2020-03-02 14:46:26 +00:00
Zoe 6d3f0f653b there's some days that the linter and i, we just really don't see eye-to-eye 2020-03-02 14:38:24 +00:00
Zoe 81d3534569 added return back 2020-03-02 13:06:13 +00:00
Zoe c54922dba3 Fixed up tests to match new way that crypto stores are created 2020-03-02 12:51:47 +00:00
Michael Telatynski 9da1f7b8d5 Fix unhomoglyph import to make browser-matrix.js happy once more 2020-03-02 10:54:14 +00:00
Zoe a4ed3d97fc Merge pull request #1235 from matrix-org/foldleft/12299-local-ssk
Store USK and SSK locally
2020-03-02 09:52:44 +00:00
Zoe 656694ee00 proper spacing for test output text 2020-03-02 09:45:55 +00:00
Hubert Chathi c6b5936f8a use the right operator 2020-02-28 16:09:24 -05:00
Travis Ralston 03752ab60c Merge pull request #1236 from matrix-org/travis/unpadded-qr-codes
Use unpadded base64 for QR code secrets
2020-02-28 10:20:57 -07:00
Bruno Windels 7203542cfd Merge pull request #1239 from matrix-org/bwindels/dontrequiredoneforselfverif
Don't require .done event for finishing self-verification
2020-02-28 15:16:09 +00:00
Bruno Windels 4b36bbc122 Merge pull request #1237 from matrix-org/bwindels/dontcancelas3rdparty
Don't cancel as 3rd party in verification request
2020-02-28 15:15:49 +00:00
Bruno Windels ecaf21ceb0 Don't require .done event for finishing self-verification
Instead, call onVerifierFinished from the verifier on the request
so we can internally mark it as done. This flag is not persisted,
but we don't have historical (persisted) to-device requests anyway.
2020-02-28 14:56:38 +01:00
Zoe 67fe4e1460 lint & only cache valid keys 2020-02-28 11:04:28 +00:00
Zoe a94503ad03 address PR feedback 2020-02-28 10:43:57 +00:00
Bruno Windels ce6dd8688c Merge pull request #1234 from matrix-org/bwindels/evenmoreloggingforverif
Verification: log when switching start event
2020-02-28 10:24:09 +00:00
Hubert Chathi 1151bdc6db initial work in migrating ssss to symmetric 2020-02-27 22:56:34 -05:00
Hubert Chathi ed223d1d76 remove unnecessary awaits 2020-02-27 22:54:43 -05:00
Bruno Windels 650eee7705 dont cancel as 3rd party in verification request 2020-02-27 18:38:16 +01:00
Travis Ralston 4510eb6540 Match all the equals
Co-Authored-By: Hubert Chathi <hubert@uhoreg.ca>
2020-02-27 10:10:24 -07:00
Travis Ralston 9a236f317d Use unpadded base64 for QR code secrets 2020-02-27 10:00:56 -07:00
Zoe 25c467d608 Wire cache through to matrix client 2020-02-27 16:53:26 +00:00
Zoe c2daf0d74e Store data in cryptostore 2020-02-27 16:53:26 +00:00
J. Ryan Stinnett fa19616ad1 Merge pull request #1233 from matrix-org/jryans/safari-e2e-idb
Perform crypto store operations directly after transaction
2020-02-27 16:48:09 +00:00
Zoe 02cbd33284 Added cache callbacks to CrossSigningInfo 2020-02-27 16:37:25 +00:00
Zoe 941ae18d74 Added tests for CrossSigningInfo.getCrossSigningKey 2020-02-27 16:37:25 +00:00
Bruno Windels 90f400abe1 log when switching start event 2020-02-27 17:35:58 +01:00
J. Ryan Stinnett ff2d93d421 Perform crypto store operations directly after transaction
At least on Safari but perhaps other browsers as well, you must perform
IndexedDB operations in the same JS task as you start the transaction. As a
concrete example, you cannot open the transaction and await some promise before
actually using it.

This fixes the crypto store to meet this requirement.

Fixes https://github.com/vector-im/riot-web/issues/12207
2020-02-27 14:57:07 +00:00
Bruno Windels 8d26bd9a17 Merge pull request #1232 from matrix-org/bwindels/logeventidinverifreq
More verification request logging
2020-02-27 13:26:53 +00:00
J. Ryan Stinnett a9fa0484ff Add exception handling to crypto store paths
A few of the crypto store backend paths were missing try / catch wrappers to
abort the transaction if the inner callback throws.
2020-02-27 12:26:18 +00:00
J. Ryan Stinnett d3d12ab62f Merge pull request #1231 from matrix-org/jryans/upgrade-deps-2020-02-26
Upgrade deps
2020-02-27 11:24:33 +00:00
Bruno Windels 1e29b1a31d log event id in verif request to differentiate between double processing vs double sending 2020-02-26 18:49:18 +01:00
J. Ryan Stinnett 9318bf5f2f Upgrade deps 2020-02-26 15:00:43 +00:00
RiotRobot 6b35302442 v5.1.0-rc.1 2020-02-26 14:16:57 +00:00
RiotRobot 2937e58215 Prepare changelog for v5.1.0-rc.1 2020-02-26 14:16:57 +00:00
J. Ryan Stinnett d42589b6cc Merge pull request #1230 from matrix-org/jryans/dist-tags
Add latest dist-tag for releases
2020-02-26 14:14:09 +00:00
J. Ryan Stinnett 26e9dfb4fb Add latest dist-tag for a release 2020-02-26 14:07:20 +00:00
J. Ryan Stinnett f27d03a6bc Always publish to next tag
This ensures that anyone who wants the latest version (pre-release or final
release) can always use the `next` tag.
2020-02-26 13:55:46 +00:00
J. Ryan Stinnett b1e3150a81 Reset device list dirty flag only after writing
This ensures we wait until after the device list writes to the crypto store
before marking thing as clean. This is particularly important for the error
path, as the write to the crypto store can fail.

Part of https://github.com/vector-im/riot-web/issues/12207
2020-02-25 17:56:47 +00:00
Hubert Chathi 5d52053caa use symmetric encryption for SSSS 2020-02-24 17:38:53 -05:00
Bruno Windels ce668d051c Merge pull request #1225 from matrix-org/bwindels/aliasautocomplete
Add room method for alt_aliases
2020-02-24 12:17:45 +00:00
David Baker e06579ecf5 Merge pull request #1227 from matrix-org/dbkr/move_bk_pipelines
Remove buildkite pipeline
2020-02-21 17:34:09 +00:00
David Baker 6c30af245c Remove buildkite pipeline
Now moved to the pipelines repo
2020-02-21 17:21:42 +00:00
Bruno Windels c9c40a6dde Merge pull request #1226 from matrix-org/bwindels/dontfailonhistoricalcancelafterstart
don't assume verify has been called when receiving a cancellation in verifier
2020-02-21 17:19:03 +00:00
Travis Ralston e748ac3d00 Merge pull request #1221 from matrix-org/travis/qr-binary
Reduce secret size for new binary packing
2020-02-21 10:05:03 -07:00
Bruno Windels aec79f3a79 don't assume verify has been called when receiving a cancellation in verifier 2020-02-21 17:26:29 +01:00
Hubert Chathi bf92cb1522 try to re-fetch devices before giving up on trying to heal a broken olm (#1224) 2020-02-21 10:20:46 -05:00
Bruno Windels 14e1920ff5 fix docs parser error 2020-02-21 13:43:08 +01:00
Bruno Windels c95cdf5a11 add room method for alt_aliases 2020-02-21 13:37:14 +01:00
Bruno Windels c14d0616ea always return null if there is no canonical alias 2020-02-21 13:36:52 +01:00
Hubert Chathi 0112701145 Merge pull request #1223 from matrix-org/uhoreg/misc_rageshake_fixes
misc rageshake fixes
2020-02-20 16:28:49 -05:00
Hubert Chathi cb69515be9 add some logging when sender could not establish an olm session 2020-02-20 14:49:32 -05:00
Hubert Chathi 3cd791e08f add function for getting the user's curve25519 key 2020-02-20 14:44:28 -05:00
Hubert Chathi 6e233e860e remove leftover debugging messages 2020-02-20 14:43:59 -05:00
Hubert Chathi b4f0ea441b remove obsolete comment 2020-02-20 14:43:24 -05:00
Bruno Windels 39974d3a61 Merge pull request #1220 from matrix-org/bwindels/fixhistoricalcancelledrequests
Fix cancelled historical requests not appearing as cancelled
2020-02-20 17:07:35 +00:00
Bruno Windels a998006842 Merge pull request #1217 from matrix-org/bwindels/fixqrcode
Fix renaming error that broke QR code verification
2020-02-20 11:00:39 +00:00
Bruno Windels c4e449fc45 add null check for when there is no response 2020-02-20 11:42:33 +01:00
Travis Ralston 765fbe2182 Reduce secret size for new binary packing
See https://github.com/matrix-org/matrix-react-sdk/pull/4091
2020-02-19 17:21:56 -07:00
Bruno Windels 08dfa73b57 pending excludes observeOnly now, still allow observeOnly requests to get cancelled 2020-02-19 17:51:53 +01:00
RiotRobot a58e7a34e7 v5.0.1 2020-02-19 15:03:04 +00:00
RiotRobot 7a481beec6 Prepare changelog for v5.0.1 2020-02-19 15:03:03 +00:00
Bruno Windels d51fad2de4 Merge pull request #1219 from matrix-org/bwindels/fixaliases
add method for new /aliases endpoint
2020-02-19 10:02:32 +00:00
Bruno Windels c66755a756 jsdoc 2020-02-19 10:13:32 +01:00
Bruno Windels 886ad03505 add method to check server feature flag 2020-02-19 10:08:05 +01:00
Bruno Windels ba33ef0a68 use unstable prefix 2020-02-19 10:07:52 +01:00
Bruno Windels fe97dc3ece add method for new /aliases endpoint 2020-02-18 15:33:41 +01:00
Bruno Windels 76c4875088 fix targetDevice renaming 2020-02-18 11:23:04 +01:00
Bruno Windels 04a3aaee35 Merge pull request #1213 from matrix-org/bwindels/filterverifmethods
method for checking if other party supports verification method
2020-02-18 10:15:49 +00:00
Bruno Windels fef03cda9b Update src/crypto/verification/request/VerificationRequest.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-02-18 10:03:02 +00:00
Bruno Windels 3292fde41b Merge pull request #1210 from matrix-org/bwindels/localecho2
add local echo state for accepting or declining a verif req
2020-02-18 09:55:09 +00:00
RiotRobot 38cf25ac5a Merge branch 'master' into develop 2020-02-17 11:58:01 +00:00
Bruno Windels 62c344b633 Merge pull request #1214 from matrix-org/bwindels/workswithrageshakes
make logging compatible with rageshakes
2020-02-14 16:39:05 +00:00
Bruno Windels 75ce2729f9 comment typo 2020-02-14 17:35:07 +01:00
Bruno Windels 6669554867 make logging compatible with rageshakes 2020-02-14 17:31:40 +01:00
Bruno Windels d3294da37c Merge pull request #1209 from matrix-org/bwindels/oneverifrequest
Find existing requests when starting a new verification request
2020-02-14 15:33:06 +00:00
Bruno Windels 9b56bf25cf Update src/crypto/verification/request/InRoomChannel.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-02-14 14:43:50 +00:00
Bruno Windels e1a33d8a7b Update src/crypto/verification/request/ToDeviceChannel.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-02-14 13:41:38 +00:00
Bruno Windels 47a1224c13 Merge pull request #1211 from matrix-org/bwindels/logsasmac
log MAC calculation during SAS
2020-02-14 12:54:40 +00:00
Bruno Windels 5c57d81e94 method for checking if other party supports verification method 2020-02-14 13:47:24 +01:00
Bruno Windels edefd3ec88 log MAC calculation 2020-02-14 12:20:02 +01:00
Bruno Windels f15098efde add local echo state for accepting or declining a verif req 2020-02-13 17:27:18 +01:00
Bruno Windels 365bb772bc also find existing request for to-device verification 2020-02-13 15:37:21 +01:00
Bruno Windels 5ee6ada973 use pending instead of individual checks 2020-02-13 15:37:04 +01:00
Bruno Windels ee0fa0e687 fix lint 2020-02-13 14:47:35 +01:00
Bruno Windels 0d41f6aafc remove commented out logging 2020-02-13 14:36:18 +01:00
Bruno Windels 91b6499815 more consistent naming 2020-02-13 14:36:09 +01:00
Bruno Windels 7cd1166a47 allow finding existing verif req without starting a new one 2020-02-13 14:31:33 +01:00
Bruno Windels f76cb677ff store sasEvent on verifier so we can get it if we missed show_sas event 2020-02-13 14:31:03 +01:00
Bruno Windels 05e7f4e6f7 look for existing verification request when trying to start a new one 2020-02-13 14:30:38 +01:00
67 changed files with 6296 additions and 2805 deletions
-58
View File
@@ -1,58 +0,0 @@
steps:
- label: ":eslint: JS Lint"
command:
- "yarn install"
- "yarn lint:js"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":tslint: TS Lint"
command:
- "yarn install"
- "yarn lint:ts"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":typescript: Types Lint"
command:
- "yarn install"
- "yarn lint:types"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: "🛠 Build"
command:
- "yarn install"
- "yarn build"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":jest: Tests"
command:
- "yarn install"
- "yarn test"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: "📃 Docs"
command:
- "yarn install"
- "yarn gendoc"
plugins:
- docker#v3.0.1:
image: "node:12"
- wait
- label: "🐴 Trigger matrix-react-sdk"
trigger: "matrix-react-sdk"
branches: "develop"
build:
branch: "develop"
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
async: true
+423
View File
@@ -1,3 +1,426 @@
Changes in [6.2.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.2) (2020-06-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.1...v6.2.2)
* Use existing session id for fetching flows as to not get a new session
[\#1407](https://github.com/matrix-org/matrix-js-sdk/pull/1407)
Changes in [6.2.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.1) (2020-06-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.0...v6.2.1)
* Bring back backup key format migration
[\#1399](https://github.com/matrix-org/matrix-js-sdk/pull/1399)
Changes in [6.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.0) (2020-06-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.0-rc.1...v6.2.0)
* No changes since rc.1
Changes in [6.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.0-rc.1) (2020-06-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.1.0...v6.2.0-rc.1)
* Make auth argument in the register request compliant with r0.6.0
[\#1304](https://github.com/matrix-org/matrix-js-sdk/pull/1304)
* Send the wrong auth params with the right auth params
[\#1393](https://github.com/matrix-org/matrix-js-sdk/pull/1393)
* encrypt cached keys with pickle key
[\#1387](https://github.com/matrix-org/matrix-js-sdk/pull/1387)
* Fix replying to key share requests
[\#1385](https://github.com/matrix-org/matrix-js-sdk/pull/1385)
* Add dist to package.json files so CDNs can serve it
[\#1384](https://github.com/matrix-org/matrix-js-sdk/pull/1384)
* Fix getVersion warning saying undefined room
[\#1382](https://github.com/matrix-org/matrix-js-sdk/pull/1382)
* Combine the two places we processed client-level default push rules
[\#1379](https://github.com/matrix-org/matrix-js-sdk/pull/1379)
* make MAC check robust against unpadded vs padded base64 differences
[\#1378](https://github.com/matrix-org/matrix-js-sdk/pull/1378)
* Remove key backup format migration
[\#1375](https://github.com/matrix-org/matrix-js-sdk/pull/1375)
* Add simple browserify browser-matrix.js tests
[\#1241](https://github.com/matrix-org/matrix-js-sdk/pull/1241)
* support new key agreement method for SAS
[\#1376](https://github.com/matrix-org/matrix-js-sdk/pull/1376)
Changes in [6.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.1.0) (2020-05-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.1.0-rc.1...v6.1.0)
* No changes since rc.1
Changes in [6.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.1.0-rc.1) (2020-05-14)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.0.0...v6.1.0-rc.1)
* Remove support for asymmetric 4S encryption
[\#1373](https://github.com/matrix-org/matrix-js-sdk/pull/1373)
* Increase timeout for 2nd phase of Olm session creation
[\#1367](https://github.com/matrix-org/matrix-js-sdk/pull/1367)
* Add logging on decryption retries
[\#1366](https://github.com/matrix-org/matrix-js-sdk/pull/1366)
* Emit event when a trusted self-key is stored
[\#1364](https://github.com/matrix-org/matrix-js-sdk/pull/1364)
* Customize error payload for oversized messages
[\#1352](https://github.com/matrix-org/matrix-js-sdk/pull/1352)
* Return null for key backup state when we haven't checked yet
[\#1363](https://github.com/matrix-org/matrix-js-sdk/pull/1363)
* Added a progressCallback for backup key loading
[\#1351](https://github.com/matrix-org/matrix-js-sdk/pull/1351)
* Add initialFetch param to willUpdateDevices / devicesUpdated
[\#1360](https://github.com/matrix-org/matrix-js-sdk/pull/1360)
* Fix race between sending .request and receiving .ready over to_device
[\#1359](https://github.com/matrix-org/matrix-js-sdk/pull/1359)
* Handle race between sending and await next event from other party
[\#1357](https://github.com/matrix-org/matrix-js-sdk/pull/1357)
* Add crypto.willUpdateDevices event and make
getStoredDevices/getStoredDevicesForUser synchronous
[\#1354](https://github.com/matrix-org/matrix-js-sdk/pull/1354)
* Fix sender of local echo events in unsigned redactions
[\#1350](https://github.com/matrix-org/matrix-js-sdk/pull/1350)
* Remove redundant key backup setup path
[\#1353](https://github.com/matrix-org/matrix-js-sdk/pull/1353)
* Remove some dead code from _retryDecryption
[\#1349](https://github.com/matrix-org/matrix-js-sdk/pull/1349)
* Don't send key requests until after sync processing is finished
[\#1348](https://github.com/matrix-org/matrix-js-sdk/pull/1348)
* Prevent attempts to send olm messages to ourselves
[\#1346](https://github.com/matrix-org/matrix-js-sdk/pull/1346)
* Retry account data upload requests
[\#1345](https://github.com/matrix-org/matrix-js-sdk/pull/1345)
* Log first known index with megolm session updates
[\#1344](https://github.com/matrix-org/matrix-js-sdk/pull/1344)
* Prune to_device messages to avoid sending empty messages
[\#1343](https://github.com/matrix-org/matrix-js-sdk/pull/1343)
* Convert bunch of things to TypeScript
[\#1335](https://github.com/matrix-org/matrix-js-sdk/pull/1335)
* Add logging when making new Olm sessions
[\#1342](https://github.com/matrix-org/matrix-js-sdk/pull/1342)
* Fix: handle filter not found
[\#1340](https://github.com/matrix-org/matrix-js-sdk/pull/1340)
* Make getAccountDataFromServer return null if not found
[\#1338](https://github.com/matrix-org/matrix-js-sdk/pull/1338)
* Fix setDefaultKeyId to fail if the request fails
[\#1336](https://github.com/matrix-org/matrix-js-sdk/pull/1336)
* Document setRoomEncryption not modifying room state
[\#1328](https://github.com/matrix-org/matrix-js-sdk/pull/1328)
* Fix: don't do extra /filter request when enabling lazy loading of members
[\#1332](https://github.com/matrix-org/matrix-js-sdk/pull/1332)
* Reject attemptAuth promise if no auth flow found
[\#1329](https://github.com/matrix-org/matrix-js-sdk/pull/1329)
* Fix FilterComponent allowed_values check
[\#1327](https://github.com/matrix-org/matrix-js-sdk/pull/1327)
* Serialise Olm prekey decryptions
[\#1326](https://github.com/matrix-org/matrix-js-sdk/pull/1326)
* Fix: crash when backup key needs fixing from corruption issue
[\#1324](https://github.com/matrix-org/matrix-js-sdk/pull/1324)
* Fix cross-signing/SSSS reset
[\#1322](https://github.com/matrix-org/matrix-js-sdk/pull/1322)
* Implement QR code reciprocate for self-verification with untrusted MSK
[\#1320](https://github.com/matrix-org/matrix-js-sdk/pull/1320)
Changes in [6.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.0.0) (2020-05-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.0.0-rc.2...v6.0.0)
* Add progress callback for key backups
[\#1368](https://github.com/matrix-org/matrix-js-sdk/pull/1368)
Changes in [6.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.0.0-rc.2) (2020-05-01)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.0.0-rc.1...v6.0.0-rc.2)
* Emit event when a trusted self-key is stored
[\#1365](https://github.com/matrix-org/matrix-js-sdk/pull/1365)
Changes in [6.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.0.0-rc.1) (2020-04-30)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.4...v6.0.0-rc.1)
BREAKING CHANGES
---
* client.getStoredDevicesForUser and client.getStoredDevices are no longer async
All Changes
---
* Add initialFetch param to willUpdateDevices / devicesUpdated
[\#1362](https://github.com/matrix-org/matrix-js-sdk/pull/1362)
* Fix race between sending .request and receiving .ready over to_device
[\#1361](https://github.com/matrix-org/matrix-js-sdk/pull/1361)
* Handle race between sending and await next event from other party
[\#1358](https://github.com/matrix-org/matrix-js-sdk/pull/1358)
* Add crypto.willUpdateDevices event and make
getStoredDevices/getStoredDevicesForUser synchronous
[\#1356](https://github.com/matrix-org/matrix-js-sdk/pull/1356)
* Remove redundant key backup setup path
[\#1355](https://github.com/matrix-org/matrix-js-sdk/pull/1355)
Changes in [5.3.1-rc.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.4) (2020-04-23)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.3...v5.3.1-rc.4)
* Retry account data upload requests
[\#1347](https://github.com/matrix-org/matrix-js-sdk/pull/1347)
* Fix: handle filter not found
[\#1341](https://github.com/matrix-org/matrix-js-sdk/pull/1341)
* Make getAccountDataFromServer return null if not found
[\#1339](https://github.com/matrix-org/matrix-js-sdk/pull/1339)
* Fix setDefaultKeyId to fail if the request fails
[\#1337](https://github.com/matrix-org/matrix-js-sdk/pull/1337)
* Fix: don't do extra /filter request when enabling lazy loading of members
[\#1333](https://github.com/matrix-org/matrix-js-sdk/pull/1333)
* Reject attemptAuth promise if no auth flow found
[\#1331](https://github.com/matrix-org/matrix-js-sdk/pull/1331)
* Serialise Olm prekey decryptions
[\#1330](https://github.com/matrix-org/matrix-js-sdk/pull/1330)
Changes in [5.3.1-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.3) (2020-04-17)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.2...v5.3.1-rc.3)
* Fix cross-signing/SSSS reset
[\#1323](https://github.com/matrix-org/matrix-js-sdk/pull/1323)
* Fix: crash when backup key needs fixing from corruption issue
[\#1325](https://github.com/matrix-org/matrix-js-sdk/pull/1325)
Changes in [5.3.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.2) (2020-04-16)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.1...v5.3.1-rc.2)
* Implement QR code reciprocate for self-verification with untrusted MSK
[\#1321](https://github.com/matrix-org/matrix-js-sdk/pull/1321)
Changes in [5.3.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.1) (2020-04-15)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.0-rc.1...v5.3.1-rc.1)
* Adapt release script for riot-desktop
[\#1319](https://github.com/matrix-org/matrix-js-sdk/pull/1319)
* Fix: prevent spurious notifications from indexer
[\#1318](https://github.com/matrix-org/matrix-js-sdk/pull/1318)
* Always create our own user object
[\#1317](https://github.com/matrix-org/matrix-js-sdk/pull/1317)
* Fix incorrect backup key format in SSSS
[\#1311](https://github.com/matrix-org/matrix-js-sdk/pull/1311)
* Fix e2ee crash after refreshing after having received a cross-singing key
reset
[\#1315](https://github.com/matrix-org/matrix-js-sdk/pull/1315)
* Fix: catch send errors in SAS verifier
[\#1314](https://github.com/matrix-org/matrix-js-sdk/pull/1314)
* Clear cross-signing keys when detecting the keys have changed
[\#1312](https://github.com/matrix-org/matrix-js-sdk/pull/1312)
* Upgrade deps
[\#1310](https://github.com/matrix-org/matrix-js-sdk/pull/1310)
Changes in [5.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.0-rc.1) (2020-04-08)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0...v5.3.0-rc.1)
* Store key backup key in cache as Uint8Array
[\#1308](https://github.com/matrix-org/matrix-js-sdk/pull/1308)
* Use the correct request body for the /keys/query endpoint.
[\#1307](https://github.com/matrix-org/matrix-js-sdk/pull/1307)
* Avoid creating two devices on registration
[\#1305](https://github.com/matrix-org/matrix-js-sdk/pull/1305)
* Lower max-warnings to 81
[\#1306](https://github.com/matrix-org/matrix-js-sdk/pull/1306)
* Move key backup key creation before caching
[\#1303](https://github.com/matrix-org/matrix-js-sdk/pull/1303)
* Expose function to force-reset outgoing room key requests
[\#1298](https://github.com/matrix-org/matrix-js-sdk/pull/1298)
* Add isSelfVerification property to VerificationRequest
[\#1302](https://github.com/matrix-org/matrix-js-sdk/pull/1302)
* QR code reciprocation
[\#1297](https://github.com/matrix-org/matrix-js-sdk/pull/1297)
* Add ability to check symmetric SSSS key before we try to use it
[\#1294](https://github.com/matrix-org/matrix-js-sdk/pull/1294)
* Add some debug logging for events stuck to bottom of timeline
[\#1296](https://github.com/matrix-org/matrix-js-sdk/pull/1296)
* Fix: spontanous verification request cancellation under some circumstances
[\#1295](https://github.com/matrix-org/matrix-js-sdk/pull/1295)
* Receive private key for caching from the app layer
[\#1293](https://github.com/matrix-org/matrix-js-sdk/pull/1293)
* Track whether we have verified a user before
[\#1292](https://github.com/matrix-org/matrix-js-sdk/pull/1292)
* Fix: error during tests
[\#1222](https://github.com/matrix-org/matrix-js-sdk/pull/1222)
* Send .done event for to_device verification
[\#1288](https://github.com/matrix-org/matrix-js-sdk/pull/1288)
* Request the key backup key & restore backup
[\#1291](https://github.com/matrix-org/matrix-js-sdk/pull/1291)
* Make screen sharing works on Chrome using getDisplayMedia()
[\#1276](https://github.com/matrix-org/matrix-js-sdk/pull/1276)
* Fix isVerified returning false
[\#1289](https://github.com/matrix-org/matrix-js-sdk/pull/1289)
* Fix: verification gets cancelled when event gets duplicated
[\#1286](https://github.com/matrix-org/matrix-js-sdk/pull/1286)
* Use requestSecret on the client to request secrets
[\#1287](https://github.com/matrix-org/matrix-js-sdk/pull/1287)
* Allow guests to fetch TURN servers
[\#1277](https://github.com/matrix-org/matrix-js-sdk/pull/1277)
Changes in [5.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0) (2020-03-30)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0-rc.1...v5.2.0)
* Fix isVerified returning false
[\#1290](https://github.com/matrix-org/matrix-js-sdk/pull/1290)
Changes in [5.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0-rc.1) (2020-03-26)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.1...v5.2.0-rc.1)
* Add a flag for whether cross signing signatures are trusted
[\#1285](https://github.com/matrix-org/matrix-js-sdk/pull/1285)
* Cache user and self signing keys during bootstrap
[\#1282](https://github.com/matrix-org/matrix-js-sdk/pull/1282)
* remove unnecessary promise
[\#1283](https://github.com/matrix-org/matrix-js-sdk/pull/1283)
* Functions to cache session backups key automatically
[\#1281](https://github.com/matrix-org/matrix-js-sdk/pull/1281)
* Add function for checking cross-signing is ready
[\#1279](https://github.com/matrix-org/matrix-js-sdk/pull/1279)
* Use symmetric encryption for SSSS
[\#1228](https://github.com/matrix-org/matrix-js-sdk/pull/1228)
* Migrate SSSS to use symmetric algorithm
[\#1238](https://github.com/matrix-org/matrix-js-sdk/pull/1238)
* Migration to symmetric SSSS
[\#1272](https://github.com/matrix-org/matrix-js-sdk/pull/1272)
* Reduce number of one-time-key requests
[\#1280](https://github.com/matrix-org/matrix-js-sdk/pull/1280)
* Fix: assume the requested method is supported by other party with to_device
[\#1275](https://github.com/matrix-org/matrix-js-sdk/pull/1275)
* Use checkDeviceTrust when computing untrusted devices
[\#1278](https://github.com/matrix-org/matrix-js-sdk/pull/1278)
* Add a store for backup keys
[\#1271](https://github.com/matrix-org/matrix-js-sdk/pull/1271)
* Upload only new device signature of master key
[\#1268](https://github.com/matrix-org/matrix-js-sdk/pull/1268)
* Expose prepareToEncrypt in the client API
[\#1270](https://github.com/matrix-org/matrix-js-sdk/pull/1270)
* Don't kill the whole device download if one device gives an error
[\#1269](https://github.com/matrix-org/matrix-js-sdk/pull/1269)
* Handle racing .start event during self verification
[\#1267](https://github.com/matrix-org/matrix-js-sdk/pull/1267)
* A crypto.keySignatureUploadFailure event reported the wrong source
[\#1266](https://github.com/matrix-org/matrix-js-sdk/pull/1266)
* Fix editing of unsent messages by waiting for actual event id
[\#1263](https://github.com/matrix-org/matrix-js-sdk/pull/1263)
* Fix: ensureOlmSessionsForDevices parameter format
[\#1264](https://github.com/matrix-org/matrix-js-sdk/pull/1264)
* Remove stuff that yarn install doesn't think we need
[\#1261](https://github.com/matrix-org/matrix-js-sdk/pull/1261)
* Fix: prevent error being thrown during sync in some cases
[\#1258](https://github.com/matrix-org/matrix-js-sdk/pull/1258)
* Force `is_verified` for key backups to bool and fix computation
[\#1259](https://github.com/matrix-org/matrix-js-sdk/pull/1259)
* Add a method for legacy single device verification, returning a verification
request
[\#1257](https://github.com/matrix-org/matrix-js-sdk/pull/1257)
* yarn upgrade
[\#1256](https://github.com/matrix-org/matrix-js-sdk/pull/1256)
Changes in [5.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.1) (2020-03-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.1-rc.1...v5.1.1)
* Fix: ensureOlmSessionsForDevices parameter format
[\#1265](https://github.com/matrix-org/matrix-js-sdk/pull/1265)
* Fix: prevent error being thrown during sync in some cases
[\#1262](https://github.com/matrix-org/matrix-js-sdk/pull/1262)
* Force `is_verified` for key backups to bool and fix computation
[\#1260](https://github.com/matrix-org/matrix-js-sdk/pull/1260)
Changes in [5.1.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.1-rc.1) (2020-03-11)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.0...v5.1.1-rc.1)
* refactor megolm encryption to improve perceived speed
[\#1252](https://github.com/matrix-org/matrix-js-sdk/pull/1252)
* Remove v1 identity server fallbacks
[\#1253](https://github.com/matrix-org/matrix-js-sdk/pull/1253)
* Use alt_aliases instead of local ones for room names
[\#1251](https://github.com/matrix-org/matrix-js-sdk/pull/1251)
* Upload cross-signing key signatures in the background
[\#1250](https://github.com/matrix-org/matrix-js-sdk/pull/1250)
* Fix secret sharing names to match spec
[\#1249](https://github.com/matrix-org/matrix-js-sdk/pull/1249)
* Cleanup: remove crypto.verification.start event
[\#1248](https://github.com/matrix-org/matrix-js-sdk/pull/1248)
* Fix regression in key backup request params
[\#1246](https://github.com/matrix-org/matrix-js-sdk/pull/1246)
* Use cross-signing trust to mark backups verified
[\#1244](https://github.com/matrix-org/matrix-js-sdk/pull/1244)
* Check both cross-signing and local trust for key sharing
[\#1243](https://github.com/matrix-org/matrix-js-sdk/pull/1243)
* Fixed up tests to match new way that crypto stores are created
[\#1242](https://github.com/matrix-org/matrix-js-sdk/pull/1242)
* Store USK and SSK locally
[\#1235](https://github.com/matrix-org/matrix-js-sdk/pull/1235)
* Use unpadded base64 for QR code secrets
[\#1236](https://github.com/matrix-org/matrix-js-sdk/pull/1236)
* Don't require .done event for finishing self-verification
[\#1239](https://github.com/matrix-org/matrix-js-sdk/pull/1239)
* Don't cancel as 3rd party in verification request
[\#1237](https://github.com/matrix-org/matrix-js-sdk/pull/1237)
* Verification: log when switching start event
[\#1234](https://github.com/matrix-org/matrix-js-sdk/pull/1234)
* Perform crypto store operations directly after transaction
[\#1233](https://github.com/matrix-org/matrix-js-sdk/pull/1233)
* More verification request logging
[\#1232](https://github.com/matrix-org/matrix-js-sdk/pull/1232)
* Upgrade deps
[\#1231](https://github.com/matrix-org/matrix-js-sdk/pull/1231)
Changes in [5.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.0) (2020-03-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.0-rc.1...v5.1.0)
* No changes since rc.1
Changes in [5.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.0-rc.1) (2020-02-26)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.1...v5.1.0-rc.1)
* Add latest dist-tag for releases
[\#1230](https://github.com/matrix-org/matrix-js-sdk/pull/1230)
* Add room method for alt_aliases
[\#1225](https://github.com/matrix-org/matrix-js-sdk/pull/1225)
* Remove buildkite pipeline
[\#1227](https://github.com/matrix-org/matrix-js-sdk/pull/1227)
* don't assume verify has been called when receiving a cancellation in
verifier
[\#1226](https://github.com/matrix-org/matrix-js-sdk/pull/1226)
* Reduce secret size for new binary packing
[\#1221](https://github.com/matrix-org/matrix-js-sdk/pull/1221)
* misc rageshake fixes
[\#1223](https://github.com/matrix-org/matrix-js-sdk/pull/1223)
* Fix cancelled historical requests not appearing as cancelled
[\#1220](https://github.com/matrix-org/matrix-js-sdk/pull/1220)
* Fix renaming error that broke QR code verification
[\#1217](https://github.com/matrix-org/matrix-js-sdk/pull/1217)
Changes in [5.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.1) (2020-02-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.0...v5.0.1)
* add method for new /aliases endpoint
[\#1219](https://github.com/matrix-org/matrix-js-sdk/pull/1219)
* method for checking if other party supports verification method
[\#1213](https://github.com/matrix-org/matrix-js-sdk/pull/1213)
* add local echo state for accepting or declining a verif req
[\#1210](https://github.com/matrix-org/matrix-js-sdk/pull/1210)
* make logging compatible with rageshakes
[\#1214](https://github.com/matrix-org/matrix-js-sdk/pull/1214)
* Find existing requests when starting a new verification request
[\#1209](https://github.com/matrix-org/matrix-js-sdk/pull/1209)
* log MAC calculation during SAS
[\#1211](https://github.com/matrix-org/matrix-js-sdk/pull/1211)
Changes in [5.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.0) (2020-02-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.0-rc.1...v5.0.0)
+6 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "5.0.0",
"version": "6.2.2",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
@@ -14,7 +14,7 @@
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:ts && yarn lint:js",
"lint:js": "eslint --max-warnings 93 src spec",
"lint:js": "eslint --max-warnings 81 src spec",
"lint:types": "tsc --noEmit",
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
"test": "jest spec/ --coverage --testEnvironment node",
@@ -35,6 +35,7 @@
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
"dist",
"lib",
"src",
"git-revision.txt",
@@ -68,6 +69,7 @@
"@babel/preset-typescript": "^7.7.4",
"@babel/register": "^7.7.4",
"@types/node": "12",
"@types/request": "^2.48.4",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babelify": "^10.0.0",
@@ -78,7 +80,9 @@
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-jest": "^23.0.4",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.0.0",
"jest": "^24.9.0",
"jest-localstorage-mock": "^2.4.0",
"jsdoc": "^3.5.5",
"matrix-mock-request": "^1.2.3",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
+29 -16
View File
@@ -38,6 +38,7 @@ $USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
-z: skip generating the jsdoc
-n: skip publish to NPM
EOF
}
@@ -60,9 +61,10 @@ fi
skip_changelog=
skip_jsdoc=
skip_npm=
changelog_file="CHANGELOG.md"
expected_npm_user="matrixdotorg"
while getopts hc:u:xz f; do
while getopts hc:u:xzn f; do
case $f in
h)
help
@@ -77,6 +79,9 @@ while getopts hc:u:xz f; do
z)
skip_jsdoc=1
;;
n)
skip_npm=1
;;
u)
expected_npm_user="$OPTARG"
;;
@@ -96,10 +101,12 @@ 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.
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
if [ -z "$skip_npm" ]; then
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
fi
# ignore leading v on release
@@ -294,14 +301,18 @@ 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_flags
# Tag both releases and prereleases as `next` so the last stable release remains
# the default.
if [ -z "$skip_npm" ]; then
npm publish --tag next
if [ $prerelease -eq 0 ]; then
# For a release, also add the default `latest` tag.
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
fi
fi
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
@@ -336,8 +347,10 @@ if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# finally, merge master back onto develop
git checkout develop
git pull
git merge master
git push origin develop
# finally, merge master back onto develop (if it exists)
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
git checkout develop
git pull
git merge master
git push origin develop
fi
+1 -1
View File
@@ -185,7 +185,7 @@ TestClient.prototype.expectKeyQuery = function(response) {
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual(
{},
[],
"Expected key query for " + userId + ", got " +
Object.keys(content.device_keys),
);
+23
View File
@@ -0,0 +1,23 @@
/*
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.
*/
// stub for browser-matrix browserify tests
global.XMLHttpRequest = jest.fn();
afterAll(() => {
// clean up XMLHttpRequest mock
global.XMLHttpRequest = undefined;
});
+103
View File
@@ -0,0 +1,103 @@
/*
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.
*/
// load XmlHttpRequest mock
import "./setupTests";
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import {MockStorageApi} from "../MockStorageApi";
import {WebStorageSessionStore} from "../../src/store/session/webstorage";
import MockHttpBackend from "matrix-mock-request";
import {LocalStorageCryptoStore} from "../../src/crypto/store/localStorage-crypto-store";
import * as utils from "../test-utils";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
/* global matrixcs */
describe("Browserify Test", function() {
let client;
let httpBackend;
async function createTestClient() {
const sessionStoreBackend = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
const httpBackend = new MockHttpBackend();
const options = {
baseUrl: "http://" + USER_ID + ".test.server",
userId: USER_ID,
accessToken: ACCESS_TOKEN,
deviceId: DEVICE_ID,
sessionStore: sessionStore,
request: httpBackend.requestFn,
cryptoStore: new LocalStorageCryptoStore(sessionStoreBackend),
};
const client = matrixcs.createClient(options);
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
return { client, httpBackend };
}
beforeEach(async () => {
({client, httpBackend} = await createTestClient());
await client.startClient();
});
afterEach(async () => {
client.stopClient();
await httpBackend.stop();
});
it("Sync", async function() {
const event = utils.mkMembership({
room: ROOM_ID,
mship: "join",
user: "@other_user:server.test",
name: "Displayname",
});
const syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[ROOM_ID] = {
timeline: {
events: [
event,
],
limited: false,
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
await Promise.race([
Promise.all([
httpBackend.flushAllExpected(),
]),
new Promise((_, reject) => {
client.once("sync.unexpectedError", reject);
}),
]);
}, 10000);
});
+2 -2
View File
@@ -70,7 +70,7 @@ function expectAliQueryKeys() {
aliTestClient.httpBackend.when("POST", "/keys/query")
.respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual(
{},
[],
"Expected Alice to key query for " + bobUserId + ", got " +
Object.keys(content.device_keys),
);
@@ -98,7 +98,7 @@ function expectBobQueryKeys() {
"POST", "/keys/query",
).respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual(
{},
[],
"Expected Bob to key query for " + aliUserId + ", got " +
Object.keys(content.device_keys),
);
+2 -2
View File
@@ -347,8 +347,8 @@ describe("MatrixClient", function() {
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {
'boris': {},
'chaz': {},
'boris': [],
'chaz': [],
}});
}).respond(200, {
device_keys: {
+3 -2
View File
@@ -3,6 +3,7 @@ import HttpBackend from "matrix-mock-request";
import {MatrixClient} from "../../src/matrix";
import {MatrixScheduler} from "../../src/scheduler";
import {MemoryStore} from "../../src/store/memory";
import {MatrixError} from "../../src/http-api";
describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something";
@@ -132,10 +133,10 @@ describe("MatrixClient opts", function() {
});
it("shouldn't retry sending events", function(done) {
httpBackend.when("PUT", "/txn1").fail(500, {
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
errcode: "M_SOMETHING",
error: "Ruh roh",
});
}));
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
}, function(err) {
+9
View File
@@ -16,6 +16,7 @@ limitations under the License.
*/
import {logger} from '../src/logger';
import * as utils from "../src/utils";
// try to load the olm library.
try {
@@ -24,3 +25,11 @@ try {
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available");
}
// also try to set node crypto
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
console.log('nodejs was compiled without crypto support: some tests will fail');
}
+4
View File
@@ -313,6 +313,10 @@ describe("Crypto", function() {
// make a room key request, and record the transaction ID for the
// sendToDevice call
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
// key requests get queued until the sync has finished, but we don't
// let the client set up enough for that to happen, so gut-wrench a bit
// to force it to send now.
aliceClient._crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
jest.runAllTimers();
await Promise.resolve();
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
+250
View File
@@ -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 '../../olm-loader';
import {
CrossSigningInfo,
createCryptoStoreCacheCallbacks,
} from '../../../src/crypto/CrossSigning';
import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
import {OlmDevice} from "../../../src/crypto/OlmDevice";
const userId = "@alice:example.com";
// Private key for tests only
const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const types = [
{ type: "master", shouldCache: false },
{ type: "self_signing", shouldCache: true },
{ type: "user_signing", shouldCache: true },
{ type: "invalid", shouldCache: false },
];
const badKey = Uint8Array.from(testKey);
badKey[0] ^= 1;
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
return;
}
beforeAll(function() {
return global.Olm.init();
});
it("should throw if no callback is provided", async () => {
const info = new CrossSigningInfo(userId);
await expect(info.getCrossSigningKey("master")).rejects.toThrow();
});
it.each(types)("should throw if the callback returns falsey",
async ({type, shouldCache}) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => false,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
});
it("should throw if the expected key doesn't come back", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => masterKeyPub,
});
await expect(info.getCrossSigningKey("master", "")).rejects.toThrow();
});
it("should return a key from its callback", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => testKey,
});
const [pubKey, ab] = await info.getCrossSigningKey("master", masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(ab).toEqual({a: 106712, b: 106712});
});
it.each(types)("should request a key from the cache callback (if set)" +
" and does not call app if one is found" +
" %o",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
if (shouldCache) {
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
}
});
it.each(types)("should store a key with the cache callback (if set)",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
if (shouldCache) {
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
}
});
it.each(types)("does not store a bad key to the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
it.each(types)("does not store a value to the cache if it came from the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
new Error("Tried to store a value from cache"),
);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
" if one is not found", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
it.each(types)("requests a key from the cache callback (if set) and then" +
" calls app if that key doesn't match", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
});
/*
* Note that MemoryStore is weird. It's only used for testing - as far as I can tell,
* it's not possible to get one in normal execution unless you hack as we do here.
*/
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
store._backend = new MemoryCryptoStore();
store._backendPromise = Promise.resolve(store._backend);
return store;
}],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
let store;
beforeAll(() => {
store = dbFactory();
});
beforeEach(async () => {
await store.deleteAllData();
});
it("should cache data to the store and retrieve it", async () => {
await store.startup();
const olmDevice = new OlmDevice(store);
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
createCryptoStoreCacheCallbacks(store, olmDevice);
await storeCrossSigningKeyCache("self_signing", testKey);
// If we've not saved anything, don't expect anything
// Definitely don't accidentally return the wrong key for the type
const nokey = await getCrossSigningKeyCache("self", "");
expect(nokey).toBeNull();
const key = await getCrossSigningKeyCache("self_signing", "");
expect(new Uint8Array(key)).toEqual(testKey);
});
});
+24 -18
View File
@@ -297,6 +297,10 @@ describe("MegolmDecryption", function() {
},
}));
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
@@ -320,14 +324,14 @@ describe("MegolmDecryption", function() {
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
mockBaseApis.claimOneTimeKeys.mockReset();
@@ -516,21 +520,22 @@ describe("MegolmDecryption", function() {
};
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
const sendPromise = new Promise((resolve, reject) => {
aliceClient.sendToDevice = async (msgtype, contentMap) => {
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
},
});
};
});
resolve();
};
});
const event = new MatrixEvent({
type: "m.room.message",
@@ -540,8 +545,7 @@ describe("MegolmDecryption", function() {
content: {},
});
await aliceClient._crypto.encryptEvent(event, aliceRoom);
expect(run).toBe(true);
await sendPromise;
});
it("throws an error describing why it doesn't have a key", async function() {
@@ -598,6 +602,7 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
aliceClient._crypto.downloadKeys = async () => {};
const bobDevice = bobClient._crypto._olmDevice;
const roomId = "!someroom";
@@ -649,6 +654,7 @@ describe("MegolmDecryption", function() {
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
aliceClient._crypto.downloadKeys = async () => {};
const roomId = "!someroom";
+26
View File
@@ -541,5 +541,31 @@ describe("MegolmBackup", function() {
expect(res.clearEvent.content).toEqual('testytest');
});
});
it('has working cache functions', async function() {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client._crypto.storeSessionBackupPrivateKey(key);
const result = await client._crypto.getSessionBackupPrivateKey();
expect(new Uint8Array(result)).toEqual(key);
});
it('caches session backup keys as it encounters them', async function() {
const cachedNull = await client._crypto.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull();
client._http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
{ cacheCompleteCallback: resolve },
);
});
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
expect(cachedKey).not.toBeNull();
});
});
});
@@ -0,0 +1,87 @@
/*
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 {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
import {
ROOM_KEY_REQUEST_STATES,
} from '../../../src/crypto/OutgoingRoomKeyRequestManager';
const requests = [
{
requestId: "A",
requestBody: { session_id: "A", room_id: "A" },
state: ROOM_KEY_REQUEST_STATES.SENT,
},
{
requestId: "B",
requestBody: { session_id: "B", room_id: "B" },
state: ROOM_KEY_REQUEST_STATES.SENT,
},
{
requestId: "C",
requestBody: { session_id: "C", room_id: "C" },
state: ROOM_KEY_REQUEST_STATES.UNSENT,
},
];
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
store._backend = new MemoryCryptoStore();
store._backendPromise = Promise.resolve(store._backend);
return store;
}],
])("Outgoing room key requests [%s]", function(name, dbFactory) {
let store;
beforeAll(async () => {
store = dbFactory();
await store.startup();
await Promise.all(requests.map((request) =>
store.getOrAddOutgoingRoomKeyRequest(request),
));
});
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
async () => {
const r = await
store.getAllOutgoingRoomKeyRequestsByState(ROOM_KEY_REQUEST_STATES.SENT);
expect(r).toHaveLength(2);
requests.filter((e) => e.state == ROOM_KEY_REQUEST_STATES.SENT).forEach((e) => {
expect(r).toContainEqual(e);
});
});
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
async () => {
const r =
await store.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.SENT]);
expect(r).not.toBeNull();
expect(r).not.toBeUndefined();
expect(r.state).toEqual(ROOM_KEY_REQUEST_STATES.SENT);
expect(requests).toContainEqual(r);
});
});
+406 -83
View File
@@ -16,10 +16,20 @@ limitations under the License.
import '../../olm-loader';
import * as olmlib from "../../../src/crypto/olmlib";
import {SECRET_STORAGE_ALGORITHM_V1} from "../../../src/crypto/SecretStorage";
import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage";
import {MatrixEvent} from "../../../src/models/event";
import {TestClient} from '../../TestClient';
import {makeTestClients} from './verification/util';
import {encryptAES} from "../../../src/crypto/aes";
import * as utils from "../../../src/utils";
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
console.log('nodejs was compiled without crypto support');
}
async function makeTestClient(userInfo, options) {
const client = (new TestClient(
@@ -40,6 +50,13 @@ async function makeTestClient(userInfo, options) {
return client;
}
// Wrapper around pkSign to return a signed object. pkSign returns the
// signature, rather than the signed object.
function sign(obj, key, userId) {
olmlib.pkSign(obj, key, userId);
return obj;
}
describe("Secrets", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
@@ -51,9 +68,8 @@ describe("Secrets", function() {
});
it("should store and retrieve a secret", async function() {
const decryption = new global.Olm.PkDecryption();
const pubkey = decryption.generate_key();
const privkey = decryption.get_private_key();
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const signing = new global.Olm.PkSigning();
const signingKey = signing.generate_seed();
@@ -69,7 +85,7 @@ describe("Secrets", function() {
const getKey = jest.fn(e => {
expect(Object.keys(e.keys)).toEqual(["abc"]);
return ['abc', privkey];
return ['abc', key];
});
const alice = await makeTestClient(
@@ -100,8 +116,7 @@ describe("Secrets", function() {
};
const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1,
pubkey: pubkey,
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
};
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
@@ -112,11 +127,11 @@ describe("Secrets", function() {
}),
]);
expect(await secretStorage.isStored("foo")).toBe(false);
expect(await secretStorage.isStored("foo")).toBeFalsy();
await secretStorage.store("foo", "bar", ["abc"]);
expect(await secretStorage.isStored("foo")).toBe(true);
expect(await secretStorage.isStored("foo")).toBeTruthy();
expect(await secretStorage.get("foo")).toBe("bar");
expect(getKey).toHaveBeenCalled();
@@ -149,6 +164,13 @@ describe("Secrets", function() {
});
it("should encrypt with default key if keys is null", async function() {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn(e => {
expect(Object.keys(e.keys)).toEqual([newKeyId]);
return [newKeyId, key];
});
let keys = {};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
@@ -156,6 +178,7 @@ describe("Secrets", function() {
cryptoCallbacks: {
getCrossSigningKey: t => keys[t],
saveCrossSigningKeys: k => keys = k,
getSecretStorageKey: getKey,
},
},
);
@@ -170,7 +193,7 @@ describe("Secrets", function() {
alice.resetCrossSigningKeys();
const newKeyId = await alice.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1,
SECRET_STORAGE_ALGORITHM_V1_AES,
);
// we don't await on this because it waits for the event to come down the sync
// which won't happen in the test setup
@@ -251,89 +274,389 @@ describe("Secrets", function() {
expect(secret).toBe("bar");
});
it("bootstraps when no storage or cross-signing keys locally", async function() {
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
describe("bootstrap", function() {
// keys used in some of the tests
const XSK = new Uint8Array(
olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="),
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0";
const USK = new Uint8Array(
olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="),
);
const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU";
const SSK = new Uint8Array(
olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="),
);
const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q";
const SSSSKey = new Uint8Array(
olmlib.decodeBase64(
"XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0=",
),
);
it("bootstraps when no storage or cross-signing keys locally", async function() {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn(e => {
return [Object.keys(e.keys)[0], key];
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
await bob.bootstrapSecretStorage();
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
it("bootstraps when cross-signing keys in secret storage", async function() {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: getKey,
},
},
},
);
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob._crypto.checkKeyBackup = async () => {};
await bob.bootstrapSecretStorage();
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({ pubkey: storagePublicKey }),
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
// Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {};
await bob.bootstrapSecretStorage();
it("bootstraps when cross-signing keys in secret storage", async function() {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
},
},
},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob._crypto.checkKeyBackup = async () => {};
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({
// `pubkey` not used anymore with symmetric 4S
keyInfo: { pubkey: storagePublicKey },
privateKey: storagePrivateKey,
}),
});
// Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {};
await bob.bootstrapSecretStorage();
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
it("adds passphrase checking if it's lacking", async function() {
let crossSigningKeys = {
master: XSK,
user_signing: USK,
self_signing: SSK,
};
const secretStorageKeys = {
key_id: SSSSKey,
};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
getCrossSigningKey: t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getSecretStorageKey: ({keys}, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
return [keyId, secretStorageKeys[keyId]];
}
}
},
},
},
);
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: "m.secret_storage.default_key",
content: {
key: "key_id",
},
}),
new MatrixEvent({
type: "m.secret_storage.key.key_id",
content: {
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
passphrase: {
algorithm: "m.pbkdf2",
iterations: 500000,
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
},
},
}),
// we never use these values, other than checking that they
// exist, so just use dummy values
new MatrixEvent({
type: "m.cross_signing.master",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.cross_signing.self_signing",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.cross_signing.user_signing",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
]);
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
}, XSK, "@alice:example.com"),
},
});
alice.getKeyBackupVersion = async () => {
return {
version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
}, XSK, "@alice:example.com"),
};
};
alice.setAccountData = async function(name, data) {
const event = new MatrixEvent({
type: name,
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
};
await alice.bootstrapSecretStorage();
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
.toEqual({key: "key_id"});
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
.getContent();
expect(keyInfo.algorithm)
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({
algorithm: "m.pbkdf2",
iterations: 500000,
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
});
expect(keyInfo).toHaveProperty("iv");
expect(keyInfo).toHaveProperty("mac");
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
.toBeTruthy();
});
it("fixes backup keys in the wrong format", async function() {
let crossSigningKeys = {
master: XSK,
user_signing: USK,
self_signing: SSK,
};
const secretStorageKeys = {
key_id: SSSSKey,
};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
getCrossSigningKey: t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getSecretStorageKey: ({keys}, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
return [keyId, secretStorageKeys[keyId]];
}
}
},
},
},
);
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: "m.secret_storage.default_key",
content: {
key: "key_id",
},
}),
new MatrixEvent({
type: "m.secret_storage.key.key_id",
content: {
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
passphrase: {
algorithm: "m.pbkdf2",
iterations: 500000,
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
},
},
}),
new MatrixEvent({
type: "m.cross_signing.master",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.cross_signing.self_signing",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.cross_signing.user_signing",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.megolm_backup.v1",
content: {
encrypted: {
key_id: await encryptAES(
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
secretStorageKeys.key_id, "m.megolm_backup.v1",
),
},
},
}),
]);
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
}, XSK, "@alice:example.com"),
},
});
alice.getKeyBackupVersion = async () => {
return {
version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
}, XSK, "@alice:example.com"),
};
};
alice.setAccountData = async function(name, data) {
const event = new MatrixEvent({
type: name,
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
};
await alice.bootstrapSecretStorage();
const backupKey = alice.getAccountData("m.megolm_backup.v1")
.getContent();
expect(backupKey.encrypted).toHaveProperty("key_id");
expect(await alice.getSecret("m.megolm_backup.v1"))
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
});
});
});
+20 -7
View File
@@ -44,7 +44,16 @@ describe("SAS verification", function() {
});
it("should error on an unexpected event", async function() {
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
//channel, baseApis, userId, deviceId, startEvent, request
const request = {
onVerifierCancelled: function() {},
};
const channel = {
send: function() {
return Promise.resolve();
},
};
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
sas.handleEvent(new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
@@ -122,8 +131,8 @@ describe("SAS verification", function() {
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.client.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
bob.client.on("crypto.verification.request", request => {
request.verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
@@ -139,7 +148,7 @@ describe("SAS verification", function() {
}
}
});
resolve(verifier);
resolve(request.verifier);
});
});
@@ -172,11 +181,14 @@ describe("SAS verification", function() {
it("should verify a key", async () => {
let macMethod;
let keyAgreement;
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
bob.client.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
.message_authentication_code;
keyAgreement = map[alice.client.getUserId()][alice.client.deviceId]
.key_agreement_protocol;
}
return origSendToDevice(type, map);
};
@@ -203,6 +215,7 @@ describe("SAS verification", function() {
// make sure that it uses the preferred method
expect(macMethod).toBe("hkdf-hmac-sha256");
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
// make sure Alice and Bob verified each other
const bobDevice
@@ -339,11 +352,11 @@ describe("SAS verification", function() {
};
const bobPromise = new Promise((resolve, reject) => {
bob.client.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
bob.client.on("crypto.verification.request", request => {
request.verifier.on("show_sas", (e) => {
e.mismatch();
});
resolve(verifier);
resolve(request.verifier);
});
});
@@ -0,0 +1,117 @@
/*
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 {VerificationBase} from '../../../../src/crypto/verification/Base';
import {CrossSigningInfo} from '../../../../src/crypto/CrossSigning';
import {encodeBase64} from "../../../../src/crypto/olmlib";
import {setupWebcrypto, teardownWebcrypto} from './util';
jest.useFakeTimers();
// Private key for tests only
const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("self-verifications", () => {
beforeAll(function() {
setupWebcrypto();
return global.Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("triggers a request for key sharing upon completion", async () => {
const userId = "@test:localhost";
const cacheCallbacks = {
getCrossSigningKeyCache: jest.fn().mockReturnValue(null),
storeCrossSigningKeyCache: jest.fn(),
};
const _crossSigningInfo = new CrossSigningInfo(
userId,
{},
cacheCallbacks,
);
_crossSigningInfo.keys = {
self_signing: { keys: { X: testKeyPub } },
user_signing: { keys: { X: testKeyPub } },
};
const _secretStorage = {
request: jest.fn().mockReturnValue({
promise: Promise.resolve(encodeBase64(testKey)),
}),
};
const storeSessionBackupPrivateKey = jest.fn();
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
const client = {
_crypto: {
_crossSigningInfo,
_secretStorage,
storeSessionBackupPrivateKey,
getSessionBackupPrivateKey: () => null,
},
requestSecret: _secretStorage.request.bind(_secretStorage),
getUserId: () => userId,
getKeyBackupVersion: () => Promise.resolve({}),
restoreKeyBackupWithCache,
};
const request = {
onVerifierFinished: () => undefined,
};
const verification = new VerificationBase(
undefined, // channel
client, // baseApis
userId,
"ABC", // deviceId
undefined, // startEvent
request,
);
verification._resolve = () => undefined;
const result = await verification.done();
/* We should request, and store, two cross signing key and the key backup key */
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(2);
expect(_secretStorage.request.mock.calls.length).toBe(3);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
.toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
.toEqual(testKey);
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
.toEqual(testKey);
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
expect(result).toBeInstanceOf(Array);
expect(result[0][0]).toBe(testKeyPub);
expect(result[1][0]).toBe(testKeyPub);
});
});
@@ -120,7 +120,6 @@ async function distributeEvent(ownRequest, theirRequest, event) {
}
describe("verification request unit tests", function() {
beforeAll(function() {
setupWebcrypto();
});
+34
View File
@@ -0,0 +1,34 @@
import {FilterComponent} from "../../src/filter-component";
import {mkEvent} from '../test-utils';
describe("Filter Component", function() {
describe("types", function() {
it("should filter out events with other types", function() {
const filter = new FilterComponent({ types: ['m.room.message'] });
const event = mkEvent({
type: 'm.room.member',
content: { },
room: 'roomId',
event: true,
});
const checkResult = filter.check(event);
expect(checkResult).toBe(false);
});
it("should validate events with the same type", function() {
const filter = new FilterComponent({ types: ['m.room.message'] });
const event = mkEvent({
type: 'm.room.message',
content: { },
room: 'roomId',
event: true,
});
const checkResult = filter.check(event);
expect(checkResult).toBe(true);
});
});
});
+30 -1
View File
@@ -97,7 +97,7 @@ describe("InteractiveAuth", function() {
// first we expect a call to doRequest
doRequest.mockImplementation(function(authData) {
logger.log("request1", authData);
expect(authData).toEqual({});
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [
@@ -143,4 +143,33 @@ describe("InteractiveAuth", function() {
expect(stateUpdated).toBeCalledTimes(1);
});
});
it("should start an auth stage and reject if no auth flow", function() {
const doRequest = jest.fn();
const stateUpdated = jest.fn();
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest: doRequest,
stateUpdated: stateUpdated,
});
doRequest.mockImplementation(function(authData) {
logger.log("request1", authData);
expect(authData).toEqual(null); // first request should be null
const err = new MatrixError({
session: "sessionId",
flows: [],
params: {
"logintype": { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
return ia.attemptAuth().catch(function(error) {
expect(error.message).toBe('No appropriate authentication flow found');
});
});
});
+5 -10
View File
@@ -617,15 +617,10 @@ describe("Room", function() {
}, event: true,
})]);
};
const setAliases = function(aliases, stateKey) {
if (!stateKey) {
stateKey = aliases.length
? aliases[0].split(':').splice(1).join(':') // domain+port
: 'fibble';
}
const setAltAliases = function(aliases) {
room.addLiveEvents([utils.mkEvent({
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
aliases: aliases,
type: "m.room.canonical_alias", room: roomId, skey: "", content: {
alt_aliases: aliases,
}, event: true,
})]);
};
@@ -862,7 +857,7 @@ describe("Room", function() {
"(invite join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("invite");
setAliases([alias, "#another:here"]);
setAltAliases([alias, "#another:here"]);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
@@ -872,7 +867,7 @@ describe("Room", function() {
"(public join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("public");
setAliases([alias, "#another:here"]);
setAltAliases([alias, "#another:here"]);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
+25
View File
@@ -0,0 +1,25 @@
/*
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.
*/
export {};
declare global {
namespace NodeJS {
interface Global {
localStorage: Storage;
}
}
}
+4 -2
View File
@@ -502,10 +502,12 @@ export class AutoDiscovery {
request(
{ method: "GET", uri: url, timeout: 5000 },
(err, response, body) => {
if (err || response.statusCode < 200 || response.statusCode >= 300) {
if (err || response &&
(response.statusCode < 200 || response.statusCode >= 300)
) {
let action = "FAIL_PROMPT";
let reason = (err ? err.message : null) || "General failure";
if (response.statusCode === 404) {
if (response && response.statusCode === 404) {
action = "IGNORE";
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
}
+80 -156
View File
@@ -28,13 +28,7 @@ import {SERVICE_TYPES} from './service-types';
import {logger} from './logger';
import {PushProcessor} from "./pushprocessor";
import * as utils from "./utils";
import {
MatrixHttpApi,
PREFIX_IDENTITY_V1,
PREFIX_IDENTITY_V2,
PREFIX_R0,
PREFIX_UNSTABLE,
} from "./http-api";
import {MatrixHttpApi, PREFIX_IDENTITY_V2, PREFIX_R0, PREFIX_UNSTABLE} from "./http-api";
function termsUrlForService(serviceType, baseUrl) {
switch (serviceType) {
@@ -212,9 +206,6 @@ MatrixBaseApis.prototype.register = function(
inhibitLogin = undefined;
}
if (auth === undefined || auth === null) {
auth = {};
}
if (sessionId) {
auth.session = sessionId;
}
@@ -1119,6 +1110,21 @@ MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
);
};
/**
* @param {string} roomId
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: an object with an `aliases` property, containing an array of local aliases
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.unstableGetLocalAliases =
function(roomId, callback) {
const path = utils.encodeUri("/rooms/$roomId/aliases",
{$roomId: roomId});
const prefix = PREFIX_UNSTABLE + "/org.matrix.msc2432";
return this._http.authedRequest(callback, "GET", path,
null, null, { prefix });
};
/**
* Get room info for the given alias.
* @param {string} alias The room alias to resolve.
@@ -1748,7 +1754,7 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
content.token = opts.token;
}
userIds.forEach((u) => {
content.device_keys[u] = {};
content.device_keys[u] = [];
});
return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
@@ -1761,10 +1767,13 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
*
* @param {string} [key_algorithm = signed_curve25519] desired key type
*
* @param {number} [timeout] the time (in milliseconds) to wait for keys from remote
* servers
*
* @return {Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm, timeout) {
const queries = {};
if (key_algorithm === undefined) {
@@ -1779,6 +1788,9 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
query[deviceId] = key_algorithm;
}
const content = {one_time_keys: queries};
if (timeout) {
content.timeout = timeout;
}
const path = "/keys/claim";
return this._http.authedRequest(undefined, "POST", path, undefined, content);
};
@@ -1880,28 +1892,10 @@ MatrixBaseApis.prototype.requestEmailToken = async function(
next_link: nextLink,
};
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/email/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
return await this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
};
/**
@@ -1948,28 +1942,10 @@ MatrixBaseApis.prototype.requestMsisdnToken = async function(
next_link: nextLink,
};
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/msisdn/requestToken",
params, PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
return await this._http.idServerRequest(
callback, "POST", "/validate/msisdn/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
};
/**
@@ -2003,24 +1979,10 @@ MatrixBaseApis.prototype.submitMsisdnToken = async function(
token: msisdnToken,
};
try {
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, PREFIX_IDENTITY_V1,
);
}
throw err;
}
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
};
/**
@@ -2175,53 +2137,32 @@ MatrixBaseApis.prototype.lookupThreePid = async function(
callback,
identityAccessToken,
) {
try {
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
[[address, medium]], identityAccessToken,
);
const result = response.find(p => p.address === address);
if (!result) {
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, {});
return {};
}
const mapping = {
address,
medium,
mxid: result.mxid,
// We can't reasonably fill these parameters:
// not_before
// not_after
// ts
// signatures
};
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, mapping);
return mapping;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
const params = {
medium: medium,
address: address,
};
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "GET", "/lookup",
params, PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err, undefined);
throw err;
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
[[address, medium]], identityAccessToken,
);
const result = response.find(p => p.address === address);
if (!result) {
if (callback) callback(null, {});
return {};
}
const mapping = {
address,
medium,
mxid: result.mxid,
// We can't reasonably fill these parameters:
// not_before
// not_after
// ts
// signatures
};
if (callback) callback(null, mapping);
return mapping;
};
/**
@@ -2239,46 +2180,29 @@ MatrixBaseApis.prototype.bulkLookupThreePids = async function(
query,
identityAccessToken,
) {
try {
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
// We have to reverse the query order to get [address, medium] pairs
query.map(p => [p[1], p[0]]), identityAccessToken,
);
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
// We have to reverse the query order to get [address, medium] pairs
query.map(p => [p[1], p[0]]), identityAccessToken,
);
const v1results = [];
for (const mapping of response) {
const originalQuery = query.find(p => p[1] === mapping.address);
if (!originalQuery) {
throw new Error("Identity sever returned unexpected results");
}
v1results.push([
originalQuery[0], // medium
mapping.address,
mapping.mxid,
]);
const v1results = [];
for (const mapping of response) {
const originalQuery = query.find(p => p[1] === mapping.address);
if (!originalQuery) {
throw new Error("Identity sever returned unexpected results");
}
return {threepids: v1results};
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
const params = {
threepids: query,
};
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/bulk_lookup", params,
PREFIX_IDENTITY_V1, identityAccessToken,
);
}
throw err;
v1results.push([
originalQuery[0], // medium
mapping.address,
mapping.mxid,
]);
}
return {threepids: v1results};
};
/**
+304 -98
View File
@@ -35,19 +35,25 @@ import {StubStore} from "./store/stub";
import {createNewMatrixCall} from "./webrtc/call";
import * as utils from './utils';
import {sleep} from './utils';
import {MatrixError, PREFIX_MEDIA_R0, PREFIX_UNSTABLE} from "./http-api";
import {
MatrixError,
PREFIX_MEDIA_R0,
PREFIX_UNSTABLE,
retryNetworkOperation,
} from "./http-api";
import {getHttpUriForMxc} from "./content-repo";
import * as ContentHelpers from "./content-helpers";
import * as olmlib from "./crypto/olmlib";
import {ReEmitter} from './ReEmitter';
import {RoomList} from './crypto/RoomList';
import {logger} from './logger';
import {Crypto, isCryptoAvailable} from './crypto';
import {Crypto, isCryptoAvailable, fixBackupKey} from './crypto';
import {decodeRecoveryKey} from './crypto/recoverykey';
import {keyFromAuthData} from './crypto/key_passphrase';
import {randomString} from './randomstring';
import {PushProcessor} from "./pushprocessor";
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
import { User } from "./models/user";
const SCROLLBACK_DELAY_MS = 3000;
export const CRYPTO_ENABLED = isCryptoAvailable();
@@ -104,6 +110,9 @@ function keyFromRecoverySession(session, decryptionKey) {
* If provided, opts.deviceId and opts.userId should **NOT** be provided
* (they are present in the exported data).
*
* @param {string} opts.pickleKey Key used to pickle olm objects or other
* sensitive 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
@@ -217,8 +226,10 @@ function keyFromRecoverySession(session, decryptionKey) {
* Args:
* {object} keys Information about the keys:
* {
* <key name>: {
* pubkey: {UInt8Array}
* keys: {
* <key name>: {
* pubkey: {UInt8Array}
* }, ...
* }
* }
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
@@ -276,18 +287,26 @@ export function MatrixClient(opts) {
// will be used during async initialization of the crypto
this._exportedOlmDeviceToImport = opts.deviceToImport.olmDevice;
}
} else if (opts.pickleKey) {
this.pickleKey = opts.pickleKey;
}
this.scheduler = opts.scheduler;
if (this.scheduler) {
const self = this;
this.scheduler.setProcessFunction(function(eventToSend) {
this.scheduler.setProcessFunction(async function(eventToSend) {
const room = self.getRoom(eventToSend.getRoomId());
if (eventToSend.status !== EventStatus.SENDING) {
_updatePendingEventStatus(room, eventToSend,
EventStatus.SENDING);
}
return _sendEventHttpRequest(self, eventToSend);
const res = await _sendEventHttpRequest(self, eventToSend);
if (room) {
// ensure we update pending event before the next scheduler run so that any listeners to event id
// updates on the synchronous event emitter get a chance to run first.
room.updatePendingEvent(eventToSend, EventStatus.SENT, res.event_id);
}
return res;
});
}
this.clientRunning = false;
@@ -407,9 +426,9 @@ export function MatrixClient(opts) {
break;
}
highlightCount += this.getPushActionsForEvent(
event,
).tweaks.highlight ? 1 : 0;
const pushActions = this.getPushActionsForEvent(event);
highlightCount += pushActions.tweaks &&
pushActions.tweaks.highlight ? 1 : 0;
}
// Note: we don't need to handle 'total' notifications because the counts
@@ -685,6 +704,9 @@ MatrixClient.prototype.initCrypto = async function() {
throw new Error(`Cannot enable encryption: no cryptoStore provided`);
}
logger.log("Crypto: Starting up crypto store...");
await this._cryptoStore.startup();
// initialise the list of encrypted rooms (whether or not crypto is enabled)
logger.log("Crypto: initialising roomlist...");
await this._roomList.init();
@@ -720,6 +742,7 @@ MatrixClient.prototype.initCrypto = async function() {
"crypto.roomKeyRequestCancellation",
"crypto.warning",
"crypto.devicesUpdated",
"crypto.willUpdateDevices",
"deviceVerificationChanged",
"userTrustStatusChanged",
"crossSigning.keysChanged",
@@ -728,6 +751,7 @@ MatrixClient.prototype.initCrypto = async function() {
logger.log("Crypto: initialising crypto object...");
await crypto.init({
exportedOlmDevice: this._exportedOlmDeviceToImport,
pickleKey: this.pickleKey,
});
delete this._exportedOlmDeviceToImport;
@@ -749,7 +773,6 @@ MatrixClient.prototype.isCryptoEnabled = function() {
return this._crypto !== null;
};
/**
* Get the Ed25519 key for this device
*
@@ -763,6 +786,19 @@ MatrixClient.prototype.getDeviceEd25519Key = function() {
return this._crypto.getDeviceEd25519Key();
};
/**
* Get the Curve25519 key for this device
*
* @return {?string} base64-encoded curve25519 key. Null if crypto is
* disabled.
*/
MatrixClient.prototype.getDeviceCurve25519Key = function() {
if (!this._crypto) {
return null;
}
return this._crypto.getDeviceCurve25519Key();
};
/**
* Upload the device keys to the homeserver.
* @return {object} A promise that will resolve when the keys are uploaded.
@@ -796,9 +832,9 @@ MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
*
* @param {string} userId the user to list keys for.
*
* @return {Promise<module:crypto/deviceinfo[]>} list of devices
* @return {module:crypto/deviceinfo[]} list of devices
*/
MatrixClient.prototype.getStoredDevicesForUser = async function(userId) {
MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
@@ -811,9 +847,9 @@ 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 {module:crypto/deviceinfo} device or null
*/
MatrixClient.prototype.getStoredDevice = async function(userId, deviceId) {
MatrixClient.prototype.getStoredDevice = function(userId, deviceId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
@@ -918,6 +954,20 @@ MatrixClient.prototype.requestVerificationDM = function(userId, roomId) {
return this._crypto.requestVerificationDM(userId, roomId);
};
/**
* Finds a DM verification request that is already in progress for the given room id
*
* @param {string} roomId the room to use for verification
*
* @returns {module:crypto/verification/request/VerificationRequest?} the VerificationRequest that is in progress, if any
*/
MatrixClient.prototype.findVerificationRequestDMInProgress = function(roomId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.findVerificationRequestDMInProgress(roomId);
};
/**
* Request a key verification from another user.
*
@@ -1097,6 +1147,12 @@ function wrapCryptoFuncs(MatrixClient, names) {
* @returns {boolean} true if the key matches, otherwise false
*/
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
*/
wrapCryptoFuncs(MatrixClient, [
"resetCrossSigningKeys",
"getCrossSigningId",
@@ -1105,6 +1161,11 @@ wrapCryptoFuncs(MatrixClient, [
"checkDeviceTrust",
"checkOwnCrossSigningTrust",
"checkCrossSigningPrivateKey",
"legacyDeviceVerification",
"prepareToEncrypt",
"isCrossSigningReady",
"getCryptoTrustCrossSignedDevices",
"setCryptoTrustCrossSignedDevices",
]);
/**
@@ -1131,7 +1192,9 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
* @param {string} password Passphrase string that can be entered by the user
* when restoring the backup as an alternative to entering the recovery key.
* Optional.
* @returns {Promise<String>} The user-facing recovery key string.
* @returns {Promise<Object>} Object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
/**
@@ -1203,7 +1266,9 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
* @param {boolean} checkKey check if the secret is encrypted by a trusted
* key
*
* @return {boolean} whether or not the secret is stored
* @return {object?} map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
/**
@@ -1257,6 +1322,7 @@ wrapCryptoFuncs(MatrixClient, [
"requestSecret",
"getDefaultSecretStorageKeyId",
"setDefaultSecretStorageKeyId",
"checkSecretStorageKey",
"checkSecretStoragePrivateKey",
]);
@@ -1303,7 +1369,8 @@ MatrixClient.prototype.cancelAndResendEventRoomKeyRequest = function(event) {
};
/**
* Enable end-to-end encryption for a room.
* Enable end-to-end encryption for a room. This does not modify room state.
* Any messages sent before the returned promise resolves will be sent unencrypted.
* @param {string} roomId The room ID to enable encryption in.
* @param {object} config The encryption config for the room.
* @return {Promise} A promise that will resolve when encryption is set up.
@@ -1375,15 +1442,17 @@ MatrixClient.prototype.exportRoomKeys = function() {
* Import a list of room keys previously exported by exportRoomKeys
*
* @param {Object[]} keys a list of session export objects
* @param {Object} opts
* @param {Function} opts.progressCallback called with an object that has a "stage" param
*
* @return {Promise} a promise which resolves when the keys
* have been imported
*/
MatrixClient.prototype.importRoomKeys = function(keys) {
MatrixClient.prototype.importRoomKeys = function(keys, opts) {
if (!this._crypto) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.importRoomKeys(keys);
return this._crypto.importRoomKeys(keys, opts);
};
/**
@@ -1443,12 +1512,16 @@ MatrixClient.prototype.isKeyBackupTrusted = function(info) {
/**
* @returns {bool} true if the client is configured to back up keys to
* the server, otherwise false.
* the server, otherwise false. If we haven't completed a successful check
* of key backup status yet, returns null.
*/
MatrixClient.prototype.getKeyBackupEnabled = function() {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
if (!this._crypto._checkedForBackup) {
return null;
}
return Boolean(this._crypto.backupKey);
};
@@ -1512,7 +1585,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
throw new Error("End-to-end encryption disabled");
}
const [keyInfo, encodedPrivateKey, privateKey] =
const { keyInfo, encodedPrivateKey, privateKey } =
await this.createRecoveryKeyFromPassphrase(password);
if (secureSecretStorage) {
@@ -1537,7 +1610,9 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
/**
* Check whether the key backup private key is stored in secret storage.
* @return {Promise<boolean>} Whether the backup key is stored.
* @return {Promise<object?>} map of key name to key info the secret is
* encrypted with, or null if it is not present or not encrypted with a
* trusted key
*/
MatrixClient.prototype.isKeyBackupKeyStored = async function() {
return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */);
@@ -1690,6 +1765,35 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
}
};
/**
* Get the raw key for a key backup from the password
* Used when migrating key backups into SSSS
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {string} password Passphrase
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @return {Promise<Buffer>} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromPassword = function(
password, backupInfo,
) {
return keyFromAuthData(backupInfo.auth_data, password);
};
/**
* Get the raw key for a key backup from the recovery key
* Used when migrating key backups into SSSS
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {string} recoveryKey The recovery key
* @return {Buffer} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function(recoveryKey) {
return decodeRecoveryKey(recoveryKey);
};
MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
/**
@@ -1701,15 +1805,16 @@ MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
password, targetRoomId, targetSessionId, backupInfo,
password, targetRoomId, targetSessionId, backupInfo, opts,
) {
const privKey = await keyFromAuthData(backupInfo.auth_data, password);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
@@ -1722,15 +1827,26 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function(
backupInfo, targetRoomId, targetSessionId,
backupInfo, targetRoomId, targetSessionId, opts,
) {
const privKey = decodeBase64(await this.getSecret("m.megolm_backup.v1"));
const storedKey = await this.getSecret("m.megolm_backup.v1");
// ensure that the key is in the right format. If not, fix the key and
// store the fixed version
const fixedKey = fixBackupKey(storedKey);
if (fixedKey) {
const [keyId] = await this._crypto.getSecretStorageKey();
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]);
}
const privKey = decodeBase64(fixedKey || storedKey);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
@@ -1743,20 +1859,50 @@ MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function(
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
recoveryKey, targetRoomId, targetSessionId, backupInfo,
recoveryKey, targetRoomId, targetSessionId, backupInfo, opts,
) {
const privKey = decodeRecoveryKey(recoveryKey);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
/**
* Restore from an existing key backup using a cached key, or fail
*
* @param {string} [targetRoomId] Room ID to target a specific room.
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithCache = async function(
targetRoomId, targetSessionId, backupInfo, opts,
) {
const privKey = await this._crypto.getSessionBackupPrivateKey();
if (!privKey) {
throw new Error("Couldn't get key");
}
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
MatrixClient.prototype._restoreKeyBackup = function(
privKey, targetRoomId, targetSessionId, backupInfo,
{
cacheCompleteCallback, // For sequencing during tests
progressCallback,
}={},
) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
@@ -1784,6 +1930,19 @@ MatrixClient.prototype._restoreKeyBackup = function(
return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY});
}
// Cache the key, if possible.
// This is async.
this._crypto.storeSessionBackupPrivateKey(privKey)
.catch((e) => {
console.warn("Error caching session backup key:", e);
}).then(cacheCompleteCallback);
if (progressCallback) {
progressCallback({
stage: "fetch",
});
}
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData, undefined,
{prefix: PREFIX_UNSTABLE},
@@ -1818,7 +1977,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
}
}
return this.importRoomKeys(keys);
return this.importRoomKeys(keys, { progressCallback });
}).then(() => {
return this._crypto.setTrustedBackupPubKey(backupPubKey);
}).then(() => {
@@ -1954,6 +2113,7 @@ MatrixClient.prototype.getUsers = function() {
/**
* Set account data event for the current user.
* It will retry the request up to 5 times.
* @param {string} eventType The event type
* @param {Object} contents the contents object for the event
* @param {module:client.callback} callback Optional.
@@ -1965,9 +2125,13 @@ MatrixClient.prototype.setAccountData = function(eventType, contents, callback)
$userId: this.credentials.userId,
$type: eventType,
});
return this._http.authedRequest(
callback, "PUT", path, undefined, contents,
);
const promise = retryNetworkOperation(5, () => {
return this._http.authedRequest(undefined, "PUT", path, undefined, contents);
});
if (callback) {
promise.then(result => callback(null, result), callback);
}
return promise;
};
/**
@@ -2002,9 +2166,17 @@ MatrixClient.prototype.getAccountDataFromServer = async function(eventType) {
$userId: this.credentials.userId,
$type: eventType,
});
return this._http.authedRequest(
undefined, "GET", path, undefined,
);
try {
const result = await this._http.authedRequest(
undefined, "GET", path, undefined,
);
return result;
} catch (e) {
if (e.data && e.data.errcode === 'M_NOT_FOUND') {
return null;
}
throw e;
}
};
/**
@@ -2318,6 +2490,7 @@ MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
const localEvent = new MatrixEvent(Object.assign(eventObject, {
event_id: "~" + roomId + ":" + txnId,
user_id: this.credentials.userId,
sender: this.credentials.userId,
room_id: roomId,
origin_server_ts: new Date().getTime(),
}));
@@ -2381,7 +2554,7 @@ function _sendEvent(client, room, event, callback) {
let promise;
// this event may be queued
if (client.scheduler) {
// if this returns a promsie then the scheduler has control now and will
// if this returns a promise then the scheduler has control now and will
// resolve/reject when it is done. Internally, the scheduler will invoke
// processFn which is set to this._sendEventHttpRequest so the same code
// path is executed regardless.
@@ -2395,12 +2568,15 @@ function _sendEvent(client, room, event, callback) {
if (!promise) {
promise = _sendEventHttpRequest(client, event);
if (room) {
promise = promise.then(res => {
room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
return res;
});
}
}
return promise;
}).then(function(res) { // the request was sent OK
if (room) {
room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
}
if (callback) {
callback(null, res);
}
@@ -4375,64 +4551,54 @@ MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
* @param {Filter} filter
* @return {Promise<String>} Filter ID
*/
MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
MatrixClient.prototype.getOrCreateFilter = async function(filterName, filter) {
const filterId = this.store.getFilterIdByName(filterName);
let promise = Promise.resolve();
const self = this;
let existingId = undefined;
if (filterId) {
// check that the existing filter matches our expectations
promise = self.getFilter(self.credentials.userId,
filterId, true,
).then(function(existingFilter) {
const oldDef = existingFilter.getDefinition();
const newDef = filter.getDefinition();
try {
const existingFilter =
await this.getFilter(this.credentials.userId, filterId, true);
if (existingFilter) {
const oldDef = existingFilter.getDefinition();
const newDef = filter.getDefinition();
if (utils.deepCompare(oldDef, newDef)) {
// super, just use that.
// debuglog("Using existing filter ID %s: %s", filterId,
// JSON.stringify(oldDef));
return Promise.resolve(filterId);
if (utils.deepCompare(oldDef, newDef)) {
// super, just use that.
// debuglog("Using existing filter ID %s: %s", filterId,
// JSON.stringify(oldDef));
existingId = filterId;
}
}
// debuglog("Existing filter ID %s: %s; new filter: %s",
// filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
self.store.setFilterIdByName(filterName, undefined);
return undefined;
}, function(error) {
} catch (error) {
// Synapse currently returns the following when the filter cannot be found:
// {
// errcode: "M_UNKNOWN",
// name: "M_UNKNOWN",
// message: "No row found",
// data: Object, httpStatus: 404
// }
if (error.httpStatus === 404 &&
(error.errcode === "M_UNKNOWN" || error.errcode === "M_NOT_FOUND")) {
// Clear existing filterId from localStorage
// if it no longer exists on the server
self.store.setFilterIdByName(filterName, undefined);
// Return a undefined value for existingId further down the promise chain
return undefined;
} else {
if (error.errcode !== "M_UNKNOWN" && error.errcode !== "M_NOT_FOUND") {
throw error;
}
});
}
// if the filter doesn't exist anymore on the server, remove from store
if (!existingId) {
this.store.setFilterIdByName(filterName, undefined);
}
}
return promise.then(function(existingId) {
if (existingId) {
return existingId;
}
if (existingId) {
return existingId;
}
// create a new filter
return self.createFilter(filter.getDefinition(),
).then(function(createdFilter) {
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
// JSON.stringify(createdFilter.getDefinition()));
self.store.setFilterIdByName(filterName, createdFilter.filterId);
return createdFilter.filterId;
});
});
// create a new filter
const createdFilter = await this.createFilter(filter.getDefinition());
// debuglog("Created new filter ID %s: %s", createdFilter.filterId,
// JSON.stringify(createdFilter.getDefinition()));
this.store.setFilterIdByName(filterName, createdFilter.filterId);
return createdFilter.filterId;
};
@@ -4610,6 +4776,13 @@ MatrixClient.prototype.startClient = async function(opts) {
};
}
// Create our own user object artificially (instead of waiting for sync)
// so it's always available, even if the user is not in any rooms etc.
const userId = this.getUserId();
if (userId) {
this.store.storeUser(new User(userId));
}
if (this._crypto) {
this._crypto.uploadDeviceKeys();
this._crypto.start();
@@ -4783,6 +4956,19 @@ MatrixClient.prototype.doesServerSupportSeparateAddAndBind = async function() {
|| (unstableFeatures && unstableFeatures["m.separate_add_and_bind"]);
};
/**
* Query the server to see if it lists support for an unstable feature
* in the /versions response
* @param {string} feature the feature name
* @return {Promise<boolean>} true if the feature is supported
*/
MatrixClient.prototype.doesServerSupportUnstableFeature = async function(feature) {
const response = await this.getVersions();
if (!response) return false;
const unstableFeatures = response["unstable_features"];
return unstableFeatures && !!unstableFeatures[feature];
};
/**
* Get if lazy loading members is being used.
* @return {boolean} Whether or not members are lazy loaded by this client
@@ -5070,9 +5256,6 @@ function checkTurnServers(client) {
if (!client._supportsVoip) {
return;
}
if (client.isGuest()) {
return; // guests can't access TURN servers
}
client.turnServer().then(function(res) {
if (res.uris) {
@@ -5113,17 +5296,20 @@ function _resolve(callback, resolve, res) {
resolve(res);
}
function _PojoToMatrixEventMapper(client) {
function _PojoToMatrixEventMapper(client, options) {
const preventReEmit = Boolean(options && options.preventReEmit);
function mapper(plainOldJsObject) {
const event = new MatrixEvent(plainOldJsObject);
if (event.isEncrypted()) {
client.reEmitter.reEmit(event, [
"Event.decrypted",
]);
if (!preventReEmit) {
client.reEmitter.reEmit(event, [
"Event.decrypted",
]);
}
event.attemptDecryption(client._crypto);
}
const room = client.getRoom(event.getRoomId());
if (room) {
if (room && !preventReEmit) {
room.reEmitter.reEmit(event, ["Event.replaced"]);
}
return event;
@@ -5132,10 +5318,21 @@ function _PojoToMatrixEventMapper(client) {
}
/**
* @param {object} [options]
* @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client
* @return {Function}
*/
MatrixClient.prototype.getEventMapper = function() {
return _PojoToMatrixEventMapper(this);
MatrixClient.prototype.getEventMapper = function(options = undefined) {
return _PojoToMatrixEventMapper(this, options);
};
/**
* The app may wish to see if we have a key cached without
* triggering a user interaction.
* @return {object}
*/
MatrixClient.prototype.getCrossSigningCacheCallbacks = function() {
return this._crypto && this._crypto._crossSigningInfo.getCacheCallbacks();
};
// Identity Server Operations
@@ -5412,6 +5609,22 @@ MatrixClient.prototype.generateClientSecret = function() {
* });
*/
/**
* Fires whenever the stored devices for a user have changed
* @event module:client~MatrixClient#"crypto.devicesUpdated"
* @param {String[]} users A list of user IDs that were updated
* @param {bool} initialFetch If true, the store was empty (apart
* from our own device) and has been seeded.
*/
/**
* Fires whenever the stored devices for a user will be updated
* @event module:client~MatrixClient#"crypto.willUpdateDevices"
* @param {String[]} users A list of user IDs that will be updated
* @param {bool} initialFetch If true, the store is empty (apart
* from our own device) and is being seeded.
*/
/**
* Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled()
* @event module:client~MatrixClient#"crypto.keyBackupStatus"
@@ -5454,13 +5667,6 @@ MatrixClient.prototype.generateClientSecret = function() {
* reject the key verification.
*/
/**
* Fires when a key verification started message is received.
* @event module:client~MatrixClient#"crypto.verification.start"
* @param {module:crypto/verification/Base} verifier a verifier object to
* perform the key verification
*/
/**
* Fires when a secret request has been cancelled. If the client is prompting
* the user to ask whether they want to share a secret, the prompt can be
+166 -28
View File
@@ -23,6 +23,8 @@ limitations under the License.
import {decodeBase64, encodeBase64, pkSign, pkVerify} from './olmlib';
import {EventEmitter} from 'events';
import {logger} from '../logger';
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
import {decryptAES, encryptAES} from './aes';
function publicKeyFromKeyInfo(keyInfo) {
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
@@ -40,8 +42,9 @@ export class CrossSigningInfo extends EventEmitter {
* @param {string} userId the user that the information is about
* @param {object} callbacks Callbacks used to interact with the app
* Requires getCrossSigningKey and saveCrossSigningKeys
* @param {object} cacheCallbacks Callbacks used to interact with the cache
*/
constructor(userId, callbacks) {
constructor(userId, callbacks, cacheCallbacks) {
super();
// you can't change the userId
@@ -50,8 +53,15 @@ export class CrossSigningInfo extends EventEmitter {
value: userId,
});
this._callbacks = callbacks || {};
this._cacheCallbacks = cacheCallbacks || {};
this.keys = {};
this.firstUse = true;
// This tracks whether we've ever verified this user with any identity.
// When you verify a user, any devices online at the time that receive
// the verifying signature via the homeserver will latch this to true
// and can use it in the future to detect cases where the user has
// become unverifed later for any reason.
this.crossSigningVerifiedBefore = false;
}
/**
@@ -62,6 +72,8 @@ export class CrossSigningInfo extends EventEmitter {
* @returns {Array} An array with [ public key, Olm.PkSigning ]
*/
async getCrossSigningKey(type, expectedPubkey) {
const shouldCache = ["self_signing", "user_signing"].indexOf(type) >= 0;
if (!this._callbacks.getCrossSigningKey) {
throw new Error("No getCrossSigningKey callback supplied");
}
@@ -70,22 +82,47 @@ export class CrossSigningInfo extends EventEmitter {
expectedPubkey = this.getId(type);
}
const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
function validateKey(key) {
if (!key) return;
const signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(key);
if (gotPubkey === expectedPubkey) {
return [gotPubkey, signing];
}
signing.free();
}
let privkey;
if (this._cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
privkey = await this._cacheCallbacks
.getCrossSigningKeyCache(type, expectedPubkey);
}
const cacheresult = validateKey(privkey);
if (cacheresult) {
return cacheresult;
}
privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
const result = validateKey(privkey);
if (result) {
if (this._cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
await this._cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
}
return result;
}
/* No keysource even returned a key */
if (!privkey) {
throw new Error(
"getCrossSigningKey callback for " + type + " returned falsey",
);
}
const signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(privkey);
if (gotPubkey !== expectedPubkey) {
signing.free();
throw new Error(
"Key type " + type + " from getCrossSigningKey callback did not match",
);
} else {
return [gotPubkey, signing];
}
/* We got some keys from the keysource, but none of them were valid */
throw new Error(
"Key type " + type + " from getCrossSigningKey callback did not match",
);
}
static fromStorage(obj, userId) {
@@ -102,6 +139,7 @@ export class CrossSigningInfo extends EventEmitter {
return {
keys: this.keys,
firstUse: this.firstUse,
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
};
}
@@ -111,14 +149,28 @@ export class CrossSigningInfo extends EventEmitter {
* want to know this anyway...
*
* @param {SecretStorage} secretStorage The secret store using account data
* @returns {boolean} Whether all private keys were found in storage
* @returns {object} map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
async isStoredInSecretStorage(secretStorage) {
let stored = true;
for (const type of ["master", "self_signing", "user_signing"]) {
stored &= await secretStorage.isStored(`m.cross_signing.${type}`, false);
// check what SSSS keys have encrypted the master key (if any)
const stored =
await secretStorage.isStored("m.cross_signing.master", false) || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s) {
for (const k of Object.keys(stored)) {
if (!s[k]) {
delete stored[k];
}
}
}
return stored;
for (const type of ["self_signing", "user_signing"]) {
intersect(
await secretStorage.isStored(`m.cross_signing.${type}`, false) || {},
);
}
return Object.keys(stored).length ? stored : null;
}
/**
@@ -259,6 +311,13 @@ export class CrossSigningInfo extends EventEmitter {
}
}
/**
* unsets the keys, used when another session has reset the keys, to disable cross-signing
*/
clearKeys() {
this.keys = {};
}
setKeys(keys) {
const signingKeys = {};
if (keys.master) {
@@ -331,6 +390,14 @@ export class CrossSigningInfo extends EventEmitter {
}
}
updateCrossSigningVerifiedBefore(isCrossSigningVerified) {
// It is critical that this value latches forward from false to true but
// never back to false to avoid a downgrade attack.
if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) {
this.crossSigningVerifiedBefore = true;
}
}
async signObject(data, type) {
if (!this.keys[type]) {
throw new Error(
@@ -389,13 +456,13 @@ export class CrossSigningInfo extends EventEmitter {
&& this.getId("self_signing")
&& this.getId("self_signing") === userCrossSigning.getId("self_signing")
) {
return new UserTrustLevel(true, this.firstUse);
return new UserTrustLevel(true, true, this.firstUse);
}
if (!this.keys.user_signing) {
// If there's no user signing key, they can't possibly be verified.
// They may be TOFU trusted though.
return new UserTrustLevel(false, userCrossSigning.firstUse);
return new UserTrustLevel(false, false, userCrossSigning.firstUse);
}
let userTrusted;
@@ -407,7 +474,11 @@ export class CrossSigningInfo extends EventEmitter {
} catch (e) {
userTrusted = false;
}
return new UserTrustLevel(userTrusted, userCrossSigning.firstUse);
return new UserTrustLevel(
userTrusted,
userCrossSigning.crossSigningVerifiedBefore,
userCrossSigning.firstUse,
);
}
/**
@@ -416,17 +487,20 @@ export class CrossSigningInfo extends EventEmitter {
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
* @param {module:crypto/deviceinfo} device The device to check
* @param {bool} localTrust Whether the device is trusted locally
* @param {bool} trustCrossSignedDevices Whether we trust cross signed devices
*
* @returns {DeviceTrustLevel}
*/
checkDeviceTrust(userCrossSigning, device, localTrust) {
checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) {
const userTrust = this.checkUserTrust(userCrossSigning);
const userSSK = userCrossSigning.keys.self_signing;
if (!userSSK) {
// if the user has no self-signing key then we cannot make any
// trust assertions about this device from cross-signing
return new DeviceTrustLevel(false, false, localTrust);
return new DeviceTrustLevel(
false, false, localTrust, trustCrossSignedDevices,
);
}
const deviceObj = deviceToObject(device, userCrossSigning.userId);
@@ -438,11 +512,22 @@ export class CrossSigningInfo extends EventEmitter {
deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId,
);
// ...then we trust this device as much as far as we trust the user
return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust);
return DeviceTrustLevel.fromUserTrustLevel(
userTrust, localTrust, trustCrossSignedDevices,
);
} catch (e) {
return new DeviceTrustLevel(false, false, localTrust);
return new DeviceTrustLevel(
false, false, localTrust, trustCrossSignedDevices,
);
}
}
/**
* @returns {object} Cache callbacks
*/
getCacheCallbacks() {
return this._cacheCallbacks;
}
}
function deviceToObject(device, userId) {
@@ -465,8 +550,9 @@ export const CrossSigningLevel = {
* Represents the ways in which we trust a user
*/
export class UserTrustLevel {
constructor(crossSigningVerified, tofu) {
constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) {
this._crossSigningVerified = crossSigningVerified;
this._crossSigningVerifiedBefore = crossSigningVerifiedBefore;
this._tofu = tofu;
}
@@ -484,6 +570,14 @@ export class UserTrustLevel {
return this._crossSigningVerified;
}
/**
* @returns {bool} true if we ever verified this user before (at least for
* the history of verifications observed by this device).
*/
wasCrossSigningVerified() {
return this._crossSigningVerifiedBefore;
}
/**
* @returns {bool} true if this user's key is trusted on first use
*/
@@ -496,17 +590,19 @@ export class UserTrustLevel {
* Represents the ways in which we trust a device
*/
export class DeviceTrustLevel {
constructor(crossSigningVerified, tofu, localVerified) {
constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) {
this._crossSigningVerified = crossSigningVerified;
this._tofu = tofu;
this._localVerified = localVerified;
this._trustCrossSignedDevices = trustCrossSignedDevices;
}
static fromUserTrustLevel(userTrustLevel, localVerified) {
static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) {
return new DeviceTrustLevel(
userTrustLevel._crossSigningVerified,
userTrustLevel._tofu,
localVerified,
trustCrossSignedDevices,
);
}
@@ -514,7 +610,9 @@ export class DeviceTrustLevel {
* @returns {bool} true if this device is verified via any means
*/
isVerified() {
return this.isCrossSigningVerified() || this.isLocallyVerified();
return Boolean(this.isLocallyVerified() || (
this._trustCrossSignedDevices && this.isCrossSigningVerified()
));
}
/**
@@ -539,3 +637,43 @@ export class DeviceTrustLevel {
return this._tofu;
}
}
export function createCryptoStoreCacheCallbacks(store, olmdevice) {
return {
getCrossSigningKeyCache: async function(type, _expectedPublicKey) {
const key = await new Promise((resolve) => {
return store.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
store.getSecretStorePrivateKey(txn, resolve, type);
},
);
});
if (key && key.ciphertext) {
const pickleKey = Buffer.from(olmdevice._pickleKey);
const decrypted = await decryptAES(key, pickleKey, type);
return decodeBase64(decrypted);
} else {
return key;
}
},
storeCrossSigningKeyCache: async function(type, key) {
if (!(key instanceof Uint8Array)) {
throw new Error(
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
);
}
const pickleKey = Buffer.from(olmdevice._pickleKey);
key = await encryptAES(encodeBase64(key), pickleKey, type);
return store.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
store.storeSecretStorePrivateKey(txn, type, key);
},
);
},
};
}
+58 -22
View File
@@ -109,6 +109,9 @@ export class DeviceList extends EventEmitter {
this._savePromiseTime = null;
// The timer used to delay the save
this._saveTimer = null;
// True if we have fetched data from the server or loaded a non-empty
// set of device data from the store
this._hasFetched = null;
}
/**
@@ -118,6 +121,7 @@ export class DeviceList extends EventEmitter {
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
this._hasFetched = Boolean(deviceData && deviceData.devices);
this._devices = deviceData ? deviceData.devices : {},
this._crossSigningInfo = deviceData ?
deviceData.crossSigningInfo || {} : {};
@@ -197,16 +201,16 @@ export class DeviceList extends EventEmitter {
const resolveSavePromise = this._resolveSavePromise;
this._savePromiseTime = targetTime;
this._saveTimer = setTimeout(() => {
logger.log('Saving device tracking data at token ' + this._syncToken);
logger.log('Saving device tracking data', this._syncToken);
// null out savePromise now (after the delay but before the write),
// otherwise we could return the existing promise when the save has
// actually already happened. Likewise for the dirty flag.
// actually already happened.
this._savePromiseTime = null;
this._saveTimer = null;
this._savePromise = null;
this._resolveSavePromise = null;
this._dirty = false;
this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({
@@ -217,7 +221,13 @@ export class DeviceList extends EventEmitter {
}, txn);
},
).then(() => {
// The device list is considered dirty until the write
// completes.
this._dirty = false;
resolveSavePromise();
}, err => {
logger.error('Failed to save device tracking data', this._syncToken);
logger.error(err);
});
}, delay);
}
@@ -311,6 +321,15 @@ export class DeviceList extends EventEmitter {
return stored;
}
/**
* Returns a list of all user IDs the DeviceList knows about
*
* @return {array} All known user IDs
*/
getKnownUserIds() {
return Object.keys(this._devices);
}
/**
* Get the stored device keys for a user id
*
@@ -373,6 +392,26 @@ export class DeviceList extends EventEmitter {
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
}
/**
* Get a user ID by one of their device's curve25519 identity key
*
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
*
* @return {string} user ID
*/
getUserByIdentityKey(algorithm, senderKey) {
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
// we only deal in olm keys
return null;
}
return this._userByIdentityKey[senderKey];
}
/**
* Find a device by curve25519 identity key
*
@@ -382,19 +421,11 @@ export class DeviceList extends EventEmitter {
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(algorithm, senderKey) {
const userId = this._userByIdentityKey[senderKey];
const userId = this.getUserByIdentityKey(algorithm, senderKey);
if (!userId) {
return null;
}
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
// we only deal in olm keys
return null;
}
const devices = this._devices[userId];
if (!devices) {
return null;
@@ -625,6 +656,7 @@ export class DeviceList extends EventEmitter {
});
const finished = (success) => {
this.emit("crypto.willUpdateDevices", users, !this._hasFetched);
users.forEach((u) => {
this._dirty = true;
@@ -650,7 +682,8 @@ export class DeviceList extends EventEmitter {
}
});
this.saveIfDirty();
this.emit("crypto.devicesUpdated", users);
this.emit("crypto.devicesUpdated", users, !this._hasFetched);
this._hasFetched = true;
};
return prom;
@@ -749,31 +782,34 @@ class DeviceListUpdateSerialiser {
this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then((res) => {
).then(async (res) => {
const dk = res.device_keys || {};
const masterKeys = res.master_keys || {};
const ssks = res.self_signing_keys || {};
const usks = res.user_signing_keys || {};
// do each user in a separate promise, to avoid wedging the CPU
// yield to other things that want to execute in between users, to
// avoid wedging the CPU
// (https://github.com/vector-im/riot-web/issues/3158)
//
// of course we ought to do this in a web worker or similar, but
// this serves as an easy solution for now.
let prom = Promise.resolve();
for (const userId of downloadUsers) {
prom = prom.then(sleep(5)).then(() => {
return this._processQueryResponseForUser(
await sleep(5);
try {
await this._processQueryResponseForUser(
userId, dk[userId], {
master: masterKeys[userId],
self_signing: ssks[userId],
user_signing: usks[userId],
},
);
});
} catch (e) {
// log the error but continue, so that one bad key
// doesn't kill the whole process
logger.error(`Error processing keys for ${userId}:`, e);
}
}
return prom;
}).then(() => {
logger.log('Completed key download for ' + downloadUsers);
@@ -794,7 +830,7 @@ class DeviceListUpdateSerialiser {
}
async _processQueryResponseForUser(
userId, dkResponse, crossSigningResponse, sskResponse,
userId, dkResponse, crossSigningResponse,
) {
logger.log('got device keys for ' + userId + ':', dkResponse);
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
+15 -1
View File
@@ -36,9 +36,15 @@ function checkPayloadLength(payloadString) {
// Note that even if we manage to do the encryption, the message send may fail,
// because by the time we've wrapped the ciphertext in the event object, it may
// exceed 65K. But at least we won't just fail with "abort()" in that case.
throw new Error("Message too long (" + payloadString.length + " bytes). " +
const err = new Error("Message too long (" + payloadString.length + " bytes). " +
"The maximum for an encrypted message is " +
MAX_PLAINTEXT_LENGTH + " bytes.");
// TODO: [TypeScript] We should have our own error types
err.data = {
errcode: "M_TOO_LARGE",
error: "Payload too large for encrypted message",
};
throw err;
}
}
@@ -105,6 +111,9 @@ export function OlmDevice(cryptoStore) {
// Keep track of sessions that we're starting, so that we don't start
// multiple sessions for the same device at the same time.
this._sessionsInProgress = {};
// Used by olm to serialise prekey message decryptions
this._olmPrekeyPromise = Promise.resolve();
}
/**
@@ -1029,6 +1038,11 @@ OlmDevice.prototype.addInboundGroupSession = async function(
}
}
logger.info(
"Storing megolm session " + senderKey + "/" + sessionId +
" with first index " + session.first_known_index(),
);
const sessionData = {
room_id: roomId,
session: session.pickle(this._pickleKey),
+26 -15
View File
@@ -58,7 +58,7 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500;
*
* @enum {number}
*/
const ROOM_KEY_REQUEST_STATES = {
export const ROOM_KEY_REQUEST_STATES = {
/** request not yet sent */
UNSENT: 0,
@@ -97,10 +97,6 @@ export class OutgoingRoomKeyRequestManager {
*/
start() {
this._clientRunning = true;
// set the timer going, to handle any requests which didn't get sent
// on the previous run of the client.
this._startTimer();
}
/**
@@ -113,7 +109,14 @@ export class OutgoingRoomKeyRequestManager {
}
/**
* Send off a room key request, if we haven't already done so.
* Send any requests that have been queued
*/
sendQueuedRequests() {
this._startTimer();
}
/**
* Queue up a room key request, if we haven't already queued or sent one.
*
* The `requestBody` is compared (with a deep-equality check) against
* previous queued or sent requests and if it matches, no change is made.
@@ -129,7 +132,7 @@ export class OutgoingRoomKeyRequestManager {
* pending list (or we have established that a similar request already
* exists)
*/
async sendRoomKeyRequest(requestBody, recipients, resend=false) {
async queueRoomKeyRequest(requestBody, recipients, resend=false) {
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
);
@@ -184,7 +187,7 @@ export class OutgoingRoomKeyRequestManager {
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
// raced with another tab to mark the request cancelled.
// Try again, to make sure the request is resent.
return await this.sendRoomKeyRequest(
return await this.queueRoomKeyRequest(
requestBody, recipients, resend,
);
}
@@ -220,9 +223,6 @@ export class OutgoingRoomKeyRequestManager {
throw new Error('unhandled state: ' + req.state);
}
}
// some of the branches require the timer to be started. Just start it
// all the time, because it doesn't hurt to start it.
this._startTimer();
}
/**
@@ -327,6 +327,21 @@ export class OutgoingRoomKeyRequestManager {
);
}
/**
* Find anything in `sent` state, and kick it around the loop again.
* This is intended for situations where something substantial has changed, and we
* don't really expect the other end to even care about the cancellation.
* For example, after initialization or self-verification.
* @return {Promise} An array of `queueRoomKeyRequest` outputs.
*/
async cancelAndResendAllOutgoingRequests() {
const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(
ROOM_KEY_REQUEST_STATES.SENT,
);
return Promise.all(outgoings.map(({ requestBody, recipients }) =>
this.queueRoomKeyRequest(requestBody, recipients, true)));
}
// start the background timer to send queued requests, if the timer isn't
// already running
_startTimer() {
@@ -366,15 +381,12 @@ export class OutgoingRoomKeyRequestManager {
return Promise.resolve();
}
logger.log("Looking for queued outgoing room key requests");
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
ROOM_KEY_REQUEST_STATES.UNSENT,
]).then((req) => {
if (!req) {
logger.log("No more outgoing room key requests");
this._sendOutgoingRoomKeyRequestsTimer = null;
return;
}
@@ -398,7 +410,6 @@ export class OutgoingRoomKeyRequestManager {
}).catch((e) => {
logger.error("Error sending room key request; will retry later.", e);
this._sendOutgoingRoomKeyRequestsTimer = null;
this._startTimer();
});
});
}
+109 -170
View File
@@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,21 +17,24 @@ limitations under the License.
import {EventEmitter} from 'events';
import {logger} from '../logger';
import * as olmlib from './olmlib';
import {pkVerify} from './olmlib';
import {randomString} from '../randomstring';
import {encryptAES, decryptAES} from './aes';
import {encodeBase64} from "./olmlib";
export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-sha2";
export const SECRET_STORAGE_ALGORITHM_V1_AES
= "m.secret_storage.v1.aes-hmac-sha2";
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
*/
export class SecretStorage extends EventEmitter {
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
constructor(baseApis, cryptoCallbacks) {
super();
this._baseApis = baseApis;
this._cryptoCallbacks = cryptoCallbacks;
this._crossSigningInfo = crossSigningInfo;
this._requests = {};
this._incomingRequests = {};
}
@@ -45,7 +48,7 @@ export class SecretStorage extends EventEmitter {
}
setDefaultKeyId(keyId) {
return new Promise((resolve) => {
return new Promise(async (resolve, reject) => {
const listener = (ev) => {
if (
ev.getType() === 'm.secret_storage.default_key' &&
@@ -57,10 +60,15 @@ export class SecretStorage extends EventEmitter {
};
this._baseApis.on('accountData', listener);
this._baseApis.setAccountData(
'm.secret_storage.default_key',
{ key: keyId },
);
try {
await this._baseApis.setAccountData(
'm.secret_storage.default_key',
{ key: keyId },
);
} catch (e) {
this._baseApis.removeListener('accountData', listener);
reject(e);
}
});
}
@@ -84,28 +92,16 @@ export class SecretStorage extends EventEmitter {
keyData.name = opts.name;
}
switch (algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
{
const decryption = new global.Olm.PkDecryption();
try {
const { passphrase, pubkey } = opts;
// Copies in public key details of the form generated by
// the Crypto module's `createRecoveryKeyFromPassphrase`.
if (passphrase && pubkey) {
keyData.passphrase = passphrase;
keyData.pubkey = pubkey;
} else if (pubkey) {
keyData.pubkey = pubkey;
} else {
keyData.pubkey = decryption.generate_key();
}
} finally {
decryption.free();
if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (opts.passphrase) {
keyData.passphrase = opts.passphrase;
}
break;
}
default:
if (opts.key) {
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
keyData.iv = iv;
keyData.mac = mac;
}
} else {
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
}
@@ -119,8 +115,6 @@ export class SecretStorage extends EventEmitter {
);
}
await this._crossSigningInfo.signObject(keyData, 'master');
await this._baseApis.setAccountData(
`m.secret_storage.key.${keyId}`, keyData,
);
@@ -129,30 +123,25 @@ export class SecretStorage extends EventEmitter {
}
/**
* Signs a given secret storage key with the cross-signing master key.
* Get the key information for a given ID.
*
* @param {string} [keyId = default key's ID] The ID of the key to sign.
* Defaults to the default key ID if not provided.
* @param {string} [keyId = default key's ID] The ID of the key to check
* for. Defaults to the default key ID if not provided.
* @returns {Array?} If the key was found, the return value is an array of
* the form [keyId, keyInfo]. Otherwise, null is returned.
*/
async signKey(keyId) {
async getKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
throw new Error("signKey requires a key ID");
return null;
}
const keyInfo = await this._baseApis.getAccountDataFromServer(
`m.secret_storage.key.${keyId}`,
);
if (!keyInfo) {
throw new Error(`Key ${keyId} does not exist in account data`);
}
await this._crossSigningInfo.signObject(keyInfo, 'master');
await this._baseApis.setAccountData(
`m.secret_storage.key.${keyId}`, keyInfo,
"m.secret_storage.key." + keyId,
);
return keyInfo ? [keyId, keyInfo] : null;
}
/**
@@ -163,16 +152,33 @@ export class SecretStorage extends EventEmitter {
* @return {boolean} Whether we have the key.
*/
async hasKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
return false;
}
return !!(await this.getKey(keyId));
}
return !!this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
/**
* Check whether a key matches what we expect based on the key info
*
* @param {Uint8Array} key the key to check
* @param {object} info the key info
*
* @return {boolean} whether or not the key matches
*/
async checkKey(key, info) {
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (info.mac) {
const {mac} = await SecretStorage._calculateKeyCheck(key, info.iv);
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
} else {
// if we have no information, we have to assume the key is right
return true;
}
} else {
throw new Error("Unknown algorithm");
}
}
static async _calculateKeyCheck(key, iv) {
return await encryptAES(ZERO_STR, key, "", iv);
}
/**
@@ -207,27 +213,12 @@ export class SecretStorage extends EventEmitter {
throw new Error("Unknown key: " + keyId);
}
// check signature of key info
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
// encrypt secret, based on the algorithm
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
{
const encryption = new global.Olm.PkEncryption();
try {
encryption.set_recipient_key(keyInfo.pubkey);
encrypted[keyId] = encryption.encrypt(secret);
} finally {
encryption.free();
}
break;
}
default:
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const keys = {[keyId]: keyInfo};
const [, encryption] = await this._getSecretStorageKey(keys, name);
encrypted[keyId] = await encryption.encrypt(secret);
} else {
logger.warn("unknown algorithm for secret storage key " + keyId
+ ": " + keyInfo.algorithm);
// do nothing if we don't understand the encryption algorithm
@@ -238,29 +229,6 @@ export class SecretStorage extends EventEmitter {
await this._baseApis.setAccountData(name, {encrypted});
}
/**
* Store a secret defined to be the same as the given key.
* No secret information will be stored, instead the secret will
* be stored with a marker to say that the contents of the secret is
* the value of the given key.
* This is useful for migration from systems that predate SSSS such as
* key backup.
*
* @param {string} name The name of the secret
* @param {string} keyId The ID of the key whose value will be the
* value of the secret
* @returns {Promise} resolved when account data is saved
*/
storePassthrough(name, keyId) {
return this._baseApis.setAccountData(name, {
encrypted: {
[keyId]: {
passthrough: true,
},
},
});
}
/**
* Temporary method to fix up existing accounts where secrets
* are incorrectly stored without the 'encrypted' level
@@ -316,19 +284,11 @@ export class SecretStorage extends EventEmitter {
"m.secret_storage.key." + keyId,
);
const encInfo = secretInfo.encrypted[keyId];
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
if (
keyInfo.pubkey && (
(encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) ||
encInfo.passthrough
)
) {
// only use keys we understand the encryption algorithm of
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
keys[keyId] = keyInfo;
}
break;
default:
// do nothing if we don't understand the encryption algorithm
}
}
@@ -341,18 +301,13 @@ export class SecretStorage extends EventEmitter {
const encInfo = secretInfo.encrypted[keyId];
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself.
if (encInfo.passthrough) return decryption.get_private_key();
// since we just want to return the key itself. It must be base64
// encoded, since this is how a key would normally be stored.
if (encInfo.passthrough) return encodeBase64(decryption.get_private_key());
// decrypt secret
switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
return decryption.decrypt(
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
);
}
return await decryption.decrypt(encInfo);
} finally {
if (decryption) decryption.free();
if (decryption && decryption.free) decryption.free();
}
}
@@ -362,55 +317,43 @@ export class SecretStorage extends EventEmitter {
* @param {string} name the name of the secret
* @param {boolean} checkKey check if the secret is encrypted by a trusted key
*
* @return {boolean} whether or not the secret is stored
* @return {object?} map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
async isStored(name, checkKey) {
// check if secret exists
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo) return false;
if (!secretInfo) return null;
if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
return false;
return null;
}
}
if (checkKey === undefined) checkKey = true;
// check if secret is encrypted by a known/trusted secret and
// encryption looks sane
const ret = {};
// filter secret encryption keys with supported algorithm
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) return false;
if (!keyInfo) continue;
const encInfo = secretInfo.encrypted[keyId];
if (checkKey) {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
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
&& encInfo.ephemeral) {
return true;
// only use keys we understand the encryption algorithm of
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
ret[keyId] = keyInfo;
}
break;
default:
// do nothing if we don't understand the encryption algorithm
}
}
return false;
return Object.keys(ret).length ? ret : null;
}
/**
@@ -462,6 +405,7 @@ export class SecretStorage extends EventEmitter {
for (const device of devices) {
toDevice[device] = requestData;
}
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
this._baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice,
});
@@ -515,6 +459,7 @@ export class SecretStorage extends EventEmitter {
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
});
if (secret) {
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
const payload = {
type: "m.secret.send",
content: {
@@ -532,7 +477,7 @@ export class SecretStorage extends EventEmitter {
this._baseApis,
{
[sender]: [
await this._baseApis.getStoredDevice(sender, deviceId),
this._baseApis.getStoredDevice(sender, deviceId),
],
},
);
@@ -542,7 +487,7 @@ export class SecretStorage extends EventEmitter {
this._baseApis.deviceId,
this._baseApis._crypto._olmDevice,
sender,
this._baseApis._crypto.getStoredDevice(sender, deviceId),
this._baseApis.getStoredDevice(sender, deviceId),
payload,
);
const contentMap = {
@@ -551,7 +496,10 @@ export class SecretStorage extends EventEmitter {
},
};
logger.info(`Sending ${content.name} secret for ${deviceId}`);
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
} else {
logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
}
}
}
@@ -563,7 +511,7 @@ export class SecretStorage extends EventEmitter {
return;
}
const content = event.getContent();
logger.log("got secret share for request ", content.request_id);
logger.log("got secret share for request", content.request_id);
const requestControl = this._requests[content.request_id];
if (requestControl) {
// make sure that the device that sent it is one of the devices that
@@ -606,27 +554,18 @@ export class SecretStorage extends EventEmitter {
throw new Error("App returned unknown key from getSecretStorageKey!");
}
switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
{
const decryption = new global.Olm.PkDecryption();
let pubkey;
try {
pubkey = decryption.init_with_private_key(privateKey);
} catch (e) {
decryption.free();
throw new Error("getSecretStorageKey callback returned invalid key");
}
if (pubkey !== keys[keyId].pubkey) {
decryption.free();
throw new Error(
"getSecretStorageKey callback returned incorrect key",
);
}
return [keyId, decryption];
}
default:
throw new Error("Unknown key type: " + keys[keyId].algorithm);
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
const decryption = {
encrypt: async function(secret) {
return await encryptAES(secret, privateKey, name);
},
decrypt: async function(encInfo) {
return await decryptAES(encInfo, privateKey, name);
},
};
return [keyId, decryption];
} else {
throw new Error("Unknown key type: " + keys[keyId].algorithm);
}
}
}
+251
View File
@@ -0,0 +1,251 @@
/*
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 {getCrypto} from '../utils';
import {decodeBase64, encodeBase64} from './olmlib';
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
// salt for HKDF, with 8 bytes of zeros
const zerosalt = new Uint8Array(8);
/**
* encrypt a string in Node.js
*
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptNode(data, key, name, ivStr) {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
let iv;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = crypto.randomBytes(16);
}
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
const [aesKey, hmacKey] = deriveKeysNode(key, name);
const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv);
const ciphertext = cipher.update(data, "utf-8", "base64")
+ cipher.final("base64");
const hmac = crypto.createHmac("sha256", hmacKey)
.update(ciphertext, "base64").digest("base64");
return {
iv: encodeBase64(iv),
ciphertext: ciphertext,
mac: hmac,
};
}
/**
* decrypt a string in Node.js
*
* @param {object} data the encrypted data
* @param {string} data.ciphertext the ciphertext in base64
* @param {string} data.iv the initialization vector in base64
* @param {string} data.mac the HMAC in base64
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptNode(data, key, name) {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
const [aesKey, hmacKey] = deriveKeysNode(key, name);
const hmac = crypto.createHmac("sha256", hmacKey)
.update(data.ciphertext, "base64").digest("base64").replace(/=+$/g, '');
if (hmac !== data.mac.replace(/=+$/g, '')) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const decipher = crypto.createDecipheriv(
"aes-256-ctr", aesKey, decodeBase64(data.iv),
);
return decipher.update(data.ciphertext, "base64", "utf-8")
+ decipher.final("utf-8");
}
function deriveKeysNode(key, name) {
const crypto = getCrypto();
const prk = crypto.createHmac("sha256", zerosalt)
.update(key).digest();
const b = Buffer.alloc(1, 1);
const aesKey = crypto.createHmac("sha256", prk)
.update(name, "utf-8").update(b).digest();
b[0] = 2;
const hmacKey = crypto.createHmac("sha256", prk)
.update(aesKey).update(name, "utf-8").update(b).digest();
return [aesKey, hmacKey];
}
/**
* encrypt a string in Node.js
*
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
* @param {string} ivStr the initialization vector to use
*/
async function encryptBrowser(data, key, name, ivStr) {
let iv;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);
}
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
const encodedData = new TextEncoder().encode(data);
const ciphertext = await subtleCrypto.encrypt(
{
name: "AES-CTR",
counter: iv,
length: 64,
},
aesKey,
encodedData,
);
const hmac = await subtleCrypto.sign(
{name: 'HMAC'},
hmacKey,
ciphertext,
);
return {
iv: encodeBase64(iv),
ciphertext: encodeBase64(ciphertext),
mac: encodeBase64(hmac),
};
}
/**
* decrypt a string in the browser
*
* @param {object} data the encrypted data
* @param {string} data.ciphertext the ciphertext in base64
* @param {string} data.iv the initialization vector in base64
* @param {string} data.mac the HMAC in base64
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptBrowser(data, key, name) {
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
const ciphertext = decodeBase64(data.ciphertext);
if (!await subtleCrypto.verify(
{name: "HMAC"},
hmacKey,
decodeBase64(data.mac),
ciphertext,
)) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const plaintext = await subtleCrypto.decrypt(
{
name: "AES-CTR",
counter: decodeBase64(data.iv),
length: 64,
},
aesKey,
ciphertext,
);
return new TextDecoder().decode(new Uint8Array(plaintext));
}
async function deriveKeysBrowser(key, name) {
const hkdfkey = await subtleCrypto.importKey(
'raw',
key,
{name: "HKDF"},
false,
["deriveBits"],
);
const keybits = await subtleCrypto.deriveBits(
{
name: "HKDF",
salt: zerosalt,
info: (new TextEncoder().encode(name)),
hash: "SHA-256",
},
hkdfkey,
512,
);
const aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32);
const aesProm = subtleCrypto.importKey(
'raw',
aesKey,
{name: 'AES-CTR'},
false,
['encrypt', 'decrypt'],
);
const hmacProm = subtleCrypto.importKey(
'raw',
hmacKey,
{
name: 'HMAC',
hash: {name: 'SHA-256'},
},
false,
['sign', 'verify'],
);
return await Promise.all([aesProm, hmacProm]);
}
export function encryptAES(...args) {
return subtleCrypto ? encryptBrowser(...args) : encryptNode(...args);
}
export function decryptAES(...args) {
return subtleCrypto ? decryptBrowser(...args) : decryptNode(...args);
}
+9
View File
@@ -60,6 +60,15 @@ export class EncryptionAlgorithm {
this._roomId = params.roomId;
}
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
*/
prepareToEncrypt(room) {
}
/**
* Encrypt a message event
*
+322 -188
View File
@@ -185,15 +185,15 @@ 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
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
* session creation
*
* @return {Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked,
devicesInRoom, blocked, singleOlmCreationPhase,
) {
const self = this;
let session;
// takes the previous OutboundSessionInfo, and considers whether to create
@@ -201,12 +201,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
// Updates `session` to hold the final OutboundSessionInfo.
//
// returns a promise which resolves once the keyshare is successful.
async function prepareSession(oldSession) {
const prepareSession = async (oldSession) => {
session = oldSession;
// need to make a brand new session?
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
this._sessionRotationPeriodMs)
) {
logger.log("Starting new megolm session because we need to rotate.");
session = null;
@@ -218,32 +218,20 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
if (!session) {
logger.log(`Starting new megolm session for room ${self._roomId}`);
session = await self._prepareNewSession();
logger.log(`Starting new megolm session for room ${this._roomId}`);
session = await this._prepareNewSession();
logger.log(`Started new megolm session ${session.sessionId} ` +
`for room ${self._roomId}`);
self._outboundSessions[session.sessionId] = session;
`for room ${this._roomId}`);
this._outboundSessions[session.sessionId] = session;
}
// now check if we need to share with any devices
const shareMap = {};
for (const userId in devicesInRoom) {
if (!devicesInRoom.hasOwnProperty(userId)) {
continue;
}
const userDevices = devicesInRoom[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
const deviceInfo = userDevices[deviceId];
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
for (const [deviceId, deviceInfo] of Object.entries(userDevices)) {
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
if (key == this._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
@@ -258,51 +246,99 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
}
const errorDevices = [];
await self._shareKeyWithDevices(
session, shareMap, errorDevices,
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
},
};
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
this._olmDevice, this._baseApis, shareMap,
);
// are there any new blocked devices that we need to notify?
const blockedMap = {};
for (const userId in blocked) {
if (!blocked.hasOwnProperty(userId)) {
continue;
}
await Promise.all([
(async () => {
// share keys with devices that we already have a session for
await this._shareKeyWithOlmSessions(
session, key, payload, olmSessions,
);
})(),
(async () => {
const errorDevices = [];
const userBlockedDevices = blocked[userId];
// meanwhile, establish olm sessions for devices that we don't
// already have a session for, and share keys with them. If
// we're doing two phases of olm session creation, use a
// shorter timeout when fetching one-time keys for the first
// phase.
const start = Date.now();
const failedServers = [];
await this._shareKeyWithDevices(
session, key, payload, devicesWithoutSession, errorDevices,
singleOlmCreationPhase ? 10000 : 2000, failedServers,
);
for (const deviceId in userBlockedDevices) {
if (!userBlockedDevices.hasOwnProperty(deviceId)) {
continue;
if (!singleOlmCreationPhase && (Date.now() - start < 10000)) {
// perform the second phase of olm session creation if requested,
// and if the first phase didn't take too long
(async () => {
// Retry sending keys to devices that we were unable to establish
// an olm session for. This time, we use a longer timeout, but we
// do this in the background and don't block anything else while we
// do this. We only need to retry users from servers that didn't
// respond the first time.
const retryDevices = {};
const failedServerMap = new Set;
for (const server of failedServers) {
failedServerMap.add(server);
}
const failedDevices = [];
for (const {userId, deviceInfo} of errorDevices) {
const userHS = userId.slice(userId.indexOf(":") + 1);
if (failedServerMap.has(userHS)) {
retryDevices[userId] = retryDevices[userId] || [];
retryDevices[userId].push(deviceInfo);
} else {
// if we aren't going to retry, then handle it
// as a failed device
failedDevices.push({userId, deviceInfo});
}
}
await this._shareKeyWithDevices(
session, key, payload, retryDevices, failedDevices, 30000,
);
await this._notifyFailedOlmDevices(session, key, failedDevices);
})();
} else {
await this._notifyFailedOlmDevices(session, key, errorDevices);
}
})(),
(async () => {
// also, notify blocked devices that they're blocked
const blockedMap = {};
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
for (const [deviceId, device] of Object.entries(userBlockedDevices)) {
if (
!session.blockedDevicesNotified[userId] ||
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || {};
blockedMap[userId][deviceId] = {device};
}
}
}
if (
!session.blockedDevicesNotified[userId] ||
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push(userBlockedDevices[deviceId]);
}
}
}
const filteredErrorDevices =
await self._olmDevice.filterOutNotifiedErrorDevices(errorDevices);
for (const {userId, deviceInfo} of filteredErrorDevices) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push({
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
});
}
// notify blocked devices that they're blocked
await self._notifyBlockedDevices(session, blockedMap);
}
await this._notifyBlockedDevices(session, blockedMap);
})(),
]);
};
// helper which returns the session prepared by prepareSession
function returnSession() {
@@ -349,96 +385,47 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
};
/**
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call. Also filters out devices we don't have
* a session with.
* Determines what devices in devicesByUser don't have an olm session as given
* in devicemap.
*
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
* @param {object} devicemap the devices that have olm sessions, as returned by
* olmlib.ensureOlmSessionsForDevices.
* @param {object} devicesByUser a map of user IDs to array of deviceInfo
* @param {array} [noOlmDevices] an array to fill with devices that don't have
* olm sessions
*
* @param {number} chainIndex current chain index
*
* @param {object<userId, deviceId>} devicemap
* mapping from userId to deviceId to {@link module:crypto~OlmSessionResult}
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that can't get an
* olm session for
*
* @return {array<object<userid, deviceInfo>>}
* @return {array} an array of devices that don't have olm sessions. If
* noOlmDevices is specified, then noOlmDevices will be returned.
*/
MegolmEncryption.prototype._splitUserDeviceMap = function(
session, chainIndex, devicemap, devicesByUser, errorDevices,
MegolmEncryption.prototype._getDevicesWithoutSessions = function(
devicemap, devicesByUser, noOlmDevices,
) {
const maxUsersPerRequest = 20;
noOlmDevices = noOlmDevices || [];
// use an array where the slices of a content map gets stored
const mapSlices = [];
let currentSliceId = 0; // start inserting in the first slice
let entriesInCurrentSlice = 0;
for (const userId of Object.keys(devicesByUser)) {
const devicesToShareWith = devicesByUser[userId];
for (const [userId, devicesToShareWith] of Object.entries(devicesByUser)) {
const sessionResults = devicemap[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
for (const deviceInfo of devicesToShareWith) {
const deviceId = deviceInfo.deviceId;
const sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
// mark this device as "handled" because we don't want to try
// to claim a one-time-key for dead devices on every message.
session.markSharedWithDevice(userId, deviceId, chainIndex);
errorDevices.push({userId, deviceInfo});
noOlmDevices.push({userId, deviceInfo});
delete sessionResults[deviceId];
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
logger.log(
"share keys with device " + userId + ":" + deviceId,
);
if (!mapSlices[currentSliceId]) {
mapSlices[currentSliceId] = [];
}
mapSlices[currentSliceId].push({
userId: userId,
deviceInfo: deviceInfo,
});
entriesInCurrentSlice++;
}
// We do this in the per-user loop as we prefer that all messages to the
// same user end up in the same API call to make it easier for the
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (entriesInCurrentSlice > maxUsersPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
}
return mapSlices;
return noOlmDevices;
};
/**
@@ -451,20 +438,18 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
*
* @return {array<array<object>>} the blocked devices, split into chunks
*/
MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
const maxUsersPerRequest = 20;
MegolmEncryption.prototype._splitDevices = function(devicesByUser) {
const maxDevicesPerRequest = 20;
// use an array where the slices of a content map gets stored
let currentSlice = [];
const mapSlices = [currentSlice];
for (const userId of Object.keys(devicesByUser)) {
const userBlockedDevicesToShareWith = devicesByUser[userId];
for (const blockedInfo of userBlockedDevicesToShareWith) {
for (const [userId, userDevices] of Object.entries(devicesByUser)) {
for (const deviceInfo of Object.values(userDevices)) {
currentSlice.push({
userId: userId,
blockedInfo: blockedInfo,
deviceInfo: deviceInfo.device,
});
}
@@ -473,7 +458,7 @@ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (currentSlice.length > maxUsersPerRequest) {
if (currentSlice.length > maxDevicesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
currentSlice = [];
mapSlices.push(currentSlice);
@@ -536,6 +521,33 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
}
return Promise.all(promises).then(() => {
// prune out any devices that encryptMessageForDevice could not encrypt for,
// in which case it will have just not added anything to the ciphertext object.
// There's no point sending messages to devices if we couldn't encrypt to them,
// since that's effectively a blank message.
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) {
logger.log(
"No ciphertext for device " +
userId + ":" + deviceId + ": pruning",
);
delete contentMap[userId][deviceId];
}
}
// No devices left for that user? Strip that too.
if (Object.keys(contentMap[userId]).length === 0) {
logger.log("Pruned all devices for user " + userId);
delete contentMap[userId];
}
}
// Is there anything left?
if (Object.keys(contentMap).length === 0) {
logger.log("No users left to send to: aborting");
return;
}
return this._baseApis.sendToDevice("m.room.encrypted", contentMap).then(() => {
// store that we successfully uploaded the keys of the current slice
for (const userId of Object.keys(contentMap)) {
@@ -568,7 +580,7 @@ MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
for (const val of userDeviceMap) {
const userId = val.userId;
const blockedInfo = val.blockedInfo;
const blockedInfo = val.deviceInfo;
const deviceInfo = blockedInfo.deviceInfo;
const deviceId = deviceInfo.deviceId;
@@ -643,9 +655,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {
[userId]: {
[device.deviceId]: device,
},
[userId]: [device],
},
);
@@ -688,37 +698,44 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object} key the session key as returned by
* OlmDevice.getOutboundGroupSessionKey
*
* @param {object} payload the base to-device message payload for sharing keys
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that we can't get an
* olm session for
*
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
* one-time keys for establishing new olm sessions.
*
* @param {Array} [failedServers] An array to fill with remote servers that
* failed to respond to one-time-key requests.
*/
MegolmEncryption.prototype._shareKeyWithDevices = async function(
session, devicesByUser, errorDevices,
session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers,
) {
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
},
};
const devicemap = await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser,
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
);
const userDeviceMaps = this._splitUserDeviceMap(
session, key.chain_index, devicemap, devicesByUser, errorDevices,
);
this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
await this._shareKeyWithOlmSessions(session, key, payload, devicemap);
};
MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
session, key, payload, devicemap,
) {
const userDeviceMaps = this._splitDevices(devicemap);
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
@@ -736,6 +753,52 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(
}
};
/**
* Notify devices that we weren't able to create olm sessions.
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object} key
*
* @param {Array<object>} failedDevices the devices that we were unable to
* create olm sessions for, as returned by _shareKeyWithDevices
*/
MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
session, key, failedDevices,
) {
// mark the devices that failed as "handled" because we don't want to try
// to claim a one-time-key for dead devices on every message.
for (const {userId, deviceInfo} of failedDevices) {
const deviceId = deviceInfo.deviceId;
session.markSharedWithDevice(
userId, deviceId, key.chain_index,
);
}
const filteredFailedDevices =
await this._olmDevice.filterOutNotifiedErrorDevices(
failedDevices,
);
const blockedMap = {};
for (const {userId, deviceInfo} of filteredFailedDevices) {
blockedMap[userId] = blockedMap[userId] || {};
// we use a similar format to what
// olmlib.ensureOlmSessionsForDevices returns, so that
// we can use the same function to split
blockedMap[userId][deviceInfo.deviceId] = {
device: {
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
},
};
}
// send the notifications
await this._notifyBlockedDevices(session, blockedMap);
};
/**
* Notify blocked devices that they have been blocked.
*
@@ -754,7 +817,7 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
sender_key: this._olmDevice.deviceCurve25519Key,
};
const userDeviceMaps = this._splitBlockedDevices(devicesByUser);
const userDeviceMaps = this._splitDevices(devicesByUser);
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
@@ -772,6 +835,38 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
}
};
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
*/
MegolmEncryption.prototype.prepareToEncrypt = function(room) {
if (this.encryptionPreparation) {
// We're already preparing something, so don't do anything else.
// FIXME: check if we need to restart
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
return;
}
logger.debug(`Preparing to encrypt events for ${this._roomId}`);
this.encryptionPreparation = (async () => {
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
// Drop unknown devices for now. When the message gets sent, we'll
// throw an error, but we'll still be prepared to send to the known
// devices.
this._removeUnknownDevices(devicesInRoom);
}
await this._ensureOutboundSession(devicesInRoom, blocked, true);
delete this.encryptionPreparation;
})();
};
/**
* @inheritdoc
*
@@ -782,38 +877,48 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
* @return {Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
const self = this;
logger.log(`Starting to encrypt event for ${this._roomId}`);
if (this.encryptionPreparation) {
// If we started sending keys, wait for it to be done.
// FIXME: check if we need to cancel
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
try {
await this.encryptionPreparation;
} catch (e) {
// ignore any errors -- if the preparation failed, we'll just
// restart everything here
}
}
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
self._checkForUnknownDevices(devicesInRoom);
this._checkForUnknownDevices(devicesInRoom);
}
const session = await self._ensureOutboundSession(devicesInRoom, blocked);
const session = await this._ensureOutboundSession(devicesInRoom, blocked);
const payloadJson = {
room_id: self._roomId,
room_id: this._roomId,
type: eventType,
content: content,
};
const ciphertext = self._olmDevice.encryptGroupMessage(
const ciphertext = this._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
device_id: this._deviceId,
};
session.useCount++;
@@ -861,6 +966,27 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
}
};
/**
* Remove unknown devices from a set of devices. The devicesInRoom parameter
* will be modified.
*
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* devices we should shared the session with.
*/
MegolmEncryption.prototype._removeUnknownDevices = function(devicesInRoom) {
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
for (const [deviceId, device] of Object.entries(userDevices)) {
if (device.isUnverified() && !device.isKnown()) {
delete userDevices[deviceId];
}
}
if (Object.keys(userDevices).length === 0) {
delete devicesInRoom[userId];
}
}
};
/**
* Get the list of unblocked devices for all users in the room
*
@@ -904,8 +1030,10 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
continue;
}
const deviceTrust = this._crypto.checkDeviceTrust(userId, deviceId);
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting)
(!deviceTrust.isVerified() && isBlacklisting)
) {
if (!blocked[userId]) {
blocked[userId] = {};
@@ -1195,7 +1323,6 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed();
}
logger.log(`Received and adding key for megolm session ${senderKey}|${sessionId}`);
return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
@@ -1247,6 +1374,9 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
if (content.code === "m.no_olm") {
const sender = event.getSender();
logger.warn(
`${sender}:${senderKey} was unable to establish an olm session with us`,
);
// if the sender says that they haven't been able to establish an olm
// session, let's proactively establish one
@@ -1258,21 +1388,30 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
if (await this._olmDevice.getSessionIdForDevice(senderKey)) {
// a session has already been established, so we don't need to
// create a new one.
logger.debug("New session already created. Not creating a new one.");
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
this.retryDecryptionFromSender(senderKey);
return;
}
const device = this._crypto._deviceList.getDeviceByIdentityKey(
let device = this._crypto._deviceList.getDeviceByIdentityKey(
content.algorithm, senderKey,
);
if (!device) {
logger.info(
"Couldn't find device for identity key " + senderKey +
": not establishing session",
// if we don't know about the device, fetch the user's devices again
// and retry before giving up
await this._crypto.downloadKeys([sender], false);
device = this._crypto._deviceList.getDeviceByIdentityKey(
content.algorithm, senderKey,
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false);
this.retryDecryptionFromSender(senderKey);
return;
if (!device) {
logger.info(
"Couldn't find device for identity key " + senderKey +
": not establishing session",
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false);
this.retryDecryptionFromSender(senderKey);
return;
}
}
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {[sender]: [device]}, false,
@@ -1442,7 +1581,8 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
};
/**
* Have another go at decrypting events after we receive a key
* Have another go at decrypting events after we receive a key. Resolves once
* decryption has been re-attempted on all events.
*
* @private
* @param {String} senderKey
@@ -1461,27 +1601,22 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
return true;
}
pending.delete(sessionId);
if (pending.size === 0) {
this._pendingEvents[senderKey];
}
logger.debug("Retrying decryption on events", [...pending]);
await Promise.all([...pending].map(async (ev) => {
try {
await ev.attemptDecryption(this._crypto);
await ev.attemptDecryption(this._crypto, true);
} catch (e) {
// don't die if something goes wrong
}
}));
// ev.attemptDecryption will re-add to this._pendingEvents if an event
// couldn't be decrypted
// If decrypted successfully, they'll have been removed from _pendingEvents
return !((this._pendingEvents[senderKey] || {})[sessionId]);
};
MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) {
const senderPendingEvents = this._pendingEvents[senderKey];
logger.warn(senderPendingEvents);
if (!senderPendingEvents) {
return true;
}
@@ -1491,7 +1626,6 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
await Promise.all([...pending].map(async (ev) => {
try {
logger.warn(ev.getId());
await ev.attemptDecryption(this._crypto);
} catch (e) {
// don't die if something goes wrong
+19
View File
@@ -264,6 +264,25 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
*/
OlmDecryption.prototype._decryptMessage = async function(
theirDeviceIdentityKey, message,
) {
// This is a wrapper that serialises decryptions of prekey messages, because
// otherwise we race between deciding we have no active sessions for the message
// and creating a new one, which we can only do once because it removes the OTK.
if (message.type !== 0) {
// not a prekey message: we can safely just try & decrypt it
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
} else {
const myPromise = this._olmDevice._olmPrekeyPromise.then(() => {
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
});
// we want the error, but don't propagate it to the next decryption
this._olmDevice._olmPrekeyPromise = myPromise.catch(() => {});
return await myPromise;
}
};
OlmDecryption.prototype._reallyDecryptMessage = async function(
theirDeviceIdentityKey, message,
) {
const sessionIds = await this._olmDevice.getSessionIdsForDevice(
theirDeviceIdentityKey,
+757 -177
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -19,6 +19,8 @@ import {randomString} from '../randomstring';
const DEFAULT_ITERATIONS = 500000;
const DEFAULT_BITSIZE = 256;
export async function keyFromAuthData(authData, password) {
if (!global.Olm) {
throw new Error("Olm is not available");
@@ -34,6 +36,7 @@ export async function keyFromAuthData(authData, password) {
return await deriveKey(
password, authData.private_key_salt,
authData.private_key_iterations,
authData.private_key_bits || DEFAULT_BITSIZE,
);
}
@@ -44,12 +47,12 @@ export async function keyFromPassphrase(password) {
const salt = randomString(32);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
return { key, salt, iterations: DEFAULT_ITERATIONS };
}
export async function deriveKey(password, salt, iterations) {
export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
@@ -73,7 +76,7 @@ export async function deriveKey(password, salt, iterations) {
hash: 'SHA-512',
},
key,
global.Olm.PRIVATE_KEY_LENGTH * 8,
numBits,
);
return new Uint8Array(keybits);
+115 -16
View File
@@ -113,6 +113,57 @@ export async function encryptMessageForDevice(
);
}
/**
* Get the existing olm sessions for the given devices, and the devices that
* don't have olm sessions.
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
*
* @return {Promise} resolves to an array. The first element of the array is a
* a map of user IDs to arrays of deviceInfo, representing the devices that
* don't have established olm sessions. The second element of the array is
* a map from userId to deviceId to {@link module:crypto~OlmSessionResult}
*/
export async function getExistingOlmSessions(
olmDevice, baseApis, devicesByUser,
) {
const devicesWithoutSession = {};
const sessions = {};
const promises = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
promises.push((async () => {
const sessionId = await olmDevice.getSessionIdForDevice(
key, true,
);
if (sessionId === null) {
devicesWithoutSession[userId] = devicesWithoutSession[userId] || [];
devicesWithoutSession[userId].push(deviceInfo);
} else {
sessions[userId] = sessions[userId] || {};
sessions[userId][deviceId] = {
device: deviceInfo,
sessionId: sessionId,
};
}
})());
}
}
await Promise.all(promises);
return [devicesWithoutSession, sessions];
}
/**
* Try to make sure we have established olm sessions for the given devices.
*
@@ -123,32 +174,58 @@ export async function encryptMessageForDevice(
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
*
* @param {boolean} force If true, establish a new session even if one already exists.
* Optional.
* @param {boolean} [force=false] If true, establish a new session even if one
* already exists.
*
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
* one-time keys for establishing new olm sessions.
*
* @param {Array} [failedServers] An array to fill with remote servers that
* failed to respond to one-time-key requests.
*
* @return {Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
export async function ensureOlmSessionsForDevices(
olmDevice, baseApis, devicesByUser, force,
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers,
) {
if (typeof force === "number") {
failedServers = otkTimeout;
otkTimeout = force;
force = false;
}
const devicesWithoutSession = [
// [userId, deviceId], ...
];
const result = {};
const resolveSession = {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
result[userId] = {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
if (key === olmDevice.deviceCurve25519Key) {
// We should never be trying to start a session with ourself.
// Apart from talking to yourself being the first sign of madness,
// olm sessions can't do this because they get confused when
// they get a message and see that the 'other side' has started a
// new chain when this side has an active sender chain.
// If you see this message being logged in the wild, we should find
// the thing that is trying to send Olm messages to itself and fix it.
logger.info("Attempted to start session with ourself! Ignoring");
// We must fill in the section in the return value though, as callers
// expect it to be there.
result[userId][deviceId] = {
device: deviceInfo,
sessionId: null,
};
continue;
}
if (!olmDevice._sessionsInProgress[key]) {
// pre-emptively mark the session as in-progress to avoid race
// conditions. If we find that we already have a session, then
@@ -180,6 +257,11 @@ export async function ensureOlmSessionsForDevices(
delete resolveSession[key];
}
if (sessionId === null || force) {
if (force) {
logger.info("Forcing new Olm session for " + userId + ":" + deviceId);
} else {
logger.info("Making new Olm session for " + userId + ":" + deviceId);
}
devicesWithoutSession.push([userId, deviceId]);
}
result[userId][deviceId] = {
@@ -197,7 +279,7 @@ export async function ensureOlmSessionsForDevices(
let res;
try {
res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
@@ -207,18 +289,26 @@ export async function ensureOlmSessionsForDevices(
throw e;
}
if (failedServers && "failures" in res) {
failedServers.push(...Object.keys(res.failures));
}
const otk_res = res.one_time_keys || {};
const promises = [];
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
const userRes = otk_res[userId] || {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
if (key === olmDevice.deviceCurve25519Key) {
// We've already logged about this above. Skip here too
// otherwise we'll log saying there are no one-time keys
// which will be confusing.
continue;
}
if (result[userId][deviceId].sessionId && !force) {
// we already have a result for this device
continue;
@@ -407,6 +497,15 @@ export function encodeBase64(uint8Array) {
return Buffer.from(uint8Array).toString("base64");
}
/**
* Encode a typed array of uint8 as unpadded base64.
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array) {
return encodeBase64(uint8Array).replace(/=+$/g, '');
}
/**
* Decode a base64 string to a typed array of uint8.
* @param {string} base64 The base64 to decode.
+2 -2
View File
@@ -59,8 +59,8 @@ export function decodeRecoveryKey(recoverykey) {
throw new Error("Incorrect length");
}
return result.slice(
return Uint8Array.from(result.slice(
OLM_RECOVERY_KEY_PREFIX.length,
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
);
));
}
@@ -203,6 +203,23 @@ export class Backend {
return promiseifyTxn(txn).then(() => result);
}
/**
*
* @param {Number} wantedState
* @return {Promise<Array<*>>} All elements in a given state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const index = store.index("state");
const request = index.getAll(wantedState);
request.onsuccess = (ev) => resolve(ev.target.result);
request.onerror = (ev) => reject(ev.target.error);
});
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
let stateIndex = 0;
const results = [];
@@ -341,18 +358,39 @@ export class Backend {
};
}
getSecretStorePrivateKey(txn, func, type) {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
getReq.onsuccess = function() {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
}
};
}
storeCrossSigningKeys(txn, keys) {
const objectStore = txn.objectStore("account");
objectStore.put(keys, "crossSigningKeys");
}
storeSecretStorePrivateKey(txn, type, key) {
const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`);
}
// Olm Sessions
countEndToEndSessions(txn, func) {
const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count();
countReq.onsuccess = function() {
func(countReq.result);
try {
func(countReq.result);
} catch (e) {
abortWithException(txn, e);
}
};
}
@@ -402,16 +440,16 @@ export class Backend {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
func(cursor.value);
cursor.continue();
} else {
try {
try {
const cursor = getReq.result;
if (cursor) {
func(cursor.value);
cursor.continue();
} else {
func(null);
} catch (e) {
abortWithException(txn, e);
}
} catch (e) {
abortWithException(txn, e);
}
};
}
+83 -111
View File
@@ -46,6 +46,7 @@ export class IndexedDBCryptoStore {
this._indexedDB = indexedDB;
this._dbName = dbName;
this._backendPromise = null;
this._backend = null;
}
static exists(indexedDB, dbName) {
@@ -56,10 +57,12 @@ export class IndexedDBCryptoStore {
* Ensure the database exists and is up-to-date, or fall back to
* a local storage or in-memory store.
*
* This must be called before the store can be used.
*
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
*/
_connect() {
startup() {
if (this._backendPromise) {
return this._backendPromise;
}
@@ -135,6 +138,8 @@ export class IndexedDBCryptoStore {
);
return new MemoryCryptoStore();
}
}).then(backend => {
this._backend = backend;
});
return this._backendPromise;
@@ -189,9 +194,7 @@ export class IndexedDBCryptoStore {
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
return this._connect().then((backend) => {
return backend.getOrAddOutgoingRoomKeyRequest(request);
});
return this._backend.getOrAddOutgoingRoomKeyRequest(request);
}
/**
@@ -205,9 +208,7 @@ export class IndexedDBCryptoStore {
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequest(requestBody);
});
return this._backend.getOutgoingRoomKeyRequest(requestBody);
}
/**
@@ -221,9 +222,18 @@ export class IndexedDBCryptoStore {
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestByState(wantedStates);
});
return this._backend.getOutgoingRoomKeyRequestByState(wantedStates);
}
/**
* Look for room key requests by state
* unlike above, return a list of all entries in one state.
*
* @param {Number} wantedState
* @return {Promise<Array<*>>} Returns an array of requests in the given state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
}
/**
@@ -237,11 +247,9 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
});
return this._backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
}
/**
@@ -257,11 +265,9 @@ export class IndexedDBCryptoStore {
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
return this._connect().then((backend) => {
return backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
});
return this._backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
}
/**
@@ -274,9 +280,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
return this._connect().then((backend) => {
return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
});
return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
}
// Olm Account
@@ -289,9 +293,7 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the account pickle
*/
getAccount(txn, func) {
this._backendPromise.then(backend => {
backend.getAccount(txn, func);
});
this._backend.getAccount(txn, func);
}
/**
@@ -302,9 +304,7 @@ export class IndexedDBCryptoStore {
* @param {string} newData The new account pickle to store.
*/
storeAccount(txn, newData) {
this._backendPromise.then(backend => {
backend.storeAccount(txn, newData);
});
this._backend.storeAccount(txn, newData);
}
/**
@@ -316,9 +316,16 @@ export class IndexedDBCryptoStore {
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
*/
getCrossSigningKeys(txn, func) {
this._backendPromise.then(backend => {
backend.getCrossSigningKeys(txn, func);
});
this._backend.getCrossSigningKeys(txn, func);
}
/**
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the private key
* @param {string} type A key type
*/
getSecretStorePrivateKey(txn, func, type) {
this._backend.getSecretStorePrivateKey(txn, func, type);
}
/**
@@ -328,9 +335,18 @@ export class IndexedDBCryptoStore {
* @param {string} keys keys object as getCrossSigningKeys()
*/
storeCrossSigningKeys(txn, keys) {
this._backendPromise.then(backend => {
backend.storeCrossSigningKeys(txn, keys);
});
this._backend.storeCrossSigningKeys(txn, keys);
}
/**
* Write the cross-signing private keys back to the store
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys()
*/
storeSecretStorePrivateKey(txn, type, key) {
this._backend.storeSecretStorePrivateKey(txn, type, key);
}
// Olm sessions
@@ -341,9 +357,7 @@ export class IndexedDBCryptoStore {
* @param {function(int)} func Called with the count of sessions
*/
countEndToEndSessions(txn, func) {
this._backendPromise.then(backend => {
backend.countEndToEndSessions(txn, func);
});
this._backend.countEndToEndSessions(txn, func);
}
/**
@@ -359,9 +373,7 @@ export class IndexedDBCryptoStore {
* a message.
*/
getEndToEndSession(deviceKey, sessionId, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndSession(deviceKey, sessionId, txn, func);
});
this._backend.getEndToEndSession(deviceKey, sessionId, txn, func);
}
/**
@@ -376,9 +388,7 @@ export class IndexedDBCryptoStore {
* a message.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndSessions(deviceKey, txn, func);
});
this._backend.getEndToEndSessions(deviceKey, txn, func);
}
/**
@@ -389,9 +399,7 @@ export class IndexedDBCryptoStore {
* and session keys.
*/
getAllEndToEndSessions(txn, func) {
this._backendPromise.then(backend => {
backend.getAllEndToEndSessions(txn, func);
});
this._backend.getAllEndToEndSessions(txn, func);
}
/**
@@ -402,29 +410,21 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndSession(
deviceKey, sessionId, sessionInfo, txn,
);
});
this._backend.storeEndToEndSession(
deviceKey, sessionId, sessionInfo, txn,
);
}
storeEndToEndSessionProblem(deviceKey, type, fixed) {
return this._backendPromise.then(async (backend) => {
await backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
});
return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
getEndToEndSessionProblem(deviceKey, timestamp) {
return this._backendPromise.then(async (backend) => {
return await backend.getEndToEndSessionProblem(deviceKey, timestamp);
});
return this._backend.getEndToEndSessionProblem(deviceKey, timestamp);
}
filterOutNotifiedErrorDevices(devices) {
return this._backendPromise.then(async (backend) => {
return await backend.filterOutNotifiedErrorDevices(devices);
});
return this._backend.filterOutNotifiedErrorDevices(devices);
}
// Inbound group sessions
@@ -439,11 +439,9 @@ export class IndexedDBCryptoStore {
* to Base64 end-to-end session.
*/
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
});
this._backend.getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
}
/**
@@ -454,9 +452,7 @@ export class IndexedDBCryptoStore {
* sessionData}, then once with null to indicate the end of the list.
*/
getAllEndToEndInboundGroupSessions(txn, func) {
this._backendPromise.then(backend => {
backend.getAllEndToEndInboundGroupSessions(txn, func);
});
this._backend.getAllEndToEndInboundGroupSessions(txn, func);
}
/**
@@ -469,11 +465,9 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.then(backend => {
backend.addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
this._backend.addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
/**
@@ -486,21 +480,17 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
this._backend.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
this._backendPromise.then(backend => {
backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
this._backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
// End-to-end device tracking
@@ -516,9 +506,7 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndDeviceData(deviceData, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndDeviceData(deviceData, txn);
});
this._backend.storeEndToEndDeviceData(deviceData, txn);
}
/**
@@ -529,9 +517,7 @@ export class IndexedDBCryptoStore {
* device data
*/
getEndToEndDeviceData(txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndDeviceData(txn, func);
});
this._backend.getEndToEndDeviceData(txn, func);
}
// End to End Rooms
@@ -543,9 +529,7 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndRoom(roomId, roomInfo, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndRoom(roomId, roomInfo, txn);
});
this._backend.storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
@@ -554,9 +538,7 @@ export class IndexedDBCryptoStore {
* @param {function(Object)} func Function called with the end to end encrypted rooms
*/
getEndToEndRooms(txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndRooms(txn, func);
});
this._backend.getEndToEndRooms(txn, func);
}
// session backups
@@ -568,9 +550,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves to an array of inbound group sessions
*/
getSessionsNeedingBackup(limit) {
return this._connect().then((backend) => {
return backend.getSessionsNeedingBackup(limit);
});
return this._backend.getSessionsNeedingBackup(limit);
}
/**
@@ -579,9 +559,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves to the number of sessions
*/
countSessionsNeedingBackup(txn) {
return this._connect().then((backend) => {
return backend.countSessionsNeedingBackup(txn);
});
return this._backend.countSessionsNeedingBackup(txn);
}
/**
@@ -591,9 +569,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves when the sessions are unmarked
*/
unmarkSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.unmarkSessionsNeedingBackup(sessions, txn);
});
return this._backend.unmarkSessionsNeedingBackup(sessions, txn);
}
/**
@@ -603,9 +579,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves when the sessions are marked
*/
markSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.markSessionsNeedingBackup(sessions, txn);
});
return this._backend.markSessionsNeedingBackup(sessions, txn);
}
/**
@@ -630,9 +604,7 @@ export class IndexedDBCryptoStore {
* exception will propagate to the caller of the getFoo method.
*/
doTxn(mode, stores, func) {
return this._connect().then((backend) => {
return backend.doTxn(mode, stores, func);
});
return this._backend.doTxn(mode, stores, func);
}
}
@@ -367,12 +367,23 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(keys);
}
getSecretStorePrivateKey(txn, func, type) {
const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key);
}
storeCrossSigningKeys(txn, keys) {
setJsonItem(
this.store, KEY_CROSS_SIGNING_KEYS, keys,
);
}
storeSecretStorePrivateKey(txn, type, key) {
setJsonItem(
this.store, E2E_PREFIX + `ssss_cache.${type}`, key,
);
}
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
+36
View File
@@ -33,6 +33,8 @@ export class MemoryCryptoStore {
this._outgoingRoomKeyRequests = [];
this._account = null;
this._crossSigningKeys = null;
this._privateKeys = {};
this._backupKeys = {};
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
@@ -51,6 +53,18 @@ export class MemoryCryptoStore {
this._sessionsNeedingBackup = {};
}
/**
* Ensure the database exists and is up-to-date.
*
* This must be called before the store can be used.
*
* @return {Promise} resolves to the store.
*/
async startup() {
// No startup work to do for the memory store.
return this;
}
/**
* Delete all data from this store.
*
@@ -152,6 +166,19 @@ export class MemoryCryptoStore {
return Promise.resolve(null);
}
/**
*
* @param {Number} wantedState
* @return {Promise<Array<*>>} All OutgoingRoomKeyRequests in state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
return Promise.resolve(
this._outgoingRoomKeyRequests.filter(
(r) => r.state == wantedState,
),
);
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
const results = [];
@@ -243,10 +270,19 @@ export class MemoryCryptoStore {
func(this._crossSigningKeys);
}
getSecretStorePrivateKey(txn, func, type) {
const result = this._privateKeys[type];
return func(result || null);
}
storeCrossSigningKeys(txn, keys) {
this._crossSigningKeys = keys;
}
storeSecretStorePrivateKey(txn, type, key) {
this._privateKeys[type] = key;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
+109 -9
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.
@@ -24,6 +25,8 @@ import {EventEmitter} from 'events';
import {logger} from '../../logger';
import {DeviceInfo} from '../deviceinfo';
import {newTimeoutError} from "./Error";
import {CrossSigningInfo} from "../CrossSigning";
import {decodeBase64} from "../olmlib";
const timeoutException = new Error("Verification timed out");
@@ -76,6 +79,8 @@ export class VerificationBase extends EventEmitter {
this._transactionTimeoutTimer = null;
}
static keyRequestTimeoutMs = 1000 * 60;
get initiatedByMe() {
// if there is no start event yet,
// we probably want to send it,
@@ -117,6 +122,11 @@ export class VerificationBase extends EventEmitter {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
const existingEvent = this.request.getEventFromOtherParty(type);
if (existingEvent) {
return Promise.resolve(existingEvent);
}
this._expectedEvent = type;
return new Promise((resolve, reject) => {
this._resolveEvent = resolve;
@@ -130,6 +140,8 @@ export class VerificationBase extends EventEmitter {
switchStartEvent(event) {
if (this.canSwitchStartEvent(event)) {
logger.log("Verification Base: switching verification start event",
{restartingFlow: !!this._rejectEvent});
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
@@ -155,10 +167,13 @@ export class VerificationBase extends EventEmitter {
} else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject;
this._reject = undefined;
const content = e.getContent();
const {reason, code} = content;
reject(new Error(`Other side cancelled verification ` +
`because ${reason} (${code})`));
// there is only promise to reject if verify has been called
if (reject) {
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
@@ -181,11 +196,95 @@ export class VerificationBase extends EventEmitter {
done() {
this._endTimer(); // always kill the activity timer
if (!this._done) {
if (this._channel.needsDoneMessage) {
// verification in DM requires a done message
this._send("m.key.verification.done", {});
}
this.request.onVerifierFinished();
this._resolve();
//#region Cross-signing keys request
// If this is a self-verification, ask the other party for keys
if (this._baseApis.getUserId() !== this.userId) {
return;
}
// FIXME: This is a lot of logic that isn't anything to do with verification
// and probably ought to be somewhere else.
console.log("VerificationBase.done: Self-verification done; requesting keys");
/* This happens asynchronously, and we're not concerned about
* waiting for it. We return here in order to test. */
return new Promise((resolve, reject) => {
const client = this._baseApis;
const original = client._crypto._crossSigningInfo;
/* We already have all of the infrastructure we need to validate and
* cache cross-signing keys, so instead of replicating that, here we
* set up callbacks that request them from the other device and call
* CrossSigningInfo.getCrossSigningKey() to validate/cache */
const crossSigning = new CrossSigningInfo(
original.userId,
{ getCrossSigningKey: async (type) => {
console.debug("VerificationBase.done: requesting secret",
type, this.deviceId);
const { promise } = client.requestSecret(
`m.cross_signing.${type}`, [this.deviceId],
);
const result = await promise;
const decoded = decodeBase64(result);
return Uint8Array.from(decoded);
} },
original._cacheCallbacks,
);
crossSigning.keys = original.keys;
// XXX: get all keys out if we get one key out
// https://github.com/vector-im/riot-web/issues/12604
// then change here to reject on the timeout
/* Requests can be ignored, so don't wait around forever */
const timeout = new Promise((resolve, reject) => {
setTimeout(
resolve,
VerificationBase.keyRequestTimeoutMs,
new Error("Timeout"),
);
});
// also request and cache the key backup key
const backupKeyPromise = new Promise(async resolve => {
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
if (!cachedKey) {
logger.info("No cached backup key found. Requesting...");
const secretReq = client.requestSecret(
'm.megolm_backup.v1', [this.deviceId],
);
const base64Key = await secretReq.promise;
logger.info("Got key backup key, decoding...");
const decodedKey = decodeBase64(base64Key);
logger.info("Decoded backup key, storing...");
client._crypto.storeSessionBackupPrivateKey(
Uint8Array.from(decodedKey),
);
logger.info("Backup key stored. Starting backup restore...");
const backupInfo = await client.getKeyBackupVersion();
// no need to await for this - just let it go in the bg
client.restoreKeyBackupWithCache(
undefined, undefined, backupInfo,
).then(() => {
logger.info("Backup restored.");
});
}
resolve();
});
/* We call getCrossSigningKey() for its side-effects */
return Promise.race([
Promise.all([
crossSigning.getCrossSigningKey("self_signing"),
crossSigning.getCrossSigningKey("user_signing"),
backupKeyPromise,
]),
timeout,
]).then(resolve, reject);
}).catch((e) => {
console.warn("VerificationBase: failure while requesting keys:", e);
});
//#endregion
}
}
@@ -193,6 +292,7 @@ export class VerificationBase extends EventEmitter {
this._endTimer(); // always kill the activity timer
if (!this._done) {
this.cancelled = true;
this.request.onVerifierCancelled();
if (this.userId && this.deviceId) {
// send a cancellation to the other user (if it wasn't
// cancelled by the other user)
@@ -275,7 +375,7 @@ export class VerificationBase extends EventEmitter {
for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1];
const device = await this._baseApis.getStoredDevice(userId, deviceId);
const device = this._baseApis.getStoredDevice(userId, deviceId);
if (device) {
await verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId);
+228 -36
View File
@@ -23,8 +23,9 @@ limitations under the License.
import {VerificationBase as Base} from "./Base";
import {
newKeyMismatchError,
newUserMismatchError,
newUserCancelledError,
} from './Error';
import {encodeUnpaddedBase64, decodeBase64} from "../olmlib";
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
@@ -49,47 +50,46 @@ export class ReciprocateQRCode extends Base {
"with this method yet.");
}
const targetUserId = this.startEvent.getSender();
if (!this.userId) {
console.log("Asking to confirm user ID");
this.userId = await new Promise((resolve, reject) => {
this.emit("confirm_user_id", {
userId: targetUserId,
confirm: resolve, // takes a userId
cancel: () => reject(newUserMismatchError()),
});
});
} else if (targetUserId !== this.userId) {
throw newUserMismatchError({
expected: this.userId,
actual: targetUserId,
});
}
if (this.startEvent.getContent()['secret'] !== this.request.encodedSharedSecret) {
const {qrCodeData} = this.request;
// 1. check the secret
if (this.startEvent.getContent()['secret'] !== qrCodeData.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;
// 2. ask if other user shows shield as well
await new Promise((resolve, reject) => {
this.reciprocateQREvent = {
confirm: resolve,
cancel: () => reject(newUserCancelledError()),
};
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
});
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];
// 3. determine key to sign / mark as trusted
const keys = {};
switch (qrCodeData.mode) {
case MODE_VERIFY_OTHER_USER: {
// add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
break;
}
case MODE_VERIFY_SELF_TRUSTED: {
const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
break;
}
case MODE_VERIFY_SELF_UNTRUSTED: {
const masterKey = qrCodeData.myMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
break;
}
}
// 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
// make sure the device has the expected keys
const targetKey = keys[keyId];
if (!targetKey) throw newKeyMismatchError();
@@ -106,8 +106,200 @@ export class ReciprocateQRCode extends Base {
throw newKeyMismatchError();
}
}
// Otherwise it is probably fine
});
}
}
const CODE_VERSION = 0x02; // the version of binary QR codes we support
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
export class QRCodeData {
constructor(
mode, sharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey, buffer,
) {
this._sharedSecret = sharedSecret;
this._mode = mode;
this._otherUserMasterKey = otherUserMasterKey;
this._otherDeviceKey = otherDeviceKey;
this._myMasterKey = myMasterKey;
this._buffer = buffer;
}
static async create(request, client) {
const sharedSecret = QRCodeData._generateSharedSecret();
const mode = QRCodeData._determineMode(request, client);
let otherUserMasterKey = null;
let otherDeviceKey = null;
let myMasterKey = null;
if (mode === MODE_VERIFY_OTHER_USER) {
const otherUserCrossSigningInfo =
client.getStoredCrossSigningForUser(request.otherUserId);
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
const myUserId = client.getUserId();
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
myMasterKey = myCrossSigningInfo.getId("master");
}
const qrData = QRCodeData._generateQrData(
request, client, mode,
sharedSecret,
otherUserMasterKey,
otherDeviceKey,
myMasterKey,
);
const buffer = QRCodeData._generateBuffer(qrData);
return new QRCodeData(mode, sharedSecret,
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
}
get buffer() {
return this._buffer;
}
get mode() {
return this._mode;
}
/**
* only set when mode is MODE_VERIFY_SELF_TRUSTED
* @return {string} device key of other party at time of generating QR code
*/
get otherDeviceKey() {
return this._otherDeviceKey;
}
/**
* only set when mode is MODE_VERIFY_OTHER_USER
* @return {string} master key of other party at time of generating QR code
*/
get otherUserMasterKey() {
return this._otherUserMasterKey;
}
/**
* only set when mode is MODE_VERIFY_SELF_UNTRUSTED
* @return {string} own master key at time of generating QR code
*/
get myMasterKey() {
return this._myMasterKey;
}
/**
* The unpadded base64 encoded shared secret.
*/
get encodedSharedSecret() {
return this._sharedSecret;
}
static _generateSharedSecret() {
const secretBytes = new Uint8Array(11);
global.crypto.getRandomValues(secretBytes);
return encodeUnpaddedBase64(secretBytes);
}
static async _getOtherDeviceKey(request, client) {
const myUserId = client.getUserId();
const otherDevice = request.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
const device = client.getStoredDevice(myUserId, otherDeviceId);
if (!device) {
throw new Error("could not find device " + otherDeviceId);
}
const key = device.getFingerprint();
return key;
}
static _determineMode(request, client) {
const myUserId = client.getUserId();
const otherUserId = request.otherUserId;
let mode = MODE_VERIFY_OTHER_USER;
if (myUserId === otherUserId) {
// Mode changes depending on whether or not we trust the master cross signing key
const myTrust = client.checkUserTrust(myUserId);
if (myTrust.isCrossSigningVerified()) {
mode = MODE_VERIFY_SELF_TRUSTED;
} else {
mode = MODE_VERIFY_SELF_UNTRUSTED;
}
}
return mode;
}
static _generateQrData(request, client, mode,
encodedSharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey,
) {
const myUserId = client.getUserId();
const transactionId = request.channel.transactionId;
const qrData = {
prefix: BINARY_PREFIX,
version: CODE_VERSION,
mode,
transactionId,
firstKeyB64: '', // worked out shortly
secondKeyB64: '', // worked out shortly
secretB64: encodedSharedSecret,
};
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
if (mode === MODE_VERIFY_OTHER_USER) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other user's master cross signing key
qrData.secondKeyB64 = otherUserMasterKey;
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
qrData.secondKeyB64 = otherDeviceKey;
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
// First key is our device's key
qrData.firstKeyB64 = client.getDeviceEd25519Key();
// Second key is what we think our master cross signing key is
qrData.secondKeyB64 = myMasterKey;
}
return qrData;
}
static _generateBuffer(qrData) {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b) => {
const tmpBuf = Buffer.from([b]);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendInt = (i) => {
const tmpBuf = Buffer.alloc(2);
tmpBuf.writeInt16BE(i, 0);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendStr = (s, enc, withLengthPrefix = true) => {
const tmpBuf = Buffer.from(s, enc);
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendEncBase64 = (b64) => {
const b = decodeBase64(b64);
const tmpBuf = Buffer.from(b);
buf = Buffer.concat([buf, tmpBuf]);
};
// Actually build the buffer for the QR code
appendStr(qrData.prefix, "ascii", false);
appendByte(qrData.version);
appendByte(qrData.mode);
appendStr(qrData.transactionId, "utf-8");
appendEncBase64(qrData.firstKeyB64);
appendEncBase64(qrData.secondKeyB64);
appendEncBase64(qrData.secretB64);
return buf;
}
}
+71 -32
View File
@@ -28,6 +28,7 @@ import {
newUnknownMethodError,
newUserCancelledError,
} from './Error';
import {logger} from '../../logger';
const START_TYPE = "m.key.verification.start";
@@ -165,11 +166,42 @@ const macMethods = {
"hmac-sha256": "calculate_mac_long_kdf",
};
function calculateMAC(olmSAS, method) {
return function(...args) {
const macFunction = olmSAS[macMethods[method]];
const mac = macFunction.apply(olmSAS, args);
logger.log("SAS calculateMAC:", method, args, mac);
return mac;
};
}
const calculateKeyAgreement = {
"curve25519-hkdf-sha256": function(sas, olmSAS, bytes) {
const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|`
+ `${sas.ourSASPubKey}|`;
const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS|"
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
+ sas._channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes);
},
"curve25519": function(sas, olmSAS, bytes) {
const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`;
const theirInfo = `${sas.userId}${sas.deviceId}`;
const sasInfo =
"MATRIX_KEY_VERIFICATION_SAS"
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
+ sas._channel.transactionId;
return olmSAS.generate_bytes(sasInfo, bytes);
},
};
/* lists of algorithms/methods that are supported. The key agreement, hashes,
* and MAC lists should be sorted in order of preference (most preferred
* first).
*/
const KEY_AGREEMENT_LIST = ["curve25519"];
const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"];
const HASHES_LIST = ["sha256"];
const MAC_LIST = ["hkdf-hmac-sha256", "hmac-sha256"];
const SAS_LIST = Object.keys(sasGenerators);
@@ -281,12 +313,14 @@ export class SAS extends Base {
if (typeof content.commitment !== "string") {
throw newInvalidMessageError();
}
const keyAgreement = content.key_agreement_protocol;
const macMethod = content.message_authentication_code;
const hashCommitment = content.commitment;
const olmSAS = new global.Olm.SAS();
try {
this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(),
this.ourSASPubKey = olmSAS.get_pubkey();
await this._send("m.key.verification.key", {
key: this.ourSASPubKey,
});
@@ -298,23 +332,25 @@ export class SAS extends Base {
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
throw newMismatchedCommitmentError();
}
this.theirSASPubKey = content.key;
olmSAS.set_their_key(content.key);
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this._channel.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
};
this.emit("show_sas", this.sasEvent);
});
@@ -366,7 +402,7 @@ export class SAS extends Base {
const olmSAS = new global.Olm.SAS();
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
this._send("m.key.verification.accept", {
await this._send("m.key.verification.accept", {
key_agreement_protocol: keyAgreement,
hash: hashMethod,
message_authentication_code: macMethod,
@@ -379,26 +415,29 @@ export class SAS extends Base {
let e = await this._waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
this.theirSASPubKey = content.key;
olmSAS.set_their_key(content.key);
this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(),
this.ourSASPubKey = olmSAS.get_pubkey();
await this._send("m.key.verification.key", {
key: this.ourSASPubKey,
});
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this._channel.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
resolve();
} catch(err) {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
};
this.emit("show_sas", this.sasEvent);
});
@@ -429,7 +468,7 @@ export class SAS extends Base {
+ this._channel.transactionId;
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
mac[deviceKeyId] = olmSAS[macMethods[method]](
mac[deviceKeyId] = calculateMAC(olmSAS, method)(
this._baseApis.getDeviceEd25519Key(),
baseInfo + deviceKeyId,
);
@@ -438,18 +477,18 @@ export class SAS extends Base {
const crossSigningId = this._baseApis.getCrossSigningId();
if (crossSigningId) {
const crossSigningKeyId = `ed25519:${crossSigningId}`;
mac[crossSigningKeyId] = olmSAS[macMethods[method]](
mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(
crossSigningId,
baseInfo + crossSigningKeyId,
);
keyList.push(crossSigningKeyId);
}
const keys = olmSAS[macMethods[method]](
const keys = calculateMAC(olmSAS, method)(
keyList.sort().join(","),
baseInfo + "KEY_IDS",
);
this._send("m.key.verification.mac", { mac, keys });
return this._send("m.key.verification.mac", { mac, keys });
}
async _checkMAC(olmSAS, content, method) {
@@ -458,7 +497,7 @@ export class SAS extends Base {
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this._channel.transactionId;
if (content.keys !== olmSAS[macMethods[method]](
if (content.keys !== calculateMAC(olmSAS, method)(
Object.keys(content.mac).sort().join(","),
baseInfo + "KEY_IDS",
)) {
@@ -466,7 +505,7 @@ export class SAS extends Base {
}
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
if (keyInfo !== olmSAS[macMethods[method]](
if (keyInfo !== calculateMAC(olmSAS, method)(
device.keys[keyId],
baseInfo + keyId,
)) {
@@ -44,11 +44,6 @@ export class InRoomChannel {
this._requestEventId = null;
}
/** Whether this channel needs m.key.verification.done messages to be sent after a successful verification */
get needsDoneMessage() {
return true;
}
get receiveStartFromOtherDevices() {
return true;
}
@@ -183,6 +178,11 @@ export class InRoomChannel {
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request, isLiveEvent) {
// prevent processing the same event multiple times, as under
// some circumstances Room.timeline can get emitted twice for the same event
if (request.hasEventId(event.getId())) {
return;
}
const type = InRoomChannel.getEventType(event);
// do validations that need state (roomId, userId),
// ignore if invalid
@@ -305,7 +305,6 @@ export class InRoomRequests {
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);
}
@@ -351,4 +350,15 @@ export class InRoomRequests {
}
}
}
findRequestInProgress(roomId) {
const requestsByTxnId = this._requestsByRoomId.get(roomId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
if (request.pending) {
return request;
}
}
}
}
}
@@ -43,12 +43,22 @@ export class ToDeviceChannel {
this._deviceId = deviceId;
}
get deviceId() {
return this._deviceId;
isToDevices(devices) {
if (devices.length === this._devices.length) {
for (const device of devices) {
const d = this._devices.find(d => d.deviceId === device.deviceId);
if (!d) {
return false;
}
}
return true;
} else {
return false;
}
}
get needsDoneMessage() {
return true;
get deviceId() {
return this._deviceId;
}
static getEventType(event) {
@@ -335,4 +345,15 @@ export class ToDeviceRequests {
}
}
}
findRequestInProgress(userId, devices) {
const requestsByTxnId = this._requestsByUserId.get(userId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
if (request.pending && request.channel.isToDevices(devices)) {
return request;
}
}
}
}
}
@@ -23,7 +23,7 @@ import {
newUnexpectedMessageError,
newUnknownMethodError,
} from "../Error";
import * as olmlib from "../../olmlib";
import {QRCodeData, SCAN_QR_CODE_METHOD} from "../QRCode";
// the recommended amount of time before a verification request
// should be (automatically) cancelled without user interaction
@@ -70,7 +70,16 @@ export class VerificationRequest extends EventEmitter {
this._eventsByThem = new Map();
this._observeOnly = false;
this._timeoutTimer = null;
this._sharedSecret = null; // used for QR codes
this._accepting = false;
this._declining = false;
this._verifierHasFinished = false;
this._cancelled = false;
this._chosenMethod = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
this._qrCodeData = null;
}
/**
@@ -151,6 +160,11 @@ export class VerificationRequest extends EventEmitter {
return this._commonMethods;
}
/** the method picked in the .start event */
get chosenMethod() {
return this._chosenMethod;
}
/** The current remaining amount of ms before the request should be automatically cancelled */
get timeout() {
const requestEvent = this._getEventByEither(REQUEST_TYPE);
@@ -179,10 +193,65 @@ export class VerificationRequest extends EventEmitter {
return this._verifier;
}
get canAccept() {
return this.phase < PHASE_READY && !this._accepting && !this._declining;
}
get accepting() {
return this._accepting;
}
get declining() {
return this._declining;
}
/** whether this request has sent it's initial event and needs more events to complete */
get pending() {
return this._phase !== PHASE_DONE
&& this._phase !== PHASE_CANCELLED;
return !this.observeOnly &&
this._phase !== PHASE_DONE &&
this._phase !== PHASE_CANCELLED;
}
/** Only set after a .ready if the other party can scan a QR code */
get qrCodeData() {
return this._qrCodeData;
}
/** Checks whether the other party supports a given verification method.
* This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
* if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
* For methods that need to be supported by both ends, use the `methods` property.
* @param {string} method the method to check
* @param {boolean} force to check even if the phase is not ready or started yet, internal usage
* @return {bool} whether or not the other party said the supported the method */
otherPartySupportsMethod(method, force = false) {
if (!force && !this.ready && !this.started) {
return false;
}
const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE);
if (!theirMethodEvent) {
// if we started straight away with .start event,
// we are assuming that the other side will support the
// chosen method, so return true for that.
if (this.started && this.initiatedByMe) {
const myStartEvent = this._eventsByUs.get(START_TYPE);
const content = myStartEvent && myStartEvent.getContent();
const myStartMethod = content && content.method;
return method == myStartMethod;
}
return false;
}
const content = theirMethodEvent.getContent();
if (!content) {
return false;
}
const {methods} = content;
if (!Array.isArray(methods)) {
return false;
}
return methods.includes(method);
}
/** Whether this request was initiated by the syncing user.
@@ -234,6 +303,10 @@ export class VerificationRequest extends EventEmitter {
return this.channel.userId;
}
get isSelfVerification() {
return this._client.getUserId() === this.otherUserId;
}
/**
* The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED
@@ -263,14 +336,6 @@ export class VerificationRequest extends EventEmitter {
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
@@ -317,6 +382,7 @@ export class VerificationRequest extends EventEmitter {
if (!this._verifier) {
throw newUnknownMethodError();
}
this._chosenMethod = method;
}
}
return this._verifier;
@@ -330,7 +396,6 @@ export class VerificationRequest extends EventEmitter {
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
const methods = [...this._verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, {methods});
this._generateSharedSecret();
}
}
@@ -342,6 +407,8 @@ export class VerificationRequest extends EventEmitter {
*/
async cancel({reason = "User declined", code = "m.user"} = {}) {
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
this._declining = true;
this.emit("change");
if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason)());
} else {
@@ -358,17 +425,12 @@ export class VerificationRequest extends EventEmitter {
async accept() {
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
const methods = [...this._verificationMethods.keys()];
this._accepting = true;
this.emit("change");
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.
@@ -459,12 +521,12 @@ export class VerificationRequest extends EventEmitter {
}
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
if (ourDoneEvent && phase() === PHASE_STARTED) {
if (this._verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
transitions.push({phase: PHASE_DONE});
}
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
if (cancelEvent && phase() !== PHASE_DONE) {
if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
transitions.push({phase: PHASE_CANCELLED, event: cancelEvent});
return transitions;
}
@@ -503,10 +565,80 @@ export class VerificationRequest extends EventEmitter {
const {method} = event.getContent();
if (!this._verifier && !this.observeOnly) {
this._verifier = this._createVerifier(method, event);
if (!this._verifier) {
this.cancel({
code: "m.unknown_method",
reason: `Unknown method: ${method}`,
});
} else {
this._chosenMethod = method;
}
}
}
}
_applyPhaseTransitions() {
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);
}
return newTransitions;
}
_isWinningStartRace(newEvent) {
if (newEvent.getType() !== START_TYPE) {
return false;
}
const oldEvent = this._verifier.startEvent;
let oldRaceIdentifier;
if (this.isSelfVerification) {
// if the verifier does not have a startEvent,
// it is because it's still sending and we are on the initator side
// we know we are sending a .start event because we already
// have a verifier (checked in calling method)
if (oldEvent) {
const oldContent = oldEvent.getContent();
oldRaceIdentifier = oldContent && oldContent.from_device;
} else {
oldRaceIdentifier = this._client.getDeviceId();
}
} else {
if (oldEvent) {
oldRaceIdentifier = oldEvent.getSender();
} else {
oldRaceIdentifier = this._client.getUserId();
}
}
let newRaceIdentifier;
if (this.isSelfVerification) {
const newContent = newEvent.getContent();
newRaceIdentifier = newContent && newContent.from_device;
} else {
newRaceIdentifier = newEvent.getSender();
}
return newRaceIdentifier < oldRaceIdentifier;
}
hasEventId(eventId) {
for (const event of this._eventsByUs.values()) {
if (event.getId() === eventId) {
return true;
}
}
for (const event of this._eventsByThem.values()) {
if (event.getId() === eventId) {
return true;
}
}
return false;
}
/**
* 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.
@@ -519,7 +651,7 @@ export class VerificationRequest extends EventEmitter {
*/
async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
// if reached phase cancelled or done, ignore anything else that comes
if (!this.pending) {
if (this.done || this.cancelled) {
return;
}
const wasObserveOnly = this._observeOnly;
@@ -532,31 +664,32 @@ export class VerificationRequest extends EventEmitter {
}
}
// This assumes verification won't need to send an event with
// the same type for the same party twice.
// This is true for QR and SAS verification, and was
// added here to prevent verification getting cancelled
// when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
const isDuplicateEvent = isSentByUs ?
this._eventsByUs.has(type) :
this._eventsByThem.has(type);
if (isDuplicateEvent) {
return;
}
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);
}
// this will create if needed the verifier so needs to happen before calling it
const newTransitions = this._applyPhaseTransitions();
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;
const newEventWinsRace = this._isWinningStartRace(event);
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
this._verifier.switchStartEvent(event);
} else if (!isRemoteEcho) {
if (type === CANCEL_TYPE || (this._verifier.events
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
this._verifier.handleEvent(event);
}
@@ -564,6 +697,20 @@ export class VerificationRequest extends EventEmitter {
}
if (newTransitions.length) {
// create QRCodeData if the other side can scan
// important this happens before emitting a phase change,
// so listeners can rely on it being there already
// We only do this for live events because it is important that
// we sign the keys that were in the QR code, and not the keys
// we happen to have at some later point in time.
if (isLiveEvent && newTransitions.some(t => t.phase === PHASE_READY)) {
const shouldGenerateQrCode =
this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
if (shouldGenerateQrCode) {
this._qrCodeData = await QRCodeData.create(this, this._client);
}
}
const lastTransition = newTransitions[newTransitions.length - 1];
const {phase} = lastTransition;
@@ -576,9 +723,11 @@ export class VerificationRequest extends EventEmitter {
} 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} ` +
`${type} event with id:${event.getId()}, ` +
`content:${JSON.stringify(event.getContent())} ` +
`deviceId:${this.channel.deviceId}, ` +
`sender:${event.getSender()}, isSentByUs:${isSentByUs}, ` +
`isLiveEvent:${isLiveEvent}, isRemoteEcho:${isRemoteEcho}, ` +
`phase:${oldPhase}=>${this.phase}, ` +
`observeOnly:${wasObserveOnly}=>${this._observeOnly}`);
}
@@ -622,7 +771,12 @@ export class VerificationRequest extends EventEmitter {
const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED;
if (isUnexpectedRequest || isUnexpectedReady) {
// only if phase has passed from PHASE_UNSENT should we cancel, because events
// are allowed to come in in any order (at least with InRoomChannel). So we only know
// we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED
// before that, we could be looking at somebody elses verification request and we just
// happen to be in the room
if (this.phase !== PHASE_UNSENT && (isUnexpectedRequest || isUnexpectedReady)) {
logger.warn(`Cancelling, unexpected ${type} verification ` +
`event from ${event.getSender()}`);
const reason = `Unexpected ${type} event in phase ${this.phase}`;
@@ -704,4 +858,27 @@ export class VerificationRequest extends EventEmitter {
}
return true;
}
onVerifierCancelled() {
this._cancelled = true;
// move to cancelled phase
const newTransitions = this._applyPhaseTransitions();
if (newTransitions.length) {
this._setPhase(newTransitions[newTransitions.length - 1].phase);
}
}
onVerifierFinished() {
this.channel.send("m.key.verification.done", {});
this._verifierHasFinished = true;
// move to .done phase
const newTransitions = this._applyPhaseTransitions();
if (newTransitions.length) {
this._setPhase(newTransitions[newTransitions.length - 1].phase);
}
}
getEventFromOtherParty(type) {
return this._eventsByThem.get(type);
}
}
+7
View File
@@ -44,3 +44,10 @@ InvalidCryptoStoreError.prototype = Object.create(Error.prototype, {
},
});
Reflect.setPrototypeOf(InvalidCryptoStoreError, Error);
export class KeySignatureUploadError extends Error {
constructor(message, value) {
super(message);
this.value = value;
}
}
+3 -2
View File
@@ -108,8 +108,9 @@ FilterComponent.prototype._checkFields =
}
const allowed_values = self[name];
if (allowed_values) {
if (!allowed_values.map(match_func)) {
if (allowed_values && allowed_values.length > 0) {
const anyMatch = allowed_values.some(match_func);
if (!anyMatch) {
return false;
}
}
+5 -7
View File
@@ -56,13 +56,6 @@ Filter.LAZY_LOADING_MESSAGES_FILTER = {
lazy_load_members: true,
};
Filter.LAZY_LOADING_SYNC_FILTER = {
room: {
state: Filter.LAZY_LOADING_MESSAGES_FILTER,
},
};
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
@@ -96,6 +89,7 @@ Filter.prototype.setDefinition = function(definition) {
// "state": {
// "types": ["m.room.*"],
// "not_rooms": ["!726s6s6q:example.com"],
// "lazy_load_members": true,
// },
// "timeline": {
// "limit": 10,
@@ -177,6 +171,10 @@ Filter.prototype.setTimelineLimit = function(limit) {
setProp(this.definition, "room.timeline.limit", limit);
};
Filter.prototype.setLazyLoadMembers = function(enabled) {
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
};
/**
* Control whether left rooms should be included in responses.
* @param {boolean} includeLeave True to make rooms the user has left appear
+87 -8
View File
@@ -47,6 +47,7 @@ export const PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for v1 of the the identity API
* @deprecated Use v2.
*/
export const PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
@@ -275,6 +276,9 @@ MatrixHttpApi.prototype = {
callbacks.clearTimeout(xhr.timeout_timer);
var resp;
try {
if (xhr.status === 0) {
throw new AbortError();
}
if (!xhr.responseText) {
throw new Error('No response body.');
}
@@ -788,6 +792,17 @@ const requestCallback = function(
userDefinedCallback = userDefinedCallback || function() {};
return function(err, response, body) {
if (err) {
// the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request.
// See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48
const aborted = err.name === "AbortError" || err === "aborted";
if (!aborted && !(err instanceof MatrixError)) {
// browser-request just throws normal Error objects,
// not `TypeError`s like fetch does. So just assume any
// error is due to the connection.
err = new ConnectionError("request failed", err);
}
}
if (!err) {
try {
if (response.statusCode >= 400) {
@@ -891,12 +906,76 @@ function getResponseContentType(response) {
* @prop {Object} data The raw Matrix error JSON used to construct this object.
* @prop {integer} httpStatus The numeric HTTP status code given
*/
export function MatrixError(errorJson) {
errorJson = errorJson || {};
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
this.data = errorJson;
export class MatrixError extends Error {
constructor(errorJson) {
errorJson = errorJson || {};
super(`MatrixError: ${errorJson.errcode}`);
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
this.data = errorJson;
}
}
/**
* Construct a ConnectionError. This is a JavaScript Error indicating
* that a request failed because of some error with the connection, either
* CORS was not correctly configured on the server, the server didn't response,
* the request timed out, or the internet connection on the client side went down.
* @constructor
*/
export class ConnectionError extends Error {
constructor(message, cause = undefined) {
super(message + (cause ? `: ${cause.message}` : ""));
this._cause = cause;
}
get name() {
return "ConnectionError";
}
get cause() {
return this._cause;
}
}
export class AbortError extends Error {
constructor() {
super("Operation aborted");
}
get name() {
return "AbortError";
}
}
/**
* Retries a network operation run in a callback.
* @param {number} maxAttempts maximum attempts to try
* @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again.
* @return {any} the result of the network operation
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
*/
export async function retryNetworkOperation(maxAttempts, callback) {
let attempts = 0;
let lastConnectionError = null;
while (attempts < maxAttempts) {
try {
if (attempts > 0) {
const timeout = 1000 * Math.pow(2, attempts);
console.log(`network operation failed ${attempts} times,` +
` retrying in ${timeout}ms...`);
await new Promise(r => setTimeout(r, timeout));
}
return await callback();
} catch (err) {
if (err instanceof ConnectionError) {
attempts += 1;
lastConnectionError = err;
} else {
throw err;
}
}
}
throw lastConnectionError;
}
MatrixError.prototype = Object.create(Error.prototype);
MatrixError.prototype.constructor = MatrixError;
+7
View File
@@ -21,5 +21,12 @@ import request from "request";
matrixcs.request(request);
utils.runPolyfills();
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
console.log('nodejs was compiled without crypto support');
}
export * from "./matrix";
export default matrixcs;
+42 -13
View File
@@ -143,11 +143,19 @@ InteractiveAuth.prototype = {
this._resolveFunc = resolve;
this._rejectFunc = reject;
// if we have no flows, try a request (we'll have
// just a session ID in _data if resuming)
if (!this._data.flows) {
const hasFlows = this._data && this._data.flows;
// if we have no flows, try a request to acquire the flows
if (!hasFlows) {
if (this._busyChangedCallback) this._busyChangedCallback(true);
this._doRequest(this._data).finally(() => {
// use the existing sessionid, if one is present.
let auth = null;
if (this._data.session) {
auth = {
session: this._data.session,
};
}
this._doRequest(auth).finally(() => {
if (this._busyChangedCallback) this._busyChangedCallback(false);
});
} else {
@@ -163,6 +171,8 @@ InteractiveAuth.prototype = {
*/
poll: async function() {
if (!this._data.session) return;
// likewise don't poll if there is no auth session in progress
if (!this._resolveFunc) return;
// if we currently have a request in flight, there's no point making
// another just to check what the status is
if (this._submitPromise) return;
@@ -184,7 +194,11 @@ InteractiveAuth.prototype = {
}
authDict = {
type: EMAIL_STAGE_TYPE,
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds,
threepidCreds: creds,
};
}
}
@@ -262,11 +276,16 @@ InteractiveAuth.prototype = {
}
}
// use the sessionid from the last request.
const auth = {
session: this._data.session,
};
utils.extend(auth, authData);
// use the sessionid from the last request, if one is present.
let auth;
if (this._data.session) {
auth = {
session: this._data.session,
};
utils.extend(auth, authData);
} else {
auth = authData;
}
try {
// NB. the 'background' flag is deprecated by the busyChanged
@@ -318,10 +337,12 @@ InteractiveAuth.prototype = {
try {
const result = await this._requestCallback(auth, background);
this._resolveFunc(result);
this._resolveFunc = null;
this._rejectFunc = null;
} catch (error) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null;
const haveFlows = Boolean(this._data.flows) || Boolean(errorFlows);
const haveFlows = this._data.flows || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
// doesn't look like an interactive-auth failure.
if (!background) {
@@ -347,7 +368,13 @@ InteractiveAuth.prototype = {
error.data.session = this._data.session;
}
this._data = error.data;
this._startNextAuthStage();
try {
this._startNextAuthStage();
} catch (e) {
this._rejectFunc(e);
this._resolveFunc = null;
this._rejectFunc = null;
}
if (
!this._emailSid &&
@@ -379,8 +406,10 @@ InteractiveAuth.prototype = {
// (or not being registered, depending on what we're trying
// to do) or it could be a network failure. Either way, pass
// the failure up as the user can't complete auth if we can't
// send the email, foe whatever reason.
// send the email, for whatever reason.
this._rejectFunc(e);
this._resolveFunc = null;
this._rejectFunc = null;
} finally {
this._requestingEmailToken = false;
}
@@ -408,7 +437,7 @@ InteractiveAuth.prototype = {
return;
}
if (this._data.errcode || this._data.error) {
if (this._data && this._data.errcode || this._data.error) {
this._stateUpdatedCallback(nextStage, {
errcode: this._data.errcode || "",
error: this._data.error || "",
+19
View File
@@ -29,6 +29,25 @@ import log from "loglevel";
// Part of #332 is introducing a logging library in the first place.
const DEFAULT_NAMESPACE = "matrix";
// because rageshakes in react-sdk hijack the console log, also at module load time,
// initializing the logger here races with the initialization of rageshakes.
// to avoid the issue, we override the methodFactory of loglevel that binds to the
// console methods at initialization time by a factory that looks up the console methods
// when logging so we always get the current value of console methods.
log.methodFactory = function(methodName, logLevel, loggerName) {
return function(...args) {
const supportedByConsole = methodName === "error" ||
methodName === "warn" ||
methodName === "trace" ||
methodName === "info";
if (supportedByConsole) {
return console[methodName](...args);
} else {
return console.log(...args);
}
};
};
/**
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
* Can be tailored down to specific use cases if needed.
+22 -2
View File
@@ -16,8 +16,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import type Request from "request";
import {MemoryCryptoStore} from "./crypto/store/memory-crypto-store";
import {LocalStorageCryptoStore} from "./crypto/store/localStorage-crypto-store";
import {IndexedDBCryptoStore} from "./crypto/store/indexeddb-crypto-store";
import {MemoryStore} from "./store/memory";
import {StubStore} from "./store/stub";
import {LocalIndexedDBStoreBackend} from "./store/indexeddb-local-backend";
import {RemoteIndexedDBStoreBackend} from "./store/indexeddb-remote-backend";
import {MatrixScheduler} from "./scheduler";
import {MatrixClient} from "./client";
@@ -89,6 +96,10 @@ export function wrapRequest(wrapper) {
};
}
type Store =
StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore;
let cryptoStoreFactory = () => new MemoryCryptoStore;
@@ -102,6 +113,15 @@ export function setCryptoStoreFactory(fac) {
cryptoStoreFactory = fac;
}
interface ICreateClientOpts {
baseUrl: string;
store?: Store;
cryptoStore?: CryptoStore;
scheduler?: MatrixScheduler;
request?: Request;
}
/**
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
@@ -125,10 +145,10 @@ export function setCryptoStoreFactory(fac) {
* @see {@link module:client.MatrixClient} for the full list of options for
* <code>opts</code>.
*/
export function createClient(opts) {
export function createClient(opts: ICreateClientOpts | string) {
if (typeof opts === "string") {
opts = {
"baseUrl": opts,
"baseUrl": opts as string,
};
}
opts.request = opts.request || requestInstance;
+11 -5
View File
@@ -390,11 +390,12 @@ utils.extend(MatrixEvent.prototype, {
* @internal
*
* @param {module:crypto} crypto crypto module
* @param {bool} isRetry True if this is a retry (enables more logging)
*
* @returns {Promise} promise which resolves (to undefined) when the decryption
* attempt is completed.
*/
attemptDecryption: async function(crypto) {
attemptDecryption: async function(crypto, isRetry) {
// start with a couple of sanity checks.
if (!this.isEncrypted()) {
throw new Error("Attempt to decrypt event which isn't encrypted");
@@ -406,7 +407,7 @@ utils.extend(MatrixEvent.prototype, {
) {
// we may want to just ignore this? let's start with rejecting it.
throw new Error(
"Attempt to decrypt event which has already been encrypted",
"Attempt to decrypt event which has already been decrypted",
);
}
@@ -424,7 +425,7 @@ utils.extend(MatrixEvent.prototype, {
return this._decryptionPromise;
}
this._decryptionPromise = this._decryptionLoop(crypto);
this._decryptionPromise = this._decryptionLoop(crypto, isRetry);
return this._decryptionPromise;
},
@@ -469,7 +470,7 @@ utils.extend(MatrixEvent.prototype, {
return recipients;
},
_decryptionLoop: async function(crypto) {
_decryptionLoop: async function(crypto, isRetry) {
// make sure that this method never runs completely synchronously.
// (doing so would mean that we would clear _decryptionPromise *before*
// it is set in attemptDecryption - and hence end up with a stuck
@@ -486,13 +487,18 @@ utils.extend(MatrixEvent.prototype, {
res = this._badEncryptedMessage("Encryption not enabled");
} else {
res = await crypto.decryptEvent(this);
if (isRetry) {
logger.info(`Decrypted event on retry (id=${this.getId()})`);
}
}
} catch (e) {
if (e.name !== "DecryptionError") {
// not a decryption error: log the whole exception as an error
// (and don't bother with a retry)
const re = isRetry ? 're' : '';
logger.error(
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
`Error ${re}decrypting event ` +
`(id=${this.getId()}): ${e.stack || e}`,
);
this._decryptionPromise = null;
this._retryDecryption = false;
+25 -5
View File
@@ -209,7 +209,7 @@ utils.inherits(Room, EventEmitter);
Room.prototype.getVersion = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) {
logger.warn("Room " + this.room_id + " does not have an m.room.create event");
logger.warn("Room " + this.roomId + " does not have an m.room.create event");
return '1';
}
const ver = createEvent.getContent()['room_version'];
@@ -675,7 +675,7 @@ Room.prototype.hasUnverifiedDevices = async function() {
}
const e2eMembers = await this.getEncryptionTargetMembers();
for (const member of e2eMembers) {
const devices = await this._client.getStoredDevicesForUser(member.userId);
const devices = this._client.getStoredDevicesForUser(member.userId);
if (devices.some((device) => device.isUnverified())) {
return true;
}
@@ -861,11 +861,23 @@ Room.prototype.getAliases = function() {
Room.prototype.getCanonicalAlias = function() {
const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
if (canonicalAlias) {
return canonicalAlias.getContent().alias;
return canonicalAlias.getContent().alias || null;
}
return null;
};
/**
* Get this room's alternative aliases
* @return {array} The room's alternative aliases, or an empty array
*/
Room.prototype.getAltAliases = function() {
const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
if (canonicalAlias) {
return canonicalAlias.getContent().alt_aliases || [];
}
return [];
};
/**
* Add events to a timeline
*
@@ -1266,6 +1278,11 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
const newEventId = remoteEvent.getId();
const oldStatus = localEvent.status;
logger.debug(
`Got remote echo for event ${oldEventId} -> ${newEventId} ` +
`old status ${oldStatus}`,
);
// no longer pending
delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id];
@@ -1335,7 +1352,10 @@ ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*/
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
logger.log(
`setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` +
`event ID ${event.getId()} -> ${newEventId}`,
);
// if the message was sent, we expect an event id
if (newStatus == EventStatus.SENT && !newEventId) {
@@ -1821,7 +1841,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
let alias = room.getCanonicalAlias();
if (!alias) {
const aliases = room.getAliases();
const aliases = room.getAltAliases();
if (aliases.length) {
alias = aliases[0];
+21 -43
View File
@@ -23,9 +23,8 @@ import {escapeRegExp, globToRegexp, isNullOrUndefined} from "./utils";
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
// The default override rules to apply when calculating actions for an event. These
// defaults apply under no other circumstances to avoid confusing the client with server
// state. We do this for two reasons:
// The default override rules to apply to the push rules that arrive from the server.
// We do this for two reasons:
// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
// more details.
@@ -364,33 +363,6 @@ export function PushProcessor(client) {
return actionObj;
};
const applyRuleDefaults = function(clientRuleset) {
// Deep clone the object before we mutate it
const ruleset = JSON.parse(JSON.stringify(clientRuleset));
if (!clientRuleset['global']) {
clientRuleset['global'] = {};
}
if (!clientRuleset['global']['override']) {
clientRuleset['global']['override'] = [];
}
// Apply default overrides
const globalOverrides = clientRuleset['global']['override'];
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
if (!existingRule) {
const ruleId = override.rule_id;
console.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return ruleset;
};
this.ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
@@ -410,8 +382,7 @@ export function PushProcessor(client) {
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
const rules = applyRuleDefaults(client.pushRules);
return pushActionsForEventAndRulesets(ev, rules);
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
/**
@@ -476,18 +447,25 @@ PushProcessor.rewriteDefaultRules = function(incomingRules) {
if (!newRules.global) newRules.global = {};
if (!newRules.global.override) newRules.global.override = [];
// Fix default override rules
newRules.global.override = newRules.global.override.map(r => {
const defaultRule = DEFAULT_OVERRIDE_RULES.find(d => d.rule_id === r.rule_id);
if (!defaultRule) return r;
// Merge the client-level defaults with the ones from the server
const globalOverrides = newRules.global.override;
for (const override of DEFAULT_OVERRIDE_RULES) {
const existingRule = globalOverrides
.find((r) => r.rule_id === override.rule_id);
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
r.default = defaultRule.default;
r.conditions = defaultRule.conditions;
r.actions = defaultRule.actions;
return r;
});
if (existingRule) {
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
existingRule.default = override.default;
existingRule.conditions = override.conditions;
existingRule.actions = override.actions;
} else {
// Add the rule
const ruleId = override.rule_id;
console.warn(`Adding default global override for ${ruleId}`);
globalOverrides.push(override);
}
}
return newRules;
};
+25 -2
View File
@@ -25,6 +25,15 @@ limitations under the License.
import {User} from "../models/user";
import * as utils from "../utils";
function isValidFilterId(filterId) {
const isValidStr = typeof filterId === "string" &&
!!filterId &&
filterId !== "undefined" && // exclude these as we've serialized undefined in localStorage before
filterId !== "null";
return isValidStr || typeof filterId === "number";
}
/**
* Construct a new in-memory data store for the Matrix Client.
* @constructor
@@ -273,8 +282,17 @@ MemoryStore.prototype = {
if (!this.localStorage) {
return null;
}
const key = "mxjssdk_memory_filter_" + filterName;
// XXX Storage.getItem doesn't throw ...
// or are we using something different
// than window.localStorage in some cases
// that does throw?
// that would be very naughty
try {
return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
const value = this.localStorage.getItem(key);
if (isValidFilterId(value)) {
return value;
}
} catch (e) {}
return null;
},
@@ -288,8 +306,13 @@ MemoryStore.prototype = {
if (!this.localStorage) {
return;
}
const key = "mxjssdk_memory_filter_" + filterName;
try {
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
if (isValidFilterId(filterId)) {
this.localStorage.setItem(key, filterId);
} else {
this.localStorage.removeItem(key);
}
} catch (e) {}
},
+11 -14
View File
@@ -511,6 +511,12 @@ SyncApi.prototype.sync = function() {
checkLazyLoadStatus(); // advance to the next stage
}
function buildDefaultFilter() {
const filter = new Filter(client.credentials.userId);
filter.setTimelineLimit(self.opts.initialSyncLimit);
return filter;
}
const checkLazyLoadStatus = async () => {
debuglog("Checking lazy load status...");
if (this.opts.lazyLoadMembers && client.isGuest()) {
@@ -520,19 +526,11 @@ SyncApi.prototype.sync = function() {
debuglog("Checking server lazy load support...");
const supported = await client.doesServerSupportLazyLoading();
if (supported) {
try {
debuglog("Creating and storing lazy load sync filter...");
this.opts.filter = await client.createFilter(
Filter.LAZY_LOADING_SYNC_FILTER,
);
debuglog("Created and stored lazy load sync filter");
} catch (err) {
logger.error(
"Creating and storing lazy load sync filter failed",
err,
);
throw err;
debuglog("Enabling lazy load on sync filter...");
if (!this.opts.filter) {
this.opts.filter = buildDefaultFilter();
}
this.opts.filter.setLazyLoadMembers(true);
} else {
debuglog("LL: lazy loading requested but not supported " +
"by server, so disabling");
@@ -575,8 +573,7 @@ SyncApi.prototype.sync = function() {
if (self.opts.filter) {
filter = self.opts.filter;
} else {
filter = new Filter(client.credentials.userId);
filter.setTimelineLimit(self.opts.initialSyncLimit);
filter = buildDefaultFilter();
}
let filterId;
+17 -1
View File
@@ -21,6 +21,7 @@ limitations under the License.
*/
import unhomoglyph from 'unhomoglyph';
import {ConnectionError} from "./http-api";
/**
* Encode a dictionary of query parameters.
@@ -265,7 +266,7 @@ export function checkObjectHasNoAdditionalKeys(obj: object, allowedKeys: string[
* @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: object): object {
export function deepCopy<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
@@ -372,6 +373,7 @@ export function extend() {
const target = arguments[0] || {};
for (let i = 1; i < arguments.length; i++) {
const source = arguments[i];
if (!source) continue;
for (const propName in source) { // eslint-disable-line guard-for-in
target[propName] = source[propName];
}
@@ -734,3 +736,17 @@ export async function promiseMapSeries<T>(
export function promiseTry<T>(fn: () => T): Promise<T> {
return new Promise((resolve) => resolve(fn()));
}
// We need to be able to access the Node.js crypto library from within the
// Matrix SDK without needing to `require("crypto")`, which will fail in
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
// it, we can call `getCrypto`.
let crypto: Object;
export function setCrypto(c: Object) {
crypto = c;
}
export function getCrypto(): Object {
return crypto;
}
+14 -34
View File
@@ -165,29 +165,26 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeScreenSharingCall =
function(remoteVideoElement, localVideoElement) {
async function(remoteVideoElement, localVideoElement) {
debuglog("placeScreenSharingCall");
checkForErrorListener(this);
const screenConstraints = _getScreenSharingConstraints(this);
if (!screenConstraints) {
return;
}
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
const self = this;
this.webRtc.getUserMedia(screenConstraints, function(stream) {
self.screenSharingStream = stream;
try {
self.screenSharingStream = await this.webRtc.getDisplayMedia({'audio': false});
debuglog("Got screen stream, requesting audio stream...");
const audioConstraints = _getUserMediaVideoContraints('voice');
_placeCallWithConstraints(self, audioConstraints);
}, function(err) {
} catch(err) {
self.emit("error",
callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Failed to get screen-sharing stream: " + err,
),
);
});
}
this.type = 'video';
_tryPlayRemoteStream(this);
};
@@ -1231,31 +1228,6 @@ const _createPeerConnection = function(self) {
return pc;
};
const _getScreenSharingConstraints = function(call) {
const screen = global.screen;
if (!screen) {
call.emit("error", callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Couldn't determine screen sharing constaints.",
));
return;
}
return {
video: {
mediaSource: 'screen',
mandatory: {
chromeMediaSource: "screen",
chromeMediaSourceId: "" + Date.now(),
maxWidth: screen.width,
maxHeight: screen.height,
minFrameRate: 1,
maxFrameRate: 10,
},
},
};
};
const _getUserMediaVideoContraints = function(callType) {
const isWebkit = !!global.window.navigator.webkitGetUserMedia;
@@ -1371,6 +1343,14 @@ export function createNewMatrixCall(client, roomId, options) {
};
}
const getDisplayMedia = (
w.navigator.mediaDevices && w.navigator.mediaDevices.getDisplayMedia ||
w.navigator.getDisplayMedia
);
if (getDisplayMedia) {
webRtc.getDisplayMedia = getDisplayMedia.bind(w.navigator.mediaDevices);
}
// Firefox throws on so little as accessing the RTCPeerConnection when operating in
// a secure mode. There's some information at https://bugzilla.mozilla.org/show_bug.cgi?id=1542616
// though the concern is that the browser throwing a SecurityError will brick the
+1
View File
@@ -2,6 +2,7 @@
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2016",
+1262 -1339
View File
File diff suppressed because it is too large Load Diff