Compare commits

...

185 Commits

Author SHA1 Message Date
RiotRobot 666cbbce08 v8.3.0 2020-09-14 13:26:27 +01:00
RiotRobot 69c575a4be Prepare changelog for v8.3.0 2020-09-14 13:26:27 +01:00
RiotRobot de339d3098 v8.3.0-rc.1 2020-09-09 15:06:48 +01:00
RiotRobot 0f83234be9 Prepare changelog for v8.3.0-rc.1 2020-09-09 15:06:47 +01:00
J. Ryan Stinnett eec7c4c61b Merge pull request #1452 from dalcde/develop
Add missing options in ICreateClientOpts
2020-09-03 15:51:25 +01:00
J. Ryan Stinnett 25b4b049b7 Merge pull request #1457 from matrix-org/jryans/is-x-ready-bools
Ensure ready functions return boolean values
2020-09-02 17:30:16 +01:00
J. Ryan Stinnett 2191eb3f41 Ensure ready functions return boolean values
Fixes https://github.com/vector-im/element-web/issues/15087
2020-09-02 17:18:47 +01:00
J. Ryan Stinnett bebdbf7e05 Merge pull request #1456 from matrix-org/jryans/cross-signing-verif-errors
Handle missing cross-signing keys gracefully
2020-09-02 15:07:27 +01:00
J. Ryan Stinnett 646c091966 Skip self-signing if device already signed
When adding a device to your account, both devices attempt to sign each other
using the self-signing key, but in reality only the new device needs to be
signed. This avoids a case where web would fail verification while attempting to
sign the old device because of missing private keys (since they aren't in 4S and
it hasn't requested them from the other device yet), even though signing the old
device is redundant anyway.

Part of https://github.com/vector-im/element-web/issues/14970
2020-09-02 12:54:26 +01:00
J. Ryan Stinnett 952729cb1b Abort early if cross-signing key not found
This helps us print a better error message when the key does not exist.

Part of https://github.com/vector-im/element-web/issues/14970
2020-09-02 12:32:35 +01:00
RiotRobot c6992e2056 Merge branch 'master' into develop 2020-09-01 17:00:47 +01:00
RiotRobot 77ed79e9a9 v8.2.0 2020-09-01 16:57:34 +01:00
RiotRobot c4f4add0ec Prepare changelog for v8.2.0 2020-09-01 16:57:33 +01:00
Michael Telatynski 7eeb60c838 Merge pull request #1451 from matrix-org/t3chguy/lint-ts
Fix eslint ts override tsx matching
2020-09-01 09:10:22 +01:00
Dexter Chua d42cdbbc5b Add missing options in ICreateClientOpts
This makes the examples in the README actually compile.

Signed-off-by: Dexter Chua <dec41@srcf.net>
2020-08-29 14:40:40 +08:00
Michael Telatynski 66237e1ea6 Fix eslint ts override tsx matching 2020-08-29 01:10:08 +01:00
J. Ryan Stinnett a51c0450c3 Merge pull request #1450 from matrix-org/jryans/defer-cross-signing-setup
Untangle cross-signing and secret storage
2020-08-28 12:40:28 +01:00
J. Ryan Stinnett 7a2416bb6d Add callback for app to cache secret storage key
This is useful when e.g. resetting both secret storage and cross-signing
together, as it avoids prompting for the secret storage key that was just
created.
2020-08-28 12:07:35 +01:00
J. Ryan Stinnett a1528e9e33 Change log messages for uniqueness 2020-08-28 11:38:19 +01:00
J. Ryan Stinnett 71cc4d535e Clean up comment 2020-08-28 11:36:02 +01:00
J. Ryan Stinnett c06723df3d Always store cross-signing keys when new 4S key ID 2020-08-28 11:35:25 +01:00
J. Ryan Stinnett 06b285c013 Add comment about trap with multiple devices 2020-08-28 11:11:51 +01:00
J. Ryan Stinnett 49c06ef0ca Check cross-signing reset branch first
If we're resetting keys, we have to check that branch first.
2020-08-28 11:00:10 +01:00
J. Ryan Stinnett 5f92357fec Tweak code style 2020-08-27 17:32:46 +01:00
J. Ryan Stinnett 3e0dd3d918 Copy docs to client.js as well 2020-08-27 14:09:12 +01:00
J. Ryan Stinnett f19d76b08d Untangle cross-signing and secret storage
This untangles cross-signing and secret storage setup into separate path that
can be invoked independently. There is no functional change with this patch, but
instead this just separates one giant monster API into two.

Part of https://github.com/vector-im/element-web/issues/13895
2020-08-27 13:32:54 +01:00
RiotRobot 8b6b16067b v8.2.0-rc.1 2020-08-26 11:45:48 +01:00
RiotRobot a2da0de17d Prepare changelog for v8.2.0-rc.1 2020-08-26 11:45:48 +01:00
J. Ryan Stinnett 93ff3edb6b Merge pull request #1449 from matrix-org/jryans/strict-enc-check
Add state event check
2020-08-26 11:03:42 +01:00
J. Ryan Stinnett 7c67fd69dd Add state event check
State events are never encrypted, so we can ignore them here.
2020-08-26 10:55:03 +01:00
J. Ryan Stinnett 5ef5412a55 Move storage methods up to constructor 2020-08-25 19:27:26 +01:00
J. Ryan Stinnett e88a384aa7 Update and reformat client.js docs copy 2020-08-25 12:56:30 +01:00
J. Ryan Stinnett 9067feeafb Update comment on 4S signatures
As part of changing 4S to symmetric encryption, we no longer sign the 4S key
with the cross-signing MSK.
2020-08-25 12:56:25 +01:00
J. Ryan Stinnett ed978f69fb Merge pull request #1444 from matrix-org/jryans/secure-backup-required
Add method to check whether client .well-known has been fetched
2020-08-24 17:32:49 +01:00
J. Ryan Stinnett 743f2465ea Document WellKnown.client event 2020-08-24 15:56:14 +01:00
J. Ryan Stinnett 41fffa233a Switch to promise-based API 2020-08-24 15:56:14 +01:00
J. Ryan Stinnett e45377166b Merge pull request #1443 from matrix-org/jryans/cross-signing-auth-errors
Handle auth errors during cross-signing key upload
2020-08-24 11:52:40 +01:00
David Baker 24939bf0b0 Merge pull request #1448 from matrix-org/dbkr/dont_fail_if_audio_output_unavailable
Don't fail if the requested audio output isn't available
2020-08-21 16:19:20 +01:00
David Baker 3221be4855 Don't fail if the requested audio output isn't available
This thorws an exception if the requested device isn't available,
in which case we should catch it  & carry on with the default device.

Fixes https://github.com/vector-im/element-web/issues/15019
2020-08-21 16:02:08 +01:00
David Baker 3135f1ed24 Merge pull request #1447 from matrix-org/dbkr/fix_log_fail
Fix logging failures
2020-08-21 11:12:11 +01:00
David Baker 1b0834ffb0 Fix logging failures 2020-08-21 11:01:34 +01:00
David Baker d79d613cb7 Merge pull request #1446 from matrix-org/dbkr/log_user_media_constraints
Log the constraints we pass to getUserMedia
2020-08-19 18:30:39 +01:00
David Baker d8cc1f7b7a Log the constraints we pass to getUserMedia
To help debug voip calls
2020-08-19 18:13:44 +01:00
J. Ryan Stinnett d7c8856fdd Add method to check whether client .well-known has been fetched
This allows clearly detecting whether we have _ever_ fetched .well-known at all.
Without this, it's hard to be sure whether the value is `undefined` because the
fetch has not been attempted yet or because an error occurred.
2020-08-19 16:10:10 +01:00
J. Ryan Stinnett 9d80a332aa Upload cross-signing keys first to handle failure
This changes to uploading cross-signing keys first, since they require a valid
UI auth session, and so are more likely to fail than other API calls. With the
new ordering, if they do fail, then by failing first, we won't have made any
changes to the user's account, so everything rolls back correctly.
2020-08-19 11:55:09 +01:00
J. Ryan Stinnett e14f7b63c7 Handle auth errors during cross-signing key upload
In order to handle auth errors (such as incorrect passwords), we need to ensure
we only try to upload cross-signing keys from within the auth flow helper
function.

This rearranges things to store that function in the builder to use it when the
actual upload happens.
2020-08-19 11:44:41 +01:00
Travis Ralston 3bd2880923 Revert "Merge pull request #1440 from matrix-org/travis/spec-i18n"
This reverts commit 2401ad7159.
2020-08-18 13:07:31 -06:00
Travis Ralston 2401ad7159 Merge pull request #1440 from matrix-org/travis/spec-i18n
Use SAS emoji data from matrix-doc
2020-08-18 11:41:36 -06:00
Travis Ralston 5d95398621 Use SAS emoji data from matrix-doc
Fixes https://github.com/vector-im/element-web/issues/14947

Much like element-web's Jitsi wrapper build steps, this downloads the emoji JSON at build time to ensure it gets reasonably updated. In the future, the spec might want to consider publishing a dedicated i18n package on npm for this, however this is fine for now. We download rather than copy/paste to ensure we always have an updated copy.
2020-08-17 15:35:03 -06:00
RiotRobot 64cdd73b93 v8.1.0 2020-08-17 12:43:32 +01:00
RiotRobot 48a9236ea8 Prepare changelog for v8.1.0 2020-08-17 12:43:31 +01:00
RiotRobot 8b3126e9d8 v8.1.0-rc.1 2020-08-13 12:05:35 +01:00
RiotRobot 74d497cd2d Prepare changelog for v8.1.0-rc.1 2020-08-13 12:05:35 +01:00
Michael Telatynski 5070a5c598 Merge pull request #1438 from brettz9/patch-1
Update on Promises
2020-08-09 13:11:51 +01:00
Brett Zamir 2e30b08e74 Update on Promises
It seems that with ES6 Promises being apparently used, the reference to `done()` is outdated.

Also clarifies that it is "problematic" to discard the results of promises rather than it being an "error" .
2020-08-09 17:39:53 +08:00
J. Ryan Stinnett 8bf63f5f0b Merge pull request #1437 from matrix-org/jryans/defer-cross-signing-setup
Store and request master cross-signing key
2020-08-07 16:06:28 +01:00
J. Ryan Stinnett 11665d18ee Cache all cross-signing private keys when checking trust
This ensures we try to get all private keys when e.g. logging in and using the
passphrase to unlock 4S.

Part of https://github.com/vector-im/element-web/issues/13896
2020-08-06 16:43:56 +01:00
J. Ryan Stinnett a8a9fc0c9d Log secret name received via sharing 2020-08-06 16:43:56 +01:00
J. Ryan Stinnett 098cd1b8d4 Request master cross-signing private key during verification
This change adds a request for the master cross-signing private key, in case the
other device is willing to share it.

Part of https://github.com/vector-im/element-web/issues/13896
2020-08-06 16:43:56 +01:00
J. Ryan Stinnett 3166a4880d Move cross-signing key request path out of verification
This was always quite awkwardly placed in verification, so this moves it to
somewhere more expected. No changes are made to the logic or features in this
commit.
2020-08-06 16:39:56 +01:00
J. Ryan Stinnett 9d1c7136cc Store master cross-signing private key in local cache
We now want to store all private keys in the local cache, including the master
key if available.

Part of https://github.com/vector-im/element-web/issues/13896
2020-08-06 16:39:56 +01:00
J. Ryan Stinnett e100943edf Fix typo in comments 2020-08-06 16:24:49 +01:00
RiotRobot a6fe4cdf1c Merge branch 'master' into develop 2020-08-05 15:30:42 +01:00
RiotRobot 8b5213c09a v8.0.1 2020-08-05 15:27:22 +01:00
RiotRobot 23a133c825 Prepare changelog for v8.0.1 2020-08-05 15:27:22 +01:00
J. Ryan Stinnett 69c4496dfe Merge pull request #1433 from matrix-org/bwindels/fixinvaliddisplaynames
Filter out non-string display names
2020-08-05 15:03:53 +01:00
Bruno Windels 4d74cca206 Merge pull request #1434 from matrix-org/bwindels/fixinvaliddisplaynames-release
Filter out non-string display names
2020-08-04 13:55:40 +00:00
Bruno Windels 955e081699 respect signature 2020-08-04 15:48:48 +02:00
Bruno Windels b7e73422ab filter out non-string display names 2020-08-04 15:48:48 +02:00
Bruno Windels a9a4ba33aa respect signature 2020-08-04 15:43:57 +02:00
Bruno Windels 7116ad9f58 filter out non-string display names 2020-08-04 15:30:32 +02:00
J. Ryan Stinnett 3d18bdb2aa Merge pull request #1427 from matrix-org/dependabot/npm_and_yarn/elliptic-6.5.3
Bump elliptic from 6.5.2 to 6.5.3
2020-08-04 10:38:10 +01:00
J. Ryan Stinnett 4c14581606 Merge pull request #1431 from matrix-org/jryans/riot-to-element
Replace Riot with Element in docs and comments
2020-08-04 10:27:32 +01:00
J. Ryan Stinnett a9c9ec3977 Replace Riot with Element in docs and comments
This only covers the simple cases of references to issues and repos. More
complex areas, such as deployment scripts, will be handled separately.

Part of https://github.com/vector-im/element-web/issues/14864
2020-08-03 18:32:52 +01:00
J. Ryan Stinnett 694d1f9631 Merge pull request #1430 from matrix-org/jryans/rm-tslint
Remove leftover bits of TSLint
2020-07-31 20:30:44 +01:00
J. Ryan Stinnett 2b9cfae18a Remove leftover bits of TSLint 2020-07-31 14:31:19 +01:00
RiotRobot 800d8380ce v8.0.1-rc.1 2020-07-31 13:18:04 +01:00
RiotRobot 621ca28f68 Prepare changelog for v8.0.1-rc.1 2020-07-31 13:18:03 +01:00
J. Ryan Stinnett 4792241ff6 Merge pull request #1426 from matrix-org/jryans/clean-lint-deps
Remove redundant lint dependencies
2020-07-31 11:42:21 +01:00
David Baker 0b984df5f9 Merge pull request #1428 from matrix-org/dbkr/upload_keys_when_new_key_backup_seen
Upload all keys when we start using a new key backup version
2020-07-30 22:46:39 +01:00
David Baker 2e260155ea Merge pull request #1429 from matrix-org/dbkr/countSessionsNeedingBackup
Expose countSessionsNeedingBackup
2020-07-30 22:46:22 +01:00
David Baker b15c8a2d1c Expose countSessionsNeedingBackup
Useful to see the number of keys waiting for backup (it's exposed
via events but you couldn;t get it directly). Also clarify doc on
the return value of `flagAllGroupSessionsForBackup` which was not
technically incorrect...
2020-07-30 19:05:45 +01:00
David Baker 94ab317f23 Upload all keys when we start using a new key backup version
Although see comment and other bug created

Fixes https://github.com/vector-im/riot-web/issues/14828
2020-07-30 18:08:48 +01:00
dependabot[bot] 02b37e1219 Bump elliptic from 6.5.2 to 6.5.3
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-30 12:41:01 +00:00
J. Ryan Stinnett 9d25848a21 Remove unused Jest lint plugin 2020-07-29 11:55:50 +01:00
J. Ryan Stinnett 3958768e1f Remove redundant lint dependencies
These are no longer needed with the new standard lint repo.
2020-07-29 11:36:57 +01:00
Jorik Schellekens cec00cd303 Merge pull request #1422 from matrix-org/joriks/conigure-eslint
Configure and use new eslint package
2020-07-28 19:53:54 +01:00
Jorik Schellekens 45cfef4294 New lines for let statements 2020-07-28 14:31:21 +01:00
Jorik Schellekens 2a01e99635 Merge branch 'develop' of github.com:matrix-org/matrix-js-sdk into joriks/conigure-eslint 2020-07-28 14:17:02 +01:00
Jorik Schellekens a9c3aee447 Revert quotes change in ts 2020-07-28 14:16:46 +01:00
RiotRobot c669382e12 v8.0.0 2020-07-27 20:50:09 +01:00
RiotRobot 19251c427a Prepare changelog for v8.0.0 2020-07-27 20:50:08 +01:00
Michael Telatynski a7aec9e2a4 Merge pull request #1424 from matrix-org/t3chguy/txnId
Properly support txnId
2020-07-24 00:01:31 +01:00
Michael Telatynski 99d7622b42 Properly support txnId
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-07-22 11:09:07 +01:00
Michael Telatynski 8728619d2c Merge pull request #1423 from matrix-org/t3chguy/depr/1
[BREAKING] Remove deprecated getIdenticonUri
2020-07-21 21:35:30 +01:00
Michael Telatynski f6d51fdfb8 remove outdated identicon tests
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-07-21 21:30:25 +01:00
Michael Telatynski 0ffaa8d617 Remove deprecated getIdenticonUri
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-07-21 21:18:17 +01:00
Jorik Schellekens 8a974172ab Fix babel 2020-07-21 10:40:02 +01:00
Jorik Schellekens 72675f7266 Lint ts 2020-07-21 10:33:16 +01:00
Jorik Schellekens 0f559050d8 Fix whitespace issues 2020-07-21 10:00:16 +01:00
Jorik Schellekens 58084774bf fix lock 2020-07-21 09:56:17 +01:00
Travis Ralston c7aee7cebd Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/lodash-4.17.19' into develop 2020-07-20 15:47:57 -06:00
Jorik Schellekens c68d135ae8 Use new eslint 2020-07-20 21:48:28 +01:00
dependabot[bot] 9ed6c99ec8 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-16 19:28:57 +00:00
Travis Ralston 7d398d41d0 Merge pull request #1419 from matrix-org/travis/general/perf/js-roomstate-map
[BREAKING] Convert RoomState's stored state map to a real map
2020-07-16 12:42:31 -06:00
Travis Ralston 0af763252c Have the comment represent reality 2020-07-16 12:42:17 -06:00
Travis Ralston 1c9dbbbb19 [BREAKING] Convert RoomState's stored state map to a real map
Though the dictionary format works fine, it's slow. Access times are around the 1ms range for rooms like HQ, which doesn't seem like much, but when compared to the Map's access time of 0.05ms it's slow.

Converting things to a map means we lose index access and have to instead use `.keys()` and `.values()`, both of which return iterables and not arrays. Even doing the `Array.from()` conversion we see times in the 0.05ms range. This is also what makes this a breaking change.

Memory-wise there does not appear to be any measurable impact for a large account.
2020-07-08 22:28:14 -06:00
RiotRobot 2a688bdac8 v7.1.0 2020-07-03 13:15:07 +01:00
RiotRobot 3c53c818cb Prepare changelog for v7.1.0 2020-07-03 13:15:07 +01:00
RiotRobot 8e53cb324e v7.1.0-rc.1 2020-07-01 14:15:43 +01:00
RiotRobot efbd454775 Prepare changelog for v7.1.0-rc.1 2020-07-01 14:15:43 +01:00
Bruno Windels 68c7273f56 Merge pull request #1414 from matrix-org/bwindels/fixbootstraperror
Ask general crypto callbacks for 4S privkey if operation adapter doesn't have it yet
2020-06-26 12:53:11 +00:00
Bruno Windels 8b56ff4eff ask general crypto callbacks for 4S privkey if operation adapter doesn't 2020-06-26 10:59:04 +02:00
Michael Telatynski 8134eedd93 Merge pull request #1413 from matrix-org/t3chguy/hf
Fix ICreateClientOpts missing idBaseUrl
2020-06-25 23:34:31 +01:00
Michael Telatynski a87ae770cc Fix ICreateClientOpts missing idBaseUrl
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-06-25 21:58:15 +01:00
J. Ryan Stinnett 355da0f9a9 Merge pull request #1411 from matrix-org/jryans/more-room-listeners
Increase max event listeners for rooms
2020-06-25 13:54:11 +01:00
J. Ryan Stinnett 10bdd63762 Increase max event listeners for rooms
It's common to add listeners for every displayed Matrix event, so this increases
the default to remove some log noise.
2020-06-25 13:37:35 +01:00
Hubert Chathi 69dc518c2c Merge pull request #1406 from matrix-org/uhoreg/distrust_backup
Don't trust keys megolm received from backup for verifying the sender
2020-06-23 15:37:27 -04:00
Hubert Chathi fa1dddf06c apply feedback from review 2020-06-23 15:20:41 -04:00
RiotRobot f683f4544a Merge branch 'master' into develop 2020-06-23 14:45:24 +01:00
RiotRobot bc5b587651 v7.0.0 2020-06-23 14:42:09 +01:00
RiotRobot 8083031029 Prepare changelog for v7.0.0 2020-06-23 14:42:09 +01:00
Travis Ralston 7b8102a42c Merge pull request #1410 from matrix-org/travis/room-list/setting-diff
Raise the last known account data / state event for an update
2020-06-22 14:10:16 -06:00
Travis Ralston dbe2f5e4db Fix lint 2020-06-22 10:24:02 -06:00
Travis Ralston f27791b7de Raise the last known account data / state event for an update
This is to better support the react-sdk in trying to identify what actually changed in settings instead of re-firing everything. 

Overall this change differs from `prev_content` semantics as it's the last known *stored* event, not whatever the server thinks may or may not have changed.
2020-06-22 10:17:42 -06:00
Hubert Chathi 2a78170395 lint 2020-06-19 19:31:43 -04:00
Hubert Chathi cfca1c7b06 add unit tests and improve docs 2020-06-19 19:27:29 -04:00
Hubert Chathi fb7a67025f add information function to return information about event encryption 2020-06-18 21:43:50 -04:00
Bruno Windels 8a6cd48b8e Merge pull request #1380 from matrix-org/bwindels/bootstrap-operation
Isolate encryption bootstrap side-effects
2020-06-18 14:29:35 +00:00
David Baker e21d1f539d Merge pull request #1405 from matrix-org/dbkr/fix_verification_race
Add method to get current in-flight to-device requests
2020-06-18 15:10:58 +01:00
Bruno Windels f62049559c make authUploadDeviceSigningKeys required, as default is only used by tests
and make tests pass {} instead of undefined for authDict as otherwise
we'll enter in the "obtain flows" mode and not add the keys to the builder
2020-06-18 15:06:56 +02:00
Bruno Windels 1fe9dd03a3 apparently we call the callback also to obtain flows, so handle that 2020-06-18 14:39:16 +02:00
RiotRobot c3283a7297 v7.0.0-rc.1 2020-06-17 21:33:36 +01:00
RiotRobot f423164a1c Prepare changelog for v7.0.0-rc.1 2020-06-17 21:33:35 +01:00
Bruno Windels 75fe596e24 fix tests 2020-06-17 15:36:24 +02:00
Bruno Windels c5eb290e66 remove resetCrossSigningKeys
it was only used by the bootstrap method in js-sdk,
and was not used in react-sdk either.

This is a breaking change, in case anything other than react-sdk
was using this.
2020-06-17 15:34:30 +02:00
Bruno Windels d3b2c8246d remove try/finally as we don't monkey patch the callbacks anymore 2020-06-17 15:34:02 +02:00
Bruno Windels c11796af4b don't fix old key when we're force-creating a new one 2020-06-17 15:29:21 +02:00
Bruno Windels 8591815d66 add comment 2020-06-17 15:26:30 +02:00
Bruno Windels b82870adb2 move key backup bootstrap/migration over to builder 2020-06-17 15:25:42 +02:00
Bruno Windels 3c5b304b6b move cross-signing bootstrapping over to builder 2020-06-17 15:22:24 +02:00
Bruno Windels f656698061 fixup: secret storage indenting 2020-06-17 15:14:26 +02:00
Bruno Windels d4b4bc5031 move 4S creation to operation 2020-06-17 15:06:41 +02:00
Bruno Windels 9bcf33b6d3 Add EncryptionSetupBuilder to capture bootstrapping side-effects 2020-06-17 14:55:19 +02:00
Hubert Chathi fa550e8f03 fix jsdoc 2020-06-16 12:03:29 -04:00
Hubert Chathi 2d73564eba apply feedback from review, and improve js docs 2020-06-16 11:38:36 -04:00
David Baker 1bd80247fd Better variable name 2020-06-16 14:27:26 +01:00
RiotRobot 1c194e8163 Merge branch 'master' into develop 2020-06-16 11:04:39 +01:00
Michael Telatynski aa2d0d9a08 Merge pull request #1404 from matrix-org/t3chguy/push-rules-device
Remove support for unspecced device-specific push rules
2020-06-16 10:26:08 +01:00
Hubert Chathi fd126b8563 lint 2020-06-15 17:53:37 -04:00
Hubert Chathi bc97e7a5ea don't trust keys megolm received from backup for verifying the sender 2020-06-15 17:47:25 -04:00
David Baker 3dece4f46b Add method to get current in-flight to-device requests 2020-06-15 17:38:50 +01:00
Michael Telatynski be05452c70 Fix tests
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-06-14 13:15:28 +01:00
Michael Telatynski 583650cf7d Remove support for unspecced device-specific push rules
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-06-13 14:49:49 +01:00
Michael Telatynski 505915528f Merge pull request #1403 from matrix-org/t3chguy/attemptAuth-existing-session
Use existing session id for fetching flows as to not get a new session
2020-06-12 22:33:26 +01:00
Michael Telatynski ace8a787b4 undo that because {} sux 2020-06-12 20:06:58 +01:00
Michael Telatynski ed2ea9ac8e clean up 2020-06-12 20:00:08 +01:00
Michael Telatynski 8d09a4abe6 Use existing session id for fetching flows as to not get a new session id 2020-06-12 19:50:38 +01:00
J. Ryan Stinnett 1da959ab02 Merge pull request #1400 from matrix-org/jryans/upgrade-deps-2020-06-05
Upgrade deps
2020-06-08 10:58:29 +01:00
J. Ryan Stinnett 997dd9b88a Upgrade deps 2020-06-08 10:13:30 +01:00
RiotRobot ebe66bdd6e Merge branch 'master' into develop 2020-06-05 15:10:44 +01:00
Bruno Windels 013fbb87a7 Merge pull request #1398 from matrix-org/bwindels/backupformatbug
Bring back backup key format migration
2020-06-05 13:25:00 +00:00
Bruno Windels 73764d23dc take into account key can be an object now 2020-06-05 13:38:46 +02:00
Bruno Windels 8f62703bf2 fix type check in migration code 2020-06-05 13:38:15 +02:00
Bruno Windels 6dedae2e4d Revert "remove key backup format migration"
This reverts commit 8d81240c58.
2020-06-05 11:23:04 +02:00
Bruno Windels d32131b2b8 Revert "lint"
This reverts commit 9fe0e1e85f.
2020-06-05 11:20:23 +02:00
Bruno Windels 0a790b2ae3 Merge pull request #1313 from matrix-org/bwindels/throwifcantdecrypt4s
Fix: more informative error message when we cant find a key to decrypt with
2020-06-05 08:30:43 +00:00
RiotRobot ef1d5e3d76 Merge branch 'master' into develop 2020-06-04 15:00:26 +01:00
Michael Telatynski c525a19df5 Merge pull request #1394 from matrix-org/t3chguy/e2eedefault
Add js-sdk mechanism for polling client well-known for config
2020-06-03 10:47:10 +01:00
Michael Telatynski a987a31667 Merge pull request #1388 from matrix-org/dbkr/timeouts
Fix verification request timeouts to match spec
2020-06-03 10:41:15 +01:00
Michael Telatynski 10329c3436 rename _clientWellKnownInterval to _clientWellKnownIntervalID
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-06-03 10:26:46 +01:00
Travis Ralston 29f10bcd44 Merge pull request #1391 from matrix-org/hs/drop-presence-list
Drop presence list methods
2020-06-02 19:50:12 -06:00
Travis Ralston 19fe9b8ac7 Merge pull request #1395 from matrix-org/travis/less-previews
Batch up URL previews to prevent excessive requests
2020-06-02 14:33:04 -06:00
Travis Ralston 76da708352 Call the callback on cached 2020-06-02 14:19:41 -06:00
Travis Ralston 145f01ff2d Batch up URL previews to prevent excessive requests
Cache expiration is still not implemented here.
2020-06-02 13:25:37 -06:00
Michael Telatynski 18b1e00875 Add js-sdk mechanism for polling client well-known for config
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-06-01 22:22:01 +01:00
Will Hunt d021498fa9 Drop presence lists 2020-06-01 11:29:06 +01:00
Michael Telatynski b83aa54661 Test the verification request timeouts of 10m and 2m
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-29 14:49:35 +01:00
Michael Telatynski 429550ca3e fix thing which got missed in the revert
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-29 13:54:08 +01:00
Michael Telatynski 2a6d8c2b1d revert to previous observeOnly behaviour
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-29 13:34:19 +01:00
Michael Telatynski 01c9159830 clean up
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-29 13:17:22 +01:00
Michael Telatynski 3b3ed5159c Implement verification request timeout as per the spec
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-05-29 13:10:25 +01:00
David Baker 636661dd45 WIP code for https://github.com/vector-im/riot-web/issues/12430 2020-05-29 09:53:03 +01:00
Bruno Windels cc8e8434ec Update src/crypto/SecretStorage.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-04-09 15:58:43 +00:00
Bruno Windels 1b94b3c4de throw something more informative when we cant find a key to decrypt with 2020-04-09 16:00:08 +02:00
55 changed files with 3589 additions and 1975 deletions
+21 -68
View File
@@ -1,71 +1,15 @@
module.exports = {
parser: "babel-eslint", // now needed for class properties
parserOptions: {
sourceType: "module",
ecmaFeatures: {
}
},
extends: ["matrix-org"],
plugins: [
"babel",
],
env: {
browser: true,
node: true,
// babel's transform-runtime converts references to ES6 globals such as
// Promise and Map to core-js polyfills, so we can use ES6 globals.
es6: true,
jest: true,
},
extends: ["eslint:recommended", "google"],
plugins: [
"babel",
"jest",
],
rules: {
// rules we've always adhered to or now do
"max-len": ["error", {
code: 90,
ignoreComments: true,
}],
curly: ["error", "multi-line"],
"prefer-const": ["error"],
"comma-dangle": ["error", {
arrays: "always-multiline",
objects: "always-multiline",
imports: "always-multiline",
exports: "always-multiline",
functions: "always-multiline",
}],
// loosen jsdoc requirements a little
"require-jsdoc": ["error", {
require: {
FunctionDeclaration: false,
}
}],
"valid-jsdoc": ["error", {
requireParamDescription: false,
requireReturn: false,
requireReturnDescription: false,
}],
// rules we do not want from eslint-recommended
"no-console": ["off"],
"no-constant-condition": ["off"],
"no-empty": ["error", { "allowEmptyCatch": true }],
// rules we do not want from the google styleguide
"object-curly-spacing": ["off"],
"spaced-comment": ["off"],
"guard-for-in": ["off"],
// in principle we prefer single quotes, but life is too short
quotes: ["off"],
// rules we'd ideally like to adhere to, but the current
// code does not (in most cases because it's still ES5)
// we set these to warnings, and assert that the number
// of warnings doesn't exceed a given threshold
"no-var": ["warn"],
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
"prefer-rest-params": ["warn"],
"prefer-spread": ["warn"],
"one-var": ["warn"],
@@ -79,10 +23,19 @@ module.exports = {
"asyncArrow": "always",
}],
"arrow-parens": "off",
// eslint's built in no-invalid-this rule breaks with class properties
"no-invalid-this": "off",
// so we replace it with a version that is class property aware
"babel/no-invalid-this": "error",
}
}
"prefer-promise-reject-errors": "off",
"quotes": "off",
"indent": "off",
"no-constant-condition": "off",
"no-async-promise-executor": "off",
},
overrides: [{
"files": ["src/**/*.ts"],
"extends": ["matrix-org/ts"],
"rules": {
// While we're converting to ts we make heavy use of this
"@typescript-eslint/no-explicit-any": "off",
"quotes": "off",
},
}],
};
+184
View File
@@ -1,3 +1,187 @@
Changes in [8.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.3.0) (2020-09-14)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.3.0-rc.1...v8.3.0)
* No changes since rc.1
Changes in [8.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.3.0-rc.1) (2020-09-09)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.2.0...v8.3.0-rc.1)
* Add missing options in ICreateClientOpts
[\#1452](https://github.com/matrix-org/matrix-js-sdk/pull/1452)
* Ensure ready functions return boolean values
[\#1457](https://github.com/matrix-org/matrix-js-sdk/pull/1457)
* Handle missing cross-signing keys gracefully
[\#1456](https://github.com/matrix-org/matrix-js-sdk/pull/1456)
* Fix eslint ts override tsx matching
[\#1451](https://github.com/matrix-org/matrix-js-sdk/pull/1451)
* Untangle cross-signing and secret storage
[\#1450](https://github.com/matrix-org/matrix-js-sdk/pull/1450)
Changes in [8.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.2.0) (2020-09-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.2.0-rc.1...v8.2.0)
## Security notice
JS SDK 8.2.0 fixes an issue where encrypted state events could break incoming call handling.
Thanks to @awesome-michael from Awesome Technologies for responsibly disclosing this via Matrix's
Security Disclosure Policy.
## All changes
* No changes since rc.1
Changes in [8.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.2.0-rc.1) (2020-08-26)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.1.0...v8.2.0-rc.1)
* Add state event check
[\#1449](https://github.com/matrix-org/matrix-js-sdk/pull/1449)
* Add method to check whether client .well-known has been fetched
[\#1444](https://github.com/matrix-org/matrix-js-sdk/pull/1444)
* Handle auth errors during cross-signing key upload
[\#1443](https://github.com/matrix-org/matrix-js-sdk/pull/1443)
* Don't fail if the requested audio output isn't available
[\#1448](https://github.com/matrix-org/matrix-js-sdk/pull/1448)
* Fix logging failures
[\#1447](https://github.com/matrix-org/matrix-js-sdk/pull/1447)
* Log the constraints we pass to getUserMedia
[\#1446](https://github.com/matrix-org/matrix-js-sdk/pull/1446)
* Use SAS emoji data from matrix-doc
[\#1440](https://github.com/matrix-org/matrix-js-sdk/pull/1440)
Changes in [8.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.1.0) (2020-08-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.1.0-rc.1...v8.1.0)
* No changes since rc.1
Changes in [8.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.1.0-rc.1) (2020-08-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.0.1...v8.1.0-rc.1)
* Update on Promises
[\#1438](https://github.com/matrix-org/matrix-js-sdk/pull/1438)
* Store and request master cross-signing key
[\#1437](https://github.com/matrix-org/matrix-js-sdk/pull/1437)
* Filter out non-string display names
[\#1433](https://github.com/matrix-org/matrix-js-sdk/pull/1433)
* Bump elliptic from 6.5.2 to 6.5.3
[\#1427](https://github.com/matrix-org/matrix-js-sdk/pull/1427)
* Replace Riot with Element in docs and comments
[\#1431](https://github.com/matrix-org/matrix-js-sdk/pull/1431)
* Remove leftover bits of TSLint
[\#1430](https://github.com/matrix-org/matrix-js-sdk/pull/1430)
Changes in [8.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.1) (2020-08-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.0.1-rc.1...v8.0.1)
* Filter out non-string display names
[\#1434](https://github.com/matrix-org/matrix-js-sdk/pull/1434)
Changes in [8.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.1-rc.1) (2020-07-31)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.0.0...v8.0.1-rc.1)
* Remove redundant lint dependencies
[\#1426](https://github.com/matrix-org/matrix-js-sdk/pull/1426)
* Upload all keys when we start using a new key backup version
[\#1428](https://github.com/matrix-org/matrix-js-sdk/pull/1428)
* Expose countSessionsNeedingBackup
[\#1429](https://github.com/matrix-org/matrix-js-sdk/pull/1429)
* Configure and use new eslint package
[\#1422](https://github.com/matrix-org/matrix-js-sdk/pull/1422)
Changes in [8.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.0) (2020-07-27)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.1.0...v8.0.0)
BREAKING CHANGES
---
* `RoomState` events changed to use a Map instead of an object, which changes the collection APIs available to access them.
All Changes
---
* Properly support txnId
[\#1424](https://github.com/matrix-org/matrix-js-sdk/pull/1424)
* [BREAKING] Remove deprecated getIdenticonUri
[\#1423](https://github.com/matrix-org/matrix-js-sdk/pull/1423)
* Bump lodash from 4.17.15 to 4.17.19
[\#1421](https://github.com/matrix-org/matrix-js-sdk/pull/1421)
* [BREAKING] Convert RoomState's stored state map to a real map
[\#1419](https://github.com/matrix-org/matrix-js-sdk/pull/1419)
Changes in [7.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.1.0) (2020-07-03)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.1.0-rc.1...v7.1.0)
* No changes since rc.1
Changes in [7.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.1.0-rc.1) (2020-07-01)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.0.0...v7.1.0-rc.1)
* Ask general crypto callbacks for 4S privkey if operation adapter doesn't
have it yet
[\#1414](https://github.com/matrix-org/matrix-js-sdk/pull/1414)
* Fix ICreateClientOpts missing idBaseUrl
[\#1413](https://github.com/matrix-org/matrix-js-sdk/pull/1413)
* Increase max event listeners for rooms
[\#1411](https://github.com/matrix-org/matrix-js-sdk/pull/1411)
* Don't trust keys megolm received from backup for verifying the sender
[\#1406](https://github.com/matrix-org/matrix-js-sdk/pull/1406)
* Raise the last known account data / state event for an update
[\#1410](https://github.com/matrix-org/matrix-js-sdk/pull/1410)
* Isolate encryption bootstrap side-effects
[\#1380](https://github.com/matrix-org/matrix-js-sdk/pull/1380)
* Add method to get current in-flight to-device requests
[\#1405](https://github.com/matrix-org/matrix-js-sdk/pull/1405)
Changes in [7.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.0.0) (2020-06-23)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.0.0-rc.1...v7.0.0)
* No changes since rc.1
Changes in [7.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.0.0-rc.1) (2020-06-17)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.2...v7.0.0-rc.1)
BREAKING CHANGES
---
* Presence lists were removed from the spec in r0.5.0, and the corresponding methods have now been removed here as well:
* `getPresenceList`
* `inviteToPresenceList`
* `dropFromPresenceList`
All changes
---
* Remove support for unspecced device-specific push rules
[\#1404](https://github.com/matrix-org/matrix-js-sdk/pull/1404)
* Use existing session id for fetching flows as to not get a new session
[\#1403](https://github.com/matrix-org/matrix-js-sdk/pull/1403)
* Upgrade deps
[\#1400](https://github.com/matrix-org/matrix-js-sdk/pull/1400)
* Bring back backup key format migration
[\#1398](https://github.com/matrix-org/matrix-js-sdk/pull/1398)
* Fix: more informative error message when we cant find a key to decrypt with
[\#1313](https://github.com/matrix-org/matrix-js-sdk/pull/1313)
* Add js-sdk mechanism for polling client well-known for config
[\#1394](https://github.com/matrix-org/matrix-js-sdk/pull/1394)
* Fix verification request timeouts to match spec
[\#1388](https://github.com/matrix-org/matrix-js-sdk/pull/1388)
* Drop presence list methods
[\#1391](https://github.com/matrix-org/matrix-js-sdk/pull/1391)
* Batch up URL previews to prevent excessive requests
[\#1395](https://github.com/matrix-org/matrix-js-sdk/pull/1395)
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)
+2 -4
View File
@@ -182,10 +182,8 @@ you can pass the result of the promise into it with something like:
matrixClient.someMethod(arg1, arg2).nodeify(callback);
```
The main thing to note is that it is an error to discard the result of a
promise-returning function, as that will cause exceptions to go unobserved. If
you have nothing better to do with the result, just call ``.done()`` on it. See
http://documentup.com/kriskowal/q/#the-end for more information.
The main thing to note is that it is problematic to discard the result of a
promise-returning function, as that will cause exceptions to go unobserved.
Methods which return a promise show this in their documentation.
+5 -4
View File
@@ -288,7 +288,7 @@ function printMemberList(room) {
}
function printRoomInfo(room) {
var eventDict = room.currentState.events;
var eventMap = room.currentState.events;
var eTypeHeader = " Event Type(state_key) ";
var sendHeader = " Sender ";
// pad content to 100
@@ -300,14 +300,15 @@ function printRoomInfo(room) {
var contentHeader = padSide + "Content" + padSide;
print(eTypeHeader+sendHeader+contentHeader);
print(new Array(100).join("-"));
Object.keys(eventDict).forEach(function(eventType) {
eventMap.keys().forEach(function(eventType) {
if (eventType === "m.room.member") { return; } // use /members instead.
Object.keys(eventDict[eventType]).forEach(function(stateKey) {
var eventEventMap = eventMap.get(eventType);
eventEventMap.keys().forEach(function(stateKey) {
var typeAndKey = eventType + (
stateKey.length > 0 ? "("+stateKey+")" : ""
);
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
var event = eventDict[eventType][stateKey];
var event = eventEventMap.get(stateKey);
var sendStr = fixWidth(event.getSender(), sendHeader.length);
var contentStr = fixWidth(
JSON.stringify(event.getContent()), contentHeader.length
+5 -8
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "6.2.2",
"version": "8.3.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
@@ -13,10 +13,9 @@
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:ts && yarn lint:js",
"lint:js": "eslint --max-warnings 81 src spec",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 76 src spec",
"lint:types": "tsc --noEmit",
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
"test": "jest spec/ --coverage --testEnvironment node",
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
},
@@ -75,10 +74,9 @@
"babelify": "^10.0.0",
"better-docs": "^1.4.7",
"browserify": "^16.5.0",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"eslint": "7.3.1",
"eslint-config-matrix-org": "^0.1.2",
"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",
@@ -89,7 +87,6 @@
"rimraf": "^3.0.0",
"terser": "^4.4.3",
"tsify": "^4.0.1",
"tslint": "^5.20.1",
"typescript": "^3.7.3"
},
"jest": {
+1 -1
View File
@@ -10,7 +10,7 @@
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
#
# Note: this script is also used to release matrix-react-sdk and riot-web.
# Note: this script is also used to release matrix-react-sdk and element-web.
set -e
+2 -2
View File
@@ -140,7 +140,7 @@ describe("DeviceList management:", function() {
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/riot-web/issues/3126
// https://github.com/vector-im/element-web/issues/3126
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
@@ -271,7 +271,7 @@ describe("DeviceList management:", function() {
});
}).timeout(3000);
// https://github.com/vector-im/riot-web/issues/4983
// https://github.com/vector-im/element-web/issues/4983
describe("Alice should know she has stale device lists", () => {
beforeEach(async function() {
await aliceTestClient.start();
+1 -1
View File
@@ -90,7 +90,7 @@ describe("MatrixClient retrying", function() {
// wait for the localecho of ev1 to be updated
const p3 = new Promise((resolve, reject) => {
room.on("Room.localEchoUpdated", (ev0) => {
if(ev0 === ev1) {
if (ev0 === ev1) {
resolve();
}
});
+2 -2
View File
@@ -346,7 +346,7 @@ describe("megolm", function() {
});
it("Alice receives a megolm message before the session keys", function() {
// https://github.com/vector-im/riot-web/issues/2273
// https://github.com/vector-im/element-web/issues/2273
let roomKeyEncrypted;
return aliceTestClient.start().then(() => {
@@ -726,7 +726,7 @@ describe("megolm", function() {
});
});
// https://github.com/vector-im/riot-web/issues/2676
// https://github.com/vector-im/element-web/issues/2676
it("Alice should send to her other devices", function() {
// for this test, we make the testOlmAccount be another of Alice's devices.
// it ought to get included in messages Alice sends.
+1 -28
View File
@@ -1,4 +1,4 @@
import {getHttpUriForMxc, getIdenticonUri} from "../../src/content-repo";
import {getHttpUriForMxc} from "../../src/content-repo";
describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
@@ -56,31 +56,4 @@ describe("ContentRepo", function() {
);
});
});
describe("getIdenticonUri", function() {
it("should do nothing for null input", function() {
expect(getIdenticonUri(null)).toEqual(null);
});
it("should set w/h by default to 96", function() {
expect(getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=96&height=96",
);
});
it("should be able to set custom w/h", function() {
expect(getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=32&height=64",
);
});
it("should URL encode the identicon string", function() {
expect(getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
"?width=32&height=64",
);
});
});
});
+61
View File
@@ -10,6 +10,7 @@ import * as olmlib from "../../src/crypto/olmlib";
import {sleep} from "../../src/utils";
import {EventEmitter} from "events";
import {CRYPTO_ENABLED} from "../../src/client";
import {DeviceInfo} from "../../src/crypto/deviceinfo";
const Olm = global.Olm;
@@ -26,6 +27,66 @@ describe("Crypto", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(3);
});
describe("encrypted events", function() {
it("provides encryption information", async function() {
const client = (new TestClient(
"@alice:example.com", "deviceid",
)).client;
await client.initCrypto();
// unencrypted event
const event = {
getId: () => "$event_id",
getSenderKey: () => null,
getWireContent: () => {return {};},
};
let encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeFalsy();
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
event.getWireContent = () => {return {algorithm: olmlib.MEGOLM_ALGORITHM};};
event.getForwardingCurve25519KeyChain = () => ["not empty"];
event.isKeySourceUntrusted = () => false;
event.getClaimedEd25519Key =
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
expect(encryptionInfo.authenticated).toBeFalsy();
expect(encryptionInfo.sender).toBeFalsy();
// known sender, megolm key from backup
event.getForwardingCurve25519KeyChain = () => [];
event.isKeySourceUntrusted = () => true;
const device = new DeviceInfo("FLIBBLE");
device.keys["curve25519:FLIBBLE"] =
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
device.keys["ed25519:FLIBBLE"] =
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
client._crypto._deviceList.getDeviceByIdentityKey = () => device;
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
expect(encryptionInfo.authenticated).toBeFalsy();
expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeFalsy();
// known sender, trusted megolm key, but bad ed25519key
event.isKeySourceUntrusted = () => false;
device.keys["ed25519:FLIBBLE"] =
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
encryptionInfo = client.getEventEncryptionInfo(event);
expect(encryptionInfo.encrypted).toBeTruthy();
expect(encryptionInfo.authenticated).toBeTruthy();
expect(encryptionInfo.sender).toBeTruthy();
expect(encryptionInfo.mismatchedSender).toBeTruthy();
client.stopClient();
});
});
describe('Session management', function() {
const otkResponse = {
+1 -1
View File
@@ -38,7 +38,7 @@ const testKey = new Uint8Array([
]);
const types = [
{ type: "master", shouldCache: false },
{ type: "master", shouldCache: true },
{ type: "self_signing", shouldCache: true },
{ type: "user_signing", shouldCache: true },
{ type: "invalid", shouldCache: false },
+3 -1
View File
@@ -27,6 +27,7 @@ import {MockStorageApi} from "../../MockStorageApi";
import * as testUtils from "../../test-utils";
import {OlmDevice} from "../../../src/crypto/OlmDevice";
import {Crypto} from "../../../src/crypto";
import {resetCrossSigningKeys} from "./crypto-utils";
const Olm = global.Olm;
@@ -332,7 +333,7 @@ describe("MegolmBackup", function() {
client.on("crossSigning.getKey", function(e) {
e.done(privateKeys[e.type]);
});
await client.resetCrossSigningKeys();
await resetCrossSigningKeys(client);
let numCalls = 0;
await new Promise((resolve, reject) => {
client._http.authedRequest = function(
@@ -517,6 +518,7 @@ describe("MegolmBackup", function() {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
});
});
+65 -8
View File
@@ -20,6 +20,8 @@ import anotherjson from 'another-json';
import * as olmlib from "../../../src/crypto/olmlib";
import {TestClient} from '../../TestClient';
import {HttpResponse, setHttpResponses} from '../../test-utils';
import { resetCrossSigningKeys } from "./crypto-utils";
import { MatrixError } from '../../../src/http-api';
async function makeTestClient(userInfo, options, keys) {
if (!keys) keys = {};
@@ -66,11 +68,66 @@ describe("Cross Signing", function() {
);
});
alice.uploadKeySignatures = async () => {};
alice.setAccountData = async () => {};
alice.getAccountDataFromServer = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
await alice.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
});
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
});
it("should abort bootstrap if device signing auth fails", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = async (auth, keys) => {
const errorResponse = {
session: "sessionId",
flows: [
{
stages: [
"m.login.password",
],
},
],
params: {},
};
// If we're not just polling for flows, add on error rejecting the
// auth attempt.
if (auth) {
Object.assign(errorResponse, {
completed: [],
error: "Invalid password",
errcode: "M_FORBIDDEN",
});
}
const error = new MatrixError(errorResponse);
error.httpStatus == 401;
throw error;
};
alice.uploadKeySignatures = async () => {};
alice.setAccountData = async () => {};
alice.getAccountDataFromServer = async () => { };
const authUploadDeviceSigningKeys = async func => await func({});
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
// through failure, stopping before actually applying changes.
let bootstrapDidThrow = false;
try {
await alice.bootstrapCrossSigning({
authUploadDeviceSigningKeys,
});
} catch (e) {
if (e.errcode === "M_FORBIDDEN") {
bootstrapDidThrow = true;
}
}
expect(bootstrapDidThrow).toBeTruthy();
});
it("should upload a signature when a user is verified", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
@@ -78,7 +135,7 @@ describe("Cross Signing", function() {
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
await resetCrossSigningKeys(alice);
// Alice downloads Bob's device key
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
@@ -273,7 +330,7 @@ describe("Cross Signing", function() {
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
@@ -363,7 +420,7 @@ describe("Cross Signing", function() {
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
await resetCrossSigningKeys(alice);
const selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
@@ -520,7 +577,7 @@ describe("Cross Signing", function() {
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
await resetCrossSigningKeys(alice);
// Alice downloads Bob's ssk and device key
// (NOTE: device key is not signed by ssk)
const bobMasterSigning = new global.Olm.PkSigning();
@@ -588,7 +645,7 @@ describe("Cross Signing", function() {
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
await alice.resetCrossSigningKeys();
await resetCrossSigningKeys(alice);
// Alice downloads Bob's keys
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
@@ -740,7 +797,7 @@ describe("Cross Signing", function() {
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
// set Bob's cross-signing key
await bob.resetCrossSigningKeys();
await resetCrossSigningKeys(bob);
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: {
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
@@ -766,7 +823,7 @@ describe("Cross Signing", function() {
let upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve;
});
await alice.resetCrossSigningKeys();
await resetCrossSigningKeys(alice);
await upgradePromise;
const bobTrust = alice.checkUserTrust("@bob:example.com");
+44
View File
@@ -0,0 +1,44 @@
import {IndexedDBCryptoStore} from '../../../src/crypto/store/indexeddb-crypto-store';
// needs to be phased out and replaced with bootstrapSecretStorage,
// but that is doing too much extra stuff for it to be an easy transition.
export async function resetCrossSigningKeys(client, {
level,
authUploadDeviceSigningKeys = async func => await func(),
} = {}) {
const crypto = client._crypto;
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
try {
await crypto._crossSigningInfo.resetKeys(level);
await crypto._signObject(crypto._crossSigningInfo.keys.master);
// write a copy locally so we know these are trusted keys
await crypto._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
crypto._cryptoStore.storeCrossSigningKeys(
txn, crypto._crossSigningInfo.keys);
},
);
} catch (e) {
// If anything failed here, revert the keys so we know to try again from the start
// next time.
crypto._crossSigningInfo.keys = oldKeys;
throw e;
}
crypto._baseApis.emit("crossSigning.keysChanged", {});
await crypto._afterCrossSigningLocalKeyChange();
}
export async function createSecretStorageKey() {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
decryption.free();
return {
// `pubkey` not used anymore with symmetric 4S
keyInfo: { pubkey: storagePublicKey },
privateKey: storagePrivateKey,
};
}
+14 -3
View File
@@ -21,6 +21,7 @@ import {MatrixEvent} from "../../../src/models/event";
import {TestClient} from '../../TestClient';
import {makeTestClients} from './verification/util';
import {encryptAES} from "../../../src/crypto/aes";
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
import * as utils from "../../../src/utils";
@@ -190,7 +191,7 @@ describe("Secrets", function() {
}),
]);
};
alice.resetCrossSigningKeys();
resetCrossSigningKeys(alice);
const newKeyId = await alice.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
@@ -325,7 +326,12 @@ describe("Secrets", function() {
this.emit("accountData", event);
};
await bob.bootstrapSecretStorage();
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
});
await bob.bootstrapSecretStorage({
createSecretStorageKey,
});
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
@@ -375,6 +381,9 @@ describe("Secrets", function() {
const secretStorage = bob._crypto._secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
});
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({
// `pubkey` not used anymore with symmetric 4S
@@ -389,7 +398,9 @@ describe("Secrets", function() {
crossSigning.toStorage(),
);
crossSigning.keys = {};
await bob.bootstrapSecretStorage();
await bob.bootstrapCrossSigning({
authUploadDeviceSigningKeys: async func => await func({}),
});
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
+3 -2
View File
@@ -22,6 +22,7 @@ import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
import {verificationMethods} from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib";
import {logger} from "../../../../src/logger";
import {resetCrossSigningKeys} from "../crypto-utils";
const Olm = global.Olm;
@@ -288,12 +289,12 @@ describe("SAS verification", function() {
);
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
alice.httpBackend.flush(undefined, 2);
await alice.client.resetCrossSigningKeys();
await resetCrossSigningKeys(alice.client);
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
bob.httpBackend.flush(undefined, 2);
await bob.client.resetCrossSigningKeys();
await resetCrossSigningKeys(bob.client);
bob.client._crypto._deviceList.storeCrossSigningForUser(
"@alice:example.com", {
@@ -54,6 +54,7 @@ describe("self-verifications", () => {
cacheCallbacks,
);
_crossSigningInfo.keys = {
master: { keys: { X: testKeyPub } },
self_signing: { keys: { X: testKeyPub } },
user_signing: { keys: { X: testKeyPub } },
};
@@ -96,9 +97,9 @@ describe("self-verifications", () => {
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);
/* We should request, and store, 3 cross signing keys and the key backup key */
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3);
expect(_secretStorage.request.mock.calls.length).toBe(4);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
.toEqual(testKey);
@@ -119,6 +119,8 @@ async function distributeEvent(ownRequest, theirRequest, event) {
await theirRequest.channel.handleEvent(event, theirRequest, true);
}
jest.useFakeTimers();
describe("verification request unit tests", function() {
beforeAll(function() {
setupWebcrypto();
@@ -246,4 +248,38 @@ describe("verification request unit tests", function() {
expect(bob1Request.done).toBe(true);
expect(bob2Request.done).toBe(true);
});
it("request times out after 10 minutes", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
true, true);
expect(aliceRequest.cancelled).toBe(false);
expect(aliceRequest._cancellingUserId).toBe(undefined);
jest.advanceTimersByTime(10 * 60 * 1000);
expect(aliceRequest._cancellingUserId).toBe(alice.getUserId());
});
it("request times out 2 minutes after receipt", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"), new Map(), bob);
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
expect(bobRequest.cancelled).toBe(false);
expect(bobRequest._cancellingUserId).toBe(undefined);
jest.advanceTimersByTime(2 * 60 * 1000);
expect(bobRequest._cancellingUserId).toBe(bob.getUserId());
});
});
-6
View File
@@ -33,12 +33,6 @@ describe("RoomMember", function() {
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
});
it("should return an identicon HTTP URL if allowDefault was set and there " +
"was no m.room.member event", function() {
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
expect(url.indexOf("http")).toEqual(0); // don't care about form
});
it("should return nothing if there is no m.room.member and allowDefault=false",
function() {
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
+1 -7
View File
@@ -45,12 +45,6 @@ describe("Room", function() {
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
});
it("should return an identicon HTTP URL if allowDefault was set and there " +
"was no m.room.avatar event", function() {
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", true);
expect(url.indexOf("http")).toEqual(0); // don't care about form
});
it("should return nothing if there is no m.room.avatar and allowDefault=false",
function() {
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", false);
@@ -1379,7 +1373,7 @@ describe("Room", function() {
let hasThrown = false;
try {
await room.loadMembersIfNeeded();
} catch(err) {
} catch (err) {
hasThrown = true;
}
expect(hasThrown).toEqual(true);
+1 -1
View File
@@ -143,7 +143,7 @@ describe("MatrixScheduler", function() {
deferA.reject({});
try {
await globalA;
} catch(err) {
} catch (err) {
await Promise.resolve();
expect(procCount).toEqual(2);
}
+1 -1
View File
@@ -66,7 +66,7 @@ function termsUrlForService(serviceType, baseUrl) {
* callback that returns a Promise<String> of an identity access token to supply
* with identity requests. If the object is unset, no access token will be
* supplied.
* See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
* See also https://github.com/vector-im/element-web/issues/10615 which seeks to
* replace the previous approach of manual access tokens params with this
* callback throughout the SDK.
*
+1 -1
View File
@@ -34,7 +34,7 @@ matrixcs.request(function(opts, fn) {
let indexedDB;
try {
indexedDB = global.indexedDB;
} catch(e) {}
} catch (e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
+225 -94
View File
@@ -54,6 +54,7 @@ import {randomString} from './randomstring';
import {PushProcessor} from "./pushprocessor";
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
import { User } from "./models/user";
import {AutoDiscovery} from "./autodiscovery";
const SCROLLBACK_DELAY_MS = 3000;
export const CRYPTO_ENABLED = isCryptoAvailable();
@@ -118,7 +119,7 @@ function keyFromRecoverySession(session, decryptionKey) {
* callback that returns a Promise<String> of an identity access token to supply
* with identity requests. If the object is unset, no access token will be
* supplied.
* See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
* See also https://github.com/vector-im/element-web/issues/10615 which seeks to
* replace the previous approach of manual access tokens params with this
* callback throughout the SDK.
*
@@ -234,6 +235,14 @@ function keyFromRecoverySession(session, decryptionKey) {
* }
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
*
* @param {function} [opts.cryptoCallbacks.cacheSecretStorageKey]
* Optional. Function called when a new encryption key for secret storage
* has been created. This allows the application a chance to cache this key if
* desired to avoid user prompts.
* Args:
* {string} keyId the ID of the new key
* {Uint8Array} key the new private key
*
* @param {function} [opts.cryptoCallbacks.onSecretRequested]
* Optional. Function called when a request for a secret is received from another
* device.
@@ -329,7 +338,7 @@ export function MatrixClient(opts) {
this._isGuest = false;
this._ongoingScrollbacks = {};
this.timelineSupport = Boolean(opts.timelineSupport);
this.urlPreviewCache = {};
this.urlPreviewCache = {}; // key=preview key, value=Promise for preview (may be an error)
this._notifTimelineSet = null;
this.unstableClientRelationAggregation = !!opts.unstableClientRelationAggregation;
@@ -356,6 +365,9 @@ export function MatrixClient(opts) {
this._cachedCapabilities = null; // { capabilities: {}, lastUpdated: timestamp }
this._clientWellKnown = undefined;
this._clientWellKnownPromise = undefined;
// The SDK doesn't really provide a clean way for events to recalculate the push
// actions for themselves, so we have to kinda help them out when they are encrypted.
// We do this so that push rules are correctly executed on events in their decrypted
@@ -379,7 +391,7 @@ export function MatrixClient(opts) {
? !!actions.tweaks.highlight : false;
if (oldHighlight !== newHighlight || currentCount > 0) {
// TODO: Handle mentions received while the client is offline
// See also https://github.com/vector-im/riot-web/issues/9069
// See also https://github.com/vector-im/element-web/issues/9069
if (!room.hasUserReadEvent(this.getUserId(), event.getId())) {
let newCount = currentCount;
if (newHighlight && !oldHighlight) newCount++;
@@ -397,7 +409,7 @@ export function MatrixClient(opts) {
// Like above, we have to listen for read receipts from ourselves in order to
// correctly handle notification counts on encrypted rooms.
// This fixes https://github.com/vector-im/riot-web/issues/9421
// This fixes https://github.com/vector-im/element-web/issues/9421
this.on("Room.receipt", (event, room) => {
if (room && this.isRoomEncrypted(room.roomId)) {
// Figure out if we've read something or if it's just informational
@@ -968,6 +980,20 @@ MatrixClient.prototype.findVerificationRequestDMInProgress = function(roomId) {
return this._crypto.findVerificationRequestDMInProgress(roomId);
};
/**
* Returns all to-device verification requests that are already in progress for the given user id
*
* @param {string} userId the ID of the user to query
*
* @returns {module:crypto/verification/request/VerificationRequest[]} the VerificationRequests that are in progress
*/
MatrixClient.prototype.getVerificationRequestsToDeviceInProgress = function(userId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getVerificationRequestsToDeviceInProgress(userId);
};
/**
* Request a key verification from another user.
*
@@ -1074,19 +1100,9 @@ function wrapCryptoFuncs(MatrixClient, names) {
}
}
/**
* Generate new cross-signing keys.
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#resetCrossSigningKeys
* @param {object} authDict Auth data to supply for User-Interactive auth.
* @param {CrossSigningLevel} [level] the level of cross-signing to reset. New
* keys will be created for the given level and below. Defaults to
* regenerating all keys.
*/
/**
* Get the user's cross-signing key ID.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#getCrossSigningId
@@ -1098,6 +1114,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
/**
* Get the cross signing information for a given user.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#getStoredCrossSigningForUser
@@ -1108,6 +1125,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
/**
* Check whether a given user is trusted.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkUserTrust
@@ -1118,6 +1136,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
/**
* Check whether a given device is trusted.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkDeviceTrust
@@ -1130,6 +1149,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
/**
* Check the copy of our cross-signing key that we have in the device list and
* see if we can get the private key. If so, mark it as trusted.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkOwnCrossSigningTrust
@@ -1139,6 +1159,7 @@ function wrapCryptoFuncs(MatrixClient, names) {
* Checks that a given cross-signing private key matches a given public key.
* This can be used by the getCrossSigningKey callback to verify that the
* private key it is about to supply is the one that was requested.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkCrossSigningPrivateKey
@@ -1151,10 +1172,49 @@ function wrapCryptoFuncs(MatrixClient, names) {
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @function module:client~MatrixClient#prepareToEncrypt
* @param {module:models/room} room the room the event is in
*/
/**
* Checks whether cross signing:
* - is enabled on this account and trusted by this device
* - has private keys either cached locally or stored in secret storage
*
* If this function returns false, bootstrapCrossSigning() can be used
* to fix things such that it returns true. That is to say, after
* bootstrapCrossSigning() completes successfully, this function should
* return true.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#isCrossSigningReady
* @return {bool} True if cross-signing is ready to be used on this device
*/
/**
* Bootstrap cross-signing by creating keys if needed. If everything is already
* set up, then no changes are made, so this is safe to run to ensure
* cross-signing is ready for use.
*
* This function:
* - creates new cross-signing keys if they are not found locally cached nor in
* secret storage (if it has been setup)
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#bootstrapCrossSigning
* @param {function} opts.authUploadDeviceSigningKeys Function
* called to await an interactive auth flow when uploading device signing keys.
* @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys
* already exist.
* Args:
* {function} A function that makes the request requiring auth. Receives the
* auth data as an object. Can be called multiple times, first with an empty
* authDict, to obtain the flows.
*/
wrapCryptoFuncs(MatrixClient, [
"resetCrossSigningKeys",
"getCrossSigningId",
"getStoredCrossSigningForUser",
"checkUserTrust",
@@ -1164,28 +1224,34 @@ wrapCryptoFuncs(MatrixClient, [
"legacyDeviceVerification",
"prepareToEncrypt",
"isCrossSigningReady",
"bootstrapCrossSigning",
"getCryptoTrustCrossSignedDevices",
"setCryptoTrustCrossSignedDevices",
"countSessionsNeedingBackup",
]);
/**
* Check if the sender of an event is verified
* The cross-signing API is currently UNSTABLE and may change without notice.
* Get information about the encryption of an event
*
* @param {MatrixEvent} event event to be checked
* @function module:client~MatrixClient#getEventEncryptionInfo
*
* @returns {DeviceTrustLevel}
* @param {module:models/event.MatrixEvent} event event to be checked
*
* @return {object} An object with the fields:
* - encrypted: whether the event is encrypted (if not encrypted, some of the
* other properties may not be set)
* - senderKey: the sender's key
* - algorithm: the algorithm used to encrypt the event
* - authenticated: whether we can be sure that the owner of the senderKey
* sent the event
* - sender: the sender's device information, if available
* - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
* (only meaningful if `sender` is set)
*/
MatrixClient.prototype.checkEventSenderTrust = async function(event) {
const device = await this.getEventSenderDeviceInfo(event);
if (!device) {
return 0;
}
return await this._crypto.checkDeviceTrust(event.getSender(), device.deviceId);
};
/**
* Create a recovery key from a user-supplied passphrase.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#createRecoveryKeyFromPassphrase
@@ -1198,22 +1264,59 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
*/
/**
* Bootstrap Secure Secret Storage if needed by creating a default key and signing it with
* the cross-signing master key. If everything is already set up, then no
* changes are made, so this is safe to run to ensure secret storage is ready
* for use.
* Checks whether secret storage:
* - is enabled on this account
* - is storing cross-signing private keys
* - is storing session backup key (if enabled)
*
* If this function returns false, bootstrapSecretStorage() can be used
* to fix things such that it returns true. That is to say, after
* bootstrapSecretStorage() completes successfully, this function should
* return true.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#isSecretStorageReady
* @return {bool} True if secret storage is ready to be used on this device
*/
/**
* Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
* already set up, then no changes are made, so this is safe to run to ensure secret
* storage is ready for use.
*
* This function
* - creates a new Secure Secret Storage key if no default key exists
* - if a key backup exists, it is migrated to store the key in the Secret
* Storage
* - creates a backup if none exists, and one is requested
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
* algorithm is found
*
* @function module:client~MatrixClient#bootstrapSecretStorage
* @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
* called to await an interactive auth flow when uploading device signing keys.
* Args:
* {function} A function that makes the request requiring auth. Receives the
* auth data as an object.
* @param {function} [opts.createSecretStorageKey] Optional. Function
* called to await a secret storage key creation flow.
* 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.
* @param {object} [opts.keyBackupInfo] The current key backup object. If passed,
* the passphrase and recovery key from this backup will be used.
* @param {bool} [opts.setupNewKeyBackup] If true, a new key backup version will be
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
* @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist.
* @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Buffer
* containing the key, or rejects if the key cannot be obtained.
* Returns:
* {Promise} A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
*/
/**
* Add a key for encrypting secrets.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#addSecretStorageKey
@@ -1228,6 +1331,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
/**
* Check whether we have a key with a given ID.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#hasSecretStorageKey
@@ -1237,7 +1341,8 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
*/
/**
* Store an encrypted secret on the server
* Store an encrypted secret on the server.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#storeSecret
@@ -1249,6 +1354,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
/**
* Get a secret from storage.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#getSecret
@@ -1259,6 +1365,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
/**
* Check if a secret is stored on the server.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#isSecretStored
@@ -1273,6 +1380,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
/**
* Request a secret from another device.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#requestSecret
@@ -1284,6 +1392,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
/**
* Get the current default key ID for encrypting secrets.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#getDefaultSecretStorageKeyId
@@ -1293,6 +1402,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
/**
* Set the current default key ID for encrypting secrets.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#setDefaultSecretStorageKeyId
@@ -1303,6 +1413,7 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
* Checks that a given secret storage private key matches a given public key.
* This can be used by the getSecretStorageKey callback to verify that the
* private key it is about to supply is the one that was requested.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @function module:client~MatrixClient#checkSecretStoragePrivateKey
@@ -1312,7 +1423,9 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
*/
wrapCryptoFuncs(MatrixClient, [
"getEventEncryptionInfo",
"createRecoveryKeyFromPassphrase",
"isSecretStorageReady",
"bootstrapSecretStorage",
"addSecretStorageKey",
"hasSecretStorageKey",
@@ -1918,7 +2031,7 @@ MatrixClient.prototype._restoreKeyBackup = function(
let backupPubKey;
try {
backupPubKey = decryption.init_with_private_key(privKey);
} catch(e) {
} catch (e) {
decryption.free();
throw e;
}
@@ -1977,7 +2090,11 @@ MatrixClient.prototype._restoreKeyBackup = function(
}
}
return this.importRoomKeys(keys, { progressCallback });
return this.importRoomKeys(keys, {
progressCallback,
untrusted: true,
source: "backup",
});
}).then(() => {
return this._crypto.setTrustedBackupPubKey(backupPubKey);
}).then(() => {
@@ -2512,7 +2629,7 @@ MatrixClient.prototype._sendCompleteEvent = function(roomId, eventObject, txnId,
const type = localEvent.getType();
logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
localEvent._txnId = txnId;
localEvent.setTxnId(txnId);
localEvent.setStatus(EventStatus.SENDING);
// add this event immediately to the local store as 'sending'.
@@ -2680,7 +2797,11 @@ function _updatePendingEventStatus(room, event, newStatus) {
}
function _sendEventHttpRequest(client, event) {
const txnId = event._txnId ? event._txnId : client.makeTxnId();
let txnId = event.getTxnId();
if (!txnId) {
txnId = client.makeTxnId();
event.setTxnId(txnId);
}
const pathParams = {
$roomId: event.getRoomId(),
@@ -3001,25 +3122,32 @@ MatrixClient.prototype.setRoomReadMarkers = async function(
* May return synthesized attributes if the URL lacked OG meta.
*/
MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
// bucket the timestamp to the nearest minute to prevent excessive spam to the server
// Surely 60-second accuracy is enough for anyone.
ts = Math.floor(ts / 60000) * 60000;
const key = ts + "_" + url;
const og = this.urlPreviewCache[key];
if (og) {
return Promise.resolve(og);
// If there's already a request in flight (or we've handled it), return that instead.
const cachedPreview = this.urlPreviewCache[key];
if (cachedPreview) {
if (callback) {
cachedPreview.then(callback).catch(callback);
}
return cachedPreview;
}
const self = this;
return this._http.authedRequest(
const resp = this._http.authedRequest(
callback, "GET", "/preview_url", {
url: url,
ts: ts,
}, undefined, {
prefix: PREFIX_MEDIA_R0,
},
).then(function(response) {
// TODO: expire cache occasionally
self.urlPreviewCache[key] = response;
return response;
});
);
// TODO: Expire the URL preview cache sometimes
this.urlPreviewCache[key] = resp;
return resp;
};
/**
@@ -3521,46 +3649,6 @@ MatrixClient.prototype.setPresence = function(opts, callback) {
);
};
function _presenceList(callback, client, opts, method) {
const path = utils.encodeUri("/presence/list/$userId", {
$userId: client.credentials.userId,
});
return client._http.authedRequest(callback, method, path, undefined, opts);
}
/**
* Retrieve current user presence list.
* @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.getPresenceList = function(callback) {
return _presenceList(callback, this, undefined, "GET");
};
/**
* Add users to the current user presence list.
* @param {module:client.callback} callback Optional.
* @param {string[]} userIds
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.inviteToPresenceList = function(callback, userIds) {
const opts = {"invite": userIds};
return _presenceList(callback, this, opts, "POST");
};
/**
* Drop users from the current user presence list.
* @param {module:client.callback} callback Optional.
* @param {string[]} userIds
* @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
**/
MatrixClient.prototype.dropFromPresenceList = function(callback, userIds) {
const opts = {"drop": userIds};
return _presenceList(callback, this, opts, "POST");
};
/**
* Retrieve older messages from the given room and put them in the timeline.
@@ -3819,7 +3907,9 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
return pendingRequest;
}
let path, params, promise;
let path;
let params;
let promise;
const self = this;
if (isNotifTimeline) {
@@ -4239,7 +4329,8 @@ MatrixClient.prototype.getRoomPushRule = function(scope, roomId) {
*/
MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
const self = this;
let deferred, hasDontNotifyRule;
let deferred;
let hasDontNotifyRule;
// Get the existing room-kind push rule if any
const roomPushRule = this.getRoomPushRule(scope, roomId);
@@ -4762,6 +4853,9 @@ MatrixClient.prototype.deactivateSynapseUser = function(userId) {
* @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during
* initial sync but fetch them when needed by calling `loadOutOfBandMembers`
* This will override the filter option at this moment.
* @param {Number=} opts.clientWellKnownPollPeriod The number of seconds between polls
* to /.well-known/matrix/client, undefined to disable. This should be in the order of hours.
* Default: undefined.
*/
MatrixClient.prototype.startClient = async function(opts) {
if (this.clientRunning) {
@@ -4810,6 +4904,32 @@ MatrixClient.prototype.startClient = async function(opts) {
this._clientOpts = opts;
this._syncApi = new SyncApi(this, opts);
this._syncApi.sync();
if (opts.clientWellKnownPollPeriod !== undefined) {
this._clientWellKnownIntervalID =
setInterval(() => {
this._fetchClientWellKnown();
}, 1000 * opts.clientWellKnownPollPeriod);
this._fetchClientWellKnown();
}
};
MatrixClient.prototype._fetchClientWellKnown = async function() {
// `getRawClientConfig` does not throw or reject on network errors, instead
// it absorbs errors and returns `{}`.
this._clientWellKnownPromise = AutoDiscovery.getRawClientConfig(
this.getDomain(),
);
this._clientWellKnown = await this._clientWellKnownPromise;
this.emit("WellKnown.client", this._clientWellKnown);
};
MatrixClient.prototype.getClientWellKnown = function() {
return this._clientWellKnown;
};
MatrixClient.prototype.waitForClientWellKnown = function() {
return this._clientWellKnownPromise;
};
/**
@@ -4851,6 +4971,9 @@ MatrixClient.prototype.stopClient = function() {
this._peekSync.stopPeeking();
}
global.clearTimeout(this._checkTurnServersTimeoutID);
if (this._clientWellKnownIntervalID !== undefined) {
global.clearInterval(this._clientWellKnownIntervalID);
}
};
/**
@@ -5603,8 +5726,9 @@ MatrixClient.prototype.generateClientSecret = function() {
* Fires whenever new user-scoped account_data is added.
* @event module:client~MatrixClient#"accountData"
* @param {MatrixEvent} event The event describing the account_data just added
* @param {MatrixEvent} event The previous account data, if known.
* @example
* matrixClient.on("accountData", function(event){
* matrixClient.on("accountData", function(event, oldEvent){
* myAccountData[event.type] = event.content;
* });
*/
@@ -5682,6 +5806,13 @@ MatrixClient.prototype.generateClientSecret = function() {
* @param {string} data.request_id The ID of the original request.
*/
/**
* Fires when the client .well-known info is fetched.
*
* @event module:client~MatrixClient#"WellKnown.client"
* @param {object} data The JSON object returned by the server
*/
// EventEmitter JSDocs
/**
-32
View File
@@ -75,35 +75,3 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
}
/**
* Get an identicon URL from an arbitrary string.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
* @deprecated This is no longer in the specification.
*/
export function getIdenticonUri(baseUrl, identiconString, width, height) {
if (!identiconString) {
return null;
}
if (!width) {
width = 96;
}
if (!height) {
height = 96;
}
const params = {
width: width,
height: height,
};
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
}
+153 -22
View File
@@ -26,6 +26,8 @@ import {logger} from '../logger';
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
import {decryptAES, encryptAES} from './aes';
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
function publicKeyFromKeyInfo(keyInfo) {
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
// We assume only a single key, and we want the bare form without type
@@ -64,15 +66,34 @@ export class CrossSigningInfo extends EventEmitter {
this.crossSigningVerifiedBefore = false;
}
static fromStorage(obj, userId) {
const res = new CrossSigningInfo(userId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
}
toStorage() {
return {
keys: this.keys,
firstUse: this.firstUse,
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
};
}
/**
* Calls the app callback to ask for a private key
*
* @param {string} type The key type ("master", "self_signing", or "user_signing")
* @param {string} expectedPubkey The matching public key or undefined to use
* the stored public key for the given key type.
* @returns {Array} An array with [ public key, Olm.PkSigning ]
*/
async getCrossSigningKey(type, expectedPubkey) {
const shouldCache = ["self_signing", "user_signing"].indexOf(type) >= 0;
const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0;
if (!this._callbacks.getCrossSigningKey) {
throw new Error("No getCrossSigningKey callback supplied");
@@ -125,24 +146,6 @@ export class CrossSigningInfo extends EventEmitter {
);
}
static fromStorage(obj, userId) {
const res = new CrossSigningInfo(userId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
}
toStorage() {
return {
keys: this.keys,
firstUse: this.firstUse,
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
};
}
/**
* Check whether the private keys exist in secret storage.
* XXX: This could be static, be we often seem to have an instance when we
@@ -178,12 +181,12 @@ export class CrossSigningInfo extends EventEmitter {
* typically called in conjunction with the creation of new cross-signing
* keys.
*
* @param {object} keys The keys to store
* @param {Map} keys The keys to store
* @param {SecretStorage} secretStorage The secret store using account data
*/
static async storeInSecretStorage(keys, secretStorage) {
for (const type of Object.keys(keys)) {
const encodedKey = encodeBase64(keys[type]);
for (const [type, privateKey] of keys) {
const encodedKey = encodeBase64(privateKey);
await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
}
}
@@ -199,9 +202,44 @@ export class CrossSigningInfo extends EventEmitter {
*/
static async getFromSecretStorage(type, secretStorage) {
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
if (!encodedKey) {
return null;
}
return decodeBase64(encodedKey);
}
/**
* Check whether the private keys exist in the local key cache.
*
* @returns {boolean} True if all keys are stored in the local cache.
*/
async isStoredInKeyCache() {
const cacheCallbacks = this._cacheCallbacks;
if (!cacheCallbacks) return false;
for (const type of ["master", "self_signing", "user_signing"]) {
if (!await cacheCallbacks.getCrossSigningKeyCache(type)) {
return false;
}
}
return true;
}
/**
* Get cross-signing private keys from the local cache.
*
* @returns {Map} A map from key type (string) to private key (Uint8Array)
*/
async getCrossSigningKeysFromCache() {
const keys = new Map();
const cacheCallbacks = this._cacheCallbacks;
if (!cacheCallbacks) return keys;
for (const type of ["master", "self_signing", "user_signing"]) {
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type);
keys.set(type, privKey);
}
return keys;
}
/**
* Get the ID used to identify the user. This can also be used to test for
* the existence of a given key type.
@@ -677,3 +715,96 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
},
};
}
/**
* Request cross-signing keys from another device during verification.
*
* @param {module:base-apis~MatrixBaseApis} baseApis base Matrix API interface
* @param {string} userId The user ID being verified
* @param {string} deviceId The device ID being verified
*/
export async function requestKeysDuringVerification(baseApis, userId, deviceId) {
// If this is a self-verification, ask the other party for keys
if (baseApis.getUserId() !== userId) {
return;
}
console.log("Cross-signing: 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 = 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("Cross-signing: requesting secret",
type, deviceId);
const { promise } = client.requestSecret(
`m.cross_signing.${type}`, [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/element-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,
KEY_REQUEST_TIMEOUT_MS,
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', [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("master"),
crossSigning.getCrossSigningKey("self_signing"),
crossSigning.getCrossSigningKey("user_signing"),
backupKeyPromise,
]),
timeout,
]).then(resolve, reject);
}).catch((e) => {
console.warn("Cross-signing: failure while requesting keys:", e);
});
}
+1 -1
View File
@@ -790,7 +790,7 @@ class DeviceListUpdateSerialiser {
// 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)
// (https://github.com/vector-im/element-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.
+357
View File
@@ -0,0 +1,357 @@
import { logger } from "../logger";
import {MatrixEvent} from "../models/event";
import {EventEmitter} from "events";
import {createCryptoStoreCacheCallbacks} from "./CrossSigning";
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import {
PREFIX_UNSTABLE,
} from "../http-api";
/**
* Builds an EncryptionSetupOperation by calling any of the add.. methods.
* Once done, `buildOperation()` can be called which allows to apply to operation.
*
* This is used as a helper by Crypto to keep track of all the network requests
* and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future)
* Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them
* more than once.
*/
export class EncryptionSetupBuilder {
/**
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
*/
constructor(accountData, delegateCryptoCallbacks) {
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
this.crossSigningCallbacks = new CrossSigningCallbacks();
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
this._crossSigningKeys = null;
this._keySignatures = null;
this._keyBackupInfo = null;
}
/**
* Adds new cross-signing public keys
*
* @param {function} authUpload Function called to await an interactive auth
* flow when uploading device signing keys.
* Args:
* {function} A function that makes the request requiring auth. Receives
* the auth data as an object. Can be called multiple times, first with
* an empty authDict, to obtain the flows.
* @param {Object} keys the new keys
*/
addCrossSigningKeys(authUpload, keys) {
this._crossSigningKeys = {authUpload, keys};
}
/**
* Adds the key backup info to be updated on the server
*
* Used either to create a new key backup, or add signatures
* from the new MSK.
*
* @param {Object} keyBackupInfo as received from/sent to the server
*/
addSessionBackup(keyBackupInfo) {
this._keyBackupInfo = keyBackupInfo;
}
/**
* Adds the session backup private key to be updated in the local cache
*
* Used after fixing the format of the key
*
* @param {Uint8Array} privateKey
*/
addSessionBackupPrivateKeyToCache(privateKey) {
this._sessionBackupPrivateKey = privateKey;
}
/**
* Add signatures from a given user and device/x-sign key
* Used to sign the new cross-signing key with the device key
*
* @param {String} userId
* @param {String} deviceId
* @param {String} signature
*/
addKeySignature(userId, deviceId, signature) {
if (!this._keySignatures) {
this._keySignatures = {};
}
const userSignatures = this._keySignatures[userId] || {};
this._keySignatures[userId] = userSignatures;
userSignatures[deviceId] = signature;
}
/**
* @param {String} type
* @param {Object} content
* @return {Promise}
*/
setAccountData(type, content) {
return this.accountDataClientAdapter.setAccountData(type, content);
}
/**
* builds the operation containing all the parts that have been added to the builder
* @return {EncryptionSetupOperation}
*/
buildOperation() {
const accountData = this.accountDataClientAdapter._values;
return new EncryptionSetupOperation(
accountData,
this._crossSigningKeys,
this._keyBackupInfo,
this._keySignatures,
);
}
/**
* Stores the created keys locally.
*
* This does not yet store the operation in a way that it can be restored,
* but that is the idea in the future.
*
* @param {Crypto} crypto
* @return {Promise}
*/
async persist(crypto) {
// store private keys in cache
if (this._crossSigningKeys) {
const cacheCallbacks = createCryptoStoreCacheCallbacks(
crypto._cryptoStore, crypto._olmDevice);
for (const type of ["master", "self_signing", "user_signing"]) {
logger.log(`Cache ${type} cross-signing private key locally`);
const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey);
}
// store own cross-sign pubkeys as trusted
await crypto._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
crypto._cryptoStore.storeCrossSigningKeys(
txn, this._crossSigningKeys.keys);
},
);
}
// store session backup key in cache
if (this._sessionBackupPrivateKey) {
await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey);
}
}
}
/**
* Can be created from EncryptionSetupBuilder, or
* (in a follow-up PR, not implemented yet) restored from storage, to retry.
*
* It does not have knowledge of any private keys, unlike the builder.
*/
export class EncryptionSetupOperation {
/**
* @param {Map<String, Object>} accountData
* @param {Object} crossSigningKeys
* @param {Object} keyBackupInfo
* @param {Object} keySignatures
*/
constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) {
this._accountData = accountData;
this._crossSigningKeys = crossSigningKeys;
this._keyBackupInfo = keyBackupInfo;
this._keySignatures = keySignatures;
}
/**
* Runs the (remaining part of, in the future) operation by sending requests to the server.
* @param {Crypto} crypto
*/
async apply(crypto) {
const baseApis = crypto._baseApis;
// upload cross-signing keys
if (this._crossSigningKeys) {
const keys = {};
for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) {
keys[name + "_key"] = key;
}
// We must only call `uploadDeviceSigningKeys` from inside this auth
// helper to ensure we properly handle auth errors.
await this._crossSigningKeys.authUpload(authDict => {
return baseApis.uploadDeviceSigningKeys(authDict, keys);
});
// pass the new keys to the main instance of our own CrossSigningInfo.
crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys);
}
// set account data
if (this._accountData) {
for (const [type, content] of this._accountData) {
await baseApis.setAccountData(type, content);
}
}
// upload first cross-signing signatures with the new key
// (e.g. signing our own device)
if (this._keySignatures) {
await baseApis.uploadKeySignatures(this._keySignatures);
}
// need to create/update key backup info
if (this._keyBackupInfo) {
if (this._keyBackupInfo.version) {
// session backup signature
// The backup is trusted because the user provided the private key.
// Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing.
await baseApis._http.authedRequest(
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
undefined, {
algorithm: this._keyBackupInfo.algorithm,
auth_data: this._keyBackupInfo.auth_data,
},
{prefix: PREFIX_UNSTABLE},
);
} else {
// add new key backup
await baseApis._http.authedRequest(
undefined, "POST", "/room_keys/version",
undefined, this._keyBackupInfo,
{prefix: PREFIX_UNSTABLE},
);
}
}
}
}
/**
* Catches account data set by SecretStorage during bootstrapping by
* implementing the methods related to account data in MatrixClient
*/
class AccountDataClientAdapter extends EventEmitter {
/**
* @param {Object.<String, MatrixEvent>} accountData existing account data
*/
constructor(accountData) {
super();
this._existingValues = accountData;
this._values = new Map();
}
/**
* @param {String} type
* @return {Promise<Object>} the content of the account data
*/
getAccountDataFromServer(type) {
return Promise.resolve(this.getAccountData(type));
}
/**
* @param {String} type
* @return {Object} the content of the account data
*/
getAccountData(type) {
const modifiedValue = this._values.get(type);
if (modifiedValue) {
return modifiedValue;
}
const existingValue = this._existingValues[type];
if (existingValue) {
return existingValue.getContent();
}
return null;
}
/**
* @param {String} type
* @param {Object} content
* @return {Promise}
*/
setAccountData(type, content) {
const lastEvent = this._values.get(type);
this._values.set(type, content);
// ensure accountData is emitted on the next tick,
// as SecretStorage listens for it while calling this method
// and it seems to rely on this.
return Promise.resolve().then(() => {
const event = new MatrixEvent({type, content});
this.emit("accountData", event, lastEvent);
});
}
}
/**
* Catches the private cross-signing keys set during bootstrapping
* by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
* See CrossSigningInfo constructor
*/
class CrossSigningCallbacks {
constructor() {
this.privateKeys = new Map();
}
// cache callbacks
getCrossSigningKeyCache(type, expectedPublicKey) {
return this.getCrossSigningKey(type, expectedPublicKey);
}
storeCrossSigningKeyCache(type, key) {
this.privateKeys.set(type, key);
return Promise.resolve();
}
// non-cache callbacks
getCrossSigningKey(type, _expectedPubkey) {
return Promise.resolve(this.privateKeys.get(type));
}
saveCrossSigningKeys(privateKeys) {
for (const [type, privateKey] of Object.entries(privateKeys)) {
this.privateKeys.set(type, privateKey);
}
}
}
/**
* Catches the 4S private key set during bootstrapping by implementing
* the SecretStorage crypto callbacks
*/
class SSSSCryptoCallbacks {
constructor(delegateCryptoCallbacks) {
this._privateKeys = new Map();
this._delegateCryptoCallbacks = delegateCryptoCallbacks;
}
async getSecretStorageKey({ keys }, name) {
for (const keyId of Object.keys(keys)) {
const privateKey = this._privateKeys.get(keyId);
if (privateKey) {
return [keyId, privateKey];
}
}
// if we don't have the key cached yet, ask
// for it to the general crypto callbacks and cache it
if (this._delegateCryptoCallbacks) {
const result = await this._delegateCryptoCallbacks.
getSecretStorageKey({keys}, name);
if (result) {
const [keyId, privateKey] = result;
this._privateKeys.set(keyId, privateKey);
}
return result;
}
}
addPrivateKey(keyId, privKey) {
this._privateKeys.set(keyId, privKey);
// Also pass along to application to cache if it wishes
if (
this._delegateCryptoCallbacks &&
this._delegateCryptoCallbacks.cacheSecretStorageKey
) {
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, privKey);
}
}
}
+5 -3
View File
@@ -992,11 +992,12 @@ OlmDevice.prototype._getInboundGroupSession = function(
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
* @param {boolean} exportFormat true if the megolm keys are in export format
* (ie, they lack an ed25519 signature)
* @param {Object} [extraSessionData={}] any other data to be include with the session
*/
OlmDevice.prototype.addInboundGroupSession = async function(
roomId, senderKey, forwardingCurve25519KeyChain,
sessionId, sessionKey, keysClaimed,
exportFormat,
exportFormat, extraSessionData = {},
) {
await this._cryptoStore.doTxn(
'readwrite', [
@@ -1043,12 +1044,12 @@ OlmDevice.prototype.addInboundGroupSession = async function(
" with first index " + session.first_known_index(),
);
const sessionData = {
const sessionData = Object.assign({}, extraSessionData, {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
};
});
this._cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
@@ -1224,6 +1225,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
forwardingCurve25519KeyChain: (
sessionData.forwardingCurve25519KeyChain || []
),
untrusted: sessionData.untrusted,
};
},
);
+10
View File
@@ -292,6 +292,11 @@ export class SecretStorage extends EventEmitter {
}
}
if (Object.keys(keys).length === 0) {
throw new Error(`Could not decrypt ${name} because none of ` +
`the keys it is encrypted with are for a supported algorithm`);
}
let keyId;
let decryption;
try {
@@ -368,6 +373,7 @@ export class SecretStorage extends EventEmitter {
const requestId = this._baseApis.makeTxnId();
const requestControl = this._requests[requestId] = {
name,
devices,
};
const promise = new Promise((resolve, reject) => {
@@ -531,6 +537,10 @@ export class SecretStorage extends EventEmitter {
return;
}
logger.log(
`Successfully received secret ${requestControl.name} ` +
`from ${deviceInfo.deviceId}`,
);
requestControl.resolve(content.secret);
}
}
+9 -4
View File
@@ -1015,7 +1015,7 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
// with them, which means that they will have announced any new devices via
// device_lists in their /sync response. This cache should then be maintained
// using all the device_lists changes and left fields.
// See https://github.com/vector-im/riot-web/issues/2305 for details.
// See https://github.com/vector-im/element-web/issues/2305 for details.
const devices = await this._crypto.downloadKeys(roomMembers, false);
const blocked = {};
// remove any blocked devices
@@ -1109,7 +1109,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
//
// then, if the key turns up while decryption is in progress (and
// decryption fails), we will schedule a retry.
// (fixes https://github.com/vector-im/riot-web/issues/5001)
// (fixes https://github.com/vector-im/element-web/issues/5001)
this._addEventToPendingList(event);
let res;
@@ -1201,6 +1201,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
senderCurve25519Key: res.senderKey,
claimedEd25519Key: res.keysClaimed.ed25519,
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
untrusted: res.untrusted,
};
};
@@ -1548,8 +1549,11 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
* @inheritdoc
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
* @param {object} [opts={}] options for the import
* @param {boolean} [opts.untrusted] whether the key should be considered as untrusted
* @param {string} [opts.source] where the key came from
*/
MegolmDecryption.prototype.importRoomKey = function(session) {
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
return this._olmDevice.addInboundGroupSession(
session.room_id,
session.sender_key,
@@ -1558,8 +1562,9 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
session.session_key,
session.sender_claimed_keys,
true,
opts.untrusted ? { untrusted: opts.untrusted } : {},
).then(() => {
if (this._crypto.backupInfo) {
if (this._crypto.backupInfo && opts.source !== "backup") {
// don't wait for it to complete
this._crypto.backupGroupSession(
session.room_id,
+458 -291
View File
@@ -34,11 +34,11 @@ import {DeviceInfo} from "./deviceinfo";
import * as algorithms from "./algorithms";
import {
CrossSigningInfo,
CrossSigningLevel,
DeviceTrustLevel,
UserTrustLevel,
createCryptoStoreCacheCallbacks,
} from './CrossSigning';
import {EncryptionSetupBuilder} from "./EncryptionSetup";
import {SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage} from './SecretStorage';
import {OutgoingRoomKeyRequestManager} from './OutgoingRoomKeyRequestManager';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
@@ -49,11 +49,10 @@ import {
} from './verification/QRCode';
import {SAS} from './verification/SAS';
import {keyFromPassphrase} from './key_passphrase';
import {encodeRecoveryKey} from './recoverykey';
import {encodeRecoveryKey, decodeRecoveryKey} from './recoverykey';
import {VerificationRequest} from "./verification/request/VerificationRequest";
import {InRoomChannel, InRoomRequests} from "./verification/request/InRoomChannel";
import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDeviceChannel";
import * as httpApi from "../http-api";
import {IllegalMethod} from "./verification/IllegalMethod";
import {KeySignatureUploadError} from "../errors";
import {decryptAES, encryptAES} from './aes';
@@ -407,14 +406,12 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
/**
* Checks whether cross signing:
* - is enabled on this account
* - is trusted by this device
* - has private keys stored in secret storage
* and that the account has a secret storage key
* - is enabled on this account and trusted by this device
* - has private keys either cached locally or stored in secret storage
*
* If this function returns false, bootstrapSecretStorage() can be used
* If this function returns false, bootstrapCrossSigning() can be used
* to fix things such that it returns true. That is to say, after
* bootstrapSecretStorage() completes sucessfully, this function should
* bootstrapCrossSigning() completes successfully, this function should
* return true.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
@@ -423,24 +420,179 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
*/
Crypto.prototype.isCrossSigningReady = async function() {
const publicKeysOnDevice = this._crossSigningInfo.getId();
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
this._secretStorage,
const privateKeysExistSomewhere = (
await this._crossSigningInfo.isStoredInKeyCache() ||
await this._crossSigningInfo.isStoredInSecretStorage(
this._secretStorage,
)
);
const secretStorageKeyInAccount = await this._secretStorage.hasKey();
return (
return !!(
publicKeysOnDevice &&
privateKeysInStorage &&
secretStorageKeyInAccount
privateKeysExistSomewhere
);
};
/**
* Checks whether secret storage:
* - is enabled on this account
* - is storing cross-signing private keys
* - is storing session backup key (if enabled)
*
* If this function returns false, bootstrapSecretStorage() can be used
* to fix things such that it returns true. That is to say, after
* bootstrapSecretStorage() completes successfully, this function should
* return true.
*
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @return {bool} True if secret storage is ready to be used on this device
*/
Crypto.prototype.isSecretStorageReady = async function() {
const secretStorageKeyInAccount = await this._secretStorage.hasKey();
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
this._secretStorage,
);
const sessionBackupInStorage = (
!this._baseApis.getKeyBackupEnabled() ||
this._baseApis.isKeyBackupKeyStored()
);
return !!(
secretStorageKeyInAccount &&
privateKeysInStorage &&
sessionBackupInStorage
);
};
/**
* Bootstrap Secure Secret Storage if needed by creating a default key and
* signing it with the cross-signing master key. If everything is already set
* up, then no changes are made, so this is safe to run to ensure secret storage
* is ready for use.
* Bootstrap cross-signing by creating keys if needed. If everything is already
* set up, then no changes are made, so this is safe to run to ensure
* cross-signing is ready for use.
*
* This function:
* - creates new cross-signing keys if they are not found locally cached nor in
* secret storage (if it has been setup)
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {function} opts.authUploadDeviceSigningKeys Function
* called to await an interactive auth flow when uploading device signing keys.
* @param {bool} [opts.setupNewCrossSigning] Optional. Reset even if keys
* already exist.
* Args:
* {function} A function that makes the request requiring auth. Receives the
* auth data as an object. Can be called multiple times, first with an empty
* authDict, to obtain the flows.
*/
Crypto.prototype.bootstrapCrossSigning = async function({
authUploadDeviceSigningKeys,
setupNewCrossSigning,
} = {}) {
logger.log("Bootstrapping cross-signing");
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
const builder = new EncryptionSetupBuilder(
this._baseApis.store.accountData,
delegateCryptoCallbacks,
);
const crossSigningInfo = new CrossSigningInfo(
this._userId,
builder.crossSigningCallbacks,
builder.crossSigningCallbacks,
);
// Reset the cross-signing keys
const resetCrossSigning = async () => {
crossSigningInfo.resetKeys();
// Sign master key with device key
await this._signObject(crossSigningInfo.keys.master);
// Store auth flow helper function, as we need to call it when uploading
// to ensure we handle auth errors properly.
builder.addCrossSigningKeys(authUploadDeviceSigningKeys, crossSigningInfo.keys);
// Cross-sign own device
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
const deviceSignature = await crossSigningInfo.signDevice(this._userId, device);
builder.addKeySignature(this._userId, this._deviceId, deviceSignature);
// Sign message key backup with cross-signing master key
if (this.backupInfo) {
await crossSigningInfo.signObject(this.backupInfo.auth_data, "master");
builder.addSessionBackup(this.backupInfo);
}
};
const publicKeysOnDevice = this._crossSigningInfo.getId();
const privateKeysInCache = await this._crossSigningInfo.isStoredInKeyCache();
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
this._secretStorage,
);
const privateKeysExistSomewhere = (
privateKeysInCache ||
privateKeysInStorage
);
if (!privateKeysExistSomewhere || setupNewCrossSigning) {
logger.log(
"Cross-signing private keys not found locally or in secret storage, " +
"creating new keys",
);
// If a user has multiple devices, it important to only call bootstrap
// as part of some UI flow (and not silently during startup), as they
// may have setup cross-signing on a platform which has not saved keys
// to secret storage, and this would reset them. In such a case, you
// should prompt the user to verify any existing devices first (and
// request private keys from those devices) before calling bootstrap.
await resetCrossSigning();
} else if (publicKeysOnDevice && privateKeysInCache) {
logger.log(
"Cross-signing public keys trusted and private keys found locally",
);
} else if (privateKeysInStorage) {
logger.log(
"Cross-signing private keys not found locally, but they are available " +
"in secret storage, reading storage and caching locally",
);
await this.checkOwnCrossSigningTrust();
}
// Assuming no app-supplied callback, default to storing new private keys in
// secret storage if it exists. If it does not, it is assumed this will be
// done as part of setting up secret storage later.
const crossSigningPrivateKeys = builder.crossSigningCallbacks.privateKeys;
if (
crossSigningPrivateKeys.size &&
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys
) {
const secretStorage = new SecretStorage(
builder.accountDataClientAdapter,
builder.ssssCryptoCallbacks);
if (await secretStorage.hasKey()) {
logger.log("Storing new cross-signing private keys in secret storage");
// This is writing to in-memory account data in
// builder.accountDataClientAdapter so won't fail
await CrossSigningInfo.storeInSecretStorage(
crossSigningPrivateKeys,
secretStorage,
);
}
}
const operation = builder.buildOperation();
await operation.apply(this);
// This persists private keys and public keys as trusted,
// only do this if apply succeeded for now as retry isn't in place yet
await builder.persist(this);
logger.log("Cross-signing ready");
};
/**
* Bootstrap Secure Secret Storage if needed by creating a default key. If everything is
* already set up, then no changes are made, so this is safe to run to ensure secret
* storage is ready for use.
*
* This function
* - creates a new Secure Secret Storage key if no default key exists
@@ -450,11 +602,8 @@ Crypto.prototype.isCrossSigningReady = async function() {
* - migrates Secure Secret Storage to use the latest algorithm, if an outdated
* algorithm is found
*
* @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
* called to await an interactive auth flow when uploading device signing keys.
* Args:
* {function} A function that makes the request requiring auth. Receives the
* auth data as an object.
* The Secure Secret Storage API is currently UNSTABLE and may change without notice.
*
* @param {function} [opts.createSecretStorageKey] Optional. Function
* called to await a secret storage key creation flow.
* Returns:
@@ -475,7 +624,6 @@ Crypto.prototype.isCrossSigningReady = async function() {
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
*/
Crypto.prototype.bootstrapSecretStorage = async function({
authUploadDeviceSigningKeys,
createSecretStorageKey = async () => ({ }),
keyBackupInfo,
setupNewKeyBackup,
@@ -483,42 +631,19 @@ Crypto.prototype.bootstrapSecretStorage = async function({
getKeyBackupPassphrase,
} = {}) {
logger.log("Bootstrapping Secure Secret Storage");
// Create cross-signing keys if they don't exist, as we want to sign the SSSS default
// key with the cross-signing master key. The cross-signing master key is also used
// to verify the signature on the SSSS default key when adding secrets, so we
// effectively need it for both reading and writing secrets.
const crossSigningPrivateKeys = {};
// If we happen to reset cross-signing keys here, then we want access to the
// cross-signing private keys, but only for the scope of this method, so we
// use temporary callbacks to weave them through the various APIs.
const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks);
const delegateCryptoCallbacks = this._baseApis._cryptoCallbacks;
const builder = new EncryptionSetupBuilder(
this._baseApis.store.accountData,
delegateCryptoCallbacks,
);
const secretStorage = new SecretStorage(
builder.accountDataClientAdapter,
builder.ssssCryptoCallbacks,
);
// the ID of the new SSSS key, if we create one
let newKeyId = null;
// cache SSSS keys so that we don't need to constantly pester the user about it
const ssssKeys = {};
this._baseApis._cryptoCallbacks.getSecretStorageKey =
async ({keys}, name) => {
// if we already have a key that works, return it
for (const keyId of Object.keys(keys)) {
if (ssssKeys[keyId]) {
return [keyId, ssssKeys[keyId]];
}
}
// otherwise, prompt the user and cache it
const key = await appCallbacks.getSecretStorageKey({keys}, name);
if (key) {
const [keyId, keyData] = key;
ssssKeys[keyId] = keyData;
}
return key;
};
// create a new SSSS key and set it as default
const createSSSS = async (opts, privateKey) => {
opts = opts || {};
@@ -526,28 +651,17 @@ Crypto.prototype.bootstrapSecretStorage = async function({
opts.key = privateKey;
}
const keyId = await this.addSecretStorageKey(
const keyId = await secretStorage.addKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
);
await this.setDefaultSecretStorageKeyId(keyId);
if (privateKey) {
// cache the private key so that we can access it again
ssssKeys[keyId] = privateKey;
// make the private key available to encrypt 4S secrets
builder.ssssCryptoCallbacks.addPrivateKey(keyId, privateKey);
}
return keyId;
};
// reset the cross-signing keys
const resetCrossSigning = async () => {
this._baseApis._cryptoCallbacks.saveCrossSigningKeys =
keys => Object.assign(crossSigningPrivateKeys, keys);
this._baseApis._cryptoCallbacks.getCrossSigningKey =
name => crossSigningPrivateKeys[name];
await this.resetCrossSigningKeys(
CrossSigningLevel.MASTER,
{ authUploadDeviceSigningKeys },
);
await secretStorage.setDefaultKeyId(keyId);
return keyId;
};
const ensureCanCheckPassphrase = async (keyId, keyInfo) => {
@@ -557,186 +671,159 @@ Crypto.prototype.bootstrapSecretStorage = async function({
);
if (key) {
const keyData = key[1];
ssssKeys[keyId] = keyData;
builder.ssssCryptoCallbacks.addPrivateKey(keyId, keyData);
const {iv, mac} = await SecretStorage._calculateKeyCheck(keyData);
keyInfo.iv = iv;
keyInfo.mac = mac;
await this._baseApis.setAccountData(
await builder.setAccountData(
`m.secret_storage.key.${keyId}`, keyInfo,
);
}
}
};
try {
const oldSSSSKey = await this.getSecretStorageKey();
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
const decryptionKeys =
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
const inStorage = !setupNewSecretStorage && decryptionKeys;
const oldSSSSKey = await this.getSecretStorageKey();
const [oldKeyId, oldKeyInfo] = oldSSSSKey || [null, null];
const storageExists = (
!setupNewSecretStorage &&
oldKeyInfo &&
oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES
);
if (!inStorage && !keyBackupInfo) {
// either we don't have anything, or we've been asked to restart
// from scratch
logger.log(
"Cross-signing private keys not found in secret storage, " +
"creating new keys",
);
if (!storageExists && !keyBackupInfo) {
// either we don't have anything, or we've been asked to restart
// from scratch
logger.log(
"Secret storage does not exist, creating new storage key",
);
await resetCrossSigning();
// if we already have a usable default SSSS key and aren't resetting
// SSSS just use it. otherwise, create a new one
// Note: we leave the old SSSS key in place: there could be other
// secrets using it, in theory. We could move them to the new key but a)
// that would mean we'd need to prompt for the old passphrase, and b)
// it's not clear that would be the right thing to do anyway.
const { keyInfo, privateKey } = await createSecretStorageKey();
newKeyId = await createSSSS(keyInfo, privateKey);
} else if (!storageExists && keyBackupInfo) {
// we have an existing backup, but no SSSS
logger.log("Secret storage does not exist, using key backup key");
if (
setupNewSecretStorage ||
!oldKeyInfo ||
oldKeyInfo.algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES
) {
// if we already have a usable default SSSS key and aren't resetting SSSS just use it.
// otherwise, create a new one
// Note: we leave the old SSSS key in place: there could be other secrets using it, in theory.
// We could move them to the new key but a) that would mean we'd need to prompt for the old
// passphrase, and b) it's not clear that would be the right thing to do anyway.
const { keyInfo, privateKey } = await createSecretStorageKey();
newKeyId = await createSSSS(keyInfo, privateKey);
}
// if we have the backup key already cached, use it; otherwise use the
// callback to prompt for the key
const backupKey = await this.getSessionBackupPrivateKey() ||
await getKeyBackupPassphrase();
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
}
} else if (!inStorage && keyBackupInfo) {
// we have an existing backup, but no SSSS
// create a new SSSS key and use the backup key as the new SSSS key
const opts = {};
logger.log("Secret storage default key not found, using key backup key");
// if we have the backup key already cached, use it; otherwise use the
// callback to prompt for the key
const backupKey = await this.getSessionBackupPrivateKey() ||
await getKeyBackupPassphrase();
// create new cross-signing keys
await resetCrossSigning();
// create a new SSSS key and use the backup key as the new SSSS key
const opts = {};
if (
keyBackupInfo.auth_data.private_key_salt &&
keyBackupInfo.auth_data.private_key_iterations
) {
opts.passphrase = {
algorithm: "m.pbkdf2",
iterations: keyBackupInfo.auth_data.private_key_iterations,
salt: keyBackupInfo.auth_data.private_key_salt,
bits: 256,
};
}
newKeyId = await createSSSS(opts, backupKey);
// store the backup key in secret storage
await this.storeSecret(
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
);
// The backup is trusted because the user provided the private key.
// Sign the backup with the cross signing key so the key backup can
// be trusted via cross-signing.
logger.log("Adding cross signing signature to key backup");
await this._crossSigningInfo.signObject(
keyBackupInfo.auth_data, "master",
);
await this._baseApis._http.authedRequest(
undefined, "PUT", "/room_keys/version/" + keyBackupInfo.version,
undefined, keyBackupInfo,
{prefix: httpApi.PREFIX_UNSTABLE},
);
} else if (!this._crossSigningInfo.getId()) {
// we have SSSS, but we don't know if the server's cross-signing
// keys should be trusted
logger.log("Cross-signing private keys found in secret storage");
// fetch the private keys and set up our local copy of the keys for
// use
await this.checkOwnCrossSigningTrust();
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
// make sure that the default key has the information needed to
// check the passphrase
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
}
} else {
// we have SSSS and we cross-signing is already set up
logger.log("Cross signing keys are present in secret storage");
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
// make sure that the default key has the information needed to
// check the passphrase
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
}
if (
keyBackupInfo.auth_data.private_key_salt &&
keyBackupInfo.auth_data.private_key_iterations
) {
opts.passphrase = {
algorithm: "m.pbkdf2",
iterations: keyBackupInfo.auth_data.private_key_iterations,
salt: keyBackupInfo.auth_data.private_key_salt,
bits: 256,
};
}
// If cross-signing keys were reset, store them in Secure Secret Storage.
// This is done in a separate step so we can ensure secret storage has its
// own key first.
// XXX: We need to think about how to re-do these steps if they fail.
// See also https://github.com/vector-im/riot-web/issues/11635
if (Object.keys(crossSigningPrivateKeys).length) {
logger.log("Storing cross-signing private keys in secret storage");
// Assuming no app-supplied callback, default to storing in SSSS.
if (!appCallbacks.saveCrossSigningKeys) {
await CrossSigningInfo.storeInSecretStorage(
crossSigningPrivateKeys,
this._secretStorage,
);
}
}
newKeyId = await createSSSS(opts, backupKey);
if (setupNewKeyBackup && !keyBackupInfo) {
const info = await this._baseApis.prepareKeyBackupVersion(
null /* random key */,
{ secureSecretStorage: true },
);
await this._baseApis.createKeyBackupVersion(info);
}
// store the backup key in secret storage
await secretStorage.store(
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
);
// Call `getCrossSigningKey` for side effect of caching private keys for
// future gossiping to other devices if enabled via app level callbacks.
if (this._crossSigningInfo._cacheCallbacks) {
for (const type of ["self_signing", "user_signing"]) {
logger.log(`Cache ${type} cross-signing private key locally`);
await this._crossSigningInfo.getCrossSigningKey(type);
}
}
// The backup is trusted because the user provided the private key.
// Sign the backup with the cross-signing key so the key backup can
// be trusted via cross-signing.
logger.log("Adding cross signing signature to key backup");
await this._crossSigningInfo.signObject(
keyBackupInfo.auth_data, "master",
);
builder.addSessionBackup(keyBackupInfo);
} else {
// 4S is already set up
logger.log("Secret storage exists");
// and likewise for the session backup key
const sessionBackupKey = await this.getSecret('m.megolm_backup.v1');
if (sessionBackupKey) {
logger.info("Got session backup key from secret storage: caching");
// fix up the backup key if it's in the wrong format, and replace
// in secret storage
const fixedBackupKey = fixBackupKey(sessionBackupKey);
if (fixedBackupKey) {
await this.storeSecret(
"m.megolm_backup.v1", fixedBackupKey, [newKeyId || oldKeyId],
);
}
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
fixedBackupKey || sessionBackupKey,
));
await this.storeSessionBackupPrivateKey(decodedBackupKey);
if (oldKeyInfo && oldKeyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
// make sure that the default key has the information needed to
// check the passphrase
await ensureCanCheckPassphrase(oldKeyId, oldKeyInfo);
}
} finally {
// Restore the original callbacks. NB. we must do this by manipulating
// the same object since the CrossSigning class has a reference to the
// object, so if we assign the object here then our callbacks will change
// but the instances of the CrossSigning class will be left with our
// random, otherwise dead closures.
for (const cb of Object.keys(this._baseApis._cryptoCallbacks)) {
delete this._baseApis._cryptoCallbacks[cb];
}
Object.assign(this._baseApis._cryptoCallbacks, appCallbacks);
}
// If we have cross-signing private keys cached, store them in secret
// storage if they are not there already.
if (
!this._baseApis._cryptoCallbacks.saveCrossSigningKeys &&
await this.isCrossSigningReady() &&
(newKeyId || !await this._crossSigningInfo.isStoredInSecretStorage(secretStorage))
) {
logger.log("Copying cross-signing private keys from cache to secret storage");
const crossSigningPrivateKeys =
await this._crossSigningInfo.getCrossSigningKeysFromCache();
// This is writing to in-memory account data in
// builder.accountDataClientAdapter so won't fail
await CrossSigningInfo.storeInSecretStorage(
crossSigningPrivateKeys,
secretStorage,
);
}
if (setupNewKeyBackup && !keyBackupInfo) {
logger.log("Creating new message key backup version");
const info = await this._baseApis.prepareKeyBackupVersion(
null /* random key */,
// don't write to secret storage, as it will write to this._secretStorage.
// Here, we want to capture all the side-effects of bootstrapping,
// and want to write to the local secretStorage object
{ secureSecretStorage: false },
);
// write the key ourselves to 4S
const privateKey = decodeRecoveryKey(info.recovery_key);
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
// create keyBackupInfo object to add to builder
const data = {
algorithm: info.algorithm,
auth_data: info.auth_data,
};
// sign with cross-sign master key
await this._crossSigningInfo.signObject(data.auth_data, "master");
// sign with the device fingerprint
await this._signObject(data.auth_data);
builder.addSessionBackup(data);
}
// Cache the session backup key
const sessionBackupKey = await secretStorage.get('m.megolm_backup.v1');
if (sessionBackupKey) {
logger.info("Got session backup key from secret storage: caching");
// fix up the backup key if it's in the wrong format, and replace
// in secret storage
const fixedBackupKey = fixBackupKey(sessionBackupKey);
if (fixedBackupKey) {
await secretStorage.store("m.megolm_backup.v1",
fixedBackupKey, [newKeyId || oldKeyId],
);
}
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(
fixedBackupKey || sessionBackupKey,
));
await builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
}
const operation = builder.buildOperation();
await operation.apply(this);
// this persists private keys and public keys as trusted,
// only do this if apply succeeded for now as retry isn't in place yet
await builder.persist(this);
logger.log("Secure Secret Storage ready");
};
@@ -897,57 +984,6 @@ Crypto.prototype.checkCrossSigningPrivateKey = function(privateKey, expectedPubl
}
};
/**
* Generate new cross-signing keys.
*
* @param {CrossSigningLevel} [level] the level of cross-signing to reset. New
* keys will be created for the given level and below. Defaults to
* regenerating all keys.
* @param {function} [opts.authUploadDeviceSigningKeys] Optional. Function
* called to await an interactive auth flow when uploading device signing keys.
* Args:
* {function} A function that makes the request requiring auth. Receives the
* auth data as an object.
*/
Crypto.prototype.resetCrossSigningKeys = async function(level, {
authUploadDeviceSigningKeys = async func => await func(),
} = {}) {
logger.info(`Resetting cross-signing keys at level ${level}`);
// Copy old keys (usually empty) in case we need to revert
const oldKeys = Object.assign({}, this._crossSigningInfo.keys);
try {
await this._crossSigningInfo.resetKeys(level);
await this._signObject(this._crossSigningInfo.keys.master);
// send keys to server first before storing as trusted locally
// to ensure upload succeeds
const keys = {};
for (const [name, key] of Object.entries(this._crossSigningInfo.keys)) {
keys[name + "_key"] = key;
}
await authUploadDeviceSigningKeys(async authDict => {
await this._baseApis.uploadDeviceSigningKeys(authDict, keys);
});
// write a copy locally so we know these are trusted keys
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
},
);
} catch (e) {
// If anything failed here, revert the keys so we know to try again from the start
// next time.
logger.error("Resetting cross-signing keys failed, revert to previous keys", e);
this._crossSigningInfo.keys = oldKeys;
throw e;
}
this._baseApis.emit("crossSigning.keysChanged", {});
await this._afterCrossSigningLocalKeyChange();
logger.info("Cross-signing key reset complete");
};
/**
* Run various follow-up actions after cross-signing keys have changed locally
* (either by resetting the keys for the account or by getting them from secret
@@ -1275,6 +1311,20 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) {
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
// Try to cache the self-signing private key as a side-effect
let signing = null;
try {
const ret = await this._crossSigningInfo.getCrossSigningKey(
"self_signing", newCrossSigning.getId("self_signing"),
);
signing = ret[1];
logger.info(
"Got matching private key from callback for new public self-signing key",
);
} finally {
if (signing) signing.free();
}
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
const signedDevice = await this._crossSigningInfo.signDevice(
this._userId, device,
@@ -1283,6 +1333,20 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
}
if (oldUserSigningId !== newCrossSigning.getId("user_signing")) {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
// Try to cache the user-signing private key as a side-effect
let signing = null;
try {
const ret = await this._crossSigningInfo.getCrossSigningKey(
"user_signing", newCrossSigning.getId("user_signing"),
);
signing = ret[1];
logger.info(
"Got matching private key from callback for new public user-signing key",
);
} finally {
if (signing) signing.free();
}
}
if (masterChanged) {
@@ -1453,6 +1517,12 @@ Crypto.prototype._checkAndStartKeyBackup = async function() {
);
this._baseApis.disableKeyBackup();
this._baseApis.enableKeyBackup(backupInfo);
// We're now using a new backup, so schedule all the keys we have to be
// uploaded to the new backup. This is a bit of a workaround to upload
// keys to a new backup in *most* cases, but it won't cover all cases
// because we don't remember what backup version we uploaded keys to:
// see https://github.com/vector-im/element-web/issues/14833
await this.scheduleAllGroupSessionsForBackup();
} else {
logger.log("Backup version " + backupInfo.version + " still current");
}
@@ -2052,9 +2122,18 @@ Crypto.prototype.setDeviceVerification = async function(
// do cross-signing
if (verified && userId === this._userId) {
logger.info("Own device " + deviceId + " marked verified: signing");
const device = await this._crossSigningInfo.signDevice(
userId, DeviceInfo.fromStorage(dev, deviceId),
);
// Signing only needed if other device not already signed
let device;
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
if (deviceTrust.isCrossSigningVerified()) {
logger.log(`Own device ${deviceId} already cross-signing verified`);
} else {
device = await this._crossSigningInfo.signDevice(
userId, DeviceInfo.fromStorage(dev, deviceId),
);
}
if (device) {
const upload = async ({shouldEmit}) => {
logger.info("Uploading signature for " + deviceId);
@@ -2090,6 +2169,10 @@ Crypto.prototype.findVerificationRequestDMInProgress = function(roomId) {
return this._inRoomVerificationRequests.findRequestInProgress(roomId);
};
Crypto.prototype.getVerificationRequestsToDeviceInProgress = function(userId) {
return this._toDeviceVerificationRequests.getRequestsInProgress(userId);
};
Crypto.prototype.requestVerificationDM = function(userId, roomId) {
const existingRequest = this._inRoomVerificationRequests.
findRequestInProgress(roomId);
@@ -2238,11 +2321,16 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
const forwardingChain = event.getForwardingCurve25519KeyChain();
if (forwardingChain.length > 0) {
// we got this event from somewhere else
// we got the key this event from somewhere else
// TODO: check if we can trust the forwarders.
return null;
}
if (event.isKeySourceUntrusted()) {
// we got the key for this event from a source that we consider untrusted
return null;
}
// senderKey is the Curve25519 identity key of the device which the event
// was sent from. In the case of Megolm, it's actually the Curve25519
// identity key of the device which set up the Megolm session.
@@ -2281,6 +2369,76 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
return device;
};
/**
* Get information about the encryption of an event
*
* @param {module:models/event.MatrixEvent} event event to be checked
*
* @return {object} An object with the fields:
* - encrypted: whether the event is encrypted (if not encrypted, some of the
* other properties may not be set)
* - senderKey: the sender's key
* - algorithm: the algorithm used to encrypt the event
* - authenticated: whether we can be sure that the owner of the senderKey
* sent the event
* - sender: the sender's device information, if available
* - mismatchedSender: if the event's ed25519 and curve25519 keys don't match
* (only meaningful if `sender` is set)
*/
Crypto.prototype.getEventEncryptionInfo = function(event) {
const ret = {};
ret.senderKey = event.getSenderKey();
ret.algorithm = event.getWireContent().algorithm;
if (!ret.senderKey || !ret.algorithm) {
ret.encrypted = false;
return ret;
}
ret.encrypted = true;
const forwardingChain = event.getForwardingCurve25519KeyChain();
if (forwardingChain.length > 0 || event.isKeySourceUntrusted()) {
// we got the key this event from somewhere else
// TODO: check if we can trust the forwarders.
ret.authenticated = false;
} else {
ret.authenticated = true;
}
// senderKey is the Curve25519 identity key of the device which the event
// was sent from. In the case of Megolm, it's actually the Curve25519
// identity key of the device which set up the Megolm session.
ret.sender = this._deviceList.getDeviceByIdentityKey(
ret.algorithm, ret.senderKey,
);
// so far so good, but now we need to check that the sender of this event
// hadn't advertised someone else's Curve25519 key as their own. We do that
// by checking the Ed25519 claimed by the event (or, in the case of megolm,
// the event which set up the megolm session), to check that it matches the
// fingerprint of the purported sending device.
//
// (see https://github.com/vector-im/vector-web/issues/2215)
const claimedKey = event.getClaimedEd25519Key();
if (!claimedKey) {
logger.warn("Event " + event.getId() + " claims no ed25519 key: " +
"cannot verify sending device");
ret.mismatchedSender = true;
}
if (ret.sender && claimedKey !== ret.sender.getFingerprint()) {
logger.warn(
"Event " + event.getId() + " claims ed25519 key " + claimedKey +
"but sender device has key " + ret.sender.getFingerprint());
ret.mismatchedSender = true;
}
return ret;
};
/**
* Forces the current outbound group session to be discarded such
* that another one will be created next time an event is sent.
@@ -2349,7 +2507,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
// after all the in-memory state (_roomEncryptors and _roomList) has been updated
// to avoid races when calling this method multiple times. Hence keep a hold of the promise.
let storeConfigPromise = null;
if(!existingConfig) {
if (!existingConfig) {
storeConfigPromise = this._roomList.setRoomEncryption(roomId, config);
}
@@ -2379,10 +2537,10 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
await this.trackRoomDevices(roomId);
// TODO: this flag is only not used from MatrixClient::setRoomEncryption
// which is never used (inside riot at least)
// which is never used (inside Element at least)
// but didn't want to remove it as it technically would
// be a breaking change.
if(!this.inhibitDeviceQuery) {
if (!this.inhibitDeviceQuery) {
this._deviceList.refreshOutdatedDeviceLists();
}
} else {
@@ -2525,7 +2683,7 @@ Crypto.prototype.importRoomKeys = function(keys, opts = {}) {
}
const alg = this._getRoomDecryptor(key.room_id, key.algorithm);
return alg.importRoomKey(key).finally((r) => {
return alg.importRoomKey(key, opts).finally((r) => {
successes++;
if (opts.progressCallback) { updateProgress(); }
});
@@ -2687,7 +2845,8 @@ Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions requiring a backup.
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
* (which will be equal to the number of sessions in the store).
*/
Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
await this._cryptoStore.doTxn(
@@ -2710,6 +2869,14 @@ Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
return remaining;
};
/**
* Counts the number of end to end session keys that are waiting to be backed up
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
*/
Crypto.prototype.countSessionsNeedingBackup = function() {
return this._cryptoStore.countSessionsNeedingBackup();
};
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
@@ -2944,7 +3111,7 @@ Crypto.prototype.onSyncCompleted = async function(syncData) {
// we don't start uploading one-time keys until we've caught up with
// to-device messages, to help us avoid throwing away one-time-keys that we
// are about to receive messages for
// (https://github.com/vector-im/riot-web/issues/2782).
// (https://github.com/vector-im/element-web/issues/2782).
if (!syncData.catchingUp) {
_maybeUploadOneTimeKeys(this);
this._processReceivedRoomKeyRequests();
@@ -131,7 +131,7 @@ export class Backend {
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
if(!cursor) {
if (!cursor) {
// no match found
callback(null);
return;
+2 -91
View File
@@ -25,8 +25,7 @@ import {EventEmitter} from 'events';
import {logger} from '../../logger';
import {DeviceInfo} from '../deviceinfo';
import {newTimeoutError} from "./Error";
import {CrossSigningInfo} from "../CrossSigning";
import {decodeBase64} from "../olmlib";
import {requestKeysDuringVerification} from "../CrossSigning";
const timeoutException = new Error("Verification timed out");
@@ -79,8 +78,6 @@ 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,
@@ -198,93 +195,7 @@ export class VerificationBase extends EventEmitter {
if (!this._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
return requestKeysDuringVerification(this._baseApis, this.userId, this.deviceId);
}
}
+1 -1
View File
@@ -430,7 +430,7 @@ export class SAS extends Base {
try {
await this._sendMAC(olmSAS, macMethod);
resolve();
} catch(err) {
} catch (err) {
reject(err);
}
},
@@ -356,4 +356,12 @@ export class ToDeviceRequests {
}
}
}
getRequestsInProgress(userId) {
const requestsByTxnId = this._requestsByUserId.get(userId);
if (requestsByTxnId) {
return Array.from(requestsByTxnId.values()).filter(r => r.pending);
}
return [];
}
}
@@ -25,15 +25,17 @@ import {
} from "../Error";
import {QRCodeData, SCAN_QR_CODE_METHOD} from "../QRCode";
// the recommended amount of time before a verification request
// should be (automatically) cancelled without user interaction
// and ignored.
const VERIFICATION_REQUEST_TIMEOUT = 10 * 60 * 1000; //10m
// How long after the event's timestamp that the request times out
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
// How long after we receive the event that the request times out
const TIMEOUT_FROM_EVENT_RECEIPT = 2 * 60 * 1000; // 2 minutes
// to avoid almost expired verification notifications
// from showing a notification and almost immediately
// disappearing, also ignore verification requests that
// are this amount of time away from expiring.
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; //3s
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; // 3 seconds
export const EVENT_PREFIX = "m.key.verification.";
@@ -80,6 +82,9 @@ export class VerificationRequest extends EventEmitter {
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
this._qrCodeData = null;
// The timestamp when we received the request event from the other side
this._requestReceivedAt = null;
}
/**
@@ -165,12 +170,26 @@ export class VerificationRequest extends EventEmitter {
return this._chosenMethod;
}
calculateEventTimeout(event) {
let effectiveExpiresAt = this.channel.getTimestamp(event)
+ TIMEOUT_FROM_EVENT_TS;
if (this._requestReceivedAt && !this.initiatedByMe &&
this.phase <= PHASE_REQUESTED
) {
const expiresAtByReceipt = this._requestReceivedAt
+ TIMEOUT_FROM_EVENT_RECEIPT;
effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
}
return Math.max(0, effectiveExpiresAt - Date.now());
}
/** The current remaining amount of ms before the request should be automatically cancelled */
get timeout() {
const requestEvent = this._getEventByEither(REQUEST_TYPE);
if (requestEvent) {
const elapsed = Date.now() - this.channel.getTimestamp(requestEvent);
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
return this.calculateEventTimeout(requestEvent);
}
return 0;
}
@@ -735,7 +754,7 @@ export class VerificationRequest extends EventEmitter {
_setupTimeout(phase) {
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
phase === PHASE_REQUESTED && this.initiatedByMe;
phase === PHASE_REQUESTED;
if (shouldTimeout) {
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
@@ -754,7 +773,17 @@ export class VerificationRequest extends EventEmitter {
_cancelOnTimeout = () => {
try {
this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
if (this.initiatedByMe) {
this.cancel({
reason: "Other party didn't accept in time",
code: "m.timeout",
});
} else {
this.cancel({
reason: "User didn't accept in time",
code: "m.timeout",
});
}
} catch (err) {
logger.error("Error while cancelling verification request", err);
}
@@ -791,16 +820,8 @@ export class VerificationRequest extends EventEmitter {
if (!isLiveEvent) {
this._observeOnly = true;
}
// a timestamp is not provided on all to_device events
const timestamp = this.channel.getTimestamp(event);
if (Number.isFinite(timestamp)) {
const elapsed = Date.now() - timestamp;
// don't allow interaction on old requests
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)
) {
this._observeOnly = true;
}
if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) {
this._observeOnly = true;
}
}
@@ -819,6 +840,8 @@ export class VerificationRequest extends EventEmitter {
this._eventsByThem.delete(type);
}
}
// also remember when we received the request event
this._requestReceivedAt = Date.now();
}
}
+1 -1
View File
@@ -890,7 +890,7 @@ function getResponseContentType(response) {
try {
return parseContentType(contentType);
} catch(e) {
} catch (e) {
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
}
}
+1
View File
@@ -22,6 +22,7 @@ matrixcs.request(request);
utils.runPolyfills();
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
+8 -2
View File
@@ -113,13 +113,19 @@ export function setCryptoStoreFactory(fac) {
cryptoStoreFactory = fac;
}
interface ICreateClientOpts {
baseUrl: string;
idBaseUrl?: string;
store?: Store;
cryptoStore?: CryptoStore;
scheduler?: MatrixScheduler;
request?: Request;
userId?: string;
accessToken?: string;
identityServer?: any;
localTimeoutMs?: number;
useAuthorizationHeader?: boolean;
queryParams?: Record<string, unknown>;
}
/**
@@ -187,7 +193,7 @@ export function createClient(opts: ICreateClientOpts | string) {
* @param {requestCallback} callback The request callback.
*/
/**
/**
* The request callback interface for performing HTTP requests. This matches the
* API for the {@link https://github.com/request/request#requestoptions-callback|
* request NPM module}. The SDK will implement a callback which meets this
+2 -1
View File
@@ -647,7 +647,8 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
if (timeline1 === timeline2) {
// both events are in the same timeline - figure out their
// relative indices
let idx1, idx2;
let idx1;
let idx2;
const events = timeline1.getEvents();
for (let idx = 0; idx < events.length &&
(idx1 === undefined || idx2 === undefined); idx++) {
+30 -3
View File
@@ -87,7 +87,7 @@ export const MatrixEvent = function(
// amount of needless string duplication. This can save moderate amounts of
// memory (~10% on a 350MB heap).
// 'membership' at the event level (rather than the content level) is a legacy
// field that Riot never otherwise looks at, but it will still take up a lot
// field that Element never otherwise looks at, but it will still take up a lot
// of space if we don't intern it.
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => {
if (!event[prop]) {
@@ -144,6 +144,10 @@ export const MatrixEvent = function(
*/
this._forwardingCurve25519KeyChain = [];
/* where the decryption key is untrusted
*/
this._untrusted = null;
/* if we have a process decrypting this event, a Promise which resolves
* when it is finished. Normally null.
*/
@@ -160,12 +164,16 @@ export const MatrixEvent = function(
* so it can be easily accessed from the timeline.
*/
this.verificationRequest = null;
/* The txnId with which this event was sent if it was during this session,
allows for a unique ID which does not change when the event comes back down sync.
*/
this._txnId = null;
};
utils.inherits(MatrixEvent, EventEmitter);
utils.extend(MatrixEvent.prototype, {
/**
* Get the event_id for this event.
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
@@ -599,6 +607,7 @@ utils.extend(MatrixEvent.prototype, {
decryptionResult.claimedEd25519Key || null;
this._forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
this._untrusted = decryptionResult.untrusted || false;
},
/**
@@ -617,7 +626,7 @@ utils.extend(MatrixEvent.prototype, {
* @return {boolean} True if this event is encrypted.
*/
isEncrypted: function() {
return this.event.type === "m.room.encrypted";
return !this.isState() && this.event.type === "m.room.encrypted";
},
/**
@@ -689,6 +698,16 @@ utils.extend(MatrixEvent.prototype, {
return this._forwardingCurve25519KeyChain;
},
/**
* Whether the decryption key was obtained from an untrusted source. If so,
* we cannot verify the authenticity of the message.
*
* @return {boolean}
*/
isKeySourceUntrusted: function() {
return this._untrusted;
},
getUnsigned: function() {
return this.event.unsigned || {};
},
@@ -1070,6 +1089,14 @@ utils.extend(MatrixEvent.prototype, {
setVerificationRequest: function(request) {
this.verificationRequest = request;
},
setTxnId(txnId) {
this._txnId = txnId;
},
getTxnId() {
return this._txnId;
},
});
+3 -7
View File
@@ -20,7 +20,7 @@ limitations under the License.
*/
import {EventEmitter} from "events";
import {getHttpUriForMxc, getIdenticonUri} from "../content-repo";
import {getHttpUriForMxc} from "../content-repo";
import * as utils from "../utils";
/**
@@ -274,10 +274,6 @@ RoomMember.prototype.getAvatarUrl =
);
if (httpUrl) {
return httpUrl;
} else if (allowDefault) {
return getIdenticonUri(
baseUrl, this.userId, width, height,
);
}
return null;
};
@@ -286,9 +282,9 @@ RoomMember.prototype.getAvatarUrl =
* @return {string} the mxc avatar url
*/
RoomMember.prototype.getMxcAvatarUrl = function() {
if(this.events.member) {
if (this.events.member) {
return this.events.member.getDirectionalContent().avatar_url;
} else if(this.user) {
} else if (this.user) {
return this.user.avatarUrl;
}
return null;
+24 -17
View File
@@ -68,9 +68,7 @@ export function RoomState(roomId, oobMemberFlags = undefined) {
this.members = {
// userId: RoomMember
};
this.events = {
// eventType: { stateKey: MatrixEvent }
};
this.events = new Map(); // Map<eventType, Map<stateKey, MatrixEvent>>
this.paginationToken = null;
this._sentinels = {
@@ -211,14 +209,14 @@ RoomState.prototype.getSentinelMember = function(userId) {
* <code>undefined</code>, else a single event (or null if no match found).
*/
RoomState.prototype.getStateEvents = function(eventType, stateKey) {
if (!this.events[eventType]) {
if (!this.events.has(eventType)) {
// no match
return stateKey === undefined ? [] : null;
}
if (stateKey === undefined) { // return all values
return utils.values(this.events[eventType]);
return Array.from(this.events.get(eventType).values());
}
const event = this.events[eventType][stateKey];
const event = this.events.get(eventType).get(stateKey);
return event ? event : null;
};
@@ -238,9 +236,8 @@ RoomState.prototype.clone = function() {
const status = this._oobMemberFlags.status;
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
Object.values(this.events).forEach((eventsByStateKey) => {
const eventsForType = Object.values(eventsByStateKey);
copy.setStateEvents(eventsForType);
Array.from(this.events.values()).forEach((eventsByStateKey) => {
copy.setStateEvents(Array.from(eventsByStateKey.values()));
});
// Ugly hack: see above
@@ -276,8 +273,8 @@ RoomState.prototype.clone = function() {
*/
RoomState.prototype.setUnknownStateEvents = function(events) {
const unknownStateEvents = events.filter((event) => {
return this.events[event.getType()] === undefined ||
this.events[event.getType()][event.getStateKey()] === undefined;
return !this.events.has(event.getType()) ||
!this.events.get(event.getType()).has(event.getStateKey());
});
this.setStateEvents(unknownStateEvents);
@@ -306,6 +303,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
return;
}
const lastStateEvent = self._getStateEventMatching(event);
self._setStateEvent(event);
if (event.getType() === "m.room.member") {
_updateDisplayNameCache(
@@ -313,7 +311,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
);
_updateThirdPartyTokenCache(self, event);
}
self.emit("RoomState.events", event, self);
self.emit("RoomState.events", event, self, lastStateEvent);
});
// update higher level data structures. This needs to be done AFTER the
@@ -385,10 +383,15 @@ RoomState.prototype._getOrCreateMember = function(userId, event) {
};
RoomState.prototype._setStateEvent = function(event) {
if (this.events[event.getType()] === undefined) {
this.events[event.getType()] = {};
if (!this.events.has(event.getType())) {
this.events.set(event.getType(), new Map());
}
this.events[event.getType()][event.getStateKey()] = event;
this.events.get(event.getType()).set(event.getStateKey(), event);
};
RoomState.prototype._getStateEventMatching = function(event) {
if (!this.events.has(event.getType())) return null;
return this.events.get(event.getType()).get(event.getStateKey());
};
RoomState.prototype._updateMember = function(member) {
@@ -670,7 +673,7 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
const userPowerLevel = power_levels.users && power_levels.users[userId];
if (Number.isFinite(userPowerLevel)) {
powerLevel = userPowerLevel;
} else if(Number.isFinite(power_levels.users_default)) {
} else if (Number.isFinite(power_levels.users_default)) {
powerLevel = power_levels.users_default;
}
@@ -769,8 +772,12 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.events dictionary
* was updated.
* @param {MatrixEvent} prevEvent The event being replaced by the new state, if
* known. Note that this can differ from `getPrevContent()` on the new state event
* as this is the store's view of the last state, not the previous state provided
* by the server.
* @example
* matrixClient.on("RoomState.events", function(event, state){
* matrixClient.on("RoomState.events", function(event, state, prevEvent){
* var newStateEvent = event;
* });
*/
+12 -9
View File
@@ -23,7 +23,7 @@ limitations under the License.
import {EventEmitter} from "events";
import {EventTimelineSet} from "./event-timeline-set";
import {EventTimeline} from "./event-timeline";
import {getHttpUriForMxc, getIdenticonUri} from "../content-repo";
import {getHttpUriForMxc} from "../content-repo";
import * as utils from "../utils";
import {EventStatus, MatrixEvent} from "./event";
import {RoomMember} from "./room-member";
@@ -122,6 +122,10 @@ export function Room(roomId, client, myUserId, opts) {
opts = opts || {};
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
// In some cases, we add listeners for every displayed Matrix event, so it's
// common to have quite a few more than the default limit.
this.setMaxListeners(100);
this.reEmitter = new ReEmitter(this);
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
@@ -814,10 +818,6 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
return getHttpUriForMxc(
baseUrl, mainUrl, width, height, resizeMethod,
);
} else if (allowDefault) {
return getIdenticonUri(
baseUrl, this.roomId, width, height,
);
}
return null;
@@ -1793,8 +1793,9 @@ Room.prototype.addAccountData = function(events) {
if (event.getType() === "m.tag") {
this.addTags(event);
}
const lastEvent = this.accountData[event.getType()];
this.accountData[event.getType()] = event;
this.emit("Room.accountData", event, this);
this.emit("Room.accountData", event, this, lastEvent);
}
};
@@ -1899,14 +1900,14 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
// let's try to figure out who was here before
let leftNames = otherNames;
// if we didn't have heroes, try finding them in the room state
if(!leftNames.length) {
if (!leftNames.length) {
leftNames = room.currentState.getMembers().filter((m) => {
return m.userId !== userId &&
m.membership !== "invite" &&
m.membership !== "join";
}).map((m) => m.name);
}
if(leftNames.length) {
if (leftNames.length) {
return `Empty room (was ${memberNamesToRoomName(leftNames)})`;
} else {
return "Empty room";
@@ -1991,8 +1992,10 @@ function memberNamesToRoomName(names, count = (names.length + 1)) {
* @event module:client~MatrixClient#"Room.accountData"
* @param {event} event The account_data event
* @param {Room} room The room whose account_data was updated.
* @param {MatrixEvent} prevEvent The event being replaced by
* the new account data, if known.
* @example
* matrixClient.on("Room.accountData", function(event, room){
* matrixClient.on("Room.accountData", function(event, room, oldEvent){
* if (event.getType() === "m.room.colorscheme") {
* applyColorScheme(event.getContents());
* }
+10 -2
View File
@@ -129,7 +129,11 @@ User.prototype.setPresenceEvent = function(event) {
*/
User.prototype.setDisplayName = function(name) {
const oldName = this.displayName;
this.displayName = name;
if (typeof name === "string") {
this.displayName = name;
} else {
this.displayName = undefined;
}
if (name !== oldName) {
this._updateModifiedTime();
}
@@ -142,7 +146,11 @@ User.prototype.setDisplayName = function(name) {
* @param {string} name The new display name.
*/
User.prototype.setRawDisplayName = function(name) {
this.rawDisplayName = name;
if (typeof name === "string") {
this.rawDisplayName = name;
} else {
this.rawDisplayName = undefined;
}
};
+9 -27
View File
@@ -84,12 +84,15 @@ export function PushProcessor(client) {
// $glob: RegExp,
};
const matchingRuleFromKindSet = (ev, kindset, device) => {
const matchingRuleFromKindSet = (ev, kindset) => {
for (let ruleKindIndex = 0;
ruleKindIndex < RULEKINDS_IN_ORDER.length;
++ruleKindIndex) {
const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
const ruleset = kindset[kind];
if (!ruleset) {
continue;
}
for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
const rule = ruleset[ruleIndex];
@@ -97,7 +100,7 @@ export function PushProcessor(client) {
continue;
}
const rawrule = templateRuleToRaw(kind, rule, device);
const rawrule = templateRuleToRaw(kind, rule);
if (!rawrule) {
continue;
}
@@ -111,7 +114,7 @@ export function PushProcessor(client) {
return null;
};
const templateRuleToRaw = function(kind, tprule, device) {
const templateRuleToRaw = function(kind, tprule) {
const rawrule = {
'rule_id': tprule.rule_id,
'actions': tprule.actions,
@@ -153,19 +156,12 @@ export function PushProcessor(client) {
});
break;
}
if (device) {
rawrule.conditions.push({
'kind': 'device',
'profile_tag': device,
});
}
return rawrule;
};
const eventFulfillsCondition = function(cond, ev) {
const condition_functions = {
"event_match": eventFulfillsEventMatchCondition,
"device": eventFulfillsDeviceCondition,
"contains_display_name": eventFulfillsDisplayNameCondition,
"room_member_count": eventFulfillsRoomMemberCountCondition,
"sender_notification_permission": eventFulfillsSenderNotifPermCondition,
@@ -257,10 +253,6 @@ export function PushProcessor(client) {
return content.body.search(pat) > -1;
};
const eventFulfillsDeviceCondition = function(cond, ev) {
return false; // XXX: Allow a profile tag to be set for the web client instance
};
const eventFulfillsEventMatchCondition = function(cond, ev) {
if (!cond.key) {
return false;
@@ -325,23 +317,13 @@ export function PushProcessor(client) {
};
const matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets || !rulesets.device) {
if (!rulesets) {
return null;
}
if (ev.getSender() == client.credentials.userId) {
if (ev.getSender() === client.credentials.userId) {
return null;
}
const allDevNames = Object.keys(rulesets.device);
for (let i = 0; i < allDevNames.length; ++i) {
const devname = allDevNames[i];
const devrules = rulesets.device[devname];
const matchingRule = matchingRuleFromKindSet(devrules, devname);
if (matchingRule) {
return matchingRule;
}
}
return matchingRuleFromKindSet(ev, rulesets.global);
};
@@ -392,7 +374,7 @@ export function PushProcessor(client) {
* @return {object} The push rule, or null if no such rule was found
*/
this.getPushRuleById = function(ruleId) {
for (const scope of ['device', 'global']) {
for (const scope of ['global']) {
if (client.pushRules[scope] === undefined) continue;
for (const kind of RULEKINDS_IN_ORDER) {
+2 -2
View File
@@ -185,8 +185,8 @@ function _runCallbacks() {
*/
function binarySearch(array, func) {
// min is inclusive, max exclusive.
let min = 0,
max = array.length;
let min = 0;
let max = array.length;
while (min < max) {
const mid = (min + max) >> 1;
+10 -5
View File
@@ -701,7 +701,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
try {
await this._processSyncResponse(syncEventData, data);
} catch(e) {
} catch (e) {
logger.error("Error processing cached sync", e.stack || e);
}
@@ -774,7 +774,7 @@ SyncApi.prototype._sync = async function(syncOptions) {
try {
await this._processSyncResponse(syncEventData, data);
} catch(e) {
} catch (e) {
// log the exception with stack if we have it, else fall back
// to the plain description
logger.error("Caught /sync error", e.stack || e);
@@ -894,7 +894,7 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
logger.error("/sync error %s", err);
logger.error(err);
if(this._shouldAbortSync(err)) {
if (this._shouldAbortSync(err)) {
return;
}
@@ -1023,18 +1023,23 @@ SyncApi.prototype._processSyncResponse = async function(
// handle non-room account_data
if (data.account_data && utils.isArray(data.account_data.events)) {
const events = data.account_data.events.map(client.getEventMapper());
const prevEventsMap = events.reduce((m, c) => {
m[c.getId()] = client.store.getAccountData(c.getType());
return m;
}, {});
client.store.storeAccountDataEvents(events);
events.forEach(
function(accountDataEvent) {
// Honour push rules that come down the sync stream but also
// honour push rules that were previously cached. Base rules
// will be updated when we recieve push rules via getPushRules
// will be updated when we receive push rules via getPushRules
// (see SyncApi.prototype.sync) before syncing over the network.
if (accountDataEvent.getType() === 'm.push_rules') {
const rules = accountDataEvent.getContent();
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
}
client.emit("accountData", accountDataEvent);
const prevEvent = prevEventsMap[accountDataEvent.getId()];
client.emit("accountData", accountDataEvent, prevEvent);
return accountDataEvent;
},
);
+2 -1
View File
@@ -396,7 +396,8 @@ TimelineWindow.prototype.getEvents = function() {
// (Note that both this._start.index and this._end.index are relative
// to their respective timelines' BaseIndex).
//
let startIndex = 0, endIndex = events.length;
let startIndex = 0;
let endIndex = events.length;
if (timeline === this._start.timeline) {
startIndex = this._start.index + timeline.getBaseIndex();
}
+169 -161
View File
@@ -21,7 +21,6 @@ limitations under the License.
*/
import unhomoglyph from 'unhomoglyph';
import {ConnectionError} from "./http-api";
/**
* Encode a dictionary of query parameters.
@@ -50,7 +49,7 @@ export function encodeParams(params: Record<string, string>): string {
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
export function encodeUri(pathTemplate: string,
variables: Record<string, string>): string {
variables: Record<string, string>): string {
for (const key in variables) {
if (!variables.hasOwnProperty(key)) {
continue;
@@ -86,7 +85,7 @@ export function map<T, S>(array: T[], fn: (t: T) => S): S[] {
* @return {Array} A new array with the results of the function.
*/
export function filter<T>(array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean): T[] {
fn: (t: T, i?: number, a?: T[]) => boolean): T[] {
const results: T[] = [];
for (let i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
@@ -152,10 +151,10 @@ export function forEach<T>(array: T[], fn: (t: T, i: number) => void) {
* the given function.
*/
export function findElement<T>(
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean
) {
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean,
) {
let i;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
@@ -183,10 +182,10 @@ export function findElement<T>(
* @return {boolean} True if an element was removed.
*/
export function removeElement<T>(
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean
) {
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean,
) {
let i;
let removed;
if (reverse) {
@@ -294,7 +293,7 @@ export function deepCompare(x: any, y: any): boolean {
// special-case NaN (since NaN !== NaN)
if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
return true;
return true;
}
// special-case null (since typeof null == 'object', but null.constructor
@@ -369,10 +368,10 @@ export function deepCompare(x: any, y: any): boolean {
*
* @return {Object} target
*/
export function extend() {
const target = arguments[0] || {};
for (let i = 1; i < arguments.length; i++) {
const source = arguments[i];
export function extend(...restParams) {
const target = restParams[0] || {};
for (let i = 1; i < restParams.length; i++) {
const source = restParams[i];
if (!source) continue;
for (const propName in source) { // eslint-disable-line guard-for-in
target[propName] = source[propName];
@@ -390,36 +389,36 @@ export function runPolyfills() {
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun: Function/*, thisArg*/) {
if (this === void 0 || this === null) {
throw new TypeError();
}
const t = Object(this);
const len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
const res = [];
const thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (let i = 0; i < len; i++) {
if (i in t) {
const val = t[i];
// NOTE: Technically this should Object.defineProperty at
// the next index, as push can be affected by
// properties on Object.prototype and Array.prototype.
// But that method's new, and collisions should be
// rare, so use the more-compatible alternative.
if (fun.call(thisArg, val, i, t)) {
res.push(val);
// eslint-disable-next-line no-extend-native
Array.prototype.filter = function(fun: Function/*, thisArg*/, ...restProps) {
if (this === void 0 || this === null) {
throw new TypeError();
}
}
}
return res;
};
const t = Object(this);
const len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
const res = [];
const thisArg = restProps ? restProps[0] : void 0;
for (let i = 0; i < len; i++) {
if (i in t) {
const val = t[i];
// NOTE: Technically this should Object.defineProperty at
// the next index, as push can be affected by
// properties on Object.prototype and Array.prototype.
// But that method's new, and collisions should be
// rare, so use the more-compatible alternative.
if (fun.call(thisArg, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
// Array.prototype.map
@@ -429,151 +428,156 @@ export function runPolyfills() {
// Production steps of ECMA-262, Edition 5, 15.4.4.19
// Reference: http://es5.github.io/#x15.4.4.19
if (!Array.prototype.map) {
Array.prototype.map = function(callback, thisArg) {
let T, k;
// eslint-disable-next-line no-extend-native
Array.prototype.map = function(callback, thisArg) {
let T;
let k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this|
// value as the argument.
const O = Object(this);
// 1. Let O be the result of calling ToObject passing the |this|
// value as the argument.
const O = Object(this);
// 2. Let lenValue be the result of calling the Get internal
// method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
const len = O.length >>> 0;
// 2. Let lenValue be the result of calling the Get internal
// method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
const len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let A be a new array created as if by the expression new Array(len)
// where Array is the standard built-in constructor with that name and
// len is the value of len.
const A = new Array(len);
// 6. Let A be a new array created as if by the expression new Array(len)
// where Array is the standard built-in constructor with that name and
// len is the value of len.
const A = new Array(len);
// 7. Let k be 0
k = 0;
// 7. Let k be 0
k = 0;
// 8. Repeat, while k < len
while (k < len) {
let kValue, mappedValue;
// 8. Repeat, while k < len
while (k < len) {
let kValue;
let mappedValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];
// ii. Let mappedValue be the result of calling the Call internal
// method of callback with T as the this value and argument
// list containing kValue, k, and O.
mappedValue = callback.call(T, kValue, k, O);
// ii. Let mappedValue be the result of calling the Call internal
// method of callback with T as the this value and argument
// list containing kValue, k, and O.
mappedValue = callback.call(T, kValue, k, O);
// iii. Call the DefineOwnProperty internal method of A with arguments
// Pk, Property Descriptor
// { Value: mappedValue,
// Writable: true,
// Enumerable: true,
// Configurable: true },
// and false.
// iii. Call the DefineOwnProperty internal method of A with arguments
// Pk, Property Descriptor
// { Value: mappedValue,
// Writable: true,
// Enumerable: true,
// Configurable: true },
// and false.
// In browsers that support Object.defineProperty, use the following:
// Object.defineProperty(A, k, {
// value: mappedValue,
// writable: true,
// enumerable: true,
// configurable: true
// });
// In browsers that support Object.defineProperty, use the following:
// Object.defineProperty(A, k, {
// value: mappedValue,
// writable: true,
// enumerable: true,
// configurable: true
// });
// For best browser support, use the following:
A[k] = mappedValue;
}
// d. Increase k by 1.
k++;
}
// For best browser support, use the following:
A[k] = mappedValue;
}
// d. Increase k by 1.
k++;
}
// 9. return A
return A;
};
// 9. return A
return A;
};
}
// Array.prototype.forEach
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback, thisArg) {
let T, k;
// eslint-disable-next-line no-extend-native
Array.prototype.forEach = function(callback, thisArg) {
let T;
let k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this| value as the
// argument.
const O = Object(this);
// 1. Let O be the result of calling ToObject passing the |this| value as the
// argument.
const O = Object(this);
// 2. Let lenValue be the result of calling the Get internal method of O with the
// argument "length".
// 3. Let len be ToUint32(lenValue).
const len = O.length >>> 0;
// 2. Let lenValue be the result of calling the Get internal method of O with the
// argument "length".
// 3. Let len be ToUint32(lenValue).
const len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let k be 0
k = 0;
// 6. Let k be 0
k = 0;
// 7. Repeat, while k < len
while (k < len) {
let kValue;
// 7. Repeat, while k < len
while (k < len) {
let kValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with
// argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal method of O with
// argument Pk
kValue = O[k];
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with
// argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal method of O with
// argument Pk
kValue = O[k];
// ii. Call the Call internal method of callback with T as the this value and
// argument list containing kValue, k, and O.
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// ii. Call the Call internal method of callback with T as the this value and
// argument list containing kValue, k, and O.
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// 8. return undefined
};
};
}
}
@@ -657,7 +661,10 @@ export function isNumber(value: any): boolean {
* @return {string} a string with the hidden characters removed
*/
export function removeHiddenChars(str: string): string {
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
if (typeof str === "string") {
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
}
return "";
}
// Regex matching bunch of unicode control characters and otherwise misleading/invisible characters.
@@ -667,6 +674,7 @@ export function removeHiddenChars(str: string): string {
// LTR/RTL and other directional formatting marks U+202A - U+202F
// Combining characters U+0300 - U+036F
// Zero width no-break space (BOM) U+FEFF
// eslint-disable-next-line no-misleading-character-class
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g;
export function escapeRegExp(string: string): string {
@@ -725,9 +733,9 @@ export function defer() {
}
export async function promiseMapSeries<T>(
promises: Promise<T>[],
fn: (t: T) => void
): Promise<void> {
promises: Promise<T>[],
fn: (t: T) => void,
): Promise<void> {
for (const o of await promises) {
await fn(await o);
}
+14 -7
View File
@@ -176,7 +176,7 @@ MatrixCall.prototype.placeScreenSharingCall =
debuglog("Got screen stream, requesting audio stream...");
const audioConstraints = _getUserMediaVideoContraints('voice');
_placeCallWithConstraints(self, audioConstraints);
} catch(err) {
} catch (err) {
self.emit("error",
callError(
MatrixCall.ERR_NO_USER_MEDIA,
@@ -403,7 +403,7 @@ MatrixCall.prototype._initWithHangup = function(event) {
* Answer a call.
*/
MatrixCall.prototype.answer = function() {
debuglog("Answering call %s of type %s", this.callId, this.type);
debuglog(`Answering call ${this.callId} of type ${this.type}`);
const self = this;
if (self._answerContent) {
@@ -412,8 +412,10 @@ MatrixCall.prototype.answer = function() {
}
if (!this.localAVStream && !this.waitForLocalAVStream) {
const constraints = _getUserMediaVideoContraints(this.type);
logger.log("Getting user media with constraints", constraints);
this.webRtc.getUserMedia(
_getUserMediaVideoContraints(this.type),
constraints,
hookCallback(self, self._maybeGotUserMediaForAnswer),
hookCallback(self, self._maybeGotUserMediaForAnswer),
);
@@ -1065,7 +1067,7 @@ const terminate = function(self, hangupParty, hangupReason, shouldEmit) {
};
const stopAllMedia = function(self) {
debuglog("stopAllMedia (stream=%s)", self.localAVStream);
debuglog(`stopAllMedia (stream=${self.localAVStream})`);
if (self.localAVStream) {
forAllTracksOnStream(self.localAVStream, function(t) {
if (t.stop) {
@@ -1127,7 +1129,11 @@ const _tryPlayRemoteAudioStream = async function(self) {
const player = self.getRemoteAudioElement();
// if audioOutput is non-default:
if (audioOutput) await player.setSinkId(audioOutput);
try {
if (audioOutput) await player.setSinkId(audioOutput);
} catch (e) {
logger.warn("Couldn't set requested audio output device: using default", e);
}
player.autoplay = true;
self.assignElement(player, self.remoteAStream, "remoteAudio");
@@ -1188,8 +1194,8 @@ const _sendCandidateQueue = function(self) {
if (self.candidateSendTries > 5) {
debuglog(
"Failed to send candidates on attempt %s. Giving up for now.",
self.candidateSendTries,
"Failed to send candidates on attempt " + self.candidateSendTries +
". Giving up for now.",
);
self.candidateSendTries = 0;
return;
@@ -1205,6 +1211,7 @@ const _sendCandidateQueue = function(self) {
};
const _placeCallWithConstraints = function(self, constraints) {
logger.log("Getting user media with constraints", constraints);
self.client.callList[self.callId] = self;
self.webRtc.getUserMedia(
constraints,
-72
View File
@@ -1,72 +0,0 @@
{
"rules": {
"class-name": false,
"comment-format": [
true
],
"curly": false,
"eofline": false,
"forin": false,
"indent": [
true,
"spaces"
],
"label-position": true,
"max-line-length": false,
"member-access": false,
"member-ordering": [
true,
"static-after-instance",
"variables-before-functions"
],
"no-arg": true,
"no-bitwise": false,
"no-console": false,
"no-construct": true,
"no-debugger": true,
"no-duplicate-variable": true,
"no-empty": false,
"no-eval": true,
"no-inferrable-types": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": true,
"no-use-before-declare": false,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"quotemark": false,
"radix": true,
"semicolon": [
"always"
],
"triple-equals": [],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}
+1561 -947
View File
File diff suppressed because it is too large Load Diff