Compare commits

...

87 Commits

Author SHA1 Message Date
RiotRobot 9d6210b3f9 v33.0.0-rc.0 2024-05-29 12:54:25 +00:00
Andy Balaam 909caab74e Don't run migration for Rust crypto if the legacy store is empty (#4218)
* Don't run migration for Rust crypto if the legacy store is empty

Fixes https://github.com/element-hq/element-web/issues/27447

* Add copyright for the TypeScript files in legacy DB dumps

* Provide a type for the accountPickle we check for before migration

* Remove redundant backup response

This is unused

* Simplify keys response

* Downgrade log message.

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
2024-05-28 17:43:25 +00:00
Michael Telatynski 7c87625157 Remove more deprecated methods, fields, and exports (#4217) 2024-05-28 09:12:55 +01:00
Richard van der Hoff b19817bb73 Bump matrix-sdk-crypto-wasm to 5.0.0 (#4216)
Slightly more involved than normal because it requires us to pass a backup version into OlmMachine.importBackedUpRoomKeys.

On the other hand we can now re-enable the test that was disabled in #4214 due to matrix-org/matrix-rust-sdk#3447

Fixes: element-hq/element-web#27165
2024-05-24 12:10:52 +01:00
Richard van der Hoff 36196ea422 initRustCrypto: allow app to pass in the store key directly (#4210)
* `initRustCrypto`: allow app to pass in the store key directly

... instead of using the pickleKey. This allows us to avoid a slow PBKDF
operation.

* Fix link in doc-comment
2024-05-24 09:52:34 +00:00
renovate[bot] a81adf542e Update dependency @matrix-org/matrix-sdk-crypto-wasm to v4.10.0 (#4214)
* Update dependency @matrix-org/matrix-sdk-crypto-wasm to v4.10.0

* Disable affected 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>
2024-05-23 11:19:41 +00:00
RiotRobot a49bc3ddf4 Merge branch 'master' into develop 2024-05-22 12:04:33 +00:00
RiotRobot a86d4ceb49 v32.4.0 2024-05-22 12:04:07 +00:00
R Midhun Suresh 8c3be2a56a Update organization (#4212) 2024-05-21 15:55:49 +00:00
RiotRobot 38898a60c7 v32.3.0 2024-05-21 12:24:52 +00:00
Bayyr Oorjak fd3a4d4403 Preserve ESM for async imports to work correctly (#4187)
* fix: fix lazy rust crypto import

* test: use "commonjs" for tests because of circular deps

* chore: revert commonjs for "module"

* refactor: remove unnecessary example

* refactor: add comments

Signed-off-by: Bayyr Oorjak <the.bayyr.oorjak@gmail.com>

* refactor: improve comment

Signed-off-by: Bayyr Oorjak <the.bayyr.oorjak@gmail.com>

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

* Update babel.config.js

---------

Signed-off-by: Bayyr Oorjak <the.bayyr.oorjak@gmail.com>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-05-21 11:48:00 +00:00
RiotRobot 93d96281fd Resetting package fields for development 2024-05-21 12:25:26 +00:00
RiotRobot 944dc51c58 Merge branch 'master' into develop 2024-05-21 12:25:25 +00:00
renovate[bot] c6b43dd176 Update dependency eslint-plugin-unicorn to v53 (#4209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 16:56:22 +00:00
Michael Telatynski f03dd7b7bc Remove deprecated methods and fields (#4201)
* Remove legacy `threepidCreds` field

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

* Remove deprecated shouldUpgradeToVersion

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

* Remove `added` legacy login request field

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

* Remove deprecated re-exports

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

* Remove `home_server` field

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

* Update imports in tests

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-15 15:23:04 +00:00
Michael Telatynski 51fa1866a9 Wire up verification cancel & mismatch for rust crypto (#4202)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-15 14:58:24 +00:00
renovate[bot] d76fb2baa0 Update all non-major dependencies (#4207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 11:51:51 +00:00
renovate[bot] 3feafc9c17 Update dependency typedoc-plugin-mdn-links to v3.1.25 (#4206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 11:28:29 +00:00
Michael Telatynski c9075b3dba Only pass id_server if we had one to begin with (#4200)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-15 10:59:42 +00:00
renovate[bot] 69c474dda7 Update dependency @types/node to v18.19.33 (#4205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 10:54:21 +00:00
renovate[bot] 73ce51065f Update mheap/github-action-required-labels digest to 5847eef (#4204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 10:26:32 +00:00
renovate[bot] 5d0407d0a6 Update actions/checkout digest to 0ad4b8f (#4203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 10:23:58 +00:00
RiotRobot 3a4b02d8e6 v32.3.0-rc.0 2024-05-15 09:04:25 +00:00
Kegan Dougal d421e7f829 Run complement-crypto in CI (#4197)
* Maybe run complement-crypto

* Use existing checkout

* Test that things fail if crypto breaks

* Fix test; run only on merge queue

* Prettier

* Maybe get it working in a merge queue
2024-05-13 10:26:27 +00:00
Michael Telatynski 9fd051af33 Update downstream-end-to-end-tests.yml 2024-05-13 11:20:47 +01:00
Michael Telatynski b78a1ad889 Hotfix types export point
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-10 13:26:07 +01:00
Michael Telatynski a25cdcecaa Fix state_events.ts types (#4196)
* Fix state_events.ts types

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

* Iterate

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-10 10:55:44 +00:00
RiotRobot 2a716bd076 Resetting package fields for development 2024-05-07 12:16:24 +00:00
RiotRobot ef1db8d664 Merge branch 'master' into develop 2024-05-07 12:16:23 +00:00
RiotRobot 22865fd834 v32.2.0 2024-05-07 12:15:48 +00:00
Michael Telatynski c4fe564855 Simplify OIDC types & export decodeIdToken (#4193)
* Fix types

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

* Export `decodeIdToken`

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-07 10:21:57 +00:00
Michael Telatynski 9ecb1a0381 Fix sendEventHttpRequest for m.room.redaction events without redacts (#4192)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-05-07 08:50:37 +00:00
Travis Ralston ef9490c7b1 Add missing (and common-ish) HTTP method verbs to types (#4188) 2024-05-02 23:09:15 +00:00
Travis Ralston 402adfbe8a Add helpers for authenticated media, and associated documentation (#4185)
* Add helpers for authenticated media, and associated documentation

* Appease the linter
2024-05-02 21:11:09 +00:00
renovate[bot] 41e8c2af34 Update dependency typedoc-plugin-coverage to v3.1.1 (#4186)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-01 10:23:17 +00:00
renovate[bot] 4843b40296 Update dependency typedoc-plugin-mdn-links to v3.1.22 (#4182)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 13:18:45 +00:00
renovate[bot] bc2c870152 Update babel monorepo to v7.24.5 (#4181)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 13:17:37 +00:00
renovate[bot] 7c7b2817d3 Update all non-major dependencies (#4184)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-30 13:16:31 +00:00
renovate[bot] 9f78202ecd Update typescript-eslint monorepo to v7.7.1 (#4183)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-04-30 14:40:57 +01:00
RiotRobot abc9911e95 v32.2.0-rc.0 2024-04-30 12:02:20 +00:00
David Baker efdae0d66f Remove spammy RTCSession log line (#4180)
This can get quite spammy in rooms with legacy calls and isn't really
neccesary: remove it to cut down on the log spam.
2024-04-30 10:43:20 +00:00
David Baker 2bf554761c Add some comments to the release drafter workflows (#4140)
* Add some comments to the release drafter workflows

* Rename component workflow so they have different names

* Fix comment

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2024-04-30 09:38:07 +00:00
Michael Telatynski c2687643b5 Update downstream-end-to-end-tests.yml 2024-04-30 10:30:19 +01:00
Michael Telatynski a33758eda6 Update downstream-end-to-end-tests.yml 2024-04-30 10:28:44 +01:00
Michael Telatynski 8faed02cc5 Update sonarcloud.yml 2024-04-29 17:22:32 +01:00
Michael Telatynski 3ae0dab47a Update sonarcloud.yml 2024-04-29 16:59:55 +01:00
Michael Telatynski 95394e4cbe Update sonarcloud.yml 2024-04-29 16:52:54 +01:00
Michael Telatynski 8c9bbc01fc Update downstream-end-to-end-tests.yml 2024-04-29 16:20:24 +01:00
Michael Telatynski eb888791a3 Pass all args to Sonar via sonar-project.properties file (#4179)
* Pass all args to Sonar via sonar-project.properties file

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

* Update Sonarcloud action to v3.1

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

* Apply suggestions from code review

Co-authored-by: davidegirardi <16451191+davidegirardi@users.noreply.github.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: davidegirardi <16451191+davidegirardi@users.noreply.github.com>
2024-04-29 15:12:00 +00:00
Michael Telatynski 6c0b2f55e1 Delete .github/workflows/downstream-artifacts.yml 2024-04-29 10:53:12 +01:00
Michael Telatynski c9a5eaece3 Simplify Playwright CI (#4178) 2024-04-29 08:49:00 +01:00
Hubert Chathi 64505de36b Use a different error code for UTDs when user was not in the room (#4172)
* use a different error code for UTDs when user was not in the room

* if user is invited, treat it as unexpected UTD
2024-04-26 13:38:10 +00:00
Valere 65d858f9a3 Fix rust migration when ssss secret not encrypted (#4168) 2024-04-26 12:59:17 +00:00
RiotRobot 1da5e8f56a Resetting package fields for development 2024-04-23 12:34:26 +00:00
RiotRobot 5efd4c2915 Merge branch 'master' into develop 2024-04-23 12:34:25 +00:00
RiotRobot bc03950f8a v32.1.0 2024-04-23 12:33:56 +00:00
Travis Ralston c09da9a23f Modernize window.crypto access constants (#4169)
* Force service worker-safe crypto when operating under a service worker

* Fix tests/unsafe execution

* Further fix tests?

* Docs would probably be good

* Define a type guard function

https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards

* Use `@types` repo

* Maybe don't modify tsconfig, I guess

* Revert "Use `@types` repo"

This reverts commit db46bcf1db4b94fbc7e0c97a20d5d800fcb2768b.

* Use a different type for Window

* Simplify the crypto accessors
2024-04-22 19:44:01 +00:00
Andrew Ferrazzutti e874468ba3 Improve compliance with MSC3266 (#4155)
* Fix fields of MSC 3266 summary object

Also remove redundant room_type field which is inherited from elsewhere

* Export the MSC 3266 summary type

* Use proper endpoint for MSC 3266 summary lookup

Use the endpoint recommended by the MSC

* Rename newly-exported symbol to not start with I

* Use "export type"

* Lint

* Fix type of "encryption" field

* Add TSDoc documentation

* Add basic integration test for getRoomSummary

* Lint

* Use fallback endpoint for MSC3266

* Improve test coverage

* Lint

* Refactor async catch to satisfy linter

* Increase test coverage
2024-04-22 17:39:10 +00:00
Johannes Marbach 6fedda91f9 Use encoded URI components when storing sessions in memory crypto store (#4170)
* Use encoded URI components when storing sessions in memory crypto store

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>

* Add URI en-/decoding to missing methods

* Extract convenience functions

---------

Signed-off-by: Johannes Marbach <n0-0ne+github@mailbox.org>
2024-04-22 08:12:54 +00:00
Valere d22a39f5d7 Element R: fix isCrossSigningReady not checking identity trust (#4156)
* Fix inconsistency between rust and legacy

* Add tests

* Review: better comment

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

* review: Better doc

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

* Simplify test data and some comments

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-04-17 13:36:52 +00:00
Timo 4fc6ba884e add comment to make clear that RoomStateEvent.Events does not update related objects in the js-sdk (#4152)
Signed-off-by: Timo K <toger5@hotmail.de>
2024-04-17 10:28:40 +00:00
Richard van der Hoff c30e498013 Crypto: use a new error code for UTDs from device-relative historical events (#4139)
* Add `PerSessionKeyBackupDownloader.isKeyBackupDownloadConfigured()`

* Add new `RustBackupManager.getServerBackupInfo`

... and a convenience method in PerSessionKeyBackupDownloader to access it.

* Crypto.spec: move `useRealTimers` to global `afterEach`

... so that we don't need to remember to do it everywhere.

* Use fake timers for UTD error code tests

This doesn't have any effect on the tests, but *does* stop jest from hanging
when you run the tests in in-band mode. It shouldn't *really* be needed, but
using fake timers gives more reproducible tests, and I don't have the
time/patience to debug why it is needed.

* Use new error codes for UTDs from historical events
2024-04-17 10:26:41 +00:00
renovate[bot] 8240bf0ae7 Update shogo82148/actions-upload-release-asset digest to 8f032ef (#4157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 15:59:04 +00:00
renovate[bot] 2321c44687 Update typedoc (#4162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 15:22:17 +00:00
renovate[bot] 0137e9d5a8 Update dependency eslint-plugin-unicorn to v52 (#4166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 14:29:48 +00:00
renovate[bot] 28bbc51752 Update dependency @types/node to v18.19.31 (#4160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 14:06:49 +00:00
renovate[bot] 0db3ac9b43 Update dependency eslint-plugin-jest to v28 (#4165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:56:50 +00:00
renovate[bot] 53039b78ee Update all non-major dependencies (#4158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:25:06 +00:00
renovate[bot] 2a06d19431 Update babel monorepo to v7.24.4 (#4159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:24:46 +00:00
renovate[bot] a747eef04c Update dependency typescript to v5.4.5 (#4161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:24:27 +00:00
renovate[bot] 583823c2ef Update dependency knip to v5 (#4167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:21:14 +00:00
renovate[bot] 26d13c15c3 Update typescript-eslint monorepo to v7.6.0 (#4163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-16 13:20:45 +00:00
RiotRobot c850ca3179 v32.1.0-rc.0 2024-04-16 12:16:13 +00:00
Valere 8438533532 Validate backup private key before migrating it (#4114)
* Migrate own identity trust to rust crypto

* Fix gendoc not happy if msk of IDownloadKeyResult has a signature

* add missing mock

* code review

* Code review

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* Review gh suggestion

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

* review move function down in file

* Review gh suggestion

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

* Review gh suggestion

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

* Review: Cleaning tests, renaming

* Review: better comment

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

* Comment paragraphs

* retry until initial  key query is successfull

* Validate backup private key before migrating it

* post merge fix

* Fix test, missing mock

* Use crypto wasm instead of lib olm to check backup key

* typo

* code review

* quick lint

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2024-04-15 07:17:14 +00:00
David Baker 475f82c5ce Update git-get-release action (#4148)
which appears to be breaking renovate somehow, and could probably use
and update anyway.
2024-04-11 09:36:07 +00:00
Hubert Chathi 936e7c3072 Add support for device dehydration v2 (Element R) (#4062)
* initial implementation of device dehydration

* add dehydrated flag for devices

* add missing dehydration.ts file, add test, add function to schedule dehydration

* add more dehydration utility functions

* stop scheduled dehydration when crypto stops

* bump matrix-crypto-sdk-wasm version, and fix tests

* adding dehydratedDevices member to mock OlmDevice isn't necessary any more

* fix yarn lock file

* more tests

* fix test

* more tests

* fix typo

* fix logic for checking if dehydration supported

* make changes from review

* add missing file

* move setup into another function

* apply changes from review

* implement simpler API

* fix type and move the code to the right spot

* apply suggestions from review

* make sure that cross-signing and secret storage are set up
2024-04-11 04:01:47 +00:00
RiotRobot 82ed7bd86a Resetting package fields for development 2024-04-09 10:07:09 +00:00
RiotRobot cb67eae858 Merge branch 'master' into develop 2024-04-09 10:07:07 +00:00
Michael Telatynski 5cdd524da7 OIDC improvements in prep of OIDC-QR reciprocation (#4149)
* Add `device_authorization_endpoint` field to OIDC issuer well-known metadata

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

* Allow `validateIdToken` to skip handling nonce when none is present

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

* Tweak registerOidcClient to check OIDC grant_types_supported before registration

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2024-04-08 14:07:00 +00:00
Michael Telatynski 0ff0093380 Add knip unused code & dependency analyser (#4013) 2024-04-08 11:04:40 +01:00
Valere b352405c89 ElementR| Retry query backup until it works during migration to avoid spurious correption error popup (#4113)
* retry query backup until it works during migration

* Add log line when fails to get backup during migration
2024-04-08 07:11:06 +00:00
renovate[bot] 0d73d0c6c7 Update typescript-eslint monorepo to v7.4.0 (#4146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 09:49:35 +00:00
renovate[bot] d2f76d4956 Update dependency typescript to v5.4.3 (#4145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 09:23:09 +00:00
renovate[bot] c680dd7eb2 Update dependency @types/node to v18.19.28 (#4144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 08:37:32 +00:00
renovate[bot] e24bb0f50c Update babel monorepo to v7.24.3 (#4143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-03 08:36:51 +00:00
renovate[bot] 1ed3b13f0d Update all non-major dependencies (#4142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 17:58:13 +00:00
renovate[bot] 4f628bf64c Update mheap/github-action-required-labels digest to 132879b (#4141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-04-02 17:08:08 +00:00
96 changed files with 3166 additions and 1289 deletions
-22
View File
@@ -1,22 +0,0 @@
{
"sourceMaps": true,
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": 10
},
"modules": "commonjs"
}
],
"@babel/preset-typescript"
],
"plugins": [
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime"
]
}
@@ -22,7 +22,7 @@ runs:
- name: Upload tarball signature
if: ${{ inputs.upload-url }}
uses: shogo82148/actions-upload-release-asset@5bd52f05dd8076794da5975d4c0a4f3bce7dd8f5 # v1
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # 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@5bd52f05dd8076794da5975d4c0a4f3bce7dd8f5 # v1
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}.asc
- name: Upload assets
uses: shogo82148/actions-upload-release-asset@5bd52f05dd8076794da5975d4c0a4f3bce7dd8f5 # v1
uses: shogo82148/actions-upload-release-asset@8f032eff0255912cc9c8455797fd6d72f25c7ab7 # v1
with:
upload_url: ${{ inputs.upload-url }}
asset_path: ${{ inputs.asset-path }}
@@ -1,22 +0,0 @@
name: Build downstream artifacts
on:
merge_group:
types: [checks_requested]
pull_request: {}
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
# to develop or master.
#
#push:
# branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-element-web:
name: Build element-web
uses: matrix-org/matrix-react-sdk/.github/workflows/element-web.yaml@v3.95.0
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
@@ -3,10 +3,16 @@
name: matrix-react-sdk End to End Tests
on:
workflow_run:
workflows: ["Build downstream artifacts"]
types:
- completed
merge_group:
types: [checks_requested]
pull_request: {}
# For now at least, we don't run this or the downstream-end-to-end-tests against pushes
# to develop or master.
#
#push:
# branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
@@ -15,44 +21,14 @@ concurrency:
jobs:
playwright:
name: Playwright
# We only want to run the playwright tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume multiple concurrent runners.
if: github.event.workflow_run.event == 'merge_group'
uses: matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml@develop
permissions:
actions: read
issues: read
statuses: write
pull-requests: read
deployments: write
with:
matrix-js-sdk-sha: ${{ github.sha }}
react-sdk-repository: matrix-org/matrix-react-sdk
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
# We want to make the Playwright tests a required check for the merge queue.
#
# Unfortunately, GitHub doesn't distinguish between "checks needed for branch
# protection" (ie, the things that must pass before the PR will even be added
# to the merge queue) and "checks needed in the merge queue". We just have to add
# the check to the branch protection list.
#
# Ergo, if we know we're not going to run the Playwright tests, we need to add a
# passing status check manually.
mark_skipped:
if: github.event.workflow_run.event != 'merge_group'
permissions:
statuses: write
runs-on: ubuntu-latest
steps:
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
with:
authToken: "${{ secrets.GITHUB_TOKEN }}"
state: success
description: Playwright skipped
# Keep in step with the `context` that is updated by `Sibz/github-status-action`
# in matrix-org/matrix-react-sdk/.github/workflows/end-to-end-tests.yaml.
context: "${{ github.workflow }} / end-to-end-tests"
sha: "${{ github.event.workflow_run.head_sha }}"
# We only want to run the playwright tests on merge queue to prevent regressions
# from creeping in. They take a long time to run and consume multiple concurrent runners.
skip: ${{ github.event_name != 'merge_group' }}
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
fail-fast: false
matrix:
include:
- repo: vector-im/element-web
- repo: element-hq/element-web
event: element-web-notify
- repo: matrix-org/matrix-react-sdk
event: upstream-sdk-notify
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
name: Preview Changelog
runs-on: ubuntu-latest
steps:
- uses: mheap/github-action-required-labels@80a96a4863886addcbc9f681b5b295ba7f5424e1 # v5
- uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce # v5
if: github.event_name != 'merge_group'
with:
labels: |
@@ -1,4 +1,5 @@
name: Release Drafter
# Workflow used by other workflows to generate draft releases.
name: Release Drafter Reusable
on:
workflow_call:
inputs:
+4
View File
@@ -1,3 +1,7 @@
# Generates the draft release for the js-sdk
# Normally triggered whenever anything is merged to the staging branch, but
# also has a workflow dispatch trigger in case it needs running manually due
# to failures / workflow updates etc.
name: Release Drafter
on:
push:
+1 -1
View File
@@ -59,7 +59,7 @@ jobs:
- name: Get draft release
id: draft-release
uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada # v1
uses: cardinalby/git-get-release-action@5172c3a026600b1d459b117738c605fabc9e4e44 # v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- name: 🚀 Publish to npm
id: npm-publish
uses: JS-DevTools/npm-publish@4b07b26a2f6e0a51846e1870223e545bae91c552 # v3.0.1
uses: JS-DevTools/npm-publish@19c28f1ef146469e409470805ea4279d47c3d35c # v3.1.1
with:
token: ${{ secrets.NPM_TOKEN }}
access: public
+6 -14
View File
@@ -12,10 +12,6 @@ on:
type: boolean
required: false
description: "Whether to combine multiple LCOV and jest-sonar-report files in coverage artifact"
extra_args:
type: string
required: false
description: "Extra args to pass to SonarCloud"
jobs:
sonarqube:
runs-on: ubuntu-latest
@@ -34,7 +30,7 @@ jobs:
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: "🧮 Checkout code"
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
with:
repository: ${{ github.event.workflow_run.head_repository.full_name }}
ref: ${{ github.event.workflow_run.head_branch }} # checkout commit that triggered this workflow
@@ -60,14 +56,14 @@ jobs:
- id: extra_args
run: |
coverage=$(find coverage -type f -name '*lcov.info' | tr '\n' ',' | sed 's/,$//g')
echo "reportPaths=$coverage" >> $GITHUB_OUTPUT
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' | tr '\n' ',' | sed 's/,$//g')
echo "testExecutionReportPaths=$reports" >> $GITHUB_OUTPUT
coverage=$(find coverage -type f -name '*lcov.info' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.javascript.lcov.reportPaths=$coverage" >> sonar-project.properties
reports=$(find coverage -type f -name 'jest-sonar-report*.xml' -printf '%h/%f,' | tr -d '\r\n' | sed 's/,$//g')
echo "sonar.testExecutionReportPaths=$reports" >> sonar-project.properties
- name: "🩻 SonarCloud Scan"
id: sonarcloud
uses: matrix-org/sonarcloud-workflow-action@v2.7
uses: matrix-org/sonarcloud-workflow-action@v3.2
# 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:
@@ -78,10 +74,6 @@ jobs:
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.SONAR_TOKEN }}
extra_args: |
${{ inputs.extra_args }}
-Dsonar.javascript.lcov.reportPaths=${{ steps.extra_args.outputs.reportPaths }}
-Dsonar.testExecutionReportPaths=${{ steps.extra_args.outputs.testExecutionReportPaths }}
- uses: Sibz/github-status-action@071b5370da85afbb16637d6eed8524a06bc2053e # v1
if: always()
+16
View File
@@ -90,3 +90,19 @@ jobs:
path: _docs
# We'll only use this in a workflow_run, then we're done with it
retention-days: 1
analyse_dead_code:
name: "Analyse Dead Code"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: "yarn"
- name: Install Deps
run: "yarn install --frozen-lockfile"
- name: Run linter
run: "yarn run lint:knip"
+18
View File
@@ -71,6 +71,24 @@ jobs:
disable_coverage: true
matrix-js-sdk-sha: ${{ github.sha }}
complement-crypto:
name: "Run Complement Crypto tests"
if: github.event_name == 'merge_group'
uses: matrix-org/complement-crypto/.github/workflows/single_sdk_tests.yml@main
with:
use_js_sdk: "."
# we need this so the job is reported properly when run in a merge queue
downstream-complement-crypto:
name: Downstream Complement Crypto tests
runs-on: ubuntu-latest
if: always()
needs:
- complement-crypto
steps:
- if: needs.complement-crypto.result != 'skipped' && needs.complement-crypto.result != 'success'
run: exit 1
# Hook for branch protection to skip downstream testing outside of merge queues
# and skip sonarcloud coverage within merge queues
downstream:
+1 -1
View File
@@ -6,6 +6,6 @@ on:
jobs:
call-triage-labelled:
uses: vector-im/element-web/.github/workflows/triage-labelled.yml@develop
uses: element-hq/element-web/.github/workflows/triage-labelled.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
+46
View File
@@ -1,3 +1,49 @@
Changes in [32.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.4.0) (2024-05-22)
==================================================================================================
* No changes
Changes in [32.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.3.0) (2024-05-21)
==================================================================================================
## ✨ Features
* Simplify OIDC types \& export `decodeIdToken` ([#4193](https://github.com/matrix-org/matrix-js-sdk/pull/4193)). Contributed by @t3chguy.
* Add helpers for authenticated media, and associated documentation ([#4185](https://github.com/matrix-org/matrix-js-sdk/pull/4185)). Contributed by @turt2live.
## 🐛 Bug Fixes
* Fix state\_events.ts types ([#4196](https://github.com/matrix-org/matrix-js-sdk/pull/4196)). Contributed by @t3chguy.
* Fix sendEventHttpRequest for `m.room.redaction` events without `redacts` ([#4192](https://github.com/matrix-org/matrix-js-sdk/pull/4192)). Contributed by @t3chguy.
Changes in [32.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.2.0) (2024-05-07)
==================================================================================================
## ✨ Features
* Use a different error code for UTDs when user was not in the room ([#4172](https://github.com/matrix-org/matrix-js-sdk/pull/4172)). Contributed by @uhoreg.
* Modernize window.crypto access constants ([#4169](https://github.com/matrix-org/matrix-js-sdk/pull/4169)). Contributed by @turt2live.
* Improve compliance with MSC3266 ([#4155](https://github.com/matrix-org/matrix-js-sdk/pull/4155)). Contributed by @AndrewFerr.
* Add comment to make clear that RoomStateEvent.Events does not update related objects in the js-sdk ([#4152](https://github.com/matrix-org/matrix-js-sdk/pull/4152)). Contributed by @toger5.
* Crypto: use a new error code for UTDs from device-relative historical events ([#4139](https://github.com/matrix-org/matrix-js-sdk/pull/4139)). Contributed by @richvdh.
## 🐛 Bug Fixes
* Element-R: Fix rust migration when ssss secret are stored not encryted in cache (old legacy behavior) ([#4168](https://github.com/matrix-org/matrix-js-sdk/pull/4168)). Contributed by @BillCarsonFr.
Changes in [32.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.1.0) (2024-04-23)
==================================================================================================
## ✨ Features
* Add support for device dehydration v2 (Element R) ([#4062](https://github.com/matrix-org/matrix-js-sdk/pull/4062)). Contributed by @uhoreg.
* OIDC improvements in prep of OIDC-QR reciprocation ([#4149](https://github.com/matrix-org/matrix-js-sdk/pull/4149)). Contributed by @t3chguy.
## 🐛 Bug Fixes
* Validate backup private key before migrating it ([#4114](https://github.com/matrix-org/matrix-js-sdk/pull/4114)). Contributed by @BillCarsonFr.
* ElementR| Retry query backup until it works during migration to avoid spurious correption error popup ([#4113](https://github.com/matrix-org/matrix-js-sdk/pull/4113)). Contributed by @BillCarsonFr.
Changes in [32.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v32.0.0) (2024-04-09)
==================================================================================================
## 🚨 BREAKING CHANGES
+32
View File
@@ -21,6 +21,10 @@ endpoints from before Matrix 1.1, for example.
# Quickstart
> [!IMPORTANT]
> Servers may require or use authenticated endpoints for media (images, files, avatars, etc). See the
> [Authenticated Media](#authenticated-media) section for information on how to enable support for this.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
if you do not have it already.
@@ -89,6 +93,34 @@ Object.keys(client.store.rooms).forEach((roomId) => {
});
```
## Authenticated media
Servers supporting [MSC3916](https://github.com/matrix-org/matrix-spec-proposals/pull/3916) will require clients, like
yours, to include an `Authorization` header when `/download`ing or `/thumbnail`ing media. For NodeJS environments this
may be as easy as the following code snippet, though web browsers may need to use [Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
to append the header when using the endpoints in `<img />` elements and similar.
```javascript
const downloadUrl = client.mxcUrlToHttp(
/*mxcUrl=*/ "mxc://example.org/abc123", // the MXC URI to download/thumbnail, typically from an event or profile
/*width=*/ undefined, // part of the thumbnail API. Use as required.
/*height=*/ undefined, // part of the thumbnail API. Use as required.
/*resizeMethod=*/ undefined, // part of the thumbnail API. Use as required.
/*allowDirectLinks=*/ false, // should generally be left `false`.
/*allowRedirects=*/ true, // implied supported with authentication
/*useAuthentication=*/ true, // the flag we're after in this example
);
const img = await fetch(downloadUrl, {
headers: {
Authorization: `Bearer ${client.getAccessToken()}`,
},
});
// Do something with `img`.
```
> [!WARNING]
> In future the js-sdk will _only_ return authentication-required URLs, mandating population of the `Authorization` header.
## What does this SDK do?
This SDK provides a full object model around the Matrix Client-Server API and emits
+26
View File
@@ -0,0 +1,26 @@
module.exports = {
sourceMaps: true,
presets: [
[
"@babel/preset-env",
{
targets: {
esmodules: true,
},
// We want to output ES modules for the final build (mostly to ensure that
// async imports work correctly). However, jest doesn't support ES modules very
// well yet (see https://github.com/jestjs/jest/issues/9430), so we use commonjs
// when testing.
modules: process.env.NODE_ENV === "test" ? "commonjs" : false,
},
],
"@babel/preset-typescript",
],
plugins: [
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime",
],
};
+36
View File
@@ -0,0 +1,36 @@
import { KnipConfig } from "knip";
export default {
entry: [
"src/index.ts",
"src/types.ts",
"src/browser-index.ts",
"src/indexeddb-worker.ts",
"scripts/**",
"spec/**",
"release.sh",
// For now, we include all source files as entrypoints as we have been bad about gutwrenched imports
"src/**",
],
project: ["**/*.{js,ts}"],
ignore: ["examples/**"],
ignoreDependencies: [
// Required for `action-validator`
"@action-validator/*",
// Used for git pre-commit hooks
"husky",
// Used in script which only runs in environment with `@octokit/rest` installed
"@octokit/rest",
// Used by jest
"jest-environment-jsdom",
"babel-jest",
"ts-node",
// Used by `@babel/plugin-transform-runtime`
"@babel/runtime",
],
ignoreBinaries: [
// Used when available by reusable workflow `.github/workflows/release-make.yml`
"dist",
],
ignoreExportsUsedInFile: true,
} satisfies KnipConfig;
+7 -6
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "32.0.0",
"version": "33.0.0-rc.0",
"description": "Matrix Client-Server SDK for Javascript",
"engines": {
"node": ">=18.0.0"
@@ -19,6 +19,7 @@
"lint:js-fix": "prettier --log-level=warn --write . && eslint --fix src spec",
"lint:types": "tsc --noEmit",
"lint:workflows": "find .github/workflows -type f \\( -iname '*.yaml' -o -iname '*.yml' \\) | xargs -I {} sh -c 'echo \"Linting {}\"; action-validator \"{}\"'",
"lint:knip": "knip",
"test": "jest",
"test:watch": "jest --watch",
"coverage": "yarn test --coverage"
@@ -52,7 +53,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-wasm": "^4.9.0",
"@matrix-org/matrix-sdk-crypto-wasm": "^5.0.0",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
@@ -80,7 +81,6 @@
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@matrix-org/olm": "3.2.15",
"@peculiar/webcrypto": "^1.4.5",
@@ -102,11 +102,11 @@
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.5.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jest": "^27.1.6",
"eslint-plugin-jest": "^28.0.0",
"eslint-plugin-jsdoc": "^48.0.0",
"eslint-plugin-matrix-org": "^1.0.0",
"eslint-plugin-tsdoc": "^0.2.17",
"eslint-plugin-unicorn": "^51.0.0",
"eslint-plugin-unicorn": "^53.0.0",
"fake-indexeddb": "^5.0.2",
"fetch-mock": "9.11.0",
"fetch-mock-jest": "^1.5.1",
@@ -115,12 +115,13 @@
"jest-environment-jsdom": "^29.0.0",
"jest-localstorage-mock": "^2.4.6",
"jest-mock": "^29.0.0",
"knip": "^5.0.0",
"lint-staged": "^15.0.2",
"matrix-mock-request": "^2.5.0",
"node-fetch": "^2.7.0",
"prettier": "3.2.5",
"rimraf": "^5.0.0",
"ts-node": "^10.9.1",
"ts-node": "^10.9.2",
"typedoc": "^0.25.10",
"typedoc-plugin-coverage": "^3.0.0",
"typedoc-plugin-mdn-links": "^3.0.3",
+61
View File
@@ -347,6 +347,67 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("cross-signing (%s)", (backend: s
expect(isCrossSigningReady).toBeTruthy();
});
it("should return false if identity is not trusted, even if the secrets are in 4S", async () => {
e2eKeyResponder.addCrossSigningData(SIGNED_CROSS_SIGNING_KEYS_DATA);
// Complete initial sync, to get the 4S account_data events stored
mockInitialApiRequests(aliceClient.getHomeserverUrl());
// For this test we need to have a well-formed 4S setup.
const mockSecretInfo = {
encrypted: {
// Don't care about the actual values here, just need to be present for validation
KeyId: {
iv: "IVIVIVIVIVIVIV",
ciphertext: "CIPHERTEXTB64",
mac: "MACMACMAC",
},
},
};
syncResponder.sendOrQueueSyncResponse({
next_batch: 1,
account_data: {
events: [
{
type: "m.secret_storage.key.KeyId",
content: {
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
// iv and mac not relevant for this test
},
},
{
type: "m.secret_storage.default_key",
content: {
key: "KeyId",
},
},
{
type: "m.cross_signing.master",
content: mockSecretInfo,
},
{
type: "m.cross_signing.user_signing",
content: mockSecretInfo,
},
{
type: "m.cross_signing.self_signing",
content: mockSecretInfo,
},
],
},
});
await aliceClient.startClient();
await syncPromise(aliceClient);
// Sanity: ensure that the secrets are in 4S
const status = await aliceClient.getCrypto()!.getCrossSigningStatus();
expect(status.privateKeysInSecretStorage).toBeTruthy();
const isCrossSigningReady = await aliceClient.getCrypto()!.isCrossSigningReady();
expect(isCrossSigningReady).toBeFalsy();
});
});
describe("getCrossSigningKeyId", () => {
+166 -19
View File
@@ -101,6 +101,7 @@ import {
} from "./olm-utils";
import { ToDevicePayload } from "../../../src/models/ToDeviceMessage";
import { AccountDataAccumulator } from "../../test-utils/AccountDataAccumulator";
import { UNSIGNED_MEMBERSHIP_FIELD } from "../../../src/@types/event";
import { KnownMembership } from "../../../src/@types/membership";
afterEach(() => {
@@ -108,6 +109,8 @@ afterEach(() => {
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
// eslint-disable-next-line no-global-assign
indexedDB = new IDBFactory();
jest.useRealTimers();
});
/**
@@ -467,6 +470,10 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("Unable to decrypt error codes", function () {
beforeEach(() => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
});
it("Decryption fails with UISI error", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -474,11 +481,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Ensure that the timestamp post-dates the creation of our device
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now(),
};
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } },
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
},
},
};
@@ -498,12 +511,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
await aliceClient.getCrypto()!.importRoomKeys([testData.RATCHTED_MEGOLM_SESSION_DATA]);
// Alice gets both the events in a single sync
// Ensure that the timestamp post-dates the creation of our device
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now(),
};
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[testData.TEST_ROOM_ID]: { timeline: { events: [testData.ENCRYPTED_EVENT] } },
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
},
},
};
@@ -515,6 +533,151 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.OLM_UNKNOWN_MESSAGE_INDEX);
});
describe("Historical events", () => {
async function sendEventAndAwaitDecryption(props: Partial<IEvent> = {}): Promise<MatrixEvent> {
// A promise which resolves, with the MatrixEvent which wraps the event, once the decryption fails.
const awaitDecryption = emitPromise(aliceClient, MatrixEventEvent.Decrypted);
// Ensure that the timestamp pre-dates the creation of our device: set it to 24 hours ago
const encryptedEvent = {
...testData.ENCRYPTED_EVENT,
origin_server_ts: Date.now() - 24 * 3600 * 1000,
...props,
};
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[testData.TEST_ROOM_ID]: { timeline: { events: [encryptedEvent] } },
},
},
};
syncResponder.sendOrQueueSyncResponse(syncResponse);
return await awaitDecryption;
}
newBackendOnly("fails with HISTORICAL_MESSAGE_BACKUP_NO_BACKUP when there is no backup", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
});
newBackendOnly("fails with HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED when the backup is broken", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(
DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
);
});
newBackendOnly("fails with HISTORICAL_MESSAGE_WORKING_BACKUP when backup is working", async () => {
// The test backup data is signed by a dummy device. We'll need to tell Alice about the device, and
// later, tell her to trust it, so that she trusts the backup.
const e2eResponder = new E2EKeyResponder(aliceClient.getHomeserverUrl());
e2eResponder.addDeviceKeys(testData.SIGNED_TEST_DEVICE_DATA);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
await startClientAndAwaitFirstSync();
await aliceClient
.getCrypto()!
.storeSessionBackupPrivateKey(
Buffer.from(testData.BACKUP_DECRYPTION_KEY_BASE64, "base64"),
testData.SIGNED_BACKUP_DATA.version!,
);
// Tell Alice to trust the dummy device that signed the backup
const devices = await aliceClient.getCrypto()!.getUserDeviceInfo([TEST_USER_ID]);
expect(devices.get(TEST_USER_ID)!.keys()).toContain(testData.TEST_DEVICE_ID);
await aliceClient.getCrypto()!.setDeviceVerified(testData.TEST_USER_ID, testData.TEST_DEVICE_ID);
// Tell Alice to check and enable backup
await aliceClient.getCrypto()!.checkKeyBackupAndEnable();
// Sanity: Alice should now have working backup.
expect(await aliceClient.getCrypto()!.getActiveSessionBackupVersion()).toEqual(
testData.SIGNED_BACKUP_DATA.version,
);
// Finally! we can check what happens when we get an event.
const ev = await sendEventAndAwaitDecryption();
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP);
});
newBackendOnly("fails with NOT_JOINED if user is not member of room", async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "leave",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED);
});
newBackendOnly(
"fails with another error when the server reports user was a member of the room",
async () => {
// This tests that when the server reports that the user
// was invited at the time the event was sent, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error,
// and instead get some other error, since the user should
// have gotten the key for the event.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "invite",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);
newBackendOnly(
"fails with another error when the server reports user was a member of the room",
async () => {
// This tests that when the server reports the user's
// membership, and reports that the user was joined, then we
// don't get a HISTORICAL_MESSAGE_USER_NOT_JOINED error, and
// instead get some other error.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No current backup version." },
});
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
const ev = await sendEventAndAwaitDecryption({
unsigned: {
[UNSIGNED_MEMBERSHIP_FIELD.name]: "join",
},
});
expect(ev.decryptionFailureReason).toEqual(DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP);
},
);
});
it("Decryption fails with Unable to decrypt for other errors", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -997,10 +1160,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
return encryptedMessage;
}
afterEach(() => {
jest.useRealTimers();
});
newBackendOnly("should rotate the session after 2 messages", async () => {
expectAliceKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
await startClientAndAwaitFirstSync();
@@ -2184,10 +2343,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
});
afterEach(() => {
jest.useRealTimers();
});
function awaitKeyUploadRequest(): Promise<{ keysCount: number; fallbackKeysCount: number }> {
return new Promise((resolve) => {
const listener = (url: string, options: RequestInit) => {
@@ -2250,10 +2405,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
});
describe("getUserDeviceInfo", () => {
afterEach(() => {
jest.useRealTimers();
});
// From https://spec.matrix.org/v1.6/client-server-api/#post_matrixclientv3keysquery
// Using extracted response from matrix.org, it needs to have real keys etc to pass old crypto verification
const queryResponseBody = {
@@ -2742,10 +2893,6 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("crypto (%s)", (backend: string,
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
});
afterEach(() => {
jest.useRealTimers();
});
it("Should be able to restore from 4S after bootstrap", async () => {
const backupVersion = "1";
await bootstrapSecurity(backupVersion);
@@ -0,0 +1,181 @@
/*
Copyright 2024 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 "fake-indexeddb/auto";
import fetchMock from "fetch-mock-jest";
import { createClient, ClientEvent, MatrixClient, MatrixEvent } from "../../../src";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { AddSecretStorageKeyOpts } from "../../../src/secret-storage";
import { E2EKeyReceiver } from "../../test-utils/E2EKeyReceiver";
import { E2EKeyResponder } from "../../test-utils/E2EKeyResponder";
describe("Device dehydration", () => {
it("should rehydrate and dehydrate a device", async () => {
jest.useFakeTimers({ doNotFake: ["queueMicrotask"] });
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
cryptoCallbacks: {
getSecretStorageKey: async (keys: any, name: string) => {
return [[...Object.keys(keys.keys)][0], new Uint8Array(32)];
},
},
});
await initializeSecretStorage(matrixClient, "@alice:localhost", "http://test.server");
// count the number of times the dehydration key gets set
let setDehydrationCount = 0;
matrixClient.on(ClientEvent.AccountData, (event: MatrixEvent) => {
if (event.getType() === "org.matrix.msc3814") {
setDehydrationCount++;
}
});
const crypto = matrixClient.getCrypto()!;
fetchMock.config.overwriteRoutes = true;
// start dehydration -- we start with no dehydrated device, and we
// store the dehydrated device that we create
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
let dehydratedDeviceBody: any;
let dehydrationCount = 0;
let resolveDehydrationPromise: () => void;
fetchMock.put("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", (_, opts) => {
dehydratedDeviceBody = JSON.parse(opts.body as string);
dehydrationCount++;
if (resolveDehydrationPromise) {
resolveDehydrationPromise();
}
return {};
});
await crypto.startDehydration();
expect(dehydrationCount).toEqual(1);
// a week later, we should have created another dehydrated device
const dehydrationPromise = new Promise<void>((resolve, reject) => {
resolveDehydrationPromise = resolve;
});
jest.advanceTimersByTime(7 * 24 * 60 * 60 * 1000);
await dehydrationPromise;
expect(dehydrationCount).toEqual(2);
// restart dehydration -- rehydrate the device that we created above,
// and create a new dehydrated device. We also set `createNewKey`, so
// a new dehydration key will be set
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: dehydratedDeviceBody.device_id,
device_data: dehydratedDeviceBody.device_data,
});
const eventsResponse = jest.fn((url, opts) => {
// rehydrating should make two calls to the /events endpoint.
// The first time will return a single event, and the second
// time will return no events (which will signal to the
// rehydration function that it can stop)
const body = JSON.parse(opts.body as string);
const nextBatch = body.next_batch ?? "0";
const events = nextBatch === "0" ? [{ sender: "@alice:localhost", type: "m.dummy", content: {} }] : [];
return {
events,
next_batch: nextBatch + "1",
};
});
fetchMock.post(
`path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/${encodeURIComponent(dehydratedDeviceBody.device_id)}/events`,
eventsResponse,
);
await crypto.startDehydration(true);
expect(dehydrationCount).toEqual(3);
expect(setDehydrationCount).toEqual(2);
expect(eventsResponse.mock.calls).toHaveLength(2);
matrixClient.stopClient();
});
});
/** create a new secret storage and cross-signing keys */
async function initializeSecretStorage(
matrixClient: MatrixClient,
userId: string,
homeserverUrl: string,
): Promise<void> {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
const e2eKeyReceiver = new E2EKeyReceiver(homeserverUrl);
const e2eKeyResponder = new E2EKeyResponder(homeserverUrl);
e2eKeyResponder.addKeyReceiver(userId, e2eKeyReceiver);
fetchMock.post("path:/_matrix/client/v3/keys/device_signing/upload", {});
fetchMock.post("path:/_matrix/client/v3/keys/signatures/upload", {});
const accountData: Map<string, object> = new Map();
fetchMock.get("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
const value = accountData.get(name);
if (value) {
return value;
} else {
return {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
};
}
});
fetchMock.put("glob:http://*/_matrix/client/v3/user/*/account_data/*", (url, opts) => {
const name = url.split("/").pop()!;
const value = JSON.parse(opts.body as string);
accountData.set(name, value);
matrixClient.emit(ClientEvent.AccountData, new MatrixEvent({ type: name, content: value }));
return {};
});
await matrixClient.initRustCrypto();
const crypto = matrixClient.getCrypto()! as RustCrypto;
// we need to process a sync so that the OlmMachine will upload keys
await crypto.preprocessToDeviceMessages([]);
await crypto.onSyncCompleted({});
// create initial secret storage
async function createSecretStorageKey() {
return {
keyInfo: {} as AddSecretStorageKeyOpts,
privateKey: new Uint8Array(32),
};
}
await matrixClient.bootstrapCrossSigning({ setupNewCrossSigning: true });
await matrixClient.bootstrapSecretStorage({
createSecretStorageKey,
setupNewSecretStorage: true,
setupNewKeyBackup: false,
});
}
+13 -3
View File
@@ -21,7 +21,7 @@ import { Mocked } from "jest-mock";
import {
createClient,
CryptoApi,
Crypto,
CryptoEvent,
ICreateClientOpts,
IEvent,
@@ -45,6 +45,7 @@ import { KeyBackupInfo, KeyBackupSession } from "../../../src/crypto-api/keyback
import { IKeyBackup } from "../../../src/crypto/backup";
import { flushPromises } from "../../test-utils/flushPromises";
import { defer, IDeferred } from "../../../src/utils";
import { DecryptionFailureCode } from "../../../src/crypto-api";
const ROOM_ID = testData.TEST_ROOM_ID;
@@ -242,8 +243,17 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
const room = aliceClient.getRoom(ROOM_ID)!;
const event = room.getLiveTimeline().getEvents()[0];
await advanceTimersUntil(awaitDecryption(event, { waitOnDecryptionFailure: true }));
// On the first decryption attempt, decryption fails.
await awaitDecryption(event);
expect(event.decryptionFailureReason).toEqual(
backend === "libolm"
? DecryptionFailureCode.MEGOLM_UNKNOWN_INBOUND_SESSION_ID
: DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
);
// Eventually, decryption succeeds.
await awaitDecryption(event, { waitOnDecryptionFailure: true });
expect(event.getContent()).toEqual(testData.CLEAR_EVENT.content);
});
@@ -300,7 +310,7 @@ describe.each(Object.entries(CRYPTO_BACKENDS))("megolm-keys backup (%s)", (backe
});
describe("recover from backup", () => {
let aliceCrypto: CryptoApi;
let aliceCrypto: Crypto.CryptoApi;
beforeEach(async () => {
fetchMock.get("path:/_matrix/client/v3/room_keys/version", testData.SIGNED_BACKUP_DATA);
+175
View File
@@ -23,6 +23,7 @@ import { populateStore } from "../../test-utils/test_indexeddb_cryptostore_dump"
import { MSK_NOT_CACHED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump";
import { IDENTITY_NOT_TRUSTED_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/unverified";
import { FULL_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/full_account";
import { EMPTY_ACCOUNT_DATASET } from "../../test-utils/test_indexeddb_cryptostore_dump/empty_account";
jest.setTimeout(15000);
@@ -85,6 +86,44 @@ describe("MatrixClient.initRustCrypto", () => {
);
});
it("should create the meta db if given a storageKey", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto({ storageKey: new Uint8Array(32) });
// should have two indexed dbs now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
);
});
it("should create the meta db if given a storagePassword", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: "@alice:localhost",
deviceId: "aliceDevice",
});
// No databases.
expect(await indexedDB.databases()).toHaveLength(0);
await matrixClient.initRustCrypto({ storagePassword: "the cow is on the moon" });
// should have two indexed dbs now
const databaseNames = (await indexedDB.databases()).map((db) => db.name);
expect(databaseNames).toEqual(
expect.arrayContaining(["matrix-js-sdk::matrix-sdk-crypto", "matrix-js-sdk::matrix-sdk-crypto-meta"]),
);
});
it("should ignore a second call", async () => {
const matrixClient = createClient({
baseUrl: "http://test.server",
@@ -162,6 +201,142 @@ describe("MatrixClient.initRustCrypto", () => {
expect(progressListener).toHaveBeenLastCalledWith(-1, -1);
}, 60000);
describe("Private key backup migration", () => {
it("should not migrate the backup private key if backup has changed", async () => {
// Here we have a new backup server side, and the migrated account has the previous backup key.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.newBackupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});
it("should not migrate the backup private key if backup has unknown algorithm", async () => {
// Here we have a new backup server side, and the migrated account has the previous backup key.
const backupResponse = {
...MSK_NOT_CACHED_DATASET.backupResponse,
algorithm: "m.megolm_backup.v8",
};
fetchMock.get("path:/_matrix/client/v3/room_keys/version", backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});
it("should not migrate the backup private key if the backup has been deleted", async () => {
// The old backup has been deleted server side.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "No backup found",
},
});
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeNull();
});
it("should migrate the backup private key if the backup matches", async () => {
// The old backup has been deleted server side.
fetchMock.get("path:/_matrix/client/v3/room_keys/version", MSK_NOT_CACHED_DATASET.backupResponse);
fetchMock.post("path:/_matrix/client/v3/keys/query", MSK_NOT_CACHED_DATASET.keyQueryResponse);
await populateStore("test-store", MSK_NOT_CACHED_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, "test-store");
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: MSK_NOT_CACHED_DATASET.userId,
deviceId: MSK_NOT_CACHED_DATASET.deviceId,
cryptoStore,
pickleKey: MSK_NOT_CACHED_DATASET.pickleKey,
});
await matrixClient.initRustCrypto();
const privateBackupKey = await matrixClient.getCrypto()?.getSessionBackupPrivateKey();
expect(privateBackupKey).toBeDefined();
});
});
it("should not migrate if account data is missing", async () => {
// See https://github.com/element-hq/element-web/issues/27447
// Given we have an almost-empty legacy account in the database
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
status: 404,
body: { errcode: "M_NOT_FOUND", error: "No backup found" },
});
fetchMock.post("path:/_matrix/client/v3/keys/query", EMPTY_ACCOUNT_DATASET.keyQueryResponse);
const testStoreName = "test-store";
await populateStore(testStoreName, EMPTY_ACCOUNT_DATASET.dumpPath);
const cryptoStore = new IndexedDBCryptoStore(indexedDB, testStoreName);
const matrixClient = createClient({
baseUrl: "http://test.server",
userId: EMPTY_ACCOUNT_DATASET.userId,
deviceId: EMPTY_ACCOUNT_DATASET.deviceId,
cryptoStore,
pickleKey: EMPTY_ACCOUNT_DATASET.pickleKey,
});
// When we start Rust crypto, potentially triggering an upgrade
const progressListener = jest.fn();
matrixClient.addListener(CryptoEvent.LegacyCryptoStoreMigrationProgress, progressListener);
await matrixClient.initRustCrypto();
// Then no error occurs, and no upgrade happens
expect(progressListener.mock.calls.length).toBe(0);
}, 60000);
describe("Legacy trust migration", () => {
async function populateAndStartLegacyCryptoStore(dumpPath: string): Promise<IndexedDBCryptoStore> {
const testStoreName = "test-store";
+106 -25
View File
@@ -19,7 +19,16 @@ import { Mocked } from "jest-mock";
import * as utils from "../test-utils/test-utils";
import { CRYPTO_ENABLED, IStoredClientOpts, MatrixClient } from "../../src/client";
import { MatrixEvent } from "../../src/models/event";
import { Filter, KnockRoomOpts, MemoryStore, Method, Room, SERVICE_TYPES } from "../../src/matrix";
import {
Filter,
JoinRule,
KnockRoomOpts,
MemoryStore,
Method,
Room,
RoomSummary,
SERVICE_TYPES,
} from "../../src/matrix";
import { TestClient } from "../TestClient";
import { THREAD_RELATION_TYPE } from "../../src/models/thread";
import { IFilterDefinition } from "../../src/filter";
@@ -1710,6 +1719,102 @@ describe("MatrixClient", function () {
await Promise.all([client.unbindThreePid("email", "alice@server.com"), httpBackend.flushAllExpected()]);
});
});
describe("getRoomSummary", () => {
const roomId = "!foo:bar";
const encodedRoomId = encodeURIComponent(roomId);
const roomSummary: RoomSummary = {
"room_id": roomId,
"name": "My Room",
"avatar_url": "",
"topic": "My room topic",
"world_readable": false,
"guest_can_join": false,
"num_joined_members": 1,
"room_type": "",
"join_rule": JoinRule.Public,
"membership": "leave",
"im.nheko.summary.room_version": "6",
"im.nheko.summary.encryption": "algo",
};
const prefix = "/_matrix/client/unstable/im.nheko.summary/";
const suffix = `summary/${encodedRoomId}`;
const deprecatedSuffix = `rooms/${encodedRoomId}/summary`;
const errorUnrecogStatus = 404;
const errorUnrecogBody = {
errcode: "M_UNRECOGNIZED",
error: "Unsupported endpoint",
};
const errorBadreqStatus = 400;
const errorBadreqBody = {
errcode: "M_UNKNOWN",
error: "Invalid request",
};
it("should respond with a valid room summary object", () => {
httpBackend.when("GET", prefix + suffix).respond(200, roomSummary);
const prom = client.getRoomSummary(roomId).then((response) => {
expect(response).toEqual(roomSummary);
});
httpBackend.flush("");
return prom;
});
it("should allow fallback to the deprecated endpoint", () => {
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
httpBackend.when("GET", prefix + deprecatedSuffix).respond(200, roomSummary);
const prom = client.getRoomSummary(roomId).then((response) => {
expect(response).toEqual(roomSummary);
});
httpBackend.flush("");
return prom;
});
it("should respond to unsupported path with error", () => {
httpBackend.when("GET", prefix + suffix).respond(errorUnrecogStatus, errorUnrecogBody);
httpBackend.when("GET", prefix + deprecatedSuffix).respond(errorUnrecogStatus, errorUnrecogBody);
const prom = client.getRoomSummary(roomId).then(
function (response) {
throw Error("request not failed");
},
function (error) {
expect(error.httpStatus).toEqual(errorUnrecogStatus);
expect(error.errcode).toEqual(errorUnrecogBody.errcode);
expect(error.message).toEqual(`MatrixError: [${errorUnrecogStatus}] ${errorUnrecogBody.error}`);
},
);
httpBackend.flush("");
return prom;
});
it("should respond to invalid path arguments with error", () => {
httpBackend.when("GET", prefix).respond(errorBadreqStatus, errorBadreqBody);
const prom = client.getRoomSummary("notAroom").then(
function (response) {
throw Error("request not failed");
},
function (error) {
expect(error.httpStatus).toEqual(errorBadreqStatus);
expect(error.errcode).toEqual(errorBadreqBody.errcode);
expect(error.message).toEqual(`MatrixError: [${errorBadreqStatus}] ${errorBadreqBody.error}`);
},
);
httpBackend.flush("");
return prom;
});
});
});
function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
@@ -1720,7 +1825,6 @@ function withThreadId(event: MatrixEvent, newThreadId: string): MatrixEvent {
const buildEventMessageInThread = (root: MatrixEvent) =>
new MatrixEvent({
age: 80098509,
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
@@ -1741,12 +1845,10 @@ const buildEventMessageInThread = (root: MatrixEvent) =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80098509 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventPollResponseReference = () =>
new MatrixEvent({
age: 80098509,
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
@@ -1764,7 +1866,6 @@ const buildEventPollResponseReference = () =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80106237 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventReaction = (event: MatrixEvent) =>
@@ -1804,7 +1905,6 @@ const buildEventRedaction = (event: MatrixEvent) =>
const buildEventPollStartThreadRoot = () =>
new MatrixEvent({
age: 80108647,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "ENCRYPTEDSTUFF",
@@ -1818,12 +1918,10 @@ const buildEventPollStartThreadRoot = () =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80108647 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventReply = (target: MatrixEvent) =>
new MatrixEvent({
age: 80098509,
content: {
"algorithm": "m.megolm.v1.aes-sha2",
"ciphertext": "ENCRYPTEDSTUFF",
@@ -1842,12 +1940,10 @@ const buildEventReply = (target: MatrixEvent) =>
sender: "@andybalaam-test1:matrix.org",
type: "m.room.encrypted",
unsigned: { age: 80098509 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventRoomName = () =>
new MatrixEvent({
age: 80123249,
content: {
name: "1 poll, 1 vote, 1 thread",
},
@@ -1858,12 +1954,10 @@ const buildEventRoomName = () =>
state_key: "",
type: "m.room.name",
unsigned: { age: 80123249 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventEncryption = () =>
new MatrixEvent({
age: 80123383,
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
@@ -1874,12 +1968,10 @@ const buildEventEncryption = () =>
state_key: "",
type: "m.room.encryption",
unsigned: { age: 80123383 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventGuestAccess = () =>
new MatrixEvent({
age: 80123473,
content: {
guest_access: "can_join",
},
@@ -1890,12 +1982,10 @@ const buildEventGuestAccess = () =>
state_key: "",
type: "m.room.guest_access",
unsigned: { age: 80123473 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventHistoryVisibility = () =>
new MatrixEvent({
age: 80123556,
content: {
history_visibility: "shared",
},
@@ -1906,12 +1996,10 @@ const buildEventHistoryVisibility = () =>
state_key: "",
type: "m.room.history_visibility",
unsigned: { age: 80123556 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventJoinRules = () =>
new MatrixEvent({
age: 80123696,
content: {
join_rule: KnownMembership.Invite,
},
@@ -1922,12 +2010,10 @@ const buildEventJoinRules = () =>
state_key: "",
type: "m.room.join_rules",
unsigned: { age: 80123696 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventPowerLevels = () =>
new MatrixEvent({
age: 80124105,
content: {
ban: 50,
events: {
@@ -1958,12 +2044,10 @@ const buildEventPowerLevels = () =>
state_key: "",
type: "m.room.power_levels",
unsigned: { age: 80124105 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventMember = () =>
new MatrixEvent({
age: 80125279,
content: {
avatar_url: "mxc://matrix.org/aNtbVcFfwotudypZcHsIcPOc",
displayname: "andybalaam-test1",
@@ -1976,12 +2060,10 @@ const buildEventMember = () =>
state_key: "@andybalaam-test1:matrix.org",
type: "m.room.member",
unsigned: { age: 80125279 },
user_id: "@andybalaam-test1:matrix.org",
});
const buildEventCreate = () =>
new MatrixEvent({
age: 80126105,
content: {
room_version: "6",
},
@@ -1992,7 +2074,6 @@ const buildEventCreate = () =>
state_key: "",
type: "m.room.create",
unsigned: { age: 80126105 },
user_id: "@andybalaam-test1:matrix.org",
});
function assertObjectContains(obj: Record<string, any>, expected: any): void {
@@ -333,7 +333,7 @@ describe("MatrixClient room timelines", function () {
name: userName,
url: "mxc://some/url",
});
oldMshipEvent.prev_content = {
oldMshipEvent.unsigned!.prev_content = {
displayname: "Old Alice",
avatar_url: undefined,
membership: KnownMembership.Join,
+1
View File
@@ -44,6 +44,7 @@ export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): Validated
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
+4 -2
View File
@@ -173,8 +173,10 @@ export function mkEvent(opts: IEventOpts & { event?: boolean }, client?: MatrixC
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
prev_content: opts.prev_content,
unsigned: opts.unsigned || {},
unsigned: {
...opts.unsigned,
prev_content: opts.prev_content,
},
event_id: "$" + testEventIndex++ + "-" + Math.random() + "-" + Math.random(),
txn_id: "~" + Math.random(),
redacts: opts.redacts,
@@ -0,0 +1,10 @@
## Dump of an empty libolm indexeddb cryptostore to test skipping migration
A dump of an account which is almost completely empty, and totally unsuitable
for use as a real account.
This dump was manually created by copying and editing full_account.
Created to test
["Unable to restore session" error due due to half-initialised legacy indexeddb crypto store #27447](https://github.com/element-hq/element-web/issues/27447).
We should not launch the Rust migration code when we find a DB in this state.
@@ -0,0 +1,14 @@
{
"account": [],
"device_data": [],
"inbound_group_sessions": [],
"inbound_group_sessions_withheld": [],
"notified_error_devices": [],
"outgoingRoomKeyRequests": [],
"parked_shared_history": [],
"rooms": [],
"session_problems": [],
"sessions": [],
"sessions_needing_backup": [],
"shared_history_inbound_group_sessions": []
}
@@ -0,0 +1,35 @@
/*
Copyright 2024 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 { DumpDataSetInfo } from "../index";
/**
* A key query response containing the current keys of the tested user.
* To be used during tests with fetchmock.
*/
const KEYS_QUERY_RESPONSE = { device_keys: { "@emptyuser:example.com": {} } };
/**
* A dataset containing the information for the tested user.
* To be used during tests.
*/
export const EMPTY_ACCOUNT_DATASET: DumpDataSetInfo = {
userId: "@emptyuser:example.com",
deviceId: "EMPTYDEVIC",
pickleKey: "+/bcdefghijklmnopqrstu1/zyxvutsrqponmlkjih2",
keyQueryResponse: KEYS_QUERY_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/empty_account/dump.json",
};
@@ -1,3 +1,19 @@
/*
Copyright 2024 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 { DumpDataSetInfo } from "../index";
/**
@@ -1,3 +1,19 @@
/*
Copyright 2024 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 { KeyBackupInfo } from "../../../../src/crypto-api/keybackup";
import { DumpDataSetInfo } from "../index";
@@ -245,6 +261,28 @@ const BACKUP_RESPONSE: KeyBackupInfo = {
count: 0,
};
/**
* This was generated by doing a backup reset on the account.
* This is a new valid backup for this account.
*/
const NEW_BACKUP_RESPONSE: KeyBackupInfo = {
auth_data: {
public_key: "CkDxWALi3lcChgjEZFEM6clYq5x768XBwsL++eaOzTI",
signatures: {
"@migration:localhost": {
"ed25519:YVEGEYPYWX":
"ZSYuQDdwgB9WKXQ+z5aWWfqSolBCGRw53kur1Vy956gFefgzCBkMbw5M0I2UgfU2Cukri7jZ4ig201zmLNmaAA",
"ed25519:rXCrBin/+xyh+yW//vWte+2UV0et1ZHTWfalp/Ekack":
"+UQ8EA507LoIqgK9rPsqPoGrj+iRBJeY2Oz0mMtXmVf8c1y8G0KWJNUWqvOysnOhsoJf1bt8ey48CxjjtSQ2AA",
},
},
},
version: "3",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
etag: "0",
count: 0,
};
/**
* A dataset containing the information for the tested user.
* To be used during tests.
@@ -256,5 +294,6 @@ export const MSK_NOT_CACHED_DATASET: DumpDataSetInfo = {
keyQueryResponse: KEY_QUERY_RESPONSE,
rotatedKeyQueryResponse: ROTATED_KEY_QUERY_RESPONSE,
backupResponse: BACKUP_RESPONSE,
newBackupResponse: NEW_BACKUP_RESPONSE,
dumpPath: "spec/test-utils/test_indexeddb_cryptostore_dump/no_cached_msk_dump/dump.json",
};
@@ -1,3 +1,19 @@
/*
Copyright 2024 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 { DumpDataSetInfo } from "../index";
/**
+1 -1
View File
@@ -857,7 +857,7 @@ describe("AutoDiscovery", function () {
const expected = {
"m.homeserver": {
state: AutoDiscoveryAction.FAIL_ERROR,
error: AutoDiscovery.ERROR_HOMESERVER_TOO_OLD,
error: AutoDiscovery.ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION,
base_url: "https://example.org",
},
"m.identity_server": {
+24
View File
@@ -76,5 +76,29 @@ describe("ContentRepo", function () {
baseUrl + "/_matrix/media/v3/download/server.name/resourceid#automade",
);
});
it("should return an authenticated URL when requested", function () {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, true, true)).toEqual(
baseUrl +
"/_matrix/client/unstable/org.matrix.msc3916/media/download/server.name/resourceid?allow_redirect=true",
);
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, true, true)).toEqual(
baseUrl +
"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
);
});
it("should force-enable allow_redirects when useAuthentication is set true", function () {
const mxcUri = "mxc://server.name/resourceid";
expect(getHttpUriForMxc(baseUrl, mxcUri, undefined, undefined, undefined, undefined, false, true)).toEqual(
baseUrl +
"/_matrix/client/unstable/org.matrix.msc3916/media/download/server.name/resourceid?allow_redirect=true",
);
expect(getHttpUriForMxc(baseUrl, mxcUri, 64, 64, "scale", undefined, false, true)).toEqual(
baseUrl +
"/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/server.name/resourceid?width=64&height=64&method=scale&allow_redirect=true",
);
});
});
});
+10 -10
View File
@@ -23,12 +23,12 @@ import HttpBackend from "matrix-mock-request";
import * as olmlib from "../../../src/crypto/olmlib";
import { MatrixError } from "../../../src/http-api";
import { logger } from "../../../src/logger";
import { ICrossSigningKey, ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
import { ICreateClientOpts, ISignedKey, MatrixClient } from "../../../src/client";
import { CryptoEvent } from "../../../src/crypto";
import { IDevice } from "../../../src/crypto/deviceinfo";
import { TestClient } from "../../TestClient";
import { resetCrossSigningKeys } from "./crypto-utils";
import { BootstrapCrossSigningOpts } from "../../../src/crypto-api";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../../../src/crypto-api";
const PUSH_RULES_RESPONSE: Response = {
method: "GET",
@@ -377,7 +377,7 @@ describe("Cross Signing", function () {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: ICrossSigningKey = {
const bobSSK: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -515,7 +515,7 @@ describe("Cross Signing", function () {
};
olmlib.pkSign(bobDevice, selfSigningKey as unknown as PkSigning, "@bob:example.com", "");
const bobMaster: ICrossSigningKey = {
const bobMaster: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
@@ -630,7 +630,7 @@ describe("Cross Signing", function () {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: ICrossSigningKey = {
const bobSSK: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -696,7 +696,7 @@ describe("Cross Signing", function () {
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK: ICrossSigningKey = {
const bobSSK: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -763,7 +763,7 @@ describe("Cross Signing", function () {
const bobSigning2 = new global.Olm.PkSigning();
const bobPrivkey2 = bobSigning2.generate_seed();
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
const bobSSK2: ICrossSigningKey = {
const bobSSK2: CrossSigningKeyInfo = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
@@ -913,7 +913,7 @@ describe("Cross Signing", function () {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
const aliceSSK: CrossSigningKeyInfo = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
@@ -988,7 +988,7 @@ describe("Cross Signing", function () {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
const aliceSSK: CrossSigningKeyInfo = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
@@ -1048,7 +1048,7 @@ describe("Cross Signing", function () {
const aliceSigning = new global.Olm.PkSigning();
const alicePrivkey = aliceSigning.generate_seed();
const alicePubkey = aliceSigning.init_with_seed(alicePrivkey);
const aliceSSK: ICrossSigningKey = {
const aliceSSK: CrossSigningKeyInfo = {
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
+6 -5
View File
@@ -23,12 +23,13 @@ import { makeTestClients } from "./verification/util";
import { encryptAES } from "../../../src/crypto/aes";
import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils";
import { logger } from "../../../src/logger";
import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from "../../../src/client";
import { ClientEvent, ICreateClientOpts, MatrixClient } from "../../../src/client";
import { DeviceInfo } from "../../../src/crypto/deviceinfo";
import { ISignatures } from "../../../src/@types/signed";
import { ICurve25519AuthData } from "../../../src/crypto/keybackup";
import { SecretStorageKeyDescription, SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/secret-storage";
import { decodeBase64 } from "../../../src/base64";
import { CrossSigningKeyInfo } from "../../../src/crypto-api";
async function makeTestClient(
userInfo: { userId: string; deviceId: string },
@@ -475,7 +476,7 @@ describe("Secrets", function () {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign<ICrossSigningKey>(
self_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["self_signing"],
@@ -486,7 +487,7 @@ describe("Secrets", function () {
XSK,
"@alice:example.com",
),
user_signing: sign<ICrossSigningKey>(
user_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["user_signing"],
@@ -631,7 +632,7 @@ describe("Secrets", function () {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign<ICrossSigningKey>(
self_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["self_signing"],
@@ -642,7 +643,7 @@ describe("Secrets", function () {
XSK,
"@alice:example.com",
),
user_signing: sign<ICrossSigningKey>(
user_signing: sign<CrossSigningKeyInfo>(
{
user_id: "@alice:example.com",
usage: ["user_signing"],
-5
View File
@@ -136,11 +136,6 @@ describe("EventTimelineSet", () => {
expect(eventsInLiveTimeline.length).toStrictEqual(1);
expect(eventsInLiveTimeline[0]).toStrictEqual(duplicateMessageEvent);
});
it("Make sure legacy overload passing options directly as parameters still works", () => {
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Replace, false)).not.toThrow();
expect(() => eventTimelineSet.addLiveEvent(messageEvent, DuplicateStrategy.Ignore, true)).not.toThrow();
});
});
describe("addEventToTimeline", () => {
+3 -1
View File
@@ -386,6 +386,9 @@ describe("MatrixClient", function () {
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true)).toBe(
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true),
);
expect(client.mxcUrlToHttp(mxc, 32, 46, "scale", false, true, true)).toBe(
getHttpUriForMxc(client.baseUrl, mxc, 32, 46, "scale", false, true, true),
);
});
});
@@ -1557,7 +1560,6 @@ describe("MatrixClient", function () {
},
},
event_id: "$ev1",
user_id: "@alice:matrix.org",
});
expect(rootEvent.isThreadRoot).toBe(true);
+17 -18
View File
@@ -16,11 +16,12 @@ limitations under the License.
import { IContent, MatrixClient, MatrixEvent } from "../../../src";
import { Room } from "../../../src/models/room";
import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { RelationType, UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
import { EventTimeline } from "../../../src/models/event-timeline";
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
import { MSC3089TreeSpace } from "../../../src/models/MSC3089TreeSpace";
import { EncryptedFile } from "../../../src/@types/media";
describe("MSC3089Branch", () => {
let client: MatrixClient;
@@ -254,7 +255,7 @@ describe("MSC3089Branch", () => {
it("should create new versions of itself", async () => {
const canaryName = "canary";
const canaryContents = "contents go here";
const canaryFile = {} as IEncryptedFile;
const canaryFile = {} as EncryptedFile;
const canaryAddl = { canary: true };
indexEvent.getContent = () => ({ active: true, retained: true });
const stateKeyOrder = [fileEventId2, fileEventId];
@@ -287,23 +288,21 @@ describe("MSC3089Branch", () => {
const createFn = jest
.fn()
.mockImplementation(
(name: string, contents: ArrayBuffer, info: Partial<IEncryptedFile>, addl: IContent) => {
expect(name).toEqual(canaryName);
expect(contents).toBe(canaryContents);
expect(info).toBe(canaryFile);
expect(addl).toMatchObject({
...canaryAddl,
"m.new_content": true,
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: fileEventId,
},
});
.mockImplementation((name: string, contents: ArrayBuffer, info: Partial<EncryptedFile>, addl: IContent) => {
expect(name).toEqual(canaryName);
expect(contents).toBe(canaryContents);
expect(info).toBe(canaryFile);
expect(addl).toMatchObject({
...canaryAddl,
"m.new_content": true,
"m.relates_to": {
rel_type: RelationType.Replace,
event_id: fileEventId,
},
});
return Promise.resolve({ event_id: fileEventId2 });
},
);
return Promise.resolve({ event_id: fileEventId2 });
});
directory.createFile = createFn;
await branch.createNewVersion(canaryName, canaryContents, canaryFile, canaryAddl);
+27
View File
@@ -90,4 +90,31 @@ describe("registerOidcClient()", () => {
OidcError.DynamicRegistrationInvalid,
);
});
it("should throw when required endpoints are unavailable", async () => {
await expect(() =>
registerOidcClient(
{
...delegatedAuthConfig,
registrationEndpoint: undefined,
},
metadata,
),
).rejects.toThrow(OidcError.DynamicRegistrationNotSupported);
});
it("should throw when required scopes are unavailable", async () => {
await expect(() =>
registerOidcClient(
{
...delegatedAuthConfig,
metadata: {
...delegatedAuthConfig.metadata,
grant_types_supported: [delegatedAuthConfig.metadata.grant_types_supported[0]],
},
},
metadata,
),
).rejects.toThrow(OidcError.DynamicRegistrationNotSupported);
});
});
+2 -21
View File
@@ -22,7 +22,7 @@ import { mocked } from "jest-mock";
import { M_POLL_KIND_DISCLOSED, M_POLL_RESPONSE, M_POLL_START, Optional, PollStartEvent } from "matrix-events-sdk";
import * as utils from "../test-utils/test-utils";
import { emitPromise } from "../test-utils/test-utils";
import { emitPromise, IMessageOpts } from "../test-utils/test-utils";
import {
Direction,
DuplicateStrategy,
@@ -54,7 +54,6 @@ import { Crypto } from "../../src/crypto";
import * as threadUtils from "../test-utils/thread";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../test-utils/client";
import { logger } from "../../src/logger";
import { IMessageOpts } from "../test-utils/test-utils";
import { flushPromises } from "../test-utils/flushPromises";
import { KnownMembership } from "../../src/@types/membership";
@@ -339,24 +338,6 @@ describe("Room", function () {
}),
];
it("Make sure legacy overload passing options directly as parameters still works", async () => {
await expect(room.addLiveEvents(events, DuplicateStrategy.Replace, false)).resolves.not.toThrow();
await expect(room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).resolves.not.toThrow();
await expect(
// @ts-ignore
room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false),
).rejects.toThrow();
});
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", async function () {
return expect(
// @ts-ignore
room.addLiveEvents(events, {
duplicateStrategy: "foo",
}),
).rejects.toThrow();
});
it("should replace a timeline event if dupe strategy is 'replace'", async function () {
// make a duplicate
const dupe = utils.mkMessage({
@@ -387,7 +368,7 @@ describe("Room", function () {
expect(room.timeline[0]).toEqual(events[0]);
// @ts-ignore
await room.addLiveEvents([dupe], {
duplicateStrategy: "ignore",
duplicateStrategy: DuplicateStrategy.Ignore,
});
expect(room.timeline[0]).toEqual(events[0]);
});
@@ -22,6 +22,7 @@ import {
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
PutDehydratedDeviceRequest,
RoomMessageRequest,
SignatureUploadRequest,
UploadSigningKeysRequest,
@@ -233,6 +234,35 @@ describe("OutgoingRequestProcessor", () => {
httpBackend.verifyNoOutstandingRequests();
});
it("should handle PutDehydratedDeviceRequest", async () => {
// first, mock up a request as we might expect to receive it from the Rust layer ...
const testReq = { foo: "bar" };
const outgoingRequest = new PutDehydratedDeviceRequest(JSON.stringify(testReq));
// ... then poke the request into the OutgoingRequestProcessor under test
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
// Now: check that it makes a matching HTTP request.
const testResponse = '{"result":1}';
httpBackend
.when("PUT", "/_matrix")
.check((req) => {
expect(req.path).toEqual(
"https://example.com/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device",
);
expect(JSON.parse(req.rawData)).toEqual(testReq);
expect(req.headers["Accept"]).toEqual("application/json");
expect(req.headers["Content-Type"]).toEqual("application/json");
})
.respond(200, testResponse, true);
// PutDehydratedDeviceRequest does not need to be marked as sent, so no call to OlmMachine.markAsSent is expected.
await httpBackend.flushAllExpected();
await reqProm;
httpBackend.verifyNoOutstandingRequests();
});
it("does not explode with unknown requests", async () => {
const outgoingRequest = { id: "5678", type: 987 };
const markSentCallPromise = awaitCallToMarkAsSent();
@@ -90,7 +90,7 @@ describe("PerSessionKeyBackupDownloader", () => {
mockRustBackupManager = {
getActiveBackupVersion: jest.fn(),
requestKeyBackupVersion: jest.fn(),
getServerBackupInfo: jest.fn(),
importBackedUpRoomKeys: jest.fn(),
createBackupDecryptor: jest.fn().mockReturnValue(mockBackupDecryptor),
on: jest.fn().mockImplementation((event, listener) => {
@@ -135,7 +135,7 @@ describe("PerSessionKeyBackupDownloader", () => {
decryptionKey: RustSdkCryptoJs.BackupDecryptionKey.fromBase64(TestData.BACKUP_DECRYPTION_KEY_BASE64),
} as unknown as RustSdkCryptoJs.BackupKeys);
mockRustBackupManager.requestKeyBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
mockRustBackupManager.getServerBackupInfo.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
});
it("Should download and import a missing key from backup", async () => {
@@ -155,8 +155,11 @@ describe("PerSessionKeyBackupDownloader", () => {
downloader.onDecryptionKeyMissingError(roomId, sessionId);
// `isKeyBackupDownloadConfigured` is false until the config is proven.
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
await expectAPICall;
await awaitKeyImported.promise;
expect(downloader.isKeyBackupDownloadConfigured()).toBe(true);
expect(mockRustBackupManager.createBackupDecryptor).toHaveBeenCalledTimes(1);
});
@@ -313,6 +316,9 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
it("Should not query server if backup not active", async () => {
@@ -328,6 +334,9 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
it("Should stop if backup key is not cached", async () => {
@@ -344,6 +353,9 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
it("Should stop if backup key cached as wrong version", async () => {
@@ -363,6 +375,9 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
it("Should stop if backup key version does not match the active one", async () => {
@@ -382,13 +397,16 @@ describe("PerSessionKeyBackupDownloader", () => {
expect(getConfigSpy).toHaveBeenCalledTimes(1);
expect(getConfigSpy).toHaveReturnedWith(Promise.resolve(null));
// isKeyBackupDownloadConfigured remains false
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
});
});
describe("Given Backup state update", () => {
it("After initial sync, when backup becomes trusted it should request keys for past requests", async () => {
// there is a backup
mockRustBackupManager.requestKeyBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
mockRustBackupManager.getServerBackupInfo.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
// but at this point it's not trusted and we don't have the key
mockRustBackupManager.getActiveBackupVersion.mockResolvedValue(null);
@@ -410,6 +428,7 @@ describe("PerSessionKeyBackupDownloader", () => {
// @ts-ignore access to private property
expect(downloader.hasConfigurationProblem).toEqual(true);
expect(downloader.isKeyBackupDownloadConfigured()).toBe(false);
// Now the backup becomes trusted
mockRustBackupManager.getActiveBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA.version!);
@@ -423,6 +442,7 @@ describe("PerSessionKeyBackupDownloader", () => {
mockEmitter.emit(CryptoEvent.KeyBackupStatus, true);
await jest.runAllTimersAsync();
expect(downloader.isKeyBackupDownloadConfigured()).toBe(true);
await a0Imported;
await a1Imported;
@@ -434,7 +454,7 @@ describe("PerSessionKeyBackupDownloader", () => {
describe("Error cases", () => {
beforeEach(async () => {
// there is a backup
mockRustBackupManager.requestKeyBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
mockRustBackupManager.getServerBackupInfo.mockResolvedValue(TestData.SIGNED_BACKUP_DATA);
// It's trusted
mockRustBackupManager.getActiveBackupVersion.mockResolvedValue(TestData.SIGNED_BACKUP_DATA.version!);
// And we have the key in cache
+130 -10
View File
@@ -104,7 +104,7 @@ describe("initRustCrypto", () => {
} as unknown as Mocked<OlmMachine>;
}
it("passes through the store params", async () => {
it("passes through the store params (passphrase)", async () => {
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore);
@@ -126,7 +126,30 @@ describe("initRustCrypto", () => {
expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore);
});
it("suppresses the storePassphrase if storePrefix is unset", async () => {
it("passes through the store params (key)", async () => {
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
jest.spyOn(StoreHandle, "openWithKey").mockResolvedValue(mockStore);
const testOlmMachine = makeTestOlmMachine();
jest.spyOn(OlmMachine, "initFromStore").mockResolvedValue(testOlmMachine);
const storeKey = new Uint8Array(32);
await initRustCrypto({
logger,
http: {} as MatrixClient["http"],
userId: TEST_USER,
deviceId: TEST_DEVICE_ID,
secretStorage: {} as ServerSideSecretStorage,
cryptoCallbacks: {} as CryptoCallbacks,
storePrefix: "storePrefix",
storeKey: storeKey,
});
expect(StoreHandle.openWithKey).toHaveBeenCalledWith("storePrefix", storeKey);
expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore);
});
it("suppresses the storePassphrase and storeKey if storePrefix is unset", async () => {
const mockStore = { free: jest.fn() } as unknown as StoreHandle;
jest.spyOn(StoreHandle, "open").mockResolvedValue(mockStore);
@@ -141,10 +164,11 @@ describe("initRustCrypto", () => {
secretStorage: {} as ServerSideSecretStorage,
cryptoCallbacks: {} as CryptoCallbacks,
storePrefix: null,
storeKey: new Uint8Array(),
storePassphrase: "storePassphrase",
});
expect(StoreHandle.open).toHaveBeenCalledWith(undefined, undefined);
expect(StoreHandle.open).toHaveBeenCalledWith();
expect(OlmMachine.initFromStore).toHaveBeenCalledWith(expect.anything(), expect.anything(), mockStore);
});
@@ -205,7 +229,20 @@ describe("initRustCrypto", () => {
createMegolmSessions(legacyStore, nDevices, nSessionsPerDevice);
await legacyStore.markSessionsNeedingBackup([{ senderKey: pad43("device5"), sessionId: "session5" }]);
fetchMock.get("path:/_matrix/client/v3/room_keys/version", { version: "45" });
fetchMock.get("path:/_matrix/client/v3/room_keys/version", {
auth_data: {
public_key: "backup_key_public",
},
version: "45",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
});
// The cached key should be valid for the backup
const mockBackupDecryptionKey: any = {
megolmV1PublicKey: {
publicKeyBase64: "backup_key_public",
},
};
jest.spyOn(RustSdkCryptoJs.BackupDecryptionKey, "fromBase64").mockReturnValue(mockBackupDecryptionKey);
function legacyMigrationProgressListener(progress: number, total: number): void {
logger.log(`migrated ${progress} of ${total}`);
@@ -291,6 +328,56 @@ describe("initRustCrypto", () => {
}
}, 10000);
it("migrates data from a legacy crypto store when secret are not encrypted", async () => {
const PICKLE_KEY = "pickle1234";
const legacyStore = new MemoryCryptoStore();
// It's possible for old sessions to directly store the secrets as raw UInt8Array,
// so we need to support that in the migration code.
// See https://github.com/matrix-org/matrix-js-sdk/commit/c81f11df0afd4d0da3b088892745ae2f8ba1c4a7
async function storeSecretKeyInClear(type: string, key: Uint8Array, store: CryptoStore) {
// @ts-ignore The API to store raw UInt8Array does not exist anymore, so we need that for this test.
store.privateKeys[type as keyof SecretStorePrivateKeys] = key;
}
// Populate the legacy store with some test data
const storeSecretKey = (type: string, key: string) =>
storeSecretKeyInClear(type, new TextEncoder().encode(key), legacyStore);
await legacyStore.storeAccount({}, "not a real account");
await storeSecretKey("master", "master key");
await storeSecretKey("self_signing", "ssk");
await storeSecretKey("user_signing", "usk");
fetchMock.get("path:/_matrix/client/v3/room_keys/version", 404);
function legacyMigrationProgressListener(progress: number, total: number): void {
logger.log(`migrated ${progress} of ${total}`);
}
await initRustCrypto({
logger,
http: makeMatrixHttpApi(),
userId: TEST_USER,
deviceId: TEST_DEVICE_ID,
secretStorage: {} as ServerSideSecretStorage,
cryptoCallbacks: {} as CryptoCallbacks,
storePrefix: "storePrefix",
storePassphrase: "storePassphrase",
legacyCryptoStore: legacyStore,
legacyPickleKey: PICKLE_KEY,
legacyMigrationProgressListener,
});
const data = mocked(Migration.migrateBaseData).mock.calls[0][0];
expect(data.pickledAccount).toEqual("not a real account");
expect(data.userId!.toString()).toEqual(TEST_USER);
expect(data.deviceId!.toString()).toEqual(TEST_DEVICE_ID);
expect(atob(data.privateCrossSigningMasterKey!)).toEqual("master key");
expect(atob(data.privateCrossSigningUserSigningKey!)).toEqual("usk");
expect(atob(data.privateCrossSigningSelfSigningKey!)).toEqual("ssk");
});
it("handles megolm sessions with no `keysClaimed`", async () => {
const legacyStore = new MemoryCryptoStore();
legacyStore.storeAccount({}, "not a real account");
@@ -762,8 +849,11 @@ describe("RustCrypto", () => {
},
},
};
} else if (request instanceof RustSdkCryptoJs.UploadSigningKeysRequest) {
// SigningKeysUploadRequest does not implement OutgoingRequest and does not need to be marked as sent.
} else if (
request instanceof RustSdkCryptoJs.UploadSigningKeysRequest ||
request instanceof RustSdkCryptoJs.PutDehydratedDeviceRequest
) {
// These request types do not implement OutgoingRequest and do not need to be marked as sent.
return;
}
if (request.id) {
@@ -1329,14 +1419,15 @@ describe("RustCrypto", () => {
const rustCrypto = await makeTestRustCrypto();
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
const backupVersion = testData.SIGNED_BACKUP_DATA.version!;
await olmMachine.enableBackupV1(
(testData.SIGNED_BACKUP_DATA.auth_data as Curve25519AuthData).public_key,
testData.SIGNED_BACKUP_DATA.version!,
backupVersion,
);
// we import two keys: one "from backup", and one "from export"
const [backedUpRoomKey, exportedRoomKey] = testData.MEGOLM_SESSION_DATA_ARRAY;
await rustCrypto.importBackedUpRoomKeys([backedUpRoomKey]);
await rustCrypto.importBackedUpRoomKeys([backedUpRoomKey], backupVersion);
await rustCrypto.importRoomKeys([exportedRoomKey]);
// we ask for the keys that should be backed up
@@ -1371,16 +1462,17 @@ describe("RustCrypto", () => {
const rustCrypto = await makeTestRustCrypto();
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
const backupVersion = testData.SIGNED_BACKUP_DATA.version!;
await olmMachine.enableBackupV1(
(testData.SIGNED_BACKUP_DATA.auth_data as Curve25519AuthData).public_key,
testData.SIGNED_BACKUP_DATA.version!,
backupVersion,
);
const backup = Array.from(testData.MEGOLM_SESSION_DATA_ARRAY);
// in addition to correct keys, we restore an invalid key
backup.push({ room_id: "!roomid", session_id: "sessionid" } as IMegolmSessionData);
const progressCallback = jest.fn();
await rustCrypto.importBackedUpRoomKeys(backup, { progressCallback });
await rustCrypto.importBackedUpRoomKeys(backup, backupVersion, { progressCallback });
expect(progressCallback).toHaveBeenCalledWith({
total: 3,
successes: 0,
@@ -1395,6 +1487,34 @@ describe("RustCrypto", () => {
});
});
});
describe("device dehydration", () => {
it("should detect if dehydration is supported", async () => {
const rustCrypto = await makeTestRustCrypto(makeMatrixHttpApi());
fetchMock.config.overwriteRoutes = true;
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_UNRECOGNIZED",
error: "Unknown endpoint",
},
});
expect(await rustCrypto.isDehydrationSupported()).toBe(false);
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
status: 404,
body: {
errcode: "M_NOT_FOUND",
error: "Not found",
},
});
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
fetchMock.get("path:/_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device", {
device_id: "DEVICE_ID",
device_data: "data",
});
expect(await rustCrypto.isDehydrationSupported()).toBe(true);
});
});
});
/** Build a MatrixHttpApi instance */
-5
View File
@@ -147,11 +147,6 @@ export interface LoginRequest {
* The login type being used.
*/
type: "m.login.password" | "m.login.token" | string;
/**
* Third-party identifier for the user.
* @deprecated in favour of `identifier`.
*/
address?: string;
/**
* ID of the client device.
* If this does not correspond to a known client device, a new device will be created.
+4 -3
View File
@@ -56,7 +56,6 @@ import {
SDPStreamMetadataKey,
} from "../webrtc/callEventTypes";
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types";
import { EncryptedFile } from "./media";
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls";
export enum EventType {
@@ -299,9 +298,11 @@ export const LOCAL_NOTIFICATION_SETTINGS_PREFIX = new UnstableValue(
export const UNSIGNED_THREAD_ID_FIELD = new UnstableValue("thread_id", "org.matrix.msc4023.thread_id");
/**
* @deprecated in favour of {@link EncryptedFile}
* https://github.com/matrix-org/matrix-spec-proposals/pull/4115
*
* @experimental
*/
export type IEncryptedFile = EncryptedFile;
export const UNSIGNED_MEMBERSHIP_FIELD = new UnstableValue("membership", "io.element.msc4115.membership");
/**
* Mapped type from event type to content type for all specified non-state room events.
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2024 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 "oidc-client-ts";
declare module "oidc-client-ts" {
interface OidcMetadata {
// Add the missing device_authorization_endpoint field to the OidcMetadata interface
device_authorization_endpoint?: string;
}
}
-8
View File
@@ -45,14 +45,6 @@ export enum RestrictedAllowType {
RoomMembership = "m.room_membership",
}
export interface IJoinRuleEventContent {
join_rule: JoinRule; // eslint-disable-line camelcase
allow?: {
type: RestrictedAllowType;
room_id: string; // eslint-disable-line camelcase
}[];
}
export enum GuestAccess {
CanJoin = "can_join",
Forbidden = "forbidden",
+5 -19
View File
@@ -52,21 +52,13 @@ export interface RegisterRequest {
*/
initial_device_display_name?: string;
/**
* @deprecated missing in the spec
* Guest users can also upgrade their account by going through the ordinary register flow,
* but specifying the additional POST parameter guest_access_token containing the guests access token.
* They are also required to specify the username parameter to the value of the local part of their username,
* which is otherwise optional.
* @see https://spec.matrix.org/v1.10/client-server-api/#guest-access
*/
guest_access_token?: string;
/**
* @deprecated missing in the spec
*/
x_show_msisdn?: boolean;
/**
* @deprecated missing in the spec
*/
bind_msisdn?: boolean;
/**
* @deprecated missing in the spec
*/
bind_email?: boolean;
}
/**
@@ -107,10 +99,4 @@ export interface RegisterResponse {
* Omitted if the inhibit_login option is true.
*/
refresh_token?: string;
/**
* The server_name of the homeserver on which the account has been registered.
*
* @deprecated Clients should extract the server_name from user_id (by splitting at the first colon) if they require it.
*/
home_server?: string;
}
+2 -1
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { RoomType } from "./event";
import { GuestAccess, HistoryVisibility, RestrictedAllowType } from "./partials";
import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "./partials";
import { ImageInfo } from "./media";
import { PolicyRecommendation } from "../models/invites-ignorer";
@@ -36,6 +36,7 @@ export interface RoomCreateEventContent {
}
export interface RoomJoinRulesEventContent {
join_rule: JoinRule;
allow?: {
room_id: string;
type: RestrictedAllowType;
-5
View File
@@ -43,8 +43,6 @@ export enum AutoDiscoveryError {
InvalidJson = "Invalid JSON",
UnsupportedHomeserverSpecVersion = "The homeserver does not meet the version requirements",
/** @deprecated Replaced by `UnsupportedHomeserverSpecVersion` */
HomeserverTooOld = UnsupportedHomeserverSpecVersion,
// TODO: Implement when Sydent supports the `/versions` endpoint - https://github.com/matrix-org/sydent/issues/424
//IdentityServerTooOld = "The identity server does not meet the minimum version requirements",
}
@@ -91,9 +89,6 @@ export class AutoDiscovery {
public static readonly ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION =
AutoDiscoveryError.UnsupportedHomeserverSpecVersion;
/** @deprecated Replaced by ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION */
public static readonly ERROR_HOMESERVER_TOO_OLD = AutoDiscovery.ERROR_UNSUPPORTED_HOMESERVER_SPEC_VERSION;
public static readonly ALL_ERRORS = Object.keys(AutoDiscoveryError) as AutoDiscoveryError[];
/**
+91 -70
View File
@@ -357,14 +357,14 @@ export interface ICreateClientOpts {
deviceToImport?: IExportedDevice;
/**
* Encryption key used for encrypting sensitive data (such as e2ee keys) in storage.
* Encryption key used for encrypting sensitive data (such as e2ee keys) in {@link ICreateClientOpts#cryptoStore}.
*
* This must be set to the same value every time the client is initialised for the same device.
*
* If unset, either a hardcoded key or no encryption at all is used, depending on the Crypto implementation.
*
* No particular requirement is placed on the key data (it is fed into an HKDF to generate the actual encryption
* keys).
* This is only used for the legacy crypto implementation (as used by {@link MatrixClient#initCrypto}),
* but if you use the rust crypto implementation ({@link MatrixClient#initRustCrypto}) and the device
* previously used legacy crypto (so must be migrated), then this must still be provided, so that the
* data can be migrated from the legacy store.
*/
pickleKey?: string;
@@ -560,16 +560,13 @@ export interface Capabilities {
"org.matrix.msc3882.get_login_token"?: IGetLoginTokenCapability;
}
/** @deprecated prefer {@link CrossSigningKeyInfo}. */
export type ICrossSigningKey = CrossSigningKeyInfo;
enum CrossSigningKeyType {
MasterKey = "master_key",
SelfSigningKey = "self_signing_key",
UserSigningKey = "user_signing_key",
}
export type CrossSigningKeys = Record<CrossSigningKeyType, ICrossSigningKey>;
export type CrossSigningKeys = Record<CrossSigningKeyType, CrossSigningKeyInfo>;
export type SendToDeviceContentMap = Map<string, Map<string, Record<string, any>>>;
@@ -581,7 +578,7 @@ export interface ISignedKey {
device_id: string;
}
export type KeySignatures = Record<string, Record<string, ICrossSigningKey | ISignedKey>>;
export type KeySignatures = Record<string, Record<string, CrossSigningKeyInfo | ISignedKey>>;
export interface IUploadKeySignaturesResponse {
failures: Record<
string,
@@ -865,10 +862,24 @@ interface IThirdPartyUser {
fields: object;
}
interface IRoomSummary extends Omit<IPublicRoomsChunkRoom, "canonical_alias" | "aliases"> {
room_type?: RoomType;
membership?: Membership;
is_encrypted: boolean;
/**
* The summary of a room as defined by an initial version of MSC3266 and implemented in Synapse
* Proposed at https://github.com/matrix-org/matrix-doc/pull/3266
*/
export interface RoomSummary extends Omit<IPublicRoomsChunkRoom, "canonical_alias" | "aliases"> {
/**
* The current membership of this user in the room.
* Usually "leave" if the room is fetched over federation.
*/
"membership"?: Membership;
/**
* Version of the room.
*/
"im.nheko.summary.room_version"?: string;
/**
* The encryption algorithm used for this room, if the room is encrypted.
*/
"im.nheko.summary.encryption"?: string;
}
interface IRoomKeysResponse {
@@ -1469,13 +1480,6 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
this.on(ClientEvent.Sync, this.startMatrixRTC);
// backwards compat for when 'opts' was 'historyLen'.
if (typeof opts === "number") {
opts = {
initialSyncLimit: opts,
};
}
// Create our own user object artificially (instead of waiting for sync)
// so it's always available, even if the user is not in any rooms etc.
const userId = this.getUserId();
@@ -2218,17 +2222,24 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* An alternative to {@link initCrypto}.
*
* *WARNING*: this API is very experimental, should not be used in production, and may change without notice!
* Eventually it will be deprecated and `initCrypto` will do the same thing.
*
* @experimental
*
* @param useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
* @param args.useIndexedDB - True to use an indexeddb store, false to use an in-memory store. Defaults to 'true'.
* @param args.storageKey - A key with which to encrypt the indexeddb store. If provided, it must be exactly
* 32 bytes of data, and must be the same each time the client is initialised for a given device.
* If both this and `storagePassword` are unspecified, the store will be unencrypted.
* @param args.storagePassword - An alternative to `storageKey`. A password which will be used to derive a key to
* encrypt the store with. Deriving a key from a password is (deliberately) a slow operation, so prefer
* to pass a `storageKey` directly where possible.
*
* @returns a Promise which will resolve when the crypto layer has been
* successfully initialised.
*/
public async initRustCrypto({ useIndexedDB = true }: { useIndexedDB?: boolean } = {}): Promise<void> {
public async initRustCrypto(
args: {
useIndexedDB?: boolean;
storageKey?: Uint8Array;
storagePassword?: string;
} = {},
): Promise<void> {
if (this.cryptoBackend) {
this.logger.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
@@ -2261,11 +2272,15 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
deviceId: deviceId,
secretStorage: this.secretStorage,
cryptoCallbacks: this.cryptoCallbacks,
storePrefix: useIndexedDB ? RUST_SDK_STORE_PREFIX : null,
storePassphrase: this.pickleKey,
storePrefix: args.useIndexedDB === false ? null : RUST_SDK_STORE_PREFIX,
storeKey: args.storageKey,
// temporary compatibility hack: if there is no storageKey nor storagePassword, fall back to the pickleKey
storePassphrase: args.storagePassword ?? this.pickleKey,
legacyCryptoStore: this.cryptoStore,
legacyPickleKey: this.pickleKey ?? "DEFAULT_KEY",
legacyMigrationProgressListener: (progress, total) => {
legacyMigrationProgressListener: (progress: number, total: number): void => {
this.emit(CryptoEvent.LegacyCryptoStoreMigrationProgress, progress, total);
},
});
@@ -2884,7 +2899,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
*
* @param event - event to be checked
* @returns The event information.
* @deprecated Prefer {@link CryptoApi.getEncryptionInfoForEvent | `CryptoApi.getEncryptionInfoForEvent`}.
* @deprecated Prefer {@link Crypto.CryptoApi.getEncryptionInfoForEvent | `CryptoApi.getEncryptionInfoForEvent`}.
*/
public getEventEncryptionInfo(event: MatrixEvent): IEncryptedEventInfo {
if (!this.cryptoBackend) {
@@ -3302,7 +3317,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* trust information (as returned by isKeyBackupTrusted)
* in trustInfo.
*
* @deprecated Prefer {@link CryptoApi.checkKeyBackupAndEnable}.
* @deprecated Prefer {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
*/
public checkKeyBackup(): Promise<IKeyBackupCheck | null> {
if (!this.crypto) {
@@ -3359,7 +3374,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* the server, otherwise false. If we haven't completed a successful check
* of key backup status yet, returns null.
*
* @deprecated Prefer direct access to {@link CryptoApi.getActiveSessionBackupVersion}:
* @deprecated Prefer direct access to {@link Crypto.CryptoApi.getActiveSessionBackupVersion}:
*
* ```javascript
* let enabled = (await client.getCrypto().getActiveSessionBackupVersion()) !== null;
@@ -3379,7 +3394,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param info - Backup information object as returned by getKeyBackupVersion
* @returns Promise which resolves when complete.
*
* @deprecated Do not call this directly. Instead call {@link CryptoApi.checkKeyBackupAndEnable}.
* @deprecated Do not call this directly. Instead call {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
*/
public enableKeyBackup(info: IKeyBackupInfo): Promise<void> {
if (!this.crypto) {
@@ -3841,12 +3856,13 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (!backupInfo.version) {
throw new Error("Backup version must be defined");
}
const backupVersion = backupInfo.version!;
let totalKeyCount = 0;
let totalFailures = 0;
let totalImported = 0;
const path = this.makeKeyBackupPath(targetRoomId, targetSessionId, backupInfo.version);
const path = this.makeKeyBackupPath(targetRoomId, targetSessionId, backupVersion);
const backupDecryptor = await this.cryptoBackend.getBackupDecryptor(backupInfo, privKey);
@@ -3860,7 +3876,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// Cache the key, if possible.
// This is async.
this.cryptoBackend
.storeSessionBackupPrivateKey(privKey, backupInfo.version)
.storeSessionBackupPrivateKey(privKey, backupVersion)
.catch((e) => {
this.logger.warn("Error caching session backup key:", e);
})
@@ -3900,7 +3916,8 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
async (chunk) => {
// We have a chunk of decrypted keys: import them
try {
await this.cryptoBackend!.importBackedUpRoomKeys(chunk, {
const backupVersion = backupInfo.version!;
await this.cryptoBackend!.importBackedUpRoomKeys(chunk, backupVersion, {
untrusted,
});
totalImported += chunk.length;
@@ -3930,7 +3947,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
for (const k of keys) {
k.room_id = targetRoomId!;
}
await this.cryptoBackend.importBackedUpRoomKeys(keys, {
await this.cryptoBackend.importBackedUpRoomKeys(keys, backupVersion, {
progressCallback,
untrusted,
});
@@ -3944,7 +3961,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
key.room_id = targetRoomId!;
key.session_id = targetSessionId!;
await this.cryptoBackend.importBackedUpRoomKeys([key], {
await this.cryptoBackend.importBackedUpRoomKeys([key], backupVersion, {
progressCallback,
untrusted,
});
@@ -4862,10 +4879,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
pathTemplate = "/rooms/$roomId/state/$eventType/$stateKey";
}
path = utils.encodeUri(pathTemplate, pathParams);
} else if (event.isRedaction()) {
} else if (event.isRedaction() && event.event.redacts) {
const pathTemplate = `/rooms/$roomId/redact/$redactsEventId/$txnId`;
path = utils.encodeUri(pathTemplate, {
$redactsEventId: event.event.redacts!,
$redactsEventId: event.event.redacts,
...pathParams,
});
} else {
@@ -5761,7 +5778,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* anyone they share a room with. If false, will return null for such URLs.
* @param allowRedirects - If true, the caller supports the URL being 307 or
* 308 redirected to another resource upon request. If false, redirects
* are not expected.
* are not expected. Implied `true` when `useAuthentication` is `true`.
* @param useAuthentication - If true, the caller supports authenticated
* media and wants an authentication-required URL. Note that server support
* for authenticated media will *not* be checked - it is the caller's responsibility
* to do so before calling this function. Note also that `useAuthentication`
* implies `allowRedirects`. Defaults to false (unauthenticated endpoints).
* @returns the avatar URL or null.
*/
public mxcUrlToHttp(
@@ -5771,8 +5793,18 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
resizeMethod?: string,
allowDirectLinks?: boolean,
allowRedirects?: boolean,
useAuthentication?: boolean,
): string | null {
return getHttpUriForMxc(this.baseUrl, mxcUrl, width, height, resizeMethod, allowDirectLinks, allowRedirects);
return getHttpUriForMxc(
this.baseUrl,
mxcUrl,
width,
height,
resizeMethod,
allowDirectLinks,
allowRedirects,
useAuthentication,
);
}
/**
@@ -7862,16 +7894,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
password: string,
sessionId: string | null,
auth: { session?: string; type: string },
bindThreepids?: boolean | null | { email?: boolean; msisdn?: boolean },
bindThreepids?: { email?: boolean; msisdn?: boolean },
guestAccessToken?: string,
inhibitLogin?: boolean,
): Promise<RegisterResponse> {
// backwards compat
if (bindThreepids === true) {
bindThreepids = { email: true };
} else if (bindThreepids === null || bindThreepids === undefined || bindThreepids === false) {
bindThreepids = {};
}
if (sessionId) {
auth.session = sessionId;
}
@@ -7886,27 +7912,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
if (password !== undefined && password !== null) {
params.password = password;
}
if (bindThreepids.email) {
params.bind_email = true;
}
if (bindThreepids.msisdn) {
params.bind_msisdn = true;
}
if (guestAccessToken !== undefined && guestAccessToken !== null) {
params.guest_access_token = guestAccessToken;
}
if (inhibitLogin !== undefined && inhibitLogin !== null) {
params.inhibit_login = inhibitLogin;
}
// Temporary parameter added to make the register endpoint advertise
// msisdn flows. This exists because there are clients that break
// when given stages they don't recognise. This parameter will cease
// to be necessary once these old clients are gone.
// Only send it if we send any params at all (the password param is
// mandatory, so if we send any params, we'll send the password param)
if (password !== undefined && password !== null) {
params.x_show_msisdn = true;
}
return this.registerRequest(params);
}
@@ -9786,11 +9797,21 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
* @param roomIdOrAlias - The ID or alias of the room to get the summary of.
* @param via - The list of servers which know about the room if only an ID was provided.
*/
public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise<IRoomSummary> {
const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias });
return this.http.authedRequest(Method.Get, path, { via }, undefined, {
public async getRoomSummary(roomIdOrAlias: string, via?: string[]): Promise<RoomSummary> {
const paramOpts = {
prefix: "/_matrix/client/unstable/im.nheko.summary",
});
};
try {
const path = utils.encodeUri("/summary/$roomid", { $roomid: roomIdOrAlias });
return await this.http.authedRequest(Method.Get, path, { via }, undefined, paramOpts);
} catch (e) {
if (e instanceof MatrixError && e.errcode === "M_UNRECOGNIZED") {
const path = utils.encodeUri("/rooms/$roomid/summary", { $roomid: roomIdOrAlias });
return await this.http.authedRequest(Method.Get, path, { via }, undefined, paramOpts);
} else {
throw e;
}
}
}
/**
+2 -1
View File
@@ -113,10 +113,11 @@ export interface CryptoBackend extends SyncCryptoCallbacks, CryptoApi {
* Import a list of room keys restored from backup
*
* @param keys - a list of session export objects
* @param backupVersion - the version of the backup these keys came from.
* @param opts - options object
* @returns a promise which resolves once the keys have been imported
*/
importBackedUpRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void>;
importBackedUpRoomKeys(keys: IMegolmSessionData[], backupVersion: string, opts?: ImportRoomKeysOpts): Promise<void>;
}
/** The methods which crypto implementations should expose to the Sync api
+29 -3
View File
@@ -30,7 +30,12 @@ import { encodeParams } from "./utils";
* for such URLs.
* @param allowRedirects - If true, the caller supports the URL being 307 or
* 308 redirected to another resource upon request. If false, redirects
* are not expected.
* are not expected. Implied `true` when `useAuthentication` is `true`.
* @param useAuthentication - If true, the caller supports authenticated
* media and wants an authentication-required URL. Note that server support
* for authenticated media will *not* be checked - it is the caller's responsibility
* to do so before calling this function. Note also that `useAuthentication`
* implies `allowRedirects`. Defaults to false (unauthenticated endpoints).
* @returns The complete URL to the content.
*/
export function getHttpUriForMxc(
@@ -41,6 +46,7 @@ export function getHttpUriForMxc(
resizeMethod?: string,
allowDirectLinks = false,
allowRedirects?: boolean,
useAuthentication?: boolean,
): string {
if (typeof mxc !== "string" || !mxc) {
return "";
@@ -52,8 +58,23 @@ export function getHttpUriForMxc(
return "";
}
}
if (useAuthentication) {
allowRedirects = true; // per docs (MSC3916 always expects redirects)
// Dev note: MSC3916 (as of writing) removes `allow_redirect` entirely, but
// for explicitness we set it here. This makes it slightly more obvious to
// callers, hopefully.
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/v3/download/";
let prefix: string;
if (useAuthentication) {
// TODO: Use stable once available (requires FCP on MSC3916).
prefix = "/_matrix/client/unstable/org.matrix.msc3916/media/download/";
} else {
prefix = "/_matrix/media/v3/download/";
}
const params: Record<string, string> = {};
if (width) {
@@ -68,7 +89,12 @@ export function getHttpUriForMxc(
if (Object.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/v3/thumbnail/";
if (useAuthentication) {
// TODO: Use stable once available (requires FCP on MSC3916).
prefix = "/_matrix/client/unstable/org.matrix.msc3916/media/thumbnail/";
} else {
prefix = "/_matrix/media/v3/thumbnail/";
}
}
if (typeof allowRedirects === "boolean") {
+59
View File
@@ -496,6 +496,42 @@ export interface CryptoApi {
* @param version - The backup version to delete.
*/
deleteKeyBackupVersion(version: string): Promise<void>;
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Dehydrated devices
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Returns whether MSC3814 dehydrated devices are supported by the crypto
* backend and by the server.
*
* This should be called before calling `startDehydration`, and if this
* returns `false`, `startDehydration` should not be called.
*/
isDehydrationSupported(): Promise<boolean>;
/**
* Start using device dehydration.
*
* - Rehydrates a dehydrated device, if one is available.
* - Creates a new dehydration key, if necessary, and stores it in Secret
* Storage.
* - If `createNewKey` is set to true, always creates a new key.
* - If a dehydration key is not available, creates a new one.
* - Creates a new dehydrated device, and schedules periodically creating
* new dehydrated devices.
*
* This function must not be called unless `isDehydrationSupported` returns
* `true`, and must not be called until after cross-signing and secret
* storage have been set up.
*
* @param createNewKey - whether to force creation of a new dehydration key.
* This can be used, for example, if Secret Storage is being reset. Defaults
* to false.
*/
startDehydration(createNewKey?: boolean): Promise<void>;
}
/** A reason code for a failure to decrypt an event. */
@@ -506,6 +542,29 @@ export enum DecryptionFailureCode {
/** Message was encrypted with a Megolm session which has been shared with us, but in a later ratchet state. */
OLM_UNKNOWN_MESSAGE_INDEX = "OLM_UNKNOWN_MESSAGE_INDEX",
/**
* Message was sent before the current device was created; there is no key backup on the server, so this
* decryption failure is expected.
*/
HISTORICAL_MESSAGE_NO_KEY_BACKUP = "HISTORICAL_MESSAGE_NO_KEY_BACKUP",
/**
* Message was sent before the current device was created; there was a key backup on the server, but we don't
* seem to have access to the backup. (Probably we don't have the right key.)
*/
HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED = "HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED",
/**
* Message was sent before the current device was created; there was a (usable) key backup on the server, but we
* still can't decrypt. (Either the session isn't in the backup, or we just haven't gotten around to checking yet.)
*/
HISTORICAL_MESSAGE_WORKING_BACKUP = "HISTORICAL_MESSAGE_WORKING_BACKUP",
/**
* Message was sent when the user was not a member of the room.
*/
HISTORICAL_MESSAGE_USER_NOT_JOINED = "HISTORICAL_MESSAGE_USER_NOT_JOINED",
/** Unknown or unclassified error. */
UNKNOWN_ERROR = "UNKNOWN_ERROR",
+1 -1
View File
@@ -63,7 +63,7 @@ export interface BackupTrustInfo {
}
/**
* The result of {@link CryptoApi.checkKeyBackupAndEnable}.
* The result of {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
*/
export interface KeyBackupCheck {
backupInfo: KeyBackupInfo;
+9 -9
View File
@@ -24,13 +24,13 @@ import { logger } from "../logger";
import { IndexedDBCryptoStore } from "../crypto/store/indexeddb-crypto-store";
import { decryptAES, encryptAES } from "./aes";
import { DeviceInfo } from "./deviceinfo";
import { ICrossSigningKey, ISignedKey, MatrixClient } from "../client";
import { ISignedKey, MatrixClient } from "../client";
import { OlmDevice } from "./OlmDevice";
import { ICryptoCallbacks } from ".";
import { ISignatures } from "../@types/signed";
import { CryptoStore, SecretStorePrivateKeys } from "./store/base";
import { ServerSideSecretStorage, SecretStorageKeyDescription } from "../secret-storage";
import { DeviceVerificationStatus, UserVerificationStatus as UserTrustLevel } from "../crypto-api";
import { CrossSigningKeyInfo, DeviceVerificationStatus, UserVerificationStatus as UserTrustLevel } from "../crypto-api";
import { decodeBase64, encodeBase64 } from "../base64";
// backwards-compatibility re-exports
@@ -38,7 +38,7 @@ export { UserTrustLevel };
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
function publicKeyFromKeyInfo(keyInfo: ICrossSigningKey): string {
function publicKeyFromKeyInfo(keyInfo: CrossSigningKeyInfo): string {
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
// We assume only a single key, and we want the bare form without type
// prefix, so we select the values.
@@ -51,13 +51,13 @@ export interface ICacheCallbacks {
}
export interface ICrossSigningInfo {
keys: Record<string, ICrossSigningKey>;
keys: Record<string, CrossSigningKeyInfo>;
firstUse: boolean;
crossSigningVerifiedBefore: boolean;
}
export class CrossSigningInfo {
public keys: Record<string, ICrossSigningKey> = {};
public keys: Record<string, CrossSigningKeyInfo> = {};
public firstUse = true;
// This tracks whether we've ever verified this user with any identity.
// When you verify a user, any devices online at the time that receive
@@ -296,7 +296,7 @@ export class CrossSigningInfo {
}
const privateKeys: Record<string, Uint8Array> = {};
const keys: Record<string, ICrossSigningKey> = {};
const keys: Record<string, CrossSigningKeyInfo> = {};
let masterSigning: PkSigning | undefined;
let masterPub: string | undefined;
@@ -368,8 +368,8 @@ export class CrossSigningInfo {
this.keys = {};
}
public setKeys(keys: Record<string, ICrossSigningKey>): void {
const signingKeys: Record<string, ICrossSigningKey> = {};
public setKeys(keys: Record<string, CrossSigningKeyInfo>): void {
const signingKeys: Record<string, CrossSigningKeyInfo> = {};
if (keys.master) {
if (keys.master.user_id !== this.userId) {
const error = "Mismatched user ID " + keys.master.user_id + " in master key from " + this.userId;
@@ -457,7 +457,7 @@ export class CrossSigningInfo {
}
}
public async signUser(key: CrossSigningInfo): Promise<ICrossSigningKey | undefined> {
public async signUser(key: CrossSigningInfo): Promise<CrossSigningKeyInfo | undefined> {
if (!this.keys.user_signing) {
logger.info("No user signing key: not signing user");
return;
+3 -10
View File
@@ -20,22 +20,15 @@ import { createCryptoStoreCacheCallbacks, ICacheCallbacks } from "./CrossSigning
import { IndexedDBCryptoStore } from "./store/indexeddb-crypto-store";
import { Method, ClientPrefix } from "../http-api";
import { Crypto, ICryptoCallbacks } from "./index";
import {
ClientEvent,
ClientEventHandlerMap,
CrossSigningKeys,
ICrossSigningKey,
ISignedKey,
KeySignatures,
} from "../client";
import { ClientEvent, ClientEventHandlerMap, CrossSigningKeys, ISignedKey, KeySignatures } from "../client";
import { IKeyBackupInfo } from "./keybackup";
import { TypedEventEmitter } from "../models/typed-event-emitter";
import { AccountDataClient, SecretStorageKeyDescription } from "../secret-storage";
import { BootstrapCrossSigningOpts } from "../crypto-api";
import { BootstrapCrossSigningOpts, CrossSigningKeyInfo } from "../crypto-api";
interface ICrossSigningKeys {
authUpload: BootstrapCrossSigningOpts["authUploadDeviceSigningKeys"];
keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>;
keys: Record<"master" | "self_signing" | "user_signing", CrossSigningKeyInfo>;
}
/**
+3
View File
@@ -871,6 +871,9 @@ export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
[Aes256.algorithmName]: Aes256,
};
// the linter doesn't like this but knip does
// eslint-disable-next-line tsdoc/syntax
/** @alias */
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
/**
+3 -3
View File
@@ -16,9 +16,9 @@ limitations under the License.
import { logger } from "../logger";
export let crypto = globalThis.window?.crypto;
export let subtleCrypto = globalThis.window?.crypto?.subtle ?? global.window?.crypto?.webkitSubtle;
export let TextEncoder = globalThis.window?.TextEncoder;
export let crypto = globalThis.crypto;
export let subtleCrypto = crypto?.subtle ?? crypto?.webkitSubtle; // TODO: Stop using webkitSubtle fallback
export let TextEncoder = globalThis.TextEncoder;
/* eslint-disable @typescript-eslint/no-var-requires */
if (!crypto) {
+39 -26
View File
@@ -56,14 +56,7 @@ import { Room, RoomEvent } from "../models/room";
import { RoomMember, RoomMemberEvent } from "../models/room-member";
import { EventStatus, IContent, IEvent, MatrixEvent, MatrixEventEvent } from "../models/event";
import { ToDeviceBatch } from "../models/ToDeviceMessage";
import {
ClientEvent,
ICrossSigningKey,
IKeysUploadResponse,
ISignedKey,
IUploadKeySignaturesResponse,
MatrixClient,
} from "../client";
import { ClientEvent, IKeysUploadResponse, ISignedKey, IUploadKeySignaturesResponse, MatrixClient } from "../client";
import { IRoomEncryption, RoomList } from "./RoomList";
import { IKeyBackupInfo } from "./keybackup";
import { ISyncStateData } from "../sync";
@@ -89,6 +82,7 @@ import { ISecretRequest } from "./SecretSharing";
import {
BackupTrustInfo,
BootstrapCrossSigningOpts,
CrossSigningKeyInfo,
CrossSigningStatus,
DecryptionFailureCode,
DeviceVerificationStatus,
@@ -649,7 +643,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
/**
* Implementation of {@link CryptoApi#getVersion}.
* Implementation of {@link Crypto.CryptoApi#getVersion}.
*/
public getVersion(): string {
const olmVersionTuple = Crypto.getOlmVersion();
@@ -809,7 +803,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
/**
* Implementation of {@link CryptoApi#getCrossSigningStatus}
* Implementation of {@link Crypto.CryptoApi#getCrossSigningStatus}
*/
public async getCrossSigningStatus(): Promise<CrossSigningStatus> {
const publicKeysOnDevice = Boolean(this.crossSigningInfo.getId());
@@ -1173,7 +1167,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
/**
* Implementation of {@link CryptoApi#resetKeyBackup}.
* Implementation of {@link Crypto.CryptoApi#resetKeyBackup}.
*/
public async resetKeyBackup(): Promise<void> {
// Delete existing ones
@@ -1209,7 +1203,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
/**
* Implementation of {@link CryptoApi#deleteKeyBackupVersion}.
* Implementation of {@link Crypto.CryptoApi#deleteKeyBackupVersion}.
*/
public async deleteKeyBackupVersion(version: string): Promise<void> {
await this.backupManager.deleteKeyBackupVersion(version);
@@ -1356,7 +1350,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* Get the current status of key backup.
*
* Implementation of {@link CryptoApi.getActiveSessionBackupVersion}.
* Implementation of {@link Crypto.CryptoApi.getActiveSessionBackupVersion}.
*/
public async getActiveSessionBackupVersion(): Promise<string | null> {
if (this.backupManager.getKeyBackupEnabled()) {
@@ -1378,7 +1372,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* Force a re-check of the key backup and enable/disable it as appropriate.
*
* Implementation of {@link CryptoApi.checkKeyBackupAndEnable}.
* Implementation of {@link Crypto.CryptoApi.checkKeyBackupAndEnable}.
*/
public async checkKeyBackupAndEnable(): Promise<KeyBackupCheck | null> {
const checkResult = await this.backupManager.checkKeyBackup();
@@ -1527,7 +1521,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*/
private async checkForValidDeviceSignature(
userId: string,
key: ICrossSigningKey,
key: CrossSigningKeyInfo,
devices: Record<string, IDevice>,
): Promise<string[]> {
const deviceIds: string[] = [];
@@ -1595,7 +1589,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
/**
* Implementation of {@link CryptoApi.getUserVerificationStatus}.
* Implementation of {@link Crypto.CryptoApi.getUserVerificationStatus}.
*/
public async getUserVerificationStatus(userId: string): Promise<UserTrustLevel> {
return this.checkUserTrust(userId);
@@ -1894,7 +1888,11 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* Implementation of {@link CryptoBackend#importBackedUpRoomKeys}.
*/
public importBackedUpRoomKeys(keys: IMegolmSessionData[], opts: ImportRoomKeysOpts = {}): Promise<void> {
public importBackedUpRoomKeys(
keys: IMegolmSessionData[],
backupVersion: string,
opts: ImportRoomKeysOpts = {},
): Promise<void> {
opts.source = "backup";
return this.importRoomKeys(keys, opts);
}
@@ -1904,7 +1902,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* @param keys - The new trusted set of keys
*/
private async storeTrustedSelfKeys(keys: Record<string, ICrossSigningKey> | null): Promise<void> {
private async storeTrustedSelfKeys(keys: Record<string, CrossSigningKeyInfo> | null): Promise<void> {
if (keys) {
this.crossSigningInfo.setKeys(keys);
} else {
@@ -1992,7 +1990,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* @returns base64-encoded ed25519 key.
*
* @deprecated Use {@link CryptoApi#getOwnDeviceKeys}.
* @deprecated Use {@link Crypto.CryptoApi#getOwnDeviceKeys}.
*/
public getDeviceEd25519Key(): string | null {
return this.olmDevice.deviceEd25519Key;
@@ -2003,14 +2001,14 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
*
* @returns base64-encoded curve25519 key.
*
* @deprecated Use {@link CryptoApi#getOwnDeviceKeys}
* @deprecated Use {@link Crypto.CryptoApi#getOwnDeviceKeys}
*/
public getDeviceCurve25519Key(): string | null {
return this.olmDevice.deviceCurve25519Key;
}
/**
* Implementation of {@link CryptoApi#getOwnDeviceKeys}.
* Implementation of {@link Crypto.CryptoApi#getOwnDeviceKeys}.
*/
public async getOwnDeviceKeys(): Promise<OwnDeviceKeys> {
if (!this.olmDevice.deviceCurve25519Key) {
@@ -2346,7 +2344,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* Mark the given device as locally verified.
*
* Implementation of {@link CryptoApi#setDeviceVerified}.
* Implementation of {@link Crypto.CryptoApi#setDeviceVerified}.
*/
public async setDeviceVerified(userId: string, deviceId: string, verified = true): Promise<void> {
await this.setDeviceVerification(userId, deviceId, verified);
@@ -2355,7 +2353,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
/**
* Blindly cross-sign one of our other devices.
*
* Implementation of {@link CryptoApi#crossSignDevice}.
* Implementation of {@link Crypto.CryptoApi#crossSignDevice}.
*/
public async crossSignDevice(deviceId: string): Promise<void> {
await this.setDeviceVerified(this.userId, deviceId, true);
@@ -2390,7 +2388,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
blocked: boolean | null = null,
known: boolean | null = null,
keys?: Record<string, string>,
): Promise<DeviceInfo | CrossSigningInfo | ICrossSigningKey | undefined> {
): Promise<DeviceInfo | CrossSigningInfo | CrossSigningKeyInfo | undefined> {
// Check if the 'device' is actually a cross signing key
// The js-sdk's verification treats cross-signing keys as devices
// and so uses this method to mark them verified.
@@ -2791,7 +2789,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
/**
* Implementation of {@link CryptoApi.getEncryptionInfoForEvent}.
* Implementation of {@link Crypto.CryptoApi.getEncryptionInfoForEvent}.
*/
public async getEncryptionInfoForEvent(event: MatrixEvent): Promise<EventEncryptionInfo | null> {
const encryptionInfo = this.getEventEncryptionInfo(event);
@@ -4274,7 +4272,7 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
}
/**
* Implementation of {@link CryptoApi#isEncryptionEnabledInRoom}.
* Implementation of {@link Crypto.CryptoApi#isEncryptionEnabledInRoom}.
*/
public async isEncryptionEnabledInRoom(roomId: string): Promise<boolean> {
return this.isRoomEncrypted(roomId);
@@ -4287,6 +4285,21 @@ export class Crypto extends TypedEventEmitter<CryptoEvent, CryptoEventHandlerMap
public getRoomEncryption(roomId: string): IRoomEncryption | null {
return this.roomList.getRoomEncryption(roomId);
}
/**
* Returns whether dehydrated devices are supported by the crypto backend
* and by the server.
*/
public async isDehydrationSupported(): Promise<boolean> {
return false;
}
/**
* Stub function -- dehydration is not implemented here, so throw error
*/
public async startDehydration(createNewKey?: boolean): Promise<void> {
throw new Error("Not implemented");
}
}
/**
+3 -3
View File
@@ -16,7 +16,6 @@ limitations under the License.
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { RoomKeyRequestState } from "../OutgoingRoomKeyRequestManager";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { TrackingStatus } from "../DeviceList";
import { IRoomEncryption } from "../RoomList";
@@ -27,6 +26,7 @@ import { InboundGroupSessionData } from "../OlmDevice";
import { MatrixEvent } from "../../models/event";
import { DehydrationManager } from "../dehydration";
import { IEncryptedPayload } from "../aes";
import { CrossSigningKeyInfo } from "../../crypto-api";
/**
* Internal module. Definitions for storage for the crypto module
@@ -100,13 +100,13 @@ export interface CryptoStore {
// Olm Account
getAccount(txn: unknown, func: (accountPickle: string | null) => void): void;
storeAccount(txn: unknown, accountPickle: string): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void;
getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void;
getSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
func: (key: SecretStorePrivateKeys[K] | null) => void,
type: K,
): void;
storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void;
storeCrossSigningKeys(txn: unknown, keys: Record<string, CrossSigningKeyInfo>): void;
storeSecretStorePrivateKey<K extends keyof SecretStorePrivateKeys>(
txn: unknown,
type: K,
@@ -33,11 +33,11 @@ import {
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
} from "./base";
import { IRoomKeyRequestBody, IRoomKeyRequestRecipient } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { IndexedDBCryptoStore } from "./indexeddb-crypto-store";
import { CrossSigningKeyInfo } from "../../crypto-api";
const PROFILE_TRANSACTIONS = false;
@@ -418,7 +418,7 @@ export class Backend implements CryptoStore {
public getCrossSigningKeys(
txn: IDBTransaction,
func: (keys: Record<string, ICrossSigningKey> | null) => void,
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
): void {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("crossSigningKeys");
@@ -447,7 +447,7 @@ export class Backend implements CryptoStore {
};
}
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, CrossSigningKeyInfo>): void {
const objectStore = txn.objectStore("account");
objectStore.put(keys, "crossSigningKeys");
}
+3 -3
View File
@@ -36,10 +36,10 @@ import {
ACCOUNT_OBJECT_KEY_MIGRATION_STATE,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { CrossSigningKeyInfo } from "../../crypto-api";
/*
* Internal module. indexeddb storage for e2e.
@@ -420,7 +420,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
*/
public getCrossSigningKeys(
txn: IDBTransaction,
func: (keys: Record<string, ICrossSigningKey> | null) => void,
func: (keys: Record<string, CrossSigningKeyInfo> | null) => void,
): void {
this.backend!.getCrossSigningKeys(txn, func);
}
@@ -444,7 +444,7 @@ export class IndexedDBCryptoStore implements CryptoStore {
* @param txn - An active transaction. See doTxn().
* @param keys - keys object as getCrossSigningKeys()
*/
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, ICrossSigningKey>): void {
public storeCrossSigningKeys(txn: IDBTransaction, keys: Record<string, CrossSigningKeyInfo>): void {
this.backend!.storeCrossSigningKeys(txn, keys);
}
@@ -31,9 +31,9 @@ import {
} from "./base";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { ICrossSigningKey } from "../../client";
import { InboundGroupSessionData } from "../OlmDevice";
import { safeSet } from "../../utils";
import { CrossSigningKeyInfo } from "../../crypto-api";
/**
* Internal module. Partial localStorage backed storage for e2e.
@@ -531,8 +531,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
setJsonItem(this.store, KEY_END_TO_END_ACCOUNT, accountPickle);
}
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
const keys = getJsonItem<Record<string, ICrossSigningKey>>(this.store, KEY_CROSS_SIGNING_KEYS);
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void {
const keys = getJsonItem<Record<string, CrossSigningKeyInfo>>(this.store, KEY_CROSS_SIGNING_KEYS);
func(keys);
}
@@ -545,7 +545,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore implements Crypto
func(key);
}
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
public storeCrossSigningKeys(txn: unknown, keys: Record<string, CrossSigningKeyInfo>): void {
setJsonItem(this.store, KEY_CROSS_SIGNING_KEYS, keys);
}
+26 -22
View File
@@ -32,10 +32,21 @@ import {
SESSION_BATCH_SIZE,
} from "./base";
import { IRoomKeyRequestBody } from "../index";
import { ICrossSigningKey } from "../../client";
import { IOlmDevice } from "../algorithms/megolm";
import { IRoomEncryption } from "../RoomList";
import { InboundGroupSessionData } from "../OlmDevice";
import { CrossSigningKeyInfo } from "../../crypto-api";
function encodeSessionKey(senderCurve25519Key: string, sessionId: string): string {
return encodeURIComponent(senderCurve25519Key) + "/" + encodeURIComponent(sessionId);
}
function decodeSessionKey(key: string): { senderKey: string; sessionId: string } {
const keyParts = key.split("/");
const senderKey = decodeURIComponent(keyParts[0]);
const sessionId = decodeURIComponent(keyParts[1]);
return { senderKey, sessionId };
}
/**
* Internal module. in-memory storage for e2e.
@@ -45,7 +56,7 @@ export class MemoryCryptoStore implements CryptoStore {
private migrationState: MigrationState = MigrationState.NOT_STARTED;
private outgoingRoomKeyRequests: OutgoingRoomKeyRequest[] = [];
private account: string | null = null;
private crossSigningKeys: Record<string, ICrossSigningKey> | null = null;
private crossSigningKeys: Record<string, CrossSigningKeyInfo> | null = null;
private privateKeys: Partial<SecretStorePrivateKeys> = {};
private sessions: { [deviceKey: string]: { [sessionId: string]: ISessionInfo } } = {};
@@ -308,7 +319,7 @@ export class MemoryCryptoStore implements CryptoStore {
this.account = accountPickle;
}
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, ICrossSigningKey> | null) => void): void {
public getCrossSigningKeys(txn: unknown, func: (keys: Record<string, CrossSigningKeyInfo> | null) => void): void {
func(this.crossSigningKeys);
}
@@ -321,7 +332,7 @@ export class MemoryCryptoStore implements CryptoStore {
func(result || null);
}
public storeCrossSigningKeys(txn: unknown, keys: Record<string, ICrossSigningKey>): void {
public storeCrossSigningKeys(txn: unknown, keys: Record<string, CrossSigningKeyInfo>): void {
this.crossSigningKeys = keys;
}
@@ -481,20 +492,14 @@ export class MemoryCryptoStore implements CryptoStore {
txn: unknown,
func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void,
): void {
const k = senderCurve25519Key + "/" + sessionId;
const k = encodeSessionKey(senderCurve25519Key, sessionId);
func(this.inboundGroupSessions[k] || null, this.inboundGroupSessionsWithheld[k] || null);
}
public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void {
for (const key of Object.keys(this.inboundGroupSessions)) {
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
func({
senderKey: key.slice(0, 43),
sessionId: key.slice(44),
...decodeSessionKey(key),
sessionData: this.inboundGroupSessions[key],
});
}
@@ -507,7 +512,7 @@ export class MemoryCryptoStore implements CryptoStore {
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
const k = senderCurve25519Key + "/" + sessionId;
const k = encodeSessionKey(senderCurve25519Key, sessionId);
if (this.inboundGroupSessions[k] === undefined) {
this.inboundGroupSessions[k] = sessionData;
}
@@ -519,7 +524,8 @@ export class MemoryCryptoStore implements CryptoStore {
sessionData: InboundGroupSessionData,
txn: unknown,
): void {
this.inboundGroupSessions[senderCurve25519Key + "/" + sessionId] = sessionData;
const k = encodeSessionKey(senderCurve25519Key, sessionId);
this.inboundGroupSessions[k] = sessionData;
}
public storeEndToEndInboundGroupSessionWithheld(
@@ -528,7 +534,7 @@ export class MemoryCryptoStore implements CryptoStore {
sessionData: IWithheld,
txn: unknown,
): void {
const k = senderCurve25519Key + "/" + sessionId;
const k = encodeSessionKey(senderCurve25519Key, sessionId);
this.inboundGroupSessionsWithheld[k] = sessionData;
}
@@ -554,8 +560,7 @@ export class MemoryCryptoStore implements CryptoStore {
const result: SessionExtended[] = [];
for (const [key, session] of Object.entries(this.inboundGroupSessions)) {
result.push({
senderKey: key.slice(0, 43),
sessionId: key.slice(44),
...decodeSessionKey(key),
sessionData: session,
needsBackup: key in this.sessionsNeedingBackup,
});
@@ -584,7 +589,7 @@ export class MemoryCryptoStore implements CryptoStore {
sessions: { senderKey: string; sessionId: string }[],
): Promise<void> {
for (const { senderKey, sessionId } of sessions) {
const k = senderKey + "/" + sessionId;
const k = encodeSessionKey(senderKey, sessionId);
delete this.inboundGroupSessions[k];
}
}
@@ -614,8 +619,7 @@ export class MemoryCryptoStore implements CryptoStore {
for (const session in this.sessionsNeedingBackup) {
if (this.inboundGroupSessions[session]) {
sessions.push({
senderKey: session.slice(0, 43),
sessionId: session.slice(44),
...decodeSessionKey(session),
sessionData: this.inboundGroupSessions[session],
});
if (limit && session.length >= limit) {
@@ -632,7 +636,7 @@ export class MemoryCryptoStore implements CryptoStore {
public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
for (const session of sessions) {
const sessionKey = session.senderKey + "/" + session.sessionId;
const sessionKey = encodeSessionKey(session.senderKey, session.sessionId);
delete this.sessionsNeedingBackup[sessionKey];
}
return Promise.resolve();
@@ -640,7 +644,7 @@ export class MemoryCryptoStore implements CryptoStore {
public markSessionsNeedingBackup(sessions: ISession[]): Promise<void> {
for (const session of sessions) {
const sessionKey = session.senderKey + "/" + session.sessionId;
const sessionKey = encodeSessionKey(session.senderKey, session.sessionId);
this.sessionsNeedingBackup[sessionKey] = true;
}
return Promise.resolve();
+1 -1
View File
@@ -74,7 +74,7 @@ export class ReciprocateQRCode extends Base {
await new Promise<void>((resolve, reject) => {
this.reciprocateQREvent = {
confirm: resolve,
cancel: () => reject(newUserCancelledError()),
cancel: (): void => reject(newUserCancelledError()),
};
this.emit(QrCodeEvent.ShowReciprocateQr, this.reciprocateQREvent);
});
+2 -2
View File
@@ -308,8 +308,8 @@ export class SAS extends Base {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
cancel: (): void => reject(newUserCancelledError()),
mismatch: (): void => reject(newMismatchedSASError()),
};
this.emit(SasEvent.ShowSas, this.sasEvent);
});
+3
View File
@@ -19,4 +19,7 @@ export enum Method {
Put = "PUT",
Post = "POST",
Delete = "DELETE",
Options = "OPTIONS",
Head = "HEAD",
Patch = "PATCH",
}
+4 -10
View File
@@ -121,10 +121,6 @@ interface ThreepidCreds {
type EmailIdentityDict = {
type: AuthType.Email;
threepid_creds: ThreepidCreds;
/**
* @deprecated in favour of `threepid_creds` - kept for backwards compatibility
*/
threepidCreds?: ThreepidCreds;
session: string;
};
@@ -343,15 +339,13 @@ export class InteractiveAuth<T> {
sid: this.emailSid,
client_secret: this.clientSecret,
};
const idServerParsedUrl = new URL(this.matrixClient.getIdentityServerUrl()!);
creds.id_server = idServerParsedUrl.host;
const isUrl = this.matrixClient.getIdentityServerUrl();
if (isUrl) {
creds.id_server = new URL(isUrl).host;
}
authDict = {
type: EMAIL_STAGE_TYPE,
// TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/matrix-org/synapse/issues/5665
// See https://github.com/matrix-org/matrix-doc/issues/2220
threepid_creds: creds,
threepidCreds: creds,
};
}
}
+1 -14
View File
@@ -77,6 +77,7 @@ export * from "./@types/extensible_events";
export * from "./@types/IIdentityServerProvider";
export * from "./models/room-summary";
export * from "./models/event-status";
export type { RoomSummary } from "./client";
export * as ContentHelpers from "./content-helpers";
export * as SecretStorage from "./secret-storage";
export type { ICryptoCallbacks } from "./crypto"; // used to be located here
@@ -113,20 +114,6 @@ export { LocationAssetType } from "./@types/location";
*/
export * as Crypto from "./crypto-api";
/**
* Backwards compatibility re-export
* @internal
* @deprecated use {@link Crypto.CryptoApi}
*/
export type { CryptoApi } from "./crypto-api";
/**
* Backwards compatibility re-export
* @internal
* @deprecated use {@link Crypto.DeviceVerificationStatus}
*/
export { DeviceVerificationStatus } from "./crypto-api";
let cryptoStoreFactory = (): CryptoStore => new MemoryCryptoStore();
/**
-1
View File
@@ -136,7 +136,6 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
for (const memberEvent of callMemberEvents) {
const eventMemberships: CallMembershipData[] = memberEvent.getContent()["memberships"];
if (eventMemberships === undefined) {
logger.debug(`Ignoring malformed member event from ${memberEvent.getSender()}: no memberships section`);
continue;
}
if (!Array.isArray(eventMemberships)) {
+1 -1
View File
@@ -35,7 +35,7 @@ import { ISendEventResponse } from "../@types/requests";
import { FileType } from "../http-api";
import { KnownMembership } from "../@types/membership";
import { RoomPowerLevelsEventContent, SpaceChildEventContent } from "../@types/state_events";
import { EncryptedFile, FileContent } from "../@types/media";
import type { EncryptedFile, FileContent } from "../@types/media";
/**
* The recommended defaults for a tree space's power levels. Note that this
+4
View File
@@ -51,6 +51,9 @@ export class Device {
/** display name of the device */
public readonly displayName?: string;
/** whether the device is a dehydrated device */
public readonly dehydrated: boolean = false;
public constructor(opts: DeviceParameters) {
this.deviceId = opts.deviceId;
this.userId = opts.userId;
@@ -59,6 +62,7 @@ export class Device {
this.verified = opts.verified || DeviceVerification.Unverified;
this.signatures = opts.signatures || new Map();
this.displayName = opts.displayName;
this.dehydrated = !!opts.dehydrated;
}
/**
+1 -36
View File
@@ -592,43 +592,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
*/
public addLiveEvent(
event: MatrixEvent,
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions,
): void;
/**
* @deprecated In favor of the overload with `IAddLiveEventOptions`
*/
public addLiveEvent(
event: MatrixEvent,
duplicateStrategy?: DuplicateStrategy,
fromCache?: boolean,
roomState?: RoomState,
): void;
public addLiveEvent(
event: MatrixEvent,
duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
fromCache = false,
roomState?: RoomState,
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions = {},
): void {
let duplicateStrategy = (duplicateStrategyOrOpts as DuplicateStrategy) || DuplicateStrategy.Ignore;
let timelineWasEmpty: boolean | undefined;
if (typeof duplicateStrategyOrOpts === "object") {
({
duplicateStrategy = DuplicateStrategy.Ignore,
fromCache = false,
roomState,
timelineWasEmpty,
} = duplicateStrategyOrOpts);
} else if (duplicateStrategyOrOpts !== undefined) {
// Deprecation warning
// FIXME: Remove after 2023-06-01 (technical debt)
logger.warn(
"Overload deprecated: " +
"`EventTimelineSet.addLiveEvent(event, duplicateStrategy?, fromCache?, roomState?)` " +
"is deprecated in favor of the overload with " +
"`EventTimelineSet.addLiveEvent(event, IAddLiveEventOptions)`",
);
}
if (this.filter) {
const events = this.filter.filterRoomTimeline([event]);
if (!events.length) {
+1 -22
View File
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { logger } from "../logger";
import { IMarkerFoundOptions, RoomState } from "./room-state";
import { EventTimelineSet } from "./event-timeline-set";
import { MatrixEvent } from "./event";
@@ -361,30 +360,10 @@ export class EventTimeline {
* @param event - new event
* @param options - addEvent options
*/
public addEvent(event: MatrixEvent, { toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions): void;
/**
* @deprecated In favor of the overload with `IAddEventOptions`
*/
public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, roomState?: RoomState): void;
public addEvent(
event: MatrixEvent,
toStartOfTimelineOrOpts: boolean | IAddEventOptions,
roomState?: RoomState,
{ toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions = { toStartOfTimeline: false },
): void {
let toStartOfTimeline = !!toStartOfTimelineOrOpts;
let timelineWasEmpty: boolean | undefined;
if (typeof toStartOfTimelineOrOpts === "object") {
({ toStartOfTimeline, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
} else if (toStartOfTimelineOrOpts !== undefined) {
// Deprecation warning
// FIXME: Remove after 2023-06-01 (technical debt)
logger.warn(
"Overload deprecated: " +
"`EventTimeline.addEvent(event, toStartOfTimeline, roomState?)` " +
"is deprecated in favor of the overload with `EventTimeline.addEvent(event, IAddEventOptions)`",
);
}
if (!roomState) {
roomState = toStartOfTimeline ? this.startState : this.endState;
}
+22 -17
View File
@@ -31,6 +31,7 @@ import {
RelationType,
ToDeviceMessageId,
UNSIGNED_THREAD_ID_FIELD,
UNSIGNED_MEMBERSHIP_FIELD,
} from "../@types/event";
import { Crypto } from "../crypto";
import { deepSortedObjectEntries, internaliseString } from "../utils";
@@ -76,6 +77,7 @@ export interface IUnsigned {
"invite_room_state"?: StrippedState[];
"m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
[UNSIGNED_THREAD_ID_FIELD.name]?: string;
[UNSIGNED_MEMBERSHIP_FIELD.name]?: Membership | string;
}
export interface IThreadBundledRelationship {
@@ -96,19 +98,6 @@ export interface IEvent {
membership?: Membership;
unsigned: IUnsigned;
redacts?: string;
/**
* @deprecated in favour of `sender`
*/
user_id?: string;
/**
* @deprecated in favour of `unsigned.prev_content`
*/
prev_content?: IContent;
/**
* @deprecated in favour of `origin_server_ts`
*/
age?: number;
}
export interface IAggregatedRelation {
@@ -493,7 +482,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @returns The user ID, e.g. `@alice:matrix.org`
*/
public getSender(): string | undefined {
return this.event.sender || this.event.user_id; // v2 / v1
return this.event.sender; // v2 / v1
}
/**
@@ -667,7 +656,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
*/
public getPrevContent(): IContent {
// v2 then v1 then default
return this.getUnsigned().prev_content || this.event.prev_content || {};
return this.getUnsigned().prev_content || {};
}
/**
@@ -691,7 +680,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* @returns The age of this event in milliseconds.
*/
public getAge(): number | undefined {
return this.getUnsigned().age || this.event.age; // v2 / v1
return this.getUnsigned().age;
}
/**
@@ -721,6 +710,22 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
return this.event.state_key !== undefined;
}
/**
* Get the user's room membership at the time the event was sent, as reported
* by the server. This uses MSC4115.
*
* @returns The user's room membership, or `undefined` if the server does
* not report it.
*/
public getMembershipAtEvent(): Membership | string | undefined {
const unsigned = this.getUnsigned();
if (typeof unsigned[UNSIGNED_MEMBERSHIP_FIELD.name] === "string") {
return unsigned[UNSIGNED_MEMBERSHIP_FIELD.name];
} else {
return undefined;
}
}
/**
* Replace the content of this event with encrypted versions.
* (This is used when sending an event; it should not be used by applications).
@@ -1066,7 +1071,7 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
* signing the public curve25519 key with the ed25519 key.
*
* In general, applications should not use this method directly, but should
* instead use {@link CryptoApi#getEncryptionInfoForEvent}.
* instead use {@link Crypto.CryptoApi#getEncryptionInfoForEvent}.
*/
public getClaimedEd25519Key(): string | null {
return this.claimedEd25519Key;
+5 -2
View File
@@ -20,12 +20,13 @@ import { isNumber, removeHiddenChars } from "../utils";
import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event";
import { IEvent, MatrixEvent, MatrixEventEvent } from "./event";
import { MatrixClient } from "../client";
import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
import { GuestAccess, HistoryVisibility, JoinRule } from "../@types/partials";
import { TypedEventEmitter } from "./typed-event-emitter";
import { Beacon, BeaconEvent, BeaconEventHandlerMap, getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
import { TypedReEmitter } from "../ReEmitter";
import { M_BEACON, M_BEACON_INFO } from "../@types/beacon";
import { KnownMembership } from "../@types/membership";
import { RoomJoinRulesEventContent } from "../@types/state_events";
export interface IMarkerFoundOptions {
/** Whether the timeline was empty before the marker event arrived in the
@@ -78,6 +79,8 @@ export enum RoomStateEvent {
export type RoomStateEventHandlerMap = {
/**
* Fires whenever the event dictionary in room state is updated.
* This does not guarantee that any related objects (like RoomMember) have been updated.
* Use RoomStateEvent.Update for that.
* @param event - The matrix event which caused this event to fire.
* @param state - The room state whose RoomState.events dictionary
* was updated.
@@ -960,7 +963,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
*/
public getJoinRule(): JoinRule {
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
const joinRuleContent: Partial<IJoinRuleEventContent> = joinRuleEvent?.getContent() ?? {};
const joinRuleContent: Partial<RoomJoinRulesEventContent> = joinRuleEvent?.getContent() ?? {};
return joinRuleContent["join_rule"] || JoinRule.Invite;
}
+3 -96
View File
@@ -405,12 +405,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
*/
private threads = new Map<string, Thread>();
/**
* @deprecated This value is unreliable. It may not contain the last thread.
* Use {@link Room.getLastThread} instead.
*/
public lastThread?: Thread;
/**
* A mapping of eventId to all visibility changes to apply
* to the event, by chronological order, as per
@@ -605,27 +599,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
return createEvent.getContent()["room_version"] ?? "1";
}
/**
* Determines whether this room needs to be upgraded to a new version
* @returns What version the room should be upgraded to, or null if
* the room does not require upgrading at this time.
* @deprecated Use #getRecommendedVersion() instead
*/
public shouldUpgradeToVersion(): string | null {
// TODO: Remove this function.
// This makes assumptions about which versions are safe, and can easily
// be wrong. Instead, people are encouraged to use getRecommendedVersion
// which determines a safer value. This function doesn't use that function
// because this is not async-capable, and to avoid breaking the contract
// we're deprecating this.
if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
return KNOWN_SAFE_ROOM_VERSION;
}
return null;
}
/**
* Determines the recommended room version for the room. This returns an
* object with 3 properties: `version` as the new version the
@@ -1403,32 +1376,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
}
}
/**
* Returns whether there are any devices in the room that are unverified
*
* Note: Callers should first check if crypto is enabled on this device. If it is
* disabled, then we aren't tracking room devices at all, so we can't answer this, and an
* error will be thrown.
*
* @returns the result
*
* @deprecated Not supported under rust crypto. Instead, call {@link Room.getEncryptionTargetMembers},
* {@link CryptoApi.getUserDeviceInfo}, and {@link CryptoApi.getDeviceVerificationStatus}.
*/
public async hasUnverifiedDevices(): Promise<boolean> {
if (!this.hasEncryptionStateEvent()) {
return false;
}
const e2eMembers = await this.getEncryptionTargetMembers();
for (const member of e2eMembers) {
const devices = this.client.getStoredDevicesForUser(member.userId);
if (devices.some((device) => device.isUnverified())) {
return true;
}
}
return false;
}
/**
* Return the timeline sets for this room.
* @returns array of timeline sets for this room
@@ -2479,15 +2426,6 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// and pass the event through this.
thread.addEvents(events, false);
const isNewer =
this.lastThread?.rootEvent &&
rootEvent?.localTimestamp &&
this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp;
if (!this.lastThread || isNewer) {
this.lastThread = thread;
}
// We need to update the thread root events, but the thread may not be ready yet.
// If it isn't, it will fire ThreadEvent.Update when it is and we'll call updateThreadRootEvents then.
if (this.threadsReady && thread.initialEventsFetched) {
@@ -2917,39 +2855,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* @param addLiveEventOptions - addLiveEvent options
* @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
*/
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void>;
/**
* @deprecated In favor of the overload with `IAddLiveEventOptions`
*/
public async addLiveEvents(
events: MatrixEvent[],
duplicateStrategy?: DuplicateStrategy,
fromCache?: boolean,
): Promise<void>;
public async addLiveEvents(
events: MatrixEvent[],
duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
fromCache = false,
): Promise<void> {
let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy;
let timelineWasEmpty: boolean | undefined = false;
if (typeof duplicateStrategyOrOpts === "object") {
({
duplicateStrategy,
fromCache = false,
/* roomState, (not used here) */
timelineWasEmpty,
} = duplicateStrategyOrOpts);
} else if (duplicateStrategyOrOpts !== undefined) {
// Deprecation warning
// FIXME: Remove after 2023-06-01 (technical debt)
logger.warn(
"Overload deprecated: " +
"`Room.addLiveEvents(events, duplicateStrategy?, fromCache?)` " +
"is deprecated in favor of the overload with `Room.addLiveEvents(events, IAddLiveEventOptions)`",
);
}
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void> {
const { duplicateStrategy, fromCache, timelineWasEmpty = false } = addLiveEventOptions ?? {};
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
@@ -3250,7 +3157,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
content: strippedEvent.content,
event_id: "$fake" + Date.now(),
room_id: this.roomId,
user_id: this.myUserId, // technically a lie
sender: this.myUserId, // technically a lie
}),
]);
}
+18 -27
View File
@@ -50,23 +50,31 @@ interface OidcRegistrationRequestBody {
}
/**
* Make the client registration request
* @param registrationEndpoint - URL as returned from issuer ./well-known/openid-configuration
* @param clientMetadata - registration metadata
* @returns resolves to the registered client id when registration is successful
* @throws An `Error` with `message` set to an entry in {@link OidcError},
* when the registration request fails, or the response is invalid.
* Attempts dynamic registration against the configured registration endpoint
* @param delegatedAuthConfig - Auth config from {@link discoverAndValidateOIDCIssuerWellKnown}
* @param clientMetadata - The metadata for the client which to register
* @returns Promise<string> resolved with registered clientId
* @throws when registration is not supported, on failed request or invalid response
*/
const doRegistration = async (
registrationEndpoint: string,
export const registerOidcClient = async (
delegatedAuthConfig: OidcClientConfig,
clientMetadata: OidcRegistrationClientMetadata,
): Promise<string> => {
if (!delegatedAuthConfig.registrationEndpoint) {
throw new Error(OidcError.DynamicRegistrationNotSupported);
}
const grantTypes: NonEmptyArray<string> = ["authorization_code", "refresh_token"];
if (grantTypes.some((scope) => !delegatedAuthConfig.metadata.grant_types_supported.includes(scope))) {
throw new Error(OidcError.DynamicRegistrationNotSupported);
}
// https://openid.net/specs/openid-connect-registration-1_0.html
const metadata: OidcRegistrationRequestBody = {
client_name: clientMetadata.clientName,
client_uri: clientMetadata.clientUri,
response_types: ["code"],
grant_types: ["authorization_code", "refresh_token"],
grant_types: grantTypes,
redirect_uris: clientMetadata.redirectUris,
id_token_signed_response_alg: "RS256",
token_endpoint_auth_method: "none",
@@ -82,7 +90,7 @@ const doRegistration = async (
};
try {
const response = await fetch(registrationEndpoint, {
const response = await fetch(delegatedAuthConfig.registrationEndpoint, {
method: Method.Post,
headers,
body: JSON.stringify(metadata),
@@ -108,20 +116,3 @@ const doRegistration = async (
}
}
};
/**
* Attempts dynamic registration against the configured registration endpoint
* @param delegatedAuthConfig - Auth config from {@link discoverAndValidateOIDCIssuerWellKnown}
* @param clientMetadata - The metadata for the client which to register
* @returns Promise<string> resolved with registered clientId
* @throws when registration is not supported, on failed request or invalid response
*/
export const registerOidcClient = async (
delegatedAuthConfig: OidcClientConfig,
clientMetadata: OidcRegistrationClientMetadata,
): Promise<string> => {
if (!delegatedAuthConfig.registrationEndpoint) {
throw new Error(OidcError.DynamicRegistrationNotSupported);
}
return doRegistration(delegatedAuthConfig.registrationEndpoint, clientMetadata);
};
+12 -26
View File
@@ -15,7 +15,7 @@ limitations under the License.
*/
import { jwtDecode } from "jwt-decode";
import { OidcMetadata, SigninResponse } from "oidc-client-ts";
import { IdTokenClaims, OidcMetadata, SigninResponse } from "oidc-client-ts";
import { logger } from "../logger";
import { OidcError } from "./error";
@@ -83,6 +83,7 @@ export const validateOIDCIssuerWellKnown = (wellKnown: unknown): ValidatedIssuer
requiredStringProperty(wellKnown, "revocation_endpoint"),
optionalStringProperty(wellKnown, "registration_endpoint"),
optionalStringProperty(wellKnown, "account_management_uri"),
optionalStringProperty(wellKnown, "device_authorization_endpoint"),
optionalStringArrayProperty(wellKnown, "account_management_actions_supported"),
requiredArrayValue(wellKnown, "response_types_supported", "code"),
requiredArrayValue(wellKnown, "grant_types_supported", "authorization_code"),
@@ -118,6 +119,7 @@ export type ValidatedIssuerMetadata = Partial<OidcMetadata> &
| "response_types_supported"
| "grant_types_supported"
| "code_challenge_methods_supported"
| "device_authorization_endpoint"
> & {
// MSC2965 extensions to the OIDC spec
account_management_uri?: string;
@@ -137,28 +139,7 @@ export function isValidatedIssuerMetadata(
validateOIDCIssuerWellKnown(metadata);
}
/**
* Standard JWT claims.
*
* @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
*/
interface JwtClaims {
[claim: string]: unknown;
/** The "iss" (issuer) claim identifies the principal that issued the JWT. */
iss?: string;
/** The "sub" (subject) claim identifies the principal that is the subject of the JWT. */
sub?: string;
/** The "aud" (audience) claim identifies the recipients that the JWT is intended for. */
aud?: string | string[];
/** The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. */
exp?: number;
// unused claims excluded
}
interface IdTokenClaims extends JwtClaims {
nonce?: string;
}
const decodeIdToken = (token: string): IdTokenClaims => {
export const decodeIdToken = (token: string): IdTokenClaims => {
try {
return jwtDecode<IdTokenClaims>(token);
} catch (error) {
@@ -176,7 +157,12 @@ const decodeIdToken = (token: string): IdTokenClaims => {
* @param nonce - nonce used in the authentication request
* @throws when id token is invalid
*/
export const validateIdToken = (idToken: string | undefined, issuer: string, clientId: string, nonce: string): void => {
export const validateIdToken = (
idToken: string | undefined,
issuer: string,
clientId: string,
nonce: string | undefined,
): void => {
try {
if (!idToken) {
throw new Error("No ID token");
@@ -201,7 +187,7 @@ export const validateIdToken = (idToken: string | undefined, issuer: string, cli
* If a nonce value was sent in the Authentication Request, a nonce Claim MUST be present and its value checked
* to verify that it is the same value as the one that was sent in the Authentication Request.
*/
if (claims.nonce !== nonce) {
if (nonce !== undefined && claims.nonce !== nonce) {
throw new Error("Invalid nonce");
}
@@ -269,7 +255,7 @@ export type BearerTokenResponse = {
expires_in?: number;
// from oidc-client-ts
expires_at?: number;
id_token?: string;
id_token: string;
};
/**
+307
View File
@@ -0,0 +1,307 @@
/*
Copyright 2024 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 * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { OutgoingRequestProcessor } from "./OutgoingRequestProcessor";
import { encodeUri } from "../utils";
import { IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
import { IToDeviceEvent } from "../sync-accumulator";
import { ServerSideSecretStorage } from "../secret-storage";
import { crypto } from "../crypto/crypto";
import { decodeBase64, encodeUnpaddedBase64 } from "../base64";
import { Logger } from "../logger";
/**
* The response body of `GET /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device`.
*/
interface DehydratedDeviceResp {
device_id: string;
device_data: {
algorithm: string;
};
}
/**
* The response body of `POST /_matrix/client/unstable/org.matrix.msc3814.v1/dehydrated_device/events`.
*/
interface DehydratedDeviceEventsResp {
events: IToDeviceEvent[];
next_batch: string;
}
/**
* The unstable URL prefix for dehydrated device endpoints
*/
export const UnstablePrefix = "/_matrix/client/unstable/org.matrix.msc3814.v1";
/**
* The name used for the dehydration key in Secret Storage
*/
const SECRET_STORAGE_NAME = "org.matrix.msc3814";
/**
* The interval between creating dehydrated devices. (one week)
*/
const DEHYDRATION_INTERVAL = 7 * 24 * 60 * 60 * 1000;
/**
* Manages dehydrated devices
*
* We have one of these per `RustCrypto`. It's responsible for
*
* * determining server support for dehydrated devices
* * creating new dehydrated devices when requested, including periodically
* replacing the dehydrated device with a new one
* * rehydrating a device when requested, and when present
*
* @internal
*/
export class DehydratedDeviceManager {
/** the secret key used for dehydrating and rehydrating */
private key?: Uint8Array;
/** the ID of the interval for periodically replacing the dehydrated device */
private intervalId?: ReturnType<typeof setInterval>;
public constructor(
private readonly logger: Logger,
private readonly olmMachine: RustSdkCryptoJs.OlmMachine,
private readonly http: MatrixHttpApi<IHttpOpts & { onlyData: true }>,
private readonly outgoingRequestProcessor: OutgoingRequestProcessor,
private readonly secretStorage: ServerSideSecretStorage,
) {}
/**
* Return whether the server supports dehydrated devices.
*/
public async isSupported(): Promise<boolean> {
// call the endpoint to get a dehydrated device. If it returns an
// M_UNRECOGNIZED error, then dehydration is unsupported. If it returns
// a successful response, or an M_NOT_FOUND, then dehydration is supported.
// Any other exceptions are passed through.
try {
await this.http.authedRequest<DehydratedDeviceResp>(
Method.Get,
"/dehydrated_device",
undefined,
undefined,
{
prefix: UnstablePrefix,
},
);
} catch (error) {
const err = error as MatrixError;
if (err.errcode === "M_UNRECOGNIZED") {
return false;
} else if (err.errcode === "M_NOT_FOUND") {
return true;
}
throw error;
}
return true;
}
/**
* Start using device dehydration.
*
* - Rehydrates a dehydrated device, if one is available.
* - Creates a new dehydration key, if necessary, and stores it in Secret
* Storage.
* - If `createNewKey` is set to true, always creates a new key.
* - If a dehydration key is not available, creates a new one.
* - Creates a new dehydrated device, and schedules periodically creating
* new dehydrated devices.
*
* @param createNewKey - whether to force creation of a new dehydration key.
* This can be used, for example, if Secret Storage is being reset.
*/
public async start(createNewKey?: boolean): Promise<void> {
this.stop();
try {
await this.rehydrateDeviceIfAvailable();
} catch (e) {
// If rehydration fails, there isn't much we can do about it. Log
// the error, and create a new device.
this.logger.info("dehydration: Error rehydrating device:", e);
}
if (createNewKey) {
await this.resetKey();
}
await this.scheduleDeviceDehydration();
}
/**
* Return whether the dehydration key is stored in Secret Storage.
*/
public async isKeyStored(): Promise<boolean> {
return Boolean(await this.secretStorage.isStored(SECRET_STORAGE_NAME));
}
/**
* Reset the dehydration key.
*
* Creates a new key and stores it in secret storage.
*/
public async resetKey(): Promise<void> {
const key = new Uint8Array(32);
crypto.getRandomValues(key);
await this.secretStorage.store(SECRET_STORAGE_NAME, encodeUnpaddedBase64(key));
this.key = key;
}
/**
* Get and cache the encryption key from secret storage.
*
* If `create` is `true`, creates a new key if no existing key is present.
*
* @returns the key, if available, or `null` if no key is available
*/
private async getKey(create: boolean): Promise<Uint8Array | null> {
if (this.key === undefined) {
const keyB64 = await this.secretStorage.get(SECRET_STORAGE_NAME);
if (keyB64 === undefined) {
if (!create) {
return null;
}
await this.resetKey();
} else {
this.key = decodeBase64(keyB64);
}
}
return this.key!;
}
/**
* Rehydrate the dehydrated device stored on the server.
*
* Checks if there is a dehydrated device on the server. If so, rehydrates
* the device and processes the to-device events.
*
* Returns whether or not a dehydrated device was found.
*/
public async rehydrateDeviceIfAvailable(): Promise<boolean> {
const key = await this.getKey(false);
if (!key) {
return false;
}
let dehydratedDeviceResp;
try {
dehydratedDeviceResp = await this.http.authedRequest<DehydratedDeviceResp>(
Method.Get,
"/dehydrated_device",
undefined,
undefined,
{
prefix: UnstablePrefix,
},
);
} catch (error) {
const err = error as MatrixError;
// We ignore M_NOT_FOUND (there is no dehydrated device, so nothing
// us to do) and M_UNRECOGNIZED (the server does not understand the
// endpoint). We pass through any other errors.
if (err.errcode === "M_NOT_FOUND" || err.errcode === "M_UNRECOGNIZED") {
this.logger.info("dehydration: No dehydrated device");
return false;
}
throw err;
}
this.logger.info("dehydration: dehydrated device found");
const rehydratedDevice = await this.olmMachine
.dehydratedDevices()
.rehydrate(
key,
new RustSdkCryptoJs.DeviceId(dehydratedDeviceResp.device_id),
JSON.stringify(dehydratedDeviceResp.device_data),
);
this.logger.info("dehydration: device rehydrated");
let nextBatch: string | undefined = undefined;
let toDeviceCount = 0;
let roomKeyCount = 0;
const path = encodeUri("/dehydrated_device/$device_id/events", {
$device_id: dehydratedDeviceResp.device_id,
});
// eslint-disable-next-line no-constant-condition
while (true) {
const eventResp: DehydratedDeviceEventsResp = await this.http.authedRequest<DehydratedDeviceEventsResp>(
Method.Post,
path,
undefined,
nextBatch ? { next_batch: nextBatch } : {},
{
prefix: UnstablePrefix,
},
);
if (eventResp.events.length === 0) {
break;
}
toDeviceCount += eventResp.events.length;
nextBatch = eventResp.next_batch;
const roomKeyInfos = await rehydratedDevice.receiveEvents(JSON.stringify(eventResp.events));
roomKeyCount += roomKeyInfos.length;
}
this.logger.info(`dehydration: received ${roomKeyCount} room keys from ${toDeviceCount} to-device events`);
return true;
}
/**
* Creates and uploads a new dehydrated device.
*
* Creates and stores a new key in secret storage if none is available.
*/
public async createAndUploadDehydratedDevice(): Promise<void> {
const key = (await this.getKey(true))!;
const dehydratedDevice = await this.olmMachine.dehydratedDevices().create();
const request = await dehydratedDevice.keysForUpload("Dehydrated device", key);
await this.outgoingRequestProcessor.makeOutgoingRequest(request);
this.logger.info("dehydration: uploaded device");
}
/**
* Schedule periodic creation of dehydrated devices.
*/
public async scheduleDeviceDehydration(): Promise<void> {
// cancel any previously-scheduled tasks
this.stop();
await this.createAndUploadDehydratedDevice();
this.intervalId = setInterval(() => {
this.createAndUploadDehydratedDevice().catch((error) => {
this.logger.error("Error creating dehydrated device:", error);
});
}, DEHYDRATION_INTERVAL);
}
/**
* Stop the dehydrated device manager.
*
* Cancels any scheduled dehydration tasks.
*/
public stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = undefined;
}
}
}
+8 -1
View File
@@ -20,6 +20,7 @@ import {
KeysQueryRequest,
KeysUploadRequest,
OlmMachine,
PutDehydratedDeviceRequest,
RoomMessageRequest,
SignatureUploadRequest,
ToDeviceRequest,
@@ -32,6 +33,7 @@ import { logDuration, QueryDict, sleep } from "../utils";
import { AuthDict, UIAuthCallback } from "../interactive-auth";
import { UIAResponse } from "../@types/uia";
import { ToDeviceMessageId } from "../@types/event";
import { UnstablePrefix as DehydrationUnstablePrefix } from "./DehydratedDeviceManager";
/**
* Common interface for all the request types returned by `OlmMachine.outgoingRequests`.
@@ -62,7 +64,7 @@ export class OutgoingRequestProcessor {
) {}
public async makeOutgoingRequest<T>(
msg: OutgoingRequest | UploadSigningKeysRequest,
msg: OutgoingRequest | UploadSigningKeysRequest | PutDehydratedDeviceRequest,
uiaCallback?: UIAuthCallback<T>,
): Promise<void> {
let resp: string;
@@ -102,6 +104,11 @@ export class OutgoingRequestProcessor {
);
// SigningKeysUploadRequest does not implement OutgoingRequest and does not need to be marked as sent.
return;
} else if (msg instanceof PutDehydratedDeviceRequest) {
const path = DehydrationUnstablePrefix + "/dehydrated_device";
await this.rawJsonRequest(Method.Put, path, {}, msg.body);
// PutDehydratedDeviceRequest does not implement OutgoingRequest and does not need to be marked as sent.
return;
} else {
logger.warn("Unsupported outgoing message", Object.getPrototypeOf(msg));
resp = "";
@@ -17,7 +17,7 @@ limitations under the License.
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import { OlmMachine } from "@matrix-org/matrix-sdk-crypto-wasm";
import { Curve25519AuthData, KeyBackupSession } from "../crypto-api/keybackup";
import { Curve25519AuthData, KeyBackupInfo, KeyBackupSession } from "../crypto-api/keybackup";
import { Logger } from "../logger";
import { ClientPrefix, IHttpOpts, MatrixError, MatrixHttpApi, Method } from "../http-api";
import { RustBackupManager } from "./backup";
@@ -57,10 +57,14 @@ class KeyDownloadRateLimitError extends Error {
/** Details of a megolm session whose key we are trying to fetch. */
type SessionInfo = { roomId: string; megolmSessionId: string };
/** Holds the current backup decryptor and version that should be used. */
/** Holds the current backup decryptor and version that should be used.
*
* This is intended to be used as an immutable object (a new instance should be created if the configuration changes),
* and some of the logic relies on that, so the properties are marked as `readonly`.
*/
type Configuration = {
backupVersion: string;
decryptor: BackupDecryptor;
readonly backupVersion: string;
readonly decryptor: BackupDecryptor;
};
/**
@@ -76,7 +80,11 @@ type Configuration = {
export class PerSessionKeyBackupDownloader {
private stopped = false;
/** The version and decryption key to use with current backup if all set up correctly */
/**
* The version and decryption key to use with current backup if all set up correctly.
*
* Will not be set unless `hasConfigurationProblem` is `false`.
*/
private configuration: Configuration | null = null;
/** We remember when a session was requested and not found in backup to avoid query again too soon.
@@ -119,6 +127,24 @@ export class PerSessionKeyBackupDownloader {
backupManager.on(CryptoEvent.KeyBackupDecryptionKeyCached, this.onBackupStatusChanged);
}
/**
* Check if key download is successfully configured and active.
*
* @return `true` if key download is correctly configured and active; otherwise `false`.
*/
public isKeyBackupDownloadConfigured(): boolean {
return this.configuration !== null;
}
/**
* Return the details of the latest backup on the server, when we last checked.
*
* This is just a convenience method to expose {@link RustBackupManager.getServerBackupInfo}.
*/
public async getServerBackupInfo(): Promise<KeyBackupInfo | null | undefined> {
return await this.backupManager.getServerBackupInfo();
}
/**
* Called when a MissingRoomKey or UnknownMessageIndex decryption error is encountered.
*
@@ -370,7 +396,7 @@ export class PerSessionKeyBackupDownloader {
for (const k of keys) {
k.room_id = sessionInfo.roomId;
}
await this.backupManager.importBackedUpRoomKeys(keys);
await this.backupManager.importBackedUpRoomKeys(keys, configuration.backupVersion);
}
/**
@@ -410,7 +436,7 @@ export class PerSessionKeyBackupDownloader {
private async internalCheckFromServer(): Promise<Configuration | null> {
let currentServerVersion = null;
try {
currentServerVersion = await this.backupManager.requestKeyBackupVersion();
currentServerVersion = await this.backupManager.getServerBackupInfo();
} catch (e) {
this.logger.debug(`Backup: error while checking server version: ${e}`);
this.hasConfigurationProblem = true;
+37 -3
View File
@@ -58,6 +58,16 @@ interface KeyBackupCreationInfo {
export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents, RustBackupCryptoEventMap> {
/** Have we checked if there is a backup on the server which we can use */
private checkedForBackup = false;
/**
* The latest backup version on the server, when we last checked.
*
* If there was no backup on the server, `null`. If our attempt to check resulted in an error, `undefined`.
*
* Note that the backup was not necessarily verified.
*/
private serverBackupInfo: KeyBackupInfo | null | undefined = undefined;
private activeBackupVersion: string | null = null;
private stopped = false;
@@ -89,6 +99,21 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
return this.activeBackupVersion;
}
/**
* Return the details of the latest backup on the server, when we last checked.
*
* This normally returns a cached value, but if we haven't yet made a request to the server, it will fire one off.
* It will always return the details of the active backup if key backup is enabled.
*
* If there was no backup on the server, `null`. If our attempt to check resulted in an error, `undefined`.
*/
public async getServerBackupInfo(): Promise<KeyBackupInfo | null | undefined> {
// Do a validity check if we haven't already done one. The check is likely to fail if we don't yet have the
// backup keys -- but as a side-effect, it will populate `serverBackupInfo`.
await this.checkKeyBackupAndEnable(false);
return this.serverBackupInfo;
}
/**
* Determine if a key backup can be trusted.
*
@@ -214,7 +239,11 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
/**
* Implementation of {@link CryptoBackend#importBackedUpRoomKeys}.
*/
public async importBackedUpRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void> {
public async importBackedUpRoomKeys(
keys: IMegolmSessionData[],
backupVersion: string,
opts?: ImportRoomKeysOpts,
): Promise<void> {
const keysByRoom: Map<RustSdkCryptoJs.RoomId, Map<string, IMegolmSessionData>> = new Map();
for (const key of keys) {
const roomId = new RustSdkCryptoJs.RoomId(key.room_id);
@@ -234,6 +263,7 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
};
opts?.progressCallback?.(importOpt);
},
backupVersion,
);
}
@@ -242,18 +272,21 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
/** Helper for `checkKeyBackup` */
private async doCheckKeyBackup(): Promise<KeyBackupCheck | null> {
logger.log("Checking key backup status...");
let backupInfo: KeyBackupInfo | null = null;
let backupInfo: KeyBackupInfo | null | undefined;
try {
backupInfo = await this.requestKeyBackupVersion();
} catch (e) {
logger.warn("Error checking for active key backup", e);
this.serverBackupInfo = undefined;
return null;
}
this.checkedForBackup = true;
if (backupInfo && !backupInfo.version) {
logger.warn("active backup lacks a useful 'version'; ignoring it");
backupInfo = undefined;
}
this.serverBackupInfo = backupInfo;
const activeVersion = await this.getActiveBackupVersion();
@@ -462,12 +495,13 @@ export class RustBackupManager extends TypedEventEmitter<RustBackupCryptoEvents,
}
return count;
}
/**
* Get information about the current key backup from the server
*
* @returns Information object from API or null if there is no active backup.
*/
public async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> {
private async requestKeyBackupVersion(): Promise<KeyBackupInfo | null> {
return await requestKeyBackupVersion(this.http);
}
+1
View File
@@ -80,6 +80,7 @@ export function rustDeviceToJsDevice(device: RustSdkCryptoJs.Device, userId: Rus
verified,
signatures,
displayName: device.displayName,
dehydrated: device.isDehydrated,
});
}
+21 -7
View File
@@ -64,13 +64,21 @@ export async function initRustCrypto(args: {
storePrefix: string | null;
/**
* A passphrase to use to encrypt the indexeddbs created by rust-crypto.
* A passphrase to use to encrypt the indexeddb created by rust-crypto.
*
* Ignored if `storePrefix` is null. If this is `undefined` (and `storePrefix` is not null), the indexeddbs
* will be unencrypted.
* Ignored if `storePrefix` is null, or `storeKey` is set. If neither this nor `storeKey` is set
* (and `storePrefix` is not null), the indexeddb will be unencrypted.
*/
storePassphrase?: string;
/**
* A key to use to encrypt the indexeddb created by rust-crypto.
*
* Ignored if `storePrefix` is null. Otherwise, if it is set, it must be a 32-byte cryptographic key, which
* will be used to encrypt the indexeddb. See also `storePassphrase`.
*/
storeKey?: Uint8Array;
/** If defined, we will check if any data needs migrating from this store to the rust store. */
legacyCryptoStore?: CryptoStore;
@@ -94,10 +102,16 @@ export async function initRustCrypto(args: {
new RustSdkCryptoJs.Tracing(RustSdkCryptoJs.LoggerLevel.Debug).turnOn();
logger.debug("Opening Rust CryptoStore");
const storeHandle: StoreHandle = await StoreHandle.open(
args.storePrefix ?? undefined,
(args.storePrefix && args.storePassphrase) ?? undefined,
);
let storeHandle;
if (args.storePrefix) {
if (args.storeKey) {
storeHandle = await StoreHandle.openWithKey(args.storePrefix, args.storeKey);
} else {
storeHandle = await StoreHandle.open(args.storePrefix, args.storePassphrase);
}
} else {
storeHandle = await StoreHandle.open();
}
if (args.legacyCryptoStore) {
// We have a legacy crypto store, which we may need to migrate from.
+65 -18
View File
@@ -23,8 +23,11 @@ import { decryptAES, IEncryptedPayload } from "../crypto/aes";
import { IHttpOpts, MatrixHttpApi } from "../http-api";
import { requestKeyBackupVersion } from "./backup";
import { IRoomEncryption } from "../crypto/RoomList";
import { CrossSigningKeyInfo } from "../crypto-api";
import { CrossSigningKeyInfo, Curve25519AuthData } from "../crypto-api";
import { RustCrypto } from "./rust-crypto";
import { KeyBackupInfo } from "../crypto-api/keybackup";
import { sleep } from "../utils";
import { encodeBase64 } from "../base64";
/**
* Determine if any data needs migrating from the legacy store, and do so.
@@ -80,6 +83,19 @@ export async function migrateFromLegacyCrypto(args: {
}
await legacyStore.startup();
let accountPickle: string | null = null;
await legacyStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
legacyStore.getAccount(txn, (acctPickle) => {
accountPickle = acctPickle;
});
});
if (!accountPickle) {
// This store is not properly set up. Nothing to migrate.
logger.debug("Legacy crypto store is not set up (no account found). Not migrating.");
return;
}
let migrationState = await legacyStore.getMigrationState();
if (migrationState >= MigrationState.MEGOLM_SESSIONS_MIGRATED) {
@@ -105,7 +121,7 @@ export async function migrateFromLegacyCrypto(args: {
if (migrationState === MigrationState.NOT_STARTED) {
logger.info("Migrating data from legacy crypto store. Step 1: base data");
await migrateBaseData(args.http, args.userId, args.deviceId, legacyStore, pickleKey, args.storeHandle);
await migrateBaseData(args.http, args.userId, args.deviceId, legacyStore, pickleKey, args.storeHandle, logger);
migrationState = MigrationState.INITIAL_DATA_MIGRATED;
await legacyStore.setMigrationState(migrationState);
@@ -144,6 +160,7 @@ async function migrateBaseData(
legacyStore: CryptoStore,
pickleKey: Uint8Array,
storeHandle: RustSdkCryptoJs.StoreHandle,
logger: Logger,
): Promise<void> {
const migrationData = new RustSdkCryptoJs.BaseMigrationData();
migrationData.userId = new RustSdkCryptoJs.UserId(userId);
@@ -158,12 +175,41 @@ async function migrateBaseData(
const recoveryKey = await getAndDecryptCachedSecretKey(legacyStore, pickleKey, "m.megolm_backup.v1");
// If we have a backup recovery key, we need to try to figure out which backup version it is for.
// All we can really do is ask the server for the most recent version.
// All we can really do is ask the server for the most recent version and check if the cached key we have matches.
// It is possible that the backup has changed since last time his session was opened.
if (recoveryKey) {
const backupInfo = await requestKeyBackupVersion(http);
if (backupInfo) {
migrationData.backupVersion = backupInfo.version;
migrationData.backupRecoveryKey = recoveryKey;
let backupCallDone = false;
let backupInfo: KeyBackupInfo | null = null;
while (!backupCallDone) {
try {
backupInfo = await requestKeyBackupVersion(http);
backupCallDone = true;
} catch (e) {
logger.info("Failed to get backup version during migration, retrying in 2 seconds", e);
// Retry until successful, use simple constant delay
await sleep(2000);
}
}
if (backupInfo && backupInfo.algorithm == "m.megolm_backup.v1.curve25519-aes-sha2") {
// check if the recovery key matches, as the active backup version may have changed since the key was cached
// and the migration started.
try {
const decryptionKey = RustSdkCryptoJs.BackupDecryptionKey.fromBase64(recoveryKey);
const publicKey = (backupInfo.auth_data as Curve25519AuthData)?.public_key;
const isValid = decryptionKey.megolmV1PublicKey.publicKeyBase64 == publicKey;
if (isValid) {
migrationData.backupVersion = backupInfo.version;
migrationData.backupRecoveryKey = recoveryKey;
} else {
logger.debug(
"The backup key to migrate does not match the active backup version",
`Cached pub key: ${decryptionKey.megolmV1PublicKey.publicKeyBase64}`,
`Active pub key: ${publicKey}`,
);
}
} catch (e) {
logger.warn("Failed to check if the backup key to migrate matches the active backup version", e);
}
}
}
@@ -368,19 +414,20 @@ async function getAndDecryptCachedSecretKey(
legacyPickleKey: Uint8Array,
name: string,
): Promise<string | undefined> {
let encodedKey: IEncryptedPayload | null = null;
await legacyStore.doTxn("readonly", "account", (txn) => {
legacyStore.getSecretStorePrivateKey(
txn,
(k) => {
encodedKey = k as IEncryptedPayload | null;
},
name as keyof SecretStorePrivateKeys,
);
const key = await new Promise<any>((resolve) => {
legacyStore.doTxn("readonly", [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
legacyStore.getSecretStorePrivateKey(txn, resolve, name as keyof SecretStorePrivateKeys);
});
});
return encodedKey === null ? undefined : await decryptAES(encodedKey, legacyPickleKey, name);
if (key && key.ciphertext && key.iv && key.mac) {
return await decryptAES(key as IEncryptedPayload, legacyPickleKey, name);
} else if (key instanceof Uint8Array) {
// This is a legacy backward compatibility case where the key was stored in clear.
return encodeBase64(key);
} else {
return undefined;
}
}
/**
+87 -14
View File
@@ -18,6 +18,7 @@ import anotherjson from "another-json";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-wasm";
import type { IEventDecryptionResult, IMegolmSessionData } from "../@types/crypto";
import { KnownMembership } from "../@types/membership";
import type { IDeviceLists, IToDeviceEvent } from "../sync-accumulator";
import type { IEncryptedEventInfo } from "../crypto/api";
import { IContent, MatrixEvent, MatrixEventEvent } from "../models/event";
@@ -73,6 +74,7 @@ import { ISignatures } from "../@types/signed";
import { encodeBase64 } from "../base64";
import { OutgoingRequestsManager } from "./OutgoingRequestsManager";
import { PerSessionKeyBackupDownloader } from "./PerSessionKeyBackupDownloader";
import { DehydratedDeviceManager } from "./DehydratedDeviceManager";
import { VerificationMethod } from "../types";
const ALL_VERIFICATION_METHODS = [
@@ -107,9 +109,8 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
private crossSigningIdentity: CrossSigningIdentity;
private readonly backupManager: RustBackupManager;
private outgoingRequestsManager: OutgoingRequestsManager;
private readonly perSessionBackupDownloader: PerSessionKeyBackupDownloader;
private readonly dehydratedDeviceManager: DehydratedDeviceManager;
private readonly reemitter = new TypedReEmitter<RustCryptoEvents, RustCryptoEventMap>(this);
public constructor(
@@ -148,14 +149,19 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.keyClaimManager = new KeyClaimManager(olmMachine, this.outgoingRequestProcessor);
this.backupManager = new RustBackupManager(olmMachine, http, this.outgoingRequestProcessor);
this.perSessionBackupDownloader = new PerSessionKeyBackupDownloader(
this.logger,
this.olmMachine,
this.http,
this.backupManager,
);
this.dehydratedDeviceManager = new DehydratedDeviceManager(
this.logger,
olmMachine,
http,
this.outgoingRequestProcessor,
secretStorage,
);
this.eventDecryptor = new EventDecryptor(this.logger, olmMachine, this.perSessionBackupDownloader);
this.reemitter.reEmit(this.backupManager, [
@@ -212,6 +218,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
this.backupManager.stop();
this.outgoingRequestsManager.stop();
this.perSessionBackupDownloader.stop();
this.dehydratedDeviceManager.stop();
// make sure we close() the OlmMachine; doing so means that all the Rust objects will be
// cleaned up; in particular, the indexeddb connections will be closed, which means they
@@ -604,15 +611,17 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
* Implementation of {@link CryptoApi#isCrossSigningReady}
*/
public async isCrossSigningReady(): Promise<boolean> {
const { publicKeysOnDevice, privateKeysInSecretStorage, privateKeysCachedLocally } =
await this.getCrossSigningStatus();
const { privateKeysInSecretStorage, privateKeysCachedLocally } = await this.getCrossSigningStatus();
const hasKeysInCache =
Boolean(privateKeysCachedLocally.masterKey) &&
Boolean(privateKeysCachedLocally.selfSigningKey) &&
Boolean(privateKeysCachedLocally.userSigningKey);
// The cross signing is ready if the public and private keys are available
return publicKeysOnDevice && (hasKeysInCache || privateKeysInSecretStorage);
const identity = await this.getOwnIdentity();
// Cross-signing is ready if the public identity is trusted, and the private keys
// are either cached, or accessible via secret-storage.
return !!identity?.isVerified() && (hasKeysInCache || privateKeysInSecretStorage);
}
/**
@@ -856,7 +865,7 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
}
/**
* Implementation of {@link CryptoApi.getEncryptionInfoForEvent}.
* Implementation of {@link Crypto.CryptoApi.getEncryptionInfoForEvent}.
*/
public async getEncryptionInfoForEvent(event: MatrixEvent): Promise<EventEncryptionInfo | null> {
return this.eventDecryptor.getEncryptionInfoForEvent(event);
@@ -1208,8 +1217,29 @@ export class RustCrypto extends TypedEventEmitter<RustCryptoEvents, RustCryptoEv
/**
* Implementation of {@link CryptoBackend#importBackedUpRoomKeys}.
*/
public async importBackedUpRoomKeys(keys: IMegolmSessionData[], opts?: ImportRoomKeysOpts): Promise<void> {
return await this.backupManager.importBackedUpRoomKeys(keys, opts);
public async importBackedUpRoomKeys(
keys: IMegolmSessionData[],
backupVersion: string,
opts?: ImportRoomKeysOpts,
): Promise<void> {
return await this.backupManager.importBackedUpRoomKeys(keys, backupVersion, opts);
}
/**
* Implementation of {@link CryptoBackend#isDehydrationSupported}.
*/
public async isDehydrationSupported(): Promise<boolean> {
return await this.dehydratedDeviceManager.isSupported();
}
/**
* Implementation of {@link CryptoBackend#startDehydration}.
*/
public async startDehydration(createNewKey?: boolean): Promise<void> {
if (!(await this.isCrossSigningReady()) || !(await this.isSecretStorageReady())) {
throw new Error("Device dehydration requires cross-signing and secret storage to be set up");
}
return await this.dehydratedDeviceManager.start(createNewKey);
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -1684,7 +1714,7 @@ class EventDecryptor {
};
} catch (err) {
if (err instanceof RustSdkCryptoJs.MegolmDecryptionError) {
this.onMegolmDecryptionError(event, err);
this.onMegolmDecryptionError(event, err, await this.perSessionBackupDownloader.getServerBackupInfo());
} else {
throw new DecryptionError(DecryptionFailureCode.UNKNOWN_ERROR, "Unknown error");
}
@@ -1695,9 +1725,19 @@ class EventDecryptor {
* Handle a `MegolmDecryptionError` returned by the rust SDK.
*
* Fires off a request to the `perSessionBackupDownloader`, if appropriate, and then throws a `DecryptionError`.
*
* @param event - The event which could not be decrypted.
* @param err - The error from the Rust SDK.
* @param serverBackupInfo - Details about the current backup from the server. `null` if there is no backup.
* `undefined` if our attempt to check failed.
*/
private onMegolmDecryptionError(event: MatrixEvent, err: RustSdkCryptoJs.MegolmDecryptionError): never {
private onMegolmDecryptionError(
event: MatrixEvent,
err: RustSdkCryptoJs.MegolmDecryptionError,
serverBackupInfo: KeyBackupInfo | null | undefined,
): never {
const content = event.getWireContent();
const errorDetails = { session: content.sender_key + "|" + content.session_id };
// If the error looks like it might be recoverable from backup, queue up a request to try that.
if (
@@ -1705,9 +1745,42 @@ class EventDecryptor {
err.code === RustSdkCryptoJs.DecryptionErrorCode.UnknownMessageIndex
) {
this.perSessionBackupDownloader.onDecryptionKeyMissingError(event.getRoomId()!, content.session_id!);
// If the server is telling us our membership at the time the event
// was sent, and it isn't "join", we use a different error code.
const membership = event.getMembershipAtEvent();
if (membership && membership !== KnownMembership.Join && membership !== KnownMembership.Invite) {
throw new DecryptionError(
DecryptionFailureCode.HISTORICAL_MESSAGE_USER_NOT_JOINED,
"This message was sent when we were not a member of the room.",
errorDetails,
);
}
// If the event was sent before this device was created, we use some different error codes.
if (event.getTs() <= this.olmMachine.deviceCreationTimeMs) {
if (serverBackupInfo === null) {
throw new DecryptionError(
DecryptionFailureCode.HISTORICAL_MESSAGE_NO_KEY_BACKUP,
"This message was sent before this device logged in, and there is no key backup on the server.",
errorDetails,
);
} else if (!this.perSessionBackupDownloader.isKeyBackupDownloadConfigured()) {
throw new DecryptionError(
DecryptionFailureCode.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
"This message was sent before this device logged in, and key backup is not working.",
errorDetails,
);
} else {
throw new DecryptionError(
DecryptionFailureCode.HISTORICAL_MESSAGE_WORKING_BACKUP,
"This message was sent before this device logged in. Key backup is working, but we still do not (yet) have the key.",
errorDetails,
);
}
}
}
const errorDetails = { session: content.sender_key + "|" + content.session_id };
switch (err.code) {
case RustSdkCryptoJs.DecryptionErrorCode.MissingRoomKey:
throw new DecryptionError(
+12 -4
View File
@@ -567,8 +567,10 @@ export class RustQrCodeVerifier extends BaseRustVerifer<RustSdkCryptoJs.Qr> impl
// application to prompt the user to confirm their side.
if (this.callbacks === null && this.inner.hasBeenScanned()) {
this.callbacks = {
confirm: () => this.confirmScanning(),
cancel: () => this.cancel(),
confirm: (): void => {
this.confirmScanning();
},
cancel: (): void => this.cancel(),
};
}
@@ -708,10 +710,16 @@ export class RustSASVerifier extends BaseRustVerifer<RustSdkCryptoJs.Sas> implem
}
},
mismatch: (): void => {
throw new Error("impl");
const request = this.inner.cancelWithCode("m.mismatched_sas");
if (request) {
this.outgoingRequestProcessor.makeOutgoingRequest(request);
}
},
cancel: (): void => {
throw new Error("impl");
const request = this.inner.cancelWithCode("m.user");
if (request) {
this.outgoingRequestProcessor.makeOutgoingRequest(request);
}
},
};
this.emit(VerifierEvent.ShowSas, this.callbacks);
+1 -3
View File
@@ -58,8 +58,6 @@ export interface IRoomEvent extends IMinimalEvent {
event_id: string;
sender: string;
origin_server_ts: number;
/** @deprecated - legacy field */
age?: number;
}
export interface IStateEvent extends IRoomEvent {
@@ -498,7 +496,7 @@ export class SyncAccumulator {
if (transformedEvent.unsigned !== undefined) {
transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned);
}
const age = e.unsigned ? e.unsigned.age : e.age;
const age = e.unsigned?.age;
if (age !== undefined) transformedEvent._localTs = Date.now() - age;
} else {
transformedEvent = e;
+1 -6
View File
@@ -748,12 +748,7 @@ export function safeSet<O extends Record<any, any>, K extends keyof O>(obj: O, p
}
export function noUnsafeEventProps(event: Partial<IEvent>): boolean {
return !(
unsafeProp(event.room_id) ||
unsafeProp(event.sender) ||
unsafeProp(event.user_id) ||
unsafeProp(event.event_id)
);
return !(unsafeProp(event.room_id) || unsafeProp(event.sender) || unsafeProp(event.event_id));
}
export class MapWithDefault<K, V> extends Map<K, V> {
+875 -536
View File
File diff suppressed because it is too large Load Diff