Compare commits

...

224 Commits

Author SHA1 Message Date
RiotRobot 6d018826f4 v30.2.0 2023-12-05 13:35:06 +00:00
RiotRobot df1a6a583a v30.2.0-rc.0 2023-11-28 16:11:15 +00:00
Richard van der Hoff a7496627fc Explicitly free some Rust-side objects (#3911)
* Explicitly `free` stuff returned by `OlmMachine.getIdentity()`

* Explicitly `free` stuff returned by `OlmMachine.getDevice()`

* one more
2023-11-28 13:14:53 +00:00
Richard van der Hoff 8ef2ca681c Update to matrix-sdk-crypto-wasm 3.1.0 (#3909) 2023-11-27 15:15:58 +00:00
Johannes Marbach 0c7342cb20 Set up CI to lint workflows with action-validator (#3905)
* Set up CI to lint workflows with action-validator

* Rename release-action workflow
2023-11-24 14:41:19 +00:00
Will Hunt 429c05ba85 TimestampToEventResponse.origin_server_ts should be a number (#3906) 2023-11-23 16:46:01 +00:00
Andy Balaam af9993a710 Remove 'Ignoring receipt' log line that logs very often' (#3904)
* Remove 'Ignoring receipt' log line that logs very often'

* Fix test expecting the log line I removed
2023-11-22 12:14:42 +00:00
Valere ff501834e6 Only await key query after lazy members resolved (#3902)
* Only await key query after lazy members resolved

* code review

* Update src/rust-crypto/RoomEncryptor.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-11-22 09:19:13 +00:00
Michael Telatynski ef9157b37a Fix Ingest upstream changes for downstreams with missing sections
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-21 12:31:40 +00:00
Michael Telatynski da0a55cea4 Merge remote-tracking branch 'origin/develop' into develop 2023-11-21 11:46:06 +00:00
Michael Telatynski d644f111ea Fix Ingest upstream changes for element-desktop
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-21 11:45:55 +00:00
RiotRobot b2018ef81b Resetting package fields for development 2023-11-21 11:00:05 +00:00
RiotRobot a4faab6155 Merge branch 'master' into develop 2023-11-21 11:00:04 +00:00
RiotRobot 4ab226e580 v30.1.0 2023-11-21 10:59:16 +00:00
Richard van der Hoff 1889f8dad5 Reduce console log spam (#3896)
* Reduce console log spam

A couple of different things:

 * Increase the `MaxListeners` setting on `MatrixClient` and `Thread`, so that
   we don't get "possible EventEmitter leak" warnings

 * Disable a couple of warnings/info lines that are just part of regular
   operation and are logged in large volumes.

* another noisy log line

* Reinstate warning about receipts for missing events

Apparently this is being worked on
2023-11-20 18:17:06 +00:00
Timo e98ce78027 Better fallback for the event localTimestamp calculation. (#3900)
* better fallback to localTimestamp calculation

Signed-off-by: Timo K <toger5@hotmail.de>

* make `isExpired` impl simpler to read

Signed-off-by: Timo K <toger5@hotmail.de>

* update tests

Signed-off-by: Timo K <toger5@hotmail.de>

* refactor to use localTimestamp in the mocks

Signed-off-by: Timo K <toger5@hotmail.de>

* Update src/models/event.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/models/event.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update and clarify comments.
So that the localTimestamp and localAge behavior is easier to understand.

Signed-off-by: Timo K <toger5@hotmail.de>

* Replace localTimestamp biding
with binding the whole roomState

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-11-20 17:20:04 +00:00
Richard van der Hoff 83ba0fbb49 Improve logging of event encryption in RustCrypto (#3898)
* Improve logging of event encryption in `RustCrypto`

* fix tests
2023-11-19 21:16:41 +00:00
Richard van der Hoff 757c5e1d71 Stop logging decryptions as retries (#3897)
Somebody seems to have decided that `isRetry` needs setting whenever we try to decrypt an event. This is nonsense, and leads to confusing logs.
2023-11-17 14:30:27 +00:00
Johannes Marbach eca651c0c2 Explicitly forward ELEMENT_BOT_TOKEN (#3894)
`inherit` doesn't work across orgs, sadly.
2023-11-16 14:46:56 +00:00
Andy Balaam 2205445a50 Specify the correct environment for the docs builder workflow (#3893) 2023-11-16 13:40:02 +00:00
renovate[bot] f168144c84 Update matrix-org/sonarcloud-workflow-action action to v2.7 (#3892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-16 10:58:00 +00:00
Michael Telatynski eb288d125f Remove unused dependencies and add transitive dep (#3874)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-16 09:47:30 +00:00
Michael Telatynski 4a72364fe3 Merge remote-tracking branch 'origin/develop' into develop 2023-11-16 08:33:20 +00:00
Michael Telatynski c2fa579fb2 Fix merge-release-notes.js script
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-16 08:33:08 +00:00
renovate[bot] f71735d0c2 Update babel monorepo to v7.23.3 (#3881)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-15 22:26:23 +00:00
renovate[bot] e5ccfa86fe Update actions/github-script action to v7 (#3886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-15 22:25:24 +00:00
renovate[bot] 97c531aa42 Update all non-major dependencies (#3884)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-15 22:24:58 +00:00
renovate[bot] 44487078fb Update dependency @types/jest to v29.5.8 (#3883)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-15 22:24:49 +00:00
renovate[bot] e3c70a3ee4 Update actions/setup-node action to v4 (#3887)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 22:19:02 +00:00
renovate[bot] feb60a54b2 Update definitelyTyped (#3882)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 22:10:00 +00:00
David Baker c6e6248cd6 Don't rotate keys if not managing media keys (#3877)
This could have caused weirdness in non per-user key calling mode.
2023-11-15 14:44:58 +00:00
Timo 10cd84a653 Add CallNotifyEvent to support matrixRTC ringing (#3878)
* Add CallNotifyEvent to support matrix rtc ringing

Signed-off-by: Timo K <toger5@hotmail.de>

* test SessionId

Signed-off-by: Timo K <toger5@hotmail.de>

* docs + sessionId->callId

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2023-11-15 11:20:05 +00:00
Andy Balaam c36166d156 Merge pull request #3869 from matrix-org/andybalaam/unrevert-deletion-move-prs
Unrevert "Move redacted messages out of any thread, into main thread"
2023-11-15 08:23:50 +00:00
renovate[bot] 3a2cf14a68 Update matrix-org/matrix-react-sdk digest to f6ef476 (#3879)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-15 00:58:24 +00:00
Andy Balaam dd94f67a4f Merge branch 'develop' into andybalaam/unrevert-deletion-move-prs 2023-11-14 16:13:56 +00:00
Michael Telatynski 138281c620 Fix RELEASE_ID
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-14 17:12:49 +01:00
RiotRobot f75abecc92 v30.1.0-rc.1 2023-11-14 15:37:50 +00:00
Michael Telatynski 378a91fe10 Fix yarn version call in release-action.yml
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-14 16:31:58 +01:00
Michael Telatynski 300635e3ee Update package.json 2023-11-14 15:31:26 +00:00
Michael Telatynski 37ba169abf Update release-drafter.yml 2023-11-14 14:52:56 +00:00
RiotRobot e6e7798389 v30.1.0-rc.0 2023-11-14 14:29:48 +00:00
Michael Telatynski 48fe267ea7 release-action.yml iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-14 15:26:31 +01:00
Michael Telatynski a11fd8bc86 release-npm.yml ref=staging
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-14 15:22:03 +01:00
Michael Telatynski eb9e557a64 Update release-action.yml 2023-11-14 14:19:52 +00:00
RiotRobot 41c8c40d47 v30.1.0-rc.1 2023-11-14 14:17:37 +00:00
Michael Telatynski b9e684fdc3 Update release-action.yml 2023-11-14 14:07:25 +00:00
RiotRobot 9faff0dbff v30.1.0-rc.0 2023-11-14 14:04:14 +00:00
Michael Telatynski 9044145a7e Update release-action.yml 2023-11-14 14:03:09 +00:00
Michael Telatynski 939def2aa1 Update release-drafter.yml 2023-11-14 13:51:16 +00:00
Michael Telatynski c54f8f6106 Update release-drafter.yml 2023-11-14 13:47:56 +00:00
Michael Telatynski 25a777a0a6 Update release-drafter.yml 2023-11-14 13:41:37 +00:00
Michael Telatynski 7de9b23e59 Add support for ingest-changes to refer to a project without package.json (#3864)
* Tidy reusable release workflow

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

* Add ability to include upstream changes

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

* Add ability to upload assets and gpg sign them

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

* Update relative composite actions

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

* Wire up validating release tarball signature

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

* Validate release has expected assets

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

* Paths

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

* Use gpg outputs for email instead of scraping it ourselves

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

* v6

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

* Extract pre-release and post-merge-master scripts

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

* Reuse pre-release and post-merge-master scripts in gha

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

* Cull unused vars

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

* Revert

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

* Remove unused variables

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

* Simplify

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

* Simplify and fix merge-release-notes script

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

* Tidy release automation

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

* Update release.sh

* Move environment

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

* s/includes/contains/

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

* Iterate uses syntax

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

* Fix action-repo calls

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

* Fix RELEASE_NOTES env

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

* Fix if check

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

* Fix gpg tag signing

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

* Cull stale params

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

* Fix sign-release-tarball paths being outside the workspace

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

* Fix gpg validation (of course wget uses `-O` and not `-o`)

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

* Fix expected asset assertion

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

* Fix release publish mode

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

* Add support for ingest-changes to refer to a project without it being in node_modules

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-13 12:43:18 +00:00
Michael Telatynski d179b8c557 Add automation to advance release blocker labels during the release (#3866)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-13 09:46:11 +00:00
ElementRobot 76f993e7ff Merge branch 'master' into develop 2023-11-13 09:44:12 +00:00
ElementRobot 430e6cae94 v30.0.1 2023-11-13 09:44:06 +00:00
ElementRobot e01a1d533c Prepare changelog for v30.0.1 2023-11-13 09:44:04 +00:00
ElementRobot 46a6a76a41 [Backport staging] Ensure setUserCreator is called when a store is assigned (#3876)
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
2023-11-13 09:11:27 +00:00
Johannes Marbach d2e951738a Automatically add tech-debt issues to the right project (#3872) 2023-11-11 07:21:31 +00:00
R Midhun Suresh 882dc920c3 Ensure setUserCreator is called when a store is assigned (#3867)
* Add method to set store

* Use not null assertion

* Use getter/setter

* No need for check if we use setter
2023-11-11 07:20:36 +00:00
Michael Telatynski 9efc0acb9d Automate checking there is no published release using the version already (#3865)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-11 06:53:14 +00:00
Michael Telatynski 625753c388 Update tests.yml (#3847) 2023-11-10 20:30:33 +00:00
Richard van der Hoff a28530004a Bump matrix-sdk-crypto-wasm to 3.0.1 (#3849)
* Bump matrix-sdk-crypto-wasm to 3.0.0

... which changes the API of `bootstrapCrossSigning` a bit.

* Fix class names in test

* fix brokenness in bootstrapCrossSigning

* Bump to `matrix-sdk-crypto-wasm` 3.0.1
2023-11-10 16:57:50 +00:00
Richard van der Hoff 437b7ff780 Revert "Better fallback for unavailable event age (#3854)" (#3870)
This reverts commit 84bd8ab81f.
2023-11-10 00:06:48 +00:00
Michael Telatynski 24ed030294 Extend release automation with GPG signing, assets & changelog merging (#3852)
* Tidy reusable release workflow

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

* Add ability to include upstream changes

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

* Add ability to upload assets and gpg sign them

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

* Update relative composite actions

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

* Wire up validating release tarball signature

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

* Validate release has expected assets

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

* Paths

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

* Use gpg outputs for email instead of scraping it ourselves

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

* v6

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

* Extract pre-release and post-merge-master scripts

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

* Reuse pre-release and post-merge-master scripts in gha

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

* Cull unused vars

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

* Revert

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

* Remove unused variables

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

* Simplify

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

* Simplify and fix merge-release-notes script

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

* Tidy release automation

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

* Update release.sh

* Move environment

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

* s/includes/contains/

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

* Iterate uses syntax

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

* Fix action-repo calls

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

* Fix RELEASE_NOTES env

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

* Fix if check

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

* Fix gpg tag signing

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

* Cull stale params

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

* Fix sign-release-tarball paths being outside the workspace

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

* Fix gpg validation (of course wget uses `-O` and not `-o`)

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

* Fix expected asset assertion

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

* Fix release publish mode

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-09 20:54:29 +00:00
Andy Balaam 5c160d0f45 Shorten TimelineWindow when an event is removed (#3862)
* Shorten TimelineWindow when an event is removed

Needed for the fix for https://github.com/vector-im/element-web/issues/26498

* Declare onTimelineEvent as a standard method to match surrounding code
2023-11-09 16:41:15 +00:00
Andy Balaam 53615c9938 Merge pull request #3855 from matrix-org/rav/cypress-element-r
Cypress workflow: remove redundant `rust-crypto` param
2023-11-09 15:34:49 +00:00
Andy Balaam d8735cf543 Merge pull request #3868 from matrix-org/midhun/update-cypress-workfile
Update workfile to pull in changes from react-sdk
2023-11-09 15:07:40 +00:00
R Midhun Suresh ffb4cae792 Update workfile 2023-11-09 20:00:45 +05:30
Andy Balaam 0261868eb6 Revert "Revert "Move the redaction event to main at the same time we move redacted""
This reverts commit 11755f5a0a1486fa6ad3cb9e4b8959ddc7e1d276.
2023-11-09 14:30:41 +00:00
Andy Balaam 6ba4b35526 Revert "Revert "Don't remove thread info from a thread root when it is redacted""
This reverts commit 4dbff2a837cbc5ba37424c65ccdc833a1843deb2.
2023-11-09 14:30:41 +00:00
Andy Balaam f5ad4d0a73 Revert "Revert "Move all related messages into main timeline on redaction""
This reverts commit 257b40bceb304001c03aaec7b140a1fd05c96d9e.
2023-11-09 14:30:41 +00:00
Andy Balaam 582ea68c31 Revert "Revert "Factor out the code for moving an event to the main timeline""
This reverts commit 272be48a54a45df89603a27fbbe6e26da88b95ba.
2023-11-09 14:30:41 +00:00
Andy Balaam 304c2b12bf Revert "Revert "Factor out utils in redaction tests""
This reverts commit 2525c82049dc1a958446b66cc85656c1b57a5271.
2023-11-09 14:30:41 +00:00
Andy Balaam a3762c8e22 Revert "Revert "Move redaction event tests into their own describe block""
This reverts commit 2e24481df335411ee489ac7046c5514821afa4fa.
2023-11-09 14:30:41 +00:00
Andy Balaam 8b2a334ac4 Revert "Revert "Move redacted messages out of any thread, into main timeline.""
This reverts commit 46114a025c5ec5b2658803c8c86d5e855b55a4fb.
2023-11-09 14:30:41 +00:00
Michael Telatynski 5931a5119c Tidy release automation (#3857) 2023-11-08 12:20:25 +00:00
renovate[bot] 6ae3c208f6 Update all non-major dependencies (#3863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-08 12:08:04 +00:00
ElementRobot 107e28e114 Resetting package fields for development 2023-11-07 15:11:56 +00:00
ElementRobot 1d1157f546 Merge branch 'master' into develop 2023-11-07 15:11:53 +00:00
ElementRobot fe3f969698 v30.0.0 2023-11-07 15:11:47 +00:00
ElementRobot 96c6c99644 Prepare changelog for v30.0.0 2023-11-07 15:11:45 +00:00
ElementRobot 55230dd0ea [Backport staging] Revert code moving deleted messages to main timeline (#3859)
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2023-11-07 14:31:37 +00:00
Andy Balaam 7813e12eb0 Revert code moving deleted messages to main timeline (#3858)
* Revert "Move the redaction event to main at the same time we move redacted"

This reverts commit 378a776815f63fdd1e4d507af35046c0ba88153c.

Context: https://github.com/vector-im/element-web/issues/26498

* Revert "Don't remove thread info from a thread root when it is redacted"

This reverts commit 17b61a69c20677e39e4f3b1b4ed5903421eaee6b.

Context: https://github.com/vector-im/element-web/issues/26498

* Revert "Move all related messages into main timeline on redaction"

This reverts commit d8fc1795f1319b6a77175c5c584ae03be53c457c.

Context: https://github.com/vector-im/element-web/issues/26498

* Revert "Factor out the code for moving an event to the main timeline"

This reverts commit 942dfcb84b8aef6ea84a419d73e845d3611bd91c.

Context: https://github.com/vector-im/element-web/issues/26498

* Revert "Factor out utils in redaction tests"

This reverts commit 43a0dc56e130f75ef695b52cd23945e10393119a.

Context: https://github.com/vector-im/element-web/issues/26498

* Revert "Move redaction event tests into their own describe block"

This reverts commit 9b0ea80f93fe944da03c27df26064cae1765c94d.

Context: https://github.com/vector-im/element-web/issues/26498

* Revert "Move redacted messages out of any thread, into main timeline."

This reverts commit b94d137398.

Context: https://github.com/vector-im/element-web/issues/26498
2023-11-07 13:41:33 +00:00
David Baker 036fd943ac Rotate per-participant keys when a member leaves (#3833)
* WIP refactor for removing m.call events

* Always remember rtcsessions since we need to only have one instance

* Fix tests

* Fix import loop

* Fix more cyclic imports & tests

* Test session joining

* Attempt to make tests happy

* Always leave calls in the tests to clean up

* comment + desperate attempt to work out what's failing

* More test debugging

* Okay, so these ones are fine?

* Stop more timers and hopefully have happy tests

* Test no rejoin

* Test malformed m.call.member events

* Test event emitting

and also move some code to a more sensible place in the file

* Test getActiveFoci()

* Test event emitting (and also fix it)

* Test membership updating & pruning on join

* Test getOldestMembership()

* Test member event renewal

* Don't start the rtc manager until the client has synced

Then we can initialise from the state once it's completed.

* Fix type

* Remove listeners added in constructor

* Stop the client here too

* Stop the client here also also

* ARGH. Disable tests to work out which one is causing the exception

* Disable everything

* Re-jig to avoid setting listeners in the constructor

and re-enable tests

* No need to rename this anymore

* argh, remove the right listener

* Is it this test???

* Re-enable some tests

* Try mocking getRooms to return something valid

* Re-enable other tests

* Give up trying to get the tests to work sensibly and deal with getRooms() returning nothing

* Oops, don't enable the ones that were skipped before

* One more try at the sensible way

* Didn't work, go back to the hack way.

* Log when we manage to send the member event update

* Support `getOpenIdToken()` in embedded mode (#3676)

* Call `sendContentLoaded()` (#3677)

* Start MatrixRTC in embedded mode (#3679)

* Reschedule the membership event check

* Bump widget api version

* Add mock for sendContentLoaded()

* Embeded mode pre-requisites

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Embeded mode E2EE

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Encryption condition

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Revert "Embeded mode pre-requisites"

This reverts commit 8cd73702052609c995ad754e31f85d0da0be4aa9.

* Get back event type

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

fds

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Change embedded E2EE implementation

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* More log detail

* Fix tests

and also better assert because the tests were passing undefined which
was considered fine because we were only checking for null.

* Simplify updateCallMembershipEvent a bit

* Split up updateCallMembershipEvent some more

* Use `crypto.getRandomValues()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Rename to `membershipToUserAndDeviceId()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Better error

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add log line

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add comment

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Send call ID in enc events

(also a small refactor)

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Revert making `joinRoomSession()` async

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make `client` `private` again

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Just use `toString()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix `callId` check

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix map

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix map compare

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix emitting

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Explicit logging

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Refactor

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make `updateEncryptionKeyEvent()` public

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Only update keys based on others

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix call order

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Improve logging

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Avoid races

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Revert "Avoid races"

This reverts commit f65ed72d6e.

* Add try-catch

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make `updateEncryptionKeyEvent()` private

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Handle indices and throttling

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix merge mistakes

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Mort post-merge fixes

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Split out key generation from key sending

And send all keys in a key event (changes the format of the key event)
rather than just the one we just generated.

* Remember and clear the timeout for the send key event

So we don't schedule more key updates if one is already pending.
Also don't update the last sent time when we didn't actually send the
keys.

* Make key event resends more robust

* Attempt to make tests pass

* crypto wasn't defined at all

* Hopefully get interface right

* Fix key format on the wire to base64

* Add comment

* More standard method order

* Rename encryptMedia

The js-sdk doesn't do media and therefore doesn't do media encryption

* Stop logging encryption keys now

* Use regular base64

It's not going in a URL, so no need

* Re-add base64url

randomstring was using it. Also give it a test.

* Add tests for randomstring

* Switch between either browser or node crypto

Let's see if this will work...

* Obviously crypto has already solved this

* Some tests for MatrixRTCSession key stuff

* Test keys object contents

* Change keys event format

To move away from m. keys

* Test key event retries

* Test onCallEncryption

* Test event sending & spam prevention

* Test event cancelation

* Test onCallEncryption called

* Better before/after member comparison

Only trigger for when members actually join, and just generally
make it a bit more understandable.

* Rotate per-participant keys when a member leaves

With a delay borth before making a new key, to try to batch up multiple
people leaving into a single key change, and a delay before actually
using the new key to allow time for it to arrive.

This increasingly feels like storing our own sender key in the same set
is suboptimal because we're starting to have to treat it more & more
specially.

* Some errors didn't have data

* Fix binary key comparison

& add log line

* Fix compare function with undefined values

* Test key rotation

* Test caught a merge bug!

* The missing word was, 'delay'

* More input validation

---------

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
Co-authored-by: Šimon Brandner <simon.bra.ag@gmail.com>
2023-11-07 11:12:11 +00:00
Timo 84bd8ab81f Better fallback for unavailable event age (#3854)
* Age fallback using origin_server_ts instead of 0

Signed-off-by: Timo K <toger5@hotmail.de>

* use getMsUntilExpiry for isExpired

Signed-off-by: Timo K <toger5@hotmail.de>

* fix tests
tests now also rely on localTimestamp. So this need to be mocked as well

Signed-off-by: Timo K <toger5@hotmail.de>

* better fallback for unavailable unsigned

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2023-11-06 17:12:24 +00:00
Michael Telatynski a25ba7bfd9 Iterate reusable release automation workflows (#3851)
* Clean up unused envvar

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

* Make the gitflow workflow reusable

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

* Add support for resetting dependencies to develop after merge

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

* Rename workflow file

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-06 13:25:19 +00:00
Andy Balaam 311494bd44 Ignore receipts pointing at missing or invalid events (#3817)
* Ignore receipts pointing at missing or invalid events

* Remove extra whitespace from log message

* Unit tests for ignoring invalid receipts

* Improve comments around getEventReadUpTo

* Re-instate second param to compareEventOrdering in test

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Further improve comments around getEventReadUpTo

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-11-06 09:19:21 +00:00
Richard van der Hoff 89b7e7d792 Cypress workflow: remove redundant rust-crypto param
https://github.com/matrix-org/matrix-react-sdk/pull/11828 removes the
`rust-crypto` input param from the reusable cypress workflow. This gets rid of
it on the calling side.
2023-11-03 15:51:07 +00:00
Valere 7921fee164 Fix members being loaded from server on initial sync (defeating lazy loading) (#3830)
* fix members loaded on intitial sync

* Update test to use KeyResponder

* Use E2EKeyResponder

* code review

* better comment

* fix test

* post merge fix

* fix imports

* refactoring, better names

* code review

* clean tests

* Cleanups per review comments

* fix test

* Apply suggestions from code review

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-11-03 14:55:48 +00:00
Richard van der Hoff 5bc132a24c Revert "Age fallback using origin_server_ts instead of 0 (#3839)" (#3853)
This reverts commit 685ef791c8.
2023-11-03 13:32:42 +00:00
Timo 685ef791c8 Age fallback using origin_server_ts instead of 0 (#3839)
* Age fallback using origin_server_ts instead of 0

Signed-off-by: Timo K <toger5@hotmail.de>

* use getMsUntilExpiry for isExpired

Signed-off-by: Timo K <toger5@hotmail.de>

* fix tests
tests now also rely on localTimestamp. So this need to be mocked as well

Signed-off-by: Timo K <toger5@hotmail.de>

* fix another test that now also depends on localTimestamp

Signed-off-by: Timo K <toger5@hotmail.de>

* fix tests and cleanup

Signed-off-by: Timo K <toger5@hotmail.de>

* format

Signed-off-by: Timo K <toger5@hotmail.de>

* make things simpler by calculating localTimestamp
from getLocalAge

Signed-off-by: Timo K <toger5@hotmail.de>

* this test was not covered by the change to mockRTCEvent

Signed-off-by: Timo K <toger5@hotmail.de>

* format

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2023-11-02 13:02:02 +00:00
Michael Telatynski 4458dcc2a4 Make release automation reusable and add dependency upgrade support (#3848)
* Extract release into reusable action

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

* Add dependency upgrade task to release-action

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

* Prevent develop dependencies

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

* Simplify dependency management

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

* Add missing secret declaration

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-11-01 23:24:05 +00:00
renovate[bot] 36c958642c Update all non-major dependencies (#3842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 08:34:18 +00:00
renovate[bot] b62e97eb92 Update actions/setup-node action to v4 (#3844)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-11-01 08:31:51 +00:00
Michael Telatynski 448fab9e8a New release automations (#3813) 2023-11-01 08:40:43 +00:00
Hugh Nimmo-Smith e2a2039aa8 Remove deprecated support for unstable MSC3882 (#3755)
* Support for stable MSC3882 get_login_token

* Make changes non-breaking by deprecation

* Remove deprecated exports from MSC3882 stabilisation

* Feat remove support for unstable MSC3882

* Remove bad line from rebase
2023-10-31 17:15:54 +00:00
renovate[bot] 99f70cd048 Update JS-DevTools/npm-publish action to v3 (#3843)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 16:09:00 +00:00
Šimon Brandner bf81c4bfeb Add E2EE for embedded mode of Element Call (#3667)
* WIP refactor for removing m.call events

* Always remember rtcsessions since we need to only have one instance

* Fix tests

* Fix import loop

* Fix more cyclic imports & tests

* Test session joining

* Attempt to make tests happy

* Always leave calls in the tests to clean up

* comment + desperate attempt to work out what's failing

* More test debugging

* Okay, so these ones are fine?

* Stop more timers and hopefully have happy tests

* Test no rejoin

* Test malformed m.call.member events

* Test event emitting

and also move some code to a more sensible place in the file

* Test getActiveFoci()

* Test event emitting (and also fix it)

* Test membership updating & pruning on join

* Test getOldestMembership()

* Test member event renewal

* Don't start the rtc manager until the client has synced

Then we can initialise from the state once it's completed.

* Fix type

* Remove listeners added in constructor

* Stop the client here too

* Stop the client here also also

* ARGH. Disable tests to work out which one is causing the exception

* Disable everything

* Re-jig to avoid setting listeners in the constructor

and re-enable tests

* No need to rename this anymore

* argh, remove the right listener

* Is it this test???

* Re-enable some tests

* Try mocking getRooms to return something valid

* Re-enable other tests

* Give up trying to get the tests to work sensibly and deal with getRooms() returning nothing

* Oops, don't enable the ones that were skipped before

* One more try at the sensible way

* Didn't work, go back to the hack way.

* Log when we manage to send the member event update

* Support `getOpenIdToken()` in embedded mode (#3676)

* Call `sendContentLoaded()` (#3677)

* Start MatrixRTC in embedded mode (#3679)

* Reschedule the membership event check

* Bump widget api version

* Add mock for sendContentLoaded()

* Embeded mode pre-requisites

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Embeded mode E2EE

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Encryption condition

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Revert "Embeded mode pre-requisites"

This reverts commit 8cd73702052609c995ad754e31f85d0da0be4aa9.

* Get back event type

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

fds

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Change embedded E2EE implementation

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* More log detail

* Fix tests

and also better assert because the tests were passing undefined which
was considered fine because we were only checking for null.

* Simplify updateCallMembershipEvent a bit

* Split up updateCallMembershipEvent some more

* Use `crypto.getRandomValues()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Rename to `membershipToUserAndDeviceId()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Better error

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add log line

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Add comment

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Send call ID in enc events

(also a small refactor)

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Revert making `joinRoomSession()` async

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make `client` `private` again

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Just use `toString()`

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix `callId` check

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix map

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix map compare

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix emitting

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Explicit logging

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Refactor

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make `updateEncryptionKeyEvent()` public

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Only update keys based on others

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix call order

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Improve logging

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Avoid races

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Revert "Avoid races"

This reverts commit f65ed72d6e.

* Add try-catch

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Make `updateEncryptionKeyEvent()` private

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Handle indices and throttling

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Fix merge mistakes

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Mort post-merge fixes

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>

* Split out key generation from key sending

And send all keys in a key event (changes the format of the key event)
rather than just the one we just generated.

* Remember and clear the timeout for the send key event

So we don't schedule more key updates if one is already pending.
Also don't update the last sent time when we didn't actually send the
keys.

* Make key event resends more robust

* Attempt to make tests pass

* crypto wasn't defined at all

* Hopefully get interface right

* Fix key format on the wire to base64

* Add comment

* More standard method order

* Rename encryptMedia

The js-sdk doesn't do media and therefore doesn't do media encryption

* Stop logging encryption keys now

* Use regular base64

It's not going in a URL, so no need

* Re-add base64url

randomstring was using it. Also give it a test.

* Add tests for randomstring

* Switch between either browser or node crypto

Let's see if this will work...

* Obviously crypto has already solved this

* Some tests for MatrixRTCSession key stuff

* Test keys object contents

* Change keys event format

To move away from m. keys

* Test key event retries

* Test onCallEncryption

* Test event sending & spam prevention

* Test event cancelation

* Test onCallEncryption called

* Some errors didn't have data

* Fix binary key comparison

& add log line

* Fix compare function with undefined values

* Remove more key logging

* Check content.keys is an array

* Check key index & key

* Better function name

* Tests too

---------

Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
Co-authored-by: David Baker <dave@matrix.org>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2023-10-31 16:01:46 +00:00
renovate[bot] 370dd6a0eb Update dependency eslint-plugin-unicorn to v49 (#3845)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 15:59:16 +00:00
renovate[bot] f760ece8b4 Update dependency @types/jest to v29.5.6 (#3841)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 15:57:27 +00:00
renovate[bot] 93e339affe Update definitelyTyped (#3840)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-31 15:57:11 +00:00
ElementRobot 5707b48fd2 v30.0.0-rc.1 2023-10-31 14:40:50 +00:00
ElementRobot 8ac918c10f Prepare changelog for v30.0.0-rc.1 2023-10-31 14:40:48 +00:00
Florian Duros 1cd8bed705 Element-R: Add the git sha of the binding crate to CryptoApi#getVersion (#3838)
* Update `@matrix-org/matrix-sdk-crypto-wasm` to `v2.2.0`

* Add the git sha of the binding crate to `CryptoApi#getVersions`
2023-10-31 09:15:14 +00:00
Hubert Chathi e0dacf7529 use olm from default npm registry, since it's there now (#3837)
and bump to latest version
2023-10-28 10:23:03 +00:00
Dariusz Niemczyk 29d9bdac61 feat: Add autoformat and lint for ts/tsx files (#3835)
* feat: Add autoformat and lint for ts/tsx files

* Update .lintstagedrc

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-10-27 14:38:18 +00:00
R Midhun Suresh 88d066a10c Fix reemitter not being correctly wired on user objects created in storage classes (#3796)
* Fix issue

* Fix jest test

* Fix even more jest failures

* Fix formatting

* Add a test

* Write test for older code

* Fix lint

* Rename method

* Make ctor deprecated
2023-10-27 07:00:13 +00:00
Florian Duros ce7b7bf44f Element-R: Wire up globalBlacklistUnverifiedDevices field to rust crypto encryption settings (#3790)
* Wire up `globalBlacklistUnverifiedDevices` rust crypto encrypted settings

* Improve test comment

* Update comments

* Review changes

* Fix lint due to merge
2023-10-26 13:57:37 +00:00
Richard van der Hoff 07a9eb3c96 Element-R: reduce log spam when checking server key backup (#3826)
* Element-R: reduce log spam when checking server key backup

Fixes a lot of spam in the logs about "uncaught in promise: No room_keys
found".

* Improve integ tests for backup query after UTD

* Yield in the backup decryption loop

* Fix another broken test
2023-10-26 11:10:04 +00:00
Florian Duros f8f22a3edd Element-R: Wire up room rotation (#3807)
* Wire up rotation

* Wire up algorithm

* Add encryption settings test

* Update comments
2023-10-25 15:12:04 +00:00
Richard van der Hoff 084beaa947 Fix up comments on globalErrorOnUnknownDevices (#3834)
The current deprecation notice advises you to use a method which does something
completely different.

Fixing this "properly" is slightly challenging because we don't want to support
setting it to `true` in Rust Crypto; yet I don't really want to change the
default for legacy crypto.

Let's just document the behaviour for now.
2023-10-25 14:32:16 +00:00
Florian Duros 73a87652fe Element-R: Add current version of the rust-sdk and vodozemac (#3825)
* Add current version of the rust-sdk and vodozemac

* Return OlmVersion in `CryptoApi#getVersion` for old crypto

* Add `Olm` prefix

* Fix documentation

* Review changes
2023-10-25 13:12:15 +00:00
Florian Duros 4a4b454f27 Element-R: Wire up room history visibility (#3805)
* Wire up history visibility in `RoomEncryptor.ts`

* Add more tests to history visibility conversion

* Factorize `expectSendMessage` and `expectSendMegolmMessage`

* Use correct import

* Fix overwriteRoutes

* Update comments
2023-10-25 11:49:03 +00:00
Richard van der Hoff 6f82f08c7b Element-R: silence log errors when viewing a pending event (#3824)
* Element-R: silence log errors when viewing a pending event

Fixes the second half of vector-im/element-web#26272

* Update spec/integ/crypto/crypto.spec.ts
2023-10-25 09:11:40 +00:00
Dominik Henneke c41949de15 Don't emit a closed event if the indexeddb is closed by Element (#3832)
Signed-off-by: Dominik Henneke <dominik.henneke@nordeck.net>
2023-10-25 08:14:12 +00:00
David Baker f941fd896e Change latest node ver to '*' (#3831)
* Change latest node ver to '*'

This uses the latest cached version rather than fetching the latest released version so we don't reply on (and hammer) node's download servers for the very latest version before the actions runners get updated. We'll still stay current, just not quite so aggressively current.

* Fix artifact uploading hopefully

* Hopefully make job name 'node latest'
2023-10-24 14:45:03 +00:00
ElementRobot d750e33ec9 Resetting package fields for development 2023-10-24 15:27:12 +01:00
ElementRobot a370442328 Merge branch 'master' into develop 2023-10-24 15:27:09 +01:00
ElementRobot bddf2b9682 v29.1.0 2023-10-24 15:27:03 +01:00
ElementRobot 74a2e694c3 Prepare changelog for v29.1.0 2023-10-24 15:27:01 +01:00
Michael Telatynski 748d03ba11 Delete .github/workflows/upgrade_dependencies.yml 2023-10-24 00:48:22 +01:00
Richard van der Hoff 2f3f0b340e Bump matrix-sdk-crypto-wasm to 2.1.1 (#3823)
... for recent changes including a fix to a panic
2023-10-23 12:42:50 +01:00
Richard van der Hoff 12e479a93e Element-R: silence log errors when viewing a decryption failure (#3821) 2023-10-23 10:16:42 +00:00
Richard van der Hoff 6e2ac03f7e Reduce log spam from rust crypto (#3819)
* turn the log level down to DEBUG
* don't pointlessly log every call to `outgoingRequests`
2023-10-20 21:43:28 +00:00
David Baker 6359e10bcf Merge pull request #3820 from matrix-org/dbkr/export_base64
Export base64 utils
2023-10-20 18:18:08 +01:00
David Baker b3a2b8b8c4 Export base64 utils
We use these from react-sdk (from crypto, when they were there,
but I just moved them and inadvertantly broke react-sdk).
2023-10-20 17:47:57 +01:00
Valere 30a9119e31 Bump wasm bindings version to 2.1.0 (#3811) 2023-10-20 17:44:55 +01:00
David Baker 7a52dba86c Merge pull request #3818 from matrix-org/dbkr/all_your_base64
Refactor & make base64 functions browser-safe
2023-10-20 16:47:29 +01:00
David Baker d6177cdfc9 Another one appeared 2023-10-20 16:23:58 +01:00
David Baker c4f3fd3289 Remove another crypto mention. None of this is crypto specific. 2023-10-20 16:11:48 +01:00
David Baker 31f38550e3 Refactor & make base64 functions browser-safe
We had two identical sets of base64 functions in the js-sdk, both
using Buffer which isn't really available in the browser unless you're
using an old webpack (ie. what element-web uses). This PR:

 * Takes the crypto base64 file and moves it out of crypto (because
   we use base64 for much more than just crypto)
 * Makes them work in a browser without the Buffer global
 * Removes the other base64 functions
 * Changes everything to use the new common ones
 * Adds a comment explaining why the function is kinda ugly and how
   soul destroyingly awful the JS ecosystem is.
 * Runs the tests with both impls
 * Changes the test to not just test the decoder against the encoder
 * Adds explicit support & tests for (decoding) base64Url (I'll add an
   encode method later, no need for that to go in this PR too).
2023-10-20 16:00:55 +01:00
Andy Balaam 0643f38592 Don't remove thread info from a thread root when it is redacted (#3814)
* Don't remove thread info from a thread root when it is redacted

* Move the redaction event to main at the same time we move redacted

Since the redacted event is moving to the main timeline, the redaction
belongs there too, since its relationship to the redacted event is the
only thing making it part of the thread.
2023-10-20 14:45:34 +00:00
Richard van der Hoff c0264954ed Reduce some log spam from MatrixRTCSession.callMembershipsForRoom (#3812)
This gets logged very frequently - including once for each room at startup -
and it's filling the logs.
2023-10-20 09:49:20 +00:00
Richard van der Hoff 7501e28dec Element-R: log when we send to-device messages (#3810)
* Log when we send to-device messages

* lint

* fix test
2023-10-19 12:58:49 +00:00
Valere febc4c9ad6 Handle backup secret gossip (#3778)
* Handle backup secret gossip

* use getSecretsFromInbox

* add gossip test

* use delete secret API

* fix logger

* better comment and cleaning

* free the pkSigning

* fix typo

* add missing mocks

* improve coverage

* better var name

* quick refactoring

* add more tests

* Review, format and comments

* refactor move more logic to backup.ts

* poll secret inbox

* missing mock

* Update src/rust-crypto/rust-crypto.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/rust-crypto/rust-crypto.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/rust-crypto/rust-crypto.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/rust-crypto/backup.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/rust-crypto/rust-crypto.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* code review

* fix comment

* remove comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* quick factorise

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-10-19 12:39:16 +00:00
Andy Balaam 6b1d53cc14 Move events related to a redacted event into the main timeline (#3800)
* Move redaction event tests into their own describe block

* Factor out utils in redaction tests

* Factor out the code for moving an event to the main timeline

* Move all related messages into main timeline on redaction
2023-10-19 07:58:46 +00:00
renovate[bot] 04fcd5880b Update all non-major dependencies (#3776)
* Update all non-major dependencies

* Remove `name wrap-ansi-cjs`

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

* Remove `name string-width-cjs`

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

* Hold jest-sonar-reporter back

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-10-19 07:31:15 +00:00
Kerry 4bcea2cead OIDC: document OidcError use (#3808)
* comments

* Update src/oidc/authorize.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/oidc/register.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-10-18 21:01:56 +00:00
renovate[bot] 6468d79458 Update typedoc (#3740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 11:49:04 +00:00
renovate[bot] a871376350 Update dependency fake-indexeddb to v5 (#3803)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-18 08:12:53 +00:00
Michael Telatynski 6beb693616 Update cypress.yml 2023-10-18 09:11:40 +01:00
Andy Balaam 11661bbc8d Merge pull request #3798 from matrix-org/andybalaam/move-redacted-message-to-main
Move redacted messages out of any thread, into main timeline.
2023-10-18 08:00:29 +01:00
renovate[bot] 2d57f28d5a Update matrix-org/matrix-react-sdk digest to 94ca061 (#3774)
* Update matrix-org/matrix-react-sdk digest to 94ca061

* Update cypress.yml

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-10-17 17:40:07 +00:00
dependabot[bot] c52f857599 Bump @babel/traverse from 7.22.17 to 7.23.2 (#3806)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.17 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-17 17:05:31 +00:00
renovate[bot] 5d016c1e4f Update tspascoal/get-user-teams-membership action to v3 (#3804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-17 16:50:40 +00:00
Richard van der Hoff 9f04c0555c Deprecate MatrixEvent.toJSON (#3801)
* Deprecate `MatrixEvent.toJSON`

Per https://github.com/vector-im/element-web/issues/26380, this method is too
easy to use accidentally, and per the comments, it doesn't even return a
meaningful JSON-serialisation of the object.

* Update src/models/event.ts
2023-10-17 15:54:02 +00:00
renovate[bot] 9293986e3b Update definitelyTyped (#3775)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-17 15:44:16 +00:00
renovate[bot] 8426d8cae1 Update babel monorepo (#3777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-17 15:07:20 +00:00
Andy Balaam 3baf6ec2c6 Add events to the thread even if they appear to be out of order (#3787) 2023-10-17 14:15:15 +00:00
ElementRobot 38cd6f93e6 v29.1.0-rc.1 2023-10-17 15:09:00 +01:00
ElementRobot a3a6742c67 Prepare changelog for v29.1.0-rc.1 2023-10-17 15:08:57 +01:00
David Baker 4ce837b20e Fix sending call member events on leave (#3799)
https://github.com/matrix-org/matrix-js-sdk/pull/3756 changed
the membership update function to await on the next call, but this
meant it never returned and therefore never cleared
`updateCallMembershipRunning`. We therefore didn't send the updated
call member event when leaving, instead sending it whenever the next
poll interval arrived.

This changes it to only await if we are retrying, not if we're just
scheduling the next poll.

Fixes https://github.com/vector-im/element-call/issues/1763
2023-10-17 13:26:18 +00:00
Michael Telatynski 884bd2585a Prettier
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-10-17 10:27:19 +01:00
Michael Telatynski c306d87f80 Add SUMMARY.md
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2023-10-17 10:01:17 +01:00
Andy Balaam b94d137398 Move redacted messages out of any thread, into main timeline.
For consistency with the spec at room version 11. See
https://github.com/matrix-org/matrix-spec-proposals/pull/3389
for a proposal to make this unnecessary.
2023-10-16 12:49:57 +01:00
Andy Balaam 5595e8497f Clarify code that chooses a thread ID to include in a receipt (#3797)
* Extract threadIdForReceipt function from sendReceipt

* Tests for threadIdForReceipt

* Correct test of threadIdForReceipt to expect main for redaction of threaded

* Expand and comment implementation of threadIdForReceipt
2023-10-16 10:35:36 +00:00
Valere 5d233f3863 Bump wasm bindings version to 2.0.0 (#3795) 2023-10-12 16:29:30 +01:00
Kerry 0f4fa5ad51 OIDC: refresh tokens (#3764)
* very messy poc

* iterate

* more types and use tokenRefreshFunction

* working refresh without persistence

* tidy

* add claims to completeauhtorizationcodegrant response

* export tokenrefresher from matrix

* add idtokenclaims

* add claims to completeauhtorizationcodegrant response

* only one token refresh attempt at a time

* tests

* comments

* add tokenRefresher class

* export generateScope

* export oidc from matrix

* test refreshtoken

* mark experimental

* add getRefreshToken to client

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* remove some vars in test

* make TokenRefresher un-abstract, comments and improvements

* remove invalid jsdoc

* Update src/oidc/tokenRefresher.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Code review improvements

* fix verification integ tests

* remove unused type from props

* fix incomplete mock fn in fetch.spec

* document TokenRefreshFunction

* comments

* tidying

* update for injected logger

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-10-11 22:00:02 +00:00
Kerry 1de6de05a1 add prompt param to OIDC auth url creation (#3794) 2023-10-11 02:20:23 +00:00
David Baker c8f8fb587d Don't use event.sender in CallMembership (#3793)
* Don't use event.sender in CallMembership

I fell into another js-sdk trap: this is "only guaranteed to be set
for events that appear in a timeline" and not state events. It does
not say why. We only ever used it to get the sender user ID anyway,
so just use getSender().

* Fix test
2023-10-10 15:19:52 +00:00
Richard van der Hoff 2f79e6c056 Element-R: Don't mark QR code verification as done until it's done (#3791)
* Element-R: Don't mark QR code verification as done too soon

The rust crypto sdk doesn't actually finish QR code verification until the
`m.key.verification.done` is received, so make sure we don't tell the
application it is done before that happens.

Fixes https://github.com/vector-im/element-web/issues/26293

* ignore fallback line

* Revert unnecessary changes

Can't get the coverage high enough on this and it's not needed.
2023-10-10 09:38:30 +00:00
Richard van der Hoff 42be793a56 Allow applications to specify their own logger instance (#3792)
* Support MatrixClient-specific loggers.

Signed-off-by: Patrick Cloke <clokep@patrick.cloke.us>

* Use client-specific logger in client.ts.

Signed-off-by: Patrick Cloke <clokep@patrick.cloke.us>

* Log `fetch` requests to the per-client logger

* Use client-specific logger in rust-crypto
2023-10-10 10:34:03 +01:00
Dharshan 7c2a12085c Fix small typo in README.md in root dir (#3788) 2023-10-10 08:37:04 +00:00
RiotRobot 3cf6f568f3 Resetting package fields for development 2023-10-10 09:13:20 +01:00
RiotRobot 4db08cb78e Merge branch 'master' into develop 2023-10-10 09:13:15 +01:00
RiotRobot 25e5d79cf6 v29.0.0 2023-10-10 09:13:04 +01:00
RiotRobot 6c8e3d0707 Prepare changelog for v29.0.0 2023-10-10 09:13:01 +01:00
Kerry 3139f5729b OIDC: Token refresher class (#3769)
* add tokenRefresher class

* export generateScope

* export oidc from matrix

* mark experimental

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* remove some vars in test

* make TokenRefresher un-abstract, comments and improvements

* remove invalid jsdoc

* Update src/oidc/tokenRefresher.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Code review improvements

* document TokenRefreshFunction

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-10-09 20:46:43 +00:00
Florian Duros bb8a894105 Call RustBackupManager.checkKeyBackupAndEnable when RustCrypto is created (#3784) 2023-10-09 13:23:45 +00:00
Richard van der Hoff 223dfffdfb Define a new Logger interface (#3789)
* rename loglevel import to loglevel

* Define new `Logger` interface to replace `PrefixedLogger`

* PrefixedLogger -> Logger in crypto store

* PrefixedLogger -> Logger in `src/crypto`

* PrefixedLogger -> Logger in rust-crypto
2023-10-09 13:06:16 +00:00
Andy Balaam f19f0a8793 Comments attempting to explain the addEvent method (#3786)
* Warn when we drop an event trying to add it to a thread

Added to try and help debug https://github.com/vector-im/element-web/issues/26254

* Comment trying to explain lastEvent

* Document some of my understanding of the addEvent logic

* Refer to stable spec instead of MSC

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Re-word comments based on review

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-10-09 12:47:16 +00:00
Timo a5224c1820 make leaveRoomSession async (#3756)
* make leaveRoomSession async.
This does not resolve the promise until the event is actually send.
No network connection would make awaiting on this blocking.

Signed-off-by: Timo K <toger5@hotmail.de>

* add timeout to leave

Signed-off-by: Timo K <toger5@hotmail.de>

* formatting

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
2023-10-09 11:52:19 +00:00
Andy Balaam 513201b9c1 Warn when we drop an event trying to add it to a thread (#3785)
Added to try and help debug https://github.com/vector-im/element-web/issues/26254
2023-10-06 14:40:57 +00:00
Richard van der Hoff 02ca5c78cf Element-R: don't log every HTTP request (#3780)
Since #3485, we log every request anyway, so there's no need to log twice.,
2023-10-05 14:31:28 +00:00
Florian Duros af63d9bd05 Element-R: Avoid errors in VerificationRequest.generateQRCode when QR code is unavailable (#3779)
* Avoid `VerificationRequest.generateQRCode` to crash when QRCode is unavailable

* Add tests `can try to generate a QR code when QR code is not supported`
2023-10-05 08:17:39 +00:00
Valere 95baccfbc1 Rust crypto: ensure we persist the key backup version (#3770)
Fixes vector-im/element-web#26259
2023-10-04 11:38:50 +01:00
Richard van der Hoff 10b6c2463d Grab bag of Element-R cleanups (#3773)
* `RustBackupManager.getActiveBackupVersion`: check that backup is enabled

The previous check on `isBackupEnabled` was a no-op

* Fix log spam on shieldless events

* Reduce log spam about tracking users

* Reduce log spam about decrypting events

Logging the entire event is excessive
2023-10-04 09:15:54 +00:00
Kerry 6e8d15e5ed add claims to completeauhtorizationcodegrant response (#3765) 2023-10-04 05:05:54 +01:00
Florian Duros 2e4276437a ElementR: Check key backup when user identity changes (#3760)
Fixes vector-im/element-web#26244
2023-10-03 13:38:51 +01:00
Richard van der Hoff 6a761af867 Element-R: emit VerificationRequestReceived on incoming request (#3762) 2023-10-03 13:37:58 +01:00
RiotRobot 53a72df01b v29.0.0-rc.1 2023-10-03 11:40:01 +01:00
RiotRobot 75e710d93e Prepare changelog for v29.0.0-rc.1 2023-10-03 11:39:58 +01:00
RiotRobot 1457ab0cf4 Revert "v29.0.0-rc.1"
This reverts commit f01037fe0d.
2023-10-03 11:39:12 +01:00
RiotRobot 14aafb7977 Revert "Prepare changelog for v29.0.0-rc.1"
This reverts commit 2cda6655d7.
2023-10-03 11:39:03 +01:00
Andy Balaam 90d00b863f Merge pull request #3772 from matrix-org/andybalaam/manually-backport-remove-dist-from-package
Remove the dist script since we no longer have a browserify build
2023-10-03 11:34:08 +01:00
Andy Balaam 5f0ada9578 Remove the dist script since we no longer have a browserify build 2023-10-03 11:32:59 +01:00
RiotRobot f01037fe0d v29.0.0-rc.1 2023-10-03 11:08:34 +01:00
RiotRobot 2cda6655d7 Prepare changelog for v29.0.0-rc.1 2023-10-03 11:08:31 +01:00
Michael Telatynski 6eec2ceeeb Export AutoDiscoveryError and fix type of ALL_ERRORS (#3768) 2023-10-03 11:01:16 +01:00
Michael Telatynski 68317ac836 Remove browserify builds (#3759) 2023-10-03 10:23:11 +01:00
Michael Telatynski 5c45c980e9 Update cypress.yml 2023-10-03 08:40:40 +01:00
Damon (Toal-Rossi) Vestervand 66251e0855 Use globalThis instead of global (#3763)
Switches use of `global` to `globalThis`, which is better supported when building with modern build tools like Vite.

Refs #2903

Signed-off-by: Damon Vestervand <damon@beyondwork.ai>
Signed-off-by: Damon <damon@vestervand.net>
2023-10-02 12:04:05 +00:00
Richard van der Hoff ff53557957 Clean up integ tests for incoming user verification (#3758)
Move the tests into verification.spec.ts, enable for both stacks, and other cleanups.
2023-09-29 17:26:24 +01:00
Richard van der Hoff 126352afd5 Bump matrix-sdk-crypto-wasm to 1.3.0 (#3757) 2023-09-29 17:25:33 +01:00
Hugh Nimmo-Smith f33da83d90 Support for stable MSC3882 get_login_token (#3416)
* Support for stable MSC3882 get_login_token

* Make changes non-breaking by deprecation

* Update src/@types/auth.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update spec/integ/matrix-client-methods.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Suggestions from review

* Update src/client.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Fix and test prefix behaviour

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
2023-09-29 13:14:22 +00:00
Valere 74193ad057 Implement exportCrossSigningKeysToStorage (#3731)
* Implement exportCrossSigningKeysToStorage

* fix bootstrap cross signing

* Update src/rust-crypto/CrossSigningIdentity.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/rust-crypto/CrossSigningIdentity.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update src/rust-crypto/CrossSigningIdentity.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* Update spec/unit/rust-crypto/CrossSigningIdentity.spec.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

* code review

* Update src/rust-crypto/CrossSigningIdentity.ts

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-27 09:07:02 +00:00
Kerry c672cad1a1 remove IsUserMention and IsRoomMention from DEFAULT_OVERRIDE_RULES (#3752) 2023-09-26 21:33:41 +00:00
Andy Balaam d59bb240fa Merge pull request #3746 from matrix-org/valere/element-r/backup/restore_test
fix restoreKeyBackupWithSecretStorage for rust
2023-09-26 12:42:47 +01:00
RiotRobot 65d988734e Resetting package fields for development 2023-09-26 12:20:14 +01:00
RiotRobot 4a402f0bd7 Merge branch 'master' into develop 2023-09-26 12:20:05 +01:00
Valere 9fed45e47c quick test if no crypto 2023-09-26 12:05:27 +02:00
Valere fe67a68c95 fix typo 2023-09-26 09:13:19 +02:00
valere 4d3d4028a0 Merge branch 'develop' into valere/element-r/backup/restore_test 2023-09-26 09:05:03 +02:00
Robin 8f901590ff Fix a case where joinRoom creates a duplicate Room object (#3747)
When calling MatrixClient.joinRoom with a room alias, the method would create a new Room object, even if you were already present in that room. This changes its behavior to no-op, as the doc comment promises.
2023-09-25 18:07:51 +00:00
Michael Telatynski d29b8520f7 Update cypress.yml 2023-09-25 17:17:26 +01:00
Richard van der Hoff 37e1fd9af5 Another attempt at fixing the Cypress job (#3749) 2023-09-25 15:58:38 +01:00
Valere 76dbc7500f Update spec/integ/crypto/crypto.spec.ts
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-25 16:34:25 +02:00
Valere 3664f8c3c2 Update spec/integ/crypto/crypto.spec.ts
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-25 16:34:17 +02:00
Valere d0a10497bb Update spec/integ/crypto/crypto.spec.ts
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2023-09-25 16:34:10 +02:00
David Baker 6385c9c0da Add membershipID to call memberships (#3745)
* Add membershipID to call memberships

This allows us to recognise easily when a membership is from some
previous sessions rather than our own and therefore ignore it
(see comment for more).

This was causing us to see existing, expired membership events and
bump the expiry on them rather than send a new membership. This might
have been okay if we bumped them enough to actually make them un-expired,
but it's a fresh session so semanticly we want to post a fresh membership
rather than resurrecting a previous, expired membership.

* Fix test types

* Fix tests

* Make test coverage happy

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2023-09-22 15:31:17 +00:00
valere cecac3152e Merge branch 'develop' into valere/element-r/backup/restore_test 2023-09-22 14:09:35 +02:00
Valere f6c99b1d25 fix restoreKeyBackupWithSecretStorage for rust 2023-09-22 13:28:05 +02:00
Richard van der Hoff 407ec4d67a Add github action to mark cypress tests skipped (#3744) 2023-09-22 09:22:00 +00:00
Valere 4947a0cb64 Implement isSecretStorageReady in rust (#3730)
* Implement isSecretStorageReady in rust

* refactor extract common code to check 4S access

* fix incomplete mocks

* code review

* Remove keyId para from secretStorageCanAccessSecrets

* use map instead of array

* code review
2023-09-21 16:55:41 +00:00
Richard van der Hoff f134d6db01 Fix the message for messages from unknown devices (#3743) 2023-09-21 08:34:34 +00:00
Malte Finsterwalder fde6cebc20 Stop keep alive, when sync was stoped (#3720)
* T-Defect: stop keep alive, when sync was stoped

Signed-off-by: Malte Finsterwalder <malte@holi.team>

* T-Defect: add tests for keep alive

Signed-off-by: Malte Finsterwalder <malte@holi.team>

* fix copyright year

Signed-off-by: Malte Finsterwalder <malte@holi.team>

* fix copyright

Signed-off-by: Malte Finsterwalder <malte@holi.team>

---------

Signed-off-by: Malte Finsterwalder <malte@holi.team>
Co-authored-by: Malte Finsterwalder <malte@holi.team>
2023-09-20 16:44:27 +00:00
Richard van der Hoff 425cf6b91e Element-R: use the pickleKey to encrypt the crypto store (#3732)
* Element-R: use the pickleKey to encrypt the crypto store

`pickleKey` is a passphrase set by the application for this express purpose.

* update tests

* fix tests, again
2023-09-20 11:35:32 +00:00
renovate[bot] a3e273d6f1 Update all non-major dependencies (#3735)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-20 10:26:16 +00:00
renovate[bot] b1a3b264e5 Update SimenB/github-actions-cpu-cores action to v2 (#3741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-20 10:25:40 +00:00
renovate[bot] 053643a8ba Update babel monorepo to v7.22.20 (#3737)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 19:38:40 +00:00
Richard van der Hoff d2ea149012 Undeprecate MatrixClient.getKeyBackupVersion (#3733)
... and add some better documentation while we're at it.
2023-09-19 17:33:41 +00:00
Richard van der Hoff 23d244520c Sort imports in MatrixClient (#3734)
My IDE keeps trying to do this, so let's just do it in its own PR
2023-09-19 17:32:58 +00:00
renovate[bot] 267b52099b Update definitelyTyped (#3736)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 16:18:45 +00:00
renovate[bot] 430fd5660a Update dependency @types/jest to v29.5.5 (#3738)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-19 16:18:08 +00:00
147 changed files with 9307 additions and 4257 deletions
@@ -0,0 +1,28 @@
name: Sign Release Tarball
description: Generates signature for release tarball and uploads it as a release asset
inputs:
gpg-fingerprint:
description: Fingerprint of the GPG key to use for signing the tarball.
required: true
upload-url:
description: GitHub release upload URL to upload the signature file to.
required: true
runs:
using: composite
steps:
- name: Generate tarball signature
shell: bash
run: |
git -c tar.tar.gz.command='gzip -cn' archive --format=tar.gz --prefix="${REPO#*/}-${VERSION#v}/" -o "/tmp/${VERSION}.tar.gz" "${VERSION}"
gpg -u "$GPG_FINGERPRINT" --armor --output "${VERSION}.tar.gz.asc" --detach-sig "/tmp/${VERSION}.tar.gz"
rm "/tmp/${VERSION}.tar.gz"
env:
GPG_FINGERPRINT: ${{ inputs.gpg-fingerprint }}
REPO: ${{ github.repository }}
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -0,0 +1,41 @@
name: Upload release assets
description: Uploads assets to an existing release and optionally signs them
inputs:
gpg-fingerprint:
description: Fingerprint of the GPG key to use for signing the assets, if any.
required: false
upload-url:
description: GitHub release upload URL to upload the assets to.
required: true
asset-path:
description: |
The path to the asset you want to upload, if any. You can use glob patterns here.
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
required: true
runs:
using: composite
steps:
- name: Sign assets
if: inputs.gpg-fingerprint
shell: bash
run: |
for FILE in $ASSET_PATH
do
gpg -u "$GPG_FINGERPRINT" --armor --output "$FILE".asc --detach-sig "$FILE"
done
env:
GPG_FINGERPRINT: ${{ inputs.gpg-fingerprint }}
ASSET_PATH: ${{ inputs.asset-path }}
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@dccd6d23e64fd6a746dce6814c0bde0a04886085 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+31
View File
@@ -0,0 +1,31 @@
name-template: "v$RESOLVED_VERSION"
tag-template: "v$RESOLVED_VERSION"
change-template: "* $TITLE ([#$NUMBER]($URL)). Contributed by @$AUTHOR."
categories:
- title: "🚨 BREAKING CHANGES"
label: "X-Breaking-Change"
- title: "🦖 Deprecations"
label: "T-Deprecation"
- title: "✨ Features"
label: "T-Enhancement"
- title: "🐛 Bug Fixes"
label: "T-Defect"
- title: "🧰 Maintenance"
label: "Dependencies"
collapse-after: 5
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
labels:
- "X-Breaking-Change"
default: minor
exclude-labels:
- "T-Task"
- "X-Reverted"
exclude-contributors:
- "RiotRobot"
template: |
$CHANGES
prerelease: true
prerelease-identifier: rc
include-pre-releases: false
+29 -2
View File
@@ -15,7 +15,12 @@ concurrency:
jobs:
cypress:
name: Cypress
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@v3.79.0
# We only want to run the cypress tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume 4 concurrent runners.
if: github.event.workflow_run.event == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/cypress.yaml@f6ef476f7905cc2b1f060f1a360b482e7546e682
permissions:
actions: read
issues: read
@@ -28,4 +33,26 @@ jobs:
TCMS_PASSWORD: ${{ secrets.TCMS_PASSWORD }}
with:
react-sdk-repository: matrix-org/matrix-react-sdk
rust-crypto: true
# We want to make the cypress tests a required check for the merge queue.
#
# Unfortunately, github doesn't distinguish between "checks needed for branch
# protection" (ie, the things that must pass before the PR will even be added
# to the merge queue) and "checks needed in the merge queue". We just have to add
# the check to the branch protection list.
#
# Ergo, if we know we're not going to run the cypress tests, we need to add a
# passing status check manually.
mark_skipped:
if: github.event.workflow_run.event != 'merge_group'
permissions:
statuses: write
runs-on: ubuntu-latest
steps:
- uses: Sibz/github-status-action@650dd1a882a76dbbbc4576fb5974b8d22f29847f # v1.1.6
with:
authToken: "${{ secrets.GITHUB_TOKEN }}"
state: success
description: Cypress skipped
context: "${{ github.workflow }} / cypress"
sha: "${{ github.event.workflow_run.head_sha }}"
+1
View File
@@ -32,3 +32,4 @@ jobs:
site_id: ${{ secrets.NETLIFY_SITE_ID }}
desc: Documentation preview
deployment_env: PR Documentation Preview
environment: PR Documentation Preview
+3 -4
View File
@@ -1,11 +1,10 @@
name: Build downstream artifacts
on:
# We only want the Rust Crypto Cypress tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume 4 concurrent runners.
# Anyone working on Rust Crypto is able to run the tests locally if required.
merge_group:
types: [checks_requested]
pull_request: {}
# For now at least, we don't run this or the cypress-tests against pushes
# to develop or master.
#
@@ -21,7 +20,7 @@ concurrency:
jobs:
build-element-web:
name: Build element-web
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.79.0
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.84.1
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
+4 -4
View File
@@ -27,7 +27,7 @@ jobs:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@v6
uses: actions/github-script@v7
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
@@ -39,7 +39,7 @@ jobs:
if: github.event.action == 'opened'
steps:
- name: Check membership
uses: tspascoal/get-user-teams-membership@37c08f7b52a72ca95d12af2e7ab2553ca9adf13b # v2
uses: tspascoal/get-user-teams-membership@ba78054988f58bea69b7c6136d563236f8ed2fc0 # v3
id: teams
with:
username: ${{ github.event.pull_request.user.login }}
@@ -49,7 +49,7 @@ jobs:
- name: Add label
if: ${{ steps.teams.outputs.isTeamMember == 'false' }}
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
@@ -68,7 +68,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
+21
View File
@@ -0,0 +1,21 @@
name: Release Drafter
on:
push:
branches: [staging]
workflow_dispatch:
inputs:
previous-version:
description: What release to use as a base for release note purposes
required: false
type: string
concurrency: ${{ github.workflow }}
jobs:
draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@e64b19c4c46173209ed9f2e5a2f4ca7de89a0e86 # v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
disable-autolabeler: true
previous-version: ${{ inputs.previous-version }}
+85
View File
@@ -0,0 +1,85 @@
# Gitflow merge-back master->develop
name: Merge master -> develop
on:
push:
branches: [master]
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: true
inputs:
dependencies:
description: List of dependencies to reset.
type: string
required: false
concurrency: ${{ github.workflow }}
jobs:
merge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
- name: Get actions scripts
uses: actions/checkout@v4
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
path: .action-repo
sparse-checkout: |
scripts/release
- uses: actions/setup-node@v4
with:
cache: "yarn"
- name: Install Deps
run: "yarn install --frozen-lockfile"
- name: Set up git
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- name: Merge to develop
run: |
git checkout develop
git merge -X ours master
- name: Run post-merge-master script to revert package.json fields
run: ./.action-repo/scripts/release/post-merge-master.sh
- name: Reset dependencies
if: inputs.dependencies
run: |
while IFS= read -r PACKAGE; do
[ -z "$PACKAGE" ] && continue
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
echo "Current $PACKAGE version is $CURRENT_VERSION"
if [ "$CURRENT_VERSION" == "null" ]
then
echo "Unable to find $PACKAGE in package.json"
exit 1
fi
if [ "$CURRENT_VERSION" == "develop" ]
then
echo "Not updating dependency $PACKAGE"
continue
fi
echo "Resetting $1 to develop branch..."
yarn add "github:matrix-org/$PACKAGE#develop"
git add -u
git commit -m "Reset $PACKAGE back to develop branch"
done <<< "$DEPENDENCIES"
env:
DEPENDENCIES: ${{ inputs.dependencies }}
FINAL: ${{ inputs.final }}
- name: Push changes
run: git push origin develop
+353
View File
@@ -0,0 +1,353 @@
name: Release Make
on:
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: true
NPM_TOKEN:
required: false
GPG_PASSPHRASE:
required: false
GPG_PRIVATE_KEY:
required: false
inputs:
final:
description: Make final release
required: true
default: false
type: boolean
npm:
description: Publish to npm
type: boolean
default: false
dependencies:
description: |
List of dependencies to update in `npm-dep=version` format.
`version` can be `"current"` to leave it at the current version.
type: string
required: false
include-changes:
description: Project to include changelog entries from in this release.
type: string
required: false
gpg-fingerprint:
description: Fingerprint of the GPG key to use for signing the git tag and assets, if any.
type: string
required: false
asset-path:
description: |
The path to the asset you want to upload, if any. You can use glob patterns here.
Will be GPG signed and an `.asc` file included in the release artifacts if `gpg-fingerprint` is set.
type: string
required: false
expected-asset-count:
description: The number of expected assets, including signatures, excluding generated zip & tarball.
type: number
required: false
jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: Release
steps:
- name: Load GPG key
id: gpg
if: inputs.gpg-fingerprint
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ inputs.gpg-fingerprint }}
- name: Get draft release
id: release
uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada # v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
draft: true
latest: true
- uses: actions/checkout@v4
with:
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
- name: Get actions scripts
uses: actions/checkout@v4
with:
repository: matrix-org/matrix-js-sdk
persist-credentials: false
path: .action-repo
sparse-checkout: |
.github/actions
scripts/release
- name: Prepare variables
id: prepare
run: |
echo "VERSION=$VERSION" >> $GITHUB_ENV
{
echo "RELEASE_NOTES<<EOF"
echo "$BODY"
echo "EOF"
} >> $GITHUB_ENV
HAS_DIST=0
jq -e .scripts.dist package.json >/dev/null 2>&1 && HAS_DIST=1
echo "has-dist-script=$HAS_DIST" >> $GITHUB_OUTPUT
env:
BODY: ${{ steps.release.outputs.body }}
VERSION: ${{ steps.release.outputs.tag_name }}
- name: Finalise version
if: inputs.final
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
- name: Check version number not in use
uses: actions/github-script@v7
with:
script: |
const { VERSION } = process.env;
github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: VERSION,
}).then(() => {
core.setFailed(`Version ${VERSION} already exists`);
}).catch(() => {
// This is fine, we expect there to not be any release with this version yet
});
- name: Set up git
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- uses: actions/setup-node@v4
with:
cache: "yarn"
- name: Install dependencies
run: "yarn install --frozen-lockfile"
- name: Update dependencies
id: update-dependencies
if: inputs.dependencies
run: |
UPDATED=()
while IFS= read -r DEPENDENCY; do
[ -z "$DEPENDENCY" ] && continue
IFS="=" read -r PACKAGE UPDATE_VERSION <<< "$DEPENDENCY"
CURRENT_VERSION=$(cat package.json | jq -r .dependencies[\"$PACKAGE\"])
echo "Current $PACKAGE version is $CURRENT_VERSION"
if [ "$CURRENT_VERSION" == "null" ]
then
echo "Unable to find $PACKAGE in package.json"
exit 1
fi
if [ "$UPDATE_VERSION" == "current" ] || [ "$UPDATE_VERSION" == "$CURRENT_VERSION" ]
then
echo "Not updating dependency $PACKAGE"
continue
fi
echo "Upgrading $PACKAGE to $UPDATE_VERSION..."
yarn upgrade "$PACKAGE@$UPDATE_VERSION" --exact
git add -u
git commit -m "Upgrade $PACKAGE to $UPDATE_VERSION"
UPDATED+=("$PACKAGE")
done <<< "$DEPENDENCIES"
JSON=$(jq --compact-output --null-input '$ARGS.positional' --args -- "${UPDATED[@]}")
echo "updated=$JSON" >> $GITHUB_OUTPUT
env:
DEPENDENCIES: ${{ inputs.dependencies }}
- name: Prevent develop dependencies
if: inputs.dependencies
run: |
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
- name: Bump package.json version
run: yarn version --no-git-tag-version --new-version "${VERSION#v}"
- name: Ingest upstream changes
if: |
inputs.include-changes &&
(!inputs.dependencies || contains(fromJSON(steps.update-dependencies.outputs.updated), inputs.include-changes))
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
DEPENDENCY: ${{ inputs.include-changes }}
with:
retries: 3
script: |
const { RELEASE_ID: releaseId, DEPENDENCY, VERSION } = process.env;
const { owner, repo } = context.repo;
const script = require("./.action-repo/scripts/release/merge-release-notes.js");
const notes = await script({
github,
releaseId,
dependencies: [DEPENDENCY.replace("$VERSION", VERSION)],
});
core.exportVariable("RELEASE_NOTES", notes);
- name: Add to CHANGELOG.md
if: inputs.final
run: |
mv CHANGELOG.md CHANGELOG.md.old
HEADER="Changes in [${VERSION#v}](https://github.com/${{ github.repository }}/releases/tag/$VERSION) ($(date '+%Y-%m-%d'))"
{
echo "$HEADER"
printf '=%.0s' $(seq ${#HEADER})
echo ""
echo "$RELEASE_NOTES"
echo ""
} > CHANGELOG.md
cat CHANGELOG.md.old >> CHANGELOG.md
rm CHANGELOG.md.old
git add CHANGELOG.md
- name: Run pre-release script to update package.json fields
run: |
./.action-repo/scripts/release/pre-release.sh
git add package.json
- name: Commit changes
run: git commit -m "$VERSION"
- name: Build assets
if: steps.prepare.outputs.has-dist-script == '1'
run: DIST_VERSION="$VERSION" yarn dist
- name: Upload release assets & signatures
if: inputs.asset-path
uses: ./.action-repo/.github/actions/upload-release-assets
with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.release.outputs.upload_url }}
asset-path: ${{ inputs.asset-path }}
- name: Create signed tag
if: inputs.gpg-fingerprint
run: |
GIT_COMMITTER_EMAIL="$SIGNING_ID" GPG_TTY=$(tty) git tag -u "$SIGNING_ID" -m "Release $VERSION" "$VERSION"
env:
SIGNING_ID: ${{ steps.gpg.outputs.email }}
- name: Generate & upload tarball signature
if: inputs.gpg-fingerprint
uses: ./.action-repo/.github/actions/sign-release-tarball
with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.release.outputs.upload_url }}
# We defer pushing changes until after the release assets are built,
# signed & uploaded to improve the atomicity of this action.
- name: Push changes to staging
run: |
git push origin staging $TAG
git reset --hard
env:
TAG: ${{ inputs.gpg-fingerprint && env.VERSION || '' }}
- name: Validate tarball signature
if: inputs.gpg-fingerprint
run: |
wget https://github.com/$GITHUB_REPOSITORY/archive/refs/tags/$VERSION.tar.gz
gpg --verify "$VERSION.tar.gz.asc" "$VERSION.tar.gz"
- name: Validate release has expected assets
if: inputs.expected-asset-count
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
with:
retries: 3
script: |
const { RELEASE_ID: release_id, EXPECTED_ASSET_COUNT } = process.env;
const { owner, repo } = context.repo;
const { data: release } = await github.rest.repos.getRelease({
owner,
repo,
release_id,
});
if (release.assets.length !== parseInt(EXPECTED_ASSET_COUNT, 10)) {
core.setFailed(`Found ${release.assets.length} assets but expected ${EXPECTED_ASSET_COUNT}`);
}
- name: Merge to master
if: inputs.final
run: |
git checkout master
git merge -X theirs staging
git push origin master
- name: Publish release
uses: actions/github-script@v7
env:
RELEASE_ID: ${{ steps.release.outputs.id }}
FINAL: ${{ inputs.final }}
with:
retries: 3
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
script: |
const { RELEASE_ID: release_id, RELEASE_NOTES, VERSION, FINAL } = process.env;
const { owner, repo } = context.repo;
const opts = {
owner,
repo,
release_id,
tag_name: VERSION,
name: VERSION,
draft: false,
body: RELEASE_NOTES,
};
if (FINAL == "true") {
opts.prerelease = false;
opts.make_latest = true;
}
github.rest.repos.updateRelease(opts);
npm:
name: Publish to npm
needs: release
if: inputs.npm
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
update-labels:
name: Advance release blocker labels
needs: release
runs-on: ubuntu-latest
steps:
- id: repository
run: echo "REPO=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT
- uses: garganshu/github-label-updater@3770d15ebfed2fe2cb06a241047bc340f774a7d1 # v1.0.0
with:
owner: ${{ github.repository_owner }}
repo: ${{ steps.repository.outputs.REPO }}
token: ${{ secrets.GITHUB_TOKEN }}
filter-labels: X-Upcoming-Release-Blocker
remove-labels: X-Upcoming-Release-Blocker
add-labels: X-Release-Blocker
+5 -4
View File
@@ -1,4 +1,3 @@
# Must only be called from `release#published` triggers
name: Publish to npm
on:
workflow_call:
@@ -12,9 +11,11 @@ jobs:
steps:
- name: 🧮 Checkout code
uses: actions/checkout@v4
with:
ref: staging
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
cache: "yarn"
registry-url: "https://registry.npmjs.org"
@@ -24,7 +25,7 @@ jobs:
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@5a85faf05d2ade2d5b6682bfe5359915d5159c6c # v2.2.1
uses: JS-DevTools/npm-publish@4b07b26a2f6e0a51846e1870223e545bae91c552 # v3.0.1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
@@ -32,7 +33,7 @@ jobs:
ignore-scripts: false
- name: 🎖️ Add `latest` dist-tag to final releases
if: github.event.release.prerelease == false && steps.npm-publish.outputs.id
if: steps.npm-publish.outputs.id && !contains(steps.npm-publish.outputs.id, '-rc.')
run: npm dist-tag add "$release" latest
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+36 -12
View File
@@ -1,11 +1,38 @@
name: Release Process
on:
release:
types: [published]
concurrency: ${{ github.workflow }}-${{ github.ref }}
workflow_dispatch:
inputs:
mode:
description: What type of release
required: true
default: rc
type: choice
options:
- rc
- final
docs:
description: Publish docs
required: true
type: boolean
default: true
npm:
description: Publish to npm
required: true
type: boolean
default: true
concurrency: ${{ github.workflow }}
jobs:
jsdoc:
release:
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
secrets: inherit
with:
final: ${{ inputs.mode == 'final' }}
npm: ${{ inputs.npm }}
docs:
name: Publish Documentation
needs: release
if: inputs.docs
runs-on: ubuntu-latest
steps:
- name: 🧮 Checkout code
@@ -18,7 +45,7 @@ jobs:
path: _docs
- name: 🔧 Yarn cache
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
cache: "yarn"
@@ -36,17 +63,14 @@ jobs:
yarn gendoc
symlinks -rc _docs
- name: 🚀 Deploy
- name: 🔨 Set up git
run: |
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- name: 🚀 Deploy
run: |
git add . --all
git commit -m "Update docs"
git push
working-directory: _docs
npm:
name: Publish
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.5
uses: matrix-org/sonarcloud-workflow-action@v2.7
# workflow_run fails report against the develop commit always, we don't want that for PRs
continue-on-error: ${{ github.event.workflow_run.head_branch != 'develop' }}
with:
+19 -3
View File
@@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
cache: "yarn"
@@ -41,7 +41,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
cache: "yarn"
@@ -51,13 +51,29 @@ jobs:
- name: Run Linter
run: "yarn run lint:js"
workflow_lint:
name: "Workflow Lint"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
- name: Install Deps
run: "yarn install --frozen-lockfile"
- name: Run Linter
run: "yarn lint:workflows"
docs:
name: "JSDoc Checker"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
cache: "yarn"
+7 -10
View File
@@ -12,19 +12,20 @@ env:
ENABLE_COVERAGE: ${{ github.event_name != 'merge_group' }}
jobs:
jest:
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node }})"
name: "Jest [${{ matrix.specs }}] (Node ${{ matrix.node == '*' && 'latest' || matrix.node }})"
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
specs: [browserify, integ, unit]
node: [18, latest]
specs: [integ, unit]
node: [18, "lts/*", 21]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v3
id: setupNode
uses: actions/setup-node@v4
with:
cache: "yarn"
node-version: ${{ matrix.node }}
@@ -32,13 +33,9 @@ jobs:
- name: Install dependencies
run: "yarn install"
- name: Build
if: matrix.specs == 'browserify'
run: "yarn build"
- name: Get number of CPU cores
id: cpu-cores
uses: SimenB/github-actions-cpu-cores@410541432439795d30db6501fb1d8178eb41e502 # v1
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2
- name: Run tests
run: |
@@ -55,7 +52,7 @@ jobs:
- name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true'
run: mv coverage/lcov.info coverage/${{ matrix.node }}-${{ matrix.specs }}.lcov.info
run: mv coverage/lcov.info coverage/${{ steps.setupNode.output.node-version }}-${{ matrix.specs }}.lcov.info
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
+11
View File
@@ -0,0 +1,11 @@
name: Move labelled issues to correct projects
on:
issues:
types: [labeled]
jobs:
call-triage-labelled:
uses: vector-im/element-web/.github/workflows/triage-labelled.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
@@ -1,38 +0,0 @@
name: Upgrade Dependencies
on:
workflow_dispatch: {}
workflow_call:
secrets:
ELEMENT_BOT_TOKEN:
required: true
jobs:
upgrade:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
cache: "yarn"
- name: Upgrade
run: yarn upgrade && yarn install
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/upgrade-deps
delete-branch: true
title: Upgrade dependencies
labels: |
Dependencies
T-Task
- name: Enable automerge
run: gh pr merge --merge --auto "$PR_NUMBER"
if: steps.cpr.outputs.pull-request-operation == 'created'
env:
GH_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
PR_NUMBER: ${{ steps.cpr.outputs.pull-request-number }}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
+4
View File
@@ -0,0 +1,4 @@
{
"*.(ts|tsx)": ["eslint --fix", "prettier --write"],
"*.(py|md|yaml)": ["prettier --write"]
}
+91
View File
@@ -1,3 +1,94 @@
Changes in [30.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30.2.0) (2023-12-05)
==================================================================================================
## ✨ Features
* Only await key query after lazy members resolved ([#3902](https://github.com/matrix-org/matrix-js-sdk/pull/3902)). Contributed by @BillCarsonFr.
## 🐛 Bug Fixes
* Rewrite receipt-handling code ([#3901](https://github.com/matrix-org/matrix-js-sdk/pull/3901)). Contributed by @andybalaam.
* Explicitly free some Rust-side objects ([#3911](https://github.com/matrix-org/matrix-js-sdk/pull/3911)). Contributed by @richvdh.
* Fix type for TimestampToEventResponse.origin\_server\_ts ([#3906](https://github.com/matrix-org/matrix-js-sdk/pull/3906)). Contributed by @Half-Shot.
Changes in [30.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30.1.0) (2023-11-21)
==================================================================================================
## ✨ Features
* Rotate per-participant keys when a member leaves ([#3833](https://github.com/matrix-org/matrix-js-sdk/pull/3833)). Contributed by @dbkr.
* Add E2EE for embedded mode of Element Call ([#3667](https://github.com/matrix-org/matrix-js-sdk/pull/3667)). Contributed by @SimonBrandner.
## 🐛 Bug Fixes
* Shorten TimelineWindow when an event is removed ([#3862](https://github.com/matrix-org/matrix-js-sdk/pull/3862)). Contributed by @andybalaam.
* Ignore receipts pointing at missing or invalid events ([#3817](https://github.com/matrix-org/matrix-js-sdk/pull/3817)). Contributed by @andybalaam.
* Fix members being loaded from server on initial sync (defeating lazy loading) ([#3830](https://github.com/matrix-org/matrix-js-sdk/pull/3830)). Contributed by @BillCarsonFr.
Changes in [30.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30.0.1) (2023-11-13)
==================================================================================================
## 🐛 Bug Fixes
* Ensure `setUserCreator` is called when a store is assigned ([\#3867](https://github.com/matrix-org/matrix-js-sdk/pull/3867)). Fixes vector-im/element-web#26520. Contributed by @MidhunSureshR.
Changes in [30.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v30.0.0) (2023-11-07)
==================================================================================================
## 🚨 BREAKING CHANGES
* Refactor & make base64 functions browser-safe ([\#3818](https://github.com/matrix-org/matrix-js-sdk/pull/3818)).
## 🦖 Deprecations
* Deprecate `MatrixEvent.toJSON` ([\#3801](https://github.com/matrix-org/matrix-js-sdk/pull/3801)).
## ✨ Features
* Element-R: Add the git sha of the binding crate to `CryptoApi#getVersion` ([\#3838](https://github.com/matrix-org/matrix-js-sdk/pull/3838)). Contributed by @florianduros.
* Element-R: Wire up `globalBlacklistUnverifiedDevices` field to rust crypto encryption settings ([\#3790](https://github.com/matrix-org/matrix-js-sdk/pull/3790)). Fixes vector-im/element-web#26315. Contributed by @florianduros.
* Element-R: Wire up room rotation ([\#3807](https://github.com/matrix-org/matrix-js-sdk/pull/3807)). Fixes vector-im/element-web#26318. Contributed by @florianduros.
* Element-R: Add current version of the rust-sdk and vodozemac ([\#3825](https://github.com/matrix-org/matrix-js-sdk/pull/3825)). Contributed by @florianduros.
* Element-R: Wire up room history visibility ([\#3805](https://github.com/matrix-org/matrix-js-sdk/pull/3805)). Fixes vector-im/element-web#26319. Contributed by @florianduros.
* Element-R: log when we send to-device messages ([\#3810](https://github.com/matrix-org/matrix-js-sdk/pull/3810)).
## 🐛 Bug Fixes
* Fix reemitter not being correctly wired on user objects created in storage classes ([\#3796](https://github.com/matrix-org/matrix-js-sdk/pull/3796)). Contributed by @MidhunSureshR.
* Element-R: silence log errors when viewing a pending event ([\#3824](https://github.com/matrix-org/matrix-js-sdk/pull/3824)).
* Don't emit a closed event if the indexeddb is closed by Element ([\#3832](https://github.com/matrix-org/matrix-js-sdk/pull/3832)). Fixes vector-im/element-web#25941. Contributed by @dhenneke.
* Element-R: silence log errors when viewing a decryption failure ([\#3821](https://github.com/matrix-org/matrix-js-sdk/pull/3821)).
Changes in [29.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v29.1.0) (2023-10-24)
==================================================================================================
## ✨ Features
* OIDC: refresh tokens ([\#3764](https://github.com/matrix-org/matrix-js-sdk/pull/3764)). Contributed by @kerryarchibald.
* OIDC: add `prompt` param to auth url creation ([\#3794](https://github.com/matrix-org/matrix-js-sdk/pull/3794)). Contributed by @kerryarchibald.
* Allow applications to specify their own logger instance ([\#3792](https://github.com/matrix-org/matrix-js-sdk/pull/3792)). Fixes #1899.
* Export AutoDiscoveryError and fix type of ALL_ERRORS ([\#3768](https://github.com/matrix-org/matrix-js-sdk/pull/3768)).
## 🐛 Bug Fixes
* Fix sending call member events on leave ([\#3799](https://github.com/matrix-org/matrix-js-sdk/pull/3799)). Fixes vector-im/element-call#1763.
* Don't use event.sender in CallMembership ([\#3793](https://github.com/matrix-org/matrix-js-sdk/pull/3793)).
* Element-R: Don't mark QR code verification as done until it's done ([\#3791](https://github.com/matrix-org/matrix-js-sdk/pull/3791)). Fixes vector-im/element-web#26293.
* Element-R: Connect device to key backup when crypto is created ([\#3784](https://github.com/matrix-org/matrix-js-sdk/pull/3784)). Fixes vector-im/element-web#26316. Contributed by @florianduros.
* Element-R: Avoid errors in `VerificationRequest.generateQRCode` when QR code is unavailable ([\#3779](https://github.com/matrix-org/matrix-js-sdk/pull/3779)). Fixes vector-im/element-web#26300. Contributed by @florianduros.
* ElementR: Check key backup when user identity changes ([\#3760](https://github.com/matrix-org/matrix-js-sdk/pull/3760)). Fixes vector-im/element-web#26244. Contributed by @florianduros.
* Element-R: emit `VerificationRequestReceived` on incoming request ([\#3762](https://github.com/matrix-org/matrix-js-sdk/pull/3762)). Fixes vector-im/element-web#26245.
Changes in [29.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v29.0.0) (2023-10-10)
==================================================================================================
## 🚨 BREAKING CHANGES
* Remove browserify builds ([\#3759](https://github.com/matrix-org/matrix-js-sdk/pull/3759)).
## ✨ Features
* Export AutoDiscoveryError and fix type of ALL_ERRORS ([\#3768](https://github.com/matrix-org/matrix-js-sdk/pull/3768)).
* Support for stable MSC3882 get_login_token ([\#3416](https://github.com/matrix-org/matrix-js-sdk/pull/3416)). Contributed by @hughns.
* Remove IsUserMention and IsRoomMention from DEFAULT_OVERRIDE_RULES ([\#3752](https://github.com/matrix-org/matrix-js-sdk/pull/3752)). Contributed by @kerryarchibald.
## 🐛 Bug Fixes
* Fix a case where joinRoom creates a duplicate Room object ([\#3747](https://github.com/matrix-org/matrix-js-sdk/pull/3747)).
* Add membershipID to call memberships ([\#3745](https://github.com/matrix-org/matrix-js-sdk/pull/3745)).
* Fix the warning for messages from unsigned devices ([\#3743](https://github.com/matrix-org/matrix-js-sdk/pull/3743)).
* Stop keep alive, when sync was stoped ([\#3720](https://github.com/matrix-org/matrix-js-sdk/pull/3720)). Contributed by @finsterwalder.
Changes in [28.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v28.2.0) (2023-09-26)
==================================================================================================
+2 -17
View File
@@ -23,19 +23,7 @@ endpoints from before Matrix 1.1, for example.
## In a browser
### Note, the browserify build has been deprecated. Please use a bundler like webpack or vite instead.
Download the browser version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
`<script>` to your page. There will be a global variable `matrixcs`
attached to `window` through which you can access the SDK. See below for how to
include libolm to enable end-to-end-encryption.
The browser bundle supports recent versions of browsers. Typically this is ES2015
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
[browserlists](https://github.com/browserslist/browserslist).
Please check [the working browser example](examples/browser) for more information.
### Note, the browserify build has been removed. Please use a bundler like webpack or vite instead.
## In Node.js
@@ -363,7 +351,7 @@ First, you need to pull in the right build tools:
## Building
To build a browser version from scratch when developing::
To build a browser version from scratch when developing:
```
$ yarn build
@@ -375,9 +363,6 @@ To run tests (Jest):
$ yarn test
```
> **Note**
> The `sync-browserify.spec.ts` requires a browser build (`yarn build`) in order to pass
To run linting:
```
+9
View File
@@ -0,0 +1,9 @@
# Summary
- [Introduction](../README.md)
# Deep dive
- [Release Process](release.md)
- [Storage notes](storage-notes.md)
- [Unverified devices](warning-on-unverified-devices.md)
+24
View File
@@ -0,0 +1,24 @@
# Release Process
## Hotfix and off-cycle releases
1. Prepare the `staging` branch by using the backport automation and manually merging
2. Go to [Releasing](#Releasing)
## Release candidates
1. Prepare the `staging` branch by running the [branch cut automation](https://github.com/vector-im/element-web/actions/workflows/release_prepare.yml)
2. Go to [Releasing](#Releasing)
## Releasing
1. Open the [Releases page](https://github.com/matrix-org/matrix-js-sdk/releases) and inspect the draft release there
2. Make any modifications to the release notes and tag/version as required
3. Run [workflow](https://github.com/matrix-org/matrix-js-sdk/actions/workflows/release.yml) with the type set appropriately
## Artifacts
Releasing the Matrix JS SDK has just two artifacts:
- Package published to [npm](https://github.com/matrix-org/matrix-js-sdk)
- Docs published to [Github Pages](https://matrix-org.github.io/matrix-js-sdk/)
@@ -1,31 +1,29 @@
Random notes from Matthew on the two possible approaches for warning users about unexpected
unverified devices popping up in their rooms....
Original idea...
================
# Original idea...
Warn when an existing user adds an unknown device to a room.
Warn when a user joins the room with unverified or unknown devices.
Warn when you initial sync if the room has any unverified devices in it.
^ this is good enough if we're doing local storage.
OR, better:
^ this is good enough if we're doing local storage.
OR, better:
Warn when you initial sync if the room has any new undefined devices since you were last there.
=> This means persisting the rooms that devices are in, across initial syncs.
=> This means persisting the rooms that devices are in, across initial syncs.
Updated idea...
===============
# Updated idea...
Warn when the user tries to send a message:
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
...this should be good enough.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
throw an error when trying to encrypt if there are pure unverified devices there
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
- or megolm could warn which devices are causing the problems.
- or megolm could warn which devices are causing the problems.
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
-10
View File
@@ -1,10 +0,0 @@
To try it out, **you must build the SDK first** and then host this folder:
```
$ yarn install
$ yarn build
$ cd examples/browser
$ python -m http.server 8003
```
Then visit `http://localhost:8003`.
-9
View File
@@ -1,9 +0,0 @@
console.log("Loading browser sdk");
var client = matrixcs.createClient({ baseUrl: "https://matrix.org" });
client.publicRooms().then(function (data) {
console.log("data %s [...]", JSON.stringify(data).substring(0, 100));
console.log("Congratulations! The SDK is working on the browser!");
var result = document.getElementById("result");
result.innerHTML = "<p>The SDK appears to be working correctly.</p>";
});
-17
View File
@@ -1,17 +0,0 @@
<html lang="en">
<head>
<title>Test</title>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
<script src="lib/matrix.js"></script>
<script src="browserTest.js"></script>
</head>
<body>
Sanity Testing (check the console) : This example is here to make sure that the SDK works inside a browser. It
simply does a GET /publicRooms on matrix.org
<br />
You should see a message confirming that the SDK works below:
<br />
<div id="result"></div>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
../../../dist/browser-matrix.js
-2
View File
@@ -1,2 +0,0 @@
olm.js
olm.wasm
-1
View File
@@ -1 +0,0 @@
../../../dist/browser-matrix.js
@@ -1,60 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Test Crypto in Browser</title>
<script src="lib/olm.js"></script>
<script src="lib/matrix.js"></script>
</head>
<body>
<h1>Testing export/import of Olm devices in the browser</h1>
<ul>
<li>Make sure you built the current version of the Matrix JS SDK (<code>yarn build</code>)</li>
<li>
copy <code>olm.js</code> and <code>olm.wasm</code> from a recent release of Olm (was tested with version
3.1.4) in directory <code>lib/</code>
</li>
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
<li>
in the JS console, do:
<pre>
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
await aliceMatrixClient.exportDevice();
await aliceMatrixClient.getAccessToken();
</pre
>
</li>
<li>
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere (<strong
>not</strong
>
in a JS variable as it will be destroyed when you refresh the page)
</li>
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
<li>
Do the following, replacing <code>ALICE_ID</code>
with the user ID of Alice (you can find it in the exported data)
<pre>
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
</pre
>
</li>
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
<li>
Now do the following, using the exported data and the access token you saved previously:
<pre>
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
</pre
>
</li>
<li>You should see the message sent by Bob printed in the console.</li>
</ul>
<script src="olm-device-export-import.js"></script>
</body>
</html>
@@ -1,105 +0,0 @@
if (!Olm) {
console.error("global.Olm does not seem to be present." + " Did you forget to add olm in the lib/ directory?");
}
const BASE_URL = "http://localhost:8008";
const ROOM_CRYPTO_CONFIG = { algorithm: "m.megolm.v1.aes-sha2" };
const PASSWORD = "password";
// useful to create new usernames
window.randomHex = () => Math.floor(Math.random() * 10 ** 6).toString(16);
window.newMatrixClient = async function (username) {
const registrationClient = matrixcs.createClient(BASE_URL);
const userRegisterResult = await registrationClient.register(username, PASSWORD, null, { type: "m.login.dummy" });
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
userId: userRegisterResult.user_id,
accessToken: userRegisterResult.access_token,
deviceId: userRegisterResult.device_id,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
};
window.importMatrixClient = async function (exportedDevice, accessToken) {
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
deviceToImport: exportedDevice,
accessToken,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
};
function extendMatrixClient(matrixClient) {
// automatic join
matrixClient.on("RoomMember.membership", async (event, member) => {
if (member.membership === "invite" && member.userId === matrixClient.getUserId()) {
await matrixClient.joinRoom(member.roomId);
// setting up of room encryption seems to be triggered automatically
// but if we don't wait for it the first messages we send are unencrypted
await matrixClient.setRoomEncryption(member.roomId, { algorithm: "m.megolm.v1.aes-sha2" });
}
});
matrixClient.onDecryptedMessage = (message) => {
console.log("Got encrypted message: ", message);
};
matrixClient.on("Event.decrypted", (event) => {
if (event.getType() === "m.room.message") {
matrixClient.onDecryptedMessage(event.getContent().body);
} else {
console.log("decrypted an event of type", event.getType());
console.log(event);
}
});
matrixClient.createEncryptedRoom = async function (usersToInvite) {
const { room_id: roomId } = await this.createRoom({
visibility: "private",
invite: usersToInvite,
});
// matrixClient.setRoomEncryption() only updates local state
// but does not send anything to the server
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
// so we do it ourselves with 'sendStateEvent'
await this.sendStateEvent(roomId, "m.room.encryption", ROOM_CRYPTO_CONFIG);
await this.setRoomEncryption(roomId, ROOM_CRYPTO_CONFIG);
// Marking all devices as verified
let room = this.getRoom(roomId);
let members = (await room.getEncryptionTargetMembers()).map((x) => x["userId"]);
let memberkeys = await this.downloadKeys(members);
for (const userId in memberkeys) {
for (const deviceId in memberkeys[userId]) {
await this.setDeviceVerified(userId, deviceId);
}
}
return roomId;
};
matrixClient.sendTextMessage = async function (message, roomId) {
return matrixClient.sendMessage(roomId, {
body: message,
msgtype: "m.text",
});
};
}
+16 -40
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "28.2.0",
"version": "30.2.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
@@ -8,19 +8,17 @@
"scripts": {
"prepublishOnly": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist",
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
"clean": "rimraf lib",
"build": "yarn build:dev",
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
"build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdir dist && BROWSERIFYSWAP_ENV='no-rust-crypto' browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] | 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": "typedoc",
"lint": "yarn lint:types && yarn lint:js",
"lint": "yarn lint:types && yarn lint:js && yarn lint:workflows",
"lint:js": "eslint --max-warnings 0 src spec && prettier --check .",
"lint:js-fix": "prettier --loglevel=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
@@ -42,7 +40,6 @@
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
"dist",
"lib",
"src",
"git-revision.txt",
@@ -55,7 +52,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^1.2.3-alpha.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^3.1.0",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
@@ -70,6 +67,8 @@
"uuid": "9"
},
"devDependencies": {
"@action-validator/cli": "^0.5.3",
"@action-validator/core": "^0.5.3",
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
@@ -82,8 +81,8 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@casualbot/jest-sonar-reporter": "^2.2.5",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@matrix-org/olm": "3.2.15",
"@types/bs58": "^4.0.1",
"@types/content-type": "^1.1.5",
"@types/debug": "^4.1.7",
@@ -96,12 +95,9 @@
"@typescript-eslint/parser": "^5.45.0",
"allchange": "^1.0.6",
"babel-jest": "^29.0.0",
"babelify": "^10.0.0",
"browserify": "^17.0.0",
"browserify-swap": "^0.2.2",
"debug": "^4.3.4",
"domexception": "^4.0.0",
"eslint": "8.48.0",
"eslint": "8.53.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
@@ -110,20 +106,20 @@
"eslint-plugin-jsdoc": "^46.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^48.0.0",
"exorcist": "^2.0.0",
"fake-indexeddb": "^4.0.0",
"eslint-plugin-unicorn": "^49.0.0",
"fake-indexeddb": "^5.0.0",
"fetch-mock": "9.11.0",
"fetch-mock-jest": "^1.5.1",
"husky": "^8.0.3",
"jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"prettier": "2.8.8",
"rimraf": "^5.0.0",
"terser": "^5.5.1",
"ts-node": "^10.9.1",
"tsify": "^5.0.2",
"typedoc": "^0.24.0",
"typedoc-plugin-coverage": "^2.1.0",
"typedoc-plugin-mdn-links": "^3.0.3",
@@ -137,25 +133,5 @@
"outputName": "jest-sonar-report.xml",
"relativePaths": true
},
"browserify": {
"transform": [
"browserify-swap",
[
"babelify",
{
"sourceMaps": "inline",
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
}
]
]
},
"browserify-swap": {
"no-rust-crypto": {
"src/rust-crypto/index.ts$": "./src/rust-crypto/browserify-index.ts"
}
},
"typings": "./lib/index.d.ts"
}
+1 -23
View File
@@ -10,28 +10,6 @@ set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
if [ "$(git branch -lr | grep origin/develop -c)" -ge 1 ]; then
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
for i in main typings browser
do
# If a `lib` prefixed value is present, it means we adjusted the field
# earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back
# to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
"$(dirname "$0")/scripts/release/post-merge-master.sh"
git push origin develop
fi
+1 -12
View File
@@ -175,18 +175,7 @@ echo "yarn version"
# manually commit the result.
yarn version --no-git-tag-version --new-version "$release"
# For the published and dist versions of the package, we copy the
# `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if
# they exist). This small bit of gymnastics allows us to use the TypeScript
# source directly for development without needing to build before linting or
# testing.
for i in main typings browser
do
lib_value=$(jq -r ".matrix_lib_$i" package.json)
if [ "$lib_value" != "null" ]; then
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
done
"$(dirname "$0")/scripts/release/pre-release.sh"
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && $(git status --porcelain yarn.lock | grep '^ M') ]];
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env node
const fs = require("fs");
async function getRelease(github, dependency) {
let owner;
let repo;
let tag;
if (dependency.includes("/") && dependency.includes("@")) {
owner = dependency.split("/")[0];
repo = dependency.split("/")[1].split("@")[0];
tag = dependency.split("@")[1];
} else {
const upstreamPackageJson = JSON.parse(fs.readFileSync(`./node_modules/${dependency}/package.json`, "utf8"));
[owner, repo] = upstreamPackageJson.repository.url.split("/").slice(-2);
tag = `v${upstreamPackageJson.version}`;
}
const response = await github.rest.repos.getReleaseByTag({
owner,
repo,
tag,
});
return response.data;
}
const HEADING_PREFIX = "## ";
const main = async ({ github, releaseId, dependencies }) => {
const { GITHUB_REPOSITORY } = process.env;
const [owner, repo] = GITHUB_REPOSITORY.split("/");
const sections = new Map();
let heading = null;
for (const dependency of dependencies) {
const release = await getRelease(github, dependency);
for (const line of release.body.split("\n")) {
if (line.startsWith(HEADING_PREFIX)) {
heading = line.trim();
sections.set(heading, []);
continue;
}
if (heading && line) {
sections.get(heading).push(line.trim());
}
}
}
const { data: release } = await github.rest.repos.getRelease({
owner,
repo,
release_id: releaseId,
});
const headings = ["🚨 BREAKING CHANGES", "🦖 Deprecations", "✨ Features", "🐛 Bug Fixes", "🧰 Maintenance"].map(
(h) => HEADING_PREFIX + h,
);
heading = null;
const output = [];
for (const line of [...release.body.split("\n"), null]) {
if (line === null || line.startsWith(HEADING_PREFIX)) {
// If we have a heading, and it's not the first in the list of pending headings, output the section.
// If we're processing the last line (null) then output all remaining sections.
while (headings.length > 0 && (line === null || (heading && headings[0] !== heading))) {
const heading = headings.shift();
if (sections.has(heading)) {
output.push(heading);
output.push(...sections.get(heading));
}
}
if (heading && sections.has(heading)) {
const lastIsBlank = !output.at(-1)?.trim();
if (lastIsBlank) output.pop();
output.push(...sections.get(heading));
if (lastIsBlank) output.push("");
}
heading = line;
}
output.push(line);
}
return output.join("\n");
};
// This is just for testing locally
// Needs environment variables GITHUB_TOKEN & GITHUB_REPOSITORY
if (require.main === module) {
const { Octokit } = require("@octokit/rest");
const github = new Octokit({ auth: process.env.GITHUB_TOKEN });
if (process.argv.length < 4) {
// eslint-disable-next-line no-console
console.error("Usage: node merge-release-notes.js owner/repo:release_id npm-package-name ...");
process.exit(1);
}
const [releaseId, ...dependencies] = process.argv.slice(2);
main({ github, releaseId, dependencies }).then((output) => {
// eslint-disable-next-line no-console
console.log(output);
});
}
module.exports = main;
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# When merging to develop, we need revert the `main` and `typings` fields if we adjusted them previously.
for i in main typings browser
do
# If a `lib` prefixed value is present, it means we adjusted the field earlier at publish time, so we should revert it now.
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
# If there's a `src` prefixed value, use that, otherwise delete.
# This is used to delete the `typings` field and reset `main` back to the TypeScript source.
src_value=$(jq -r ".matrix_src_$i" package.json)
if [ "$src_value" != "null" ]; then
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
else
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
fi
done
if [ -n "$(git ls-files --modified package.json)" ]; then
echo "Committing develop package.json"
git commit package.json -m "Resetting package fields for development"
fi
+14
View File
@@ -0,0 +1,14 @@
#!/bin/bash
# For the published and dist versions of the package,
# we copy the `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if they exist).
# This small bit of gymnastics allows us to use the TypeScript source directly for development without
# needing to build before linting or testing.
for i in main typings browser
do
lib_value=$(jq -r ".matrix_lib_$i" package.json)
if [ "$lib_value" != "null" ]; then
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json && yarn prettier --write package.json
fi
done
-34
View File
@@ -1,34 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
import type { default as BrowserMatrix } from "../../src/browser-index";
// stub for browser-matrix browserify tests
// @ts-ignore
global.XMLHttpRequest = jest.fn();
afterAll(() => {
// clean up XMLHttpRequest mock
// @ts-ignore
global.XMLHttpRequest = undefined;
});
// Akin to spec/setupTests.ts - but that won't affect the browser-matrix bundle
global.matrixcs = {
...global.matrixcs,
timeoutSignal: () => new AbortController().signal,
} as typeof BrowserMatrix;
-92
View File
@@ -1,92 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import HttpBackend from "matrix-mock-request";
import "./setupTests"; // uses browser-matrix instead of the src
import type { MatrixClient } from "../../src";
const USER_ID = "@user:test.server";
const DEVICE_ID = "device_id";
const ACCESS_TOKEN = "access_token";
const ROOM_ID = "!room_id:server.test";
describe("Browserify Test", function () {
let client: MatrixClient;
let httpBackend: HttpBackend;
beforeEach(() => {
httpBackend = new HttpBackend();
client = new global.matrixcs.MatrixClient({
baseUrl: "http://test.server",
userId: USER_ID,
accessToken: ACCESS_TOKEN,
deviceId: DEVICE_ID,
fetchFn: httpBackend.fetchFn as typeof global.fetch,
});
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
});
afterEach(async () => {
client.stopClient();
client.http.abort();
httpBackend.verifyNoOutstandingRequests();
httpBackend.verifyNoOutstandingExpectation();
await httpBackend.stop();
});
it("Sync", async () => {
const event = {
type: "m.room.member",
room_id: ROOM_ID,
content: {
membership: "join",
name: "Displayname",
},
event_id: "$foobar",
};
const syncData = {
next_batch: "batch1",
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [event],
limited: false,
},
},
},
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, syncData);
const syncPromise = new Promise((r) => client.once(global.matrixcs.ClientEvent.Sync, r));
const unexpectedErrorFn = jest.fn();
client.once(global.matrixcs.ClientEvent.SyncUnexpectedError, unexpectedErrorFn);
client.startClient();
await httpBackend.flushAllExpected();
await syncPromise;
expect(unexpectedErrorFn).not.toHaveBeenCalled();
}, 20000); // additional timeout as this test can take quite a while
});
File diff suppressed because it is too large Load Diff
+153 -49
View File
@@ -23,10 +23,17 @@ import { SyncResponder } from "../../test-utils/SyncResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { awaitDecryption, CRYPTO_BACKENDS, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import {
advanceTimersUntil,
awaitDecryption,
CRYPTO_BACKENDS,
InitCrypto,
syncPromise,
} from "../../test-utils/test-utils";
import * as testData from "../../test-utils/test-data";
import { KeyBackupInfo } from "../../../src/crypto-api/keybackup";
import { IKeyBackup } from "../../../src/crypto/backup";
import { flushPromises } from "../../test-utils/flushPromises";
const ROOM_ID = testData.TEST_ROOM_ID;
@@ -110,9 +117,9 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
/** an object which intercepts `/keys/query` requests on the test homeserver */
let e2eKeyResponder: E2EKeyResponder;
jest.useFakeTimers();
beforeEach(async () => {
jest.useFakeTimers();
// anything that we don't have a specific matcher for silently returns a 404
fetchMock.catch(404);
fetchMock.config.warnOnFallback = false;
@@ -134,6 +141,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await jest.runAllTimersAsync();
fetchMock.mockReset();
jest.restoreAllMocks();
});
async function initTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
@@ -149,48 +157,131 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
return client;
}
it("Alice checks key backups when receiving a message she can't decrypt", async function () {
const syncResponse = {
describe("Key backup check on UTD message", () => {
// sync response which contains an encrypted event
const SYNC_RESPONSE = {
next_batch: 1,
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [testData.ENCRYPTED_EVENT],
},
},
},
},
rooms: { join: { [ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } } } },
};
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
testData.CURVE25519_KEY_BACKUP_DATA,
);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const EXPECTED_URL =
[
"https://alice-server.com/_matrix/client/v3/room_keys/keys",
encodeURIComponent(testData.TEST_ROOM_ID),
encodeURIComponent(testData.MEGOLM_SESSION_DATA.session_id),
].join("/") + "?version=1";
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"));
/** Flush promises enough times to get the crypto stacks to make the backup request */
async function flushBackupRequest() {
// we have to run flushPromises lots of times. It seems like each time the rust code touches indexeddb,
// it needs another round of flushPromises to progress, or something.
for (let i = 0; i < 10; i++) {
await flushPromises();
}
}
// start after saving the private key
await aliceClient.startClient();
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
// XXX: should we automatically re-check after a device becomes verified?
await waitForDeviceList();
await aliceCrypto.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
// ignore requests to send room key requests
fetchMock.put("express:/_matrix/client/v3/sendToDevice/m.room_key_request/:request_id", {});
// Now, send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
aliceClient = await initTestClient();
const aliceCrypto = aliceClient.getCrypto()!;
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
await awaitDecryption(event, { waitOnDecryptionFailure: true });
// start after saving the private key
await aliceClient.startClient();
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
// tell Alice to trust the dummy device that signed the backup, and re-check the backup.
// XXX: should we automatically re-check after a device becomes verified?
await waitForDeviceList();
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
});
it("Alice checks key backups when receiving a message she can't decrypt", async () => {
fetchMock.get("express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id", (url, request) => {
// check that the version is correct
const version = new URLSearchParams(new URL(url).search).get("version");
if (version == "1") {
return testData.CURVE25519_KEY_BACKUP_DATA;
} else {
return {
status: 403,
body: {
current_version: "1",
errcode: "M_WRONG_ROOM_KEYS_VERSION",
error: "Wrong backup version.",
},
};
}
});
// Send Alice a message that she won't be able to decrypt, and check that she fetches the key from the backup.
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await syncPromise(aliceClient);
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
});
it("handles error on backup query gracefully", async () => {
jest.spyOn(console, "error").mockImplementation(() => {});
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
{ status: 404, body: { errcode: "M_NOT_FOUND" } },
{ name: "getKey" },
);
// Send Alice a message that she won't be able to decrypt
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest();
const calls = fetchMock.calls("getKey");
expect(calls.length).toEqual(1);
expect(calls[0][0]).toEqual(EXPECTED_URL);
await flushBackupRequest();
// we should not have logged an error.
// eslint-disable-next-line no-console
expect(console.error).not.toHaveBeenCalled();
});
it("Only queries once", async () => {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/keys/:room_id/:session_id",
{ status: 404, body: { errcode: "M_NOT_FOUND" } },
{ name: "getKey" },
);
// Send Alice a message that she won't be able to decrypt
syncResponder.sendOrQueueSyncResponse(SYNC_RESPONSE);
await flushBackupRequest();
const calls = fetchMock.calls("getKey");
expect(calls.length).toEqual(1);
expect(calls[0][0]).toEqual(EXPECTED_URL);
fetchMock.resetHistory();
// another message
const event2 = { ...testData.ENCRYPTED_EVENT, event_id: "$event2" };
const syncResponse2 = {
next_batch: 1,
rooms: { join: { [ROOM_ID]: { timeline: { events: [event2] } } } },
};
syncResponder.sendOrQueueSyncResponse(syncResponse2);
await flushBackupRequest();
expect(fetchMock.calls("getKey").length).toEqual(0);
});
});
describe("recover from backup", () => {
@@ -224,19 +315,28 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
onKeyCached = resolve;
});
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
const result = await advanceTimersUntil(
aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
undefined,
undefined,
check!.backupInfo!,
{
cacheCompleteCallback: () => onKeyCached(),
},
),
);
expect(result.imported).toStrictEqual(1);
await awaitKeyCached;
// The key should be now cached
const afterCache = await advanceTimersUntil(
aliceClient.restoreKeyBackupWithCache(undefined, undefined, check!.backupInfo!),
);
expect(afterCache.imported).toStrictEqual(1);
});
it("recover specific session from backup", async function () {
@@ -257,11 +357,13 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const check = await aliceCrypto.checkKeyBackupAndEnable();
const result = await aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
ROOM_ID,
testData.MEGOLM_SESSION_DATA.session_id,
check!.backupInfo!,
const result = await advanceTimersUntil(
aliceClient.restoreKeyBackupWithRecoveryKey(
testData.BACKUP_DECRYPTION_KEY_BASE58,
ROOM_ID,
testData.MEGOLM_SESSION_DATA.session_id,
check!.backupInfo!,
),
);
expect(result.imported).toStrictEqual(1);
@@ -640,6 +742,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await aliceClient.startClient();
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
const result = await aliceCrypto.isKeyBackupTrusted(testData.SIGNED_BACKUP_DATA);
@@ -653,6 +756,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
await aliceClient.startClient();
await aliceCrypto.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
const backup: KeyBackupInfo = JSON.parse(JSON.stringify(testData.SIGNED_BACKUP_DATA));
+408
View File
@@ -0,0 +1,408 @@
/*
Copyright 2016-2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Olm from "@matrix-org/olm";
import anotherjson from "another-json";
import { IContent, IDeviceKeys, IDownloadKeyResult, IEvent, Keys, MatrixClient, SigningKeys } from "../../../src";
import { IE2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { ISyncResponder } from "../../test-utils/SyncResponder";
import { syncPromise } from "../../test-utils/test-utils";
import { KeyBackupInfo } from "../../../src/crypto-api";
/**
* @module
*
* A set of utilities for creating Olm accounts and sessions, and encrypting/decrypting with Olm/Megolm.
*/
/** Create an Olm Account object */
export async function createOlmAccount(): Promise<Olm.Account> {
await Olm.init();
const testOlmAccount = new Olm.Account();
testOlmAccount.create();
return testOlmAccount;
}
/**
* Get the device keys for the test Olm Account
*
* @param olmAccount - Test olm account
* @param userId - The user ID to present the keys as belonging to
*/
export function getTestOlmAccountKeys(olmAccount: Olm.Account, userId: string, deviceId: string): IDeviceKeys {
const testE2eKeys = JSON.parse(olmAccount.identity_keys());
const testDeviceKeys: IDeviceKeys = {
algorithms: ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
device_id: deviceId,
keys: {
[`curve25519:${deviceId}`]: testE2eKeys.curve25519,
[`ed25519:${deviceId}`]: testE2eKeys.ed25519,
},
user_id: userId,
};
const j = anotherjson.stringify(testDeviceKeys);
const sig = olmAccount.sign(j);
testDeviceKeys.signatures = { [userId]: { [`ed25519:${deviceId}`]: sig } };
return testDeviceKeys;
}
/**
* Bootstrap cross signing for the given Olm account.
*
* Will generate the cross signing keys and sign them with the master key, and returns the `IDownloadKeyResult`
* that can be directly fed into a test e2eKeyResponder.
*
* The cross-signing keys are randomly generated, similar to how the olm account keys are generated. There may not
* be any value in using static vectors, as the device keys change at every test run.
*
* If some `KeyBackupInfo` are provided, the `auth_data` of each backup info will be signed with the
* master key, meaning the backups will be then trusted after verification.
*
* @param olmAccount - The Olm account object to use for signing the device keys.
* @param userId - The user ID to associate with the device keys.
* @param deviceId - The device ID to associate with the device keys.
* @param keyBackupInfo - Optional key backup infos to sign with the master key.
* @returns A valid keys/query response that can be fed into a test e2eKeyResponder.
*/
export function bootstrapCrossSigningTestOlmAccount(
olmAccount: Olm.Account,
userId: string,
deviceId: string,
keyBackupInfo: KeyBackupInfo[] = [],
): Partial<IDownloadKeyResult> {
const olmAliceMSK = new global.Olm.PkSigning();
const masterPrivkey = olmAliceMSK.generate_seed();
const masterPubkey = olmAliceMSK.init_with_seed(masterPrivkey);
const olmAliceUSK = new global.Olm.PkSigning();
const userPrivkey = olmAliceUSK.generate_seed();
const userPubkey = olmAliceUSK.init_with_seed(userPrivkey);
const olmAliceSSK = new global.Olm.PkSigning();
const sskPrivkey = olmAliceSSK.generate_seed();
const sskPubkey = olmAliceSSK.init_with_seed(sskPrivkey);
const mskInfo: Keys = {
user_id: userId,
usage: ["master"],
keys: {
["ed25519:" + masterPubkey]: masterPubkey,
},
};
const sskInfo: Partial<SigningKeys> = {
user_id: userId,
usage: ["self_signing"],
keys: {
["ed25519:" + sskPubkey]: sskPubkey,
},
};
// sign the ssk with the msk
const sskSig = olmAliceMSK.sign(anotherjson.stringify(sskInfo));
sskInfo.signatures = {
[userId]: {
["ed25519:" + masterPubkey]: sskSig,
},
};
const uskInfo: Partial<SigningKeys> = {
user_id: userId,
usage: ["user_signing"],
keys: {
["ed25519:" + userPubkey]: userPubkey,
},
};
// sign the usk with the msk
const uskSig = olmAliceMSK.sign(anotherjson.stringify(uskInfo));
uskInfo.signatures = {
[userId]: {
["ed25519:" + masterPubkey]: uskSig,
},
};
// get the device keys and sign them with the ssk (the device is then cross signed)
const deviceKeys = getTestOlmAccountKeys(olmAccount, userId, deviceId);
const copy = Object.assign({}, deviceKeys);
delete copy.signatures;
const crossSignature = olmAliceSSK.sign(anotherjson.stringify(copy));
// add the signature
deviceKeys.signatures![userId]["ed25519:" + sskPubkey] = crossSignature;
// if we have some key backup info, sign them with the msk
keyBackupInfo.forEach((info) => {
const unsignedAuthData = Object.assign({}, info.auth_data);
delete unsignedAuthData.signatures;
const backupSignature = olmAliceMSK.sign(anotherjson.stringify(unsignedAuthData));
info.auth_data.signatures = {
[userId]: {
["ed25519:" + masterPubkey]: backupSignature,
},
};
});
// clean the olm resources as we don't need them anymore
olmAliceMSK.free();
olmAliceSSK.free();
olmAliceUSK.free();
return {
master_keys: { [userId]: mskInfo },
user_signing_keys: { [userId]: uskInfo as SigningKeys },
self_signing_keys: { [userId]: sskInfo as SigningKeys },
device_keys: { [userId]: { [deviceId]: deviceKeys } },
};
}
/** start an Olm session with a given recipient */
export async function createOlmSession(
olmAccount: Olm.Account,
recipientTestClient: IE2EKeyReceiver,
): Promise<Olm.Session> {
const keys = await recipientTestClient.awaitOneTimeKeyUpload();
const otkId = Object.keys(keys)[0];
const otk = keys[otkId];
const session = new global.Olm.Session();
session.create_outbound(olmAccount, recipientTestClient.getDeviceKey(), otk.key);
return session;
}
// IToDeviceEvent isn't exported by src/sync-accumulator.ts
export interface ToDeviceEvent {
content: IContent;
sender: string;
type: string;
}
/** encrypt an event with an existing olm session */
export function encryptOlmEvent(opts: {
/** the sender's user id */
sender?: string;
/** the sender's curve25519 key */
senderKey: string;
/** the sender's ed25519 key */
senderSigningKey: string;
/** the olm session to use for encryption */
p2pSession: Olm.Session;
/** the recipient's user id */
recipient: string;
/** the recipient's curve25519 key */
recipientCurve25519Key: string;
/** the recipient's ed25519 key */
recipientEd25519Key: string;
/** the payload of the message */
plaincontent?: object;
/** the event type of the payload */
plaintype?: string;
}): ToDeviceEvent {
expect(opts.senderKey).toBeTruthy();
expect(opts.p2pSession).toBeTruthy();
expect(opts.recipient).toBeTruthy();
const plaintext = {
content: opts.plaincontent || {},
recipient: opts.recipient,
recipient_keys: {
ed25519: opts.recipientEd25519Key,
},
keys: {
ed25519: opts.senderSigningKey,
},
sender: opts.sender || "@bob:xyz",
type: opts.plaintype || "m.test",
};
return {
content: {
algorithm: "m.olm.v1.curve25519-aes-sha2",
ciphertext: {
[opts.recipientCurve25519Key]: opts.p2pSession.encrypt(JSON.stringify(plaintext)),
},
sender_key: opts.senderKey,
},
sender: opts.sender || "@bob:xyz",
type: "m.room.encrypted",
};
}
// encrypt an event with megolm
export function encryptMegolmEvent(opts: {
senderKey: string;
groupSession: Olm.OutboundGroupSession;
plaintext?: Partial<IEvent>;
room_id?: string;
}): IEvent {
expect(opts.senderKey).toBeTruthy();
expect(opts.groupSession).toBeTruthy();
const plaintext = opts.plaintext || {};
if (!plaintext.content) {
plaintext.content = {
body: "42",
msgtype: "m.text",
};
}
if (!plaintext.type) {
plaintext.type = "m.room.message";
}
if (!plaintext.room_id) {
expect(opts.room_id).toBeTruthy();
plaintext.room_id = opts.room_id;
}
return encryptMegolmEventRawPlainText({
senderKey: opts.senderKey,
groupSession: opts.groupSession,
plaintext,
});
}
export function encryptMegolmEventRawPlainText(opts: {
senderKey: string;
groupSession: Olm.OutboundGroupSession;
plaintext: Partial<IEvent>;
origin_server_ts?: number;
}): IEvent {
return {
event_id: "$test_megolm_event_" + Math.random(),
sender: opts.plaintext.sender ?? "@not_the_real_sender:example.com",
origin_server_ts: opts.plaintext.origin_server_ts ?? 1672944778000,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(opts.plaintext)),
device_id: "testDevice",
sender_key: opts.senderKey,
session_id: opts.groupSession.session_id(),
},
type: "m.room.encrypted",
unsigned: {},
};
}
/** build an encrypted room_key event to share a group session, using an existing olm session */
export function encryptGroupSessionKey(opts: {
/** recipient's user id */
recipient: string;
/** the recipient's curve25519 key */
recipientCurve25519Key: string;
/** the recipient's ed25519 key */
recipientEd25519Key: string;
/** sender's olm account */
olmAccount: Olm.Account;
/** sender's olm session with the recipient */
p2pSession: Olm.Session;
groupSession: Olm.OutboundGroupSession;
room_id?: string;
}): ToDeviceEvent {
const senderKeys = JSON.parse(opts.olmAccount.identity_keys());
return encryptOlmEvent({
senderKey: senderKeys.curve25519,
senderSigningKey: senderKeys.ed25519,
recipient: opts.recipient,
recipientCurve25519Key: opts.recipientCurve25519Key,
recipientEd25519Key: opts.recipientEd25519Key,
p2pSession: opts.p2pSession,
plaincontent: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: opts.room_id,
session_id: opts.groupSession.session_id(),
session_key: opts.groupSession.session_key(),
},
plaintype: "m.room_key",
});
}
/**
* Test utility to correctly encrypt a secret send event to a test device using the provided p2p session.
*
* @param opts - the options for the secret send event
* @returns the to-device event, ready to be returned in a sync response for the test device.
*/
export function encryptSecretSend(opts: {
/** the sender's user id */
sender: string;
/** recipient's user id */
recipient: string;
/** the recipient's curve25519 key */
recipientCurve25519Key: string;
/** the recipient's ed25519 key */
recipientEd25519Key: string;
/** sender's olm account */
olmAccount: Olm.Account;
/** sender's olm session with the recipient */
p2pSession: Olm.Session;
/** The requestId of the secret request that this secret send is replying. */
requestId: string;
/** The secret value */
secret: string;
}): ToDeviceEvent {
const senderKeys = JSON.parse(opts.olmAccount.identity_keys());
return encryptOlmEvent({
sender: opts.sender,
senderKey: senderKeys.curve25519,
senderSigningKey: senderKeys.ed25519,
recipient: opts.recipient,
recipientCurve25519Key: opts.recipientCurve25519Key,
recipientEd25519Key: opts.recipientEd25519Key,
p2pSession: opts.p2pSession,
plaincontent: {
request_id: opts.requestId,
secret: opts.secret,
},
plaintype: "m.secret.send",
});
}
/**
* Establish an Olm Session with the test user
*
* Waits for the test user to upload their keys, then sends a /sync response with a to-device message which will
* establish an Olm session.
*
* @param testClient - the MatrixClient under test, which we expect to upload account keys, and to make a
* /sync request which we will respond to.
* @param keyReceiver - an IE2EKeyReceiver which will intercept the /keys/upload request from the client under test
* @param syncResponder - an ISyncResponder which will intercept /sync requests from the client under test
* @param peerOlmAccount: an OlmAccount which will be used to initiate the Olm session.
*/
export async function establishOlmSession(
testClient: MatrixClient,
keyReceiver: IE2EKeyReceiver,
syncResponder: ISyncResponder,
peerOlmAccount: Olm.Account,
): Promise<Olm.Session> {
const peerE2EKeys = JSON.parse(peerOlmAccount.identity_keys());
const p2pSession = await createOlmSession(peerOlmAccount, keyReceiver);
const olmEvent = encryptOlmEvent({
senderKey: peerE2EKeys.curve25519,
senderSigningKey: peerE2EKeys.ed25519,
recipient: testClient.getUserId()!,
recipientCurve25519Key: keyReceiver.getDeviceKey(),
recipientEd25519Key: keyReceiver.getSigningKey(),
p2pSession: p2pSession,
});
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
to_device: { events: [olmEvent] },
});
await syncPromise(testClient);
return p2pSession;
}
+21 -2
View File
@@ -41,7 +41,7 @@ describe("MatrixClient.initRustCrypto", () => {
await expect(() => unknownDeviceClient.initRustCrypto()).rejects.toThrow("unknown deviceId");
});
it("should create the indexed dbs", async () => {
it("should create the indexed db", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
@@ -53,7 +53,25 @@ describe("MatrixClient.initRustCrypto", () => {
await matrixClient.initRustCrypto();
// should have two dbs now
// should have an indexed db now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto"]));
});
it("should create the meta db if given a pickleKey", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
pickleKey: "testKey",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto();
// should have two indexed dbs now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
@@ -78,6 +96,7 @@ describe("MatrixClient.clearStores", () => {
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
pickleKey: "testKey",
});
await matrixClient.initRustCrypto();
+637 -9
View File
@@ -21,12 +21,15 @@ import { MockResponse } from "fetch-mock";
import fetchMock from "fetch-mock-jest";
import { IDBFactory } from "fake-indexeddb";
import { createHash } from "crypto";
import Olm from "@matrix-org/olm";
import {
createClient,
CryptoEvent,
DeviceVerification,
IContent,
ICreateClientOpts,
IEvent,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
@@ -41,13 +44,23 @@ import {
Verifier,
VerifierEvent,
} from "../../../src/crypto-api/verification";
import { escapeRegExp } from "../../../src/utils";
import { CRYPTO_BACKENDS, emitPromise, getSyncResponse, InitCrypto, syncPromise } from "../../test-utils/test-utils";
import { defer, escapeRegExp } from "../../../src/utils";
import {
awaitDecryption,
CRYPTO_BACKENDS,
emitPromise,
getSyncResponse,
InitCrypto,
syncPromise,
} from "../../test-utils/test-utils";
import { SyncResponder } from "../../test-utils/SyncResponder";
import {
BACKUP_DECRYPTION_KEY_BASE64,
BOB_ONE_TIME_KEYS,
BOB_SIGNED_CROSS_SIGNING_KEYS_DATA,
BOB_SIGNED_TEST_DEVICE_DATA,
BOB_TEST_USER_ID,
CURVE25519_KEY_BACKUP_DATA,
MASTER_CROSS_SIGNING_PUBLIC_KEY_BASE64,
SIGNED_CROSS_SIGNING_KEYS_DATA,
SIGNED_TEST_DEVICE_DATA,
@@ -55,11 +68,20 @@ import {
TEST_DEVICE_PUBLIC_ED25519_KEY_BASE64,
TEST_ROOM_ID,
TEST_USER_ID,
BOB_ONE_TIME_KEYS,
} from "../../test-utils/test-data";
import { mockInitialApiRequests } from "../../test-utils/mockEndpoints";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import {
bootstrapCrossSigningTestOlmAccount,
createOlmSession,
encryptGroupSessionKey,
encryptMegolmEvent,
encryptSecretSend,
ToDeviceEvent,
} from "./olm-utils";
import { KeyBackupInfo } from "../../../src/crypto-api";
import { encodeBase64 } from "../../../src/base64";
// The verification flows use javascript timers to set timeouts. We tell jest to use mock timer implementations
// to ensure that we don't end up with dangling timeouts.
@@ -138,6 +160,12 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
beforeEach(async () => {
// pretend that we have another device, which we will verify
e2eKeyResponder.addDeviceKeys(SIGNED_TEST_DEVICE_DATA);
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/${escapeRegExp("m.secret.request")}`),
{ ok: false, status: 404 },
{ overwriteRoutes: true },
);
});
// test with (1) the default verification method list, (2) a custom verification method list.
@@ -409,6 +437,15 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
});
it("can verify another via QR code with an untrusted cross-signing key", async () => {
// This is a slightly weird thing to test; if we don't trust the cross-signing key, normally we would
// spam out a verification request to all devices rather than targeting a single device. Still, it's
// a thing both the Matrix protocol and the js-sdk API support, so we may as well test it.
//
// Since we don't yet trust the master key, this is a type 0x02 QR code:
// "self-verifying in which the current device does not yet trust the master key"
//
// By the end of it, we should trust the master key.
aliceClient = await startTestClient();
// QRCode fails if we don't yet have the cross-signing keys, so make sure we have them now.
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
@@ -480,12 +517,51 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
reciprocateQRCodeCallbacks.confirm();
await sendToDevicePromise;
// at this point, on legacy crypto, the master key is already marked as trusted, and the request is "Done".
// Rust crypto, on the other hand, waits for the 'done' to arrive from the other side.
if (request.phase === VerificationPhase.Done) {
// legacy crypto: we're all done
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
// eslint-disable-next-line jest/no-conditional-expect
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
await verificationPromise;
} else {
// rust crypto: still in flight
// eslint-disable-next-line jest/no-conditional-expect
expect(request.phase).toEqual(VerificationPhase.Started);
}
// the dummy device replies with its own 'done'
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
// ... and the whole thing should be done!
// ... and now we're really done.
await verificationPromise;
expect(request.phase).toEqual(VerificationPhase.Done);
const userVerificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
});
it("can try to generate a QR code when QR code is not supported", async () => {
aliceClient = await startTestClient();
// we need cross-signing keys for a QR code verification
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
await waitForDeviceList();
// Alice sends a m.key.verification.request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, TEST_DEVICE_ID),
]);
const transactionId = request.transactionId!;
// The dummy device replies with an m.key.verification.ready, indicating it can only use SaS
returnToDeviceMessageFromSync(buildReadyMessage(transactionId, ["m.sas.v1"]));
await waitForVerificationRequestChanged(request);
expect(request.phase).toEqual(VerificationPhase.Ready);
// Alice tries to generate a QR Code but it's unavailable
const qrCodeBuffer = await request.generateQRCode();
expect(qrCodeBuffer).toBeUndefined();
});
newBackendOnly("can verify another by scanning their QR code", async () => {
@@ -897,6 +973,491 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
});
});
describe("Incoming verification in a DM", () => {
let testOlmAccount: Olm.Account;
beforeEach(async () => {
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
await Olm.init();
testOlmAccount = new Olm.Account();
testOlmAccount.create();
aliceClient = await startTestClient();
aliceClient.setGlobalErrorOnUnknownDevices(false);
syncResponder.sendOrQueueSyncResponse(getSyncResponse([BOB_TEST_USER_ID]));
await syncPromise(aliceClient);
});
/**
* Return a plaintext verification request event from Bob to Alice
* @see https://spec.matrix.org/v1.7/client-server-api/#mkeyverificationrequest
*/
function createVerificationRequestEvent(): IEvent {
return {
content: {
body: "Verification request from Bob to Alice",
from_device: "BobDevice",
methods: ["m.sas.v1"],
msgtype: "m.key.verification.request",
to: aliceClient.getUserId()!,
},
event_id: "$143273582443PhrSn:example.org",
origin_server_ts: Date.now(),
room_id: TEST_ROOM_ID,
sender: "@bob:xyz",
type: "m.room.message",
unsigned: {
age: 1234,
},
};
}
/**
* Create a to-device event from Bob to Alice, sharing the group session key
* @param groupSession - group session key to share
* @param p2pSession - test Olm session to encrypt the key with
*/
function encryptGroupSessionKeyForAlice(
groupSession: Olm.OutboundGroupSession,
p2pSession: Olm.Session,
): ToDeviceEvent {
return encryptGroupSessionKey({
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
olmAccount: testOlmAccount,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: TEST_ROOM_ID,
});
}
/**
* Create and encrypt a verification request event
* @param groupSession
*/
function createEncryptedVerificationRequest(groupSession: Olm.OutboundGroupSession): IEvent {
const testOlmAccountKeys = JSON.parse(testOlmAccount.identity_keys());
return encryptMegolmEvent({
senderKey: testOlmAccountKeys.curve25519,
groupSession: groupSession,
room_id: TEST_ROOM_ID,
plaintext: createVerificationRequestEvent(),
});
}
it("Verification request not found", async () => {
// Expect to not find any verification request
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
expect(request).toBeUndefined();
});
it("ignores old verification requests", async () => {
const eventHandler = jest.fn();
aliceClient.on(CryptoEvent.VerificationRequestReceived, eventHandler);
const verificationRequestEvent = createVerificationRequestEvent();
verificationRequestEvent.origin_server_ts -= 1000000;
returnRoomMessageFromSync(TEST_ROOM_ID, verificationRequestEvent);
await syncPromise(aliceClient);
// make sure the event has arrived
const room = aliceClient.getRoom(TEST_ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];
expect(matrixEvent.getId()).toEqual(verificationRequestEvent.event_id);
// check that an event has not been raised, and that the request is not found
expect(eventHandler).not.toHaveBeenCalled();
expect(
aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz"),
).not.toBeDefined();
});
it("Plaintext verification request from Bob to Alice", async () => {
// Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createVerificationRequestEvent());
// Wait for the request to be received
const request1 = await emitPromise(aliceClient, CryptoEvent.VerificationRequestReceived);
expect(request1.roomId).toBe(TEST_ROOM_ID);
expect(request1.isSelfVerification).toBe(false);
expect(request1.otherUserId).toBe("@bob:xyz");
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// Expect to find the verification request received during the sync
expect(request?.roomId).toBe(TEST_ROOM_ID);
expect(request?.isSelfVerification).toBe(false);
expect(request?.otherUserId).toBe("@bob:xyz");
});
it("Encrypted verification request from Bob to Alice", async () => {
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event, but don't send it yet
const toDeviceEvent = encryptGroupSessionKeyForAlice(groupSession, p2pSession);
// Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createEncryptedVerificationRequest(groupSession));
// Wait for the sync response to be processed
await syncPromise(aliceClient);
const room = aliceClient.getRoom(TEST_ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];
// wait for a first attempt at decryption: should fail
await awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
const requestEventPromise = emitPromise(aliceClient, CryptoEvent.VerificationRequestReceived);
// Send Bob the room keys
returnToDeviceMessageFromSync(toDeviceEvent);
// advance the clock, because the devicelist likes to sleep for 5ms during key downloads
await jest.advanceTimersByTimeAsync(10);
// Wait for the request to be decrypted
const request1 = await requestEventPromise;
expect(request1.roomId).toBe(TEST_ROOM_ID);
expect(request1.isSelfVerification).toBe(false);
expect(request1.otherUserId).toBe("@bob:xyz");
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// Expect to find the verification request received during the sync
expect(request?.roomId).toBe(TEST_ROOM_ID);
expect(request?.isSelfVerification).toBe(false);
expect(request?.otherUserId).toBe("@bob:xyz");
});
newBackendOnly(
"If the verification request is not decrypted within 5 minutes, the request is ignored",
async () => {
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event, but don't send it yet
const toDeviceEvent = encryptGroupSessionKeyForAlice(groupSession, p2pSession);
// Add verification request from Bob to Alice in the DM between them
returnRoomMessageFromSync(TEST_ROOM_ID, createEncryptedVerificationRequest(groupSession));
// Wait for the sync response to be processed
await syncPromise(aliceClient);
const room = aliceClient.getRoom(TEST_ROOM_ID)!;
const matrixEvent = room.getLiveTimeline().getEvents()[0];
// wait for a first attempt at decryption: should fail
await awaitDecryption(matrixEvent);
expect(matrixEvent.getContent().msgtype).toEqual("m.bad.encrypted");
// Advance time by 5mins, the verification request should be ignored after that
jest.advanceTimersByTime(5 * 60 * 1000);
// Send Bob the room keys
returnToDeviceMessageFromSync(toDeviceEvent);
// Wait for the message to be decrypted
await awaitDecryption(matrixEvent, { waitOnDecryptionFailure: true });
const request = aliceClient.getCrypto()!.findVerificationRequestDMInProgress(TEST_ROOM_ID, "@bob:xyz");
// the request should not be present
expect(request).not.toBeDefined();
},
);
});
describe("Secrets are gossiped after verification", () => {
// We use a legacy olm session as the existing session.
// This will give us access to low level olm functions in order to
// simulate a backup key request with proper olm encryption.
let testOlmAccount: Olm.Account;
const olmDeviceId = "OLM_DEVICE";
let usermasterPubKey: string;
const matchingBackupInfo: KeyBackupInfo = {
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
const nonMatchingBackupInfo: KeyBackupInfo = {
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
version: "1",
auth_data: {
public_key: "EjDwCYkwp1R0i33ctD73Wg2/Og0mOBr066Spjqqaqqo",
},
};
const unknownAlgorithmBackupInfo: KeyBackupInfo = {
algorithm: "m.megolm_backup.foo_bar",
version: "1",
auth_data: {
public_key: "EjDwCYkwp1R0i33ctD73Wg2/Og0mOBr066Spjqqaqqo",
},
};
beforeEach(async () => {
// create a test olm device which we will use to communicate with alice. We use libolm to implement this.
await Olm.init();
testOlmAccount = new Olm.Account();
testOlmAccount.create();
const bootstrapped = bootstrapCrossSigningTestOlmAccount(testOlmAccount, TEST_USER_ID, olmDeviceId, [
matchingBackupInfo,
nonMatchingBackupInfo,
]);
e2eKeyResponder.addDeviceKeys(bootstrapped.device_keys![TEST_USER_ID]![olmDeviceId]);
e2eKeyResponder.addCrossSigningData(bootstrapped);
usermasterPubKey = Object.values(bootstrapped.master_keys![TEST_USER_ID].keys)[0];
aliceClient = await startTestClient();
syncResponder.sendOrQueueSyncResponse(getSyncResponse([TEST_USER_ID]));
await syncPromise(aliceClient);
// DeviceList has a sleep(5) which we need to make happen
await jest.advanceTimersByTimeAsync(10);
// The client should now know about the olm device
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
expect(devices.get(TEST_USER_ID)!.keys()).toContain(olmDeviceId);
});
afterEach(async () => {
aliceClient?.stopClient();
testOlmAccount?.free();
// Allow in-flight things to complete before we tear down the test
await jest.runAllTimersAsync();
fetchMock.mockReset();
});
newBackendOnly("Should request cross signing keys after verification", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
// The secret must have been requested
await requestPromises.get("m.cross_signing.master");
await requestPromises.get("m.cross_signing.user_signing");
await requestPromises.get("m.cross_signing.self_signing");
});
newBackendOnly("Should accept the backup decryption key gossip if valid", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
const requestId = await requestPromises.get("m.megolm_backup.v1");
await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, matchingBackupInfo);
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers();
// the backup secret should be cached
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
expect(cachedKey).toBeTruthy();
expect(encodeBase64(cachedKey!)).toEqual(BACKUP_DECRYPTION_KEY_BASE64);
});
newBackendOnly("Should not accept the backup decryption key gossip if private key do not match", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
const requestId = await requestPromises.get("m.megolm_backup.v1");
await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, nonMatchingBackupInfo);
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers();
// the backup secret should not be cached
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
expect(cachedKey).toBeNull();
});
newBackendOnly("Should not accept the backup decryption key gossip if backup not trusted", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
const requestId = await requestPromises.get("m.megolm_backup.v1");
const infoCopy = Object.assign({}, matchingBackupInfo);
delete infoCopy.auth_data.signatures;
await sendBackupGossipAndExpectVersion(requestId!, BACKUP_DECRYPTION_KEY_BASE64, infoCopy);
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers();
// the backup secret should not be cached
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
expect(cachedKey).toBeNull();
});
newBackendOnly("Should not accept the backup decryption key gossip if backup algorithm unknown", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
const requestId = await requestPromises.get("m.megolm_backup.v1");
await sendBackupGossipAndExpectVersion(
requestId!,
BACKUP_DECRYPTION_KEY_BASE64,
unknownAlgorithmBackupInfo,
);
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers();
// the backup secret should not be cached
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
expect(cachedKey).toBeNull();
});
newBackendOnly("Should not accept an invalid backup decryption key", async () => {
const requestPromises = mockSecretRequestAndGetPromises();
await doInteractiveVerification();
const requestId = await requestPromises.get("m.megolm_backup.v1");
await sendBackupGossipAndExpectVersion(requestId!, "InvalidSecret", matchingBackupInfo);
// We are lacking a way to signal that the secret has been received, so we wait a bit..
jest.useRealTimers();
await new Promise((resolve) => {
setTimeout(resolve, 500);
});
jest.useFakeTimers();
// the backup secret should not be cached
const cachedKey = await aliceClient.getCrypto()!.getSessionBackupPrivateKey();
expect(cachedKey).toBeNull();
});
/**
* Common test setup for gossiping secrets.
* Creates a peer to peer session, sends the secret, mockup the version API, send the secret back from sync, then await for the backup check.
*/
async function sendBackupGossipAndExpectVersion(
requestId: string,
secret: string,
expectBackup: KeyBackupInfo,
) {
const p2pSession = await createOlmSession(testOlmAccount, e2eKeyReceiver);
const toDeviceEvent = encryptSecretSend({
sender: aliceClient.getUserId()!,
recipient: aliceClient.getUserId()!,
recipientCurve25519Key: e2eKeyReceiver.getDeviceKey(),
recipientEd25519Key: e2eKeyReceiver.getSigningKey(),
p2pSession: p2pSession,
olmAccount: testOlmAccount,
requestId: requestId!,
secret: secret,
});
const expectBackupCheck = new Promise((resolve) => {
fetchMock.get(
"express:/_matrix/client/v3/room_keys/version",
(url, request) => {
resolve(undefined);
return expectBackup;
},
{
overwriteRoutes: true,
},
);
});
fetchMock.get("express:/_matrix/client/v3/room_keys/keys", CURVE25519_KEY_BACKUP_DATA);
// The dummy device sends the secret
returnToDeviceMessageFromSync(toDeviceEvent);
await expectBackupCheck;
}
/**
* Do an interactive verification between alice and the dummy device.
*/
async function doInteractiveVerification(): Promise<void> {
// Do a QR code verification for simplicity
// Alice sends a m.key.verification.request
const [, request] = await Promise.all([
expectSendToDeviceMessage("m.key.verification.request"),
aliceClient.getCrypto()!.requestDeviceVerification(TEST_USER_ID, olmDeviceId),
]);
const transactionId = request.transactionId!;
// The dummy device replies with an m.key.verification.ready, indicating it can show a QR code
returnToDeviceMessageFromSync(
buildReadyMessage(transactionId, ["m.qr_code.show.v1", "m.reciprocate.v1"], olmDeviceId),
);
await waitForVerificationRequestChanged(request);
const currentDeviceKey = e2eKeyReceiver.getSigningKey();
// the dummy device shows a QR code
const sharedSecret = "SUPERSEKRET";
// use mode 0x01, self-verifying in which the current device does trust the master key
const mode = 0x01;
const qrCodeBuffer = buildQRCode(transactionId, usermasterPubKey, currentDeviceKey, sharedSecret, mode);
// Alice scans the QR code
const sendToDevicePromise = expectSendToDeviceMessage("m.key.verification.start");
const verifier = await request.scanQRCode(qrCodeBuffer);
await sendToDevicePromise;
const verificationPromise = verifier.verify();
// the dummy device confirms that Alice scanned the QR code, by replying with a done
returnToDeviceMessageFromSync(buildDoneMessage(transactionId));
// Alice also replies with a 'done'
await expectSendToDeviceMessage("m.key.verification.done");
// ... and the whole thing should be done!
await verificationPromise;
// The other device should now be verified.
const otherDevice = (await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]))
.get(TEST_USER_ID)!
.get(olmDeviceId);
expect(otherDevice?.verified).toEqual(DeviceVerification.Verified);
}
});
async function startTestClient(opts: Partial<ICreateClientOpts> = {}): Promise<MatrixClient> {
const client = createClient({
baseUrl: TEST_HOMESERVER_URL,
@@ -927,6 +1488,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("verification (%s)", (backend: st
ev.sender ??= TEST_USER_ID;
syncResponder.sendOrQueueSyncResponse({ to_device: { events: [ev] } });
}
function returnRoomMessageFromSync(roomId: string, ev: IEvent): void {
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
rooms: {
join: {
[roomId]: { timeline: { events: [ev] } },
},
},
});
}
});
/**
@@ -947,6 +1519,52 @@ function expectSendToDeviceMessage(msgtype: string): Promise<{ messages: any }>
});
}
/**
* Utility to add all needed mocks for secret requesting (to-device of type `m.secret.request`).
*
* The following secrets are mocked: `m.cross_signing.master`, `m.cross_signing.self_signing`,
* `m.cross_signing.user_signing`, `m.megolm_backup.v1`.
*
* @returns a map of secret name to promise that will resolve (with the id of the secret request) when the secret is requested.
*/
function mockSecretRequestAndGetPromises(): Map<string, Promise<string>> {
const mskRequestDefer = defer<string>();
const sskRequestDefer = defer<string>();
const uskRequestDefer = defer<string>();
const backupKeyRequestDefer = defer<string>();
fetchMock.put(
new RegExp(`/_matrix/client/(r0|v3)/sendToDevice/m.secret.request`),
(url: string, opts: RequestInit): MockResponse => {
const messages = JSON.parse(opts.body as string).messages[TEST_USER_ID];
// rust crypto broadcasts to all devices, old crypto to a specific device, take the first one
const content = Object.values(messages)[0] as any;
if (content.action == "request") {
const name = content.name;
const requestId = content.request_id;
if (name == "m.cross_signing.user_signing") {
uskRequestDefer.resolve(requestId);
} else if (name == "m.cross_signing.master") {
mskRequestDefer.resolve(requestId);
} else if (name == "m.cross_signing.self_signing") {
sskRequestDefer.resolve(requestId);
} else if (name == "m.megolm_backup.v1") {
backupKeyRequestDefer.resolve(requestId);
}
}
return {};
},
{ overwriteRoutes: true },
);
const promiseMap = new Map<string, Promise<string>>();
promiseMap.set("m.cross_signing.master", mskRequestDefer.promise);
promiseMap.set("m.cross_signing.self_signing", sskRequestDefer.promise);
promiseMap.set("m.cross_signing.user_signing", uskRequestDefer.promise);
promiseMap.set("m.megolm_backup.v1", backupKeyRequestDefer.promise);
return promiseMap;
}
/** wait for the verification request to emit a 'Change' event */
function waitForVerificationRequestChanged(request: VerificationRequest): Promise<void> {
return new Promise<void>((resolve) => {
@@ -991,12 +1609,16 @@ function buildRequestMessage(transactionId: string): { type: string; content: ob
};
}
/** build an m.key.verification.ready to-device message originating from the dummy device */
function buildReadyMessage(transactionId: string, methods: string[]): { type: string; content: object } {
/** build an m.key.verification.ready to-device message originating from the given `fromDevice` (default to `TEST_DEVICE_ID` if not provided) */
function buildReadyMessage(
transactionId: string,
methods: string[],
fromDevice?: string,
): { type: string; content: object } {
return {
type: "m.key.verification.ready",
content: {
from_device: TEST_DEVICE_ID,
from_device: fromDevice || TEST_DEVICE_ID,
methods: methods,
transaction_id: transactionId,
},
@@ -1094,14 +1716,20 @@ function buildDoneMessage(transactionId: string) {
};
}
function buildQRCode(transactionId: string, key1Base64: string, key2Base64: string, sharedSecret: string): Uint8Array {
function buildQRCode(
transactionId: string,
key1Base64: string,
key2Base64: string,
sharedSecret: string,
mode = 0x02,
): Uint8Array {
// https://spec.matrix.org/v1.7/client-server-api/#qr-code-format
const qrCodeBuffer = Buffer.alloc(150); // oversize
let idx = 0;
idx += qrCodeBuffer.write("MATRIX", idx, "ascii");
idx = qrCodeBuffer.writeUInt8(0x02, idx); // version
idx = qrCodeBuffer.writeUInt8(0x02, idx); // mode
idx = qrCodeBuffer.writeUInt8(mode, idx); // mode
idx = qrCodeBuffer.writeInt16BE(transactionId.length, idx);
idx += qrCodeBuffer.write(transactionId, idx, "ascii");
@@ -194,6 +194,37 @@ describe("MatrixClient events", function () {
expect(fired).toBe(true);
});
it("should emit User events when presence data is absent in first sync", async () => {
const MODIFIED_SYNC_DATA: any = structuredClone(SYNC_DATA);
delete MODIFIED_SYNC_DATA["presence"];
const MODIFIED_NEXT_SYNC_DATA: any = structuredClone(NEXT_SYNC_DATA);
MODIFIED_NEXT_SYNC_DATA.presence = {
events: [
utils.mkPresence({
user: "@foo:bar",
name: "Foo Bar",
presence: "online",
}),
],
};
httpBackend!.when("GET", "/sync").respond(200, MODIFIED_SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, MODIFIED_NEXT_SYNC_DATA);
let fired = false;
client!.on(UserEvent.Presence, function (event, user) {
fired = true;
expect(user).toBeTruthy();
expect(event).toBeTruthy();
if (!user || !event) {
return;
}
expect(event.event).toEqual(MODIFIED_NEXT_SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(MODIFIED_NEXT_SYNC_DATA.presence.events[0]?.content?.presence);
});
client!.startClient();
await httpBackend!.flushAllExpected();
expect(fired).toBe(true);
});
it("should emit Room events", function () {
httpBackend!.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend!.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
+29 -36
View File
@@ -150,7 +150,7 @@ describe("MatrixClient", function () {
});
describe("joinRoom", function () {
it("should no-op if you've already joined a room", function () {
it("should no-op given the ID of a room you've already joined", async () => {
const roomId = "!foo:bar";
const room = new Room(roomId, client, userId);
client.fetchRoomEvent = () =>
@@ -168,8 +168,32 @@ describe("MatrixClient", function () {
]);
httpBackend.verifyNoOutstandingRequests();
store.storeRoom(room);
client.joinRoom(roomId);
const joinPromise = client.joinRoom(roomId);
httpBackend.verifyNoOutstandingRequests();
expect(await joinPromise).toBe(room);
});
it("should no-op given the alias of a room you've already joined", async () => {
const roomId = "!roomId:server";
const roomAlias = "#my-fancy-room:server";
const room = new Room(roomId, client, userId);
room.addLiveEvents([
utils.mkMembership({
user: userId,
room: roomId,
mship: "join",
event: true,
}),
]);
store.storeRoom(room);
// The method makes a request to resolve the alias
httpBackend.when("POST", "/join/" + encodeURIComponent(roomAlias)).respond(200, { room_id: roomId });
const joinPromise = client.joinRoom(roomAlias);
await httpBackend.flushAllExpected();
expect(await joinPromise).toBe(room);
});
it("should send request to inviteSignUrl if specified", async () => {
@@ -1180,51 +1204,20 @@ describe("MatrixClient", function () {
describe("requestLoginToken", () => {
it("should hit the expected API endpoint with UIA", async () => {
httpBackend
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
const response = {};
const uiaData = {};
const prom = client.requestLoginToken(uiaData);
httpBackend
.when("POST", "/unstable/org.matrix.msc3882/login/get_token", { auth: uiaData })
.respond(200, response);
httpBackend.when("POST", "/v1/login/get_token", { auth: uiaData }).respond(200, response);
await httpBackend.flush("");
expect(await prom).toStrictEqual(response);
});
it("should hit the expected API endpoint without UIA", async () => {
httpBackend
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: true } } });
const response = { login_token: "xyz", expires_in_ms: 5000 };
const prom = client.requestLoginToken();
httpBackend.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response);
httpBackend.when("POST", "/v1/login/get_token", {}).respond(200, response);
await httpBackend.flush("");
// check that expires_in has been populated for compatibility with r0
expect(await prom).toStrictEqual({ ...response, expires_in: 5 });
});
it("should hit the r1 endpoint when capability is disabled", async () => {
httpBackend
.when("GET", "/capabilities")
.respond(200, { capabilities: { "org.matrix.msc3882.get_login_token": { enabled: false } } });
const response = { login_token: "xyz", expires_in_ms: 5000 };
const prom = client.requestLoginToken();
httpBackend.when("POST", "/unstable/org.matrix.msc3882/login/get_token", {}).respond(200, response);
await httpBackend.flush("");
// check that expires_in has been populated for compatibility with r0
expect(await prom).toStrictEqual({ ...response, expires_in: 5 });
});
it("should hit the r0 endpoint for fallback", async () => {
httpBackend.when("GET", "/capabilities").respond(200, {});
const response = { login_token: "xyz", expires_in: 5 };
const prom = client.requestLoginToken();
httpBackend.when("POST", "/unstable/org.matrix.msc3882/login/token", {}).respond(200, response);
await httpBackend.flush("");
// check that expires_in has been populated for compatibility with r1
expect(await prom).toStrictEqual({ ...response, expires_in_ms: 5000 });
expect(await prom).toStrictEqual(response);
});
});
@@ -0,0 +1,162 @@
/*
Copyright 2023 Holi Moli GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import { MatrixClient, ClientEvent, createClient, SyncState } from "../../src";
const makeQueryablePromise = <T = void>(promise: Promise<T>) => {
let resolved = false;
let rejected = false;
// Observe the promise, saving the fulfillment in a closure scope.
const newPromise = promise.then(
(value) => {
resolved = true;
return value;
},
(error) => {
rejected = true;
throw error;
},
);
const isFulfilled = () => {
return resolved || rejected;
};
const isResolved = () => {
return resolved;
};
const isRejected = () => {
return rejected;
};
return { promise: newPromise, isFulfilled, isResolved, isRejected };
};
const queryablePromise = <T = void>() => {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
const promise = makeQueryablePromise<T>(
new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
}),
);
return { resolve, reject, ...promise };
};
describe("MatrixClient syncing errors", () => {
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const unknownTokenErrorData = {
status: 401,
body: {
errcode: "M_UNKNOWN_TOKEN",
error: "Invalid access token passed.",
soft_logout: false,
},
};
let client: MatrixClient | undefined;
beforeEach(() => {
client = createClient({
baseUrl: "http://tocal.test.server",
userId: selfUserId,
accessToken: selfAccessToken,
deviceId: "myDevice",
});
});
it("should retry, until errors are solved.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
fetchMock
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
.getOnce("end:versions", 429) // second version check fails with 429 triggering another retry
.get("end:versions", {}) // further version checks succeed
.getOnce("end:pushrules/", 429) // first pushrules check fails starting retry
.get("end:pushrules/", {}) // further pushrules check succeed
.catch({}); // all other calls succeed
const syncEvents = Array.from({ length: 5 }, queryablePromise<SyncState>);
client!.on(ClientEvent.Sync, (state: SyncState, lastState: SyncState | null) => {
let i = 0;
for (; i < syncEvents.length && syncEvents[i].isFulfilled(); i++) {
// find index of first unfulfilled promise
}
syncEvents[i].resolve(state);
});
await client!.startClient();
expect(await syncEvents[0].promise).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[1].promise).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[2].promise).toBe(SyncState.Prepared);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[3].promise).toBe(SyncState.Syncing);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive/sync
expect(await syncEvents[4].promise).toBe(SyncState.Syncing);
});
it("should stop sync keep alive when client is stopped.", async () => {
jest.useFakeTimers();
fetchMock.config.overwriteRoutes = false;
fetchMock
.getOnce("end:versions", {}) // first version check without credentials needs to succeed
.get("end:versions", unknownTokenErrorData) // further version checks fails with 401
.get("end:pushrules/", 401) // fails with 401 without an error. This does happen in practice e.g. with Synapse
.post("end:logout", unknownTokenErrorData) // just to keep up a consistent scenario. Does not have a real effect for this testcase
.post("end:filter", 401); // just to keep up a consistent scenario. Does not have a real effect for this testcase
const firstSyncEvent = queryablePromise<SyncState>();
const secondSyncEvent = queryablePromise<SyncState>();
client!.on(ClientEvent.Sync, (state: SyncState, lastState: SyncState | null) => {
if (firstSyncEvent.isFulfilled()) secondSyncEvent.resolve(state);
firstSyncEvent.resolve(state);
});
await client!.startClient();
const logoutDone = queryablePromise();
client!
.logout(true)
.then(() => {
logoutDone.resolve();
})
.catch((e) => {
logoutDone.resolve();
});
const syntState = await firstSyncEvent.promise;
expect(syntState).toBe(SyncState.Error);
jest.runAllTimers(); // this will skip forward to trigger the keepAlive
jest.useRealTimers(); // we need real timer for the setTimout below to work
const timeoutPromise = makeQueryablePromise(new Promise<void>((res) => setTimeout(res, 1)));
await Promise.race([secondSyncEvent.promise, timeoutPromise.promise]);
// when syncing stopped, then the secondSyncEvent will never happen and the promise will not be resolved,
/// so the timeoutPromise will be resolved instead
expect(timeoutPromise.isFulfilled()).toBe(true);
expect(secondSyncEvent.isFulfilled()).toBe(false);
await logoutDone.promise; // wait for the logout to finish to prevent processing and logging after the test is done.
});
});
+1 -1
View File
@@ -20,7 +20,7 @@ import { logger } from "../src/logger";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
global.Olm = require("@matrix-org/olm");
globalThis.Olm = require("@matrix-org/olm");
logger.log("loaded libolm");
} catch (e) {
logger.warn("unable to run crypto tests: libolm not available", e);
+23 -2
View File
@@ -315,6 +315,7 @@ export interface IMessageOpts {
event?: boolean;
relatesTo?: IEventRelation;
ts?: number;
unsigned?: IUnsigned;
}
/**
@@ -537,8 +538,6 @@ export async function awaitDecryption(
});
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",
@@ -561,3 +560,25 @@ CRYPTO_BACKENDS["rust-sdk"] = (client: MatrixClient) => client.initRustCrypto();
if (global.Olm) {
CRYPTO_BACKENDS["libolm"] = (client: MatrixClient) => client.initCrypto();
}
export const emitPromise = (e: EventEmitter, k: string): Promise<any> => new Promise((r) => e.once(k, r));
/**
* Advance the fake timers in a loop until the given promise resolves or rejects.
*
* Returns the result of the promise.
*
* This can be useful when there are multiple steps in the code which require an iteration of the event loop.
*/
export async function advanceTimersUntil<T>(promise: Promise<T>): Promise<T> {
let resolved = false;
promise.finally(() => {
resolved = true;
});
while (!resolved) {
await jest.advanceTimersByTimeAsync(1);
}
return await promise;
}
+88
View File
@@ -0,0 +1,88 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { TextEncoder, TextDecoder } from "util";
import NodeBuffer from "node:buffer";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64, encodeUnpaddedBase64Url } from "../../src/base64";
describe.each(["browser", "node"])("Base64 encoding (%s)", (env) => {
let origBuffer = Buffer;
beforeAll(() => {
if (env === "browser") {
origBuffer = Buffer;
// @ts-ignore
// eslint-disable-next-line no-global-assign
Buffer = undefined;
global.atob = NodeBuffer.atob;
global.btoa = NodeBuffer.btoa;
}
});
afterAll(() => {
// eslint-disable-next-line no-global-assign
Buffer = origBuffer;
// @ts-ignore
global.atob = undefined;
// @ts-ignore
global.btoa = undefined;
});
it("Should decode properly encoded data", () => {
const decoded = new TextDecoder().decode(decodeBase64("ZW5jb2RpbmcgaGVsbG8gd29ybGQ="));
expect(decoded).toStrictEqual("encoding hello world");
});
it("Should encode unpadded URL-safe base64", () => {
const toEncode = "?????";
const data = new TextEncoder().encode(toEncode);
const encoded = encodeUnpaddedBase64Url(data);
expect(encoded).toEqual("Pz8_Pz8");
});
it("Should decode URL-safe base64", () => {
const decoded = new TextDecoder().decode(decodeBase64("Pz8_Pz8="));
expect(decoded).toStrictEqual("?????");
});
it("Encode unpadded should not have padding", () => {
const toEncode = "encoding hello world";
const data = new TextEncoder().encode(toEncode);
const paddedEncoded = encodeBase64(data);
const unpaddedEncoded = encodeUnpaddedBase64(data);
expect(paddedEncoded).not.toEqual(unpaddedEncoded);
const padding = paddedEncoded.charAt(paddedEncoded.length - 1);
expect(padding).toStrictEqual("=");
});
it("Decode should be indifferent to padding", () => {
const withPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ=";
const withoutPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ";
const decodedPad = decodeBase64(withPadding);
const decodedNoPad = decodeBase64(withoutPadding);
expect(decodedPad).toStrictEqual(decodedNoPad);
});
});
+12 -2
View File
@@ -115,6 +115,16 @@ describe("Crypto", function () {
expect(Crypto.getOlmVersion()[0]).toEqual(3);
});
it("getVersion() should return the current version of the olm library", async () => {
const client = new TestClient("@alice:example.com", "deviceid").client;
await client.initCrypto();
const olmVersionTuple = Crypto.getOlmVersion();
expect(client.getCrypto()?.getVersion()).toBe(
`Olm ${olmVersionTuple[0]}.${olmVersionTuple[1]}.${olmVersionTuple[2]}`,
);
});
describe("encrypted events", function () {
it("provides encryption information for events from unverified senders", async function () {
const client = new TestClient("@alice:example.com", "deviceid").client;
@@ -258,7 +268,7 @@ describe("Crypto", function () {
const event = await buildEncryptedEvent();
expect(await client.getCrypto()!.getEncryptionInfoForEvent(event)).toEqual({
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNVERIFIED_IDENTITY,
shieldReason: EventShieldReason.UNSIGNED_DEVICE,
});
});
@@ -1106,7 +1116,7 @@ describe("Crypto", function () {
describe("Secret storage", function () {
it("creates secret storage even if there is no keyInfo", async function () {
jest.spyOn(logger, "log").mockImplementation(() => {});
jest.spyOn(logger, "debug").mockImplementation(() => {});
jest.setTimeout(10000);
const client = new TestClient("@a:example.com", "dev").client;
await client.initCrypto();
+30
View File
@@ -215,6 +215,36 @@ describe("MegolmBackup", function () {
jest.spyOn(global, "setTimeout").mockRestore();
});
test("fail if crypto not enabled", async () => {
const client = makeTestClient(cryptoStore);
const data = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
version: "1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
await expect(client.restoreKeyBackupWithSecretStorage(data)).rejects.toThrow(
"End-to-end encryption disabled",
);
});
test("fail if given backup has no version", async () => {
const client = makeTestClient(cryptoStore);
await client.initCrypto();
const data = {
algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client.getCrypto()!.storeSessionBackupPrivateKey(key, "1");
await expect(client.restoreKeyBackupWithCache(undefined, undefined, data)).rejects.toThrow(
"Backup version must be defined",
);
});
it("automatically calls the key back up", function () {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
-52
View File
@@ -1,52 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { TextEncoder, TextDecoder } from "util";
import { decodeBase64, encodeBase64, encodeUnpaddedBase64 } from "../../../src/common-crypto/base64";
describe("Crypto Base64 encoding", () => {
it("Should decode properly encoded data", async () => {
const toEncode = "encoding hello world";
const encoded = encodeBase64(new TextEncoder().encode(toEncode));
const decoded = new TextDecoder().decode(decodeBase64(encoded));
expect(decoded).toStrictEqual(toEncode);
});
it("Encode unpadded should not have padding", async () => {
const toEncode = "encoding hello world";
const data = new TextEncoder().encode(toEncode);
const paddedEncoded = encodeBase64(data);
const unpaddedEncoded = encodeUnpaddedBase64(data);
expect(paddedEncoded).not.toEqual(unpaddedEncoded);
const padding = paddedEncoded.charAt(paddedEncoded.length - 1);
expect(padding).toStrictEqual("=");
});
it("Decode should be indifferent to padding", async () => {
const withPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ=";
const withoutPadding = "ZW5jb2RpbmcgaGVsbG8gd29ybGQ";
const decodedPad = decodeBase64(withPadding);
const decodedNoPad = decodeBase64(withoutPadding);
expect(decodedPad).toStrictEqual(decodedNoPad);
});
});
+5 -4
View File
@@ -28,6 +28,7 @@ import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { ISignatures } from "../../../src/@types/signed";
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { decodeBase64 } from "../../../src/base64";
async function makeTestClient(
userInfo: { userId: string; deviceId: string },
@@ -275,13 +276,13 @@ describe("Secrets", function () {
describe("bootstrap", function () {
// keys used in some of the tests
const XSK = new Uint8Array(olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="));
const XSK = new Uint8Array(decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="));
const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0";
const USK = new Uint8Array(olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="));
const USK = new Uint8Array(decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="));
const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU";
const SSK = new Uint8Array(olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="));
const SSK = new Uint8Array(decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="));
const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q";
const SSSSKey = new Uint8Array(olmlib.decodeBase64("XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0="));
const SSSSKey = new Uint8Array(decodeBase64("XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0="));
it("bootstraps when no storage or cross-signing keys locally", async function () {
const key = new Uint8Array(16);
@@ -16,7 +16,7 @@ limitations under the License.
import "../../../olm-loader";
import { MatrixClient, MatrixEvent } from "../../../../src/matrix";
import { encodeBase64 } from "../../../../src/crypto/olmlib";
import { encodeBase64 } from "../../../../src/base64";
import "../../../../src/crypto"; // import this to cycle-break
import { CrossSigningInfo } from "../../../../src/crypto/CrossSigning";
import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest";
+1
View File
@@ -34,6 +34,7 @@ describe("eventMapperFor", function () {
getRoom(roomId: string): Room | null {
return rooms.find((r) => r.roomId === roomId) ?? null;
},
setUserCreator(_) {},
} as IStore,
scheduler: {
setProcessFunction: jest.fn(),
+161 -14
View File
@@ -14,14 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked } from "jest-mock";
import { Mocked } from "jest-mock";
import { FetchHttpApi } from "../../../src/http-api/fetch";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { ClientPrefix, HttpApiEvent, HttpApiEventHandlerMap, IdentityPrefix, IHttpOpts, Method } from "../../../src";
import {
ClientPrefix,
HttpApiEvent,
HttpApiEventHandlerMap,
IdentityPrefix,
IHttpOpts,
MatrixError,
Method,
} from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
import { defer, QueryDict } from "../../../src/utils";
import { logger } from "../../../src/logger";
import { Logger } from "../../../src/logger";
describe("FetchHttpApi", () => {
const baseUrl = "http://baseUrl";
@@ -231,13 +239,145 @@ describe("FetchHttpApi", () => {
});
describe("authedRequest", () => {
it("should not include token if unset", () => {
const fetchFn = jest.fn();
it("should not include token if unset", async () => {
const fetchFn = jest.fn().mockResolvedValue({ ok: true });
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn });
api.authedRequest(Method.Post, "/account/password");
await api.authedRequest(Method.Post, "/account/password");
expect(fetchFn.mock.calls[0][1].headers.Authorization).toBeUndefined();
});
describe("with refresh token", () => {
const accessToken = "test-access-token";
const refreshToken = "test-refresh-token";
describe("when an unknown token error is encountered", () => {
const unknownTokenErrBody = {
errcode: "M_UNKNOWN_TOKEN",
error: "Token is not active",
soft_logout: false,
};
const unknownTokenErr = new MatrixError(unknownTokenErrBody, 401);
const unknownTokenResponse = {
ok: false,
status: 401,
headers: {
get(name: string): string | null {
return name === "Content-Type" ? "application/json" : null;
},
},
text: jest.fn().mockResolvedValue(JSON.stringify(unknownTokenErrBody)),
};
const okayResponse = {
ok: true,
status: 200,
};
describe("without a tokenRefreshFunction", () => {
it("should emit logout and throw", async () => {
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, { baseUrl, prefix, fetchFn, accessToken, refreshToken });
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
unknownTokenErr,
);
expect(emitter.emit).toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
});
});
describe("with a tokenRefreshFunction", () => {
it("should emit logout and throw when token refresh fails", async () => {
const error = new Error("uh oh");
const tokenRefreshFunction = jest.fn().mockRejectedValue(error);
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
fetchFn,
tokenRefreshFunction,
accessToken,
refreshToken,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
unknownTokenErr,
);
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
expect(emitter.emit).toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
});
it("should refresh token and retry request", async () => {
const newAccessToken = "new-access-token";
const newRefreshToken = "new-refresh-token";
const tokenRefreshFunction = jest.fn().mockResolvedValue({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
});
const fetchFn = jest
.fn()
.mockResolvedValueOnce(unknownTokenResponse)
.mockResolvedValueOnce(okayResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
fetchFn,
tokenRefreshFunction,
accessToken,
refreshToken,
});
const result = await api.authedRequest(Method.Post, "/account/password");
expect(result).toEqual(okayResponse);
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
expect(fetchFn).toHaveBeenCalledTimes(2);
// uses new access token
expect(fetchFn.mock.calls[1][1].headers.Authorization).toEqual("Bearer new-access-token");
expect(emitter.emit).not.toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
});
it("should only try to refresh the token once", async () => {
const newAccessToken = "new-access-token";
const newRefreshToken = "new-refresh-token";
const tokenRefreshFunction = jest.fn().mockResolvedValue({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
});
// fetch doesn't like our new or old tokens
const fetchFn = jest.fn().mockResolvedValue(unknownTokenResponse);
const emitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
jest.spyOn(emitter, "emit");
const api = new FetchHttpApi(emitter, {
baseUrl,
prefix,
fetchFn,
tokenRefreshFunction,
accessToken,
refreshToken,
});
await expect(api.authedRequest(Method.Post, "/account/password")).rejects.toThrow(
unknownTokenErr,
);
// tried to refresh the token once
expect(tokenRefreshFunction).toHaveBeenCalledWith(refreshToken);
expect(tokenRefreshFunction).toHaveBeenCalledTimes(1);
expect(fetchFn).toHaveBeenCalledTimes(2);
// uses new access token on retry
expect(fetchFn.mock.calls[1][1].headers.Authorization).toEqual("Bearer new-access-token");
// logged out after refreshed access token is rejected
expect(emitter.emit).toHaveBeenCalledWith(HttpApiEvent.SessionLoggedOut, unknownTokenErr);
});
});
});
});
});
describe("getUrl()", () => {
@@ -300,22 +440,29 @@ describe("FetchHttpApi", () => {
jest.useFakeTimers();
const deferred = defer<Response>();
const fetchFn = jest.fn().mockReturnValue(deferred.promise);
jest.spyOn(logger, "debug").mockImplementation(() => {});
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), { baseUrl, prefix, fetchFn });
const mockLogger = {
debug: jest.fn(),
} as unknown as Mocked<Logger>;
const api = new FetchHttpApi(new TypedEventEmitter<any, any>(), {
baseUrl,
prefix,
fetchFn,
logger: mockLogger,
});
const prom = api.requestOtherUrl(Method.Get, "https://server:8448/some/path?query=param#fragment");
jest.advanceTimersByTime(1234);
deferred.resolve({ ok: true, status: 200, text: () => Promise.resolve("RESPONSE") } as Response);
await prom;
expect(logger.debug).not.toHaveBeenCalledWith("fragment");
expect(logger.debug).not.toHaveBeenCalledWith("query");
expect(logger.debug).not.toHaveBeenCalledWith("param");
expect(logger.debug).toHaveBeenCalledTimes(2);
expect(mocked(logger.debug).mock.calls[0]).toMatchInlineSnapshot(`
expect(mockLogger.debug).not.toHaveBeenCalledWith("fragment");
expect(mockLogger.debug).not.toHaveBeenCalledWith("query");
expect(mockLogger.debug).not.toHaveBeenCalledWith("param");
expect(mockLogger.debug).toHaveBeenCalledTimes(2);
expect(mockLogger.debug.mock.calls[0]).toMatchInlineSnapshot(`
[
"FetchHttpApi: --> GET https://server:8448/some/path?query=xxx",
]
`);
expect(mocked(logger.debug).mock.calls[1]).toMatchInlineSnapshot(`
expect(mockLogger.debug.mock.calls[1]).toMatchInlineSnapshot(`
[
"FetchHttpApi: <-- GET https://server:8448/some/path?query=xxx [1234ms 200]",
]
+1
View File
@@ -328,6 +328,7 @@ describe("MatrixClient", function () {
"storeFilter",
"startup",
"deleteAllData",
"setUserCreator",
] as const
).reduce((r, k) => {
r[k] = jest.fn();
+3 -4
View File
@@ -23,14 +23,13 @@ const membershipTemplate: CallMembershipData = {
application: "m.call",
device_id: "AAAAAAA",
expires: 5000,
membershipID: "bloop",
};
function makeMockEvent(originTs = 0): MatrixEvent {
return {
getTs: jest.fn().mockReturnValue(originTs),
sender: {
userId: "@alice:example.org",
},
getSender: jest.fn().mockReturnValue("@alice:example.org"),
} as unknown as MatrixEvent;
}
@@ -86,7 +85,7 @@ describe("CallMembership", () => {
it("considers memberships expired when local age large", () => {
const fakeEvent = makeMockEvent(1000);
fakeEvent.getLocalAge = jest.fn().mockReturnValue(6000);
fakeEvent.localTimestamp = Date.now() - 6000;
const membership = new CallMembership(fakeEvent, membershipTemplate);
expect(membership.isExpired()).toEqual(true);
});
+357 -44
View File
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventTimeline, EventType, MatrixClient, Room } from "../../../src";
import { EventTimeline, EventType, MatrixClient, MatrixError, MatrixEvent, Room } from "../../../src";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSession, MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession";
import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types";
import { randomString } from "../../../src/randomstring";
import { makeMockRoom, mockRTCEvent } from "./mocks";
import { makeMockRoom, makeMockRoomState, mockRTCEvent } from "./mocks";
const membershipTemplate: CallMembershipData = {
call_id: "",
@@ -26,6 +27,7 @@ const membershipTemplate: CallMembershipData = {
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
};
const mockFocus = { type: "mock" };
@@ -56,14 +58,16 @@ describe("MatrixRTCSession", () => {
expect(sess?.memberships[0].scope).toEqual("m.room");
expect(sess?.memberships[0].application).toEqual("m.call");
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships[0].membershipID).toEqual("bloop");
expect(sess?.memberships[0].isExpired()).toEqual(false);
expect(sess?.callId).toEqual("");
});
it("ignores expired memberships events", () => {
const expiredMembership = Object.assign({}, membershipTemplate);
expiredMembership.expires = 1000;
expiredMembership.device_id = "EXPIRED";
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], () => 10000);
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership], 10000);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess?.memberships.length).toEqual(1);
@@ -182,8 +186,15 @@ describe("MatrixRTCSession", () => {
describe("joining", () => {
let mockRoom: Room;
let sendStateEventMock: jest.Mock;
let sendEventMock: jest.Mock;
beforeEach(() => {
sendStateEventMock = jest.fn();
sendEventMock = jest.fn();
client.sendStateEvent = sendStateEventMock;
client.sendEvent = sendEventMock;
mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
});
@@ -203,8 +214,6 @@ describe("MatrixRTCSession", () => {
});
it("sends a membership event when joining a call", () => {
client.sendStateEvent = jest.fn();
sess!.joinRoomSession([mockFocus]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
@@ -219,6 +228,7 @@ describe("MatrixRTCSession", () => {
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [{ type: "mock" }],
membershipID: expect.stringMatching(".*"),
},
],
},
@@ -227,9 +237,6 @@ describe("MatrixRTCSession", () => {
});
it("does nothing if join called when already joined", () => {
const sendStateEventMock = jest.fn();
client.sendStateEvent = sendStateEventMock;
sess!.joinRoomSession([mockFocus]);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
@@ -259,7 +266,7 @@ describe("MatrixRTCSession", () => {
const timeElapsed = 60 * 60 * 1000 - 1000;
mockRoom.getLiveTimeline().getState(EventTimeline.FORWARDS)!.getStateEvents = jest
.fn()
.mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, () => timeElapsed));
.mockReturnValue(mockRTCEvent(eventContent.memberships, mockRoom.roomId, timeElapsed));
const eventReSentPromise = new Promise<Record<string, any>>((r) => {
resolveFn = (_roomId: string, _type: string, val: Record<string, any>) => {
@@ -286,6 +293,7 @@ describe("MatrixRTCSession", () => {
expires: 3600000 * 2,
foci_active: [{ type: "mock" }],
created_ts: 1000,
membershipID: expect.stringMatching(".*"),
},
],
},
@@ -295,15 +303,244 @@ describe("MatrixRTCSession", () => {
jest.useRealTimers();
}
});
it("creates a key when joining", () => {
sess!.joinRoomSession([mockFocus], true);
const keys = sess?.getKeysForParticipant("@alice:example.org", "AAAAAAA");
expect(keys).toHaveLength(1);
const allKeys = sess!.getEncryptionKeys();
expect(allKeys).toBeTruthy();
expect(Array.from(allKeys)).toHaveLength(1);
});
it("sends keys when joining", async () => {
const eventSentPromise = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
sess!.joinRoomSession([mockFocus], true);
await eventSentPromise;
expect(sendEventMock).toHaveBeenCalledWith(expect.stringMatching(".*"), "io.element.call.encryption_keys", {
call_id: "",
device_id: "AAAAAAA",
keys: [
{
index: 0,
key: expect.stringMatching(".*"),
},
],
});
});
it("retries key sends", async () => {
jest.useFakeTimers();
let firstEventSent = false;
try {
const eventSentPromise = new Promise<void>((resolve) => {
sendEventMock.mockImplementation(() => {
if (!firstEventSent) {
jest.advanceTimersByTime(10000);
firstEventSent = true;
const e = new Error() as MatrixError;
e.data = {};
throw e;
} else {
resolve();
}
});
});
sess!.joinRoomSession([mockFocus], true);
jest.advanceTimersByTime(10000);
await eventSentPromise;
expect(sendEventMock).toHaveBeenCalledTimes(2);
} finally {
jest.useRealTimers();
}
});
it("cancels key send event that fail", async () => {
const eventSentinel = {} as unknown as MatrixEvent;
client.cancelPendingEvent = jest.fn();
sendEventMock.mockImplementation(() => {
const e = new Error() as MatrixError;
e.data = {};
e.event = eventSentinel;
throw e;
});
sess!.joinRoomSession([mockFocus], true);
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
});
it("Re-sends key if a new member joins", async () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const keysSentPromise1 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
sess.joinRoomSession([mockFocus], true);
await keysSentPromise1;
sendEventMock.mockClear();
jest.advanceTimersByTime(10000);
const keysSentPromise2 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
const member2 = Object.assign({}, membershipTemplate, {
device_id: "BBBBBBB",
});
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId, undefined));
sess.onMembershipUpdate();
await keysSentPromise2;
expect(sendEventMock).toHaveBeenCalled();
} finally {
jest.useRealTimers();
}
});
it("Rotates key if a member leaves", async () => {
jest.useFakeTimers();
try {
const member2 = Object.assign({}, membershipTemplate, {
device_id: "BBBBBBB",
});
const mockRoom = makeMockRoom([membershipTemplate, member2]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const onMyEncryptionKeyChanged = jest.fn();
sess.on(
MatrixRTCSessionEvent.EncryptionKeyChanged,
(_key: Uint8Array, _idx: number, participantId: string) => {
if (participantId === `${client.getUserId()}:${client.getDeviceId()}`) {
onMyEncryptionKeyChanged();
}
},
);
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
sess.joinRoomSession([mockFocus], true);
const firstKeysPayload = await keysSentPromise1;
expect(firstKeysPayload.keys).toHaveLength(1);
sendEventMock.mockClear();
const keysSentPromise2 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
});
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate], mockRoom.roomId, undefined));
sess.onMembershipUpdate();
jest.advanceTimersByTime(10000);
const secondKeysPayload = await keysSentPromise2;
expect(secondKeysPayload.keys).toHaveLength(2);
expect(onMyEncryptionKeyChanged).toHaveBeenCalledTimes(2);
} finally {
jest.useRealTimers();
}
});
it("Doesn't re-send key immediately", async () => {
const realSetImmediate = setImmediate;
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const keysSentPromise1 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
});
sess.joinRoomSession([mockFocus], true);
await keysSentPromise1;
sendEventMock.mockClear();
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
const member2 = Object.assign({}, membershipTemplate, {
device_id: "BBBBBBB",
});
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([membershipTemplate, member2], mockRoom.roomId, undefined));
sess.onMembershipUpdate();
await new Promise((resolve) => {
realSetImmediate(resolve);
});
expect(sendEventMock).not.toHaveBeenCalled();
} finally {
jest.useRealTimers();
}
});
});
it("Does not emits if no membership changes", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
sess.onMembershipUpdate();
expect(onMembershipsChanged).not.toHaveBeenCalled();
});
it("Emits on membership changes", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
mockRoom.getLiveTimeline().getState = jest
.fn()
.mockReturnValue(makeMockRoomState([], mockRoom.roomId, undefined));
sess.onMembershipUpdate();
expect(onMembershipsChanged).toHaveBeenCalled();
});
it("emits an event at the time a membership event expires", () => {
jest.useFakeTimers();
try {
let eventAge = 0;
const membership = Object.assign({}, membershipTemplate);
const mockRoom = makeMockRoom([membership], () => eventAge);
const mockRoom = makeMockRoom([membership], 0);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const membershipObject = sess.memberships[0];
@@ -311,7 +548,6 @@ describe("MatrixRTCSession", () => {
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
eventAge = 61 * 1000 * 1000;
jest.advanceTimersByTime(61 * 1000 * 1000);
expect(onMembershipsChanged).toHaveBeenCalledWith([membershipObject], []);
@@ -322,46 +558,49 @@ describe("MatrixRTCSession", () => {
});
it("prunes expired memberships on update", () => {
client.sendStateEvent = jest.fn();
jest.useFakeTimers();
try {
client.sendStateEvent = jest.fn();
let eventAge = 0;
const mockRoom = makeMockRoom(
[
const mockMemberships = [
Object.assign({}, membershipTemplate, {
device_id: "OTHERDEVICE",
expires: 1000,
}),
],
() => eventAge,
);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
];
const mockRoomNoExpired = makeMockRoom(mockMemberships, 0);
// sanity check
expect(sess.memberships).toHaveLength(1);
expect(sess.memberships[0].deviceId).toEqual("OTHERDEVICE");
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoomNoExpired);
eventAge = 10000;
// sanity check
expect(sess.memberships).toHaveLength(1);
expect(sess.memberships[0].deviceId).toEqual("OTHERDEVICE");
sess.joinRoomSession([mockFocus]);
jest.advanceTimersByTime(10000);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [mockFocus],
},
],
},
"@alice:example.org",
);
sess.joinRoomSession([mockFocus]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoomNoExpired!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
},
"@alice:example.org",
);
} finally {
jest.useRealTimers();
}
});
it("fills in created_ts for other memberships on update", () => {
@@ -388,6 +627,7 @@ describe("MatrixRTCSession", () => {
device_id: "OTHERDEVICE",
expires: 3600000,
created_ts: 1000,
membershipID: expect.stringMatching(".*"),
},
{
application: "m.call",
@@ -396,10 +636,83 @@ describe("MatrixRTCSession", () => {
device_id: "AAAAAAA",
expires: 3600000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
},
"@alice:example.org",
);
});
it("collects keys from encryption events", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 0,
key: "dGhpcyBpcyB0aGUga2V5",
},
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(1);
expect(bobKeys[0]).toEqual(Buffer.from("this is the key", "utf-8"));
});
it("collects keys at non-zero indices", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: "bobsphone",
call_id: "",
keys: [
{
index: 4,
key: "dGhpcyBpcyB0aGUga2V5",
},
],
}),
getSender: jest.fn().mockReturnValue("@bob:example.org"),
} as unknown as MatrixEvent);
const bobKeys = sess.getKeysForParticipant("@bob:example.org", "bobsphone")!;
expect(bobKeys).toHaveLength(5);
expect(bobKeys[0]).toBeFalsy();
expect(bobKeys[1]).toBeFalsy();
expect(bobKeys[2]).toBeFalsy();
expect(bobKeys[3]).toBeFalsy();
expect(bobKeys[4]).toEqual(Buffer.from("this is the key", "utf-8"));
});
it("ignores keys event for the local participant", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess.onCallEncryption({
getType: jest.fn().mockReturnValue("io.element.call.encryption_keys"),
getContent: jest.fn().mockReturnValue({
device_id: client.getDeviceId(),
call_id: "",
keys: [
{
index: 4,
key: "dGhpcyBpcyB0aGUga2V5",
},
],
}),
getSender: jest.fn().mockReturnValue(client.getUserId()),
} as unknown as MatrixEvent);
const myKeys = sess.getKeysForParticipant(client.getUserId()!, client.getDeviceId()!)!;
expect(myKeys).toBeFalsy();
});
});
@@ -14,7 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
import {
ClientEvent,
EventTimeline,
EventType,
IRoomTimelineData,
MatrixClient,
MatrixEvent,
RoomEvent,
} from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
@@ -26,6 +34,7 @@ const membershipTemplate: CallMembershipData = {
application: "m.call",
device_id: "AAAAAAA",
expires: 60 * 60 * 1000,
membershipID: "bloop",
};
describe("MatrixRTCSessionManager", () => {
@@ -77,4 +86,26 @@ describe("MatrixRTCSessionManager", () => {
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
it("Calls onCallEncryption on encryption keys event", () => {
const room1 = makeMockRoom([membershipTemplate]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
const onCallEncryptionMock = jest.fn();
client.matrixRTC.getRoomSession(room1).onCallEncryption = onCallEncryptionMock;
const timelineEvent = {
getType: jest.fn().mockReturnValue(EventType.CallEncryptionKeysPrefix),
getContent: jest.fn().mockReturnValue({}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getRoomId: jest.fn().mockReturnValue("!room:id"),
sender: {
userId: "@mock:user.example",
},
} as unknown as MatrixEvent;
client.emit(RoomEvent.Timeline, timelineEvent, undefined, undefined, false, {} as IRoomTimelineData);
expect(onCallEncryptionMock).toHaveBeenCalled();
});
});
+8 -17
View File
@@ -18,37 +18,29 @@ import { EventType, MatrixEvent, Room } from "../../../src";
import { CallMembershipData } from "../../../src/matrixrtc/CallMembership";
import { randomString } from "../../../src/randomstring";
export function makeMockRoom(
memberships: CallMembershipData[],
getLocalAge: (() => number) | undefined = undefined,
): Room {
export function makeMockRoom(memberships: CallMembershipData[], localAge: number | null = null): Room {
const roomId = randomString(8);
// Caching roomState here so it does not get recreated when calling `getLiveTimeline.getState()`
const roomState = makeMockRoomState(memberships, roomId, localAge);
return {
roomId: roomId,
getLiveTimeline: jest.fn().mockReturnValue({
getState: jest.fn().mockReturnValue(makeMockRoomState(memberships, roomId, getLocalAge)),
getState: jest.fn().mockReturnValue(roomState),
}),
} as unknown as Room;
}
function makeMockRoomState(memberships: CallMembershipData[], roomId: string, getLocalAge: (() => number) | undefined) {
export function makeMockRoomState(memberships: CallMembershipData[], roomId: string, localAge: number | null = null) {
const event = mockRTCEvent(memberships, roomId, localAge);
return {
getStateEvents: (_: string, stateKey: string) => {
const event = mockRTCEvent(memberships, roomId, getLocalAge);
if (stateKey !== undefined) return event;
return [event];
},
};
}
export function mockRTCEvent(
memberships: CallMembershipData[],
roomId: string,
getLocalAge: (() => number) | undefined,
): MatrixEvent {
const getLocalAgeFn = getLocalAge ?? (() => 10);
export function mockRTCEvent(memberships: CallMembershipData[], roomId: string, localAge: number | null): MatrixEvent {
return {
getType: jest.fn().mockReturnValue(EventType.GroupCallMemberPrefix),
getContent: jest.fn().mockReturnValue({
@@ -56,8 +48,7 @@ export function mockRTCEvent(
}),
getSender: jest.fn().mockReturnValue("@mock:user.example"),
getTs: jest.fn().mockReturnValue(1000),
getLocalAge: getLocalAgeFn,
localTimestamp: Date.now(),
localTimestamp: Date.now() - (localAge ?? 10),
getRoomId: jest.fn().mockReturnValue(roomId),
sender: {
userId: "@mock:user.example",
+279 -21
View File
@@ -14,10 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MockedObject } from "jest-mock";
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
import { emitPromise } from "../../test-utils/test-utils";
import { Crypto, IEventDecryptionResult } from "../../../src/crypto";
import { IAnnotatedPushRule, PushRuleActionName, TweakName } from "../../../src";
import {
IAnnotatedPushRule,
MatrixClient,
PushRuleActionName,
Room,
THREAD_RELATION_TYPE,
TweakName,
} from "../../../src";
describe("MatrixEvent", () => {
it("should create copies of itself", () => {
@@ -61,31 +70,264 @@ describe("MatrixEvent", () => {
expect(a.toSnapshot().isEquivalentTo(b)).toBe(false);
});
it("should prune clearEvent when being redacted", () => {
const ev = new MatrixEvent({
type: "m.room.message",
content: {
body: "Test",
},
event_id: "$event1:server",
describe("redaction", () => {
it("should prune clearEvent when being redacted", () => {
const ev = createEvent("$event1:server", "Test");
expect(ev.getContent().body).toBe("Test");
expect(ev.getWireContent().body).toBe("Test");
ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", "");
expect(ev.getContent().body).toBe("Test");
expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).toBe("xyz");
const mockClient = {} as unknown as MockedObject<MatrixClient>;
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const redaction = createRedaction(ev.getId()!);
ev.makeRedacted(redaction, room);
expect(ev.getContent().body).toBeUndefined();
expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).toBeUndefined();
});
expect(ev.getContent().body).toBe("Test");
expect(ev.getWireContent().body).toBe("Test");
ev.makeEncrypted("m.room.encrypted", { ciphertext: "xyz" }, "", "");
expect(ev.getContent().body).toBe("Test");
expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).toBe("xyz");
it("should remain in the main timeline when redacted", async () => {
// Given an event in the main timeline
const mockClient = createMockClient();
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const ev = createEvent("$event1:server");
const redaction = new MatrixEvent({
type: "m.room.redaction",
redacts: ev.getId(),
await room.addLiveEvents([ev]);
await room.createThreadsTimelineSets();
expect(ev.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
// When I redact it
const redaction = createRedaction(ev.getId()!);
ev.makeRedacted(redaction, room);
// Then it remains in the main timeline
expect(ev.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([ev.getId()]);
});
ev.makeRedacted(redaction);
expect(ev.getContent().body).toBeUndefined();
expect(ev.getWireContent().body).toBeUndefined();
expect(ev.getWireContent().ciphertext).toBeUndefined();
it("should keep thread roots in both timelines when redacted", async () => {
// Given a thread exists
const mockClient = createMockClient();
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]);
await room.createThreadsTimelineSets();
expect(threadRoot.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId()]);
// When I redact the thread root
const redaction = createRedaction(ev.getId()!);
threadRoot.makeRedacted(redaction, room);
// Then it remains in the main timeline and the thread
expect(threadRoot.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId()]);
});
it("should move into the main timeline when redacted", async () => {
// Given an event in a thread
const mockClient = createMockClient();
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
await room.addLiveEvents([threadRoot, ev]);
await room.createThreadsTimelineSets();
expect(ev.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId()]);
// When I redact it
const redaction = createRedaction(ev.getId()!);
ev.makeRedacted(redaction, room);
// Then it disappears from the thread and appears in the main timeline
expect(ev.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId()]);
expect(threadLiveEventIds(room, 0)).not.toContain(ev.getId());
});
it("should move reactions to a redacted event into the main timeline", async () => {
// Given an event in a thread with a reaction
const mockClient = createMockClient();
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const reaction = createReactionEvent("$reaction:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, reaction]);
await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId(), reaction.getId()]);
// When I redact the event
const redaction = createRedaction(ev.getId()!);
ev.makeRedacted(redaction, room);
// Then the reaction moves into the main timeline
expect(reaction.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), reaction.getId()]);
expect(threadLiveEventIds(room, 0)).not.toContain(reaction.getId());
});
it("should move edits of a redacted event into the main timeline", async () => {
// Given an event in a thread with a reaction
const mockClient = createMockClient();
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const edit = createEditEvent("$edit:server", ev.getId()!);
await room.addLiveEvents([threadRoot, ev, edit]);
await room.createThreadsTimelineSets();
expect(edit.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
expect(threadLiveEventIds(room, 0)).toEqual([threadRoot.getId(), ev.getId(), edit.getId()]);
// When I redact the event
const redaction = createRedaction(ev.getId()!);
ev.makeRedacted(redaction, room);
// Then the edit moves into the main timeline
expect(edit.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId(), ev.getId(), edit.getId()]);
expect(threadLiveEventIds(room, 0)).not.toContain(edit.getId());
});
it("should move reactions to replies to replies a redacted event into the main timeline", async () => {
// Given an event in a thread with a reaction
const mockClient = createMockClient();
const room = new Room("!roomid:e.xyz", mockClient, "myname");
const threadRoot = createEvent("$threadroot:server");
const ev = createThreadedEvent("$event1:server", threadRoot.getId()!);
const reply1 = createReplyEvent("$reply1:server", ev.getId()!);
const reply2 = createReplyEvent("$reply2:server", reply1.getId()!);
const reaction = createReactionEvent("$reaction:server", reply2.getId()!);
await room.addLiveEvents([threadRoot, ev, reply1, reply2, reaction]);
await room.createThreadsTimelineSets();
expect(reaction.threadRootId).toEqual(threadRoot.getId());
expect(mainTimelineLiveEventIds(room)).toEqual([threadRoot.getId()]);
expect(threadLiveEventIds(room, 0)).toEqual([
threadRoot.getId(),
ev.getId(),
reply1.getId(),
reply2.getId(),
reaction.getId(),
]);
// When I redact the event
const redaction = createRedaction(ev.getId()!);
ev.makeRedacted(redaction, room);
// Then the replies move to the main thread and the reaction disappears
expect(reaction.threadRootId).toBeUndefined();
expect(mainTimelineLiveEventIds(room)).toEqual([
threadRoot.getId(),
ev.getId(),
reply1.getId(),
reply2.getId(),
reaction.getId(),
]);
expect(threadLiveEventIds(room, 0)).not.toContain(reply1.getId());
expect(threadLiveEventIds(room, 0)).not.toContain(reply2.getId());
expect(threadLiveEventIds(room, 0)).not.toContain(reaction.getId());
});
function createMockClient(): MatrixClient {
return {
supportsThreads: jest.fn().mockReturnValue(true),
decryptEventIfNeeded: jest.fn().mockReturnThis(),
getUserId: jest.fn().mockReturnValue("@user:server"),
} as unknown as MockedObject<MatrixClient>;
}
function createEvent(eventId: string, body?: string): MatrixEvent {
return new MatrixEvent({
type: "m.room.message",
content: {
body: body ?? eventId,
},
event_id: eventId,
});
}
function createThreadedEvent(eventId: string, threadRootId: string): MatrixEvent {
return new MatrixEvent({
type: "m.room.message",
content: {
"body": eventId,
"m.relates_to": {
rel_type: THREAD_RELATION_TYPE.name,
event_id: threadRootId,
},
},
event_id: eventId,
});
}
function createEditEvent(eventId: string, repliedToId: string): MatrixEvent {
return new MatrixEvent({
type: "m.room.message",
content: {
"body": "Edited",
"m.new_content": {
body: "Edited",
},
"m.relates_to": {
event_id: repliedToId,
rel_type: "m.replace",
},
},
event_id: eventId,
});
}
function createReplyEvent(eventId: string, repliedToId: string): MatrixEvent {
return new MatrixEvent({
type: "m.room.message",
content: {
"m.relates_to": {
event_id: repliedToId,
key: "x",
rel_type: "m.in_reply_to",
},
},
event_id: eventId,
});
}
function createReactionEvent(eventId: string, reactedToId: string): MatrixEvent {
return new MatrixEvent({
type: "m.reaction",
content: {
"m.relates_to": {
event_id: reactedToId,
key: "x",
rel_type: "m.annotation",
},
},
event_id: eventId,
});
}
function createRedaction(redactedEventid: string): MatrixEvent {
return new MatrixEvent({
type: "m.room.redaction",
redacts: redactedEventid,
});
}
});
describe("applyVisibilityEvent", () => {
@@ -330,3 +572,19 @@ describe("MatrixEvent", () => {
expect(stateEvent.threadRootId).toBeUndefined();
});
});
function mainTimelineLiveEventIds(room: Room): Array<string> {
return room
.getLiveTimeline()
.getEvents()
.map((e) => e.getId()!);
}
function threadLiveEventIds(room: Room, threadIndex: number): Array<string> {
return room
.getThreads()
[threadIndex].getUnfilteredTimelineSet()
.getLiveTimeline()
.getEvents()
.map((e) => e.getId()!);
}
+63
View File
@@ -675,6 +675,69 @@ describe("Thread", () => {
});
});
});
describe("addEvent", () => {
describe("Given server support for threads", () => {
let previousThreadHasServerSideSupport: FeatureSupport;
beforeAll(() => {
previousThreadHasServerSideSupport = Thread.hasServerSideSupport;
Thread.hasServerSideSupport = FeatureSupport.Stable;
});
afterAll(() => {
Thread.hasServerSideSupport = previousThreadHasServerSideSupport;
});
it("Adds events even if they appear out of order", async () => {
// Given a thread exists
const client = createClient();
const user = "@alice:matrix.org";
const room = "!room:z";
const thread = await createThread(client, user, room);
const prevNumEvents = thread.timeline.length;
// When two messages come in but the later one has an older timestamp
const message1 = createThreadMessage(thread.id, user, room, "message1");
const message2 = createThreadMessage(thread.id, user, room, "message2");
message2.localTimestamp -= 10000;
await thread.addEvent(message1, false);
await thread.addEvent(message2, false);
// Then both events end up in the timeline
expect(thread.timeline.length - prevNumEvents).toEqual(2);
const lastEvent = thread.timeline.at(-1)!;
const secondLastEvent = thread.timeline.at(-2)!;
expect(lastEvent).toBe(message2);
expect(secondLastEvent).toBe(message1);
});
it("Adds events to start even if they appear out of order", async () => {
// Given a thread exists
const client = createClient();
const user = "@alice:matrix.org";
const room = "!room:z";
const thread = await createThread(client, user, room);
const prevNumEvents = thread.timeline.length;
// When two messages come in but the later one has an older timestamp
const message1 = createThreadMessage(thread.id, user, room, "message1");
const message2 = createThreadMessage(thread.id, user, room, "message2");
message2.localTimestamp -= 10000;
await thread.addEvent(message1, false);
await thread.addEvent(message2, true);
// Then both events end up in the timeline
expect(thread.timeline.length - prevNumEvents).toEqual(2);
const lastEvent = thread.timeline.at(-1)!;
const firstEvent = thread.timeline.at(0)!;
expect(lastEvent).toBe(message1);
expect(firstEvent).toBe(message2);
});
});
});
});
/**
+21
View File
@@ -134,6 +134,25 @@ describe("oidc authorization", () => {
expect(authUrl.searchParams.get("code_challenge")).toBeTruthy();
});
it("should generate url with create prompt", async () => {
const nonce = "abc123";
const metadata = delegatedAuthConfig.metadata;
const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
nonce,
prompt: "create",
}),
);
expect(authUrl.searchParams.get("prompt")).toEqual("create");
});
});
describe("completeAuthorizationCodeGrant", () => {
@@ -284,6 +303,7 @@ describe("oidc authorization", () => {
expires_at: result.tokenResponse.expires_at,
scope,
},
idTokenClaims: result.idTokenClaims,
});
});
@@ -325,6 +345,7 @@ describe("oidc authorization", () => {
expires_at: result.tokenResponse.expires_at,
scope,
},
idTokenClaims: result.idTokenClaims,
});
expect(result.tokenResponse.token_type).toEqual("Bearer");
+270
View File
@@ -0,0 +1,270 @@
/**
* @jest-environment jsdom
*/
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import fetchMock from "fetch-mock-jest";
import { OidcTokenRefresher } from "../../../src";
import { logger } from "../../../src/logger";
import { makeDelegatedAuthConfig } from "../../test-utils/oidc";
describe("OidcTokenRefresher", () => {
// OidcTokenRefresher props
// see class declaration for info
const authConfig = {
issuer: "https://issuer.org/",
};
const clientId = "test-client-id";
const redirectUri = "https://test.org";
const deviceId = "abc123";
const idTokenClaims = {
exp: Date.now() / 1000 + 100000,
aud: clientId,
iss: authConfig.issuer,
sub: "123",
iat: 123,
};
// used to mock a valid token response, as consumed by OidcClient library
const scope = `openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:${deviceId}`;
// auth config used in mocked calls to OP .well-known
const config = makeDelegatedAuthConfig(authConfig.issuer);
const makeTokenResponse = (accessToken: string, refreshToken?: string) => ({
access_token: accessToken,
refresh_token: refreshToken,
token_type: "Bearer",
expires_in: 300,
scope: scope,
});
beforeEach(() => {
fetchMock.get(`${config.metadata.issuer}.well-known/openid-configuration`, config.metadata);
fetchMock.get(`${config.metadata.issuer}jwks`, {
status: 200,
headers: {
"Content-Type": "application/json",
},
keys: [],
});
fetchMock.post(config.metadata.token_endpoint, {
status: 200,
headers: {
"Content-Type": "application/json",
},
...makeTokenResponse("new-access-token", "new-refresh-token"),
});
});
afterEach(() => {
jest.restoreAllMocks();
fetchMock.resetBehavior();
});
it("throws when oidc client cannot be initialised", async () => {
jest.spyOn(logger, "error");
fetchMock.get(
`${config.metadata.issuer}.well-known/openid-configuration`,
{
ok: false,
status: 404,
},
{ overwriteRoutes: true },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
await expect(refresher.oidcClientReady).rejects.toThrow();
expect(logger.error).toHaveBeenCalledWith(
"Failed to initialise OIDC client.",
// error from OidcClient
expect.any(Error),
);
});
it("initialises oidc client", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// @ts-ignore peek at private property to see we initialised the client correctly
expect(refresher.oidcClient.settings).toEqual(
expect.objectContaining({
client_id: clientId,
redirect_uri: redirectUri,
authority: authConfig.issuer,
scope,
}),
);
});
describe("doRefreshAccessToken()", () => {
it("should throw when oidcClient has not been initialised", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
await expect(refresher.doRefreshAccessToken("token")).rejects.toThrow(
"Cannot get new token before OIDC client is initialised.",
);
});
it("should refresh the tokens", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
const result = await refresher.doRefreshAccessToken("refresh-token");
expect(fetchMock).toHaveFetched(config.metadata.token_endpoint, {
method: "POST",
});
expect(result).toEqual({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
});
});
it("should persist the new tokens", async () => {
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// spy on our stub
jest.spyOn(refresher, "persistTokens");
await refresher.doRefreshAccessToken("refresh-token");
expect(refresher.persistTokens).toHaveBeenCalledWith({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
});
});
it("should only have one inflight refresh request at once", async () => {
fetchMock
.postOnce(
config.metadata.token_endpoint,
{
status: 200,
headers: {
"Content-Type": "application/json",
},
...makeTokenResponse("first-new-access-token", "first-new-refresh-token"),
},
{ overwriteRoutes: true },
)
.postOnce(
config.metadata.token_endpoint,
{
status: 200,
headers: {
"Content-Type": "application/json",
},
...makeTokenResponse("second-new-access-token", "second-new-refresh-token"),
},
{ overwriteRoutes: false },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// reset call counts
fetchMock.resetHistory();
const refreshToken = "refresh-token";
const first = refresher.doRefreshAccessToken(refreshToken);
const second = refresher.doRefreshAccessToken(refreshToken);
const result1 = await second;
const result2 = await first;
// only one call to token endpoint
expect(fetchMock).toHaveFetchedTimes(1, config.metadata.token_endpoint);
expect(result1).toEqual({
accessToken: "first-new-access-token",
refreshToken: "first-new-refresh-token",
});
// same response
expect(result1).toEqual(result2);
// call again after first request resolves
const third = await refresher.doRefreshAccessToken("first-new-refresh-token");
// called token endpoint, got new tokens
expect(third).toEqual({
accessToken: "second-new-access-token",
refreshToken: "second-new-refresh-token",
});
});
it("should log and rethrow when token refresh fails", async () => {
fetchMock.post(
config.metadata.token_endpoint,
{
status: 503,
headers: {
"Content-Type": "application/json",
},
},
{ overwriteRoutes: true },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow();
});
it("should make fresh request after a failed request", async () => {
// make sure inflight request is cleared after a failure
fetchMock
.postOnce(
config.metadata.token_endpoint,
{
status: 503,
headers: {
"Content-Type": "application/json",
},
},
{ overwriteRoutes: true },
)
.postOnce(
config.metadata.token_endpoint,
{
status: 200,
headers: {
"Content-Type": "application/json",
},
...makeTokenResponse("second-new-access-token", "second-new-refresh-token"),
},
{ overwriteRoutes: false },
);
const refresher = new OidcTokenRefresher(authConfig, clientId, redirectUri, deviceId, idTokenClaims);
await refresher.oidcClientReady;
// reset call counts
fetchMock.resetHistory();
// first call fails
await expect(refresher.doRefreshAccessToken("refresh-token")).rejects.toThrow();
// call again after first request resolves
const result = await refresher.doRefreshAccessToken("first-new-refresh-token");
// called token endpoint, got new tokens
expect(result).toEqual({
accessToken: "second-new-access-token",
refreshToken: "second-new-refresh-token",
});
});
});
});
+66
View File
@@ -0,0 +1,66 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { decodeBase64 } from "../../src/base64";
import {
randomLowercaseString,
randomString,
randomUppercaseString,
secureRandomBase64Url,
} from "../../src/randomstring";
describe("Random strings", () => {
it.each([8, 16, 32])("secureRandomBase64 generates %i valid base64 bytes", (n: number) => {
const randb641 = secureRandomBase64Url(n);
const randb642 = secureRandomBase64Url(n);
expect(randb641).not.toEqual(randb642);
const decoded = decodeBase64(randb641);
expect(decoded).toHaveLength(n);
});
it.each([8, 16, 32])("randomString generates string of %i characters", (n: number) => {
const rand1 = randomString(n);
const rand2 = randomString(n);
expect(rand1).not.toEqual(rand2);
expect(rand1).toHaveLength(n);
});
it.each([8, 16, 32])("randomLowercaseString generates lowercase string of %i characters", (n: number) => {
const rand1 = randomLowercaseString(n);
const rand2 = randomLowercaseString(n);
expect(rand1).not.toEqual(rand2);
expect(rand1).toHaveLength(n);
expect(rand1.toLowerCase()).toEqual(rand1);
});
it.each([8, 16, 32])("randomUppercaseString generates lowercase string of %i characters", (n: number) => {
const rand1 = randomUppercaseString(n);
const rand2 = randomUppercaseString(n);
expect(rand1).not.toEqual(rand2);
expect(rand1).toHaveLength(n);
expect(rand1.toUpperCase()).toEqual(rand1);
});
});
+198 -1
View File
@@ -18,7 +18,7 @@ import MockHttpBackend from "matrix-mock-request";
import { MAIN_ROOM_TIMELINE, ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts";
import { MatrixClient } from "../../src/client";
import { EventType, MatrixEvent, Room } from "../../src/matrix";
import { EventType, MatrixEvent, RelationType, Room, threadIdForReceipt } from "../../src/matrix";
import { synthesizeReceipt } from "../../src/models/read-receipt";
import { encodeUri } from "../../src/utils";
import * as utils from "../test-utils/test-utils";
@@ -225,6 +225,7 @@ describe("Read receipt", () => {
it("should not allow an older unthreaded receipt to clobber a `main` threaded one", () => {
const userId = client.getSafeUserId();
const room = new Room(ROOM_ID, client, userId);
room.findEventById = jest.fn().mockReturnValue({} as MatrixEvent);
const unthreadedReceipt: WrappedReceipt = {
eventId: "$olderEvent",
@@ -258,4 +259,200 @@ describe("Read receipt", () => {
expect(room.getEventReadUpTo(userId)).toBe(mainTimelineReceipt.eventId);
});
});
describe("Determining the right thread ID for a receipt", () => {
it("provides the thread root ID for a normal threaded message", () => {
const event = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: "!roomx",
content: {
"body": "Hello from a thread",
"m.relates_to": {
"event_id": "$thread1",
"m.in_reply_to": {
event_id: "$thread1",
},
"rel_type": "m.thread",
},
},
});
expect(threadIdForReceipt(event)).toEqual("$thread1");
});
it("provides 'main' for a non-thread message", () => {
const event = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: "!roomx",
content: { body: "Hello" },
});
expect(threadIdForReceipt(event)).toEqual("main");
});
it("provides 'main' for a thread root", () => {
const event = utils.mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@bob:matrix.org",
room: "!roomx",
content: { body: "Hello" },
});
// Set thread ID to this event's ID, meaning this is the thread root
event.setThreadId(event.getId());
expect(threadIdForReceipt(event)).toEqual("main");
});
it("provides 'main' for a reaction to a thread root", () => {
const event = utils.mkEvent({
event: true,
type: EventType.Reaction,
user: "@bob:matrix.org",
room: "!roomx",
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: "$thread1",
key: Math.random().toString(),
},
},
});
// Set thread Id, meaning this looks like it's in the thread (this
// happens for relations like this, so that they appear in the
// thread's timeline).
event.setThreadId("$thread1");
// But because it's a reaction to the thread root, it's in main
expect(threadIdForReceipt(event)).toEqual("main");
});
it("provides the thread ID for a reaction to a threaded message", () => {
const event = utils.mkEvent({
event: true,
type: EventType.Reaction,
user: "@bob:matrix.org",
room: "!roomx",
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: "$withinthread2",
key: Math.random().toString(),
},
},
});
// Set thread Id, to say this message is in the thread. This happens
// when the message arrived and is classified.
event.setThreadId("$thread1");
// It's in the thread because it refers to something else, not the
// thread root
expect(threadIdForReceipt(event)).toEqual("$thread1");
});
it("(suprisingly?) provides 'main' for a redaction of a threaded message", () => {
const event = utils.mkEvent({
event: true,
type: EventType.RoomRedaction,
content: {
reason: "Spamming",
},
redacts: "$withinthread2",
room: "!roomx",
user: "@bob:matrix.org",
});
// Set thread Id, to say this message is in the thread.
event.setThreadId("$thread1");
// Because redacting a message removes all its m.relations, the
// message is no longer in the thread, so we must send a receipt for
// it in the main timeline.
//
// This is surprising, but it follows the spec (at least up to
// current latest room version, 11). In fact, the event should no
// longer have a thread ID set on it, so this testcase should not
// come up. (At time of writing, this is not the case though - it
// does still have threadId set.)
expect(threadIdForReceipt(event)).toEqual("main");
});
it("provides the thread ID for an edit of a threaded message", () => {
const event = utils.mkEvent({
event: true,
type: EventType.RoomRedaction,
content: {
"body": "Edited!",
"m.new_content": {
body: "Edited!",
},
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: "$withinthread2",
},
},
room: "!roomx",
user: "@bob:matrix.org",
});
// Set thread Id, to say this message is in the thread.
event.setThreadId("$thread1");
// It's in the thread, because it redacts something inside the
// thread (not the thread root)
expect(threadIdForReceipt(event)).toEqual("$thread1");
});
it("provides 'main' for an edit of a thread root", () => {
const event = utils.mkEvent({
event: true,
type: EventType.RoomRedaction,
content: {
"body": "Edited!",
"m.new_content": {
body: "Edited!",
},
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: "$thread1",
},
},
room: "!roomx",
user: "@bob:matrix.org",
});
// Set thread Id, to say this message is in the thread.
event.setThreadId("$thread1");
// It's in the thread, because it redacts something inside the
// thread (not the thread root)
expect(threadIdForReceipt(event)).toEqual("main");
});
it("provides 'main' for a redaction of the thread root", () => {
const event = utils.mkEvent({
event: true,
type: EventType.RoomRedaction,
content: {
reason: "Spamming",
},
redacts: "$thread1",
room: "!roomx",
user: "@bob:matrix.org",
});
// Set thread Id, to say this message is in the thread.
event.setThreadId("$thread1");
// It's in the thread, because it redacts something inside the
// thread (not the thread root)
expect(threadIdForReceipt(event)).toEqual("main");
});
});
});
+1 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
import "../../olm-loader";
import { RendezvousFailureReason, RendezvousIntent } from "../../../src/rendezvous";
import { MSC3903ECDHPayload, MSC3903ECDHv2RendezvousChannel } from "../../../src/rendezvous/channels";
import { decodeBase64 } from "../../../src/crypto/olmlib";
import { decodeBase64 } from "../../../src/base64";
import { DummyTransport } from "./DummyTransport";
function makeTransport(name: string) {
+17 -17
View File
@@ -29,7 +29,7 @@ import {
MSC3886SimpleHttpRendezvousTransportDetails,
} from "../../../src/rendezvous/transports";
import { DummyTransport } from "./DummyTransport";
import { decodeBase64 } from "../../../src/crypto/olmlib";
import { decodeBase64 } from "../../../src/base64";
import { logger } from "../../../src/logger";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
@@ -37,7 +37,7 @@ function makeMockClient(opts: {
userId: string;
deviceId: string;
deviceKey?: string;
msc3882Enabled: boolean;
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
msc3886Enabled: boolean;
devices?: Record<string, Partial<DeviceInfo>>;
@@ -54,7 +54,7 @@ function makeMockClient(opts: {
getVersions() {
return {
unstable_features: {
"org.matrix.msc3882": opts.msc3882Enabled,
"org.matrix.msc3882": opts.getLoginTokenEnabled,
"org.matrix.msc3886": opts.msc3886Enabled,
},
};
@@ -64,8 +64,8 @@ function makeMockClient(opts: {
? {}
: {
capabilities: {
"org.matrix.msc3882.get_login_token": {
enabled: opts.msc3882Enabled,
"m.get_login_token": {
enabled: opts.getLoginTokenEnabled,
},
},
};
@@ -122,7 +122,7 @@ describe("Rendezvous", function () {
userId: "@alice:example.com",
deviceId: "DEVICEID",
msc3886Enabled: false,
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: true,
});
httpBackend.when("POST", "https://fallbackserver/rz").response = {
@@ -180,10 +180,10 @@ describe("Rendezvous", function () {
});
async function testNoProtocols({
msc3882Enabled,
getLoginTokenEnabled,
msc3882r0Only,
}: {
msc3882Enabled: boolean;
getLoginTokenEnabled: boolean;
msc3882r0Only: boolean;
}) {
const aliceTransport = makeTransport("Alice");
@@ -198,7 +198,7 @@ describe("Rendezvous", function () {
userId: "alice",
deviceId: "ALICE",
msc3886Enabled: false,
msc3882Enabled,
getLoginTokenEnabled,
msc3882r0Only,
});
const aliceEcdh = new MSC3903ECDHRendezvousChannel(aliceTransport, undefined, aliceOnFailure);
@@ -241,11 +241,11 @@ describe("Rendezvous", function () {
}
it("no protocols - r0", async function () {
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: true });
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: true });
});
it("no protocols - r1", async function () {
await testNoProtocols({ msc3882Enabled: false, msc3882r0Only: false });
it("no protocols - stable", async function () {
await testNoProtocols({ getLoginTokenEnabled: false, msc3882r0Only: false });
});
it("new device declines protocol with outcome unsupported", async function () {
@@ -260,7 +260,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
@@ -319,7 +319,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
@@ -378,7 +378,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
@@ -439,7 +439,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
});
@@ -508,7 +508,7 @@ describe("Rendezvous", function () {
const alice = makeMockClient({
userId: "alice",
deviceId: "ALICE",
msc3882Enabled: true,
getLoginTokenEnabled: true,
msc3882r0Only: false,
msc3886Enabled: false,
devices,
+7 -2
View File
@@ -27,6 +27,7 @@ import { M_BEACON } from "../../src/@types/beacon";
import { MatrixClient } from "../../src/client";
import { DecryptionError } from "../../src/crypto/algorithms";
import { defer } from "../../src/utils";
import { Room } from "../../src/models/room";
describe("RoomState", function () {
const roomId = "!foo:bar";
@@ -362,9 +363,11 @@ describe("RoomState", function () {
});
it("does not add redacted beacon info events to state", () => {
const mockClient = {} as unknown as MockedObject<MatrixClient>;
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = new MatrixEvent({ type: "m.room.redaction" });
redactedBeaconEvent.makeRedacted(redactionEvent);
const room = new Room(roomId, mockClient, userA);
redactedBeaconEvent.makeRedacted(redactionEvent, room);
const emitSpy = jest.spyOn(state, "emit");
state.setStateEvents([redactedBeaconEvent]);
@@ -394,11 +397,13 @@ describe("RoomState", function () {
});
it("destroys and removes redacted beacon events", () => {
const mockClient = {} as unknown as MockedObject<MatrixClient>;
const beaconId = "$beacon1";
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactionEvent = new MatrixEvent({ type: "m.room.redaction", redacts: beaconEvent.getId() });
redactedBeaconEvent.makeRedacted(redactionEvent);
const room = new Room(roomId, mockClient, userA);
redactedBeaconEvent.makeRedacted(redactionEvent, room);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
+172 -83
View File
@@ -1746,6 +1746,7 @@ describe("Room", function () {
it("should acknowledge if an event has been read", function () {
const ts = 13787898424;
room.addReceipt(mkReceipt(roomId, [mkRecord(eventToAck.getId()!, "m.read", userB, ts)]));
room.findEventById = jest.fn().mockReturnValue({} as MatrixEvent);
expect(room.hasUserReadEvent(userB, eventToAck.getId()!)).toEqual(true);
});
it("return false for an unknown event", function () {
@@ -3147,106 +3148,194 @@ describe("Room", function () {
const client = new TestClient(userA).client;
const room = new Room(roomId, client, userA);
it("handles missing receipt type", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
return receiptType === ReceiptType.ReadPrivate ? ({ eventId: "eventId" } as WrappedReceipt) : null;
};
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
});
describe("prefers newer receipt", () => {
it("should compare correctly using timelines", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as WrappedReceipt;
}
return null;
};
for (let i = 1; i <= 2; i++) {
room.getUnfilteredTimelineSet = () =>
({
compareEventOrdering: (event1, event2) => {
return event1 === `eventId${i}` ? 1 : -1;
},
} as EventTimelineSet);
expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`);
}
describe("invalid receipts", () => {
beforeEach(() => {
// Clear the spies on logger.warn
jest.clearAllMocks();
});
describe("correctly compares by timestamp", () => {
it("should correctly compare, if we have all receipts", () => {
it("ignores receipts pointing at missing events", () => {
// Given a receipt exists
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
return { eventId: "missingEventId" } as WrappedReceipt;
};
// But the event ID it contains does not refer to an event we have
room.findEventById = jest.fn().mockReturnValue(null);
// When we ask what they have read
// Then we say "nothing"
expect(room.getEventReadUpTo(userA)).toBeNull();
});
it("ignores receipts pointing at the wrong thread", () => {
// Given a threaded receipt exists
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
return { eventId: "wrongThreadEventId", data: { ts: 0, thread_id: "thread1" } } as WrappedReceipt;
};
// But the event it refers to is in a thread
room.findEventById = jest.fn().mockReturnValue({ threadRootId: "thread2" } as MatrixEvent);
// When we ask what they have read
// Then we say "nothing"
expect(room.getEventReadUpTo(userA)).toBeNull();
expect(logger.warn).toHaveBeenCalledWith(
"Ignoring receipt because its thread_id (thread1) disagrees with the thread root (thread2) " +
"of the referenced event (event ID = wrongThreadEventId)",
);
});
it("accepts unthreaded receipts pointing at an event in a thread", () => {
// Given an unthreaded receipt exists
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
return { eventId: "inThreadEventId" } as WrappedReceipt;
};
// And the event it refers to is in a thread
room.findEventById = jest.fn().mockReturnValue({ threadRootId: "thread2" } as MatrixEvent);
// When we ask what they have read
// Then we say the event
expect(room.getEventReadUpTo(userA)).toEqual("inThreadEventId");
});
it("accepts main thread receipts pointing at an event in main timeline", () => {
// Given a threaded receipt exists, in main thread
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
return { eventId: "mainThreadEventId", data: { ts: 12, thread_id: "main" } } as WrappedReceipt;
};
// And the event it refers to is in a thread
room.findEventById = jest.fn().mockReturnValue({ threadRootId: undefined } as MatrixEvent);
// When we ask what they have read
// Then we say the event
expect(room.getEventReadUpTo(userA)).toEqual("mainThreadEventId");
});
it("accepts main thread receipts pointing at a thread root", () => {
// Given a threaded receipt exists, in main thread
room.getReadReceiptForUserId = (): WrappedReceipt | null => {
return { eventId: "rootId", data: { ts: 12, thread_id: "main" } } as WrappedReceipt;
};
// And the event it refers to is in a thread, because it is a thread root
room.findEventById = jest
.fn()
.mockReturnValue({ isThreadRoot: true, threadRootId: "thread1" } as MatrixEvent);
// When we ask what they have read
// Then we say the event
expect(room.getEventReadUpTo(userA)).toEqual("rootId");
});
});
describe("valid receipts", () => {
beforeEach(() => {
// When we look up the event referred to by the receipt, it exists
room.findEventById = jest.fn().mockReturnValue({} as MatrixEvent);
});
it("handles missing receipt type", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
return receiptType === ReceiptType.ReadPrivate ? ({ eventId: "eventId" } as WrappedReceipt) : null;
};
expect(room.getEventReadUpTo(userA)).toEqual("eventId");
});
describe("prefers newer receipt", () => {
it("should compare correctly using timelines", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1" } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2" } as WrappedReceipt;
}
return null;
};
for (let i = 1; i <= 2; i++) {
room.getUnfilteredTimelineSet = () =>
({
compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as WrappedReceipt;
}
return null;
};
compareEventOrdering: (event1: string, _event2: string) => {
return event1 === `eventId${i}` ? 1 : -1;
},
findEventById: jest.fn().mockReturnValue({} as MatrixEvent),
} as unknown as EventTimelineSet);
expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`);
}
});
it("should correctly compare, if private read receipt is missing", () => {
room.getUnfilteredTimelineSet = () =>
({
compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: 1 } } as WrappedReceipt;
describe("correctly compares by timestamp", () => {
it("should correctly compare, if we have all receipts", () => {
for (let i = 1; i <= 2; i++) {
room.getUnfilteredTimelineSet = () =>
({
compareEventOrdering: () => null,
findEventById: jest.fn().mockReturnValue({} as MatrixEvent),
} as unknown as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1", data: { ts: i === 1 ? 2 : 1 } } as WrappedReceipt;
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: i === 2 ? 2 : 1 } } as WrappedReceipt;
}
return null;
};
expect(room.getEventReadUpTo(userA)).toEqual(`eventId${i}`);
}
return null;
};
});
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
});
});
it("should correctly compare, if private read receipt is missing", () => {
room.getUnfilteredTimelineSet = () =>
({
compareEventOrdering: () => null,
findEventById: jest.fn().mockReturnValue({} as MatrixEvent),
} as unknown as EventTimelineSet);
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: 1 } } as WrappedReceipt;
}
return null;
};
describe("fallback precedence", () => {
beforeAll(() => {
room.getUnfilteredTimelineSet = () =>
({
compareEventOrdering: (_1, _2) => null,
} as EventTimelineSet);
expect(room.getEventReadUpTo(userA)).toEqual(`eventId2`);
});
});
it("should give precedence to m.read.private", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1", data: { ts: 123 } };
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: 123 } };
}
return null;
};
describe("fallback precedence", () => {
beforeAll(() => {
room.getUnfilteredTimelineSet = () =>
({
compareEventOrdering: () => null,
findEventById: jest.fn().mockReturnValue({} as MatrixEvent),
} as unknown as EventTimelineSet);
});
expect(room.getEventReadUpTo(userA)).toEqual(`eventId1`);
});
it("should give precedence to m.read.private", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.ReadPrivate) {
return { eventId: "eventId1", data: { ts: 123 } };
}
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId2", data: { ts: 123 } };
}
return null;
};
it("should give precedence to m.read", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as WrappedReceipt;
}
return null;
};
expect(room.getEventReadUpTo(userA)).toEqual(`eventId1`);
});
expect(room.getEventReadUpTo(userA)).toEqual(`eventId3`);
it("should give precedence to m.read", () => {
room.getReadReceiptForUserId = (userId, ignore, receiptType): WrappedReceipt | null => {
if (receiptType === ReceiptType.Read) {
return { eventId: "eventId3" } as WrappedReceipt;
}
return null;
};
expect(room.getEventReadUpTo(userA)).toEqual(`eventId3`);
});
});
});
});
@@ -3564,7 +3653,7 @@ describe("Room", function () {
expect(room.polls.get(pollStartEvent.getId()!)).toBeTruthy();
const redactedEvent = new MatrixEvent({ type: "m.room.redaction" });
pollStartEvent.makeRedacted(redactedEvent);
pollStartEvent.makeRedacted(redactedEvent, room);
await flushPromises();
@@ -41,6 +41,7 @@ describe("CrossSigningIdentity", () => {
olmMachine = {
crossSigningStatus: jest.fn(),
bootstrapCrossSigning: jest.fn(),
exportCrossSigningKeys: jest.fn(),
close: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
@@ -50,6 +51,8 @@ describe("CrossSigningIdentity", () => {
secretStorage = {
get: jest.fn(),
hasKey: jest.fn(),
store: jest.fn(),
} as unknown as Mocked<ServerSideSecretStorage>;
crossSigning = new CrossSigningIdentity(olmMachine, outgoingRequestProcessor, secretStorage);
@@ -61,7 +64,8 @@ describe("CrossSigningIdentity", () => {
hasSelfSigning: true,
hasUserSigning: true,
});
// TODO: secret storage
// in secret storage
secretStorage.get.mockResolvedValue("base64-saved-in-storage");
await crossSigning.bootstrapCrossSigning({});
expect(olmMachine.bootstrapCrossSigning).not.toHaveBeenCalled();
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
@@ -73,6 +77,19 @@ describe("CrossSigningIdentity", () => {
expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true);
});
it("Shoud update 4S on reset if 4S is set up", async () => {
olmMachine.bootstrapCrossSigning.mockResolvedValue([]);
secretStorage.hasKey.mockResolvedValue(true);
olmMachine.exportCrossSigningKeys.mockResolvedValue({
masterKey: "base64_aaaaaaaaaa",
self_signing_key: "base64_bbbbbbbbbbb",
userSigningKey: "base64_cccccccc",
});
await crossSigning.bootstrapCrossSigning({ setupNewCrossSigning: true });
expect(olmMachine.bootstrapCrossSigning).toHaveBeenCalledWith(true);
expect(secretStorage.store).toHaveBeenCalledTimes(3);
});
it("should call bootstrapCrossSigning if we need new keys", async () => {
olmMachine.crossSigningStatus.mockResolvedValue({
hasMaster: false,
@@ -23,6 +23,7 @@ import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingReque
import { KeyClaimManager } from "../../../src/rust-crypto/KeyClaimManager";
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
import { logger, LogSpan } from "../../../src/logger";
afterEach(() => {
fetchMock.mockReset();
@@ -93,7 +94,7 @@ describe("KeyClaimManager", () => {
olmMachine.markRequestAsSent.mockResolvedValueOnce(undefined);
// fire off the request
await keyClaimManager.ensureSessionsForUsers([u1, u2]);
await keyClaimManager.ensureSessionsForUsers(new LogSpan(logger, "test"), [u1, u2]);
// check that all the calls were made
expect(olmMachine.getMissingSessions).toHaveBeenCalledWith([u1, u2]);
@@ -119,12 +120,13 @@ describe("KeyClaimManager", () => {
let markRequestAsSentPromise = awaitCallToMarkRequestAsSent();
// fire off two requests, and keep track of whether their promises resolve
const span = new LogSpan(logger, "test");
let req1Resolved = false;
keyClaimManager.ensureSessionsForUsers([u1]).then(() => {
keyClaimManager.ensureSessionsForUsers(span, [u1]).then(() => {
req1Resolved = true;
});
let req2Resolved = false;
const req2 = keyClaimManager.ensureSessionsForUsers([u2]).then(() => {
const req2 = keyClaimManager.ensureSessionsForUsers(span, [u2]).then(() => {
req2Resolved = true;
});
@@ -24,7 +24,7 @@ import {
KeysUploadRequest,
RoomMessageRequest,
SignatureUploadRequest,
SigningKeysUploadRequest,
UploadSigningKeysRequest,
ToDeviceRequest,
} from "@matrix-org/matrix-sdk-crypto-wasm";
@@ -82,12 +82,6 @@ describe("OutgoingRequestProcessor", () => {
"https://example.com/_matrix/client/v3/keys/signatures/upload",
],
["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
[
"SigningKeysUploadRequest",
SigningKeysUploadRequest,
"POST",
"https://example.com/_matrix/client/v3/keys/device_signing/upload",
],
];
test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
@@ -121,7 +115,7 @@ describe("OutgoingRequestProcessor", () => {
it("should handle ToDeviceRequests", async () => {
// first, mock up the ToDeviceRequest as we might expect to receive it from the Rust layer ...
const testBody = '{ "foo": "bar" }';
const testBody = '{ "messages": { "user": {"device": "bar" }}}';
const outgoingRequest = new ToDeviceRequest("1234", "test/type", "test/txnid", testBody);
// ... then poke it into the OutgoingRequestProcessor under test.
@@ -179,10 +173,37 @@ describe("OutgoingRequestProcessor", () => {
httpBackend.verifyNoOutstandingRequests();
});
it("should handle SigningKeysUploadRequests with UIA", async () => {
it("should handle UploadSigningKeysRequest without UIA", async () => {
// first, mock up a request as we might expect to receive it from the Rust layer ...
const testReq = { foo: "bar" };
const outgoingRequest = new SigningKeysUploadRequest("1234", JSON.stringify(testReq));
const outgoingRequest = new UploadSigningKeysRequest(JSON.stringify(testReq));
// ... then poke the request into the OutgoingRequestProcessor under test
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
// Now: check that it makes a matching HTTP request.
const testResponse = '{"result":1}';
httpBackend
.when("POST", "/_matrix")
.check((req) => {
expect(req.path).toEqual("https://example.com/_matrix/client/v3/keys/device_signing/upload");
expect(JSON.parse(req.rawData)).toEqual(testReq);
expect(req.headers["Accept"]).toEqual("application/json");
expect(req.headers["Content-Type"]).toEqual("application/json");
})
.respond(200, testResponse, true);
// SigningKeysUploadRequest does not need to be marked as sent, so no call to OlmMachine.markAsSent is expected.
await httpBackend.flushAllExpected();
await reqProm;
httpBackend.verifyNoOutstandingRequests();
});
it("should handle UploadSigningKeysRequest with UIA", async () => {
// first, mock up a request as we might expect to receive it from the Rust layer ...
const testReq = { foo: "bar" };
const outgoingRequest = new UploadSigningKeysRequest(JSON.stringify(testReq));
// also create a UIA callback
const authCallback: UIAuthCallback<Object> = async (makeRequest) => {
@@ -192,7 +213,7 @@ describe("OutgoingRequestProcessor", () => {
// ... then poke the request into the OutgoingRequestProcessor under test
const reqProm = processor.makeOutgoingRequest(outgoingRequest, authCallback);
// Now: check that it makes a matching HTTP request ...
// Now: check that it makes a matching HTTP request.
const testResponse = '{"result":1}';
httpBackend
.when("POST", "/_matrix")
@@ -204,12 +225,10 @@ describe("OutgoingRequestProcessor", () => {
})
.respond(200, testResponse, true);
// ... and that it calls OlmMachine.markAsSent.
const markSentCallPromise = awaitCallToMarkAsSent();
await httpBackend.flushAllExpected();
// SigningKeysUploadRequest does not need to be marked as sent, so no call to OlmMachine.markAsSent is expected.
await Promise.all([reqProm, markSentCallPromise]);
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
await httpBackend.flushAllExpected();
await reqProm;
httpBackend.verifyNoOutstandingRequests();
});
@@ -0,0 +1,237 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Mocked } from "jest-mock";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { OutgoingRequest, OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
import { defer, IDeferred } from "../../../src/utils";
import { logger } from "../../../src/logger";
describe("OutgoingRequestsManager", () => {
/** the OutgoingRequestsManager implementation under test */
let manager: OutgoingRequestsManager;
/** a mock OutgoingRequestProcessor */
let processor: Mocked<OutgoingRequestProcessor>;
/** a mocked-up OlmMachine which manager is connected to */
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
beforeEach(async () => {
olmMachine = {
outgoingRequests: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
processor = {
makeOutgoingRequest: jest.fn(),
} as unknown as Mocked<OutgoingRequestProcessor>;
manager = new OutgoingRequestsManager(logger, olmMachine, processor);
});
describe("Call doProcessOutgoingRequests", () => {
it("The call triggers handling of the machine outgoing requests", async () => {
const request1 = new RustSdkCryptoJs.KeysQueryRequest("foo", "{}");
const request2 = new RustSdkCryptoJs.KeysUploadRequest("foo2", "{}");
olmMachine.outgoingRequests.mockImplementationOnce(async () => {
return [request1, request2];
});
processor.makeOutgoingRequest.mockImplementationOnce(async () => {
return;
});
await manager.doProcessOutgoingRequests();
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(2);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
});
it("Stack and batch calls to doProcessOutgoingRequests while one is already running", async () => {
const request1 = new RustSdkCryptoJs.KeysQueryRequest("foo", "{}");
const request2 = new RustSdkCryptoJs.KeysUploadRequest("foo2", "{}");
const request3 = new RustSdkCryptoJs.KeysBackupRequest("foo3", "{}", "1");
const firstOutgoingRequestDefer = defer<OutgoingRequest[]>();
olmMachine.outgoingRequests
.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
return firstOutgoingRequestDefer.promise;
})
.mockImplementationOnce(async () => {
return [request3];
});
const firstRequest = manager.doProcessOutgoingRequests();
// stack 2 additional requests while the first one is still running
const secondRequest = manager.doProcessOutgoingRequests();
const thirdRequest = manager.doProcessOutgoingRequests();
// let the first request complete
firstOutgoingRequestDefer.resolve([request1, request2]);
await firstRequest;
await secondRequest;
await thirdRequest;
// outgoingRequests should be called twice in total, as the second and third requests are
// processed in the same loop.
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(2);
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(3);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request3);
});
it("Process 3 consecutive calls to doProcessOutgoingRequests while not blocking previous ones", async () => {
const request1 = new RustSdkCryptoJs.KeysQueryRequest("foo", "{}");
const request2 = new RustSdkCryptoJs.KeysUploadRequest("foo2", "{}");
const request3 = new RustSdkCryptoJs.KeysBackupRequest("foo3", "{}", "1");
// promises which will resolve when OlmMachine.outgoingRequests is called
const outgoingRequestCalledPromises: Promise<void>[] = [];
// deferreds which will provide the results of OlmMachine.outgoingRequests
const outgoingRequestResultDeferreds: IDeferred<OutgoingRequest[]>[] = [];
for (let i = 0; i < 3; i++) {
const resultDeferred = defer<OutgoingRequest[]>();
const calledPromise = new Promise<void>((resolve) => {
olmMachine.outgoingRequests.mockImplementationOnce(() => {
resolve();
return resultDeferred.promise;
});
});
outgoingRequestCalledPromises.push(calledPromise);
outgoingRequestResultDeferreds.push(resultDeferred);
}
const call1 = manager.doProcessOutgoingRequests();
// First call will start an iteration and for now is awaiting on outgoingRequests
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
// Make a new call now: this will request a new iteration
const call2 = manager.doProcessOutgoingRequests();
// let the first iteration complete
outgoingRequestResultDeferreds[0].resolve([request1]);
// The first call should now complete
await call1;
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(1);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request1);
// Wait for the second iteration to fire and be waiting on `outgoingRequests`
await outgoingRequestCalledPromises[1];
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(2);
// Stack a new call that should be processed in an additional iteration.
const call3 = manager.doProcessOutgoingRequests();
outgoingRequestResultDeferreds[1].resolve([request2]);
await call2;
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(2);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request2);
// Wait for the third iteration to fire and be waiting on `outgoingRequests`
await outgoingRequestCalledPromises[2];
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(3);
outgoingRequestResultDeferreds[2].resolve([request3]);
await call3;
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(3);
expect(processor.makeOutgoingRequest).toHaveBeenCalledWith(request3);
// ensure that no other iteration is going on
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(3);
});
it("Should not bubble exceptions if server request is rejected", async () => {
const request = new RustSdkCryptoJs.KeysQueryRequest("foo", "{}");
olmMachine.outgoingRequests.mockImplementationOnce(async () => {
return [request];
});
processor.makeOutgoingRequest.mockImplementationOnce(async () => {
throw new Error("Some network error");
});
await manager.doProcessOutgoingRequests();
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
});
});
describe("Calling stop on the manager should stop ongoing work", () => {
it("When the manager is stopped after outgoingRequests() call, do not make sever requests", async () => {
const request1 = new RustSdkCryptoJs.KeysQueryRequest("foo", "{}");
const firstOutgoingRequestDefer = defer<OutgoingRequest[]>();
olmMachine.outgoingRequests.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
return firstOutgoingRequestDefer.promise;
});
const firstRequest = manager.doProcessOutgoingRequests();
// stop
manager.stop();
// let the first request complete
firstOutgoingRequestDefer.resolve([request1]);
await firstRequest;
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(0);
});
it("When the manager is stopped while doing server calls, it should stop before the next sever call", async () => {
const request1 = new RustSdkCryptoJs.KeysQueryRequest("11", "{}");
const request2 = new RustSdkCryptoJs.KeysUploadRequest("12", "{}");
const firstRequestDefer = defer<void>();
olmMachine.outgoingRequests.mockImplementationOnce(async (): Promise<OutgoingRequest[]> => {
return [request1, request2];
});
processor.makeOutgoingRequest
.mockImplementationOnce(async () => {
manager.stop();
return firstRequestDefer.promise;
})
.mockImplementationOnce(async () => {
return;
});
const firstRequest = manager.doProcessOutgoingRequests();
firstRequestDefer.resolve();
await firstRequest;
// should have been called once but not twice
expect(processor.makeOutgoingRequest).toHaveBeenCalledTimes(1);
});
});
});
@@ -0,0 +1,31 @@
/*
*
* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* /
*/
import { HistoryVisibility as RustHistoryVisibility } from "@matrix-org/matrix-sdk-crypto-wasm";
import { HistoryVisibility } from "../../../src";
import { toRustHistoryVisibility } from "../../../src/rust-crypto/RoomEncryptor";
it.each([
[HistoryVisibility.Invited, RustHistoryVisibility.Invited],
[HistoryVisibility.Joined, RustHistoryVisibility.Joined],
[HistoryVisibility.Shared, RustHistoryVisibility.Shared],
[HistoryVisibility.WorldReadable, RustHistoryVisibility.WorldReadable],
])("JS HistoryVisibility to Rust HistoryVisibility: converts %s to %s", (historyVisibility, expected) => {
expect(toRustHistoryVisibility(historyVisibility)).toBe(expected);
});
+187 -54
View File
@@ -44,19 +44,103 @@ import {
EventShieldColour,
EventShieldReason,
ImportRoomKeysOpts,
KeyBackupCheck,
VerificationRequest,
} from "../../../src/crypto-api";
import * as testData from "../../test-utils/test-data";
import { defer } from "../../../src/utils";
import { logger } from "../../../src/logger";
import { OutgoingRequestsManager } from "../../../src/rust-crypto/OutgoingRequestsManager";
const TEST_USER = "@alice:example.com";
const TEST_DEVICE_ID = "TEST_DEVICE";
afterEach(() => {
fetchMock.reset();
jest.restoreAllMocks();
});
describe("initRustCrypto", () => {
function makeTestOlmMachine(): Mocked<OlmMachine> {
return {
registerRoomKeyUpdatedCallback: jest.fn(),
registerUserIdentityUpdatedCallback: jest.fn(),
getSecretsFromInbox: jest.fn().mockResolvedValue(["dGhpc2lzYWZha2VzZWNyZXQ="]),
deleteSecretsFromInbox: jest.fn(),
registerReceiveSecretCallback: jest.fn(),
outgoingRequests: jest.fn(),
} as unknown as Mocked<OlmMachine>;
}
it("passes through the store params", async () => {
const testOlmMachine = makeTestOlmMachine();
jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine);
await initRustCrypto(
logger,
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
"storePrefix",
"storePassphrase",
);
expect(OlmMachine.initialize).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
"storePrefix",
"storePassphrase",
);
});
it("suppresses the storePassphrase if storePrefix is unset", async () => {
const testOlmMachine = makeTestOlmMachine();
jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine);
await initRustCrypto(
logger,
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
null,
"storePassphrase",
);
expect(OlmMachine.initialize).toHaveBeenCalledWith(expect.anything(), expect.anything(), undefined, undefined);
});
it("Should get secrets from inbox on start", async () => {
const testOlmMachine = makeTestOlmMachine() as OlmMachine;
jest.spyOn(OlmMachine, "initialize").mockResolvedValue(testOlmMachine);
await initRustCrypto(
logger,
{} as MatrixClient["http"],
TEST_USER,
TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
"storePrefix",
"storePassphrase",
);
expect(testOlmMachine.getSecretsFromInbox).toHaveBeenCalledWith("m.megolm_backup.v1");
});
});
describe("RustCrypto", () => {
it("getVersion() should return the current version of the rust sdk and vodozemac", async () => {
const rustCrypto = await makeTestRustCrypto();
const versions = RustSdkCryptoJs.getVersions();
expect(rustCrypto.getVersion()).toBe(
`Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`,
);
});
describe(".importRoomKeys and .exportRoomKeys", () => {
let rustCrypto: RustCrypto;
@@ -162,6 +246,7 @@ describe("RustCrypto", () => {
it("returns sensible values on a default client", async () => {
const secretStorage = {
isStored: jest.fn().mockResolvedValue(null),
getDefaultKeyId: jest.fn().mockResolvedValue("key"),
} as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
@@ -182,6 +267,7 @@ describe("RustCrypto", () => {
it("throws if `stop` is called mid-call", async () => {
const secretStorage = {
isStored: jest.fn().mockResolvedValue(null),
getDefaultKeyId: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, secretStorage);
@@ -208,7 +294,10 @@ describe("RustCrypto", () => {
});
it("isSecretStorageReady", async () => {
const rustCrypto = await makeTestRustCrypto();
const mockSecretStorage = {
getDefaultKeyId: jest.fn().mockResolvedValue(null),
} as unknown as Mocked<ServerSideSecretStorage>;
const rustCrypto = await makeTestRustCrypto(undefined, undefined, undefined, mockSecretStorage);
await expect(rustCrypto.isSecretStorageReady()).resolves.toBe(false);
});
@@ -259,7 +348,10 @@ describe("RustCrypto", () => {
makeOutgoingRequest: jest.fn(),
} as unknown as Mocked<OutgoingRequestProcessor>;
const outgoingRequestsManager = new OutgoingRequestsManager(logger, olmMachine, outgoingRequestProcessor);
rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixHttpApi<any>,
TEST_USER,
@@ -268,6 +360,7 @@ describe("RustCrypto", () => {
{} as CryptoCallbacks,
);
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
rustCrypto["outgoingRequestsManager"] = outgoingRequestsManager;
});
it("should poll for outgoing messages and send them", async () => {
@@ -306,50 +399,6 @@ describe("RustCrypto", () => {
await awaitCallToMakeOutgoingRequest();
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(2);
});
it("stops looping when stop() is called", async () => {
for (let i = 0; i < 5; i++) {
outgoingRequestQueue.push([new KeysQueryRequest("1234", "{}")]);
}
let makeRequestPromise = awaitCallToMakeOutgoingRequest();
rustCrypto.onSyncCompleted({});
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeTruthy();
// go a couple of times round the loop
let resolveMakeRequest = await makeRequestPromise;
makeRequestPromise = awaitCallToMakeOutgoingRequest();
resolveMakeRequest();
resolveMakeRequest = await makeRequestPromise;
makeRequestPromise = awaitCallToMakeOutgoingRequest();
resolveMakeRequest();
// a second sync while this is going on shouldn't make any difference
rustCrypto.onSyncCompleted({});
resolveMakeRequest = await makeRequestPromise;
outgoingRequestProcessor.makeOutgoingRequest.mockReset();
resolveMakeRequest();
// now stop...
rustCrypto.stop();
// which should (eventually) cause the loop to stop with no further calls to outgoingRequests
olmMachine.outgoingRequests.mockReset();
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy();
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
expect(olmMachine.outgoingRequests).not.toHaveBeenCalled();
// we sent three, so there should be 2 left
expect(outgoingRequestQueue.length).toEqual(2);
});
});
describe(".getEventEncryptionInfo", () => {
@@ -389,6 +438,7 @@ describe("RustCrypto", () => {
getRoomEventEncryptionInfo: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixClient["http"],
TEST_USER,
@@ -398,10 +448,6 @@ describe("RustCrypto", () => {
);
});
afterEach(() => {
jest.restoreAllMocks();
});
async function makeEncryptedEvent(): Promise<MatrixEvent> {
const encryptedEvent = mkEvent({
event: true,
@@ -427,6 +473,26 @@ describe("RustCrypto", () => {
expect(olmMachine.getRoomEventEncryptionInfo).not.toHaveBeenCalled();
});
it("should handle decryption failures", async () => {
const event = mkEvent({
event: true,
type: "m.room.encrypted",
content: { algorithm: "fake_alg" },
room: "!room:id",
});
event.event.event_id = "$event:id";
const mockCryptoBackend = {
decryptEvent: () => {
throw new Error("UISI");
},
};
await event.attemptDecryption(mockCryptoBackend as unknown as CryptoBackend);
const res = await rustCrypto.getEncryptionInfoForEvent(event);
expect(res).toBe(null);
expect(olmMachine.getRoomEventEncryptionInfo).not.toHaveBeenCalled();
});
it("passes the event into the OlmMachine", async () => {
const encryptedEvent = await makeEncryptedEvent();
const res = await rustCrypto.getEncryptionInfoForEvent(encryptedEvent);
@@ -447,7 +513,7 @@ describe("RustCrypto", () => {
[RustSdkCryptoJs.ShieldColor.Red, EventShieldColour.RED],
])("gets the right shield color (%i)", async (rustShield, expectedShield) => {
const mockEncryptionInfo = {
shieldState: jest.fn().mockReturnValue({ color: rustShield, message: null }),
shieldState: jest.fn().mockReturnValue({ color: rustShield, message: undefined }),
} as unknown as RustSdkCryptoJs.EncryptionInfo;
olmMachine.getRoomEventEncryptionInfo.mockResolvedValue(mockEncryptionInfo);
@@ -458,7 +524,7 @@ describe("RustCrypto", () => {
});
it.each([
[null, null],
[undefined, null],
["Encrypted by an unverified user.", EventShieldReason.UNVERIFIED_IDENTITY],
["Encrypted by a device not verified by its owner.", EventShieldReason.UNSIGNED_DEVICE],
[
@@ -567,6 +633,7 @@ describe("RustCrypto", () => {
getDevice: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixClient["http"],
TEST_USER,
@@ -578,6 +645,7 @@ describe("RustCrypto", () => {
it("should call getDevice", async () => {
olmMachine.getDevice.mockResolvedValue({
free: jest.fn(),
isCrossSigningTrusted: jest.fn().mockReturnValue(false),
isLocallyTrusted: jest.fn().mockReturnValue(false),
isCrossSignedByOwner: jest.fn().mockReturnValue(false),
@@ -732,10 +800,23 @@ describe("RustCrypto", () => {
it("can save and restore a key", async () => {
const key = "testtesttesttesttesttesttesttest";
const rustCrypto = await makeTestRustCrypto();
await rustCrypto.storeSessionBackupPrivateKey(new TextEncoder().encode(key));
await rustCrypto.storeSessionBackupPrivateKey(
new TextEncoder().encode(key),
testData.SIGNED_BACKUP_DATA.version!,
);
const fetched = await rustCrypto.getSessionBackupPrivateKey();
expect(new TextDecoder().decode(fetched!)).toEqual(key);
});
it("fails to save a key if version not provided", async () => {
const key = "testtesttesttesttesttesttesttest";
const rustCrypto = await makeTestRustCrypto();
await expect(() => rustCrypto.storeSessionBackupPrivateKey(new TextEncoder().encode(key))).rejects.toThrow(
"storeSessionBackupPrivateKey: version is required",
);
const fetched = await rustCrypto.getSessionBackupPrivateKey();
expect(fetched).toBeNull();
});
});
describe("getActiveSessionBackupVersion", () => {
@@ -772,6 +853,7 @@ describe("RustCrypto", () => {
getIdentity: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
rustCrypto = new RustCrypto(
logger,
olmMachine,
{} as MatrixClient["http"],
TEST_USER,
@@ -790,7 +872,7 @@ describe("RustCrypto", () => {
});
it("returns a verified UserVerificationStatus when the UserIdentity is verified", async () => {
olmMachine.getIdentity.mockResolvedValue({ isVerified: jest.fn().mockReturnValue(true) });
olmMachine.getIdentity.mockResolvedValue({ free: jest.fn(), isVerified: jest.fn().mockReturnValue(true) });
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.isVerified()).toBeTruthy();
@@ -799,6 +881,57 @@ describe("RustCrypto", () => {
expect(userVerificationStatus.wasCrossSigningVerified()).toBeFalsy();
});
});
describe("key backup", () => {
it("is started when rust crypto is created", async () => {
// `RustCrypto.checkKeyBackupAndEnable` async call is made in background in the RustCrypto constructor.
// We don't have an instance of the rust crypto yet, we spy directly in the prototype.
const spyCheckKeyBackupAndEnable = jest
.spyOn(RustCrypto.prototype, "checkKeyBackupAndEnable")
.mockResolvedValue({} as KeyBackupCheck);
await makeTestRustCrypto();
expect(spyCheckKeyBackupAndEnable).toHaveBeenCalled();
});
it("raises KeyBackupStatus event when identify change", async () => {
// Return the key backup
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
const mockHttpApi = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
baseUrl: "http://server/",
prefix: "",
onlyData: true,
});
const olmMachine = {
getIdentity: jest.fn(),
// Force the backup to be trusted by the olmMachine
verifyBackup: jest.fn().mockResolvedValue({ trusted: jest.fn().mockReturnValue(true) }),
isBackupEnabled: jest.fn().mockReturnValue(true),
getBackupKeys: jest.fn(),
enableBackupV1: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
const rustCrypto = new RustCrypto(
logger,
olmMachine,
mockHttpApi,
testData.TEST_USER_ID,
testData.TEST_DEVICE_ID,
{} as ServerSideSecretStorage,
{} as CryptoCallbacks,
);
// Wait for the key backup to be available
const keyBackupStatusPromise = new Promise<boolean>((resolve) =>
rustCrypto.once(CryptoEvent.KeyBackupStatus, resolve),
);
await rustCrypto.onUserIdentityUpdated(new RustSdkCryptoJs.UserId(testData.TEST_USER_ID));
expect(await keyBackupStatusPromise).toBe(true);
});
});
});
/** build a basic RustCrypto instance for testing
@@ -812,5 +945,5 @@ async function makeTestRustCrypto(
secretStorage: ServerSideSecretStorage = {} as ServerSideSecretStorage,
cryptoCallbacks: CryptoCallbacks = {} as CryptoCallbacks,
): Promise<RustCrypto> {
return await initRustCrypto(http, userId, deviceId, secretStorage, cryptoCallbacks, null);
return await initRustCrypto(logger, http, userId, deviceId, secretStorage, cryptoCallbacks, null, undefined);
}
+61 -2
View File
@@ -14,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { secretStorageContainsCrossSigningKeys } from "../../../src/rust-crypto/secret-storage";
import {
secretStorageCanAccessSecrets,
secretStorageContainsCrossSigningKeys,
} from "../../../src/rust-crypto/secret-storage";
import { ServerSideSecretStorage } from "../../../src/secret-storage";
describe("secret-storage", () => {
@@ -22,6 +25,7 @@ describe("secret-storage", () => {
it("should return false when the master cross-signing key is not stored in secret storage", async () => {
const secretStorage = {
isStored: jest.fn().mockReturnValue(false),
getDefaultKeyId: jest.fn().mockResolvedValue("SFQ3TbqGOdaaRVfxHtNkn0tvhx0rVj9S"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
@@ -35,6 +39,7 @@ describe("secret-storage", () => {
if (type === "m.cross_signing.master") return { secretStorageKey: {} };
else return { secretStorageKey2: {} };
},
getDefaultKeyId: jest.fn().mockResolvedValue("SFQ3TbqGOdaaRVfxHtNkn0tvhx0rVj9S"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
@@ -51,19 +56,73 @@ describe("secret-storage", () => {
return { secretStorageKey2: {} };
}
},
getDefaultKeyId: jest.fn().mockResolvedValue("secretStorageKey"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});
it("should return true when there is shared secret storage key between master, user signing and self signing keys", async () => {
it("should return true when master, user signing and self signing keys are all encrypted with default key", async () => {
const secretStorage = {
isStored: jest.fn().mockReturnValue({ secretStorageKey: {} }),
getDefaultKeyId: jest.fn().mockResolvedValue("secretStorageKey"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeTruthy();
});
it("should return false when master, user signing and self signing keys are all encrypted with a non-default key", async () => {
const secretStorage = {
isStored: jest.fn().mockResolvedValue({ defaultKey: {} }),
getDefaultKeyId: jest.fn().mockResolvedValue("anotherCommonKey"),
} as unknown as ServerSideSecretStorage;
const result = await secretStorageContainsCrossSigningKeys(secretStorage);
expect(result).toBeFalsy();
});
});
it("Check canAccessSecrets", async () => {
const secretStorage = {
isStored: jest.fn((secretName) => {
if (secretName == "secretA") {
return { aaaa: {} };
} else if (secretName == "secretB") {
return { bbbb: {} };
} else if (secretName == "secretC") {
return { cccc: {} };
} else if (secretName == "secretD") {
return { aaaa: {} };
} else if (secretName == "secretE") {
return { aaaa: {}, bbbb: {} };
} else {
null;
}
}),
getDefaultKeyId: jest.fn().mockResolvedValue("aaaa"),
} as unknown as ServerSideSecretStorage;
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretE"])).toStrictEqual(true);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA"])).toStrictEqual(true);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretC"])).toStrictEqual(false);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD"])).toStrictEqual(true);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretC"])).toStrictEqual(false);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretC", "secretA"])).toStrictEqual(false);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD", "secretB"])).toStrictEqual(
false,
);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD", "Unknown"])).toStrictEqual(
false,
);
expect(await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretD", "secretE"])).toStrictEqual(
true,
);
expect(
await secretStorageCanAccessSecrets(secretStorage, ["secretA", "secretC", "secretD", "secretE"]),
).toStrictEqual(false);
expect(await secretStorageCanAccessSecrets(secretStorage, [])).toStrictEqual(true);
});
});
+58 -1
View File
@@ -16,8 +16,9 @@ limitations under the License.
import "fake-indexeddb/auto";
import "jest-localstorage-mock";
import { IDBFactory } from "fake-indexeddb";
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore } from "../../../src";
import { IndexedDBStore, IStateEventWithRoomId, MemoryStore, User, UserEvent } from "../../../src";
import { emitPromise } from "../../test-utils/test-utils";
import { LocalIndexedDBStoreBackend } from "../../../src/store/indexeddb-local-backend";
import { defer } from "../../../src/utils";
@@ -76,6 +77,62 @@ describe("IndexedDBStore", () => {
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(2);
});
it("Should load presence events on startup", async () => {
// 1. Create idb database
const indexedDB = new IDBFactory();
const setupDefer = defer<Event>();
const req = indexedDB.open("matrix-js-sdk:db3", 1);
let db: IDBDatabase;
req.onupgradeneeded = () => {
db = req.result;
db.createObjectStore("users", { keyPath: ["userId"] });
db.createObjectStore("accountData", { keyPath: ["type"] });
db.createObjectStore("sync", { keyPath: ["clobber"] });
};
req.onsuccess = setupDefer.resolve;
await setupDefer.promise;
// 2. Fill in user presence data
const writeDefer = defer<Event>();
const transaction = db!.transaction(["users"], "readwrite");
const objectStore = transaction.objectStore("users");
const request = objectStore.put({
userId: "@alice:matrix.org",
event: {
content: {
presence: "online",
},
sender: "@alice:matrix.org",
type: "m.presence",
},
});
request.onsuccess = writeDefer.resolve;
await writeDefer.promise;
// 3. Close database
req.result.close();
// 2. Check if the code loads presence events
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "db3",
localStorage,
});
let userCreated = false;
let presenceEventEmitted = false;
store.setUserCreator((id: string) => {
userCreated = true;
const user = new User(id);
user.on(UserEvent.Presence, () => {
presenceEventEmitted = true;
});
return user;
});
await store.startup();
expect(userCreated).toBe(true);
expect(presenceEventEmitted).toBe(true);
});
it("should use MemoryStore methods for pending events if no localStorage", async () => {
jest.spyOn(MemoryStore.prototype, "setPendingEvents");
jest.spyOn(MemoryStore.prototype, "getPendingEvents");
+62
View File
@@ -22,12 +22,15 @@ import { Room } from "../../src/models/room";
import { EventTimeline } from "../../src/models/event-timeline";
import { TimelineIndex, TimelineWindow } from "../../src/timeline-window";
import { mkMessage } from "../test-utils/test-utils";
import { MatrixEvent } from "../../src/models/event";
const ROOM_ID = "roomId";
const USER_ID = "userId";
const mockClient = {
getEventTimeline: jest.fn(),
paginateEventTimeline: jest.fn(),
supportsThreads: jest.fn(),
getUserId: jest.fn().mockReturnValue(USER_ID),
} as unknown as MockedObject<MatrixClient>;
/*
@@ -64,6 +67,23 @@ function addEventsToTimeline(timeline: EventTimeline, numEvents: number, toStart
}
}
function createEvents(numEvents: number): Array<MatrixEvent> {
const ret = [];
for (let i = 0; i < numEvents; i++) {
ret.push(
mkMessage({
room: ROOM_ID,
user: USER_ID,
event: true,
unsigned: { age: 1 },
}),
);
}
return ret;
}
/*
* create a pair of linked timelines
*/
@@ -412,4 +432,46 @@ describe("TimelineWindow", function () {
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS)).toBe(true);
});
});
function idsOf(events: Array<MatrixEvent>): Array<string> {
return events.map((e) => (e ? e.getId() ?? "MISSING_ID" : "MISSING_EVENT"));
}
describe("removing events", () => {
it("should shorten if removing an event within the window makes it overflow", function () {
// Given a room with events in two timelines
const room = new Room(ROOM_ID, mockClient, USER_ID, { timelineSupport: true });
const timelineSet = room.getUnfilteredTimelineSet();
const liveTimeline = room.getLiveTimeline();
const oldTimeline = room.addTimeline();
liveTimeline.setNeighbouringTimeline(oldTimeline, EventTimeline.BACKWARDS);
oldTimeline.setNeighbouringTimeline(liveTimeline, EventTimeline.FORWARDS);
const oldEvents = createEvents(5);
const liveEvents = createEvents(5);
const [, , e3, e4, e5] = oldEvents;
const [, e7, e8, e9, e10] = liveEvents;
room.addLiveEvents(liveEvents);
room.addEventsToTimeline(oldEvents, true, oldTimeline);
// And 2 windows over the timelines in this room
const oldWindow = new TimelineWindow(mockClient, timelineSet);
oldWindow.load(e5.getId(), 6);
expect(idsOf(oldWindow.getEvents())).toEqual(idsOf([e5, e4, e3]));
const newWindow = new TimelineWindow(mockClient, timelineSet);
newWindow.load(e9.getId(), 4);
expect(idsOf(newWindow.getEvents())).toEqual(idsOf([e7, e8, e9, e10]));
// When I remove an event
room.removeEvent(e8.getId()!);
// Then the affected timeline is shortened (because it would have
// been too long with the removed event gone)
expect(idsOf(newWindow.getEvents())).toEqual(idsOf([e7, e9, e10]));
// And the unaffected one is not
expect(idsOf(oldWindow.getEvents())).toEqual(idsOf([e5, e4, e3]));
});
});
});
+1 -9
View File
@@ -243,21 +243,13 @@ export interface LoginResponse {
}
/**
* The result of a successful [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882)
* `m.login.token` issuance request.
* Note that this is UNSTABLE and subject to breaking changes without notice.
* The result of a successful `m.login.token` issuance request as per https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv1loginget_token
*/
export interface LoginTokenPostResponse {
/**
* The token to use with `m.login.token` to authenticate.
*/
login_token: string;
/**
* Expiration in seconds.
*
* @deprecated this is only provided for compatibility with original revision of the MSC.
*/
expires_in: number;
/**
* Expiration in milliseconds.
*/
+4
View File
@@ -55,6 +55,7 @@ export enum EventType {
CallReplaces = "m.call.replaces",
CallAssertedIdentity = "m.call.asserted_identity",
CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",
CallEncryptionKeysPrefix = "io.element.call.encryption_keys",
KeyVerificationRequest = "m.key.verification.request",
KeyVerificationStart = "m.key.verification.start",
KeyVerificationCancel = "m.key.verification.cancel",
@@ -93,6 +94,9 @@ export enum EventType {
// Group call events
GroupCallPrefix = "org.matrix.msc3401.call",
GroupCallMemberPrefix = "org.matrix.msc3401.call.member",
// MatrixRTC events
CallNotify = "org.matrix.msc4075.call.notify",
}
export enum RelationType {
+2 -2
View File
@@ -41,7 +41,7 @@ export enum AutoDiscoveryAction {
FAIL_ERROR = "FAIL_ERROR",
}
enum AutoDiscoveryError {
export enum AutoDiscoveryError {
Invalid = "Invalid homeserver discovery response",
GenericFailure = "Failed to get autodiscovery configuration from server",
InvalidHsBaseUrl = "Invalid base_url for m.homeserver",
@@ -114,7 +114,7 @@ export class AutoDiscovery {
public static readonly ERROR_HOMESERVER_TOO_OLD = AutoDiscoveryError.HomeserverTooOld;
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError);
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError) as AutoDiscoveryError[];
/**
* The auto discovery failed. The client is expected to communicate
+88
View File
@@ -0,0 +1,88 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Base64 encoding and decoding utilities
*/
/**
* Encode a typed array of uint8 as base64.
* @param uint8Array - The data to encode.
* @returns The base64.
*/
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
// A brief note on the state of base64 encoding in Javascript.
// As of 2023, there is still no common native impl between both browsers and
// node. Older Webpack provides an impl for Buffer and there is a polyfill class
// for it. There are also plenty of pure js impls, eg. base64-js which has 2336
// dependents at current count. Using this would probably be fine although it's
// a little under-docced and run by an individual. The node impl works fine,
// the browser impl works but predates Uint8Array and so only uses strings.
// Right now, switching between native (or polyfilled) impls like this feels
// like the least bad option, but... *shrugs*.
if (typeof Buffer === "function") {
return Buffer.from(uint8Array).toString("base64");
} else if (typeof btoa === "function" && uint8Array instanceof Uint8Array) {
// ArrayBuffer is a node concept so the param should always be a Uint8Array on
// the browser. We need to check because ArrayBuffers don't have reduce.
return btoa(uint8Array.reduce((acc, current) => acc + String.fromCharCode(current), ""));
} else {
throw new Error("No base64 impl found!");
}
}
/**
* Encode a typed array of uint8 as unpadded base64.
* @param uint8Array - The data to encode.
* @returns The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeBase64(uint8Array).replace(/={1,2}$/, "");
}
/**
* Encode a typed array of uint8 as unpadded base64 using the URL-safe encoding.
* @param uint8Array - The data to encode.
* @returns The unpadded base64.
*/
export function encodeUnpaddedBase64Url(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeUnpaddedBase64(uint8Array).replace("+", "-").replace("/", "_");
}
/**
* Decode a base64 (or base64url) string to a typed array of uint8.
* @param base64 - The base64 to decode.
* @returns The decoded data.
*/
export function decodeBase64(base64: string): Uint8Array {
// See encodeBase64 for a short treatise on base64 en/decoding in JS
if (typeof Buffer === "function") {
return Buffer.from(base64, "base64");
} else if (typeof atob === "function") {
const itFunc = function* (): Generator<number> {
const decoded = atob(
// built-in atob doesn't support base64url: convert so we support either
base64.replace("-", "+").replace("_", "/"),
);
for (let i = 0; i < decoded.length; ++i) {
yield decoded.charCodeAt(i);
}
};
return Uint8Array.from(itFunc());
} else {
throw new Error("No base64 impl found!");
}
}
+4 -7
View File
@@ -24,15 +24,15 @@ declare global {
/* eslint-enable no-var */
}
if (global.__js_sdk_entrypoint) {
if (globalThis.__js_sdk_entrypoint) {
throw new Error("Multiple matrix-js-sdk entrypoints detected!");
}
global.__js_sdk_entrypoint = true;
globalThis.__js_sdk_entrypoint = true;
// just *accessing* indexedDB throws an exception in firefox with indexeddb disabled.
let indexedDB: IDBFactory | undefined;
try {
indexedDB = global.indexedDB;
indexedDB = globalThis.indexedDB;
} catch (e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
@@ -40,8 +40,5 @@ if (indexedDB) {
matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto"));
}
// We export 3 things to make browserify happy as well as downstream projects.
// It's awkward, but required.
export * from "./matrix";
export default matrixcs; // keep export for browserify package deps
global.matrixcs = matrixcs;
globalThis.matrixcs = matrixcs;
+259 -140
View File
@@ -47,42 +47,43 @@ import { Direction, EventTimeline } from "./models/event-timeline";
import { IActionsObject, PushProcessor } from "./pushprocessor";
import { AutoDiscovery, AutoDiscoveryAction } from "./autodiscovery";
import * as olmlib from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./crypto/olmlib";
import { decodeBase64, encodeBase64 } from "./base64";
import { IExportedDevice as IExportedOlmDevice } from "./crypto/OlmDevice";
import { IOlmDevice } from "./crypto/algorithms/megolm";
import { TypedReEmitter } from "./ReEmitter";
import { IRoomEncryption, RoomList } from "./crypto/RoomList";
import { logger } from "./logger";
import { logger, Logger } from "./logger";
import { SERVICE_TYPES } from "./service-types";
import {
Body,
ClientPrefix,
FileType,
HttpApiEvent,
HttpApiEventHandlerMap,
Upload,
UploadOpts,
MatrixError,
MatrixHttpApi,
Method,
retryNetworkOperation,
ClientPrefix,
MediaPrefix,
HTTPError,
IdentityPrefix,
IHttpOpts,
FileType,
UploadResponse,
HTTPError,
IRequestOpts,
Body,
TokenRefreshFunction,
MatrixError,
MatrixHttpApi,
MediaPrefix,
Method,
retryNetworkOperation,
Upload,
UploadOpts,
UploadResponse,
} from "./http-api";
import {
Crypto,
CryptoEvent,
CryptoEventHandlerMap,
fixBackupKey,
ICryptoCallbacks,
ICheckOwnCrossSigningTrustOpts,
ICryptoCallbacks,
IRoomKeyRequestBody,
isCryptoAvailable,
VerificationMethod,
IRoomKeyRequestBody,
} from "./crypto";
import { DeviceInfo } from "./crypto/deviceinfo";
import { decodeRecoveryKey } from "./crypto/recoverykey";
@@ -110,7 +111,7 @@ import { VerificationRequest } from "./crypto/verification/request/VerificationR
import { VerificationBase as Verification } from "./crypto/verification/Base";
import * as ContentHelpers from "./content-helpers";
import { CrossSigningInfo, DeviceTrustLevel, ICacheCallbacks, UserTrustLevel } from "./crypto/CrossSigning";
import { Room, NotificationCountType, RoomEvent, RoomEventHandlerMap, RoomNameState } from "./models/room";
import { NotificationCountType, Room, RoomEvent, RoomEventHandlerMap, RoomNameState } from "./models/room";
import { RoomMemberEvent, RoomMemberEventHandlerMap } from "./models/room-member";
import { IPowerLevelsContent, RoomStateEvent, RoomStateEventHandlerMap } from "./models/room-state";
import {
@@ -119,8 +120,10 @@ import {
IContextResponse,
ICreateRoomOpts,
IEventSearchOpts,
IFilterResponse,
IGuestAccessOpts,
IJoinRoomOpts,
INotificationsResponse,
IPaginateOpts,
IPresenceOpts,
IRedactOpts,
@@ -129,15 +132,14 @@ import {
IRoomDirectoryOptions,
ISearchOpts,
ISendEventResponse,
INotificationsResponse,
IFilterResponse,
ITagsResponse,
IStatusResponse,
ITagsResponse,
KnockRoomOpts,
} from "./@types/requests";
import {
EventType,
LOCAL_NOTIFICATION_SETTINGS_PREFIX,
MSC3912_RELATION_BASED_REDACTIONS_PROP,
MsgType,
PUSHER_ENABLED,
RelationType,
@@ -146,7 +148,6 @@ import {
UNSTABLE_MSC3088_ENABLED,
UNSTABLE_MSC3088_PURPOSE,
UNSTABLE_MSC3089_TREE_SUBTYPE,
MSC3912_RELATION_BASED_REDACTIONS_PROP,
} from "./@types/event";
import { IdServerUnbindResult, IImageInfo, JoinRule, Preset, Visibility } from "./@types/partials";
import { EventMapper, eventMapperFor, MapperOpts } from "./event-mapper";
@@ -178,30 +179,30 @@ import {
} from "./@types/PushRules";
import { IThreepid } from "./@types/threepids";
import { CryptoStore, OutgoingRoomKeyRequest } from "./crypto/store/base";
import { GroupCall, IGroupCallDataChannelOptions, GroupCallIntent, GroupCallType } from "./webrtc/groupCall";
import { GroupCall, GroupCallIntent, GroupCallType, IGroupCallDataChannelOptions } from "./webrtc/groupCall";
import { MediaHandler } from "./webrtc/mediaHandler";
import {
LoginTokenPostResponse,
ILoginFlowsResponse,
IRefreshTokenResponse,
SSOAction,
LoginResponse,
LoginRequest,
LoginResponse,
LoginTokenPostResponse,
SSOAction,
} from "./@types/auth";
import { TypedEventEmitter } from "./models/typed-event-emitter";
import { MAIN_ROOM_TIMELINE, ReceiptType } from "./@types/read_receipts";
import { MSC3575SlidingSyncRequest, MSC3575SlidingSyncResponse, SlidingSync } from "./sliding-sync";
import { SlidingSyncSdk } from "./sliding-sync-sdk";
import {
determineFeatureSupport,
FeatureSupport,
Thread,
THREAD_RELATION_TYPE,
determineFeatureSupport,
ThreadFilterType,
threadFilterTypeToFilter,
} from "./models/thread";
import { MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon";
import { UnstableValue } from "./NamespacedValue";
import { M_BEACON_INFO, MBeaconInfoEventContent } from "./@types/beacon";
import { NamespacedValue, UnstableValue } from "./NamespacedValue";
import { ToDeviceMessageQueue } from "./ToDeviceMessageQueue";
import { ToDeviceBatch } from "./models/ToDeviceMessage";
import { IgnoredInvites } from "./models/invites-ignorer";
@@ -294,6 +295,14 @@ export interface ICreateClientOpts {
deviceId?: string;
accessToken?: string;
refreshToken?: string;
/**
* Function used to attempt refreshing access and refresh tokens
* Called by http-api when a possibly expired token is encountered
* and a refreshToken is found
*/
tokenRefreshFunction?: TokenRefreshFunction;
/**
* Identity server provider to retrieve the user's access token when accessing
@@ -343,7 +352,14 @@ export interface ICreateClientOpts {
deviceToImport?: IExportedDevice;
/**
* Key used to pickle olm objects or other sensitive data.
* Encryption key used for encrypting sensitive data (such as e2ee keys) in storage.
*
* This must be set to the same value every time the client is initialised for the same device.
*
* If unset, either a hardcoded key or no encryption at all is used, depending on the Crypto implementation.
*
* No particular requirement is placed on the key data (it is fed into an HKDF to generate the actual encryption
* keys).
*/
pickleKey?: string;
@@ -408,6 +424,12 @@ export interface ICreateClientOpts {
* so that livekit media can be used in the application layert (js-sdk contains no livekit code).
*/
useLivekitForGroupCalls?: boolean;
/**
* A logger to associate with this MatrixClient.
* Defaults to the built-in global logger.
*/
logger?: Logger;
}
export interface IMatrixClientCreateOpts extends ICreateClientOpts {
@@ -512,9 +534,12 @@ export interface IChangePasswordCapability extends ICapability {}
export interface IThreadsCapability extends ICapability {}
export interface IMSC3882GetLoginTokenCapability extends ICapability {}
export interface IGetLoginTokenCapability extends ICapability {}
export const UNSTABLE_MSC3882_CAPABILITY = new UnstableValue("m.get_login_token", "org.matrix.msc3882.get_login_token");
export const GET_LOGIN_TOKEN_CAPABILITY = new NamespacedValue(
"m.get_login_token",
"org.matrix.msc3882.get_login_token",
);
export const UNSTABLE_MSC2666_SHARED_ROOMS = "uk.half-shot.msc2666";
export const UNSTABLE_MSC2666_MUTUAL_ROOMS = "uk.half-shot.msc2666.mutual_rooms";
@@ -529,8 +554,8 @@ export interface Capabilities {
"m.change_password"?: IChangePasswordCapability;
"m.room_versions"?: IRoomVersionsCapability;
"io.element.thread"?: IThreadsCapability;
[UNSTABLE_MSC3882_CAPABILITY.name]?: IMSC3882GetLoginTokenCapability;
[UNSTABLE_MSC3882_CAPABILITY.altName]?: IMSC3882GetLoginTokenCapability;
"m.get_login_token"?: IGetLoginTokenCapability;
"org.matrix.msc3882.get_login_token"?: IGetLoginTokenCapability;
}
/** @deprecated prefer {@link CrossSigningKeyInfo}. */
@@ -870,7 +895,7 @@ interface IRoomHierarchy {
export interface TimestampToEventResponse {
event_id: string;
origin_server_ts: string;
origin_server_ts: number;
}
interface IWhoamiResponse {
@@ -1187,13 +1212,26 @@ const SSO_ACTION_PARAM = new UnstableValue("action", "org.matrix.msc3824.action"
export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHandlerMap> {
public static readonly RESTORE_BACKUP_ERROR_BAD_KEY = "RESTORE_BACKUP_ERROR_BAD_KEY";
private readonly logger: Logger;
public reEmitter = new TypedReEmitter<EmittedEvents, ClientEventHandlerMap>(this);
public olmVersion: [number, number, number] | null = null; // populated after initCrypto
public usingExternalCrypto = false;
public store: Store;
private _store!: Store;
public deviceId: string | null;
public credentials: { userId: string | null };
/**
* Encryption key used for encrypting sensitive data (such as e2ee keys) in storage.
*
* As supplied in the constructor via {@link IMatrixClientCreateOpts#pickleKey}.
*
* If unset, either a hardcoded key or no encryption at all is used, depending on the Crypto implementation.
*
* @deprecated this should be a private property.
*/
public pickleKey?: string;
public scheduler?: MatrixScheduler;
public clientRunning = false;
public timelineSupport = false;
@@ -1281,6 +1319,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public constructor(opts: IMatrixClientCreateOpts) {
super();
// If a custom logger is provided, use it. Otherwise, default to the global
// one in logger.ts.
this.logger = opts.logger ?? logger;
opts.baseUrl = utils.ensureNoTrailingSlash(opts.baseUrl);
opts.idBaseUrl = utils.ensureNoTrailingSlash(opts.idBaseUrl);
@@ -1301,26 +1343,29 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
baseUrl: opts.baseUrl,
idBaseUrl: opts.idBaseUrl,
accessToken: opts.accessToken,
refreshToken: opts.refreshToken,
tokenRefreshFunction: opts.tokenRefreshFunction,
prefix: ClientPrefix.V3,
onlyData: true,
extraParams: opts.queryParams,
localTimeoutMs: opts.localTimeoutMs,
useAuthorizationHeader: opts.useAuthorizationHeader,
logger: this.logger,
});
if (opts.deviceToImport) {
if (this.deviceId) {
logger.warn(
this.logger.warn(
"not importing device because device ID is provided to " +
"constructor independently of exported data",
);
} else if (this.credentials.userId) {
logger.warn(
this.logger.warn(
"not importing device because user ID is provided to " +
"constructor independently of exported data",
);
} else if (!opts.deviceToImport.deviceId) {
logger.warn("not importing device because no device ID in exported data");
this.logger.warn("not importing device because no device ID in exported data");
} else {
this.deviceId = opts.deviceToImport.deviceId;
this.credentials.userId = opts.deviceToImport.userId;
@@ -1449,6 +1494,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.ignoredInvites = new IgnoredInvites(this);
this._secretStorage = new ServerSideSecretStorageImpl(this, opts.cryptoCallbacks ?? {});
// having lots of event listeners is not unusual. 0 means "unlimited".
this.setMaxListeners(0);
}
public set store(newStore: Store) {
this._store = newStore;
this._store.setUserCreator((userId) => User.createUser(userId, this));
}
public get store(): Store {
return this._store;
}
/**
@@ -1492,7 +1549,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (this.syncApi) {
// This shouldn't happen since we thought the client was not running
logger.error("Still have sync object whilst not running: stopping old one");
this.logger.error("Still have sync object whilst not running: stopping old one");
this.syncApi.stop();
}
@@ -1506,7 +1563,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
Thread.setServerSideListSupport(list);
Thread.setServerSideFwdPaginationSupport(fwdPagination);
} catch (e) {
logger.error("Can't fetch server versions, continuing to initialise sync, this will be retried later", e);
this.logger.error(
"Can't fetch server versions, continuing to initialise sync, this will be retried later",
e,
);
}
this.clientOpts = opts ?? {};
@@ -1522,7 +1582,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
if (this.clientOpts.hasOwnProperty("experimentalThreadSupport")) {
logger.warn("`experimentalThreadSupport` has been deprecated, use `threadSupport` instead");
this.logger.warn("`experimentalThreadSupport` has been deprecated, use `threadSupport` instead");
}
// If `threadSupport` is omitted and the deprecated `experimentalThreadSupport` has been passed
@@ -1534,7 +1594,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.clientOpts.threadSupport = this.clientOpts.experimentalThreadSupport;
}
this.syncApi.sync();
this.syncApi.sync().catch((e) => this.logger.info("Sync startup aborted with an error:", e));
if (this.clientOpts.clientWellKnownPollPeriod !== undefined) {
this.clientWellKnownIntervalID = setInterval(() => {
@@ -1573,7 +1633,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (!this.clientRunning) return; // already stopped
logger.log("stopping MatrixClient");
this.logger.debug("stopping MatrixClient");
this.clientRunning = false;
@@ -1623,7 +1683,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
if (!getDeviceResult.device_data || !getDeviceResult.device_id) {
logger.info("no dehydrated device found");
this.logger.info("no dehydrated device found");
return;
}
@@ -1631,16 +1691,16 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
try {
const deviceData = getDeviceResult.device_data;
if (deviceData.algorithm !== DEHYDRATION_ALGORITHM) {
logger.warn("Wrong algorithm for dehydrated device");
this.logger.warn("Wrong algorithm for dehydrated device");
return;
}
logger.log("unpickling dehydrated device");
this.logger.debug("unpickling dehydrated device");
const key = await this.cryptoCallbacks.getDehydrationKey(deviceData, (k) => {
// copy the key so that it doesn't get clobbered
account.unpickle(new Uint8Array(k), deviceData.account);
});
account.unpickle(key, deviceData.account);
logger.log("unpickled device");
this.logger.debug("unpickled device");
const rehydrateResult = await this.http.authedRequest<{ success: boolean }>(
Method.Post,
@@ -1656,7 +1716,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (rehydrateResult.success) {
this.deviceId = getDeviceResult.device_id;
logger.info("using dehydrated device");
this.logger.info("using dehydrated device");
const pickleKey = this.pickleKey || "DEFAULT_KEY";
this.exportedOlmDeviceToImport = {
pickledAccount: account.pickle(pickleKey),
@@ -1667,12 +1727,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.deviceId;
} else {
account.free();
logger.info("not using dehydrated device");
this.logger.info("not using dehydrated device");
return;
}
} catch (e) {
account.free();
logger.warn("could not unpickle", e);
this.logger.warn("could not unpickle", e);
}
}
@@ -1692,7 +1752,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
},
);
} catch (e) {
logger.info("could not get dehydrated device", e);
this.logger.info("could not get dehydrated device", e);
return;
}
}
@@ -1714,7 +1774,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
deviceDisplayName?: string,
): Promise<void> {
if (!this.crypto) {
logger.warn("not dehydrating device if crypto is not enabled");
this.logger.warn("not dehydrating device if crypto is not enabled");
return;
}
return this.crypto.dehydrationManager.setKeyAndQueueDehydration(key, keyInfo, deviceDisplayName);
@@ -1735,7 +1795,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
deviceDisplayName?: string,
): Promise<string | undefined> {
if (!this.crypto) {
logger.warn("not dehydrating device if crypto is not enabled");
this.logger.warn("not dehydrating device if crypto is not enabled");
return;
}
await this.crypto.dehydrationManager.setKey(key, keyInfo, deviceDisplayName);
@@ -1744,7 +1804,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
public async exportDevice(): Promise<IExportedDevice | undefined> {
if (!this.crypto) {
logger.warn("not exporting device if crypto is not enabled");
this.logger.warn("not exporting device if crypto is not enabled");
return;
}
return {
@@ -1787,10 +1847,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
`${RUST_SDK_STORE_PREFIX}::matrix-sdk-crypto-meta`,
]) {
const prom = new Promise((resolve, reject) => {
logger.info(`Removing IndexedDB instance ${dbname}`);
this.logger.info(`Removing IndexedDB instance ${dbname}`);
const req = indexedDB.deleteDatabase(dbname);
req.onsuccess = (_): void => {
logger.info(`Removed IndexedDB instance ${dbname}`);
this.logger.info(`Removed IndexedDB instance ${dbname}`);
resolve(0);
};
req.onerror = (e): void => {
@@ -1799,11 +1859,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// database that did not allow mutations."
//
// it seems like the only thing we can really do is ignore the error.
logger.warn(`Failed to remove IndexedDB instance ${dbname}:`, e);
this.logger.warn(`Failed to remove IndexedDB instance ${dbname}:`, e);
resolve(0);
};
req.onblocked = (e): void => {
logger.info(`cannot yet remove IndexedDB instance ${dbname}`);
this.logger.info(`cannot yet remove IndexedDB instance ${dbname}`);
};
});
await prom;
@@ -2110,7 +2170,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (this.cachedCapabilities && !fresh) {
if (now < this.cachedCapabilities.expiration) {
logger.log("Returning cached capabilities");
this.logger.debug("Returning cached capabilities");
return Promise.resolve(this.cachedCapabilities.capabilities);
}
}
@@ -2122,7 +2182,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
.authedRequest<Response>(Method.Get, "/capabilities")
.catch((e: Error): Response => {
// We swallow errors because we need a default object anyhow
logger.error(e);
this.logger.error(e);
return {};
})
.then((r = {}) => {
@@ -2137,7 +2197,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
expiration: now + cacheMs,
};
logger.log("Caching capabilities: ", capabilities);
this.logger.debug("Caching capabilities: ", capabilities);
return capabilities;
});
}
@@ -2160,7 +2220,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
if (this.cryptoBackend) {
logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
this.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
}
@@ -2169,11 +2229,11 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error(`Cannot enable encryption: no cryptoStore provided`);
}
logger.log("Crypto: Starting up crypto store...");
this.logger.debug("Crypto: Starting up crypto store...");
await this.cryptoStore.startup();
// initialise the list of encrypted rooms (whether or not crypto is enabled)
logger.log("Crypto: initialising roomlist...");
this.logger.debug("Crypto: initialising roomlist...");
await this.roomList.init();
const userId = this.getUserId();
@@ -2213,7 +2273,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
CryptoEvent.KeysChanged,
]);
logger.log("Crypto: initialising crypto object...");
this.logger.debug("Crypto: initialising crypto object...");
await crypto.init({
exportedOlmDevice: this.exportedOlmDeviceToImport,
pickleKey: this.pickleKey,
@@ -2229,7 +2289,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// upload our keys in the background
this.crypto.uploadDeviceKeys().catch((e) => {
// TODO: throwing away this error is a really bad idea.
logger.error("Error uploading device keys", e);
this.logger.error("Error uploading device keys", e);
});
}
@@ -2250,7 +2310,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*/
public async initRustCrypto({ useIndexedDB = true }: { useIndexedDB?: boolean } = {}): Promise<void> {
if (this.cryptoBackend) {
logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
this.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
}
@@ -2273,12 +2333,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// needed.
const RustCrypto = await import("./rust-crypto");
const rustCrypto = await RustCrypto.initRustCrypto(
this.logger,
this.http,
userId,
deviceId,
this.secretStorage,
this.cryptoCallbacks,
useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
this.pickleKey,
);
rustCrypto.setSupportedVerificationMethods(this.verificationMethods);
@@ -2350,7 +2412,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @deprecated Does nothing.
*/
public async uploadKeys(): Promise<void> {
logger.warn("MatrixClient.uploadKeys is deprecated");
this.logger.warn("MatrixClient.uploadKeys is deprecated");
}
/**
@@ -2619,12 +2681,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* This API is currently UNSTABLE and may change or be removed without notice.
*
* It has no effect with the Rust crypto implementation.
*
* @param value - whether error on unknown devices
*
* @deprecated Prefer direct access to {@link CryptoApi.globalBlacklistUnverifiedDevices}:
*
* ```ts
* client.getCrypto().globalBlacklistUnverifiedDevices = value;
* client.getCrypto().globalErrorOnUnknownDevices = value;
* ```
*/
public setGlobalErrorOnUnknownDevices(value: boolean): void {
@@ -3313,10 +3375,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
/**
* Get information about the current key backup.
* @returns Information object from API or null
* Get information about the current key backup from the server.
*
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
* Performs some basic validity checks on the shape of the result, and raises an error if it is not as expected.
*
* **Note**: there is no (supported) way to distinguish between "failure to talk to the server" and "another client
* uploaded a key backup version using an algorithm I don't understand.
*
* @returns Information object from API, or null if no backup is present on the server.
*/
public async getKeyBackupVersion(): Promise<IKeyBackupInfo | null> {
let res: IKeyBackupInfo;
@@ -3426,7 +3492,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (opts.secureSecretStorage) {
await this.secretStorage.store("m.megolm_backup.v1", encodeBase64(privateKey));
logger.info("Key backup private key stored in secret storage");
this.logger.info("Key backup private key stored in secret storage");
}
return {
@@ -3495,7 +3561,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// sessions.
await this.checkKeyBackup();
if (!this.getKeyBackupEnabled()) {
logger.error("Key backup not usable even though we just created it");
this.logger.error("Key backup not usable even though we just created it");
}
return res;
@@ -3696,7 +3762,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
targetSessionId?: string,
opts?: IKeyBackupRestoreOpts,
): Promise<IKeyBackupRestoreResult> {
if (!this.crypto) {
if (!this.cryptoBackend) {
throw new Error("End-to-end encryption disabled");
}
const storedKey = await this.secretStorage.get("m.megolm_backup.v1");
@@ -3828,6 +3894,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
throw new Error("End-to-end encryption disabled");
}
if (!backupInfo.version) {
throw new Error("Backup version must be defined");
}
let totalKeyCount = 0;
let keys: IMegolmSessionData[] = [];
@@ -3845,9 +3915,9 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// Cache the key, if possible.
// This is async.
this.cryptoBackend
.storeSessionBackupPrivateKey(privKey)
.storeSessionBackupPrivateKey(privKey, backupInfo.version)
.catch((e) => {
logger.warn("Error caching session backup key:", e);
this.logger.warn("Error caching session backup key:", e);
})
.then(cacheCompleteCallback);
@@ -3894,7 +3964,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
key.session_id = targetSessionId!;
keys.push(key);
} catch (e) {
logger.log("Failed to decrypt megolm session from backup", e);
this.logger.debug("Failed to decrypt megolm session from backup", e);
}
}
} finally {
@@ -3940,7 +4010,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const roomEncryption = this.roomList.getRoomEncryption(roomId);
if (!roomEncryption) {
// unknown room, or unencrypted room
logger.error("Unknown room. Not sharing decryption keys");
this.logger.error("Unknown room. Not sharing decryption keys");
return;
}
@@ -3955,7 +4025,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (alg.sendSharedHistoryInboundSessions) {
await alg.sendSharedHistoryInboundSessions(devicesByUser);
} else {
logger.warn("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
this.logger.warn("Algorithm does not support sharing previous keys", roomEncryption.algorithm);
}
}
@@ -4164,9 +4234,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const room = this.getRoom(roomIdOrAlias);
if (room?.hasMembershipState(this.credentials.userId!, "join")) {
return Promise.resolve(room);
}
if (room?.hasMembershipState(this.credentials.userId!, "join")) return room;
let signPromise: Promise<IThirdPartySigned | void> = Promise.resolve();
@@ -4191,6 +4259,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const res = await this.http.authedRequest<{ room_id: string }>(Method.Post, path, queryString, data);
const roomId = res.room_id;
// In case we were originally given an alias, check the room cache again
// with the resolved ID - this method is supposed to no-op if we already
// were in the room, after all.
const resolvedRoom = this.getRoom(roomId);
if (resolvedRoom?.hasMembershipState(this.credentials.userId!, "join")) return resolvedRoom;
const syncApi = new SyncApi(this, this.clientOpts, this.buildSyncApiOptions());
const syncRoom = syncApi.createRoom(roomId);
if (opts.syncRoom) {
@@ -4534,7 +4608,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const type = localEvent.getType();
logger.log(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
this.logger.debug(`sendEvent of type ${type} in ${roomId} with txnId ${txnId}`);
localEvent.setTxnId(txnId);
localEvent.setStatus(EventStatus.SENDING);
@@ -4606,7 +4680,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return promise;
})
.catch((err) => {
logger.error("Error sending event", err.stack || err);
this.logger.error("Error sending event", err.stack || err);
try {
// set the error on the event before we update the status:
// updating the status emits the event, so the state should be
@@ -4614,7 +4688,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
event.error = err;
this.updatePendingEventStatus(room, event, EventStatus.NOT_SENT);
} catch (e) {
logger.error("Exception in error handler!", (<Error>e).stack || err);
this.logger.error("Exception in error handler!", (<Error>e).stack || err);
}
if (err instanceof MatrixError) {
err.event = event;
@@ -4727,7 +4801,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http
.authedRequest<ISendEventResponse>(Method.Put, path, undefined, event.getWireContent())
.then((res) => {
logger.log(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`);
this.logger.debug(`Event sent to ${event.getRoomId()} with event id ${res.event_id}`);
return res;
});
}
@@ -5085,24 +5159,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
$eventId: event.getId()!,
});
if (!unthreaded && this.supportsThreads()) {
// XXX: the spec currently says a threaded read receipt can be sent for the root of a thread,
// but in practice this isn't possible and the spec needs updating.
const isThread =
!!event.threadRootId &&
// A thread cannot be just a thread root and a thread root can only be read in the main timeline
!event.isThreadRoot &&
// Similarly non-thread relations upon the thread root (reactions, edits) should also be for the main timeline.
event.isRelation() &&
(event.isRelation(THREAD_RELATION_TYPE.name) || event.relationEventId !== event.threadRootId);
body = {
...body,
// Only thread replies should define a specific thread. Thread roots can only be read in the main timeline.
thread_id: isThread ? event.threadRootId : MAIN_ROOM_TIMELINE,
};
}
// Unless we're explicitly making an unthreaded receipt or we don't
// support threads, include the `thread_id` property in the body.
const shouldAddThreadId = !unthreaded && this.supportsThreads();
const fullBody = shouldAddThreadId ? { ...body, thread_id: threadIdForReceipt(event) } : body;
const promise = this.http.authedRequest<{}>(Method.Post, path, undefined, body || {});
const promise = this.http.authedRequest<{}>(Method.Post, path, undefined, fullBody || {});
const room = this.getRoom(event.getRoomId());
if (room && this.credentials.userId) {
@@ -5827,7 +5889,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const mapper = this.getEventMapper();
const event = mapper(res.event);
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
logger.warn("Tried loading a regular timeline at the position of a thread event");
this.logger.warn("Tried loading a regular timeline at the position of a thread event");
return undefined;
}
const events = [
@@ -6964,7 +7026,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// cleanup locks
this.syncLeftRoomsPromise
.then(() => {
logger.log("Marking success of sync left room request");
this.logger.debug("Marking success of sync left room request");
this.syncedLeftRooms = true; // flip the bit on success
})
.finally(() => {
@@ -7169,14 +7231,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
let credentialsGood = false;
const remainingTime = this.turnServersExpiry - Date.now();
if (remainingTime > TURN_CHECK_INTERVAL) {
logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones.");
this.logger.debug("TURN creds are valid for another " + remainingTime + " ms: not fetching new ones.");
credentialsGood = true;
} else {
logger.debug("Fetching new TURN credentials");
this.logger.debug("Fetching new TURN credentials");
try {
const res = await this.turnServer();
if (res.uris) {
logger.log("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
this.logger.debug("Got TURN URIs: " + res.uris + " refresh in " + res.ttl + " secs");
// map the response to a format that can be fed to RTCPeerConnection
const servers: ITurnServer = {
urls: res.uris,
@@ -7190,10 +7252,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.emit(ClientEvent.TurnServers, this.turnServers);
}
} catch (err) {
logger.error("Failed to get TURN URIs", err);
this.logger.error("Failed to get TURN URIs", err);
if ((<HTTPError>err).httpStatus === 403) {
// We got a 403, so there's no point in looping forever.
logger.info("TURN access unavailable for this account: stopping credentials checks");
this.logger.info("TURN access unavailable for this account: stopping credentials checks");
if (this.checkTurnServersIntervalID !== null) global.clearInterval(this.checkTurnServersIntervalID);
this.checkTurnServersIntervalID = undefined;
this.emit(ClientEvent.TurnServersError, <HTTPError>err, true); // fatal
@@ -7655,6 +7717,14 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
return this.http.opts.accessToken || null;
}
/**
* Get the refresh token associated with this account.
* @returns The refresh_token or null
*/
public getRefreshToken(): string | null {
return this.http.opts.refreshToken ?? null;
}
/**
* Set the access token associated with this account.
* @param token - The new access token.
@@ -7932,7 +8002,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
try {
while ((await this.crypto.backupManager.backupPendingKeys(200)) > 0);
} catch (err) {
logger.error("Key backup request failed when logging out. Some keys may be missing from backup", err);
this.logger.error(
"Key backup request failed when logging out. Some keys may be missing from backup",
err,
);
}
}
@@ -7973,40 +8046,23 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/**
* Make a request for an `m.login.token` to be issued as per
* [MSC3882](https://github.com/matrix-org/matrix-spec-proposals/pull/3882).
* https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv1loginget_token
*
* The server may require User-Interactive auth.
* Note that this is UNSTABLE and subject to breaking changes without notice.
*
* @param auth - Optional. Auth data to supply for User-Interactive auth.
* @returns Promise which resolves: On success, the token response
* or UIA auth data.
*/
public async requestLoginToken(auth?: AuthDict): Promise<UIAResponse<LoginTokenPostResponse>> {
// use capabilities to determine which revision of the MSC is being used
const capabilities = await this.getCapabilities();
// use r1 endpoint if capability is exposed otherwise use old r0 endpoint
const endpoint = UNSTABLE_MSC3882_CAPABILITY.findIn(capabilities)
? "/org.matrix.msc3882/login/get_token" // r1 endpoint
: "/org.matrix.msc3882/login/token"; // r0 endpoint
const body: UIARequest<{}> = { auth };
const res = await this.http.authedRequest<UIAResponse<LoginTokenPostResponse>>(
return this.http.authedRequest<UIAResponse<LoginTokenPostResponse>>(
Method.Post,
endpoint,
"/login/get_token",
undefined, // no query params
body,
{ prefix: ClientPrefix.Unstable },
{ prefix: ClientPrefix.V1 },
);
// the representation of expires_in changed from revision 0 to revision 1 so we populate
if ("login_token" in res) {
if (typeof res.expires_in_ms === "number") {
res.expires_in = Math.floor(res.expires_in_ms / 1000);
} else if (typeof res.expires_in === "number") {
res.expires_in_ms = res.expires_in * 1000;
}
}
return res;
}
/**
@@ -8083,7 +8139,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
templatedUrl += "/$eventType";
}
} else if (eventType !== null) {
logger.warn(`eventType: ${eventType} ignored when fetching
this.logger.warn(`eventType: ${eventType} ignored when fetching
relations as relationType is null`);
eventType = null;
}
@@ -9375,7 +9431,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
targets.set(userId, Array.from(deviceMessages.keys()));
}
logger.log(`PUT ${path}`, targets);
this.logger.debug(`PUT ${path}`, targets);
return this.http.authedRequest(Method.Put, path, undefined, body);
}
@@ -9634,7 +9690,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @deprecated use supportsThreads() instead
*/
public supportsExperimentalThreads(): boolean {
logger.warn(`supportsExperimentalThreads() is deprecated, use supportThreads() instead`);
this.logger.warn(`supportsExperimentalThreads() is deprecated, use supportThreads() instead`);
return this.clientOpts?.experimentalThreadSupport || false;
}
@@ -9832,3 +9888,66 @@ export function fixNotificationCountOnDecryption(cli: MatrixClient, event: Matri
}
}
}
/**
* Given an event, figure out the thread ID we should use for it in a receipt.
*
* This will either be "main", or event.threadRootId. For the thread root, or
* e.g. reactions to the thread root, this will be main. For events inside the
* thread, or e.g. reactions to them, this will be event.threadRootId.
*
* (Exported for test.)
*/
export function threadIdForReceipt(event: MatrixEvent): string {
return inMainTimelineForReceipt(event) ? MAIN_ROOM_TIMELINE : event.threadRootId!;
}
/**
* a) True for non-threaded messages, thread roots and non-thread relations to thread roots.
* b) False for messages with thread relations to the thread root.
* c) False for messages with any kind of relation to a message from case b.
*
* Note: true for redactions of messages that are in threads. Redacted messages
* are not really in threads (because their relations are gone), so if they look
* like they are in threads, that is a sign of a bug elsewhere. (At time of
* writing, this bug definitely exists - messages are not moved to another
* thread when they are redacted.)
*
* @returns true if this event is considered to be in the main timeline as far
* as receipts are concerned.
*/
function inMainTimelineForReceipt(event: MatrixEvent): boolean {
if (!event.threadRootId) {
// Not in a thread: then it is in the main timeline
return true;
}
if (event.isThreadRoot) {
// Thread roots are in the main timeline. Note: the spec is ambiguous (or
// wrong) on this - see
// https://github.com/matrix-org/matrix-spec-proposals/pull/4037
return true;
}
if (!event.isRelation()) {
// If it's not related to anything, it can't be related via a chain of
// relations to a thread root.
//
// Note: this is a bug, because how does it have a threadRootId if it is
// neither a thread root, nor related to one?
logger.warn(`Event is not a relation or a thread root, but still has a threadRootId! id=${event.getId()}`);
return true;
}
if (event.isRelation(THREAD_RELATION_TYPE.name)) {
// It's a message in a thread - definitely not in the main timeline.
return false;
}
const isRelatedToRoot = event.relationEventId === event.threadRootId;
// If it's related to the thread root (and we already know it's not a thread
// relation) then it's in the main timeline. If it's related to something
// else, then it's in the thread (because it has a thread ID).
return isRelatedToRoot;
}
+2 -2
View File
@@ -35,8 +35,8 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* symmetry with setGlobalBlacklistUnverifiedDevices but there is currently
* no room-level equivalent for this setting.
*
* @remarks this is here, rather than in `CryptoApi`, because I don't think we're
* going to support it in the rust crypto implementation.
* @remarks This has no effect in Rust Crypto; it exists only for the sake of
* the accessors in MatrixClient.
*/
globalErrorOnUnknownDevices: boolean;
-46
View File
@@ -1,46 +0,0 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Base64 encoding and decoding utility for crypo.
*/
/**
* Encode a typed array of uint8 as base64.
* @param uint8Array - The data to encode.
* @returns The base64.
*/
export function encodeBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return Buffer.from(uint8Array).toString("base64");
}
/**
* Encode a typed array of uint8 as unpadded base64.
* @param uint8Array - The data to encode.
* @returns The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array: ArrayBuffer | Uint8Array): string {
return encodeBase64(uint8Array).replace(/={1,2}$/, "");
}
/**
* Decode a base64 string to a typed array of uint8.
* @param base64 - The base64 to decode.
* @returns The decoded data.
*/
export function decodeBase64(base64: string): Uint8Array {
return Buffer.from(base64, "base64");
}
+21
View File
@@ -39,6 +39,13 @@ export interface CryptoApi {
*/
globalBlacklistUnverifiedDevices: boolean;
/**
* Return the current version of the crypto module.
* For example: `Rust SDK ${versions.matrix_sdk_crypto} (${versions.git_sha}), Vodozemac ${versions.vodozemac}`.
* @returns the formatted version
*/
getVersion(): string;
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
@@ -372,11 +379,25 @@ export interface CryptoApi {
* Store the backup decryption key.
*
* This should be called if the client has received the key from another device via secret sharing (gossiping).
* It is the responsability of the caller to check that the decryption key is valid for the current backup version.
*
* @param key - the backup decryption key
*
* @deprecated prefer the variant with a `version` parameter.
*/
storeSessionBackupPrivateKey(key: Uint8Array): Promise<void>;
/**
* Store the backup decryption key.
*
* This should be called if the client has received the key from another device via secret sharing (gossiping).
* It is the responsability of the caller to check that the decryption key is valid for the given backup version.
*
* @param key - the backup decryption key
* @param version - the backup version corresponding to this decryption key
*/
storeSessionBackupPrivateKey(key: Uint8Array, version: string): Promise<void>;
/**
* Get the current status of key backup.
*
+15 -3
View File
@@ -224,13 +224,25 @@ export enum VerificationPhase {
/** An `m.key.verification.ready` event has been sent or received, indicating the verification request is accepted. */
Ready,
/** An `m.key.verification.start` event has been sent or received, choosing a verification method */
/**
* The verification is in flight.
*
* This means that an `m.key.verification.start` event has been sent or received, choosing a verification method;
* however the verification has not yet completed or been cancelled.
*/
Started,
/** An `m.key.verification.cancel` event has been sent or received at any time before the `done` event, cancelling the verification request */
/**
* An `m.key.verification.cancel` event has been sent or received at any time before the `done` event, cancelling
* the verification request
*/
Cancelled,
/** An `m.key.verification.done` event has been **sent**, completing the verification request. */
/**
* The verification request is complete.
*
* Normally this means that `m.key.verification.done` events have been sent and received.
*/
Done,
}
+2 -1
View File
@@ -19,7 +19,7 @@ limitations under the License.
*/
import type { PkSigning } from "@matrix-org/olm";
import { decodeBase64, encodeBase64, IObject, pkSign, pkVerify } from "./olmlib";
import { IObject, pkSign, pkVerify } from "./olmlib";
import { logger } from "../logger";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
import { decryptAES, encryptAES } from "./aes";
@@ -31,6 +31,7 @@ import { ISignatures } from "../@types/signed";
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
import { ServerSideSecretStorage, SecretStorageKeyDescription } from "../secret-storage";
import { DeviceVerificationStatus, UserVerificationStatus as UserTrustLevel } from "../crypto-api";
import { decodeBase64, encodeBase64 } from "../base64";
// backwards-compatibility re-exports
export { UserTrustLevel };
+15 -15
View File
@@ -16,7 +16,7 @@ limitations under the License.
import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm";
import { logger, PrefixedLogger } from "../logger";
import { logger, Logger } from "../logger";
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
import * as algorithms from "./algorithms";
import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base";
@@ -531,7 +531,7 @@ export class OlmDevice {
}
});
},
logger.withPrefix("[createOutboundSession]"),
logger.getChild("[createOutboundSession]"),
);
return newSessionId!;
}
@@ -588,7 +588,7 @@ export class OlmDevice {
}
});
},
logger.withPrefix("[createInboundSession]"),
logger.getChild("[createInboundSession]"),
);
return result!;
@@ -602,7 +602,7 @@ export class OlmDevice {
* @returns a list of known session ids for the device
*/
public async getSessionIdsForDevice(theirDeviceIdentityKey: string): Promise<string[]> {
const log = logger.withPrefix("[getSessionIdsForDevice]");
const log = logger.getChild("[getSessionIdsForDevice]");
if (theirDeviceIdentityKey in this.sessionsInProgress) {
log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
@@ -642,7 +642,7 @@ export class OlmDevice {
public async getSessionIdForDevice(
theirDeviceIdentityKey: string,
nowait = false,
log?: PrefixedLogger,
log?: Logger,
): Promise<string | null> {
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait, log);
@@ -686,9 +686,9 @@ export class OlmDevice {
public async getSessionInfoForDevice(
deviceIdentityKey: string,
nowait = false,
log = logger,
log: Logger = logger,
): Promise<{ sessionId: string; lastReceivedMessageTs: number; hasReceivedMessage: boolean }[]> {
log = log.withPrefix("[getSessionInfoForDevice]");
log = log.getChild("[getSessionInfoForDevice]");
if (deviceIdentityKey in this.sessionsInProgress && !nowait) {
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
@@ -764,7 +764,7 @@ export class OlmDevice {
this.saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
logger.withPrefix("[encryptMessage]"),
logger.getChild("[encryptMessage]"),
);
return res!;
}
@@ -806,7 +806,7 @@ export class OlmDevice {
this.saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
logger.withPrefix("[decryptMessage]"),
logger.getChild("[decryptMessage]"),
);
return payloadString!;
}
@@ -842,7 +842,7 @@ export class OlmDevice {
matches = sessionInfo.session.matches_inbound(ciphertext);
});
},
logger.withPrefix("[matchesSession]"),
logger.getChild("[matchesSession]"),
);
return matches!;
}
@@ -1142,7 +1142,7 @@ export class OlmDevice {
},
);
},
logger.withPrefix("[addInboundGroupSession]"),
logger.getChild("[addInboundGroupSession]"),
);
}
@@ -1282,7 +1282,7 @@ export class OlmDevice {
};
});
},
logger.withPrefix("[decryptGroupMessage]"),
logger.getChild("[decryptGroupMessage]"),
);
if (error!) {
@@ -1328,7 +1328,7 @@ export class OlmDevice {
}
});
},
logger.withPrefix("[hasInboundSessionKeys]"),
logger.getChild("[hasInboundSessionKeys]"),
);
return result!;
@@ -1398,7 +1398,7 @@ export class OlmDevice {
};
});
},
logger.withPrefix("[getInboundGroupSessionKey]"),
logger.getChild("[getInboundGroupSessionKey]"),
);
return result;
@@ -1443,7 +1443,7 @@ export class OlmDevice {
(txn) => {
result = this.cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
},
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
logger.getChild("[getSharedHistoryInboundGroupSessionsForRoom]"),
);
return result!;
}
+1 -1
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { decodeBase64, encodeBase64 } from "./olmlib";
import { decodeBase64, encodeBase64 } from "../base64";
import { subtleCrypto, crypto, TextEncoder } from "./crypto";
// salt for HKDF, with 8 bytes of zeros
+18 -18
View File
@@ -21,7 +21,7 @@ limitations under the License.
import { v4 as uuidv4 } from "uuid";
import type { IEventDecryptionResult, IMegolmSessionData } from "../../@types/crypto";
import { logger, PrefixedLogger } from "../../logger";
import { logger, Logger } from "../../logger";
import * as olmlib from "../olmlib";
import {
DecryptionAlgorithm,
@@ -246,12 +246,12 @@ export class MegolmEncryption extends EncryptionAlgorithm {
};
protected readonly roomId: string;
private readonly prefixedLogger: PrefixedLogger;
private readonly prefixedLogger: Logger;
public constructor(params: IParams & Required<Pick<IParams, "roomId">>) {
super(params);
this.roomId = params.roomId;
this.prefixedLogger = logger.withPrefix(`[${this.roomId} encryption]`);
this.prefixedLogger = logger.getChild(`[${this.roomId} encryption]`);
this.sessionRotationPeriodMsgs = params.config?.rotation_period_msgs ?? 100;
this.sessionRotationPeriodMs = params.config?.rotation_period_ms ?? 7 * 24 * 3600 * 1000;
@@ -333,7 +333,7 @@ export class MegolmEncryption extends EncryptionAlgorithm {
// need to make a brand new session?
if (session?.needsRotation(this.sessionRotationPeriodMsgs, this.sessionRotationPeriodMs)) {
this.prefixedLogger.log("Starting new megolm session because we need to rotate.");
this.prefixedLogger.debug("Starting new megolm session because we need to rotate.");
session = null;
}
@@ -343,9 +343,9 @@ export class MegolmEncryption extends EncryptionAlgorithm {
}
if (!session) {
this.prefixedLogger.log("Starting new megolm session");
this.prefixedLogger.debug("Starting new megolm session");
session = await this.prepareNewSession(sharedHistory);
this.prefixedLogger.log(`Started new megolm session ${session.sessionId}`);
this.prefixedLogger.debug(`Started new megolm session ${session.sessionId}`);
this.outboundSessions[session.sessionId] = session;
}
@@ -968,12 +968,12 @@ export class MegolmEncryption extends EncryptionAlgorithm {
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
await this.sendBlockedNotificationsToDevices(session, userDeviceMaps[i], payload);
this.prefixedLogger.log(
this.prefixedLogger.debug(
`Completed blacklist notification for ${session.sessionId} ` +
`(slice ${i + 1}/${userDeviceMaps.length})`,
);
} catch (e) {
this.prefixedLogger.log(
this.prefixedLogger.debug(
`blacklist notification for ${session.sessionId} ` +
`(slice ${i + 1}/${userDeviceMaps.length}) failed`,
);
@@ -1054,7 +1054,7 @@ export class MegolmEncryption extends EncryptionAlgorithm {
* @returns Promise which resolves to the new event body
*/
public async encryptMessage(room: Room, eventType: string, content: IContent): Promise<IMegolmEncryptedContent> {
this.prefixedLogger.log("Starting to encrypt event");
this.prefixedLogger.debug("Starting to encrypt event");
if (this.encryptionPreparation != null) {
// If we started sending keys, wait for it to be done.
@@ -1291,12 +1291,12 @@ export class MegolmDecryption extends DecryptionAlgorithm {
private olmlib = olmlib;
protected readonly roomId: string;
private readonly prefixedLogger: PrefixedLogger;
private readonly prefixedLogger: Logger;
public constructor(params: DecryptionClassParams<IParams & Required<Pick<IParams, "roomId">>>) {
super(params);
this.roomId = params.roomId;
this.prefixedLogger = logger.withPrefix(`[${this.roomId} decryption]`);
this.prefixedLogger = logger.getChild(`[${this.roomId} decryption]`);
}
/**
@@ -1740,7 +1740,7 @@ export class MegolmDecryption extends DecryptionAlgorithm {
"readwrite",
["parked_shared_history"],
(txn) => this.crypto.cryptoStore.addParkedSharedHistory(roomKey.roomId, parkedData, txn),
logger.withPrefix("[addParkedSharedHistory]"),
logger.getChild("[addParkedSharedHistory]"),
);
}
@@ -1955,7 +1955,7 @@ export class MegolmDecryption extends DecryptionAlgorithm {
return null;
}
this.prefixedLogger.log(
this.prefixedLogger.debug(
"sharing keys for session " +
body.sender_key +
"|" +
@@ -2051,7 +2051,7 @@ export class MegolmDecryption extends DecryptionAlgorithm {
this.crypto.backupManager.backupGroupSession(session.sender_key, session.session_id).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
this.prefixedLogger.log("Failed to back up megolm session", e);
this.prefixedLogger.debug("Failed to back up megolm session", e);
});
}
// have another go at decrypting events sent with this session.
@@ -2135,7 +2135,7 @@ export class MegolmDecryption extends DecryptionAlgorithm {
await olmlib.ensureOlmSessionsForDevices(this.olmDevice, this.baseApis, devicesByUser);
const sharedHistorySessions = await this.olmDevice.getSharedHistoryInboundGroupSessions(this.roomId);
this.prefixedLogger.log(
this.prefixedLogger.debug(
`Sharing history in with users ${Array.from(devicesByUser.keys())}`,
sharedHistorySessions.map(([senderKey, sessionId]) => `${senderKey}|${sessionId}`),
);
@@ -2178,20 +2178,20 @@ export class MegolmDecryption extends DecryptionAlgorithm {
for (const [userId, deviceMessages] of contentMap) {
for (const [deviceId, content] of deviceMessages) {
if (!hasCiphertext(content)) {
this.prefixedLogger.log("No ciphertext for device " + userId + ":" + deviceId + ": pruning");
this.prefixedLogger.debug("No ciphertext for device " + userId + ":" + deviceId + ": pruning");
deviceMessages.delete(deviceId);
}
}
// No devices left for that user? Strip that too.
if (deviceMessages.size === 0) {
this.prefixedLogger.log("Pruned all devices for user " + userId);
this.prefixedLogger.debug("Pruned all devices for user " + userId);
contentMap.delete(userId);
}
}
// Is there anything left?
if (contentMap.size === 0) {
this.prefixedLogger.log("No users left to send to: aborting");
this.prefixedLogger.debug("No users left to send to: aborting");
return;
}
-1
View File
@@ -480,7 +480,6 @@ export class BackupManager {
const delay = Math.random() * maxDelay;
await sleep(delay);
if (!this.clientRunning) {
logger.debug("Key backup send aborted, client stopped");
this.sendingBackups = false;
return;
}
+3 -3
View File
@@ -16,9 +16,9 @@ limitations under the License.
import { logger } from "../logger";
export let crypto = global.window?.crypto;
export let subtleCrypto = global.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
export let TextEncoder = global.window?.TextEncoder;
export let crypto = globalThis.window?.crypto;
export let subtleCrypto = globalThis.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
export let TextEncoder = globalThis.window?.TextEncoder;
/* eslint-disable @typescript-eslint/no-var-requires */
if (!crypto) {
+1 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
import anotherjson from "another-json";
import type { IDeviceKeys, IOneTimeKey } from "../@types/crypto";
import { decodeBase64, encodeBase64 } from "./olmlib";
import { decodeBase64, encodeBase64 } from "../base64";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
import { decryptAES, encryptAES } from "./aes";
import { logger } from "../logger";
+30 -13
View File
@@ -102,6 +102,7 @@ import {
import { Device, DeviceMap } from "../models/device";
import { deviceInfoToDevice } from "./device-converter";
import { ClientPrefix, MatrixError, Method } from "../http-api";
import { decodeBase64, encodeBase64 } from "../base64";
/* re-exports for backwards compatibility */
export type {
@@ -134,7 +135,7 @@ export const verificationMethods = {
export type VerificationMethod = keyof typeof verificationMethods | string;
export function isCryptoAvailable(): boolean {
return Boolean(global.Olm);
return Boolean(globalThis.Olm);
}
// minimum time between attempting to unwedge an Olm session, if we succeeded
@@ -500,7 +501,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
await this.secretStorage.store("m.megolm_backup.v1", fixedKey, [keys![0]]);
}
return olmlib.decodeBase64(fixedKey || storedKey);
return decodeBase64(fixedKey || storedKey);
}
// try to get key from app
@@ -609,6 +610,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
this.backupManager.checkAndStart();
}
/**
* Implementation of {@link CryptoApi#getVersion}.
*/
public getVersion(): string {
const olmVersionTuple = Crypto.getOlmVersion();
return `Olm ${olmVersionTuple[0]}.${olmVersionTuple[1]}.${olmVersionTuple[2]}`;
}
/**
* Whether to trust a others users signatures of their devices.
* If false, devices will only be considered 'verified' if we have
@@ -1050,7 +1059,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
newKeyId = await createSSSS(opts, backupKey);
// store the backup key in secret storage
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey!), [newKeyId]);
await secretStorage.store("m.megolm_backup.v1", 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
@@ -1094,7 +1103,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
);
// write the key to 4S
const privateKey = decodeRecoveryKey(info.recovery_key);
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
await secretStorage.store("m.megolm_backup.v1", encodeBase64(privateKey));
// create keyBackupInfo object to add to builder
const data: IKeyBackupInfo = {
@@ -1122,7 +1131,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const keyId = newKeyId || oldKeyId;
await secretStorage.store("m.megolm_backup.v1", fixedBackupKey, keyId ? [keyId] : null);
}
const decodedBackupKey = new Uint8Array(olmlib.decodeBase64(fixedBackupKey || sessionBackupKey));
const decodedBackupKey = new Uint8Array(decodeBase64(fixedBackupKey || sessionBackupKey));
builder.addSessionBackupPrivateKeyToCache(decodedBackupKey);
} else if (this.backupManager.getKeyBackupEnabled()) {
// key backup is enabled but we don't have a session backup key in SSSS: see if we have one in
@@ -1137,7 +1146,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
return;
}
logger.info("Got session backup key from cache/user that wasn't in SSSS: saving to SSSS");
await secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(backupKey));
await secretStorage.store("m.megolm_backup.v1", encodeBase64(backupKey));
}
const operation = builder.buildOperation();
@@ -1178,7 +1187,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// write the key to 4S
const privateKey = info.privateKey;
await this.secretStorage.store("m.megolm_backup.v1", olmlib.encodeBase64(privateKey));
await this.secretStorage.store("m.megolm_backup.v1", encodeBase64(privateKey));
await this.storeSessionBackupPrivateKey(privateKey);
await this.backupManager.checkAndStart();
@@ -1301,13 +1310,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// make sure we have a Uint8Array, rather than a string
if (typeof encodedKey === "string") {
key = new Uint8Array(olmlib.decodeBase64(fixBackupKey(encodedKey) || encodedKey));
key = new Uint8Array(decodeBase64(fixBackupKey(encodedKey) || encodedKey));
await this.storeSessionBackupPrivateKey(key);
}
if (encodedKey && typeof encodedKey === "object" && "ciphertext" in encodedKey) {
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const decrypted = await decryptAES(encodedKey, pickleKey, "m.megolm_backup.v1");
key = olmlib.decodeBase64(decrypted);
key = decodeBase64(decrypted);
}
return key;
}
@@ -1317,13 +1326,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
* @param key - the private key
* @returns a promise so you can catch failures
*/
public async storeSessionBackupPrivateKey(key: ArrayLike<number>): Promise<void> {
public async storeSessionBackupPrivateKey(key: ArrayLike<number>, version?: string): Promise<void> {
if (!(key instanceof Uint8Array)) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
throw new Error(`storeSessionBackupPrivateKey expects Uint8Array, got ${key}`);
}
const pickleKey = Buffer.from(this.olmDevice.pickleKey);
const encryptedKey = await encryptAES(olmlib.encodeBase64(key), pickleKey, "m.megolm_backup.v1");
const encryptedKey = await encryptAES(encodeBase64(key), pickleKey, "m.megolm_backup.v1");
return this.cryptoStore.doTxn("readwrite", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this.cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", encryptedKey);
});
@@ -2738,6 +2747,8 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
const senderId = event.getSender();
if (!senderId || encryptionInfo.mismatchedSender) {
// something definitely wrong is going on here
// previously: E2EState.Warning -> E2ePadlockUnverified -> Red/"Encrypted by an unverified session"
return {
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.MISMATCHED_SENDER_KEY,
@@ -2750,11 +2761,13 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
// shield, otherwise if the user isn't cross-signed then
// nothing's needed
if (!encryptionInfo.authenticated) {
// previously: E2EState.Unauthenticated -> E2ePadlockUnauthenticated -> Grey/"The authenticity of this encrypted message can't be guaranteed on this device."
return {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
};
} else {
// previously: E2EState.Normal -> no icon
return { shieldColour: EventShieldColour.NONE, shieldReason: null };
}
}
@@ -2765,6 +2778,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
(await this.getDeviceVerificationStatus(senderId, encryptionInfo.sender.deviceId));
if (!eventSenderTrust) {
// previously: E2EState.Unknown -> E2ePadlockUnknown -> Grey/"Encrypted by a deleted session"
return {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.UNKNOWN_DEVICE,
@@ -2772,19 +2786,22 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
if (!eventSenderTrust.isVerified()) {
// previously: E2EState.Warning -> E2ePadlockUnverified -> Red/"Encrypted by an unverified session"
return {
shieldColour: EventShieldColour.RED,
shieldReason: EventShieldReason.UNVERIFIED_IDENTITY,
shieldReason: EventShieldReason.UNSIGNED_DEVICE,
};
}
if (!encryptionInfo.authenticated) {
// previously: E2EState.Unauthenticated -> E2ePadlockUnauthenticated -> Grey/"The authenticity of this encrypted message can't be guaranteed on this device."
return {
shieldColour: EventShieldColour.GREY,
shieldReason: EventShieldReason.AUTHENTICITY_NOT_GUARANTEED,
};
}
// previously: E2EState.Verified -> no icon
return { shieldColour: EventShieldColour.NONE, shieldReason: null };
}
@@ -4188,7 +4205,7 @@ export function fixBackupKey(key?: string): string | null {
return null;
}
const fixedKey = Uint8Array.from(key.split(","), (x) => parseInt(x));
return olmlib.encodeBase64(fixedKey);
return encodeBase64(fixedKey);
}
/**

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