Compare commits

...

90 Commits

Author SHA1 Message Date
Michael Telatynski 14b2ee2da4 Remove dependency on uuid (#5297)
Notify Downstream Projects / notify-downstream (element-web-notify, element-hq/element-web) (push) Has been skipped
Static Analysis / Typescript Syntax Check (push) Failing after 1m36s
Static Analysis / ESLint (push) Failing after 31s
Static Analysis / Node.js example (push) Failing after 42s
Static Analysis / Workflow Lint (push) Failing after 9m59s
Static Analysis / JSDoc Checker (push) Failing after 50s
Static Analysis / Analyse Dead Code (push) Failing after 36s
Static Analysis / Downstream tsc element-web (push) Has been skipped
Tests / Vitest [integ] (Node 22) (push) Failing after 4s
Tests / Vitest [unit] (Node 22) (push) Failing after 39s
Tests / Vitest [integ] (Node lts/*) (push) Failing after 39s
Tests / Vitest [unit] (Node lts/*) (push) Failing after 35s
Tests / Downstream test element-web (push) Has been skipped
Tests / Run Complement Crypto tests (push) Has been skipped
Static Analysis / Static Analysis (push) Successful in 1s
Tests / Tests (push) Failing after 1s
Tests / Downstream Complement Crypto tests (push) Successful in 1s
Tests / Downstream tests (push) Successful in 4s
2026-04-23 09:53:18 +00:00
renovate[bot] bb083222d9 Update dependency vite to v8 (#5295)
* Update dependency vite to v8

* Update lockfile

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-04-22 16:16:29 +00:00
Andy Balaam fa424c44b4 Support stable identifiers for MSC4268 (#5290)
* Support stable identifier m.room_key_bundle

* Support stable identifier m.shared_history

* Test that checks isRoomKeyBundleMessage works for stable and unstable identifiers

* Replace similar tests with use of it.each
2026-04-22 09:10:39 +00:00
renovate[bot] fef093747e Update dependency @typescript-eslint/eslint-plugin to v8.59.0 (#5292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 14:38:05 +00:00
renovate[bot] 4b33892d48 Update npm non-major dependencies (#5293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 13:58:22 +00:00
renovate[bot] d7d771fadb Update vite to v4.1.5 (#5291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 12:37:38 +00:00
Hubert Chathi 4ee3e591bf Handle secret pushing for key backups (#5189)
* push backup key to other verified devices when we reset backup

* handle receiving pushed backup keys

- make sure that backup gets enabled after we receive a pushed key that
  matches the current, valid backup

* apply requested changes from review
2026-04-20 21:48:51 +00:00
renovate[bot] 668183d722 Update vite to v4.1.4 (#5289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 11:25:37 +00:00
renovate[bot] 854dae0dc0 Update typescript (#5288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 10:01:43 +00:00
renovate[bot] 9d1aca2232 Update eslint-plugins (#5286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 09:11:28 +00:00
renovate[bot] 81569f3461 Update actions/setup-node digest to 48b55a0 (#5285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 08:50:10 +00:00
renovate[bot] 50783aba76 Update npm non-major dependencies (#5287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-20 08:17:20 +00:00
Michael Telatynski fd01b17236 Tidy knip config (#5284) 2026-04-17 13:00:21 +00:00
Will Hunt 25e92009b7 Throw more helpful errors from indexedb-local-backend (#5282)
* Throw more helpful errors from indexedb-local-backend

* Add a test

* Do not tempt fate
2026-04-17 08:33:56 +00:00
Michael Telatynski eb7acfb810 Add support for m.recent_emoji account data event (#5280) 2026-04-16 21:59:30 +00:00
Andy Balaam ca5655bced Rotate the room key when anyone leaves a room for any reason (#5279) 2026-04-16 12:09:42 +00:00
Michael Telatynski ef9b13e2a6 Fix coverage reports clobbering each other (#5267)
merge-multiple would silently drop files with clashing names - it ultimately isn't necessary given the `find` command will happily find them in nested subdirs
2026-04-16 11:48:51 +00:00
fkwp 159cca0363 Adapt LiveKit Identity hash calculation to latest MSC4195 update (#5268)
* adapt hash calculation to latest MSC4195 update.

Stop using '|' delimiters in hashes; use JSON arrays + canonical JSON instead

Signed-off-by: fkwp <github-fkwp@w4ve.de>

* Update the test for RTC backend identities and add tests for calculating the identity hash.

Signed-off-by: fkwp <github-fkwp@w4ve.de>

* linting

Signed-off-by: fkwp <github-fkwp@w4ve.de>

* add copyright header

---------

Signed-off-by: fkwp <github-fkwp@w4ve.de>
2026-04-16 09:48:11 +00:00
renovate[bot] 3879111850 Update typescript (#5274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 11:37:50 +00:00
Andy Balaam f9a5aa87e3 Support the stable prefix for MSC4287 (key backup preference) (#5258)
* Support the stable prefix for MSC4287 (key backup preference)

* Remove incorrect doc coment on disableKeyStorage
2026-04-15 11:07:48 +00:00
renovate[bot] ed58df040c Update eslint-plugins (#5276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 09:51:25 +00:00
Michael Telatynski 0a3448d4c9 Fix references to matrix-react-sdk in CI (#5269) 2026-04-15 09:25:54 +00:00
renovate[bot] 25c1c1ea26 Update actions/github-script action to v9 (#5277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 09:11:09 +00:00
renovate[bot] a5e67af31f Update actions/upload-pages-artifact action to v5 (#5278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:37:38 +00:00
renovate[bot] b91e80814a Update zizmorcore/zizmor-action action to v0.5.3 (#5275)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:37:07 +00:00
renovate[bot] d096a72605 Update npm non-major dependencies (#5273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:36:56 +00:00
renovate[bot] fb547e7b4b Update actions/upload-artifact digest to 043fb46 (#5272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 08:36:46 +00:00
Michael Telatynski 815294ca5a Allow oidc jwks_uri to be omitted (#5271)
* Allow oidc jwks_uri to be null

As signing keys are seemingly not (yet?) in the spec

* Add test coverage
2026-04-15 08:15:29 +00:00
Michael Telatynski 6d270b4685 Handle response_mode=fragment in completeAuthorizationCodeGrant (#5266)
* Handle response_mode=fragment in completeAuthorizationCodeGrant

* Add test

* Fix docs
2026-04-14 14:59:10 +00:00
Michael Telatynski 8bc3d96f6b Allow generating OIDC URIs with response_mode=fragment (#5265) 2026-04-13 22:53:05 +00:00
Richard van der Hoff b6ea6e105e Log clarifications for handleBackupSecretReceived (#5233)
Some changes to make this a bit easier to understand.
2026-04-10 12:33:30 +00:00
Valere Fedronic cd4e053fa5 Suppress and Reduce noisy logs for rtc (#5260) 2026-04-09 14:24:36 +00:00
Andy Balaam 727473af62 Expand the comment on CryptoApi.getUserDeviceInfo saying we request info from the server (#5256)
* Expand the comment on CryptoApi.getUserDeviceInfo saying we request info from the server

* Update comment to reflect waiting for in-progress requests, not making new ones

* Update the comment for userHasCrossSigningKeys too
2026-04-09 14:19:22 +00:00
Michael Telatynski f17f013f1e Satisfy pnpm audit (#5262) 2026-04-09 11:52:19 +00:00
renovate[bot] 9f4ab0b840 Update matrix-org/sonarcloud-workflow-action digest to 13968a2 (#5263)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 13:28:46 +00:00
Michael Telatynski 6371e4b252 Update pnpm command for version bumping 2026-04-08 08:23:52 +01:00
Michael Telatynski b69929e01a Allow release-make to bump multiple package.json versions (#5261) 2026-04-07 14:44:15 +00:00
RiotRobot 9dc12baaa9 Merge branch 'master' into develop 2026-04-07 13:17:46 +00:00
RiotRobot 159738597d v41.3.0 2026-04-07 13:17:11 +00:00
Andy Balaam d02205652f Re-enable Complement Crypto tests (#5257)
Since https://github.com/matrix-org/complement-crypto/pull/235 these
should be more reliable.

This reverts commit 4d59291538.
2026-04-01 12:38:11 +00:00
Richard van der Hoff 5e03add29a Expose UserVerificationStatus.known flag (#5255)
Indicate whether we have a record of this user's identity.
2026-04-01 09:58:50 +00:00
renovate[bot] eeafd7fcaa Update npm non-major dependencies (#5251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 16:36:53 +00:00
renovate[bot] 78a3c5372d Update vite (#5253)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:58:28 +00:00
renovate[bot] c0c3bc2a8c Update dependency p-retry to v8 (#5254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:28:47 +00:00
renovate[bot] c3ce49cabf Update pnpm to v10.33.0 (#5252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:19:11 +00:00
renovate[bot] 5408168dfd Update dependency eslint-plugin-jsdoc to v62.8.1 (#5250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:17:51 +00:00
renovate[bot] 61452ddc11 Update pnpm/action-setup action to v5 (#5239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-31 13:11:15 +00:00
Michael Telatynski d5160a5380 Add permissions for contents in workflow 2026-03-31 14:36:05 +01:00
Michael Telatynski 7ff4960a27 Update workflow to use build-and-test.yaml from EW
Moved in https://github.com/element-hq/element-web/pull/32929
2026-03-31 14:34:06 +01:00
RiotRobot 00f63db80f v41.3.0-rc.0 2026-03-31 12:33:30 +00:00
dependabot[bot] 9bcb83a20a Bump smol-toml from 1.6.0 to 1.6.1 (#5249)
Bumps [smol-toml](https://github.com/squirrelchat/smol-toml) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/squirrelchat/smol-toml/releases)
- [Commits](https://github.com/squirrelchat/smol-toml/compare/v1.6.0...v1.6.1)

---
updated-dependencies:
- dependency-name: smol-toml
  dependency-version: 1.6.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:59:06 +00:00
dependabot[bot] dd8d8e5410 Bump brace-expansion from 1.1.12 to 1.1.13 (#5248)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.13.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.13)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:41:40 +00:00
dependabot[bot] 32b8ff8116 Bump minimatch from 3.1.2 to 3.1.5 (#5247)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 11:40:24 +00:00
Skye Elliot 3ceadd512d Refactor history sharing tests using setupClients helper (#5235)
* tests: Refactor history sharing tests using `setupClients` helper

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* tests: Use separate destructors for test clients

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-30 11:13:37 +00:00
renovate[bot] 8182180550 Update dependency happy-dom to v20.8.9 [SECURITY] (#5244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-30 09:09:17 +00:00
renovate[bot] aed74c5a72 Update dependency happy-dom to v20.8.8 [SECURITY] (#5243)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-27 20:09:41 +00:00
renovate[bot] 8c259c53a6 Update actions/deploy-pages action to v5 (#5238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 20:06:08 +00:00
Michael Telatynski 4d59291538 Disable Complement Crypto tests (#5242)
Disabled Complement Crypto tests due to flakiness.
2026-03-27 17:38:37 +00:00
Michael Telatynski 80009a1b31 Add support for non-root package.json for version calculation in Sonar workflow (#5240)
* Add support for non-root package.json for version calculation in Sonar workflow

* Fix version_cmd
2026-03-27 09:16:01 +00:00
renovate[bot] 93e6c95953 Update dependency typescript to v6 (#5234)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 03:16:33 +00:00
renovate[bot] 27a5507cef Update dependency knip to v6 (#5237)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-26 02:41:41 +00:00
renovate[bot] be06f6655e Update dependency typedoc to v0.28.18 (#5236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-25 22:56:30 +00:00
Skye Elliot 71152f33bf Rotate the current room key when we see a member leave (#5231)
* feat: Rotate room key when a member leaves the room

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* test: Assert room key rotated to prevent MSC4268 leaking keys

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* docs: Outline key rotation scenario above discard logic

* feat: Use `RoomStateEvents.Events` over membership event

* docs: Correct spelling in scenario explanation

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

* docs: Pull scenario explanation up to `onRoomStateEvent`

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* tests: Assert room key is rotated under leave va gappy sync

* tests: Build sync response incrementally for gappy/ungappy sync

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-25 16:39:58 +00:00
RiotRobot bc8f67089c Merge branch 'master' into develop 2026-03-24 11:22:32 +00:00
RiotRobot acc9aa8939 v41.2.0 2026-03-24 11:21:57 +00:00
Richard van der Hoff e76f627fe3 Add some docs to the DeviceIsolationModes (#5232)
* Add some docs to the DeviceIsolationModes

Notes to help us/me remember how these relate to MSC4153.

* Apply suggestions from code review

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2026-03-23 15:29:35 +00:00
renovate[bot] 45b1e73842 Update npm non-major dependencies (#5229)
* Update npm non-major dependencies

* Update test

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>
2026-03-20 09:58:04 +00:00
renovate[bot] f3eefd2f32 Update element-hq/element-meta digest to 7f2f93f (#5222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 09:27:19 +00:00
renovate[bot] f7c053216b Update dependency eslint-plugin-jsdoc to v62.8.0 (#5228)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 09:18:31 +00:00
renovate[bot] 9c7739f14f Update actions/download-artifact digest to 3e5f45b (#5220)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-20 09:10:38 +00:00
renovate[bot] 897afe153a Update pnpm/action-setup digest to fc06bc1 (#5225)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 19:03:49 +00:00
renovate[bot] a929391dcd Update shogo82148/actions-upload-release-asset digest to 96bc1f0 (#5226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 18:06:53 +00:00
renovate[bot] 45c5ee9f65 Update actions/setup-node digest to 53b8394 (#5221)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 16:11:41 +00:00
renovate[bot] e56aaa16c7 Update mheap/github-action-required-labels digest to 0ac283b (#5224)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 15:47:21 +00:00
renovate[bot] da0d3d791e Update element-hq/element-web digest to 9730933 (#5223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 15:04:32 +00:00
renovate[bot] ed5eb670a1 Update zizmorcore/zizmor-action action to v0.5.2 (#5227)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 14:27:14 +00:00
renovate[bot] b7fcb6e4c1 Update pnpm to v10.32.1 (#5230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-17 14:19:06 +00:00
RiotRobot bd775f6b61 v41.2.0-rc.0 2026-03-17 11:39:31 +00:00
Skye Elliot c2f9ad28fc Only share history if room history visibility is shared (#5216)
* feat: Only share history if room history visibility is shared

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

* docs: Update documentation for `InviteOpts.shareEncryptedHistory`

* tests: Ensure shared history respects current history visibility

This commit additionally modifies `expectSendRoomEvent` to remove
the matcher on success, since fetchmock takes a while to do this
automatically.

Signed-off-by: Skye Elliot <actuallyori@gmail.com>

---------

Signed-off-by: Skye Elliot <actuallyori@gmail.com>
2026-03-16 13:03:41 +00:00
Richard van der Hoff 7f33e3462e History sharing: resume key-bundle import on restart (#5214)
* Store rooms pending key bundles in the CryptoStore

Replace the in-memory storage of which rooms are waiting for a key bundle with
permanent storage in the crypto store.

* Clear pending-key-bundle flag on malformed bundles

If we cannot import the key bundle, there is no point trying again another
time: we may as well clear the flag either way.

* Factor out some helpers in history sharing integ test

* Do not accept key bundles for rooms we joined more than 24h ago

Per discussion in crypto-internal.

* Clear pending key bundle data when we leave a room

* Resume key-bundle import on restart

* Clear pending-key-bundle flag on rooms that we joined ages ago

* fixup! Clear pending-key-bundle flag on malformed bundles
2026-03-16 11:58:36 +00:00
Richard van der Hoff d99363d288 Move shareRoomHistoryWithUser to CryptoBackend (#5218)
There is no need for this method to be exposed to the application, and it's a
footgun waiting to trap the unwary user.

It's marked `@experimental` so we're allowed to move it without a major version
bump.
2026-03-12 21:21:31 +00:00
renovate[bot] 3642b99212 Update dependency @matrix-org/matrix-sdk-crypto-wasm to v18 (#5217)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v18

* Adapt to breaking changes in rust-sdk wasm bindings

* more types fixes

* types fixes for tests

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Richard van der Hoff <richard@matrix.org>
2026-03-12 18:16:44 +00:00
Valere Fedronic 6ec0987286 re export sticky event types (#5213)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2026-03-12 17:27:11 +00:00
RiotRobot 219eb617dc Merge branch 'master' into develop 2026-03-10 13:46:12 +00:00
Michael Telatynski 5d0e2efaf3 Add zizmor CI & make it happy (#5212)
* Add zizmor CI & make it happy

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

* Fix additional zizmor warning

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2026-03-04 09:27:47 +00:00
renovate[bot] c7cd5570d3 Update eslint-plugins (#5207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 14:01:28 +00:00
renovate[bot] c2f6dd2ce0 Update crazy-max/ghaction-import-gpg action to v7 (#5210)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:24:07 +00:00
renovate[bot] 393732aaae Update npm non-major dependencies (#5208)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:23:47 +00:00
renovate[bot] d373fd8540 Update GitHub Artifact Actions (#5211)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:22:31 +00:00
renovate[bot] 44a8a9a47a Update pnpm to v10.30.3 (#5209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 13:22:19 +00:00
55 changed files with 3367 additions and 1623 deletions
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ env.VERSION }}.tar.gz.asc
@@ -29,13 +29,13 @@ runs:
- name: Upload asset signatures
if: inputs.gpg-fingerprint
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@8f6863c6c894ba46f9e676ef5cccec4752723c1e # v1
uses: shogo82148/actions-upload-release-asset@96bc1f0cb850b65efd58a6b5eaa0a69f88d38077 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
+3 -1
View File
@@ -1,6 +1,8 @@
name: Backport
on:
pull_request_target:
# Privilege escalation necessary to enable backporting PRs from forks
# 🚨 We must not execute any checked out code here.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types:
- closed
- labeled
+4 -2
View File
@@ -1,7 +1,9 @@
name: Deploy documentation PR preview
on:
workflow_run:
# Privilege escalation necessary to publish to Netlify
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["Static Analysis"]
types:
- completed
@@ -15,7 +17,7 @@ jobs:
deployments: write
steps:
- name: 📥 Download artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
@@ -1,7 +1,7 @@
# Triggers after the "Downstream artifacts" build has finished, to run the
# matrix-react-sdk playwright tests (with access to repo secrets)
# element-web playwright tests (with access to repo secrets)
name: matrix-react-sdk End to End Tests
name: Element Web End to End Tests
on:
merge_group:
types: [checks_requested]
@@ -21,11 +21,12 @@ concurrency:
jobs:
playwright:
name: Playwright
uses: element-hq/element-web/.github/workflows/end-to-end-tests.yaml@develop
uses: element-hq/element-web/.github/workflows/build-and-test.yaml@develop # zizmor: ignore[unpinned-uses]
permissions:
actions: read
issues: read
pull-requests: read
contents: read
with:
matrix-js-sdk-sha: ${{ github.sha }}
# We only want to run the playwright tests on merge queue to prevent regressions
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Notify matrix-react-sdk repo that a new SDK build is on develop so it can CI against it
- name: Notify element-web repo that a new SDK build is on develop so it can CI against it
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+8 -5
View File
@@ -1,6 +1,9 @@
name: Pull Request
on:
pull_request_target:
# Privilege escalation necessary access members of the review teams
# 🚨 We must not execute any checked out code here, and be careful around use of user-controlled inputs.
# FIXME: only `community-prs` job needs this privilege, so it should be in its own workflow file.
pull_request_target: # zizmor: ignore[dangerous-triggers]
types: [opened, edited, labeled, unlabeled, synchronize]
merge_group:
types: [checks_requested]
@@ -15,7 +18,7 @@ jobs:
name: Preview Changelog
runs-on: ubuntu-24.04
steps:
- uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5
- uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5
if: github.event_name != 'merge_group'
with:
labels: |
@@ -35,7 +38,7 @@ jobs:
pull-requests: read
steps:
- name: Add notice
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
if: contains(github.event.pull_request.labels.*.name, 'X-Blocked')
with:
script: |
@@ -60,7 +63,7 @@ jobs:
- name: Add label
if: steps.teams.outputs.isTeamMember == 'false'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
github.rest.issues.addLabels({
@@ -81,7 +84,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Close pull request
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
github.rest.issues.createComment({
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Check for X-Release-Blocker label on any open issues or PRs
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
REPO: ${{ inputs.repository }}
with:
@@ -20,9 +20,10 @@ jobs:
with:
ref: staging
fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version-file: package.json
cache: "pnpm"
@@ -49,7 +50,7 @@ jobs:
- name: Ingest upstream changes
if: inputs.include-changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
draft:
permissions:
contents: write
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-drafter-workflow.yml@develop # zizmor: ignore[unpinned-uses]
+3 -2
View File
@@ -27,6 +27,7 @@ jobs:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
persist-credentials: true
- name: Get actions scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -37,8 +38,8 @@ jobs:
sparse-checkout: |
scripts/release
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
+28 -20
View File
@@ -33,10 +33,14 @@ on:
description: The number of expected assets, including signatures, excluding generated zip & tarball.
type: number
required: false
dir:
dist-dir:
description: The directory to release
type: string
default: "."
version-dirs:
description: Directories in which to update package.json `version` field
type: string
required: false
outputs:
npm-id:
description: "The npm package@version string we published"
@@ -48,7 +52,7 @@ jobs:
permissions:
issues: read
pull-requests: read
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop # zizmor: ignore[unpinned-uses]
release:
name: Release
@@ -61,7 +65,7 @@ jobs:
- name: Load GPG key
id: gpg
if: inputs.gpg-fingerprint
uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec # v6
uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
@@ -69,7 +73,7 @@ jobs:
- name: Get draft release
id: draft-release
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # 1.2.5
env:
GITHUB_TOKEN: ${{ github.token }}
with:
@@ -82,6 +86,7 @@ jobs:
# We will be pushing to this branch and want the CI to run after we do so we cannot use the GITHUB_TOKEN
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
fetch-depth: 0
persist-credentials: true
- name: Get actions scripts
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
@@ -95,7 +100,7 @@ jobs:
- name: Prepare variables
id: prepare
working-directory: ${{ inputs.dir }}
working-directory: ${{ inputs.dist-dir }}
run: |
echo "VERSION=$VERSION" >> $GITHUB_ENV
@@ -110,7 +115,7 @@ jobs:
run: echo "VERSION=$(echo $VERSION | cut -d- -f1)" >> $GITHUB_ENV
- name: Check version number not in use
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
with:
script: |
const { VERSION } = process.env;
@@ -129,17 +134,17 @@ jobs:
git config --global user.email "releases@riot.im"
git config --global user.name "RiotRobot"
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: ${{ inputs.dir }}/package.json
node-version-file: ${{ inputs.dist-dir }}/package.json
- name: Install dependencies
run: "pnpm install --frozen-lockfile"
- name: Handle develop dependencies
working-directory: ${{ inputs.dir }}
working-directory: ${{ inputs.dist-dir }}
run: |
ret=0
cat package.json | jq -r '.dependencies | to_entries | .[] | "\(.key) \(.value)"' | grep '#develop$' | while read -r dep ; do
@@ -153,11 +158,14 @@ jobs:
git commit -m "Keep $PACKAGE at $VERSION"
done
- name: Bump package.json version
working-directory: ${{ inputs.dir }}
- name: Bump package.json versions
run: |
pnpm version --no-git-tag-version "${VERSION#v}"
git add package.json
for DIR in $DIRS; do
pnpm version -C "$DIR" --no-git-tag-version "${VERSION#v}"
git add "$DIR"/package.json
done
env:
DIRS: ${{ inputs.version-dirs || inputs.dist-dir }}
- name: Add to CHANGELOG.md
if: inputs.final
@@ -184,7 +192,7 @@ jobs:
- name: Build assets
if: steps.prepare.outputs.has-dist-script == '1'
working-directory: ${{ inputs.dir }}
working-directory: ${{ inputs.dist-dir }}
run: DIST_VERSION="$VERSION" pnpm dist
- name: Upload release assets & signatures
@@ -193,7 +201,7 @@ jobs:
with:
gpg-fingerprint: ${{ inputs.gpg-fingerprint }}
upload-url: ${{ steps.draft-release.outputs.upload_url }}
asset-path: ${{ inputs.dir }}/${{ inputs.asset-path }}
asset-path: ${{ inputs.dist-dir }}/${{ inputs.asset-path }}
- name: Create signed tag
if: inputs.gpg-fingerprint
@@ -226,7 +234,7 @@ jobs:
- name: Validate release has expected assets
if: inputs.expected-asset-count
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
EXPECTED_ASSET_COUNT: ${{ inputs.expected-asset-count }}
@@ -254,7 +262,7 @@ jobs:
git push origin master
- name: Publish release
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
RELEASE_ID: ${{ steps.draft-release.outputs.id }}
FINAL: ${{ inputs.final }}
@@ -286,9 +294,9 @@ jobs:
name: Publish to npm
needs: release
if: inputs.npm
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-npm.yml@develop # zizmor: ignore[unpinned-uses]
with:
dir: ${{ inputs.dir }}
dir: ${{ inputs.dist-dir }}
permissions:
contents: read
id-token: write
+3 -2
View File
@@ -25,10 +25,11 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: staging
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
registry-url: "https://registry.npmjs.org"
+10 -7
View File
@@ -24,7 +24,7 @@ concurrency: ${{ github.workflow }}
permissions: {} # No permissions required
jobs:
release:
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-make.yml@develop # zizmor: ignore[unpinned-uses,secrets-inherit]
permissions:
contents: write
issues: write
@@ -50,9 +50,10 @@ jobs:
repository: ${{ matrix.repo }}
ref: staging
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
persist-credentials: true
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version: "lts/*"
@@ -77,10 +78,12 @@ jobs:
steps:
- name: 🧮 Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: 🔧 pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
@@ -92,7 +95,7 @@ jobs:
run: pnpm gendoc
- name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: _docs
@@ -110,4 +113,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
+9 -5
View File
@@ -13,6 +13,10 @@ on:
type: boolean
required: false
description: "Whether to combine multiple LCOV and sonar-report files in coverage artifact"
version-pkg-json-dir:
type: string
default: "."
description: "Relative path of the directory containing package.json with the `version` to use."
permissions: {}
jobs:
sonarqube:
@@ -41,9 +45,10 @@ jobs:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
persist-credentials: false
- name: 📥 Download artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: ${{ !inputs.sharded }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -51,14 +56,13 @@ jobs:
name: coverage
path: coverage
- name: 📥 Download sharded artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
if: inputs.sharded
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: coverage-*
path: coverage
merge-multiple: true
- name: Check coverage artifact
run: |
if [ ! -d coverage ]; then
@@ -75,7 +79,7 @@ jobs:
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@ea0cd9dbd5562e79816685972bc0d03c235a900c
uses: matrix-org/sonarcloud-workflow-action@13968a27c924fa19b1dacbce6ca3ff217daa775b
# 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:
@@ -83,7 +87,7 @@ jobs:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
is_pr: ${{ github.event.workflow_run.event == 'pull_request' }}
skip_coverage_label: Z-Skip-Coverage
version_cmd: "cat package.json | jq -r .version"
version_cmd: "cat ${{ inputs.version-pkg-json-dir }}/package.json | jq -r .version"
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
+4 -2
View File
@@ -1,6 +1,8 @@
name: SonarQube
on:
workflow_run:
# Privilege escalation necessary to call upon SonarCloud
# 🚨 We must not execute any checked out code here.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: ["Tests"]
types:
- completed
@@ -16,7 +18,7 @@ jobs:
actions: read
statuses: write
id-token: write # sonar
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop
uses: matrix-org/matrix-js-sdk/.github/workflows/sonarcloud.yml@develop # zizmor: ignore[unpinned-uses]
secrets:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+45 -21
View File
@@ -15,9 +15,11 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
@@ -33,9 +35,11 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
@@ -51,9 +55,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
@@ -79,11 +85,15 @@ jobs:
workflow_lint:
name: "Workflow Lint"
runs-on: ubuntu-24.04
permissions:
security-events: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
@@ -94,14 +104,19 @@ jobs:
- name: Run Linter
run: "pnpm lint:workflows"
- name: Run zizmor
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
docs:
name: "JSDoc Checker"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
@@ -113,7 +128,7 @@ jobs:
run: "pnpm run gendoc --treatWarningsAsErrors --suppressCommentWarningsInDeclarationFiles"
- name: Upload Artifact
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: docs
path: _docs
@@ -125,9 +140,11 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version-file: package.json
@@ -146,9 +163,10 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
repository: element-hq/element-web
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version: "lts/*"
@@ -163,13 +181,19 @@ jobs:
working-directory: apps/web
run: "pnpm run lint:types"
# Hook for branch protection to skip downstream typechecking outside of merge queues
downstream:
name: Downstream Typescript Syntax Check
# Workflow consolidation job
done:
needs:
- ts_lint
- js_lint
- node_example_lint
- workflow_lint
- docs
- analyse_dead_code
- element-web
name: Static Analysis
runs-on: ubuntu-24.04
if: always()
needs:
- element-web
steps:
- if: needs.element-web.result != 'skipped' && needs.element-web.result != 'success'
- if: contains(needs.*.result , 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1
+1 -1
View File
@@ -11,7 +11,7 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
sync-labels:
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@develop
uses: element-hq/element-meta/.github/workflows/sync-labels.yml@7f2f93fb9b52ece7a0998f60e64862aa203c1746
with:
LABELS: |
element-hq/element-meta
+14 -9
View File
@@ -23,11 +23,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node
id: setupNode
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
cache: "pnpm"
node-version: ${{ matrix.node }}
@@ -42,19 +44,22 @@ jobs:
- name: Run tests
run: |
pnpm test \
--coverage=${{ env.ENABLE_COVERAGE }} \
--maxWorkers ${{ steps.cpu-cores.outputs.count }} \
--coverage=${ENABLE_COVERAGE} \
--maxWorkers ${NUM_WORKERS} \
./spec/${{ matrix.specs }}
env:
SHARD: ${{ matrix.specs }}
NUM_WORKERS: ${{ steps.cpu-cores.outputs.count }}
- name: Move coverage files into place
if: env.ENABLE_COVERAGE == 'true'
run: mv coverage/lcov.info coverage/${{ steps.setupNode.outputs.node-version }}-${{ matrix.specs }}.lcov.info
run: mv coverage/lcov.info coverage/${NODE_VERSION}-${{ matrix.specs }}.lcov.info
env:
NODE_VERSION: ${{ steps.setupNode.outputs.node-version }}
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: coverage-${{ matrix.specs }}-${{ matrix.node == 'lts/*' && 'lts' || matrix.node }}
path: |
@@ -74,7 +79,7 @@ jobs:
element-web:
name: Downstream test element-web
if: github.event_name == 'merge_group'
uses: element-hq/element-web/.github/workflows/tests.yml@develop
uses: element-hq/element-web/.github/workflows/tests.yml@develop # zizmor: ignore[unpinned-uses]
permissions:
statuses: write
with:
@@ -84,8 +89,8 @@ jobs:
complement-crypto:
name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group'
permissions: read-all
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
permissions: read-all # zizmor: ignore[excessive-permissions]
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main # zizmor: ignore[unpinned-uses]
with:
use_js_sdk: "."
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
automate-project-columns-next:
runs-on: ubuntu-24.04
steps:
- uses: actions/add-to-project@main
- uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
with:
project-url: https://github.com/orgs/element-hq/projects/120
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
+1 -1
View File
@@ -6,6 +6,6 @@ on:
permissions: {} # We use ELEMENT_BOT_TOKEN instead
jobs:
call-triage-labelled:
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@6339bcda15c71d209303b18a06a9b1c021220bf9
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+16
View File
@@ -1,3 +1,19 @@
Changes in [41.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.3.0) (2026-04-07)
==================================================================================================
## 🐛 Bug Fixes
* Rotate the current room key when we see a member leave ([#5231](https://github.com/matrix-org/matrix-js-sdk/pull/5231)). Contributed by @kaylendog.
Changes in [41.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.2.0) (2026-03-24)
==================================================================================================
## ✨ Features
* Only share history if room history visibility is shared ([#5216](https://github.com/matrix-org/matrix-js-sdk/pull/5216)). Contributed by @kaylendog.
* History sharing: resume key-bundle import on restart ([#5214](https://github.com/matrix-org/matrix-js-sdk/pull/5214)). Contributed by @richvdh.
* Move `CryptoApi.shareRoomHistoryWithUser` to `CryptoBackend` ([#5218](https://github.com/matrix-org/matrix-js-sdk/pull/5218)). Contributed by @richvdh.
Changes in [41.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v41.1.0) (2026-03-10)
==================================================================================================
## ✨ Features
+3 -4
View File
@@ -1,5 +1,8 @@
import { KnipConfig } from "knip";
// Specify this as knip loads config files which may conditionally add reporters, e.g. `vitest-sonar-reporter'
process.env.GITHUB_ACTIONS = "1";
export default {
entry: [
"src/index.ts",
@@ -28,10 +31,6 @@ export default {
"husky",
// Used in script which only runs in environment with `@octokit/rest` installed
"@octokit/rest",
// Used by `vitest`
"vitest-sonar-reporter",
// Used by `@babel/plugin-transform-runtime`
"@babel/runtime",
],
ignoreBinaries: [
// Used when available by reusable workflow `.github/workflows/release-make.yml`
+17 -14
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "41.1.0",
"version": "41.3.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=22.0.0"
@@ -18,8 +18,8 @@
"lint:types": "tsc --noEmit",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip",
"test": "vitest",
"test:watch": "vitest --watch",
"test": "vitest run",
"test:watch": "vitest watch",
"coverage": "pnpm test --coverage"
},
"repository": {
@@ -48,7 +48,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^17.1.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^18.1.0",
"another-json": "^0.2.0",
"bs58": "^6.0.0",
"content-type": "^1.0.4",
@@ -57,10 +57,9 @@
"matrix-events-sdk": "0.0.1",
"matrix-widget-api": "^1.16.1",
"oidc-client-ts": "^3.0.1",
"p-retry": "7",
"p-retry": "8",
"sdp-transform": "^3.0.0",
"unhomoglyph": "^1.0.6",
"uuid": "13"
"unhomoglyph": "^1.0.6"
},
"devDependencies": {
"@action-validator/cli": "^0.6.0",
@@ -106,21 +105,18 @@
"fetch-mock": "^12.6.0",
"happy-dom": "^20.1.0",
"husky": "^9.0.0",
"knip": "^5.0.0",
"knip": "^6.0.0",
"lint-staged": "^16.0.0",
"matrix-mock-request": "^2.5.0",
"prettier": "3.8.1",
"prettier": "3.8.3",
"typedoc": "^0.28.1",
"typedoc-plugin-coverage": "^4.0.0",
"typedoc-plugin-mdn-links": "^5.0.0",
"typedoc-plugin-missing-exports": "^4.0.0",
"typescript": "^5.4.2",
"typescript": "^6.0.0",
"vitest": "^4.0.17",
"vitest-sonar-reporter": "^3.0.0"
},
"resolutions": {
"expect": "30.2.0"
},
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
@@ -129,7 +125,14 @@
},
"allowedDeprecatedVersions": {
"eslint": "8"
},
"overrides": {
"expect": "30.3.0",
"flatted@<=3.4.1": "^3.4.2",
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
"vite": "8.0.8"
}
},
"packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319"
}
+1597 -1225
View File
File diff suppressed because it is too large Load Diff
+110 -2
View File
@@ -86,6 +86,7 @@ import {
encryptGroupSessionKey,
encryptMegolmEvent,
encryptMegolmEventRawPlainText,
encryptOlmEvent,
establishOlmSession,
getTestOlmAccountKeys,
expectSendRoomKey,
@@ -2064,6 +2065,7 @@ describe("crypto", () => {
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.known).toBe(false); // We haven't actually stashed a copy of Alice's identity
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2078,7 +2080,8 @@ describe("crypto", () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys(TEST_USER_ID);
expect(hasCrossSigningKeysForUser).toBe(true);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(TEST_USER_ID);
expect(verificationStatus.known).toBe(true);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2089,7 +2092,8 @@ describe("crypto", () => {
const hasCrossSigningKeysForUser = await aliceClient.getCrypto()!.userHasCrossSigningKeys("@unknown:xyz");
expect(hasCrossSigningKeysForUser).toBe(false);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus("@unknown:xyz");
expect(verificationStatus.known).toBe(false);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2119,6 +2123,7 @@ describe("crypto", () => {
{
const verificationStatus = await aliceClient.getCrypto()!.getUserVerificationStatus(BOB_TEST_USER_ID);
expect(verificationStatus.known).toBe(true);
expect(verificationStatus.isVerified()).toBe(false);
expect(verificationStatus.isCrossSigningVerified()).toBe(false);
expect(verificationStatus.wasCrossSigningVerified()).toBe(false);
@@ -2311,4 +2316,107 @@ describe("crypto", () => {
);
}
});
describe("secret pushing", () => {
it("should push a new backup key when a new backup key is set", async () => {
// setup: alice has another device, DEVICE_ID, which is verified
const crypto = aliceClient.getCrypto()!;
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
await startClientAndAwaitFirstSync();
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
// when we set a new backup key
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
fetchMock.post("path:/_matrix/client/v3/room_keys/version", {
status: 200,
body: { version: "1" },
});
const secretPushPromise = new Promise<any>((resolve) => {
fetchMock.putOnce(new RegExp("/sendToDevice/m.room.encrypted/"), (callLog): RouteResponse => {
const content = JSON.parse(callLog.options.body as string);
resolve(content);
return {};
});
});
await crypto.resetKeyBackup();
// we expect the other device to get a secret push
const content = await secretPushPromise;
const curve25519key = JSON.parse(testOlmAccount.identity_keys()).curve25519;
const ciphertext = content.messages["@alice:localhost"].DEVICE_ID.ciphertext[curve25519key];
const olmSession = new Olm.Session();
olmSession.create_inbound(testOlmAccount, ciphertext.body);
const decrypted = JSON.parse(olmSession.decrypt(0, ciphertext.body));
expect(decrypted.type).toBe("io.element.msc4385.secret.push");
expect(decrypted.content.name).toBe("m.megolm_backup.v1");
});
it("should receive pushed backup key", async () => {
// setup: alice has another device, DEVICE_ID, which is verified,
// and has a key backup set up and signed by DEVICE_ID
const crypto = aliceClient.getCrypto()!;
expectAliceKeyQuery(getTestKeysQueryResponse("@alice:localhost"));
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
await startClientAndAwaitFirstSync();
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo(["@alice:localhost"]);
expect(devices.get("@alice:localhost")!.keys()).toContain("DEVICE_ID");
await crypto.setDeviceVerified("@alice:localhost", "DEVICE_ID");
expectAliceKeyClaim(getTestKeysClaimResponse("@alice:localhost"));
// after we push the backup key to alice...
const senderIdentityKeys = JSON.parse(testOlmAccount.identity_keys());
const aliceDeviceKeys = await crypto.getOwnDeviceKeys();
const p2pSession = await createOlmSession(testOlmAccount, keyReceiver);
const secretPush = encryptOlmEvent({
sender: "@alice:localhost",
senderKey: senderIdentityKeys.curve25519,
senderSigningKey: senderIdentityKeys.ed25519,
p2pSession,
recipient: "@alice:localhost",
recipientCurve25519Key: aliceDeviceKeys.curve25519,
recipientEd25519Key: aliceDeviceKeys.ed25519,
plaincontent: {
secret: testData.BACKUP_DECRYPTION_KEY_BASE64,
name: "m.megolm_backup.v1",
},
plaintype: "io.element.msc4385.secret.push",
});
const syncResponse = {
next_batch: 1,
to_device: {
events: [secretPush],
},
};
const backupKeyReceivedPromise = new Promise<string>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupDecryptionKeyCached, resolve);
});
const keyBackupEnabledPromise = new Promise<void>((resolve) => {
aliceClient.on(CryptoEvent.KeyBackupStatus, (enabled) => {
if (enabled) {
resolve();
}
});
});
syncResponder.sendOrQueueSyncResponse(syncResponse);
await syncPromise(aliceClient);
// alice should be using backup now
expect(await backupKeyReceivedPromise).toBe(testData.SIGNED_BACKUP_DATA.version);
await keyBackupEnabledPromise;
expect(await crypto.getActiveSessionBackupVersion()).toBe(testData.SIGNED_BACKUP_DATA.version);
});
});
});
File diff suppressed because it is too large Load Diff
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { QrCodeData, QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
import { QrCodeData, QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
import fetchMock from "@fetch-mock/vitest";
import {
@@ -146,7 +146,7 @@ describe("MSC4108SignInWithQR", () => {
const ourChannel = new MSC4108SecureChannel(ourMockSession);
const qrCodeData = QrCodeData.fromBytes(
await ourChannel.generateCode(QrCodeMode.Reciprocate, client.getDomain()!),
await ourChannel.generateCode(QrCodeIntent.Reciprocate, client.getDomain()!),
);
const opponentChannel = new MSC4108SecureChannel(opponentMockSession, qrCodeData.publicKey);
@@ -279,7 +279,7 @@ export const {prefix}BACKUP_DECRYPTION_KEY_BASE58 = "{ backup_recovery_key }";
/** Signed backup data, suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}/{{sessionId}}` */
export const {prefix}SIGNED_BACKUP_DATA: KeyBackupInfo = { json.dumps(backup_data, indent=4) };
/**
/**
* Per-room backup data, (supposedly) suitable for return from `GET /_matrix/client/v3/room_keys/keys/{{roomId}}`.
* Contains the key from {prefix}MEGOLM_SESSION_DATA.
*/
@@ -422,7 +422,7 @@ def build_exported_megolm_key(device_curve_key: x25519.X25519PrivateKey) -> tupl
"ed25519": encode_base64(ed25519.Ed25519PrivateKey.from_private_bytes(randbytes(32)).public_key().public_bytes(Encoding.Raw, PublicFormat.Raw)),
},
"forwarding_curve25519_key_chain": [],
"org.matrix.msc3061.shared_history": True,
"m.shared_history": True,
}
return megolm_export, private_key
+25
View File
@@ -3940,6 +3940,31 @@ describe("MatrixClient", function () {
});
expect(httpLookups.length).toEqual(0);
});
it("should handle no jwks_uri", async () => {
const { jwks_uri: _, ...metadata } = mockOpenIdConfiguration();
httpLookups = [
{
method: "GET",
path: `/auth_metadata`,
error: new MatrixError({ errcode: "M_UNRECOGNIZED" }, 404),
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
{
method: "GET",
path: `/auth_issuer`,
data: { issuer: metadata.issuer },
prefix: "/_matrix/client/unstable/org.matrix.msc2965",
},
];
fetchMock.get("https://auth.org/.well-known/openid-configuration", metadata);
await expect(client.getAuthMetadata()).resolves.toEqual({
...metadata,
signingKeys: null,
});
expect(httpLookups.length).toEqual(0);
});
});
describe("identityHashedLookup", () => {
+1 -1
View File
@@ -427,7 +427,7 @@ describe("CallMembership", () => {
});
it("uses unpadded base64 for RTC backend identities", async () => {
const membership = await CallMembership.parseFromEvent(makeMockEvent(0, { ...membershipTemplate }));
expect(membership.rtcBackendIdentity).toBe("j9N1u04ZbvI9qKf3cxrf2NauD-fIGJ4uAcYkfI9V7SY");
expect(membership.rtcBackendIdentity).toBe("jUZ0Q1yF5nV3LlAI5xfD1I7BPnAytJaPEAR57EXjJ6s");
});
});
});
@@ -0,0 +1,27 @@
/*
Copyright 2026 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 { computeRtcIdentityRaw } from "../../../src/matrixrtc/membershipData/index.ts";
describe("computeRtcIdentityRaw", () => {
it("should compute the correct identity hash", async () => {
// Test vector taken from the spec, with the expected output updated to match the unpadded base64 encoding
// https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md#appendix-hash-derivation-test-vectors
const result = await computeRtcIdentityRaw("@alice:example.com", "DEVICE123", "memberABC");
// Add assertions based on expected hash output
expect(result).toBe("J+T45tGruxc+HrUOqJJlyQSV33m728Cme4+vt8/SWrU");
});
});
+44 -1
View File
@@ -175,12 +175,29 @@ describe("oidc authorization", () => {
expect(authUrl.searchParams.get("login_hint")).toEqual("login1234");
});
it("should generate url with response_mode=fragment", async () => {
const nonce = "abc123";
const authUrl = new URL(
await generateOidcAuthorizationUrl({
metadata: delegatedAuthConfig,
homeserverUrl: baseUrl,
clientId,
redirectUri: baseUrl,
nonce,
responseMode: "fragment",
}),
);
expect(authUrl.searchParams.get("response_mode")).toEqual("fragment");
});
});
describe("completeAuthorizationCodeGrant", () => {
const homeserverUrl = "https://server.org/";
const identityServerUrl = "https://id.org/";
const nonce = "test-nonce";
const nonce = "hRpB6pkE06";
const redirectUri = baseUrl;
const code = "auth_code_xyz";
const validBearerTokenResponse = {
@@ -290,6 +307,32 @@ describe("oidc authorization", () => {
expect(queryParams.get("code")).toEqual(code);
});
it("should make correct request to the token endpoint with response_mode=fragment", async () => {
const state = await setupState({ responseMode: "fragment" });
const codeVerifier = getValueFromStorage(state, "code_verifier");
await completeAuthorizationCodeGrant(code, state, "fragment");
expect(fetchMock.callHistory.lastCall(metadata.token_endpoint)?.options).toStrictEqual(
expect.objectContaining({
method: "post",
credentials: "same-origin",
headers: {
"accept": "application/json",
"content-type": "application/x-www-form-urlencoded",
},
}),
);
// check body is correctly formed
const queryParams = fetchMock.callHistory.lastCall(metadata.token_endpoint)!.options
.body as URLSearchParams;
expect(queryParams.get("grant_type")).toEqual("authorization_code");
expect(queryParams.get("client_id")).toEqual(clientId);
expect(queryParams.get("code_verifier")).toEqual(codeVerifier);
expect(queryParams.get("redirect_uri")).toEqual(redirectUri);
expect(queryParams.get("code")).toEqual(code);
});
it("should return with valid bearer token", async () => {
const state = await setupState();
const scope = getValueFromStorage(state, "scope");
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type EstablishedEcies, QrCodeData, QrCodeMode, Ecies } from "@matrix-org/matrix-sdk-crypto-wasm";
import { type EstablishedEcies, QrCodeData, QrCodeIntent, Ecies } from "@matrix-org/matrix-sdk-crypto-wasm";
import { MSC4108RendezvousSession, MSC4108SecureChannel, PayloadType } from "../../../../src/rendezvous";
@@ -28,7 +28,7 @@ describe("MSC4108SecureChannel", () => {
});
const channel = new MSC4108SecureChannel(session);
const code = await channel.generateCode(QrCodeMode.Login);
const code = await channel.generateCode(QrCodeIntent.Login);
expect(code).toHaveLength(71);
const text = new TextDecoder().decode(code);
expect(text.startsWith("MATRIX")).toBeTruthy();
@@ -43,7 +43,7 @@ describe("MSC4108SecureChannel", () => {
} as unknown as MSC4108RendezvousSession;
const channel = new MSC4108SecureChannel(mockSession);
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl));
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeIntent.Reciprocate, baseUrl));
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
qrCodeData.publicKey,
"MATRIX_QR_CODE_LOGIN_INITIATE",
@@ -64,7 +64,7 @@ describe("MSC4108SecureChannel", () => {
vi.mocked(mockSession.receive).mockResolvedValue("");
await expect(channel.connect()).rejects.toThrow("No response from other device");
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl));
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeIntent.Reciprocate, baseUrl));
const { initial_message: ciphertext } = new Ecies().establish_outbound_channel(
qrCodeData.publicKey,
"NOT_REAL_MATRIX_QR_CODE_LOGIN_INITIATE",
@@ -87,7 +87,7 @@ describe("MSC4108SecureChannel", () => {
} as unknown as MSC4108RendezvousSession;
channel = new MSC4108SecureChannel(mockSession);
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeMode.Reciprocate, baseUrl));
const qrCodeData = QrCodeData.fromBytes(await channel.generateCode(QrCodeIntent.Reciprocate, baseUrl));
const { channel: _opponentChannel, initial_message: ciphertext } = new Ecies().establish_outbound_channel(
qrCodeData.publicKey,
"MATRIX_QR_CODE_LOGIN_INITIATE",
+79 -1
View File
@@ -21,6 +21,7 @@ import {
KeysQueryRequest,
Migration,
OlmMachine,
type OtherUserIdentity,
type PickledInboundGroupSession,
type PickledSession,
StoreHandle,
@@ -109,6 +110,7 @@ describe("initRustCrypto", () => {
getBackupKeys: vi.fn(),
getIdentity: vi.fn().mockResolvedValue(null),
trackedUsers: vi.fn(),
getAllRoomsPendingKeyBundles: vi.fn().mockResolvedValue([]),
} as unknown as Mocked<OlmMachine>;
}
@@ -590,6 +592,72 @@ describe("RustCrypto", () => {
expect(res.length).toEqual(0);
});
it.each(["m.room_key_bundle", "io.element.msc4268.room_key_bundle"])(
"should accept key bundles when we find out about them",
async (type: string) => {
// Given we are faking that the received to-device message is a
// decrypted room key bundle.
// @ts-ignore Overriding a private function
rustCrypto.receiveSyncChanges = vi.fn().mockReturnValue([keyBundleEvent(type)]);
// And that there is a pending key bundle
// @ts-ignore Overriding a private function
rustCrypto.olmMachine.getPendingKeyBundleDetailsForRoom = vi.fn().mockReturnValue({
inviteAcceptedAtMillis: Date.now(),
inviterId: { toString: vi.fn().mockReturnValue("@inv:s.co") },
});
// When we process to-device messages
rustCrypto.maybeAcceptKeyBundle = vi.fn().mockName("maybeAcceptKeyBundle").mockResolvedValue(null);
await rustCrypto.preprocessToDeviceMessages([]);
// Then we accepted the key bundle
expect(rustCrypto.maybeAcceptKeyBundle).toHaveBeenCalledWith("!r:s.co", "@inv:s.co");
},
);
it("should not accept other to-device messages as key bundles when we receive them", async () => {
// Given we are faking that the received to-device message looks
// like a room key bundle, except it has the wrong type.
// @ts-ignore Overriding a private function
rustCrypto.receiveSyncChanges = vi.fn().mockReturnValue([keyBundleEvent("foo.some_other_type")]);
// And that there is a pending key bundle
// @ts-ignore Overriding a private function
rustCrypto.olmMachine.getPendingKeyBundleDetailsForRoom = vi.fn().mockReturnValue({
inviteAcceptedAtMillis: Date.now(),
inviterId: { toString: vi.fn().mockReturnValue("@inv:s.co") },
});
// When we process to-device messages
rustCrypto.maybeAcceptKeyBundle = vi.fn().mockName("maybeAcceptKeyBundle").mockResolvedValue(null);
await rustCrypto.preprocessToDeviceMessages([]);
// Then we do not try to accepted a key bundle
expect(rustCrypto.maybeAcceptKeyBundle).not.toHaveBeenCalledWith();
});
function keyBundleEvent(type: string): RustSdkCryptoJs.ProcessedToDeviceEvent {
return {
rawEvent: JSON.stringify({
content: { room_id: "!r:s.co" },
sender: "",
type,
}),
type: 0,
encryptionInfo: {
sender: "",
senderDevice: null,
senderCurve25519Key: "",
isSenderVerified: vi.fn().mockReturnValue(true),
},
} as any as RustSdkCryptoJs.ProcessedToDeviceEvent;
}
it("emits VerificationRequestReceived on incoming m.key.verification.request", async () => {
rustCrypto = await makeTestRustCrypto(
new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
@@ -788,6 +856,7 @@ describe("RustCrypto", () => {
undefined,
secretStorage,
);
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
async function createSecretStorageKey() {
return {
@@ -835,6 +904,7 @@ describe("RustCrypto", () => {
{} as CryptoCallbacks,
false,
);
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
async function createSecretStorageKey() {
return {
@@ -1524,6 +1594,7 @@ describe("RustCrypto", () => {
it("returns an unverified UserVerificationStatus when there is no UserIdentity", async () => {
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.known).toBe(false);
expect(userVerificationStatus.isVerified()).toBeFalsy();
expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeFalsy();
@@ -1535,9 +1606,10 @@ describe("RustCrypto", () => {
free: vi.fn(),
isVerified: vi.fn().mockReturnValue(true),
wasPreviouslyVerified: vi.fn().mockReturnValue(true),
});
} as unknown as OtherUserIdentity);
const userVerificationStatus = await rustCrypto.getUserVerificationStatus(testData.TEST_USER_ID);
expect(userVerificationStatus.known).toBe(true);
expect(userVerificationStatus.isVerified()).toBeTruthy();
expect(userVerificationStatus.isTofu()).toBeFalsy();
expect(userVerificationStatus.isCrossSigningVerified()).toBeTruthy();
@@ -2320,6 +2392,7 @@ describe("RustCrypto", () => {
});
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi(), undefined, undefined, secretStorage);
vi.spyOn(rustCrypto, "pushSecretToVerifiedDevices").mockResolvedValue();
// We have a key backup
await waitFor(async () => expect(await rustCrypto.getActiveSessionBackupVersion()).not.toBeNull());
@@ -2388,6 +2461,7 @@ describe("RustCrypto", () => {
queryKeysForUsers: vi.fn().mockReturnValue({}),
getReceivedRoomKeyBundleData: vi.fn(),
receiveRoomKeyBundle: vi.fn(),
clearRoomPendingKeyBundle: vi.fn(),
} as unknown as Mocked<OlmMachine>;
const http = new MatrixHttpApi(new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>(), {
@@ -2448,6 +2522,10 @@ describe("RustCrypto", () => {
expect(mockOlmMachine.receiveRoomKeyBundle).toHaveBeenCalledTimes(1);
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][0]).toBe(bundleData);
expect(mockOlmMachine.receiveRoomKeyBundle.mock.calls[0][1]).toEqual(new TextEncoder().encode("asdfghjkl"));
// It should also flag the room as not waiting for a key bundle
expect(mockOlmMachine.clearRoomPendingKeyBundle).toHaveBeenCalledTimes(1);
expect(mockOlmMachine.clearRoomPendingKeyBundle.mock.calls[0][0].toString()).toEqual("!room_id");
});
});
+10 -4
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { type OutgoingRequest } from "@matrix-org/matrix-sdk-crypto-wasm";
import { type OtherUserIdentity, type OutgoingRequest } from "@matrix-org/matrix-sdk-crypto-wasm";
import { type Mocked } from "vitest";
import {
@@ -138,7 +138,9 @@ describe("VerificationRequest", () => {
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
// Alice requests verification
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
const bobUserIdentity = (await aliceOlmMachine.getIdentity(
new RustSdkCryptoJs.UserId(bobUserId),
)) as OtherUserIdentity;
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
@@ -276,7 +278,9 @@ describe("VerificationRequest", () => {
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
// Alice requests verification
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
const bobUserIdentity = (await aliceOlmMachine.getIdentity(
new RustSdkCryptoJs.UserId(bobUserId),
)) as OtherUserIdentity;
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
const methods = [verificationMethodIdentifierToMethod("m.sas.v1")];
@@ -394,7 +398,9 @@ describe("VerificationRequest", () => {
await bobOlmMachine.updateTrackedUsers([new RustSdkCryptoJs.UserId(aliceUserId)]);
// Alice requests verification
const bobUserIdentity = await aliceOlmMachine.getIdentity(new RustSdkCryptoJs.UserId(bobUserId));
const bobUserIdentity = (await aliceOlmMachine.getIdentity(
new RustSdkCryptoJs.UserId(bobUserId),
)) as OtherUserIdentity;
const roomId = new RustSdkCryptoJs.RoomId("!roomId:example.org");
const methods = [
+35
View File
@@ -79,6 +79,41 @@ describe("IndexedDBStore", () => {
expect(await store.getOutOfBandMembers(roomId)).toHaveLength(2);
});
it("should handle failed queries", async () => {
const store = new IndexedDBStore({
indexedDB: indexedDB,
dbName: "database",
localStorage,
});
await store.startup();
// Simulate a failed query
let txn: IDBRequest;
(store.backend as LocalIndexedDBStoreBackend)["db"]!.transaction = (): IDBTransaction => {
return {
objectStore: (name: string) =>
({
name,
openCursor: (query: unknown) => {
return (txn = {
error: new DOMException("Expected error"),
} as IDBRequest);
},
}) as IDBObjectStore,
} as IDBTransaction;
};
// Call backend directly as otherwise the error is masked.
const promise = store.backend.getClientOptions();
// The function uses a Promise.then(() => trick to delay execution
// so we need to wait before we can call the txn onerror handler.
process.nextTick(() => {
txn!.onerror!(new Event("we-ignore-this"));
});
await expect(() => promise).rejects.toThrow("selectQuery failed for client_options");
});
it("Should load presence events on startup", async () => {
// 1. Create idb database
const indexedDB = new IDBFactory();
+14 -2
View File
@@ -416,9 +416,12 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
[EventType.Direct]: { [userId: string]: string[] };
[EventType.IgnoredUserList]: { ignored_users: { [userId: string]: EmptyObject } };
"m.secret_storage.default_key": { key: string };
// Flag set by the rust SDK (Element X) and also used by us to mark that the user opted out of backup
// (I don't know why it's m.org.matrix...)
// MSC4287: Sharing key backup preference between clients - used to mark that the user opted out of key storage
"m.key_backup": { enabled: boolean };
// MSC4287 unstable prefix (note the boolean property has the opposite sense)
"m.org.matrix.custom.backup_disabled": { disabled: boolean };
"m.identity_server": { base_url: string | null };
[key: `${typeof LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${string}`]: LocalNotificationSettings;
[key: `m.secret_storage.key.${string}`]: SecretStorageKeyDescription;
@@ -428,6 +431,15 @@ export interface AccountDataEvents extends SecretStorageAccountDataEvents {
[POLICIES_ACCOUNT_EVENT_TYPE.altName]: { [key: string]: any };
[EventType.InvitePermissionConfig]: { default_action?: string };
// List of recently used reaction emojis
// https://spec.matrix.org/v1.18/client-server-api/#mrecent_emoji
"m.recent_emoji": {
recent_emoji: Array<{
emoji: string;
total: number;
}>;
};
}
/**
+3 -1
View File
@@ -58,7 +58,9 @@ export interface InviteOpts {
/**
* Before sending the invite, if the room is encrypted, share the keys for any messages sent while the history
* visibility was `shared`, via the experimental
* support for [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268).
* support for [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268). If the room's current
* history visibility setting is neither `shared` nor `world_readable`, history sharing will be disabled to prevent
* exposing keys for messages sent prior to the visibility restriction.
*
* @experimental
*/
+12 -7
View File
@@ -101,7 +101,7 @@ import {
type RoomNameState,
} from "./models/room.ts";
import { RoomMemberEvent, type RoomMemberEventHandlerMap } from "./models/room-member.ts";
import { type IPowerLevelsContent, type RoomStateEvent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
import { RoomStateEvent, type IPowerLevelsContent, type RoomStateEventHandlerMap } from "./models/room-state.ts";
import {
isSendDelayedEventRequestOpts,
UpdateDelayedEventAction,
@@ -2027,6 +2027,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// attach the event listeners needed by RustCrypto
this.on(RoomMemberEvent.Membership, rustCrypto.onRoomMembership.bind(rustCrypto));
this.on(RoomStateEvent.Events, rustCrypto.onRoomStateEvent.bind(rustCrypto));
this.on(ClientEvent.Event, (event) => {
rustCrypto.onLiveEventFromSync(event);
});
@@ -2428,12 +2429,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const roomId = res.room_id;
if (opts.acceptSharedHistory && inviter && this.cryptoBackend) {
// Flag upfront that we are waiting for a key bundle, so that if we crash mid-import, we can try again.
await this.cryptoBackend.markRoomAsPendingKeyBundle(roomId, inviter);
// Try to accept the room key bundle specified in a `m.room_key_bundle` to-device message we (might have) already received.
const bundleDownloaded = await this.cryptoBackend.maybeAcceptKeyBundle(roomId, inviter);
// If this fails, i.e. we haven't received this message yet, we need to wait until the to-device message arrives.
if (!bundleDownloaded) {
this.cryptoBackend.markRoomAsPendingKeyBundle(roomId, inviter);
}
await this.cryptoBackend.maybeAcceptKeyBundle(roomId, inviter);
}
// In case we were originally given an alias, check the room cache again
@@ -4088,7 +4087,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
if (opts.shareEncryptedHistory) {
await this.cryptoBackend?.shareRoomHistoryWithUser(roomId, userId);
const historyVisibility = this.getRoom(roomId)?.getHistoryVisibility() ?? HistoryVisibility.Shared;
// We should only share room history if the *current* visibility allows it.
if ([HistoryVisibility.Invited, HistoryVisibility.Joined].includes(historyVisibility)) {
this.logger.debug("Not sharing message history as the room history visibility is currently unshared");
} else {
await this.cryptoBackend?.shareRoomHistoryWithUser(roomId, userId);
}
}
return await this.membershipChange(roomId, userId, KnownMembership.Invite, opts.reason);
+13 -1
View File
@@ -80,6 +80,18 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
*/
importBackedUpRoomKeys(keys: IMegolmSessionData[], backupVersion: string, opts?: ImportRoomKeysOpts): Promise<void>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Room key history sharing (MSC4268)
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Share any shareable E2EE history in the given room with the given recipient,
* as per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
*/
shareRoomHistoryWithUser(roomId: string, userId: string): Promise<void>;
/**
* Having accepted an invite for the given room from the given user, attempt to
* find information about a room key bundle and, if found, download the
@@ -103,7 +115,7 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* @param roomId - The room we were invited to, for which we did not receive a key bundle before accepting the invite.
* @param inviterId - The user who invited us to the room and is expected to send the room key bundle.
*/
markRoomAsPendingKeyBundle(roomId: string, inviterId: string): void;
markRoomAsPendingKeyBundle(roomId: string, inviterId: string): Promise<void>;
}
/** The methods which crypto implementations should expose to the Sync api
+36 -19
View File
@@ -188,7 +188,9 @@ export interface CryptoApi {
/**
* Check if the given user has published cross-signing keys.
*
* - If the user is tracked, a `/keys/query` request is made to update locally the cross signing keys.
* - If the user is this user, a `/keys/query` request is made to update locally the cross signing keys.
* - If the user is tracked, any current `/keys/query` requests are awaited (with a timeout) and then
* the locally cached information is used.
* - If the user is not tracked locally and downloadUncached is set to true,
* a `/keys/query` request is made to the server to retrieve the cross signing keys.
* - Otherwise, return false
@@ -205,7 +207,10 @@ export interface CryptoApi {
* Get the device information for the given list of users.
*
* For any users whose device lists are cached (due to sharing an encrypted room with the user), the
* cached device data is returned.
* cached device data is returned, unless it is stale.
*
* If there are users with stale cached entries, wait (with some timeout) for any in-progress
* `/keys/query` request to complete.
*
* If there are uncached users, and the `downloadUncached` parameter is set to `true`,
* a `/keys/query` request is made to the server to retrieve these devices.
@@ -626,7 +631,6 @@ export interface CryptoApi {
* * Disables 4S, deleting the info for the default key, the default key pointer itself and any
* known 4S data (cross-signing keys and the megolm key backup key).
* * Deletes any dehydrated devices.
* * Sets the "m.org.matrix.custom.backup_disabled" account data flag to indicate that the user has disabled backups.
*/
disableKeyStorage(): Promise<void>;
@@ -720,20 +724,6 @@ export interface CryptoApi {
* @param secrets - The secrets bundle received from the other device
*/
importSecretsBundle?(secrets: Awaited<ReturnType<SecretsBundle["to_json"]>>): Promise<void>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Room key history sharing (MSC4268)
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Share any shareable E2EE history in the given room with the given recipient,
* as per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268)
*
* @experimental
*/
shareRoomHistoryWithUser(roomId: string, userId: string): Promise<void>;
}
/** A reason code for a failure to decrypt an event. */
@@ -808,6 +798,9 @@ export enum DeviceIsolationModeKind {
*
* Events from all senders are always decrypted (and should be decorated with message shields in case
* of authenticity warnings, see {@link EventEncryptionInfo}).
*
* `AllDevicesIsolationMode` is used in the legacy, non-'exclude insecure devices' mode in Element Web. It is not
* recommended (see {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4153 | MSC4153}).
*/
export class AllDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.AllDevicesIsolationMode;
@@ -834,6 +827,9 @@ export class AllDevicesIsolationMode {
*
* Events are decrypted only if they come from a cross-signed device. Other events will result in a decryption
* failure. (To access the failure reason, see {@link MatrixEvent.decryptionFailureReason}.)
*
* `OnlySignedDevicesIsolationMode` corresponds to the 'Exclude insecure devices' mode in Element Web, which is
* recommended by {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4153 | MSC4153}.
*/
export class OnlySignedDevicesIsolationMode {
public readonly kind = DeviceIsolationModeKind.OnlySignedDevicesIsolationMode;
@@ -865,6 +861,25 @@ export interface BootstrapCrossSigningOpts {
* Represents the ways in which we trust a user
*/
export class UserVerificationStatus {
/**
* Indicates if we have saved a known identity for this user. Typically, this means that we share a
* room with them (or have done in the past).
*
* If this is `false`, then the other flags ({@link isCrossSigningVerified}, {@link wasCrossSigningVerified},
* {@link needsUserApproval}) will also be `false`. This means that we haven't seen this user before.
*
* If this is `true`, then there are further possibilities:
*
* - If {@link isCrossSigningVerified} returns `true`, then we have cryptographically verified the current
* identity of this user: that is the highest form of trust we have.
*
* - If {@link needsUserApproval} is `true`, that means that the user has changed their identity.
*
* - Otherwise, the user is "TOFU trusted": we have a record of their identity, and, typically, will share
* encrypted content with them as long as they retain that identity.
*/
public readonly known: boolean;
/**
* Indicates if the identity has changed in a way that needs user approval.
*
@@ -880,12 +895,14 @@ export class UserVerificationStatus {
*/
public readonly needsUserApproval: boolean;
/** @internal */
public constructor(
private readonly crossSigningVerified: boolean,
private readonly crossSigningVerifiedBefore: boolean,
private readonly tofu: boolean,
known: boolean,
needsUserApproval: boolean = false,
) {
this.known = known;
this.needsUserApproval = needsUserApproval;
}
@@ -917,7 +934,7 @@ export class UserVerificationStatus {
* @deprecated No longer supported, with the Rust crypto stack.
*/
public isTofu(): boolean {
return this.tofu;
return false;
}
}
+1 -1
View File
@@ -806,7 +806,7 @@ export class RoomWidgetClient extends MatrixClient {
// Sliding Sync
await this.syncApi!.injectRoomEvents(this.room!, [event]);
}
logger.info(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
logger.debug(`Updated state entry ${event.getType()} ${event.getStateKey()} to ${event.getId()}`);
} else {
const { event_id: eventId, room_id: roomId } = ev.detail.data;
logger.info(`Received state entry ${eventId} for a different room ${roomId}; discarding`);
+1
View File
@@ -82,6 +82,7 @@ export * from "./models/room-summary.ts";
export * from "./models/event-status.ts";
export * from "./models/profile-keys.ts";
export * from "./models/related-relations.ts";
export { type StickyMatrixEvent, RoomStickyEventsEvent } from "./models/room-sticky-events.ts";
export type { RoomSummary } from "./client.ts";
export * as ContentHelpers from "./content-helpers.ts";
export * as SecretStorage from "./secret-storage.ts";
+2 -1
View File
@@ -863,7 +863,8 @@ function quickFilterNonRelevantContents(content: IContent, logger: Logger): bool
// We have a MSC4143 event membership event with a proper joined content
return true;
} else if (eventKeysCount === 1 && "memberships" in content) {
logger.warn(`Legacy event found. Those are ignored, they do not contribute to the MatrixRTC session`);
// Events used to have this format in the past, but are now deprecated.
// Given that state events ~cannot be deleted, there can be some remaining events in the room, just ignore them.
return false;
} else {
// Invalid or left content
+5 -4
View File
@@ -20,7 +20,7 @@ import type { RelationType } from "../../types.ts";
import { type RtcSlotEventContent, type Transport } from "../types.ts";
import { MatrixRTCMembershipParseError } from "./common.ts";
import { sha256 } from "../../digest.ts";
import { encodeUnpaddedBase64Url } from "../../base64.ts";
import { encodeUnpaddedBase64 } from "../../base64.ts";
import { slotIdToDescription } from "../utils.ts";
/**
@@ -149,8 +149,9 @@ export const checkRtcMembershipData = (data: IContent, sender: string): data is
};
export async function computeRtcIdentityRaw(userId: string, deviceId: string, memberId: string): Promise<string> {
const hashInput = `${userId}|${deviceId}|${memberId}`;
const hashBuffer = await sha256(hashInput);
const hashedString = encodeUnpaddedBase64Url(hashBuffer);
// canonical JSON serialization (Matrix canonical JSON for arrays)
const jsonStr = JSON.stringify([userId, deviceId, memberId]);
const hashBuffer = await sha256(jsonStr);
const hashedString = encodeUnpaddedBase64(hashBuffer);
return hashedString;
}
+25 -6
View File
@@ -14,7 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { type IdTokenClaims, Log, OidcClient, SigninResponse, SigninState, WebStorageStateStore } from "oidc-client-ts";
import {
type IdTokenClaims,
Log,
OidcClient,
type SigninRequestCreateArgs,
SigninResponse,
SigninState,
WebStorageStateStore,
} from "oidc-client-ts";
import { logger } from "../logger.ts";
import { secureRandomString } from "../randomstring.ts";
@@ -127,6 +135,8 @@ export const generateAuthorizationUrl = async (
* @param urlState - value to append to the opaque state identifier to uniquely identify the callback
* @param loginHint - value to send as the `login_hint` to the OP, giving a hint about the login identifier the user might use to log in.
* See {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OIDC core 3.1.2.1}.
* @param responseMode - value to send as the `response_mode` to the OP, selecting how auth is passed back during redirect.
* See {@link https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest OIDC core 3.1.2.1}.
* @returns a Promise with the url as a string
*/
export const generateOidcAuthorizationUrl = async ({
@@ -139,6 +149,7 @@ export const generateOidcAuthorizationUrl = async ({
prompt,
urlState,
loginHint,
responseMode = "query",
}: {
clientId: string;
metadata: ValidatedAuthMetadata;
@@ -149,6 +160,7 @@ export const generateOidcAuthorizationUrl = async ({
prompt?: string;
urlState?: string;
loginHint?: string;
responseMode?: SigninRequestCreateArgs["response_mode"];
}): Promise<string> => {
const scope = generateScope();
const oidcClient = new OidcClient({
@@ -156,7 +168,7 @@ export const generateOidcAuthorizationUrl = async ({
client_id: clientId,
redirect_uri: redirectUri,
authority: metadata.issuer,
response_mode: "query",
response_mode: responseMode,
response_type: "code",
scope,
stateStore: new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage }),
@@ -200,7 +212,8 @@ const normalizeBearerTokenResponseTokenType = (response: SigninResponse): Bearer
* request to the Token Endpoint, to obtain the access token, refresh token, etc.
*
* @param code - authorization code as returned by OP during authorization
* @param storedAuthorizationParams - stored params from start of oidc login flow
* @param state - authorization state param as returned by OP during authorization
* @param responseMode - the response mode used for authentication
* @returns valid bearer token response
* @throws An `Error` with `message` set to an entry in {@link OidcError},
* when the request fails, or the returned token response is invalid.
@@ -208,6 +221,7 @@ const normalizeBearerTokenResponseTokenType = (response: SigninResponse): Bearer
export const completeAuthorizationCodeGrant = async (
code: string,
state: string,
responseMode: SigninRequestCreateArgs["response_mode"] = "query",
): Promise<{
oidcClientSettings: { clientId: string; issuer: string };
tokenResponse: BearerTokenResponse;
@@ -221,13 +235,18 @@ export const completeAuthorizationCodeGrant = async (
* so that oidc-client can parse it
*/
const reconstructedUrl = new URL(window.location.origin);
reconstructedUrl.searchParams.append("code", code);
reconstructedUrl.searchParams.append("state", state);
const params = new URLSearchParams({ code, state });
if (responseMode === "query") {
reconstructedUrl.search = params.toString();
} else {
reconstructedUrl.hash = `#${params.toString()}`;
}
// set oidc-client to use our logger
Log.setLogger(logger);
try {
const response = new SigninResponse(reconstructedUrl.searchParams);
const response = new SigninResponse(params);
const stateStore = new WebStorageStateStore({ prefix: "mx_oidc_", store: window.sessionStorage });
+1 -1
View File
@@ -62,6 +62,6 @@ export const validateAuthMetadataAndKeys = async (authMetadata: unknown): Promis
return {
...validatedIssuerConfig,
signingKeys: await metadataService.getSigningKeys(),
signingKeys: validatedIssuerConfig.jwks_uri ? await metadataService.getSigningKeys() : null,
};
};
+6 -6
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { QrCodeMode } from "@matrix-org/matrix-sdk-crypto-wasm";
import { QrCodeIntent } from "@matrix-org/matrix-sdk-crypto-wasm";
import {
ClientRendezvousFailureReason,
@@ -108,7 +108,7 @@ interface SecretsPayload extends MSC4108Payload, Awaited<ReturnType<NonNullable<
* @experimental Note that this is UNSTABLE and may have breaking changes without notice.
*/
export class MSC4108SignInWithQR {
private readonly ourIntent: QrCodeMode;
private readonly ourIntent: QrCodeIntent;
private _code?: Uint8Array;
private expectingNewDeviceId?: string;
@@ -131,7 +131,7 @@ export class MSC4108SignInWithQR {
private readonly client?: MatrixClient,
public onFailure?: RendezvousFailureListener,
) {
this.ourIntent = client ? QrCodeMode.Reciprocate : QrCodeMode.Login;
this.ourIntent = client ? QrCodeIntent.Reciprocate : QrCodeIntent.Login;
}
/**
@@ -149,9 +149,9 @@ export class MSC4108SignInWithQR {
return;
}
if (this.ourIntent === QrCodeMode.Reciprocate && this.client) {
if (this.ourIntent === QrCodeIntent.Reciprocate && this.client) {
this._code = await this.channel.generateCode(this.ourIntent, this.client.getDomain()!);
} else if (this.ourIntent === QrCodeMode.Login) {
} else if (this.ourIntent === QrCodeIntent.Login) {
this._code = await this.channel.generateCode(this.ourIntent);
}
}
@@ -160,7 +160,7 @@ export class MSC4108SignInWithQR {
* Returns true if the device is the already logged in device reciprocating a new login on the other side of the channel.
*/
public get isExistingDevice(): boolean {
return this.ourIntent === QrCodeMode.Reciprocate;
return this.ourIntent === QrCodeIntent.Reciprocate;
}
/**
@@ -19,7 +19,7 @@ import {
Ecies,
type EstablishedEcies,
QrCodeData,
QrCodeMode,
QrCodeIntent,
} from "@matrix-org/matrix-sdk-crypto-wasm";
import {
@@ -56,9 +56,9 @@ export class MSC4108SecureChannel {
* @param mode the mode to generate the QR code in, either `Login` or `Reciprocate`.
* @param serverName the name of the homeserver to connect to, as defined by server discovery in the spec, required for `Reciprocate` mode.
*/
public async generateCode(mode: QrCodeMode.Login): Promise<Uint8Array>;
public async generateCode(mode: QrCodeMode.Reciprocate, serverName: string): Promise<Uint8Array>;
public async generateCode(mode: QrCodeMode, serverName?: string): Promise<Uint8Array> {
public async generateCode(mode: QrCodeIntent.Login): Promise<Uint8Array>;
public async generateCode(mode: QrCodeIntent.Reciprocate, serverName: string): Promise<Uint8Array>;
public async generateCode(mode: QrCodeIntent, serverName?: string): Promise<Uint8Array> {
const { url } = this.rendezvousSession;
if (!url) {
@@ -68,7 +68,7 @@ export class MSC4108SecureChannel {
return new QrCodeData(
this.secureChannel.public_key(),
url,
mode === QrCodeMode.Reciprocate ? serverName : undefined,
mode === QrCodeIntent.Reciprocate ? serverName : undefined,
).toBytes();
}
+31 -10
View File
@@ -161,7 +161,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
/**
* Handles a backup secret received event and store it if it matches the current backup version.
*
* @param secret - The secret as received from a `m.secret.send` event for secret `m.megolm_backup.v1`.
* @param secret - The secret as received from a `m.secret.send` or `io.element.msc4385.secret.push` event for secret `m.megolm_backup.v1`.
* @returns true if the secret is valid and has been stored, false otherwise.
*/
public async handleBackupSecretReceived(secret: string): Promise<boolean> {
@@ -180,28 +180,44 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
// There is no server-side key backup.
// This decryption key is useless to us.
this.logger.warn(
"handleBackupSecretReceived: Received a backup decryption key, but there is no trusted server-side key backup",
"handleBackupSecretReceived: Received a backup decryption key, but there is no server-side key backup",
);
return false;
}
let backupDecryptionKey: RustSdkCryptoJs.BackupDecryptionKey;
try {
backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(secret);
} catch (e) {
this.logger.warn("handleBackupSecretReceived: Invalid backup decryption key", e);
return false;
}
try {
const backupDecryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(secret);
const privateKeyMatches = this.backupInfoMatchesBackupDecryptionKey(latestBackupInfo, backupDecryptionKey);
if (!privateKeyMatches) {
this.logger.warn(
`handleBackupSecretReceived: Private decryption key does not match the public key of the current remote backup.`,
`handleBackupSecretReceived: Private decryption key does not match the public key of the current server-side backup version (${latestBackupInfo.version})`,
);
// just ignore the secret
return false;
}
this.logger.info(
`handleBackupSecretReceived: A valid backup decryption key has been received and stored in cache.`,
`handleBackupSecretReceived: Valid decryption key for the current server-side backup version (${latestBackupInfo.version}) received`,
);
await this.saveBackupDecryptionKey(backupDecryptionKey, latestBackupInfo.version);
// Check if the backup should be enabled (e.g. if it's properly
// signed), and enable it if it should
if (this.keyBackupCheckInProgress) {
await this.keyBackupCheckInProgress;
}
this.keyBackupCheckInProgress = this.doCheckKeyBackup(latestBackupInfo).finally(() => {
this.keyBackupCheckInProgress = null;
});
await this.keyBackupCheckInProgress;
return true;
} catch (e) {
this.logger.warn("handleBackupSecretReceived: Invalid backup decryption key", e);
this.logger.warn("handleBackupSecretReceived: Unable to validate backup decryption key", e);
}
return false;
@@ -281,12 +297,17 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
private keyBackupCheckInProgress: Promise<KeyBackupCheck | null> | null = null;
/** Helper for `checkKeyBackup` */
private async doCheckKeyBackup(): Promise<KeyBackupCheck | null> {
/** Helper to check the key backup status, and enable/disable it as appropriate
*
* A KeyBackupInfo can be passed if it was fetched recently, to avoid trying to
* re-fetch it from the server.
*/
private async doCheckKeyBackup(backupInfo?: KeyBackupInfo | null | undefined): Promise<KeyBackupCheck | null> {
this.logger.debug("Checking key backup status...");
let backupInfo: KeyBackupInfo | null | undefined;
try {
backupInfo = await this.requestKeyBackupVersion();
if (!backupInfo) {
backupInfo = await this.requestKeyBackupVersion();
}
} catch (e) {
this.logger.warn("Error checking for active key backup", e);
this.serverBackupInfo = undefined;
+17 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { StoreHandle } from "@matrix-org/matrix-sdk-crypto-wasm";
import { RustCrypto } from "./rust-crypto.ts";
import { MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE, RustCrypto } from "./rust-crypto.ts";
import { type IHttpOpts, type MatrixHttpApi } from "../http-api/index.ts";
import { type ServerSideSecretStorage } from "../secret-storage.ts";
import { type Logger } from "../logger.ts";
@@ -247,5 +247,21 @@ async function initOlmMachine(
}
}
// If we have any recently-joined rooms, see if we have a pending key bundle for them.
for (const pendingDetails of await olmMachine.getAllRoomsPendingKeyBundles()) {
const roomId = pendingDetails.roomId.toString();
if (Date.now() - pendingDetails.inviteAcceptedAtMillis <= MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE) {
logger.info(
`Checking for pending key bundle for recently-joined room ${roomId} (joined ${new Date(pendingDetails.inviteAcceptedAtMillis).toISOString()})`,
);
await rustCrypto.maybeAcceptKeyBundle(roomId, pendingDetails.inviterId.toString());
} else {
logger.info(
`Clearing pending-key-bundle flag for room ${roomId} (too old: joined ${new Date(pendingDetails.inviteAcceptedAtMillis).toISOString()})`,
);
await olmMachine.clearRoomPendingKeyBundle(new RustSdkCryptoJs.RoomId(roomId));
}
}
return rustCrypto;
}
+133 -53
View File
@@ -98,6 +98,7 @@ import { VerificationMethod } from "../types.ts";
import { keyFromAuthData } from "../common-crypto/key-passphrase.ts";
import { type UIAuthCallback } from "../interactive-auth.ts";
import { getHttpUriForMxc } from "../content-repo.ts";
import { type RoomState } from "../matrix.ts";
const ALL_VERIFICATION_METHODS = [
VerificationMethod.Sas,
@@ -111,6 +112,9 @@ interface ISignableObject {
unsigned?: object;
}
/** The maximum time, in milliseconds, since we accepted an invite, that we should accept a key bundle. */
export const MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE = 24 * 60 * 60 * 1000; // 24 hours
/**
* An implementation of {@link CryptoBackend} using the Rust matrix-sdk-crypto.
*
@@ -131,9 +135,6 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/** mapping of roomId → encryptor class */
private roomEncryptors: Record<string, RoomEncryptor> = {};
/** mapping of room ID -> inviter ID for rooms pending MSC4268 key bundles */
private readonly roomsPendingKeyBundles: Map<string, string> = new Map();
private eventDecryptor: EventDecryptor;
private keyClaimManager: KeyClaimManager;
private outgoingRequestProcessor: OutgoingRequestProcessor;
@@ -370,10 +371,10 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/* allowRedirects */ true,
/* useAuthentication */ true,
);
let encryptedBundle: Blob;
let encryptedBundle: Uint8Array;
try {
const bundleUrl = new URL(url);
encryptedBundle = await this.http.authedRequest<Blob>(
const encryptedBundleBlob = await this.http.authedRequest<Blob>(
Method.Get,
bundleUrl.pathname + bundleUrl.search,
{},
@@ -383,17 +384,24 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
prefix: "",
},
);
logger.info(`Received blob of length ${encryptedBundleBlob.size}`);
encryptedBundle = new Uint8Array(await encryptedBundleBlob.arrayBuffer());
} catch (err) {
logger.warn(`Error downloading encrypted bundle from ${url}:`, err);
throw err;
}
logger.info(`Received blob of length ${encryptedBundle.size}`);
try {
await this.olmMachine.receiveRoomKeyBundle(bundleData, new Uint8Array(await encryptedBundle.arrayBuffer()));
await this.olmMachine.receiveRoomKeyBundle(bundleData, encryptedBundle);
} catch (err) {
logger.warn(`Error receiving encrypted bundle:`, err);
throw err;
} finally {
// Even if we were unable to import the bundle, we still clear the flag that indicates that we
// are waiting for the bundle to be received. The only reason this can happen is that the bundle was
// malformed somehow, so we don't want to keep retrying it.
await this.olmMachine.clearRoomPendingKeyBundle(new RustSdkCryptoJs.RoomId(roomId));
}
return true;
@@ -402,8 +410,11 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/**
* Implementation of {@link CryptoBackend.markRoomAsPendingKeyBundle}.
*/
public markRoomAsPendingKeyBundle(roomId: string, inviter: string): void {
this.roomsPendingKeyBundles.set(roomId, inviter);
public async markRoomAsPendingKeyBundle(roomId: string, inviter: string): Promise<void> {
await this.olmMachine.storeRoomPendingKeyBundle(
new RustSdkCryptoJs.RoomId(roomId),
new RustSdkCryptoJs.UserId(inviter),
);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -725,7 +736,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
? userIdentity.identityNeedsUserApproval()
: false;
userIdentity.free();
return new UserVerificationStatus(verified, wasVerified, false, needsUserApproval);
return new UserVerificationStatus(verified, wasVerified, true, needsUserApproval);
}
/**
@@ -781,9 +792,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#getCrossSigningKeyId}
*/
public async getCrossSigningKeyId(type: CrossSigningKey = CrossSigningKey.Master): Promise<string | null> {
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity(
new RustSdkCryptoJs.UserId(this.userId),
);
const userIdentity = await this.getOwnIdentity();
if (!userIdentity) {
// The public keys are not available on this device
return null;
@@ -1012,9 +1021,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#getCrossSigningStatus}
*/
public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | null = await this.getOlmMachineOrThrow().getIdentity(
new RustSdkCryptoJs.UserId(this.userId),
);
const userIdentity = await this.getOwnIdentity();
const publicKeysOnDevice =
Boolean(userIdentity?.masterKey) &&
@@ -1128,9 +1135,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Implementation of {@link CryptoApi#requestVerificationDM}
*/
public async requestVerificationDM(userId: string, roomId: string): Promise<VerificationRequest> {
const userIdentity: RustSdkCryptoJs.OtherUserIdentity | undefined = await this.olmMachine.getIdentity(
new RustSdkCryptoJs.UserId(userId),
);
const userIdentity = (await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(userId))) as
| RustSdkCryptoJs.OtherUserIdentity
| undefined;
if (!userIdentity) throw new Error(`unknown userId ${userId}`);
@@ -1214,9 +1221,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* @returns a VerificationRequest when the request has been sent to the other party.
*/
public async requestOwnUserVerification(): Promise<VerificationRequest> {
const userIdentity: RustSdkCryptoJs.OwnUserIdentity | undefined = await this.olmMachine.getIdentity(
new RustSdkCryptoJs.UserId(this.userId),
);
const userIdentity = await this.getOwnIdentity();
if (userIdentity === undefined) {
throw new Error("cannot request verification for this device when there is no existing cross-signing key");
}
@@ -1371,6 +1376,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
public async resetKeyBackup(): Promise<void> {
const backupInfo = await this.backupManager.setupKeyBackup((o) => this.signObject(o));
await this.pushSecretToVerifiedDevices("m.megolm_backup.v1");
// we want to store the private key in 4S
// need to check if 4S is set up?
if (await this.secretStorageHasAESKey()) {
@@ -1726,34 +1733,37 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
},
});
// If we have received a room key bundle message, and have previously marked the room
// IDs it references as pending key bundles, tell the Rust SDK to try and accept it,
// just in case it was received after invite.
// If we have received a room key bundle message, and have recently joined the room in question,
// tell the Rust SDK to try and accept the key bundle.
//
// We don't actually need to validate the contents of the bundle message, or do
// anything with its contents at all. We simply want to inform the Rust SDK we have
// received a new room key bundle that we might be able to download.
if (
isRoomKeyBundleMessage(parsedMessage) &&
this.roomsPendingKeyBundles.has(parsedMessage.content.room_id)
) {
// No `await`-ing here, as this is called from inside the `/sync` loop.
this.maybeAcceptKeyBundle(
parsedMessage.content.room_id,
this.roomsPendingKeyBundles.get(parsedMessage.content.room_id)!,
).then(
(success) => {
if (success) {
this.roomsPendingKeyBundles.delete(parsedMessage.content.room_id);
}
},
(err) => {
this.logger.error(
`Error attempting to download key bundle for room ${parsedMessage.content.room_id}`,
);
this.logger.error(err);
},
if (isRoomKeyBundleMessage(parsedMessage)) {
const roomId = parsedMessage.content.room_id;
const pendingDetails = await this.olmMachine.getPendingKeyBundleDetailsForRoom(
new RustSdkCryptoJs.RoomId(roomId),
);
// Only accept the key bundle if we joined the room less than 24 hours ago.
if (!pendingDetails) {
this.logger.debug(
`Not yet accepting key bundle for room where we are not awaiting a bundle: ${roomId}`,
);
} else if (
Date.now() - pendingDetails.inviteAcceptedAtMillis >
MAX_INVITE_ACCEPTANCE_MS_FOR_KEY_BUNDLE
) {
this.logger.info(
`Ignoring key bundle for room we joined too long ago: ${roomId}, joining time: ${new Date(pendingDetails.inviteAcceptedAtMillis).toISOString()}`,
);
} else {
this.logger.info(`Considering key bundle for recently-joined room ${roomId}`);
// Don't block for the import to happen, here, as this is called from inside the `/sync` loop.
this.maybeAcceptKeyBundle(roomId, pendingDetails.inviterId.toString()).catch((err) => {
this.logger.error(`Error attempting to download key bundle for room ${roomId}`);
this.logger.error(err);
});
}
}
break;
@@ -1926,7 +1936,21 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* @param oldMembership - The previous membership state. Null if it's a new member.
*/
public onRoomMembership(event: MatrixEvent, member: RoomMember, oldMembership?: string): void {
const enc = this.roomEncryptors[event.getRoomId()!];
const roomId = event.getRoomId()!;
// If it's our own membership, and we are no longer joined, clear any indication that we are waiting for a key
// bundle.
if (
oldMembership === KnownMembership.Join &&
member.membership !== KnownMembership.Join &&
member.userId === this.olmMachine.userId.toString()
) {
this.olmMachine.clearRoomPendingKeyBundle(new RustSdkCryptoJs.RoomId(roomId)).catch((e) => {
this.logger.error(`Error clearing room pending key bundle indicator for ${roomId}: ${e}`);
});
}
const enc = this.roomEncryptors[roomId];
if (!enc) {
// not encrypting in this room
return;
@@ -1934,6 +1958,44 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
enc.onRoomMembership(member);
}
/**
* Previously, it was sufficient to check if we need to rotate the room key
* prior to sending a message. However, the history sharing feature
* (MSC4268) breaks this logic:
*
* 1. Alice sends a message M1 in room X;
* 2. Bob invites Charlie, who joins and immediately leaves the room;
* 3. Alice sends another message M2 in room X.
*
* Under the old logic, Alice would not rotate her key after Charlie
* leaves, resulting in M2 being encrypted with the same session as M1.
* This would allow Charlie to decrypt M2 if he ever gains access to
* the event.
*
* To counter this, we proactively discard any active outgoing Megolm
* session when we see an event indicating the user left.
*
* Note that we have to do this in `onRoomStateEvent` rather than
* `onRoomMembership`, because `onRoomMembership` is only called when we see
* a *change* in membership. In the case of a gappy sync, we might miss
* Charlie's invite and join, and only see the final `leave` event (so his
* membership goes from `leave` to `leave`).
*/
public onRoomStateEvent(event: MatrixEvent, _state: RoomState, _prevEvent: MatrixEvent | null): void {
if (event.getType() != EventType.RoomMember) {
// Ignore all events that aren't member updates.
return;
}
if (
event.getStateKey()! !== this.olmMachine.userId.toString() &&
event.getContent().membership !== KnownMembership.Join
) {
this.logger.info(`Rotating session for room ${event.getRoomId()} due to member leaving the room`);
this.forceDiscardSession(event.getRoomId()!);
}
}
/** Callback for OlmMachine.registerRoomKeyUpdatedCallback
*
* Called by the rust-sdk whenever there is an update to (megolm) room keys. We
@@ -2044,9 +2106,9 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
/**
* Handles secret received from the rust secret inbox.
*
* The gossipped secrets are received using the `m.secret.send` event type
* and are guaranteed to have been received over a 1-to-1 Olm
* Session from a verified device.
* The gossipped secrets are received using the `m.secret.send` or
* `io.element.msc4385.secret.push` event types and are guaranteed to have
* been received over a 1-to-1 Olm Session from a verified device.
*
* The only secret currently handled in this way is `m.megolm_backup.v1`.
*
@@ -2189,7 +2251,22 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, CryptoEventH
* Used during migration from legacy js-crypto to update local trust if needed.
*/
public async getOwnIdentity(): Promise<RustSdkCryptoJs.OwnUserIdentity | undefined> {
return await this.olmMachine.getIdentity(new RustSdkCryptoJs.UserId(this.userId));
const identity = (await this.getOlmMachineOrThrow().getIdentity(new RustSdkCryptoJs.UserId(this.userId))) as
| RustSdkCryptoJs.OwnUserIdentity
| undefined;
return identity;
}
/**
* Push a secret to all of the current user's verified devices.
*/
public async pushSecretToVerifiedDevices(name: string): Promise<void> {
const logger = new LogSpan(this.logger, "pushSecretToVerifiedDevices");
await this.keyClaimManager.ensureSessionsForUsers(logger, [new RustSdkCryptoJs.UserId(this.userId)]);
await this.olmMachine.pushSecretToVerifiedDevices(name);
this.outgoingRequestsManager.doProcessOutgoingRequests().catch((e) => {
logger.warn("pushSecretToVerifiedDevices: Error processing outgoing requests", e);
});
}
}
@@ -2519,7 +2596,7 @@ function rustEncryptionInfoToJsEncryptionInfo(
}
interface RoomKeyBundleMessage {
type: "io.element.msc4268.room_key_bundle";
type: "m.room_key_bundle" | "io.element.msc4268.room_key_bundle";
content: {
room_id: string;
};
@@ -2529,13 +2606,16 @@ interface RoomKeyBundleMessage {
* Determines if the given payload is a RoomKeyBundleMessage.
*
* A RoomKeyBundleMessage is identified by having a specific message type
* ("io.element.msc4268.room_key_bundle") and a valid room_id in its content.
* ("m.room_key_bundle") and a valid room_id in its content.
*
* @param message - The received to-device message to check.
* @returns True if the payload matches the RoomKeyBundleMessage structure, false otherwise.
*/
function isRoomKeyBundleMessage(message: IToDeviceEvent): message is IToDeviceEvent & RoomKeyBundleMessage {
return message.type === "io.element.msc4268.room_key_bundle" && typeof message.content.room_id === "string";
return (
(message.type === "io.element.msc4268.room_key_bundle" || message.type === "m.room_key_bundle") &&
typeof message.content.room_id === "string"
);
}
type CryptoEvents = (typeof CryptoEvent)[keyof typeof CryptoEvent];
+2 -1
View File
@@ -61,6 +61,7 @@ const VERSION = DB_MIGRATIONS.length;
* Return the data you want to keep.
* @returns Promise which resolves to an array of whatever you returned from
* resultMapper.
* @throws If there was an error completing the query.
*/
function selectQuery<T>(
store: IDBObjectStore,
@@ -71,7 +72,7 @@ function selectQuery<T>(
return new Promise((resolve, reject) => {
const results: T[] = [];
query.onerror = (): void => {
reject(new Error("Query failed: " + query.error?.name));
reject(new Error(`selectQuery failed for ${store.name}`, { cause: query.error }));
};
// collect results
query.onsuccess = (): void => {
+1 -2
View File
@@ -21,7 +21,6 @@ limitations under the License.
* This is an internal module. See {@link createNewMatrixCall} for the public API.
*/
import { v4 as uuidv4 } from "uuid";
import { parse as parseSdp, write as writeSdp } from "sdp-transform";
import { logger } from "../logger.ts";
@@ -2490,7 +2489,7 @@ export class MatrixCall extends TypedEventEmitter<CallEvent, CallEventHandlerMap
sender_session_id: this.client.getSessionId(),
dest_session_id: this.opponentSessionId,
seq: toDeviceSeq,
[ToDeviceMessageId]: uuidv4(),
[ToDeviceMessageId]: globalThis.crypto.randomUUID(),
};
this.emit(